@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
at upstream/main 147 lines 4.5 kB view raw
1<?php 2 3final class PhabricatorFileImageProxyController 4 extends PhabricatorFileController { 5 6 public function shouldAllowPublic() { 7 return true; 8 } 9 10 public function handleRequest(AphrontRequest $request) { 11 $viewer = $request->getViewer(); 12 $img_uri = $request->getStr('uri'); 13 14 // Validate the URI before doing anything 15 PhabricatorEnv::requireValidRemoteURIForLink($img_uri); 16 $uri = new PhutilURI($img_uri); 17 $proto = $uri->getProtocol(); 18 19 $allowed_protocols = array( 20 'http', 21 'https', 22 ); 23 if (!in_array($proto, $allowed_protocols)) { 24 throw new Exception( 25 pht( 26 'The provided image URI must use one of these protocols: %s.', 27 implode(', ', $allowed_protocols))); 28 } 29 30 // Check if we already have the specified image URI downloaded 31 $cached_request = id(new PhabricatorFileExternalRequest())->loadOneWhere( 32 'uriIndex = %s', 33 PhabricatorHash::digestForIndex($img_uri)); 34 35 if ($cached_request) { 36 return $this->getExternalResponse($cached_request); 37 } 38 39 $ttl = PhabricatorTime::getNow() + phutil_units('7 days in seconds'); 40 $external_request = id(new PhabricatorFileExternalRequest()) 41 ->setURI($img_uri) 42 ->setTTL($ttl); 43 44 // Cache missed, so we'll need to validate and download the image. 45 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 46 $save_request = false; 47 try { 48 // Rate limit outbound fetches to make this mechanism less useful for 49 // scanning networks and ports. 50 PhabricatorSystemActionEngine::willTakeAction( 51 array($viewer->getPHID()), 52 new PhabricatorFilesOutboundRequestAction(), 53 1); 54 55 $file = PhabricatorFile::newFromFileDownload( 56 $uri, 57 array( 58 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 59 'canCDN' => true, 60 )); 61 62 if (!$file->isViewableImage()) { 63 $mime_type = $file->getMimeType(); 64 $engine = new PhabricatorDestructionEngine(); 65 $engine->destroyObject($file); 66 $file = null; 67 throw new Exception( 68 pht( 69 'The URI "%s" does not correspond to a valid image file (got '. 70 'a file with MIME type "%s"). You must specify the URI of a '. 71 'valid image file.', 72 $uri, 73 $mime_type)); 74 } 75 76 $file->save(); 77 78 $external_request 79 ->setIsSuccessful(1) 80 ->setFilePHID($file->getPHID()); 81 82 $save_request = true; 83 } catch (HTTPFutureHTTPResponseStatus $status) { 84 $external_request 85 ->setIsSuccessful(0) 86 ->setResponseMessage($status->getMessage()); 87 88 $save_request = true; 89 } catch (Exception $ex) { 90 // Not actually saving the request in this case 91 $external_request->setResponseMessage($ex->getMessage()); 92 } 93 94 if ($save_request) { 95 try { 96 $external_request->save(); 97 } catch (AphrontDuplicateKeyQueryException $ex) { 98 // We may have raced against another identical request. If we did, 99 // just throw our result away and use the winner's result. 100 $external_request = $external_request->loadOneWhere( 101 'uriIndex = %s', 102 PhabricatorHash::digestForIndex($img_uri)); 103 if (!$external_request) { 104 throw new Exception( 105 pht( 106 'Hit duplicate key collision when saving proxied image, but '. 107 'failed to load duplicate row (for URI "%s").', 108 $img_uri)); 109 } 110 } 111 } 112 113 unset($unguarded); 114 115 116 return $this->getExternalResponse($external_request); 117 } 118 119 private function getExternalResponse( 120 PhabricatorFileExternalRequest $request) { 121 if (!$request->getIsSuccessful()) { 122 throw new Exception( 123 pht( 124 'Request to "%s" failed: %s', 125 $request->getURI(), 126 $request->getResponseMessage())); 127 } 128 129 $file = id(new PhabricatorFileQuery()) 130 ->setViewer(PhabricatorUser::getOmnipotentUser()) 131 ->withPHIDs(array($request->getFilePHID())) 132 ->executeOne(); 133 if (!$file) { 134 throw new Exception( 135 pht( 136 'The underlying file does not exist, but the cached request was '. 137 'successful. This likely means the file record was manually '. 138 'deleted by an administrator.')); 139 } 140 141 return id(new AphrontAjaxResponse()) 142 ->setContent( 143 array( 144 'imageURI' => $file->getViewURI(), 145 )); 146 } 147}