@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 208 lines 6.1 kB view raw
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}