diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3519,6 +3519,7 @@ '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 @@ 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorOwnersSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorOwnersSearchField' => 'PhabricatorSearchTokenizerField', + 'PhabricatorPDFDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorPHDConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPHID' => 'Phobject', 'PhabricatorPHIDConstants' => 'Phobject', diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -28,6 +28,7 @@ 'connect-src' => array(), 'frame-src' => array(), 'form-action' => array(), + 'object-src' => array(), ); } @@ -163,8 +164,10 @@ $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. diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -45,6 +45,8 @@ 'video/ogg' => 'video/ogg', 'video/webm' => 'video/webm', 'video/quicktime' => 'video/quicktime', + + 'application/pdf' => 'application/pdf', ); $image_default = array( diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -73,11 +73,14 @@ 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 @@ $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; } diff --git a/src/applications/files/document/PhabricatorPDFDocumentEngine.php b/src/applications/files/document/PhabricatorPDFDocumentEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/files/document/PhabricatorPDFDocumentEngine.php @@ -0,0 +1,57 @@ +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; + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -930,6 +930,19 @@ 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 diff --git a/src/view/layout/PhabricatorFileLinkView.php b/src/view/layout/PhabricatorFileLinkView.php --- a/src/view/layout/PhabricatorFileLinkView.php +++ b/src/view/layout/PhabricatorFileLinkView.php @@ -101,26 +101,39 @@ } protected function getTagName() { - return 'div'; + 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( - 'href' => $this->getFileViewURI(), - 'class' => $class, - 'sigil' => $sigil, - 'meta' => $meta, - 'mustcapture' => $mustcapture, + $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() { @@ -131,16 +144,21 @@ ->setIcon($this->getFileIcon()) ->addClass('phabricator-remarkup-embed-layout-icon'); - $dl_icon = id(new PHUIIconView()) - ->setIcon('fa-download'); + $download_link = null; - $download_link = phutil_tag( - 'a', - array( - 'class' => 'phabricator-remarkup-embed-layout-download', - 'href' => $this->getFileDownloadURI(), - ), - pht('Download')); + $download_uri = $this->getFileDownloadURI(); + if ($download_uri) { + $dl_icon = id(new PHUIIconView()) + ->setIcon('fa-download'); + + $download_link = phutil_tag( + 'a', + array( + 'class' => 'phabricator-remarkup-embed-layout-download', + 'href' => $download_uri, + ), + pht('Download')); + } $info = phutil_tag( 'span', diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -405,8 +405,9 @@ 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 @@ .phabricator-remarkup-embed-layout-link:hover { border-color: {$violet}; cursor: pointer; +} + +.device-desktop .phabricator-remarkup-embed-layout-link:hover { text-decoration: none; } diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -248,3 +248,12 @@ .document-engine-remarkup { margin: 20px; } + +.document-engine-pdf { + margin: 20px; + text-align: center; +} + +.document-engine-pdf .phabricator-remarkup-embed-layout-link { + text-align: left; +}