diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index 4508f02..65220bf 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,947 +1,947 @@ 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'); $loadDiagramName = $request->getURIData('loadDiagramName'); $loadDiagramPHID = $request->getURIData('loadDiagramPHID'); $loadDiagramVersion = $request->getURIData('loadDiagramVersion'); 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; } } if ($route == 'loadJsExtension') { $response = new AphrontFileResponse(); $response->setMimeType('application/javascript'); $base64_data = ""; if (isset($loadDiagramName) && !empty(trim($loadDiagramName))) { $diagram_id = (int) substr($loadDiagramName, strlen("DIAG")); if (isset($loadDiagramVersion) && !empty(trim($loadDiagramVersion))) { $diagramVersion = id(new DiagramVersion())->loadByVersionedDiagramID( $diagram_id, $loadDiagramVersion); } else { $diagramVersion = id(new DiagramVersion())->loadLatestByDiagramID( $diagram_id); } if ($diagramVersion) { $data = $diagramVersion->getData(); $base64_data = base64_encode($data); } } $response->setContent('loadJsExtension("' . $loadDiagramName . '", "' . $loadDiagramPHID . '", "' . $loadDiagramVersion . '", "' . $base64_data . '");'); 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 ); $file = rtrim($file, '/') . '/' . $versioneddiagramid; } else { $file = preg_replace( '/^\/diagram\/(' . $diagramid . '\/)?/', 'data/', $file ); } } // check if we are trying to load "iframe loader" files, // if so, correct the path accordingly if ($file == "data/drawio/src/main/webapp/index.html") { return $this->showIframe($request); } else if ($file == "data/drawio/src/main/webapp/iframe.css" || $file == "data/drawio/src/main/webapp/iframe-toolbtn.css" || $file == "data/drawio/src/main/webapp/iframe1.js" || $file == "data/drawio/src/main/webapp/iframe2.js" || $file == "data/drawio/src/main/webapp/iframe-toolbtn.js") { $file = str_replace("drawio/src/main/webapp/", "", $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()) ->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. */ 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 = null, string $diagramPHID = null, string $diagramVersion = null, string $diagramBase64 = null ) { $applicationUrl = "/" . explode("/", $request->getPath())[1]; $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( 'script', array( 'src' => 'phorge_extension.js', 'defer' => true ), '' ), phutil_tag( 'script', array( 'src' => $applicationUrl . '/loadJsExtension/' . $diagramName . '/' . $diagramPHID . '/' . $diagramVersion . '/', 'defer' => true ), '' ), 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; } /** * Shows the internal draw.io application */ private function showIframe( AphrontRequest $request ) { $content = phutil_tag( 'html', array(), array( phutil_tag( 'head', array(), array( phutil_tag( 'title', array(), 'Flowchart Maker & Online Diagram Software' ), phutil_tag( 'meta', array( 'charset' => 'utf-8' ) ), phutil_tag( 'meta', array( 'http-equiv' => 'content-type', 'content' => 'text/html; charset=utf-8' ) ), phutil_tag( 'meta', array( 'name' => 'viewport', 'content' => 'width=device-width, initial-scale=1.0, ' . 'maximum-scale=1.0, user-scalable=no' ) ), phutil_tag( 'meta', array( 'name' => 'mobile-web-app-capable', 'content' => 'yes' ) ), phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'iframe.css' ) ), phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'iframe-toolbtn.css' ) ), phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'styles/grapheditor.css' ) ), phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => 'iframe1.js', 'defer' => true ) ) ) ), phutil_tag( 'body', array( 'class' => 'geEditor' ), array( phutil_tag( 'div', array( 'id' => 'geinfo' ), phutil_tag( 'div', array( 'class' => 'geBlock' ), array( phutil_tag( 'h1', array(), 'Flowchart Maker and Online Diagram Software' ), phutil_tag( 'p', array(), '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( 'class' => 'init-spinner' ), phutil_tag( 'div', array( 'class' => 'content' ), array( phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(0deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -1s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(27deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.923077ss;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(55deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.846154s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(83deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.769231s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(110deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.692308s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(138deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.615385s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(166deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.538462s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(193deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.461538s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(221deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.384615s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(249deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.307692s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(276deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.230769s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(304deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.153846s;' ) ) ), phutil_tag( 'div', array( 'class' => 'spike', 'style' => 'transform: rotate(332deg)' . ' translatex(13px);' ), phutil_tag( 'div', array( 'class' => 'animator', 'style' => 'animation-delay: -0.0769231s;' ) ) ) ) ) ) ) - ), + ) ), phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => 'iframe2.js', 'defer' => true ) ), phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => 'iframe-toolbtn.js', 'defer' => true ) ) ) ) ) ); $response = id(new PlainHtmlWebpageResponse()) ->setFrameable(true) ->setContent($content); return $response; } }