@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add an async driver for document rendering and a crude "Hexdump" document engine

Summary: Depends on D19237. Ref T13105. This adds a (very basic) "Hexdump" engine (mostly just to have a second option to switch to) and a selector for choosing view modes.

Test Plan: Viewed some files, switched between audio/video/image/hexdump.

Maniphest Tasks: T13105

Differential Revision: https://secure.phabricator.com/D19238

+405 -26
+10 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 - 'core.pkg.css' => '6a8ba174', 12 + 'core.pkg.css' => '97dc0e74', 13 13 'core.pkg.js' => '8581cd02', 14 14 'differential.pkg.css' => '113e692c', 15 15 'differential.pkg.js' => 'f6d809c0', ··· 168 168 'rsrc/css/phui/phui-object-box.css' => '9cff003c', 169 169 'rsrc/css/phui/phui-pager.css' => 'edcbc226', 170 170 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 171 - 'rsrc/css/phui/phui-property-list-view.css' => '79fc3a02', 171 + 'rsrc/css/phui/phui-property-list-view.css' => 'ef864066', 172 172 'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863', 173 173 'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892', 174 174 'rsrc/css/phui/phui-spacing.css' => '042804d6', ··· 392 392 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc', 393 393 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70', 394 394 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 395 + 'rsrc/js/application/files/behavior-document-engine.js' => 'f6d6f389', 395 396 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 396 397 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 397 398 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909', ··· 606 607 'javelin-behavior-diffusion-jump-to' => '73d09eef', 607 608 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 608 609 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 610 + 'javelin-behavior-document-engine' => 'f6d6f389', 609 611 'javelin-behavior-doorkeeper-tag' => '1db13e70', 610 612 'javelin-behavior-drydock-live-operation-status' => '901935ef', 611 613 'javelin-behavior-durable-column' => '2ae077e1', ··· 848 850 'phui-oi-simple-ui-css' => 'a8beebea', 849 851 'phui-pager-css' => 'edcbc226', 850 852 'phui-pinboard-view-css' => '2495140e', 851 - 'phui-property-list-view-css' => '79fc3a02', 853 + 'phui-property-list-view-css' => 'ef864066', 852 854 'phui-remarkup-preview-css' => '54a34863', 853 855 'phui-segment-bar-view-css' => 'b1d1b892', 854 856 'phui-spacing-css' => '042804d6', ··· 2150 2152 'javelin-reactornode', 2151 2153 'javelin-util', 2152 2154 'javelin-reactor', 2155 + ), 2156 + 'f6d6f389' => array( 2157 + 'javelin-behavior', 2158 + 'javelin-dom', 2159 + 'javelin-stratcom', 2153 2160 ), 2154 2161 'f829edb3' => array( 2155 2162 'javelin-view',
+4
src/__phutil_library_map__.php
··· 3001 3001 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 3002 3002 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 3003 3003 'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php', 3004 + 'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php', 3004 3005 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 3005 3006 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 3006 3007 'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php', ··· 3140 3141 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', 3141 3142 'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php', 3142 3143 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', 3144 + 'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php', 3143 3145 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php', 3144 3146 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 3145 3147 'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php', ··· 8590 8592 'PhabricatorFileDataController' => 'PhabricatorFileController', 8591 8593 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 8592 8594 'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType', 8595 + 'PhabricatorFileDocumentController' => 'PhabricatorFileController', 8593 8596 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 8594 8597 'PhabricatorFileEditController' => 'PhabricatorFileController', 8595 8598 'PhabricatorFileEditEngine' => 'PhabricatorEditEngine', ··· 8747 8750 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 8748 8751 'PhabricatorHeraldApplication' => 'PhabricatorApplication', 8749 8752 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource', 8753 + 'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine', 8750 8754 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 8751 8755 'PhabricatorHomeApplication' => 'PhabricatorApplication', 8752 8756 'PhabricatorHomeConstants' => 'PhabricatorHomeController',
+2
src/applications/files/application/PhabricatorFilesApplication.php
··· 89 89 'iconset/(?P<key>[^/]+)/' => array( 90 90 'select/' => 'PhabricatorFileIconSetSelectController', 91 91 ), 92 + 'document/(?P<engineKey>[^/]+)/(?P<phid>[^/]+)/' 93 + => 'PhabricatorFileDocumentController', 92 94 ) + $this->getResourceSubroutes(), 93 95 ); 94 96 }
+113
src/applications/files/controller/PhabricatorFileDocumentController.php
··· 1 + <?php 2 + 3 + final class PhabricatorFileDocumentController 4 + extends PhabricatorFileController { 5 + 6 + private $file; 7 + private $engine; 8 + private $ref; 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $request->getViewer(); 12 + 13 + $file_phid = $request->getURIData('phid'); 14 + 15 + $file = id(new PhabricatorFileQuery()) 16 + ->setViewer($viewer) 17 + ->withPHIDs(array($file_phid)) 18 + ->executeOne(); 19 + if (!$file) { 20 + return $this->newErrorResponse( 21 + pht( 22 + 'This file ("%s") does not exist or could not be loaded.', 23 + $file_phid)); 24 + } 25 + $this->file = $file; 26 + 27 + $ref = id(new PhabricatorDocumentRef()) 28 + ->setFile($file); 29 + $this->ref = $ref; 30 + 31 + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); 32 + $engine_key = $request->getURIData('engineKey'); 33 + if (!isset($engines[$engine_key])) { 34 + return $this->newErrorResponse( 35 + pht( 36 + 'The engine ("%s") is unknown, or unable to render this document.', 37 + $engine_key)); 38 + } 39 + $engine = $engines[$engine_key]; 40 + $this->engine = $engine; 41 + 42 + try { 43 + $content = $engine->newDocument($ref); 44 + } catch (Exception $ex) { 45 + return $this->newErrorResponse($ex->getMessage()); 46 + } 47 + 48 + return $this->newContentResponse($content); 49 + } 50 + 51 + private function newErrorResponse($message) { 52 + $container = phutil_tag( 53 + 'div', 54 + array( 55 + 'class' => 'document-engine-error', 56 + ), 57 + array( 58 + id(new PHUIIconView()) 59 + ->setIcon('fa-exclamation-triangle red'), 60 + ' ', 61 + $message, 62 + )); 63 + 64 + return $this->newContentResponse($container); 65 + } 66 + 67 + 68 + private function newContentResponse($content) { 69 + $viewer = $this->getViewer(); 70 + $request = $this->getRequest(); 71 + 72 + $file = $this->file; 73 + $engine = $this->engine; 74 + $ref = $this->ref; 75 + 76 + if ($request->isAjax()) { 77 + return id(new AphrontAjaxResponse()) 78 + ->setContent( 79 + array( 80 + 'markup' => hsprintf('%s', $content), 81 + )); 82 + } 83 + 84 + $crumbs = $this->buildApplicationCrumbs(); 85 + if ($file) { 86 + $crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI()); 87 + } 88 + 89 + $label = $engine->getViewAsLabel($ref); 90 + if ($label) { 91 + $crumbs->addTextCrumb($label); 92 + } 93 + 94 + $crumbs->setBorder(true); 95 + 96 + $content_frame = id(new PHUIObjectBoxView()) 97 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 98 + ->appendChild($content); 99 + 100 + $page_frame = id(new PHUITwoColumnView()) 101 + ->setFooter($content_frame); 102 + 103 + return $this->newPage() 104 + ->setCrumbs($crumbs) 105 + ->setTitle( 106 + array( 107 + $ref->getName(), 108 + pht('Standalone'), 109 + )) 110 + ->appendChild($page_frame); 111 + } 112 + 113 + }
+43 -23
src/applications/files/controller/PhabricatorFileInfoController.php
··· 404 404 405 405 private function newFileContent(PhabricatorFile $file) { 406 406 $viewer = $this->getViewer(); 407 - $engines = PhabricatorDocumentEngine::getAllEngines(); 408 407 409 408 $ref = id(new PhabricatorDocumentRef()) 410 409 ->setFile($file); 411 410 412 - foreach ($engines as $key => $engine) { 413 - $engine = id(clone $engine) 414 - ->setViewer($viewer); 411 + $engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref); 412 + $engine = head($engines); 413 + 414 + $content = $engine->newDocument($ref); 415 + 416 + $icon = $engine->newDocumentIcon($ref); 415 417 416 - if (!$engine->canRenderDocument($ref)) { 417 - unset($engines[$key]); 418 + $views = array(); 419 + foreach ($engines as $candidate_engine) { 420 + $label = $candidate_engine->getViewAsLabel($ref); 421 + if ($label === null) { 418 422 continue; 419 423 } 420 424 421 - $engines[$key] = $engine; 422 - } 425 + $view_icon = $candidate_engine->getViewAsIconIcon($ref); 423 426 424 - if (!$engines) { 425 - throw new Exception(pht('No engine can render this document.')); 427 + $views[] = array( 428 + 'viewKey' => $candidate_engine->getDocumentEngineKey(), 429 + 'icon' => $view_icon, 430 + 'name' => $label, 431 + 'engineURI' => $candidate_engine->getRenderURI($ref), 432 + ); 426 433 } 427 434 428 - $vectors = array(); 429 - foreach ($engines as $key => $usable_engine) { 430 - $vectors[$key] = $usable_engine->newSortVector($ref); 431 - } 432 - $vectors = msortv($vectors, 'getSelf'); 435 + Javelin::initBehavior('document-engine'); 433 436 434 - $engine = $engines[head_key($vectors)]; 437 + $viewport_id = celerity_generate_unique_node_id(); 438 + 439 + $viewport = phutil_tag( 440 + 'div', 441 + array( 442 + 'id' => $viewport_id, 443 + ), 444 + $content); 435 445 436 - $content = $engine->newDocument($ref); 437 - if (!$content) { 438 - return null; 439 - } 446 + $meta = array( 447 + 'viewportID' => $viewport_id, 448 + 'viewKey' => $engine->getDocumentEngineKey(), 449 + 'views' => $views, 450 + 'standaloneURI' => $engine->getRenderURI($ref), 451 + ); 440 452 441 - $icon = $engine->newDocumentIcon($ref); 453 + $view_button = id(new PHUIButtonView()) 454 + ->setTag('a') 455 + ->setText(pht('View Options')) 456 + ->setIcon('fa-file-image-o') 457 + ->setColor(PHUIButtonView::GREY) 458 + ->setMetadata($meta) 459 + ->setDropdown(true) 460 + ->addSigil('document-engine-view-dropdown'); 442 461 443 462 $header = id(new PHUIHeaderView()) 444 463 ->setHeaderIcon($icon) 445 - ->setHeader($ref->getName()); 464 + ->setHeader($ref->getName()) 465 + ->addActionLink($view_button); 446 466 447 467 return id(new PHUIObjectBoxView()) 448 468 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 449 469 ->setHeader($header) 450 - ->appendChild($content); 470 + ->appendChild($viewport); 451 471 } 452 472 453 473 }
+4
src/applications/files/document/PhabricatorAudioDocumentEngine.php
··· 5 5 6 6 const ENGINEKEY = 'audio'; 7 7 8 + public function getViewAsLabel(PhabricatorDocumentRef $ref) { 9 + return pht('View as Audio'); 10 + } 11 + 8 12 protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 9 13 return 'fa-file-sound-o'; 10 14 }
+48
src/applications/files/document/PhabricatorDocumentEngine.php
··· 59 59 return 2000; 60 60 } 61 61 62 + abstract public function getViewAsLabel(PhabricatorDocumentRef $ref); 63 + 64 + public function getViewAsIconIcon(PhabricatorDocumentRef $ref) { 65 + return $this->getDocumentIconIcon($ref); 66 + } 67 + 68 + public function getRenderURI(PhabricatorDocumentRef $ref) { 69 + $file = $ref->getFile(); 70 + if (!$file) { 71 + throw new PhutilMethodNotImplementedException(); 72 + } 73 + 74 + $engine_key = $this->getDocumentEngineKey(); 75 + $file_phid = $file->getPHID(); 76 + 77 + return "/file/document/{$engine_key}/{$file_phid}/"; 78 + } 79 + 80 + final public static function getEnginesForRef( 81 + PhabricatorUser $viewer, 82 + PhabricatorDocumentRef $ref) { 83 + $engines = self::getAllEngines(); 84 + 85 + foreach ($engines as $key => $engine) { 86 + $engine = id(clone $engine) 87 + ->setViewer($viewer); 88 + 89 + if (!$engine->canRenderDocument($ref)) { 90 + unset($engines[$key]); 91 + continue; 92 + } 93 + 94 + $engines[$key] = $engine; 95 + } 96 + 97 + if (!$engines) { 98 + throw new Exception(pht('No content engine can render this document.')); 99 + } 100 + 101 + $vectors = array(); 102 + foreach ($engines as $key => $usable_engine) { 103 + $vectors[$key] = $usable_engine->newSortVector($ref); 104 + } 105 + $vectors = msortv($vectors, 'getSelf'); 106 + 107 + return array_select_keys($engines, array_keys($vectors)); 108 + } 109 + 62 110 }
+8
src/applications/files/document/PhabricatorDocumentRef.php
··· 68 68 return null; 69 69 } 70 70 71 + public function loadData() { 72 + if ($this->file) { 73 + return $this->file->loadFileData(); 74 + } 75 + 76 + throw new PhutilMethodNotImplementedException(); 77 + } 78 + 71 79 public function hasAnyMimeType(array $candidate_types) { 72 80 $mime_full = $this->getMimeType(); 73 81 $mime_parts = explode(';', $mime_full);
+83
src/applications/files/document/PhabricatorHexdumpDocumentEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorHexdumpDocumentEngine 4 + extends PhabricatorDocumentEngine { 5 + 6 + const ENGINEKEY = 'hexdump'; 7 + 8 + public function getViewAsLabel(PhabricatorDocumentRef $ref) { 9 + return pht('View as Hexdump'); 10 + } 11 + 12 + protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 13 + return 'fa-microchip'; 14 + } 15 + 16 + protected function getContentScore() { 17 + return 500; 18 + } 19 + 20 + protected function canRenderDocumentType(PhabricatorDocumentRef $ref) { 21 + return true; 22 + } 23 + 24 + protected function newDocumentContent(PhabricatorDocumentRef $ref) { 25 + $content = $ref->loadData(); 26 + 27 + $output = array(); 28 + $offset = 0; 29 + 30 + $lines = str_split($content, 16); 31 + foreach ($lines as $line) { 32 + $output[] = sprintf( 33 + '%08x %- 23s %- 23s %- 16s', 34 + $offset, 35 + $this->renderHex(substr($line, 0, 8)), 36 + $this->renderHex(substr($line, 8)), 37 + $this->renderBytes($line)); 38 + 39 + $offset += 16; 40 + } 41 + 42 + $output = implode("\n", $output); 43 + 44 + $container = phutil_tag( 45 + 'div', 46 + array( 47 + 'class' => 'document-engine-hexdump PhabricatorMonospaced', 48 + ), 49 + $output); 50 + 51 + return $container; 52 + } 53 + 54 + private function renderHex($bytes) { 55 + $length = strlen($bytes); 56 + 57 + $output = array(); 58 + for ($ii = 0; $ii < $length; $ii++) { 59 + $output[] = sprintf('%02x', ord($bytes[$ii])); 60 + } 61 + 62 + return implode(' ', $output); 63 + } 64 + 65 + private function renderBytes($bytes) { 66 + $length = strlen($bytes); 67 + 68 + $output = array(); 69 + for ($ii = 0; $ii < $length; $ii++) { 70 + $chr = $bytes[$ii]; 71 + $ord = ord($chr); 72 + 73 + if ($ord < 0x20 || $ord >= 0x7F) { 74 + $chr = '.'; 75 + } 76 + 77 + $output[] = $chr; 78 + } 79 + 80 + return implode('', $output); 81 + } 82 + 83 + }
+4
src/applications/files/document/PhabricatorImageDocumentEngine.php
··· 5 5 6 6 const ENGINEKEY = 'image'; 7 7 8 + public function getViewAsLabel(PhabricatorDocumentRef $ref) { 9 + return pht('View as Image'); 10 + } 11 + 8 12 protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 9 13 return 'fa-file-image-o'; 10 14 }
+4
src/applications/files/document/PhabricatorVideoDocumentEngine.php
··· 5 5 6 6 const ENGINEKEY = 'video'; 7 7 8 + public function getViewAsLabel(PhabricatorDocumentRef $ref) { 9 + return pht('View as Video'); 10 + } 11 + 8 12 protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 9 13 return 'fa-film'; 10 14 }
+4
src/applications/files/document/PhabricatorVoidDocumentEngine.php
··· 5 5 6 6 const ENGINEKEY = 'void'; 7 7 8 + public function getViewAsLabel(PhabricatorDocumentRef $ref) { 9 + return null; 10 + } 11 + 8 12 protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) { 9 13 return 'fa-file'; 10 14 }
+11
webroot/rsrc/css/phui/phui-property-list-view.css
··· 231 231 text-align: center; 232 232 color: {$greytext}; 233 233 } 234 + 235 + .document-engine-error { 236 + margin: 20px auto; 237 + text-align: center; 238 + color: {$redtext}; 239 + } 240 + 241 + .document-engine-hexdump { 242 + margin: 20px; 243 + white-space: pre; 244 + }
+67
webroot/rsrc/js/application/files/behavior-document-engine.js
··· 1 + /** 2 + * @provides javelin-behavior-document-engine 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-stratcom 6 + */ 7 + 8 + JX.behavior('document-engine', function() { 9 + 10 + function onmenu(e) { 11 + var node = e.getNode('document-engine-view-dropdown'); 12 + var data = JX.Stratcom.getData(node); 13 + 14 + if (data.menu) { 15 + return; 16 + } 17 + 18 + e.prevent(); 19 + 20 + var menu = new JX.PHUIXDropdownMenu(node); 21 + var list = new JX.PHUIXActionListView(); 22 + 23 + var view; 24 + for (var ii = 0; ii < data.views.length; ii++) { 25 + var spec = data.views[ii]; 26 + 27 + view = new JX.PHUIXActionView() 28 + .setName(spec.name) 29 + .setIcon(spec.icon) 30 + .setHref(spec.engineURI); 31 + 32 + view.setHandler(JX.bind(null, function(spec, e) { 33 + if (!e.isNormalClick()) { 34 + return; 35 + } 36 + 37 + e.prevent(); 38 + menu.close(); 39 + 40 + onview(data, spec); 41 + }, spec)); 42 + 43 + list.addItem(view); 44 + } 45 + 46 + menu.setContent(list.getNode()); 47 + 48 + data.menu = menu; 49 + menu.open(); 50 + } 51 + 52 + function onview(data, spec) { 53 + var handler = JX.bind(null, onrender, data); 54 + 55 + new JX.Request(spec.engineURI, handler) 56 + .send(); 57 + } 58 + 59 + function onrender(data, r) { 60 + var viewport = JX.$(data.viewportID); 61 + 62 + JX.DOM.setContent(viewport, JX.$H(r.markup)); 63 + } 64 + 65 + JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu); 66 + 67 + });