@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<?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}