diff --git a/resources/builtin/image-100x100.png b/resources/builtin/image-100x100.png new file mode 100644 index 0000000000..c56a9aa083 Binary files /dev/null and b/resources/builtin/image-100x100.png differ diff --git a/resources/builtin/image-220x220.png b/resources/builtin/image-220x220.png new file mode 100644 index 0000000000..928b5b05eb Binary files /dev/null and b/resources/builtin/image-220x220.png differ diff --git a/resources/builtin/image-280x210.png b/resources/builtin/image-280x210.png new file mode 100644 index 0000000000..48237b045e Binary files /dev/null and b/resources/builtin/image-280x210.png differ diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php index 9b893b9528..a7fcbc3c0a 100644 --- a/src/applications/files/controller/PhabricatorFileTransformController.php +++ b/src/applications/files/controller/PhabricatorFileTransformController.php @@ -1,157 +1,168 @@ getViewer(); // NOTE: This is a public/CDN endpoint, and permission to see files is // controlled by knowing the secret key, not by authentication. $source_phid = $request->getURIData('phid'); $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($source_phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $secret_key = $request->getURIData('key'); if (!$file->validateSecretKey($secret_key)) { return new Aphront403Response(); } $transform = $request->getURIData('transform'); $xform = id(new PhabricatorTransformedFile()) ->loadOneWhere( 'originalPHID = %s AND transform = %s', $source_phid, $transform); if ($xform) { return $this->buildTransformedFileResponse($xform); } $type = $file->getMimeType(); if (!$file->isViewableInBrowser() || !$file->isTransformableImage()) { return $this->buildDefaultTransformation($file, $transform); } // We're essentially just building a cache here and don't need CSRF // protection. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $xformed_file = null; $xforms = PhabricatorFileTransform::getAllTransforms(); if (isset($xforms[$transform])) { $xform = $xforms[$transform]; if ($xform->canApplyTransform($file)) { - $xformed_file = $xforms[$transform]->applyTransform($file); + try { + $xformed_file = $xforms[$transform]->applyTransform($file); + } catch (Exception $ex) { + // TODO: Provide a diagnostic mode to surface these to the viewer. + + // In normal transform mode, we ignore failures and generate a + // default transform instead. + } + } + + if (!$xformed_file) { + $xformed_file = $xform->getDefaultTransform($file); } } if (!$xformed_file) { switch ($transform) { case 'thumb-profile': $xformed_file = $this->executeThumbTransform($file, 50, 50); break; case 'thumb-280x210': $xformed_file = $this->executeThumbTransform($file, 280, 210); break; case 'preview-100': $xformed_file = $this->executePreviewTransform($file, 100); break; case 'preview-220': $xformed_file = $this->executePreviewTransform($file, 220); break; default: return new Aphront400Response(); } } if (!$xformed_file) { return new Aphront400Response(); } $xform = id(new PhabricatorTransformedFile()) ->setOriginalPHID($source_phid) ->setTransform($transform) ->setTransformedPHID($xformed_file->getPHID()) ->save(); return $this->buildTransformedFileResponse($xform); } private function buildDefaultTransformation( PhabricatorFile $file, $transform) { static $regexps = array( '@application/zip@' => 'zip', '@image/@' => 'image', '@application/pdf@' => 'pdf', '@.*@' => 'default', ); $type = $file->getMimeType(); $prefix = 'default'; foreach ($regexps as $regexp => $implied_prefix) { if (preg_match($regexp, $type)) { $prefix = $implied_prefix; break; } } switch ($transform) { case 'thumb-280x210': $suffix = '280x210'; break; case 'preview-100': $suffix = '.p100'; break; default: throw new Exception('Unsupported transformation type!'); } $path = celerity_get_resource_uri( "rsrc/image/icon/fatcow/thumbnails/{$prefix}{$suffix}.png"); return id(new AphrontRedirectResponse()) ->setURI($path); } private function buildTransformedFileResponse( PhabricatorTransformedFile $xform) { $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($xform->getTransformedPHID())) ->executeOne(); if (!$file) { return new Aphront404Response(); } // TODO: We could just delegate to the file view controller instead, // which would save the client a roundtrip, but is slightly more complex. return $file->getRedirectResponse(); } private function executePreviewTransform(PhabricatorFile $file, $size) { $xformer = new PhabricatorImageTransformer(); return $xformer->executePreviewTransform($file, $size); } private function executeThumbTransform(PhabricatorFile $file, $x, $y) { $xformer = new PhabricatorImageTransformer(); return $xformer->executeThumbTransform($file, $x, $y); } } diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php index d88451a0de..75c16e2ddf 100644 --- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -1,74 +1,90 @@ name = $name; return $this; } public function setKey($key) { $this->key = $key; return $this; } public function setDimensions($x, $y) { $this->dstX = $x; $this->dstY = $y; return $this; } public function getTransformName() { return $this->name; } public function getTransformKey() { return $this->key; } public function generateTransforms() { return array( id(new PhabricatorFileThumbnailTransform()) ->setName(pht("Profile (100px \xC3\x97 100px)")) ->setKey(self::TRANSFORM_PROFILE) ->setDimensions(100, 100), id(new PhabricatorFileThumbnailTransform()) ->setName(pht("Pinboard (280px \xC3\x97 210px)")) ->setKey(self::TRANSFORM_PINBOARD) ->setDimensions(280, 210), id(new PhabricatorFileThumbnailTransform()) ->setName(pht('Thumbgrid (100px)')) ->setKey(self::TRANSFORM_THUMBGRID) ->setDimensions(100, null), id(new PhabricatorFileThumbnailTransform()) ->setName(pht('Preview (220px)')) ->setKey(self::TRANSFORM_PREVIEW) ->setDimensions(220, null), ); } public function applyTransform(PhabricatorFile $file) { $x = $this->dstX; $y = $this->dstY; $xformer = new PhabricatorImageTransformer(); if ($y === null) { return $xformer->executePreviewTransform($file, $x); } else { return $xformer->executeThumbTransform($file, $x, $y); } } + public function getDefaultTransform(PhabricatorFile $file) { + $x = (int)$this->dstX; + $y = (int)$this->dstY; + $name = 'image-'.$x.'x'.nonempty($y, $x).'.png'; + + $params = array( + 'name' => $name, + 'canCDN' => true, + ); + + $root = dirname(phutil_get_library_root('phabricator')); + $data = Filesystem::readFile($root.'/resources/builtin/'.$name); + + return PhabricatorFile::newFromFileData($data, $params); + } + } diff --git a/src/applications/files/transform/PhabricatorFileTransform.php b/src/applications/files/transform/PhabricatorFileTransform.php index 7a36641140..f12aefd3ed 100644 --- a/src/applications/files/transform/PhabricatorFileTransform.php +++ b/src/applications/files/transform/PhabricatorFileTransform.php @@ -1,44 +1,48 @@ setAncestorClass(__CLASS__) ->loadObjects(); $result = array(); foreach ($xforms as $xform_template) { foreach ($xform_template->generateTransforms() as $xform) { $key = $xform->getTransformKey(); if (isset($result[$key])) { throw new Exception( pht( 'Two %s objects define the same transform key ("%s"), but '. 'each transform must have a unique key.', __CLASS__, $key)); } $result[$key] = $xform; } } $map = $result; } return $map; } }