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

Add a PDF document "rendering" engine

Summary:
Depends on D19251. Ref T13105. This adds rendering engine support for PDFs.

It doesn't actually render them, it just renders a link which you can click to view them in a new window. This is much easier than actually rendering them inline and at least 95% as good most of the time (and probably more-than-100%-as-good some of the time).

This makes PDF a viewable MIME type by default and adds a narrow CSP exception for it. See also T13112.

Test Plan:
  - Viewed PDFs in Files, got a link to view them in a new tab.
  - Clicked the link in Safari, Chrome, and Firefox; got inline PDFs.
  - Verified primary CSP is still `object-src 'none'` with `curl ...`.
  - Interacted with the vanilla lightbox element to check that it still works.

Maniphest Tasks: T13105

Differential Revision: https://secure.phabricator.com/D19252
This commit is contained in:
epriestley 2018-03-23 04:50:48 -07:00
parent 8b658706a8
commit fb4ce851c4
10 changed files with 154 additions and 30 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array(
'conpherence.pkg.css' => 'e68cf1fa',
'conpherence.pkg.js' => '15191c65',
'core.pkg.css' => 'afe29a6c',
'core.pkg.css' => '2d73b2f3',
'core.pkg.js' => 'b9b4a943',
'differential.pkg.css' => '113e692c',
'differential.pkg.js' => 'f6d809c0',
@ -112,7 +112,7 @@ return array(
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => '62fa3ace',
'rsrc/css/core/remarkup.css' => '97dc3523',
'rsrc/css/core/remarkup.css' => 'b375546d',
'rsrc/css/core/syntax.css' => 'cae95e89',
'rsrc/css/core/z-index.css' => '9d8f7c4b',
'rsrc/css/diviner/diviner-shared.css' => '896f1d43',
@ -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' => '94a14381',
'rsrc/css/phui/phui-property-list-view.css' => '47018d3c',
'rsrc/css/phui/phui-remarkup-preview.css' => '54a34863',
'rsrc/css/phui/phui-segment-bar-view.css' => 'b1d1b892',
'rsrc/css/phui/phui-spacing.css' => '042804d6',
@ -780,7 +780,7 @@ return array(
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '77b0ae28',
'phabricator-remarkup-css' => '97dc3523',
'phabricator-remarkup-css' => 'b375546d',
'phabricator-search-results-css' => '505dd8cf',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
@ -850,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' => '94a14381',
'phui-property-list-view-css' => '47018d3c',
'phui-remarkup-preview-css' => '54a34863',
'phui-segment-bar-view-css' => 'b1d1b892',
'phui-spacing-css' => '042804d6',

View file

@ -3519,6 +3519,7 @@ phutil_register_library_map(array(
'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php',
'PhabricatorOwnersSchemaSpec' => 'applications/owners/storage/PhabricatorOwnersSchemaSpec.php',
'PhabricatorOwnersSearchField' => 'applications/owners/searchfield/PhabricatorOwnersSearchField.php',
'PhabricatorPDFDocumentEngine' => 'applications/files/document/PhabricatorPDFDocumentEngine.php',
'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php',
'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php',
'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php',
@ -9169,6 +9170,7 @@ phutil_register_library_map(array(
'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField',
'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine',
'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPHID' => 'Phobject',
'PhabricatorPHIDConstants' => 'Phobject',

View file

@ -28,6 +28,7 @@ abstract class AphrontResponse extends Phobject {
'connect-src' => array(),
'frame-src' => array(),
'form-action' => array(),
'object-src' => array(),
);
}
@ -163,8 +164,10 @@ abstract class AphrontResponse extends Phobject {
$csp[] = "frame-ancestors 'none'";
}
// Block relics of the old world: Flash, Java applets, and so on.
$csp[] = "object-src 'none'";
// Block relics of the old world: Flash, Java applets, and so on. Note
// that Chrome prevents the user from viewing PDF documents if they are
// served with a policy which excludes the domain they are served from.
$csp[] = $this->newContentSecurityPolicy('object-src', "'none'");
// Don't allow forms to submit offsite.

View file

@ -45,6 +45,8 @@ final class PhabricatorFilesConfigOptions
'video/ogg' => 'video/ogg',
'video/webm' => 'video/webm',
'video/quicktime' => 'video/quicktime',
'application/pdf' => 'application/pdf',
);
$image_default = array(

View file

@ -73,11 +73,14 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
list($begin, $end) = $response->parseHTTPRange($range);
}
$is_viewable = $file->isViewableInBrowser();
if (!$file->isViewableInBrowser()) {
$is_download = true;
}
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
$is_lfs = ($request_type == 'git-lfs');
if ($is_viewable && !$is_download) {
if (!$is_download) {
$response->setMimeType($file->getViewableMimeType());
} else {
$is_post = $request->isHTTPPost();
@ -109,6 +112,19 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$response->setContentLength($file->getByteSize());
$response->setContentIterator($iterator);
// In Chrome, we must permit this domain in "object-src" CSP when serving a
// PDF or the browser will refuse to render it.
if (!$is_download && $file->isPDF()) {
$request_uri = id(clone $request->getAbsoluteRequestURI())
->setPath(null)
->setFragment(null)
->setQueryParams(array());
$response->addContentSecurityPolicyURI(
'object-src',
(string)$request_uri);
}
return $response;
}

View file

@ -0,0 +1,57 @@
<?php
final class PhabricatorPDFDocumentEngine
extends PhabricatorDocumentEngine {
const ENGINEKEY = 'pdf';
public function getViewAsLabel(PhabricatorDocumentRef $ref) {
return pht('View as PDF');
}
protected function getDocumentIconIcon(PhabricatorDocumentRef $ref) {
return 'fa-file-pdf-o';
}
protected function canRenderDocumentType(PhabricatorDocumentRef $ref) {
// Since we just render a link to the document anyway, we don't need to
// check anything fancy in config to see if the MIME type is actually
// viewable.
return $ref->hasAnyMimeType(
array(
'application/pdf',
));
}
protected function newDocumentContent(PhabricatorDocumentRef $ref) {
$viewer = $this->getViewer();
$file = $ref->getFile();
if ($file) {
$source_uri = $file->getViewURI();
} else {
throw new PhutilMethodNotImplementedException();
}
$name = $ref->getName();
$length = $ref->getByteLength();
$link = id(new PhabricatorFileLinkView())
->setViewer($viewer)
->setFileName($name)
->setFileViewURI($source_uri)
->setFileViewable(true)
->setFileSize(phutil_format_bytes($length));
$container = phutil_tag(
'div',
array(
'class' => 'document-engine-pdf',
),
$link);
return $container;
}
}

View file

@ -930,6 +930,19 @@ final class PhabricatorFile extends PhabricatorFileDAO
return idx($mime_map, $mime_type);
}
public function isPDF() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = array(
'application/pdf' => 'application/pdf',
);
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle

View file

@ -101,28 +101,41 @@ final class PhabricatorFileLinkView extends AphrontTagView {
}
protected function getTagName() {
if ($this->getFileDownloadURI()) {
return 'div';
} else {
return 'a';
}
}
protected function getTagAttributes() {
$mustcapture = true;
$sigil = 'lightboxable';
$meta = $this->getMeta();
$class = 'phabricator-remarkup-embed-layout-link';
if ($this->getCustomClass()) {
$class = $this->getCustomClass();
}
return array(
$attributes = array(
'href' => $this->getFileViewURI(),
'target' => '_blank',
'rel' => 'noreferrer',
'class' => $class,
);
if ($this->getFilePHID()) {
$mustcapture = true;
$sigil = 'lightboxable';
$meta = $this->getMeta();
$attributes += array(
'sigil' => $sigil,
'meta' => $meta,
'mustcapture' => $mustcapture,
);
}
return $attributes;
}
protected function getTagContent() {
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('phui-lightbox-css');
@ -131,6 +144,10 @@ final class PhabricatorFileLinkView extends AphrontTagView {
->setIcon($this->getFileIcon())
->addClass('phabricator-remarkup-embed-layout-icon');
$download_link = null;
$download_uri = $this->getFileDownloadURI();
if ($download_uri) {
$dl_icon = id(new PHUIIconView())
->setIcon('fa-download');
@ -138,9 +155,10 @@ final class PhabricatorFileLinkView extends AphrontTagView {
'a',
array(
'class' => 'phabricator-remarkup-embed-layout-download',
'href' => $this->getFileDownloadURI(),
'href' => $download_uri,
),
pht('Download'));
}
$info = phutil_tag(
'span',

View file

@ -405,8 +405,9 @@ video.phabricator-media {
color: {$blacktext};
min-width: 256px;
position: relative;
/*height: 22px;*/
line-height: 20px;
overflow: hidden;
min-height: 38px;
}
.phabricator-remarkup-embed-layout-icon {
@ -426,6 +427,9 @@ video.phabricator-media {
.phabricator-remarkup-embed-layout-link:hover {
border-color: {$violet};
cursor: pointer;
}
.device-desktop .phabricator-remarkup-embed-layout-link:hover {
text-decoration: none;
}

View file

@ -248,3 +248,12 @@ div.phui-property-list-stacked .phui-property-list-properties
.document-engine-remarkup {
margin: 20px;
}
.document-engine-pdf {
margin: 20px;
text-align: center;
}
.document-engine-pdf .phabricator-remarkup-embed-layout-link {
text-align: left;
}