1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

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
This commit is contained in:
epriestley 2018-03-19 10:55:02 -07:00
parent 01f22a8d06
commit f646153f4d
14 changed files with 411 additions and 32 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => '6a8ba174',
'core.pkg.css' => '97dc0e74',
'core.pkg.js' => '8581cd02',
'differential.pkg.css' => '113e692c',
'differential.pkg.js' => 'f6d809c0',
@ -168,7 +168,7 @@ return array(
'rsrc/css/phui/phui-object-box.css' => '9cff003c',
'rsrc/css/phui/phui-pager.css' => 'edcbc226',
'rsrc/css/phui/phui-pinboard-view.css' => '2495140e',
'rsrc/css/phui/phui-property-list-view.css' => '79fc3a02',
'rsrc/css/phui/phui-property-list-view.css' => 'ef864066',
'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863',
'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892',
'rsrc/css/phui/phui-spacing.css' => '042804d6',
@ -392,6 +392,7 @@ return array(
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'f01586dc',
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '1db13e70',
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef',
'rsrc/js/application/files/behavior-document-engine.js' => 'f6d6f389',
'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab',
'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888',
'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '191b4909',
@ -606,6 +607,7 @@ return array(
'javelin-behavior-diffusion-jump-to' => '73d09eef',
'javelin-behavior-diffusion-locate-file' => '6d3e1947',
'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc',
'javelin-behavior-document-engine' => 'f6d6f389',
'javelin-behavior-doorkeeper-tag' => '1db13e70',
'javelin-behavior-drydock-live-operation-status' => '901935ef',
'javelin-behavior-durable-column' => '2ae077e1',
@ -848,7 +850,7 @@ return array(
'phui-oi-simple-ui-css' => 'a8beebea',
'phui-pager-css' => 'edcbc226',
'phui-pinboard-view-css' => '2495140e',
'phui-property-list-view-css' => '79fc3a02',
'phui-property-list-view-css' => 'ef864066',
'phui-remarkup-preview-css' => '54a34863',
'phui-segment-bar-view-css' => 'b1d1b892',
'phui-spacing-css' => '042804d6',
@ -2151,6 +2153,11 @@ return array(
'javelin-util',
'javelin-reactor',
),
'f6d6f389' => array(
'javelin-behavior',
'javelin-dom',
'javelin-stratcom',
),
'f829edb3' => array(
'javelin-view',
'javelin-install',

View file

@ -3001,6 +3001,7 @@ phutil_register_library_map(array(
'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php',
'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php',
'PhabricatorFileDeleteTransaction' => 'applications/files/xaction/PhabricatorFileDeleteTransaction.php',
'PhabricatorFileDocumentController' => 'applications/files/controller/PhabricatorFileDocumentController.php',
'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php',
'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php',
'PhabricatorFileEditEngine' => 'applications/files/editor/PhabricatorFileEditEngine.php',
@ -3140,6 +3141,7 @@ phutil_register_library_map(array(
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php',
'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php',
'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php',
'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php',
@ -8590,6 +8592,7 @@ phutil_register_library_map(array(
'PhabricatorFileDataController' => 'PhabricatorFileController',
'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDeleteTransaction' => 'PhabricatorFileTransactionType',
'PhabricatorFileDocumentController' => 'PhabricatorFileController',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileEditController' => 'PhabricatorFileController',
'PhabricatorFileEditEngine' => 'PhabricatorEditEngine',
@ -8747,6 +8750,7 @@ phutil_register_library_map(array(
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorHeraldApplication' => 'PhabricatorApplication',
'PhabricatorHeraldContentSource' => 'PhabricatorContentSource',
'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorHomeApplication' => 'PhabricatorApplication',
'PhabricatorHomeConstants' => 'PhabricatorHomeController',

View file

@ -89,6 +89,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
'iconset/(?P<key>[^/]+)/' => array(
'select/' => 'PhabricatorFileIconSetSelectController',
),
'document/(?P<engineKey>[^/]+)/(?P<phid>[^/]+)/'
=> 'PhabricatorFileDocumentController',
) + $this->getResourceSubroutes(),
);
}

View file

@ -0,0 +1,113 @@
<?php
final class PhabricatorFileDocumentController
extends PhabricatorFileController {
private $file;
private $engine;
private $ref;
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$file_phid = $request->getURIData('phid');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($file_phid))
->executeOne();
if (!$file) {
return $this->newErrorResponse(
pht(
'This file ("%s") does not exist or could not be loaded.',
$file_phid));
}
$this->file = $file;
$ref = id(new PhabricatorDocumentRef())
->setFile($file);
$this->ref = $ref;
$engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
$engine_key = $request->getURIData('engineKey');
if (!isset($engines[$engine_key])) {
return $this->newErrorResponse(
pht(
'The engine ("%s") is unknown, or unable to render this document.',
$engine_key));
}
$engine = $engines[$engine_key];
$this->engine = $engine;
try {
$content = $engine->newDocument($ref);
} catch (Exception $ex) {
return $this->newErrorResponse($ex->getMessage());
}
return $this->newContentResponse($content);
}
private function newErrorResponse($message) {
$container = phutil_tag(
'div',
array(
'class' => 'document-engine-error',
),
array(
id(new PHUIIconView())
->setIcon('fa-exclamation-triangle red'),
' ',
$message,
));
return $this->newContentResponse($container);
}
private function newContentResponse($content) {
$viewer = $this->getViewer();
$request = $this->getRequest();
$file = $this->file;
$engine = $this->engine;
$ref = $this->ref;
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(
array(
'markup' => hsprintf('%s', $content),
));
}
$crumbs = $this->buildApplicationCrumbs();
if ($file) {
$crumbs->addTextCrumb($file->getMonogram(), $file->getInfoURI());
}
$label = $engine->getViewAsLabel($ref);
if ($label) {
$crumbs->addTextCrumb($label);
}
$crumbs->setBorder(true);
$content_frame = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($content);
$page_frame = id(new PHUITwoColumnView())
->setFooter($content_frame);
return $this->newPage()
->setCrumbs($crumbs)
->setTitle(
array(
$ref->getName(),
pht('Standalone'),
))
->appendChild($page_frame);
}
}

View file

@ -404,50 +404,70 @@ final class PhabricatorFileInfoController extends PhabricatorFileController {
private function newFileContent(PhabricatorFile $file) {
$viewer = $this->getViewer();
$engines = PhabricatorDocumentEngine::getAllEngines();
$ref = id(new PhabricatorDocumentRef())
->setFile($file);
foreach ($engines as $key => $engine) {
$engine = id(clone $engine)
->setViewer($viewer);
if (!$engine->canRenderDocument($ref)) {
unset($engines[$key]);
continue;
}
$engines[$key] = $engine;
}
if (!$engines) {
throw new Exception(pht('No engine can render this document.'));
}
$vectors = array();
foreach ($engines as $key => $usable_engine) {
$vectors[$key] = $usable_engine->newSortVector($ref);
}
$vectors = msortv($vectors, 'getSelf');
$engine = $engines[head_key($vectors)];
$engines = PhabricatorDocumentEngine::getEnginesForRef($viewer, $ref);
$engine = head($engines);
$content = $engine->newDocument($ref);
if (!$content) {
return null;
}
$icon = $engine->newDocumentIcon($ref);
$views = array();
foreach ($engines as $candidate_engine) {
$label = $candidate_engine->getViewAsLabel($ref);
if ($label === null) {
continue;
}
$view_icon = $candidate_engine->getViewAsIconIcon($ref);
$views[] = array(
'viewKey' => $candidate_engine->getDocumentEngineKey(),
'icon' => $view_icon,
'name' => $label,
'engineURI' => $candidate_engine->getRenderURI($ref),
);
}
Javelin::initBehavior('document-engine');
$viewport_id = celerity_generate_unique_node_id();
$viewport = phutil_tag(
'div',
array(
'id' => $viewport_id,
),
$content);
$meta = array(
'viewportID' => $viewport_id,
'viewKey' => $engine->getDocumentEngineKey(),
'views' => $views,
'standaloneURI' => $engine->getRenderURI($ref),
);
$view_button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View Options'))
->setIcon('fa-file-image-o')
->setColor(PHUIButtonView::GREY)
->setMetadata($meta)
->setDropdown(true)
->addSigil('document-engine-view-dropdown');
$header = id(new PHUIHeaderView())
->setHeaderIcon($icon)
->setHeader($ref->getName());
->setHeader($ref->getName())
->addActionLink($view_button);
return id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header)
->appendChild($content);
->appendChild($viewport);
}
}

View file

@ -5,6 +5,10 @@ final class PhabricatorAudioDocumentEngine
const ENGINEKEY = 'audio';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as Audio');
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-file-sound-o';
}

View file

@ -59,4 +59,52 @@ abstract class PhabricatorDocumentEngine
return 2000;
}
abstract public function getViewAsLabel(PhabricatorDocumentRef $ref);
public function getViewAsIconIcon(PhabricatorDocumentRef $ref) {
return $this->getDocumentIconIcon($ref);
}
public function getRenderURI(PhabricatorDocumentRef $ref) {
$file = $ref->getFile();
if (!$file) {
throw new PhutilMethodNotImplementedException();
}
$engine_key = $this->getDocumentEngineKey();
$file_phid = $file->getPHID();
return "/file/document/{$engine_key}/{$file_phid}/";
}
final public static function getEnginesForRef(
PhabricatorUser $viewer,
PhabricatorDocumentRef $ref) {
$engines = self::getAllEngines();
foreach ($engines as $key => $engine) {
$engine = id(clone $engine)
->setViewer($viewer);
if (!$engine->canRenderDocument($ref)) {
unset($engines[$key]);
continue;
}
$engines[$key] = $engine;
}
if (!$engines) {
throw new Exception(pht('No content engine can render this document.'));
}
$vectors = array();
foreach ($engines as $key => $usable_engine) {
$vectors[$key] = $usable_engine->newSortVector($ref);
}
$vectors = msortv($vectors, 'getSelf');
return array_select_keys($engines, array_keys($vectors));
}
}

View file

@ -68,6 +68,14 @@ final class PhabricatorDocumentRef
return null;
}
public function loadData() {
if ($this->file) {
return $this->file->loadFileData();
}
throw new PhutilMethodNotImplementedException();
}
public function hasAnyMimeType(array $candidate_types) {
$mime_full = $this->getMimeType();
$mime_parts = explode(';', $mime_full);

View file

@ -0,0 +1,83 @@
<?php
final class PhabricatorHexdumpDocumentEngine
extends PhabricatorDocumentEngine {
const ENGINEKEY = 'hexdump';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as Hexdump');
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-microchip';
}
protected function getContentScore() {
return 500;
}
protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
return true;
}
protected function newDocumentContent(PhabricatorDocumentRef $ref) {
$content = $ref->loadData();
$output = array();
$offset = 0;
$lines = str_split($content, 16);
foreach ($lines as $line) {
$output[] = sprintf(
'%08x %- 23s %- 23s %- 16s',
$offset,
$this->renderHex(substr($line, 0, 8)),
$this->renderHex(substr($line, 8)),
$this->renderBytes($line));
$offset += 16;
}
$output = implode("\n", $output);
$container = phutil_tag(
'div',
array(
'class' => 'document-engine-hexdump PhabricatorMonospaced',
),
$output);
return $container;
}
private function renderHex($bytes) {
$length = strlen($bytes);
$output = array();
for ($ii = 0; $ii < $length; $ii++) {
$output[] = sprintf('%02x', ord($bytes[$ii]));
}
return implode(' ', $output);
}
private function renderBytes($bytes) {
$length = strlen($bytes);
$output = array();
for ($ii = 0; $ii < $length; $ii++) {
$chr = $bytes[$ii];
$ord = ord($chr);
if ($ord < 0x20 || $ord >= 0x7F) {
$chr = '.';
}
$output[] = $chr;
}
return implode('', $output);
}
}

View file

@ -5,6 +5,10 @@ final class PhabricatorImageDocumentEngine
const ENGINEKEY = 'image';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as Image');
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-file-image-o';
}

