@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
3abstract class CelerityResourceController extends PhabricatorController {
4
5 protected function buildResourceTransformer() {
6 return null;
7 }
8
9 public function shouldRequireLogin() {
10 return false;
11 }
12
13 public function shouldRequireEnabledUser() {
14 return false;
15 }
16
17 public function shouldAllowPartialSessions() {
18 return true;
19 }
20
21 public function shouldAllowLegallyNonCompliantUsers() {
22 return true;
23 }
24
25 abstract public function getCelerityResourceMap();
26
27 protected function serveResource(array $spec) {
28 $path = $spec['path'];
29 $hash = idx($spec, 'hash');
30
31 // Sanity checking to keep this from exposing anything sensitive, since it
32 // ultimately boils down to disk reads.
33 if (preg_match('@(//|\.\.)@', $path)) {
34 return new Aphront400Response();
35 }
36
37 $type = CelerityResourceTransformer::getResourceType($path);
38 $type_map = self::getSupportedResourceTypes();
39
40 if (empty($type_map[$type])) {
41 throw new Exception(pht('Only static resources may be served.'));
42 }
43
44 $dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
45
46 $map = $this->getCelerityResourceMap();
47 $expect_hash = $map->getHashForName($path);
48
49 // Test if the URI hash is correct for our current resource map. If it
50 // is not, refuse to cache this resource. This avoids poisoning caches
51 // and CDNs if we're getting a request for a new resource to an old node
52 // shortly after a push.
53 $is_cacheable = ($hash === $expect_hash);
54 $is_locally_cacheable = $this->isLocallyCacheableResourceType($type);
55 if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
56 // Return a "304 Not Modified". We don't care about the value of this
57 // field since we never change what resource is served by a given URI.
58 return $this->makeResponseCacheable(new Aphront304Response());
59 }
60
61 $cache = null;
62 $cache_key = null;
63 $data = null;
64 if ($is_cacheable && $is_locally_cacheable && !$dev_mode) {
65 $cache = PhabricatorCaches::getImmutableCache();
66
67 $request_path = $this->getRequest()->getPath();
68 $cache_key = $this->getCacheKey($request_path);
69
70 $data = $cache->getKey($cache_key);
71 }
72
73 if ($data === null) {
74 if ($map->isPackageResource($path)) {
75 $resource_names = $map->getResourceNamesForPackageName($path);
76 if (!$resource_names) {
77 return new Aphront404Response();
78 }
79
80 try {
81 $data = array();
82 foreach ($resource_names as $resource_name) {
83 $data[] = $map->getResourceDataForName($resource_name);
84 }
85 $data = implode("\n\n", $data);
86 } catch (Exception $ex) {
87 return new Aphront404Response();
88 }
89 } else {
90 try {
91 $data = $map->getResourceDataForName($path);
92 } catch (Exception $ex) {
93 return new Aphront404Response();
94 }
95 }
96
97 $xformer = $this->buildResourceTransformer();
98 if ($xformer) {
99 $data = $xformer->transformResource($path, $data);
100 }
101
102 if ($cache && $cache_key !== null) {
103 $cache->setKey($cache_key, $data);
104 }
105 }
106
107 $response = id(new AphrontFileResponse())
108 ->setMimeType($type_map[$type]);
109
110 // The "Content-Security-Policy" header has no effect on the actual
111 // resources, only on the main request. Disable it on the resource
112 // responses to limit confusion.
113 $response->setDisableContentSecurityPolicy(true);
114
115 $range = AphrontRequest::getHTTPHeader('Range');
116
117 if (phutil_nonempty_string($range)) {
118 $response->setContentLength(strlen($data));
119
120 list($range_begin, $range_end) = $response->parseHTTPRange($range);
121
122 if ($range_begin !== null) {
123 if ($range_end !== null) {
124 $data = substr($data, $range_begin, ($range_end - $range_begin));
125 } else {
126 $data = substr($data, $range_begin);
127 }
128 }
129
130 $response->setContentIterator(array($data));
131 } else {
132 $response
133 ->setContent($data)
134 ->setCompressResponse(true);
135 }
136
137
138 // NOTE: This is a piece of magic required to make WOFF fonts work in
139 // Firefox and IE. Possibly we should generalize this more.
140
141 $cross_origin_types = array(
142 'woff2' => true,
143 );
144
145 if (isset($cross_origin_types[$type])) {
146 // We could be more tailored here, but it's not currently trivial to
147 // generate a comprehensive list of valid origins (an install may have
148 // arbitrarily many Phame blogs, for example), and we lose nothing by
149 // allowing access from anywhere.
150 $response->addAllowOrigin('*');
151 }
152
153 if ($is_cacheable) {
154 $response = $this->makeResponseCacheable($response);
155 }
156
157 return $response;
158 }
159
160 public static function getSupportedResourceTypes() {
161 return array(
162 'css' => 'text/css; charset=utf-8',
163 'js' => 'text/javascript; charset=utf-8',
164 'png' => 'image/png',
165 'svg' => 'image/svg+xml',
166 'gif' => 'image/gif',
167 'jpg' => 'image/jpeg',
168 'swf' => 'application/x-shockwave-flash',
169 'woff2' => 'font/woff2',
170 'mp3' => 'audio/mpeg',
171 'ico' => 'image/x-icon',
172 );
173 }
174
175 private function makeResponseCacheable(AphrontResponse $response) {
176 $response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
177 $response->setLastModified(time());
178 $response->setCanCDN(true);
179
180 return $response;
181 }
182
183
184 /**
185 * Is it appropriate to cache the data for this resource type in the fast
186 * immutable cache?
187 *
188 * Generally, text resources (which are small, and expensive to process)
189 * are cached, while other types of resources (which are large, and cheap
190 * to process) are not.
191 *
192 * @param string $type Resource type.
193 * @return bool True to enable caching.
194 */
195 private function isLocallyCacheableResourceType($type) {
196 $types = array(
197 'js' => true,
198 'css' => true,
199 );
200
201 return isset($types[$type]);
202 }
203
204 protected function getCacheKey($path) {
205 return 'celerity:'.PhabricatorHash::digestToLength($path, 64);
206 }
207
208}