diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index ac466e8..f068dcd 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,509 +1,546 @@ setRequest($request); // detetermine if GET or POST HTTP call if ($request->isHTTPPost()) { // process POST calls (like save action) return $this->handleHttpPostCall($request); } // determine type of URL by means of DiagramApplication route parameters $diagramphid = $request->getURIData('diagramphid'); $diagramid = $request->getURIData('diagramid'); $versioneddiagramid = $request->getURIData('versioneddiagramid'); $version = $request->getURIData('version'); $route = $request->getURIData('route'); $versioninfoDiagramID = $request->getURIData('versioninfodiagram'); $versioninfoPage = $request->getURIData('versioninfopage'); if (isset($diagramphid) && !empty(trim($diagramphid))) { // return PNG image data $diagram = id(new DiagramVersion())->loadByDiagramPHID($diagramphid); if ($diagram !== null) { $response = new AphrontFileResponse(); $response->setMimeType('image/png'); $response->setContent($diagram->getData()); return $response; } } if (isset($versioninfoDiagramID) && !empty(trim($versioninfoDiagramID))) { // return diagram version info if (!isset($versioninfoPage) || empty(trim($versioninfoPage))) { // versioninfoPage was dismissed -> initialize to 1 $versioninfoPage = "1"; } $diagramVersions = id(new DiagramVersion())->loadByDiagramID($versioninfoDiagramID); if ($diagramVersions !== null) { $result = []; $viewer = $request->getViewer(); // determine total count of versions $totalcount = count($diagramVersions); // filter out some of the versions we want to show $pageSize = 10; - $diagramVersions = array_slice($diagramVersions, + $diagramVersions = array_slice($diagramVersions, ($versioninfoPage - 1) * $pageSize, $pageSize ); // calculate number of pages $totalpages = ceil($totalcount / $pageSize); // create menu-items foreach ($diagramVersions as $diagramVersion) { $author = $diagramVersion->getAuthorPHID(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array( $author )) ->executeOne(); $dateModified = $diagramVersion->getDateModified(); $result[] = array( "id" => $diagramVersion->getVersion(), "datetime" => phabricator_datetime($dateModified, $viewer), "author" => $user->getUsername() ); } // reply back $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "data" => $result, "pagecount" => $totalpages, "nopager" => $totalcount <= $pageSize )); return $response; } else { // version info requested for inexistant diagram $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "data" => array(), "pagecount" => 0 )); return $response; } } $root = ''; $file = rtrim($request->getPath(), '/'); $root = dirname(phutil_get_library_root('diagram')); // determine from which application area the file should be loaded: // 1) Phorge extension source // or 2) drawio source if ($route == 'iframe') { // load from drawio source if ($file == '/diagram/iframe') $file .= '/index.html'; if ($versioneddiagramid != null && $version != null) { $file = preg_replace("/^\/diagram\/" . $versioneddiagramid . "\/" . $version ."iframe\//", "data/drawio/src/main/webapp/", $file); } else { $file = preg_replace("/^\/diagram\/(" . $diagramid . "\/?)?iframe\//", "data/drawio/src/main/webapp/", $file); } } else { // load from extension source if (rtrim($file, '/') == '/diagram') { return $this->showApplication($request); } if ($versioneddiagramid !== null && $version !== null) { - $file = preg_replace('/^\/diagram\/' . $versioneddiagramid . '\/'. $version . '\/?/', - 'data/', + $file = preg_replace('/^\/diagram\/' . $versioneddiagramid . '\/'. $version . '\/?/', + 'data/', $file ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { - $file = preg_replace('/^\/diagram\/(' . $diagramid . '\/)?/', - 'data/', + $file = preg_replace('/^\/diagram\/(' . $diagramid . '\/)?/', + 'data/', $file ); } } // determine full path $path = $root . '/' . $file; if (file_exists($path) == false || is_readable($path) == false) { if (preg_match('/^data\/DIAG(\d+)$/', $file, $matches)) { $diagram_id = (int) $matches[1]; if ($version === null) { $diagram = id(new DiagramVersion())->loadLatestByDiagramID($diagram_id); } else { $diagram = id(new DiagramVersion())->loadByVersionedDiagramID($diagram_id, $version); } if ($diagram) { $data = $diagram->getData(); $base64_data = base64_encode($data); return $this->showApplication( $request, 'DIAG' . $diagram_id, - $version ?? "", + $version ?? "", $base64_data ); } } // Invalid URL $response = id(new Aphront404Response()); return $response; } else { // process Iframe content switch (pathinfo($file, PATHINFO_EXTENSION)) { case 'html': $response = id(new PlainHtmlWebpageResponse()) ->setFrameable(true) ->setContent(file_get_contents($path)); break; case 'js': $response = new AphrontFileResponse(); $response->setMimeType('application/javascript'); break; case 'css': $response = new AphrontFileResponse(); $response->setMimeType('text/css'); break; case 'txt': $response = new AphrontFileResponse(); $response->setMimeType('text/plain'); break; case 'png': $response = new AphrontFileResponse(); $response->setMimeType('image/png'); break; case 'gif': $response = new AphrontFileResponse(); $response->setMimeType('image/gif'); break; case 'jpg': case 'jpeg': $response = new AphrontFileResponse(); $response->setMimeType('image/jpeg'); break; default: $response = new AphrontFileResponse(); $response->setMimeType('application/octet-stream'); break; } try { $response->setContent(file_get_contents($path)); } catch (Exception $e) { $response->setContent($route); } return $response; } } /** * Compares the draw.io tEXt metadata from 2 PNG base64 strings. * The content looks like this: * * * * * ... * * * * * - * + * * The modified and etag attributes of mxfile will always be different. * They are cut out before the 2 strings are compared. */ - private function equalPngMetaData($base64_1, $base64_2) { + public static function equalPngMetaData($base64_1, $base64_2) { $base64 = array($base64_1, $base64_2); $textData = array(); for ($i = 0; $i < 2; $i++) { $data = base64_decode($base64[$i]); $fp = fopen('data://text/plain;base64,' . base64_encode($data), 'rb'); $sig = fread($fp, 8); if ($sig != "\x89PNG\x0d\x0a\x1a\x0a") { fclose($fp); return false; } $textData[$i] = array(); while (!feof($fp)) { - $chunk = unpack('Nlength/a4type', fread($fp, 8)); + try { + $chunk = unpack('Nlength/a4type', fread($fp, 8)); + } catch (Exception $e) { + // invalid base64 data + return false; + } if ($chunk['type'] == 'IEND') break; if ($chunk['type'] == 'tEXt') { list($key, $val) = explode("\0", fread($fp, $chunk['length'])); if ($key == 'mxfile') { // Decode the URL-encoded XML data $decodedVal = urldecode($val); // Load the XML and remove the modified and etag attributes $xml = simplexml_load_string($decodedVal); unset($xml->attributes()->modified); unset($xml->attributes()->etag); // Save the modified XML as the value $val = $xml->asXML(); } $textData[$i][$key] = $val; fseek($fp, 4, SEEK_CUR); } else { fseek($fp, $chunk['length'] + 4, SEEK_CUR); } } fclose($fp); } if (isset($textData[0]['mxfile']) && isset($textData[1]['mxfile'])) { // Both arrays contain the mxfile key, compare their values return $textData[0]['mxfile'] == $textData[1]['mxfile']; } else { // At least one of the arrays does not contain the mxfile key, return false return false; } } /** * Processes HTTP POST calls from Diagram application, like 'Save' action */ private function handleHttpPostCall(AphrontRequest $request) { $base64_data = $request->getStr("data"); $diagram_id = $request->getStr("diagramID"); // cut off "data:image/png;base64," $base64_data = substr($base64_data, strpos($base64_data, ',') + 1); if ($diagram_id != "") { // check if we are trying to save the same data as the current data $diagram = id(new DiagramVersion())->loadLatestByDiagramID($diagram_id); if ($diagram !== null) { $data = $diagram->getData(); - $old_base64_data = base64_encode($data); + $old_data = base64_encode($data); - if ($this->equalPngMetaData($base64_data, $old_base64_data)) { + if (DiagramController::equalPngMetaData($base64_data, $old_data)) { // data hasn't been modified // => do not create new version $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "OK", "DiagramID" => $diagram->getDiagramID(), "Version" => $diagram->getVersion() )); return $response; } } } - + // Set the options for the new file $options = array( 'name' => 'diagram.png', 'viewPolicy' => PhabricatorPolicies::POLICY_USER, 'mime-type' => 'image/png', 'actor' => $this->getViewer(), 'diagramID' => $diagram_id ); try { // Create the new file object $diagram = DiagramVersion::newFromFileData($base64_data, $options); $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "OK", "DiagramID" => $diagram->getDiagramID(), "Version" => $diagram->getVersion() )); return $response; } catch (Exception $e) { $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "Status" => "ERROR", "Error" => $e->getMessage(), )); return $response; } } + /** + * Verifies if the given base64 data is draw.io compatible + */ + public static function isDrawioPngBase64($base64) { + $data = base64_decode($base64); + $fp = fopen('data://text/plain;base64,' . base64_encode($data), 'rb'); + $sig = fread($fp, 8); + if ($sig != "\x89PNG\x0d\x0a\x1a\x0a") { + fclose($fp); + return false; + } + while (!feof($fp)) { + try { + $chunk = unpack('Nlength/a4type', fread($fp, 8)); + } catch (Exception $e) { + // invalid base64 data + return false; + } + if ($chunk['type'] == 'IEND') break; + if ($chunk['type'] == 'tEXt') { + list($key, $val) = explode("\0", fread($fp, $chunk['length'])); + if ($key == 'mxfile') { + fclose($fp); + return true; + } + fseek($fp, 4, SEEK_CUR); + } else { + fseek($fp, $chunk['length'] + 4, SEEK_CUR); + } + } + fclose($fp); + return false; + } + /** * Shows the draw.io application integrated in Phorge's layout */ private function showApplication( AphrontRequest $request, string $diagramName = null, string $diagramVersion = null, string $diagramBase64 = null ) { $content = phutil_tag( 'div', array(), array( phutil_tag( 'div', array( 'id' => 'mainScreen', )), phutil_tag('div', array(), array( phutil_tag( 'img', array( 'class' => 'drawio', )), phutil_tag('div', array( 'id' => 'loadingtext', 'class' => 'geBlock', 'style' => 'margin-top:80px;' . 'text-align:center;' . 'min-width:50%;' . 'height:100vh;', ), array( phutil_tag('h1', array(), 'Flowchart Maker and Online Diagram Software' ), phutil_tag('p', array( 'style' => 'width: 800px;' . 'position: sticky;' . 'left: calc(50% - 400px);', ), 'draw.io is free online diagram software. ' . 'You can use it as a flowchart maker, network diagram ' . 'software, to create UML online, as an ER diagram tool, ' . 'to design database schema, to build BPMN online, as a ' . 'circuit diagram maker, and more. draw.io can import ' . '.vsdx, Gliffy™ and Lucidchart™ files . ' ), phutil_tag( 'h2', array( 'id' => 'geStatus', ), 'Loading...' ), phutil_tag( 'div', array( 'id' => 'spinnerLoading', )), phutil_tag( 'script', array(), 'var spinnerOpts = {' . 'hwaccel: false,' . 'length: 24,' . 'radius: 12,' . 'shadow: false,' . 'speed: 1.5,' . 'trail: 60,' . 'width: 8};' ) )) )), phutil_tag( 'script', array( 'src' => 'phorge_extension.js' ), '' ), phutil_tag( 'script', array(), phutil_safe_html('loadJsExtension("' . $diagramName . '", "' . $diagramVersion . '", "' . $diagramBase64 . '");') ), phutil_tag( 'div', array( 'class' => 'crumbs', 'style' => 'top:48px;' . 'margin-left: 4px;' . 'position: fixed;' . 'font-weight: bold;' ), array( phutil_tag( 'a', array( 'href' => '.' ), array( phutil_tag( 'span', array( 'class' => 'phui-font-fa fa-sitemap', 'style' => 'padding-right:5px;' )) )), phutil_tag( 'a', array( 'href' => '.' ), 'Diagram' ), phutil_tag( 'span', array( 'class' => 'diagramName', 'style' => 'display:none' ), array( phutil_tag( 'span', array( 'style' => 'margin: 5px;' . 'opacity: .5;' ), '>' ), phutil_tag( 'a', array(), '' ), phutil_tag( 'span', array( 'class' => 'version', 'style' => 'margin-left: 8px;' - . 'color: #999;' - ), - '', - ), + . 'color: #999;'), + ''), )) )) )); $view = id(new PhabricatorStandardPageView()) ->setRequest($request) ->setController($this) ->setDeviceReady(true) ->setTitle("Diagrams") ->appendChild($content); $response = id(new AphrontWebpageResponse()) ->setContent($view->render()); return $response; } -} \ No newline at end of file +} diff --git a/src/query/PhabricatorDiagramQuery.php b/src/query/PhabricatorDiagramQuery.php index 8302219..c3627c3 100644 --- a/src/query/PhabricatorDiagramQuery.php +++ b/src/query/PhabricatorDiagramQuery.php @@ -1,73 +1,108 @@ diagramIDs = $diagram_ids; return $this; } + public function withIDs(array $diagram_ids) { + return $this->withDiagramIDs($diagram_ids); + } + + public function withModifiedAfter($datetime) { + $this->modifiedAfter = $datetime; + return $this; + } + + public function withModifiedBefore($datetime) { + $this->modifiedBefore = $datetime; + return $this; + } + protected function loadPage() { $table = new DiagramVersion(); $conn_r = $table->establishConnection('r'); // we return a DiagramVersion object which has a different id // than the one we mention in the Remarkup code. // E.g. {DIAG1} may point to the 2nd version of the object. // Diagram's id is 1, but DiagramVersion's id is 2. // Because of this we abuse the id in the resultset a little bit $data = queryfx_all( $conn_r, - "SELECT result.diagramID AS id, /* abuse */ - result.diagramID, - result.version, - result.phid, - result.authorPHID, - result.dateCreated, - result.dateModified, - result.byteSize, - result.data, - result.viewPolicy, - result.editPolicy + 'SELECT * FROM ( - SELECT data.* - FROM %T data - INNER JOIN ( - SELECT MAX(id) AS id, - diagramid - FROM %T - GROUP BY diagramid HAVING MAX(id) - ) filter - ON data.id = filter.id - ) result %Q %Q %Q", + SELECT result.diagramID AS id, /* abuse */ + result.diagramID, + result.version, + result.phid, + result.authorPHID, + result.dateCreated, + result.dateModified, + result.byteSize, + result.data, + result.viewPolicy, + result.editPolicy + FROM ( + SELECT data.* + FROM %T data + INNER JOIN ( + SELECT MAX(id) AS id, + diagramid + FROM %T + GROUP BY diagramid HAVING MAX(id) + ) filter + ON data.id = filter.id + ) result + ) r %Q %Q %Q', $table->getTableName(), $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); + $where[] = $this->buildPagingClause($conn_r); + if ($this->diagramIDs !== null) { $where[] = qsprintf( $conn_r, 'diagramID IN (%Ld)', $this->diagramIDs); } + if ($this->modifiedAfter !== null) { + $where[] = qsprintf( + $conn_r, + 'dateModified >= %d', + $this->modifiedAfter); + } + + if ($this->modifiedBefore !== null) { + $where[] = qsprintf( + $conn_r, + 'dateModified <= %d', + $this->modifiedBefore); + } + return $this->formatWhereClause($conn_r, $where); } public function getQueryApplicationClass() { return DiagramApplication::class; } } diff --git a/src/storage/DiagramVersion.php b/src/storage/DiagramVersion.php index be98448..e896141 100644 --- a/src/storage/DiagramVersion.php +++ b/src/storage/DiagramVersion.php @@ -1,342 +1,371 @@ data === null) { + return null; + } + return base64_encode($this->data); + } + + /** + * returns the URL which links to the diagram PNG data + */ + public function getDataURI() { + return PhabricatorEnv::getCDNURI( + '/diagram/data/' + .$this->getPHID()); + } + /** * Return an array of capabilities that this object type supports. * See PhabricatorPolicyCapability for a list of available capabilities. - * + * * Interface: PhabricatorPolicyInterface */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } /** * Return the policy for the given capability. - * + * * Interface: PhabricatorPolicyInterface */ public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; default: return PhabricatorPolicies::POLICY_NOONE; } } /** * Return the URL which links to the diagram PNG image */ public function getViewURI() { if (!$this->getPHID()) { throw new Exception( pht('You must save a diagram before you can generate a view URI.') ); } $uri = '/diagram/data/' - . $this->getPHID(); + .$this->getPHID(); return $uri; } /** * Configures application-wide storage settings. * This creates a mapping of the corresponding database table. */ public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'diagramID' => 'uint32', 'version' => 'uint32', 'authorPHID' => 'phid', 'byteSize' => 'uint64', 'data' => 'bytes', 'viewPolicy' => 'policy', 'editPolicy' => 'policy', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'key_diagramID_version' => array( 'columns' => array('diagramID', 'version'), 'unique' => true, ), 'key_authorPHID' => array( 'columns' => array('authorPHID'), ), ), ) + array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Return the name of the database table that is represented by this class */ public function getTableName() { return 'diagram_version'; } /** * Return a string that uniquely identifies the PHID type for this object * type. This is used by the PHID system to generate and manage PHIDs for * this object type. */ public function getPHIDType() { return 'DGVN'; } /** * Return true if the given user has the given capability automatically, * without needing to check the object's policy. For example, you might * return true here if the user is an administrator or if they own the * object. - * + * * Interface: PhabricatorPolicyInterface */ public function hasAutomaticCapability( $capability, - PhabricatorUser $viewer - ) { + PhabricatorUser $viewer) { return false; } + /** + * Creates and initializes a new DiagramVersion object + */ public static function initializeNewDiagram(PhabricatorUser $actor) { - return id(new DiagramVersion()) + return id(new self()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setVersion(1) ->setDateCreated(time()) ->setDateModified(time()); } + /** + * Creates a new DiagramVersion object and loads the given base64 data in it + */ public static function newFromFileData( $base64_data, - array $params = array() - ) { + array $params = array()) { $actor = idx($params, 'actor'); if (!$actor) { throw new Exception(pht('Missing required actor for new file data.')); } $diagramID = idx($params, 'diagramID'); if (!is_numeric($diagramID)) { $diagramID = null; } $data = base64_decode($base64_data); $diagram = self::initializeNewDiagram($actor); $diagram->setByteSize(strlen($data)); $diagram->setData($data); $diagram->setDiagramID($diagramID); $diagram->save(); return $diagram; } - public function attachProjectPHIDs(array $phids) { - // Attach an array of project PHIDs to this object. This is used by the - // project system to manage project membership and visibility for this - // object. - } - + /** + * Permanently destroy this object. This is used by the destructible + * interface to allow administrators to permanently delete objects from + * the system. + */ public function destroyObjectPermanently( - PhabricatorDestructionEngine $engine - ) { - // Permanently destroy this object. This is used by the destructible - // interface to allow administrators to permanently delete objects from - // the system. + PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } + /** + * Returns an ID which can be used for a newly created Diagram object + */ public function generateDiagramID() { $conn_r = $this->establishConnection('r'); $table_name = $this->getTableName(); $max_diagram_id = queryfx_one( $conn_r, 'SELECT MAX(diagramID) max_diagram_id FROM %T', - $table_name - )['max_diagram_id']; - return (int) $max_diagram_id + 1; + $table_name)['max_diagram_id']; + return (int)$max_diagram_id + 1; } + /** + * Returns all DiagramVersion objects for a given diagram + */ public function loadByDiagramID($diagramID) { if (is_object($diagramID)) { - $diagramID = (string) $diagramID; + $diagramID = (string)$diagramID; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } return $this->loadAllWhere( 'diagramID = %d ORDER BY version DESC', - $diagramID - ); + $diagramID); } + /** + * Returns the latest version of the given diagram + */ public function loadLatestByDiagramID($diagramID) { if (is_object($diagramID)) { - $diagramID = (string) $diagramID; + $diagramID = (string)$diagramID; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } return $this->loadOneWhere( 'diagramID = %d ORDER BY version DESC LIMIT 1', - $diagramID - ); + $diagramID); } - public function loadByDiagramPHID($diagramPHID) { - if (is_object($diagramPHID)) { - $diagramPHID = (string) $diagramPHID; + /** + * Returns a specific DiagramVersion object + */ + public function loadByDiagramPHID($diagramVersionPHID) { + if (is_object($diagramVersionPHID)) { + $diagramVersionPHID = (string)$diagramVersionPHID; } return $this->loadOneWhere( 'phid = %s ORDER BY version DESC LIMIT 1', - $diagramPHID - ); + $diagramVersionPHID); } + /** + * Returns a specific DiagramVersion object for a given diagram and + * version number + */ public function loadByVersionedDiagramID($diagramID, $version) { if (is_object($diagramID)) { - $diagramID = (string) $diagramID; + $diagramID = (string)$diagramID; } if (is_object($version)) { - $version = (string) $version; + $version = (string)$version; } if (!$diagramID || (!is_int($diagramID) && !ctype_digit($diagramID))) { return null; } if (!$version || (!is_int($version) && !ctype_digit($version))) { return null; } return $this->loadOneWhere( 'diagramID = %d AND version = %d', $diagramID, - $version - ); + $version); } + /** + * Stores a new diagram (version) + */ public function save() { // Load the last record with the same PHID. $last_record = null; if ($this->getDiagramID() !== null) { $last_record = id(new PhabricatorDiagramQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDiagramIDs(array($this->getDiagramID())) ->setLimit(1) ->executeOne(); } if ($last_record === null) { // If there is no last record, this is a new diagram object. $this->setVersion(1); $newDiagram = new Diagram(); $newDiagram->createNewDiagram(); $this->setDiagramID($newDiagram->getID()); } else { // If there is a last record, this is a new version of an existing // diagram object. $this->setVersion($last_record->getVersion() + 1); $this->setDateCreated($last_record->getDateCreated()); } // Check if a row with the same PHID and version already exists $existing_record = null; if ($this->getPHID() !== null && $this->getVersion() !== null) { $existing_record = id(new PhabricatorDiagramQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->withWhere( array( array('version', '=', $this->getVersion()), - ) - ) + )) ->setLimit(1) ->executeOne(); } if ($existing_record === null) { // If there is no existing record, create a new row in the table. $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $this->phid = $this->generatePHID(); if ($this->diagramID === null) { $this->diagramID = $this->generateDiagramID(); } $record = array( 'phid' => $this->getPHID(), 'diagramID' => $this->getDiagramID(), 'version' => $this->getVersion(), 'authorPHID' => $this->getAuthorPHID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'byteSize' => $this->getByteSize(), 'viewPolicy' => $this->getViewPolicy(), 'editPolicy' => $this->getEditPolicy(), 'data' => $this->getData(), ); if ($this->getID() !== null) { // If the ID property is set, include it in the data to insert. $record['id'] = $this->getID(); } queryfx( $conn_w, 'INSERT INTO %T (%Q) VALUES (%Ls, %B)', $table_name, implode(', ', array_keys($record)), array_values(array_slice($record, 0, -1)), - end($record) - ); + end($record)); $this->id = $conn_w->getInsertID(); } else { // If there is an existing record, throw an exception. throw new Exception( pht('A diagram with PHID "%s" and version "%s" already exists.', $this->getPHID(), - $this->getVersion() - ) + $this->getVersion()) ); } return $this; } -} \ No newline at end of file +}