@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.

Endpoint+controller for a remarkup image proxy

Summary:
Ref T4190. Currently only have the endpoint and controller working. I added caching so subsequent attempts to proxy the same image should result in the same redirect URL. Still need to:

- Write a remarkup rule that uses the endpoint

Test Plan: Hit /file/imageproxy/?uri=http://i.imgur.com/nTvVrYN.jpg and are served the picture

Reviewers: epriestley, #blessed_reviewers

Reviewed By: epriestley, #blessed_reviewers

Subscribers: Korvin, epriestley, yelirekim

Maniphest Tasks: T4190

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

Josh Cox eea540c5 01afa791

+233 -1
+14
resources/sql/autopatches/20160921.fileexternalrequest.sql
··· 1 + CREATE TABLE {$NAMESPACE}_file.file_externalrequest ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + filePHID VARBINARY(64), 4 + ttl INT UNSIGNED NOT NULL, 5 + uri LONGTEXT NOT NULL, 6 + uriIndex BINARY(12) NOT NULL, 7 + isSuccessful BOOL NOT NULL, 8 + responseMessage LONGTEXT, 9 + dateCreated INT UNSIGNED NOT NULL, 10 + dateModified INT UNSIGNED NOT NULL, 11 + UNIQUE KEY `key_uriindex` (uriIndex), 12 + KEY `key_ttl` (ttl), 13 + KEY `key_file` (filePHID) 14 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+9
src/__phutil_library_map__.php
··· 2553 2553 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 2554 2554 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', 2555 2555 'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php', 2556 + 'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php', 2557 + 'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php', 2556 2558 'PhabricatorFileFilePHIDType' => 'applications/files/phid/PhabricatorFileFilePHIDType.php', 2557 2559 'PhabricatorFileHasObjectEdgeType' => 'applications/files/edge/PhabricatorFileHasObjectEdgeType.php', 2558 2560 'PhabricatorFileIconSetSelectController' => 'applications/files/controller/PhabricatorFileIconSetSelectController.php', 2559 2561 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 2562 + 'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php', 2560 2563 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 2561 2564 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 2562 2565 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', ··· 7368 7371 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 7369 7372 'PhabricatorFileEditController' => 'PhabricatorFileController', 7370 7373 'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor', 7374 + 'PhabricatorFileExternalRequest' => array( 7375 + 'PhabricatorFileDAO', 7376 + 'PhabricatorDestructibleInterface', 7377 + ), 7378 + 'PhabricatorFileExternalRequestGarbageCollector' => 'PhabricatorGarbageCollector', 7371 7379 'PhabricatorFileFilePHIDType' => 'PhabricatorPHIDType', 7372 7380 'PhabricatorFileHasObjectEdgeType' => 'PhabricatorEdgeType', 7373 7381 'PhabricatorFileIconSetSelectController' => 'PhabricatorFileController', ··· 7379 7387 'PhabricatorTokenReceiverInterface', 7380 7388 'PhabricatorPolicyInterface', 7381 7389 ), 7390 + 'PhabricatorFileImageProxyController' => 'PhabricatorFileController', 7382 7391 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 7383 7392 'PhabricatorFileInfoController' => 'PhabricatorFileController', 7384 7393 'PhabricatorFileLinkView' => 'AphrontView',
+1 -1
src/applications/files/application/PhabricatorFilesApplication.php
··· 78 78 'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController', 79 79 'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController', 80 80 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController', 81 - 'proxy/' => 'PhabricatorFileProxyController', 81 + 'imageproxy/' => 'PhabricatorFileImageProxyController', 82 82 'transforms/(?P<id>[1-9]\d*)/' => 83 83 'PhabricatorFileTransformListController', 84 84 'uploaddialog/(?P<single>single/)?'
+118
src/applications/files/controller/PhabricatorFileImageProxyController.php
··· 1 + <?php 2 + 3 + final class PhabricatorFileImageProxyController 4 + extends PhabricatorFileController { 5 + 6 + public function shouldAllowPublic() { 7 + return true; 8 + } 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + 12 + $show_prototypes = PhabricatorEnv::getEnvConfig( 13 + 'phabricator.show-prototypes'); 14 + if (!$show_prototypes) { 15 + throw new Exception( 16 + pht('Show prototypes is disabled. 17 + Set `phabricator.show-prototypes` to `true` to use the image proxy')); 18 + } 19 + 20 + $viewer = $request->getViewer(); 21 + $img_uri = $request->getStr('uri'); 22 + 23 + // Validate the URI before doing anything 24 + PhabricatorEnv::requireValidRemoteURIForLink($img_uri); 25 + $uri = new PhutilURI($img_uri); 26 + $proto = $uri->getProtocol(); 27 + if (!in_array($proto, array('http', 'https'))) { 28 + throw new Exception( 29 + pht('The provided image URI must be either http or https')); 30 + } 31 + 32 + // Check if we already have the specified image URI downloaded 33 + $cached_request = id(new PhabricatorFileExternalRequest())->loadOneWhere( 34 + 'uriIndex = %s', 35 + PhabricatorHash::digestForIndex($img_uri)); 36 + 37 + if ($cached_request) { 38 + return $this->getExternalResponse($cached_request); 39 + } 40 + 41 + $ttl = PhabricatorTime::getNow() + phutil_units('7 days in seconds'); 42 + $external_request = id(new PhabricatorFileExternalRequest()) 43 + ->setURI($img_uri) 44 + ->setTTL($ttl); 45 + 46 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 47 + // Cache missed so we'll need to validate and download the image 48 + try { 49 + // Rate limit outbound fetches to make this mechanism less useful for 50 + // scanning networks and ports. 51 + PhabricatorSystemActionEngine::willTakeAction( 52 + array($viewer->getPHID()), 53 + new PhabricatorFilesOutboundRequestAction(), 54 + 1); 55 + 56 + $file = PhabricatorFile::newFromFileDownload( 57 + $uri, 58 + array( 59 + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 60 + 'canCDN' => true, 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 + } else { 75 + $file->save(); 76 + } 77 + 78 + $external_request->setIsSuccessful(true) 79 + ->setFilePHID($file->getPHID()) 80 + ->save(); 81 + unset($unguarded); 82 + return $this->getExternalResponse($external_request); 83 + } catch (HTTPFutureHTTPResponseStatus $status) { 84 + $external_request->setIsSuccessful(false) 85 + ->setResponseMessage($status->getMessage()) 86 + ->save(); 87 + return $this->getExternalResponse($external_request); 88 + } catch (Exception $ex) { 89 + // Not actually saving the request in this case 90 + $external_request->setResponseMessage($ex->getMessage()); 91 + return $this->getExternalResponse($external_request); 92 + } 93 + } 94 + 95 + private function getExternalResponse( 96 + 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( 104 + '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())); 116 + } 117 + } 118 + }
+28
src/applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php
··· 1 + <?php 2 + 3 + final class PhabricatorFileExternalRequestGarbageCollector 4 + extends PhabricatorGarbageCollector { 5 + 6 + const COLLECTORCONST = 'files.externalttl'; 7 + 8 + public function getCollectorName() { 9 + return pht('External Requests (TTL)'); 10 + } 11 + 12 + public function hasAutomaticPolicy() { 13 + return true; 14 + } 15 + 16 + protected function collectGarbage() { 17 + $file_requests = id(new PhabricatorFileExternalRequest())->loadAllWhere( 18 + 'ttl < %d LIMIT 100', 19 + PhabricatorTime::getNow()); 20 + $engine = new PhabricatorDestructionEngine(); 21 + foreach ($file_requests as $request) { 22 + $engine->destroyObject($request); 23 + } 24 + 25 + return (count($file_requests) == 100); 26 + } 27 + 28 + }
+63
src/applications/files/storage/PhabricatorFileExternalRequest.php
··· 1 + <?php 2 + 3 + final class PhabricatorFileExternalRequest extends PhabricatorFileDAO 4 + implements 5 + PhabricatorDestructibleInterface { 6 + 7 + protected $uri; 8 + protected $uriIndex; 9 + protected $ttl; 10 + protected $filePHID; 11 + protected $isSuccessful; 12 + protected $responseMessage; 13 + 14 + protected function getConfiguration() { 15 + return array( 16 + self::CONFIG_COLUMN_SCHEMA => array( 17 + 'uri' => 'text', 18 + 'uriIndex' => 'bytes12', 19 + 'ttl' => 'epoch', 20 + 'filePHID' => 'phid?', 21 + 'isSuccessful' => 'bool', 22 + 'responseMessage' => 'text?', 23 + ), 24 + self::CONFIG_KEY_SCHEMA => array( 25 + 'key_uriindex' => array( 26 + 'columns' => array('uriIndex'), 27 + 'unique' => true, 28 + ), 29 + 'key_ttl' => array( 30 + 'columns' => array('ttl'), 31 + ), 32 + 'key_file' => array( 33 + 'columns' => array('filePHID'), 34 + ), 35 + ), 36 + ) + parent::getConfiguration(); 37 + } 38 + 39 + public function save() { 40 + $hash = PhabricatorHash::digestForIndex($this->getURI()); 41 + $this->setURIIndex($hash); 42 + return parent::save(); 43 + } 44 + 45 + /* -( PhabricatorDestructibleInterface )----------------------------------- */ 46 + 47 + public function destroyObjectPermanently( 48 + PhabricatorDestructionEngine $engine) { 49 + 50 + $file_phid = $this->getFilePHID(); 51 + if ($file_phid) { 52 + $file = id(new PhabricatorFileQuery()) 53 + ->setViewer($engine->getViewer()) 54 + ->withPHIDs(array($file_phid)) 55 + ->executeOne(); 56 + if ($file) { 57 + $engine->destroyObject($file); 58 + } 59 + } 60 + $this->delete(); 61 + } 62 + 63 + }