Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14416750
D12811.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
13 KB
Referenced Files
None
Subscribers
None
D12811.diff
View Options
diff --git a/src/applications/files/controller/PhabricatorFileTransformController.php b/src/applications/files/controller/PhabricatorFileTransformController.php
--- a/src/applications/files/controller/PhabricatorFileTransformController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformController.php
@@ -13,6 +13,8 @@
// NOTE: This is a public/CDN endpoint, and permission to see files is
// controlled by knowing the secret key, not by authentication.
+ $is_regenerate = $request->getBool('regenerate');
+
$source_phid = $request->getURIData('phid');
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
@@ -35,7 +37,11 @@
$transform);
if ($xform) {
- return $this->buildTransformedFileResponse($xform);
+ if ($is_regenerate) {
+ $this->destroyTransform($xform);
+ } else {
+ return $this->buildTransformedFileResponse($xform);
+ }
}
$type = $file->getMimeType();
@@ -57,10 +63,12 @@
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.
+ // default transform below. If we're explicitly regenerating the
+ // thumbnail, rethrow the exception.
+ if ($is_regenerate) {
+ throw $ex;
+ }
}
}
@@ -165,4 +173,22 @@
return $xformer->executeThumbTransform($file, $x, $y);
}
+ private function destroyTransform(PhabricatorTransformedFile $xform) {
+ $file = id(new PhabricatorFileQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($xform->getTransformedPHID()))
+ ->executeOne();
+
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+
+ if (!$file) {
+ $xform->delete();
+ } else {
+ $engine = new PhabricatorDestructionEngine();
+ $engine->destroyObject($file);
+ }
+
+ unset($unguarded);
+ }
+
}
diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php
--- a/src/applications/files/controller/PhabricatorFileTransformListController.php
+++ b/src/applications/files/controller/PhabricatorFileTransformListController.php
@@ -58,12 +58,13 @@
if ($xform->canApplyTransform($file)) {
$can_apply = pht('Yes');
+
$view_href = $file->getURIForTransform($xform);
- if ($dst_phid) {
- $view_text = pht('View Transform');
- } else {
- $view_text = pht('Generate Transform');
- }
+ $view_href = new PhutilURI($view_href);
+ $view_href->setQueryParam('regenerate', 'true');
+
+ $view_text = pht('Regenerate');
+
$view_link = phutil_tag(
'a',
array(
diff --git a/src/applications/files/transform/PhabricatorFileImageTransform.php b/src/applications/files/transform/PhabricatorFileImageTransform.php
--- a/src/applications/files/transform/PhabricatorFileImageTransform.php
+++ b/src/applications/files/transform/PhabricatorFileImageTransform.php
@@ -2,6 +2,12 @@
abstract class PhabricatorFileImageTransform extends PhabricatorFileTransform {
+ private $file;
+ private $data;
+ private $image;
+ private $imageX;
+ private $imageY;
+
public function canApplyTransform(PhabricatorFile $file) {
if (!$file->isViewableImage()) {
return false;
@@ -14,4 +20,282 @@
return true;
}
+ protected function willTransformFile(PhabricatorFile $file) {
+ $this->file = $file;
+ $this->data = null;
+ $this->image = null;
+ $this->imageX = null;
+ $this->imageY = null;
+ }
+
+ protected function applyCropAndScale(
+ $dst_w, $dst_h,
+ $src_x, $src_y,
+ $src_w, $src_h) {
+
+ // Figure out the effective destination width, height, and offsets. We
+ // never want to scale images up, so if we're copying a very small source
+ // image we're just going to center it in the destination image.
+ $cpy_w = min($dst_w, $src_w);
+ $cpy_h = min($dst_h, $src_h);
+ $off_x = ($dst_w - $cpy_w) / 2;
+ $off_y = ($dst_h - $cpy_h) / 2;
+
+ // TODO: Support imagemagick for animated GIFs.
+
+ $src = $this->getImage();
+ $dst = $this->newEmptyImage($dst_w, $dst_h);
+
+ $trap = new PhutilErrorTrap();
+ $ok = @imagecopyresampled(
+ $dst,
+ $src,
+ $off_x, $off_y,
+ $src_x, $src_y,
+ $cpy_w, $cpy_h,
+ $src_w, $src_h);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+
+ if ($ok === false) {
+ throw new Exception(
+ pht(
+ 'Failed to imagecopyresampled() image: %s',
+ $errors));
+ }
+
+ $data = PhabricatorImageTransformer::saveImageDataInAnyFormat(
+ $dst,
+ $this->file->getMimeType());
+
+ return $this->newFileFromData($data);
+ }
+
+
+ /**
+ * Create a new @{class:PhabricatorFile} from raw data.
+ *
+ * @param string Raw file data.
+ */
+ protected function newFileFromData($data) {
+ $name = $this->getTransformKey().'-'.$this->file->getName();
+
+ return PhabricatorFile::newFromFileData(
+ $data,
+ array(
+ 'name' => $name,
+ 'canCDN' => true,
+ ));
+ }
+
+
+ /**
+ * Create a new image filled with transparent pixels.
+ *
+ * @param int Desired image width.
+ * @param int Desired image height.
+ * @return resource New image resource.
+ */
+ protected function newEmptyImage($w, $h) {
+ $w = (int)$w;
+ $h = (int)$h;
+
+ if (($w <= 0) || ($h <= 0)) {
+ throw new Exception(
+ pht('Can not create an image with nonpositive dimensions.'));
+ }
+
+ $trap = new PhutilErrorTrap();
+ $img = @imagecreatetruecolor($w, $h);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+ if ($img === false) {
+ throw new Exception(
+ pht(
+ 'Unable to imagecreatetruecolor() a new empty image: %s',
+ $errors));
+ }
+
+ $trap = new PhutilErrorTrap();
+ $ok = @imagesavealpha($img, true);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+ if ($ok === false) {
+ throw new Exception(
+ pht(
+ 'Unable to imagesavealpha() a new empty image: %s',
+ $errors));
+ }
+
+ $trap = new PhutilErrorTrap();
+ $color = @imagecolorallocatealpha($img, 255, 255, 255, 127);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+ if ($color === false) {
+ throw new Exception(
+ pht(
+ 'Unable to imagecolorallocatealpha() a new empty image: %s',
+ $errors));
+ }
+
+ $trap = new PhutilErrorTrap();
+ $ok = @imagefill($img, 0, 0, $color);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+ if ($ok === false) {
+ throw new Exception(
+ pht(
+ 'Unable to imagefill() a new empty image: %s',
+ $errors));
+ }
+
+ return $img;
+ }
+
+
+ /**
+ * Get the pixel dimensions of the image being transformed.
+ *
+ * @return list<int, int> Width and height of the image.
+ */
+ protected function getImageDimensions() {
+ if ($this->imageX === null) {
+ $image = $this->getImage();
+
+ $trap = new PhutilErrorTrap();
+ $x = @imagesx($image);
+ $y = @imagesy($image);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+
+ if (($x === false) || ($y === false) || ($x <= 0) || ($y <= 0)) {
+ throw new Exception(
+ pht(
+ 'Unable to determine image dimensions with '.
+ 'imagesx()/imagesy(): %s',
+ $errors));
+ }
+
+ $this->imageX = $x;
+ $this->imageY = $y;
+ }
+
+ return array($this->imageX, $this->imageY);
+ }
+
+
+ /**
+ * Get the raw file data for the image being transformed.
+ *
+ * @return string Raw file data.
+ */
+ protected function getData() {
+ if ($this->data !== null) {
+ return $this->data;
+ }
+
+ $file = $this->file;
+
+ $max_size = (1024 * 1024 * 4);
+ $img_size = $file->getByteSize();
+ if ($img_size > $max_size) {
+ throw new Exception(
+ pht(
+ 'This image is too large to transform. The transform limit is %s '.
+ 'bytes, but the image size is %s bytes.',
+ new PhutilNumber($max_size),
+ new PhutilNumber($img_size)));
+ }
+
+ $data = $file->loadFileData();
+ $this->data = $data;
+ return $this->data;
+ }
+
+
+ /**
+ * Get the GD image resource for the image being transformed.
+ *
+ * @return resource GD image resource.
+ */
+ protected function getImage() {
+ if ($this->image !== null) {
+ return $this->image;
+ }
+
+ if (!function_exists('imagecreatefromstring')) {
+ throw new Exception(
+ pht(
+ 'Unable to transform image: the imagecreatefromstring() function '.
+ 'is not available. Install or enable the "gd" extension for PHP.'));
+ }
+
+ $data = $this->getData();
+ $data = (string)$data;
+
+ // First, we're going to write the file to disk and use getimagesize()
+ // to determine its dimensions without actually loading the pixel data
+ // into memory. For very large images, we'll bail out.
+
+ // In particular, this defuses a resource exhaustion attack where the
+ // attacker uploads a 40,000 x 40,000 pixel PNGs of solid white. These
+ // kinds of files compress extremely well, but require a huge amount
+ // of memory and CPU to process.
+
+ $tmp = new TempFile();
+ Filesystem::writeFile($tmp, $data);
+ $tmp_path = (string)$tmp;
+
+ $trap = new PhutilErrorTrap();
+ $info = @getimagesize($tmp_path);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+
+ unset($tmp);
+
+ if ($info === false) {
+ throw new Exception(
+ pht(
+ 'Unable to get image information with getimagesize(): %s',
+ $errors));
+ }
+
+ list($width, $height) = $info;
+ if (($width <= 0) || ($height <= 0)) {
+ throw new Exception(
+ pht(
+ 'Unable to determine image width and height with getimagesize().'));
+ }
+
+ $max_pixels = (4096 * 4096);
+ $img_pixels = ($width * $height);
+
+ if ($img_pixels > $max_pixels) {
+ throw new Exception(
+ pht(
+ 'This image (with dimensions %spx x %spx) is too large to '.
+ 'transform. The image has %s pixels, but transforms are limited '.
+ 'to images with %s or fewer pixels.',
+ new PhutilNumber($width),
+ new PhutilNumber($height),
+ new PhutilNumber($img_pixels),
+ new PhutilNumber($max_pixels)));
+ }
+
+ $trap = new PhutilErrorTrap();
+ $image = @imagecreatefromstring($data);
+ $errors = $trap->getErrorsAsString();
+ $trap->destroy();
+
+ if ($image === false) {
+ throw new Exception(
+ pht(
+ 'Unable to load image data with imagecreatefromstring(): %s',
+ $errors));
+ }
+
+ $this->image = $image;
+ return $this->image;
+ }
+
}
diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php
--- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php
+++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php
@@ -59,16 +59,41 @@
}
public function applyTransform(PhabricatorFile $file) {
- $x = $this->dstX;
- $y = $this->dstY;
-
$xformer = new PhabricatorImageTransformer();
+ if ($this->dstY === null) {
+ return $xformer->executePreviewTransform($file, $this->dstX);
+ }
+
+ $this->willTransformFile($file);
+
+ list($src_x, $src_y) = $this->getImageDimensions();
+ $dst_x = $this->dstX;
+ $dst_y = $this->dstY;
- if ($y === null) {
- return $xformer->executePreviewTransform($file, $x);
+ // Figure out how much we'd have to scale the image down along each
+ // dimension to get the entire thing to fit.
+ $scale_x = min(($dst_x / $src_x), 1);
+ $scale_y = min(($dst_y / $src_y), 1);
+
+ if ($scale_x > $scale_y) {
+ // This image is relatively tall and narrow. We're going to crop off the
+ // top and bottom.
+ $copy_x = $src_x;
+ $copy_y = min($src_y, $dst_y / $scale_x);
} else {
- return $xformer->executeThumbTransform($file, $x, $y);
+ // This image is relatively short and wide. We're going to crop off the
+ // left and right.
+ $copy_x = min($src_x, $dst_x / $scale_y);
+ $copy_y = $src_y;
}
+
+ return $this->applyCropAndScale(
+ $dst_x,
+ $dst_y,
+ ($src_x - $copy_x) / 2,
+ ($src_y - $copy_y) / 2,
+ $copy_x,
+ $copy_y);
}
public function getDefaultTransform(PhabricatorFile $file) {
@@ -76,15 +101,10 @@
$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);
+ return $this->newFileFromData($data);
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Dec 25, 11:00 PM (11 h, 8 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6927856
Default Alt Text
D12811.diff (13 KB)
Attached To
Mode
D12811: Implement new profile transform with amazing "error handling" feature
Attached
Detach File
Event Timeline
Log In to Comment