diff --git a/src/applications/files/favicon/PhabricatorFaviconRef.php b/src/applications/files/favicon/PhabricatorFaviconRef.php index d6c2fd80a8..08ade5283f 100644 --- a/src/applications/files/favicon/PhabricatorFaviconRef.php +++ b/src/applications/files/favicon/PhabricatorFaviconRef.php @@ -1,447 +1,454 @@ emblems = array(null, null, null, null); } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setWidth($width) { $this->width = $width; return $this; } public function getWidth() { return $this->width; } public function setHeight($height) { $this->height = $height; return $this; } public function getHeight() { return $this->height; } public function setEmblems(array $emblems) { if (count($emblems) !== 4) { throw new Exception( pht( 'Expected four elements in icon emblem list. To omit an emblem, '. 'pass "null".')); } $this->emblems = $emblems; return $this; } public function getEmblems() { return $this->emblems; } public function setURI($uri) { $this->uri = $uri; return $this; } public function getURI() { return $this->uri; } public function setCacheKey($cache_key) { $this->cacheKey = $cache_key; return $this; } public function getCacheKey() { return $this->cacheKey; } public function newDigest() { return PhabricatorHash::digestForIndex(serialize($this->toDictionary())); } public function toDictionary() { return array( 'width' => $this->width, 'height' => $this->height, 'emblems' => $this->emblems, ); } public static function newConfigurationDigest() { $all_resources = self::getAllResources(); // Because we need to access this cache on every page, it's very sticky. // Try to dirty it automatically if any relevant configuration changes. $inputs = array( 'resources' => $all_resources, 'prod' => PhabricatorEnv::getProductionURI('/'), 'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'), 'havepng' => function_exists('imagepng'), ); return PhabricatorHash::digestForIndex(serialize($inputs)); } private static function getAllResources() { $custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons'); foreach ($custom_resources as $key => $custom_resource) { $custom_resources[$key] = array( 'source-type' => 'file', 'default' => false, ) + $custom_resource; } $builtin_resources = self::getBuiltinResources(); return array_merge($builtin_resources, $custom_resources); } private static function getBuiltinResources() { return array( array( 'source-type' => 'builtin', 'source' => 'favicon/default-76x76.png', 'version' => 1, 'width' => 76, 'height' => 76, 'default' => true, ), array( 'source-type' => 'builtin', 'source' => 'favicon/default-120x120.png', 'version' => 1, 'width' => 120, 'height' => 120, 'default' => true, ), array( 'source-type' => 'builtin', 'source' => 'favicon/default-128x128.png', 'version' => 1, 'width' => 128, 'height' => 128, 'default' => true, ), array( 'source-type' => 'builtin', 'source' => 'favicon/default-152x152.png', 'version' => 1, 'width' => 152, 'height' => 152, 'default' => true, ), array( 'source-type' => 'builtin', 'source' => 'favicon/dot-pink-64x64.png', 'version' => 1, 'width' => 64, 'height' => 64, 'emblem' => 'dot-pink', 'default' => true, ), array( 'source-type' => 'builtin', 'source' => 'favicon/dot-red-64x64.png', 'version' => 1, 'width' => 64, 'height' => 64, 'emblem' => 'dot-red', 'default' => true, ), ); } public function newURI() { $dst_w = $this->getWidth(); $dst_h = $this->getHeight(); $template = $this->newTemplateFile(null, $dst_w, $dst_h); $template_file = $template['file']; $cache = $this->loadCachedFile($template_file); if ($cache) { return $cache->getViewURI(); } $data = $this->newCompositedFavicon($template); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $caught = null; try { $favicon_file = $this->newFaviconFile($data); $xform = id(new PhabricatorTransformedFile()) ->setOriginalPHID($template_file->getPHID()) ->setTransformedPHID($favicon_file->getPHID()) ->setTransform($this->getCacheKey()); try { $xform->save(); } catch (AphrontDuplicateKeyQueryException $ex) { unset($unguarded); $cache = $this->loadCachedFile($template_file); if (!$cache) { throw $ex; } id(new PhabricatorDestructionEngine()) ->destroyObject($favicon_file); return $cache->getViewURI(); } } catch (Exception $ex) { $caught = $ex; } unset($unguarded); if ($caught) { throw $caught; } return $favicon_file->getViewURI(); } private function loadCachedFile(PhabricatorFile $template_file) { $viewer = $this->getViewer(); $xform = id(new PhabricatorTransformedFile())->loadOneWhere( 'originalPHID = %s AND transform = %s', $template_file->getPHID(), $this->getCacheKey()); if (!$xform) { return null; } return id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($xform->getTransformedPHID())) ->executeOne(); } private function newCompositedFavicon($template) { $dst_w = $this->getWidth(); $dst_h = $this->getHeight(); $src_w = $template['width']; $src_h = $template['height']; - $template_data = $template['file']->loadFileData(); + try { + $template_data = $template['file']->loadFileData(); + } catch (Exception $ex) { + // In rare cases, we can end up with a corrupted or inaccessible file. + // If we do, just give up: otherwise, it's impossible to get pages to + // generate and not obvious how to fix it. + return null; + } if (!function_exists('imagecreatefromstring')) { return $template_data; } $src = @imagecreatefromstring($template_data); if (!$src) { return $template_data; } $dst = imagecreatetruecolor($dst_w, $dst_h); imagesavealpha($dst, true); $transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127); imagefill($dst, 0, 0, $transparent); imagecopyresampled( $dst, $src, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h); // Now, copy any icon emblems on top of the image. These are dots or other // marks used to indicate status information. $emblem_w = (int)floor(min($dst_w, $dst_h) / 2); $emblem_h = $emblem_w; foreach ($this->emblems as $key => $emblem) { if ($emblem === null) { continue; } $emblem_template = $this->newTemplateFile( $emblem, $emblem_w, $emblem_h); switch ($key) { case 0: $emblem_x = $dst_w - $emblem_w; $emblem_y = 0; break; case 1: $emblem_x = $dst_w - $emblem_w; $emblem_y = $dst_h - $emblem_h; break; case 2: $emblem_x = 0; $emblem_y = $dst_h - $emblem_h; break; case 3: $emblem_x = 0; $emblem_y = 0; break; } $emblem_data = $emblem_template['file']->loadFileData(); $src = @imagecreatefromstring($emblem_data); if (!$src) { continue; } imagecopyresampled( $dst, $src, $emblem_x, $emblem_y, 0, 0, $emblem_w, $emblem_h, $emblem_template['width'], $emblem_template['height']); } return PhabricatorImageTransformer::saveImageDataInAnyFormat( $dst, 'image/png'); } private function newTemplateFile($emblem, $width, $height) { $all_resources = self::getAllResources(); $scores = array(); $ratio = $width / $height; foreach ($all_resources as $key => $resource) { // We can't use an emblem resource for a different emblem, nor for an // icon base. We also can't use an icon base as an emblem. That is, if // we're looking for a picture of a red dot, we have to actually find // a red dot, not just any image which happens to have a similar size. if (idx($resource, 'emblem') !== $emblem) { continue; } $resource_width = $resource['width']; $resource_height = $resource['height']; // Never use a resource with a different aspect ratio. if (($resource_width / $resource_height) !== $ratio) { continue; } // Try to use custom resources instead of default resources. if ($resource['default']) { $default_score = 1; } else { $default_score = 0; } $width_diff = ($resource_width - $width); // If we have to resize an image, we'd rather scale a larger image down // than scale a smaller image up. if ($width_diff < 0) { $scale_score = 1; } else { $scale_score = 0; } // Otherwise, we'd rather scale an image a little bit (ideally, zero) // than scale an image a lot. $width_score = abs($width_diff); $scores[$key] = id(new PhutilSortVector()) ->addInt($default_score) ->addInt($scale_score) ->addInt($width_score); } if (!$scores) { if ($emblem === null) { throw new Exception( pht( 'Found no background template resource for dimensions %dx%d.', $width, $height)); } else { throw new Exception( pht( 'Found no template resource (for emblem "%s") with dimensions '. '%dx%d.', $emblem, $width, $height)); } } $scores = msortv($scores, 'getSelf'); $best_score = head_key($scores); $viewer = $this->getViewer(); $resource = $all_resources[$best_score]; if ($resource['source-type'] === 'builtin') { $file = PhabricatorFile::loadBuiltin($viewer, $resource['source']); if (!$file) { throw new Exception( pht( 'Failed to load favicon template builtin "%s".', $resource['source'])); } } else { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($resource['source'])) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load favicon template with PHID "%s".', $resource['source'])); } } return array( 'width' => $resource['width'], 'height' => $resource['height'], 'file' => $file, ); } private function newFaviconFile($data) { return PhabricatorFile::newFromFileData( $data, array( 'name' => 'favicon', 'canCDN' => true, )); } }