diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3915059..a8641e1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,71 +1,73 @@ 2, 'class' => array( 'Diagram' => 'storage/Diagram.php', 'DiagramApplication' => 'application/DiagramApplication.php', 'DiagramCelerityResources' => 'celerity/DiagramCelerityResources.php', 'DiagramContentTransaction' => 'xaction/DiagramContentTransaction.php', 'DiagramController' => 'controller/DiagramController.php', 'DiagramDAO' => 'storage/DiagramDAO.php', + 'DiagramDataController' => 'controller/DiagramDataController.php', 'DiagramPHIDType' => 'phid/DiagramPHIDType.php', 'DiagramPatchList' => 'storage/patch/DiagramPatchList.php', 'DiagramReplyHandler' => 'mail/DiagramReplyHandler.php', 'DiagramSchemaSpec' => 'storage/DiagramSchemaSpec.php', 'DiagramSearchConduitAPIMethod' => 'conduit/DiagramSearchConduitAPIMethod.php', 'DiagramTransaction' => 'storage/DiagramTransaction.php', 'DiagramTransactionEditor' => 'editor/DiagramTransactionEditor.php', 'DiagramTransactionType' => 'xaction/DiagramTransactionType.php', 'DiagramUploadConduitAPIMethod' => 'conduit/DiagramUploadConduitAPIMethod.php', 'DiagramVersion' => 'storage/DiagramVersion.php', 'PhabricatorDiagramQuery' => 'query/PhabricatorDiagramQuery.php', 'PhabricatorDiagramTransactionQuery' => 'query/PhabricatorDiagramTransactionQuery.php', 'PhabricatorDiagramVersionQuery' => 'query/PhabricatorDiagramVersionQuery.php', 'PhabricatorRemarkupDiagramRule' => 'remarkup/PhabricatorRemarkupDiagramRule.php', 'PlainHtmlWebpageResponse' => 'response/PlainHtmlWebpageResponse.php', ), 'function' => array(), 'xmap' => array( 'Diagram' => array( 'DiagramDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', 'PhabricatorPolicyInterface', 'PhabricatorSubscribableInterface', ), 'DiagramApplication' => 'PhabricatorApplication', 'DiagramCelerityResources' => 'CelerityResourcesOnDisk', 'DiagramContentTransaction' => 'DiagramTransactionType', 'DiagramController' => 'PhabricatorController', 'DiagramDAO' => 'PhabricatorLiskDAO', + 'DiagramDataController' => 'PhabricatorController', 'DiagramPHIDType' => 'PhabricatorPHIDType', 'DiagramPatchList' => 'PhabricatorSQLPatchList', 'DiagramReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'DiagramSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DiagramSearchConduitAPIMethod' => 'ConduitAPIMethod', 'DiagramTransaction' => 'PhabricatorModularTransaction', 'DiagramTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'DiagramTransactionType' => 'PhabricatorModularTransactionType', 'DiagramUploadConduitAPIMethod' => 'ConduitAPIMethod', 'DiagramVersion' => array( 'DiagramDAO', 'PhabricatorDestructibleInterface', 'PhabricatorPolicyInterface', ), 'PhabricatorDiagramQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDiagramTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDiagramVersionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorRemarkupDiagramRule' => array( 'PhabricatorObjectRemarkupRule', 'RemarkupSyntaxDocumentationProvider', ), 'PlainHtmlWebpageResponse' => 'AphrontHTMLResponse', ), )); diff --git a/src/application/DiagramApplication.php b/src/application/DiagramApplication.php index 28ca3fb..a4c25ea 100644 --- a/src/application/DiagramApplication.php +++ b/src/application/DiagramApplication.php @@ -1,81 +1,81 @@ array( // url to image data "data/(?PPHID-DGVN-[a-z0-9]{20})" . "(?:/(?P[^/]+))?" . "(?:/|\?|$).*" - => "DiagramController", + => "DiagramDataController", // version info requests "version/DIAG(?P(\d+))" . "/(?P(\d+))" . "(?:/|\?|$).*" => "DiagramController", // draw.io iframe urls "(?:(?PDIAG(\d+))/(?P\d+)/?)" . "(?Piframe)" . "(?:/|\?|$).*" => "DiagramController", // draw.io iframe urls "(?:(?PDIAG(\d+))/?)?" . "(?Piframe)" . "(?:/|\?|$).*" => "DiagramController", // url with diagram id and version in it "(?PDIAG(\d+))" . "/(?P\d+)" . "(?:/|\?|$).*" => "DiagramController", // url with diagram id in it "(?PDIAG(\d+))" . "(?:/|\?|$).*" => "DiagramController", // url for validating subscription "subscribed/request/" . "(?P[^/]+)/" => 'DiagramController', // other urls ".*" => "DiagramController", ) ); } } diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index cacf8d0..41f9d79 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,524 +1,511 @@ 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, ($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\//", "drawio/src/main/webapp/", $file); } else { $file = preg_replace("/^\/diagram\/($diagramid\/?)?iframe\//", "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 ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { $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) { $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( $diagram_id); } else { $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( $diagram_id, $version); } if ($diagramVersion) { $data = $diagramVersion->getData(); $base64_data = base64_encode($data); $diagram = id(new Diagram())->loadByID($diagram_id); return $this->showApplication( $request, 'DIAG' . $diagram_id, $diagram->getPHID(), $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()) ->setDisableContentSecurityPolicy(true) ->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)); $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setLastModified(time()); $response->setCanCDN(true); } 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. */ 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)) { 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 doesn't contain mxfile key, return false return false; } } /** * Processes HTTP POST calls from Diagram application, like 'Save' action */ private function handleHttpPostCall(AphrontRequest $request) { $subscriptionphid = $request->getURIData('subscriptionphid'); if (isset($subscriptionphid) && !empty(trim($subscriptionphid))) { // get list of subscriber for specified diagram phid $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $subscriptionphid); // verify if viewer is subscriber $viewer = $request->getViewer(); if ($viewer == null) { $isSubscribed = false; } else { $isSubscribed = in_array($viewer->getPHID(),$subscribers); } // reply back $response = id(new AphrontJSONResponse())->setAddJSONShield(false) ->setContent(array( "subscribed" => $isSubscribed )); return $response; } $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_data = base64_encode($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); $diagram->publishNewVersion($request, $diagram->getDiagramID()); $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 = '', string $diagramPHID = '', string $diagramVersion = '', string $base64_data = '' ) { $applicationUrl = "/" . explode("/", $request->getPath())[1]; $behaviorConfig = array(); $behaviorConfig['initParams'] = array( $diagramName, $diagramPHID, $diagramVersion, $base64_data ); $behaviorConfig['toolbarCss'] = celerity_get_resource_uri('/iframe-toolbtn.css', 'diagram-resources'); $behaviorConfig['toolbarJs'] = celerity_get_resource_uri('/iframe-toolbtn.js', 'diagram-resources'); require_celerity_resource('javelin-behavior'); Javelin::initBehavior('diagram-extension', $behaviorConfig, 'diagram-resources'); $content = phutil_tag( 'div', array(), array( phutil_tag( 'div', array( 'id' => 'mainScreen', )), phutil_tag( 'div', array( 'class' => 'crumbs', 'style' => 'top:48px;' . 'margin-left: 4px;' . 'position: fixed;' . 'font-weight: bold;' ), array( phutil_tag( 'a', array( 'href' => $applicationUrl ), array( phutil_tag( 'span', array( 'class' => 'phui-font-fa fa-sitemap', 'style' => 'padding-right:5px;' )) )), phutil_tag( 'a', array( 'href' => $applicationUrl ), '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;'), ''), )) )) )); $view = id(new PhabricatorStandardPageView()) ->setRequest($request) ->setController($this) ->setDeviceReady(true) ->setTitle("Diagrams") ->appendChild($content); $response = id(new AphrontWebpageResponse()) ->setContent($view->render()); return $response; } } diff --git a/src/controller/DiagramDataController.php b/src/controller/DiagramDataController.php new file mode 100644 index 0000000..2228571 --- /dev/null +++ b/src/controller/DiagramDataController.php @@ -0,0 +1,66 @@ +setRequest($request); + + // detetermine if GET or POST HTTP call + if (!$request->isHTTPPost()) { + // determine type of URL by means of DiagramApplication route parameters + $diagramphid = $request->getURIData('diagramphid'); + $version = $request->getURIData('version'); + + 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; + } + } + } + + // Invalid URL + $response = id(new Aphront404Response()); + return $response; + } +}