Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / files / favicon / PhabricatorFaviconRef.php
blob08ade5283fdb3685a654c5183bd06377be18acf7
1 <?php
3 final class PhabricatorFaviconRef extends Phobject {
5 private $viewer;
6 private $width;
7 private $height;
8 private $emblems;
9 private $uri;
10 private $cacheKey;
12 public function __construct() {
13 $this->emblems = array(null, null, null, null);
16 public function setViewer(PhabricatorUser $viewer) {
17 $this->viewer = $viewer;
18 return $this;
21 public function getViewer() {
22 return $this->viewer;
25 public function setWidth($width) {
26 $this->width = $width;
27 return $this;
30 public function getWidth() {
31 return $this->width;
34 public function setHeight($height) {
35 $this->height = $height;
36 return $this;
39 public function getHeight() {
40 return $this->height;
43 public function setEmblems(array $emblems) {
44 if (count($emblems) !== 4) {
45 throw new Exception(
46 pht(
47 'Expected four elements in icon emblem list. To omit an emblem, '.
48 'pass "null".'));
51 $this->emblems = $emblems;
52 return $this;
55 public function getEmblems() {
56 return $this->emblems;
59 public function setURI($uri) {
60 $this->uri = $uri;
61 return $this;
64 public function getURI() {
65 return $this->uri;
68 public function setCacheKey($cache_key) {
69 $this->cacheKey = $cache_key;
70 return $this;
73 public function getCacheKey() {
74 return $this->cacheKey;
77 public function newDigest() {
78 return PhabricatorHash::digestForIndex(serialize($this->toDictionary()));
81 public function toDictionary() {
82 return array(
83 'width' => $this->width,
84 'height' => $this->height,
85 'emblems' => $this->emblems,
89 public static function newConfigurationDigest() {
90 $all_resources = self::getAllResources();
92 // Because we need to access this cache on every page, it's very sticky.
93 // Try to dirty it automatically if any relevant configuration changes.
94 $inputs = array(
95 'resources' => $all_resources,
96 'prod' => PhabricatorEnv::getProductionURI('/'),
97 'cdn' => PhabricatorEnv::getEnvConfig('security.alternate-file-domain'),
98 'havepng' => function_exists('imagepng'),
101 return PhabricatorHash::digestForIndex(serialize($inputs));
104 private static function getAllResources() {
105 $custom_resources = PhabricatorEnv::getEnvConfig('ui.favicons');
107 foreach ($custom_resources as $key => $custom_resource) {
108 $custom_resources[$key] = array(
109 'source-type' => 'file',
110 'default' => false,
111 ) + $custom_resource;
114 $builtin_resources = self::getBuiltinResources();
116 return array_merge($builtin_resources, $custom_resources);
119 private static function getBuiltinResources() {
120 return array(
121 array(
122 'source-type' => 'builtin',
123 'source' => 'favicon/default-76x76.png',
124 'version' => 1,
125 'width' => 76,
126 'height' => 76,
127 'default' => true,
129 array(
130 'source-type' => 'builtin',
131 'source' => 'favicon/default-120x120.png',
132 'version' => 1,
133 'width' => 120,
134 'height' => 120,
135 'default' => true,
137 array(
138 'source-type' => 'builtin',
139 'source' => 'favicon/default-128x128.png',
140 'version' => 1,
141 'width' => 128,
142 'height' => 128,
143 'default' => true,
145 array(
146 'source-type' => 'builtin',
147 'source' => 'favicon/default-152x152.png',
148 'version' => 1,
149 'width' => 152,
150 'height' => 152,
151 'default' => true,
153 array(
154 'source-type' => 'builtin',
155 'source' => 'favicon/dot-pink-64x64.png',
156 'version' => 1,
157 'width' => 64,
158 'height' => 64,
159 'emblem' => 'dot-pink',
160 'default' => true,
162 array(
163 'source-type' => 'builtin',
164 'source' => 'favicon/dot-red-64x64.png',
165 'version' => 1,
166 'width' => 64,
167 'height' => 64,
168 'emblem' => 'dot-red',
169 'default' => true,
174 public function newURI() {
175 $dst_w = $this->getWidth();
176 $dst_h = $this->getHeight();
178 $template = $this->newTemplateFile(null, $dst_w, $dst_h);
179 $template_file = $template['file'];
181 $cache = $this->loadCachedFile($template_file);
182 if ($cache) {
183 return $cache->getViewURI();
186 $data = $this->newCompositedFavicon($template);
188 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
190 $caught = null;
191 try {
192 $favicon_file = $this->newFaviconFile($data);
194 $xform = id(new PhabricatorTransformedFile())
195 ->setOriginalPHID($template_file->getPHID())
196 ->setTransformedPHID($favicon_file->getPHID())
197 ->setTransform($this->getCacheKey());
199 try {
200 $xform->save();
201 } catch (AphrontDuplicateKeyQueryException $ex) {
202 unset($unguarded);
204 $cache = $this->loadCachedFile($template_file);
205 if (!$cache) {
206 throw $ex;
209 id(new PhabricatorDestructionEngine())
210 ->destroyObject($favicon_file);
212 return $cache->getViewURI();
214 } catch (Exception $ex) {
215 $caught = $ex;
218 unset($unguarded);
220 if ($caught) {
221 throw $caught;
224 return $favicon_file->getViewURI();
227 private function loadCachedFile(PhabricatorFile $template_file) {
228 $viewer = $this->getViewer();
230 $xform = id(new PhabricatorTransformedFile())->loadOneWhere(
231 'originalPHID = %s AND transform = %s',
232 $template_file->getPHID(),
233 $this->getCacheKey());
234 if (!$xform) {
235 return null;
238 return id(new PhabricatorFileQuery())
239 ->setViewer($viewer)
240 ->withPHIDs(array($xform->getTransformedPHID()))
241 ->executeOne();
244 private function newCompositedFavicon($template) {
245 $dst_w = $this->getWidth();
246 $dst_h = $this->getHeight();
247 $src_w = $template['width'];
248 $src_h = $template['height'];
250 try {
251 $template_data = $template['file']->loadFileData();
252 } catch (Exception $ex) {
253 // In rare cases, we can end up with a corrupted or inaccessible file.
254 // If we do, just give up: otherwise, it's impossible to get pages to
255 // generate and not obvious how to fix it.
256 return null;
259 if (!function_exists('imagecreatefromstring')) {
260 return $template_data;
263 $src = @imagecreatefromstring($template_data);
264 if (!$src) {
265 return $template_data;
268 $dst = imagecreatetruecolor($dst_w, $dst_h);
269 imagesavealpha($dst, true);
271 $transparent = imagecolorallocatealpha($dst, 0, 255, 0, 127);
272 imagefill($dst, 0, 0, $transparent);
274 imagecopyresampled(
275 $dst,
276 $src,
281 $dst_w,
282 $dst_h,
283 $src_w,
284 $src_h);
286 // Now, copy any icon emblems on top of the image. These are dots or other
287 // marks used to indicate status information.
288 $emblem_w = (int)floor(min($dst_w, $dst_h) / 2);
289 $emblem_h = $emblem_w;
290 foreach ($this->emblems as $key => $emblem) {
291 if ($emblem === null) {
292 continue;
295 $emblem_template = $this->newTemplateFile(
296 $emblem,
297 $emblem_w,
298 $emblem_h);
300 switch ($key) {
301 case 0:
302 $emblem_x = $dst_w - $emblem_w;
303 $emblem_y = 0;
304 break;
305 case 1:
306 $emblem_x = $dst_w - $emblem_w;
307 $emblem_y = $dst_h - $emblem_h;
308 break;
309 case 2:
310 $emblem_x = 0;
311 $emblem_y = $dst_h - $emblem_h;
312 break;
313 case 3:
314 $emblem_x = 0;
315 $emblem_y = 0;
316 break;
319 $emblem_data = $emblem_template['file']->loadFileData();
321 $src = @imagecreatefromstring($emblem_data);
322 if (!$src) {
323 continue;
326 imagecopyresampled(
327 $dst,
328 $src,
329 $emblem_x,
330 $emblem_y,
333 $emblem_w,
334 $emblem_h,
335 $emblem_template['width'],
336 $emblem_template['height']);
339 return PhabricatorImageTransformer::saveImageDataInAnyFormat(
340 $dst,
341 'image/png');
344 private function newTemplateFile($emblem, $width, $height) {
345 $all_resources = self::getAllResources();
347 $scores = array();
348 $ratio = $width / $height;
349 foreach ($all_resources as $key => $resource) {
350 // We can't use an emblem resource for a different emblem, nor for an
351 // icon base. We also can't use an icon base as an emblem. That is, if
352 // we're looking for a picture of a red dot, we have to actually find
353 // a red dot, not just any image which happens to have a similar size.
354 if (idx($resource, 'emblem') !== $emblem) {
355 continue;
358 $resource_width = $resource['width'];
359 $resource_height = $resource['height'];
361 // Never use a resource with a different aspect ratio.
362 if (($resource_width / $resource_height) !== $ratio) {
363 continue;
366 // Try to use custom resources instead of default resources.
367 if ($resource['default']) {
368 $default_score = 1;
369 } else {
370 $default_score = 0;
373 $width_diff = ($resource_width - $width);
375 // If we have to resize an image, we'd rather scale a larger image down
376 // than scale a smaller image up.
377 if ($width_diff < 0) {
378 $scale_score = 1;
379 } else {
380 $scale_score = 0;
383 // Otherwise, we'd rather scale an image a little bit (ideally, zero)
384 // than scale an image a lot.
385 $width_score = abs($width_diff);
387 $scores[$key] = id(new PhutilSortVector())
388 ->addInt($default_score)
389 ->addInt($scale_score)
390 ->addInt($width_score);
393 if (!$scores) {
394 if ($emblem === null) {
395 throw new Exception(
396 pht(
397 'Found no background template resource for dimensions %dx%d.',
398 $width,
399 $height));
400 } else {
401 throw new Exception(
402 pht(
403 'Found no template resource (for emblem "%s") with dimensions '.
404 '%dx%d.',
405 $emblem,
406 $width,
407 $height));
411 $scores = msortv($scores, 'getSelf');
412 $best_score = head_key($scores);
414 $viewer = $this->getViewer();
416 $resource = $all_resources[$best_score];
417 if ($resource['source-type'] === 'builtin') {
418 $file = PhabricatorFile::loadBuiltin($viewer, $resource['source']);
419 if (!$file) {
420 throw new Exception(
421 pht(
422 'Failed to load favicon template builtin "%s".',
423 $resource['source']));
425 } else {
426 $file = id(new PhabricatorFileQuery())
427 ->setViewer($viewer)
428 ->withPHIDs(array($resource['source']))
429 ->executeOne();
430 if (!$file) {
431 throw new Exception(
432 pht(
433 'Failed to load favicon template with PHID "%s".',
434 $resource['source']));
438 return array(
439 'width' => $resource['width'],
440 'height' => $resource['height'],
441 'file' => $file,
445 private function newFaviconFile($data) {
446 return PhabricatorFile::newFromFileData(
447 $data,
448 array(
449 'name' => 'favicon',
450 'canCDN' => true,