@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

When proxying an "{image ...}" image fails, show the user an error message

Summary:
Depends on D19192. Ref T4190. Ref T13101. Instead of directly including the proxy endpoint with `<img src="..." />`, emit a placeholder and use AJAX to make the request. If the proxy fetch fails, replace the placeholder with an error message.

This isn't the most polished implementation imaginable, but it's much less mysterious about errors.

Test Plan: Used `{image ...}` for valid and invalid images, got images and useful error messages respectively.

Maniphest Tasks: T13101, T4190

Differential Revision: https://secure.phabricator.com/D19193

+192 -37
+9 -3
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => 'e68cf1fa', 11 11 'conpherence.pkg.js' => '15191c65', 12 - 'core.pkg.css' => '2fa91e14', 12 + 'core.pkg.css' => 'c218ed53', 13 13 'core.pkg.js' => '32bb68e9', 14 14 'darkconsole.pkg.js' => '1f9a31bc', 15 15 'differential.pkg.css' => '113e692c', ··· 114 114 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 115 115 'rsrc/css/application/uiexample/example.css' => '528b19de', 116 116 'rsrc/css/core/core.css' => '62fa3ace', 117 - 'rsrc/css/core/remarkup.css' => 'cad18339', 117 + 'rsrc/css/core/remarkup.css' => '97dc3523', 118 118 'rsrc/css/core/syntax.css' => 'cae95e89', 119 119 'rsrc/css/core/z-index.css' => '9d8f7c4b', 120 120 'rsrc/css/diviner/diviner-shared.css' => '896f1d43', ··· 504 504 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 505 505 'rsrc/js/core/behavior-redirect.js' => '0213259f', 506 506 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', 507 + 'rsrc/js/core/behavior-remarkup-load-image.js' => '040fce04', 507 508 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e', 508 509 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 509 510 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', ··· 692 693 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', 693 694 'javelin-behavior-releeph-request-state-change' => 'a0b57eb8', 694 695 'javelin-behavior-releeph-request-typeahead' => 'de2e896f', 696 + 'javelin-behavior-remarkup-load-image' => '040fce04', 695 697 'javelin-behavior-remarkup-preview' => '4b700e9e', 696 698 'javelin-behavior-reorder-applications' => '76b9fc3e', 697 699 'javelin-behavior-reorder-columns' => 'e1d25dfb', ··· 800 802 'phabricator-object-selector-css' => '85ee8ce6', 801 803 'phabricator-phtize' => 'd254d646', 802 804 'phabricator-prefab' => '77b0ae28', 803 - 'phabricator-remarkup-css' => 'cad18339', 805 + 'phabricator-remarkup-css' => '97dc3523', 804 806 'phabricator-search-results-css' => '505dd8cf', 805 807 'phabricator-shaped-request' => '7cbe244b', 806 808 'phabricator-slowvote-css' => 'a94b7230', ··· 939 941 '0213259f' => array( 940 942 'javelin-behavior', 941 943 'javelin-uri', 944 + ), 945 + '040fce04' => array( 946 + 'javelin-behavior', 947 + 'javelin-request', 942 948 ), 943 949 '04b2ae03' => array( 944 950 'javelin-install',
+2
src/__phutil_library_map__.php
··· 1883 1883 'PHUIPropertyGroupView' => 'view/phui/PHUIPropertyGroupView.php', 1884 1884 'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php', 1885 1885 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 1886 + 'PHUIRemarkupImageView' => 'infrastructure/markup/view/PHUIRemarkupImageView.php', 1886 1887 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 1887 1888 'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php', 1888 1889 'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php', ··· 7287 7288 'PHUIPropertyGroupView' => 'AphrontTagView', 7288 7289 'PHUIPropertyListExample' => 'PhabricatorUIExample', 7289 7290 'PHUIPropertyListView' => 'AphrontView', 7291 + 'PHUIRemarkupImageView' => 'AphrontView', 7290 7292 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 7291 7293 'PHUIRemarkupView' => 'AphrontView', 7292 7294 'PHUISegmentBarSegmentView' => 'AphrontTagView',
+57 -26
src/applications/files/controller/PhabricatorFileImageProxyController.php
··· 44 44 ->setTTL($ttl); 45 45 46 46 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 47 + $save_request = false; 47 48 // Cache missed so we'll need to validate and download the image 48 49 try { 49 50 // Rate limit outbound fetches to make this mechanism less useful for ··· 75 76 $file->save(); 76 77 } 77 78 78 - $external_request->setIsSuccessful(true) 79 - ->setFilePHID($file->getPHID()) 80 - ->save(); 81 - unset($unguarded); 82 - return $this->getExternalResponse($external_request); 79 + $external_request 80 + ->setIsSuccessful(1) 81 + ->setFilePHID($file->getPHID()); 82 + 83 + $save_request = true; 83 84 } catch (HTTPFutureHTTPResponseStatus $status) { 84 - $external_request->setIsSuccessful(false) 85 + $external_request 86 + ->setIsSuccessful(0) 85 87 ->setResponseMessage($status->getMessage()) 86 88 ->save(); 87 - return $this->getExternalResponse($external_request); 89 + 90 + $save_request = true; 88 91 } catch (Exception $ex) { 89 92 // Not actually saving the request in this case 90 93 $external_request->setResponseMessage($ex->getMessage()); 91 - return $this->getExternalResponse($external_request); 92 94 } 95 + 96 + if ($save_request) { 97 + try { 98 + $external_request->save(); 99 + } catch (AphrontDuplicateKeyQueryException $ex) { 100 + // We may have raced against another identical request. If we did, 101 + // just throw our result away and use the winner's result. 102 + $external_request = $external_request->loadOneWhere( 103 + 'uriIndex = %s', 104 + PhabricatorHash::digestForIndex($img_uri)); 105 + if (!$external_request) { 106 + throw new Exception( 107 + pht( 108 + 'Hit duplicate key collision when saving proxied image, but '. 109 + 'failed to load duplicate row (for URI "%s").', 110 + $img_uri)); 111 + } 112 + } 113 + } 114 + 115 + unset($unguarded); 116 + 117 + 118 + return $this->getExternalResponse($external_request); 93 119 } 94 120 95 121 private function getExternalResponse( 96 122 PhabricatorFileExternalRequest $request) { 97 - if ($request->getIsSuccessful()) { 98 - $file = id(new PhabricatorFileQuery()) 99 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 100 - ->withPHIDs(array($request->getFilePHID())) 101 - ->executeOne(); 102 - if (!$file) { 103 - throw new Exception(pht( 123 + if (!$request->getIsSuccessful()) { 124 + throw new Exception( 125 + pht( 126 + 'Request to "%s" failed: %s', 127 + $request->getURI(), 128 + $request->getResponseMessage())); 129 + } 130 + 131 + $file = id(new PhabricatorFileQuery()) 132 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 133 + ->withPHIDs(array($request->getFilePHID())) 134 + ->executeOne(); 135 + if (!$file) { 136 + throw new Exception( 137 + pht( 104 138 'The underlying file does not exist, but the cached request was '. 105 - 'successful. This likely means the file record was manually deleted '. 106 - 'by an administrator.')); 107 - } 108 - return id(new AphrontRedirectResponse()) 109 - ->setIsExternal(true) 110 - ->setURI($file->getViewURI()); 111 - } else { 112 - throw new Exception(pht( 113 - "The request to get the external file from '%s' was unsuccessful:\n %s", 114 - $request->getURI(), 115 - $request->getResponseMessage())); 139 + 'successful. This likely means the file record was manually '. 140 + 'deleted by an administrator.')); 116 141 } 142 + 143 + return id(new AphrontAjaxResponse()) 144 + ->setContent( 145 + array( 146 + 'imageURI' => $file->getViewURI(), 147 + )); 117 148 } 118 149 }
+5 -8
src/applications/files/markup/PhabricatorImageRemarkupRule.php
··· 100 100 $src_uri = id(new PhutilURI('/file/imageproxy/')) 101 101 ->setQueryParam('uri', $args['uri']); 102 102 103 - $img = $this->newTag( 104 - 'img', 105 - array( 106 - 'src' => $src_uri, 107 - 'alt' => $args['alt'], 108 - 'width' => $args['width'], 109 - 'height' => $args['height'], 110 - )); 103 + $img = id(new PHUIRemarkupImageView()) 104 + ->setURI($src_uri) 105 + ->setAlt($args['alt']) 106 + ->setWidth($args['width']) 107 + ->setHeight($args['height']); 111 108 112 109 $engine->overwriteStoredText($image['token'], $img); 113 110 }
+67
src/infrastructure/markup/view/PHUIRemarkupImageView.php
··· 1 + <?php 2 + 3 + final class PHUIRemarkupImageView 4 + extends AphrontView { 5 + 6 + private $uri; 7 + private $width; 8 + private $height; 9 + private $alt; 10 + 11 + public function setURI($uri) { 12 + $this->uri = $uri; 13 + return $this; 14 + } 15 + 16 + public function getURI() { 17 + return $this->uri; 18 + } 19 + 20 + public function setWidth($width) { 21 + $this->width = $width; 22 + return $this; 23 + } 24 + 25 + public function getWidth() { 26 + return $this->width; 27 + } 28 + 29 + public function setHeight($height) { 30 + $this->height = $height; 31 + return $this; 32 + } 33 + 34 + public function getHeight() { 35 + return $this->height; 36 + } 37 + 38 + public function setAlt($alt) { 39 + $this->alt = $alt; 40 + return $this; 41 + } 42 + 43 + public function getAlt() { 44 + return $this->alt; 45 + } 46 + 47 + public function render() { 48 + $id = celerity_generate_unique_node_id(); 49 + 50 + Javelin::initBehavior( 51 + 'remarkup-load-image', 52 + array( 53 + 'uri' => (string)$this->uri, 54 + 'imageID' => $id, 55 + )); 56 + 57 + return phutil_tag( 58 + 'img', 59 + array( 60 + 'id' => $id, 61 + 'width' => $this->getWidth(), 62 + 'height' => $this->getHeight(), 63 + 'alt' => $this->getAlt(), 64 + )); 65 + } 66 + 67 + }
+7
webroot/rsrc/css/core/remarkup.css
··· 472 472 margin: .5em 1em 0; 473 473 } 474 474 475 + .phabricator-remarkup-image-error { 476 + border: 1px solid {$redborder}; 477 + background: {$sh-redbackground}; 478 + padding: 8px 12px; 479 + color: {$darkgreytext}; 480 + } 481 + 475 482 .phabricator-remarkup-embed-image { 476 483 display: inline-block; 477 484 border: 3px solid white;
+45
webroot/rsrc/js/core/behavior-remarkup-load-image.js
··· 1 + /** 2 + * @provides javelin-behavior-remarkup-load-image 3 + * @requires javelin-behavior 4 + * javelin-request 5 + */ 6 + 7 + JX.behavior('remarkup-load-image', function(config) { 8 + 9 + function get_node() { 10 + try { 11 + return JX.$(config.imageID); 12 + } catch (ex) { 13 + return null; 14 + } 15 + } 16 + 17 + function onload(r) { 18 + var node = get_node(); 19 + if (!node) { 20 + return; 21 + } 22 + 23 + node.src = r.imageURI; 24 + } 25 + 26 + function onerror(r) { 27 + var node = get_node(); 28 + if (!node) { 29 + return; 30 + } 31 + 32 + var error = JX.$N( 33 + 'div', 34 + { 35 + className: 'phabricator-remarkup-image-error' 36 + }, 37 + r.info); 38 + 39 + JX.DOM.replace(node, error); 40 + } 41 + 42 + var request = new JX.Request(config.uri, onload); 43 + request.listen('error', onerror); 44 + request.send(); 45 + });