View file

@ -5,6 +5,10 @@ final class PhabricatorVideoDocumentEngine
const ENGINEKEY = 'video';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as Video');
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-film';
}

View file

@ -5,6 +5,10 @@ final class PhabricatorVoidDocumentEngine
const ENGINEKEY = 'void';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return null;
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-file';
}

View file

@ -231,3 +231,14 @@ div.phui-property-list-stacked .phui-property-list-properties
text-align: center;
color: {$greytext};
}
.document-engine-error {
margin: 20px auto;
text-align: center;
color: {$redtext};
}
.document-engine-hexdump {
margin: 20px;
white-space: pre;
}

View file

@ -0,0 +1,67 @@
/**
* @provides javelin-behavior-document-engine
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
*/
JX.behavior('document-engine', function() {
function onmenu(e) {
var node = e.getNode('document-engine-view-dropdown');
var data = JX.Stratcom.getData(node);
if (data.menu) {
return;
}
e.prevent();
var menu = new JX.PHUIXDropdownMenu(node);
var list = new JX.PHUIXActionListView();
var view;
for (var ii = 0; ii < data.views.length; ii++) {
var spec = data.views[ii];
view = new JX.PHUIXActionView()
.setName(spec.name)
.setIcon(spec.icon)
.setHref(spec.engineURI);
view.setHandler(JX.bind(null, function(spec, e) {
if (!e.isNormalClick()) {
return;
}
e.prevent();
menu.close();
onview(data, spec);
}, spec));
list.addItem(view);
}
menu.setContent(list.getNode());
data.menu = menu;
menu.open();
}
function onview(data, spec) {
var handler = JX.bind(null, onrender, data);
new JX.Request(spec.engineURI, handler)
.send();
}
function onrender(data, r) {
var viewport = JX.$(data.viewportID);
JX.DOM.setContent(viewport, JX.$H(r.markup));
}
JX.Stratcom.listen('click', 'document-engine-view-dropdown', onmenu);
});