diff --git a/src/controller/DiagramController.php b/src/controller/DiagramController.php index 3fbe80a..6bb4aef 100644 --- a/src/controller/DiagramController.php +++ b/src/controller/DiagramController.php @@ -1,470 +1,459 @@ 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 $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(); } 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( '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( string $diagramName = '', string $diagramPHID = '', string $diagramVersion = '', string $base64_data = '' ) { $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'); require_celerity_resource('diagram-css-extension', 'diagram-resources'); Javelin::initBehavior('diagram-extension', $behaviorConfig, 'diagram-resources'); $crumbs = $this->buildApplicationCrumbs()->setBorder(true); if (!$diagramName) { $crumbs->addTextCrumb('Create Diagram'); } else { $crumbs->addTextCrumb( $diagramName, $this->getApplicationURI('/' . $diagramName) ); if ($diagramVersion) { $crumbs->addTextCrumb('Version ' . $diagramVersion); } } return $this->newPage() ->addClass('diagram-editor-page') ->setTitle('Diagrams') ->setCrumbs($crumbs) ->appendChild(phutil_tag( 'div', array( 'id' => 'diagram-editor-wrapper' ) )); } }