diff --git a/src/applications/celerity/CelerityResourceMap.php b/src/applications/celerity/CelerityResourceMap.php index 03ccfe607d..e53b656aaf 100644 --- a/src/applications/celerity/CelerityResourceMap.php +++ b/src/applications/celerity/CelerityResourceMap.php @@ -1,261 +1,266 @@ resources = $resources; $map = $resources->loadMap(); $this->symbolMap = idx($map, 'symbols', array()); $this->requiresMap = idx($map, 'requires', array()); $this->packageMap = idx($map, 'packages', array()); $this->nameMap = idx($map, 'names', array()); // We derive these reverse maps at runtime. $this->hashMap = array_flip($this->nameMap); $this->componentMap = array(); foreach ($this->packageMap as $package_name => $symbols) { foreach ($symbols as $symbol) { $this->componentMap[$symbol] = $package_name; } } } public static function getNamedInstance($name) { if (empty(self::$instances[$name])) { $resources_list = CelerityPhysicalResources::getAll(); if (empty($resources_list[$name])) { throw new Exception( pht( 'No resource source exists with name "%s"!', $name)); } $instance = new CelerityResourceMap($resources_list[$name]); self::$instances[$name] = $instance; } return self::$instances[$name]; } public function getNameMap() { return $this->nameMap; } public function getSymbolMap() { return $this->symbolMap; } public function getRequiresMap() { return $this->requiresMap; } public function getPackageMap() { return $this->packageMap; } public function getPackagedNamesForSymbols(array $symbols) { $resolved = $this->resolveResources($symbols); return $this->packageResources($resolved); } private function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { continue; } $this->resolveResource($map, $symbol); } return $map; } private function resolveResource(array &$map, $symbol) { if (empty($this->symbolMap[$symbol])) { throw new Exception( pht( 'Attempting to resolve unknown resource, "%s".', $symbol)); } $hash = $this->symbolMap[$symbol]; $map[$symbol] = $hash; if (isset($this->requiresMap[$hash])) { $requires = $this->requiresMap[$hash]; } else { $requires = array(); } foreach ($requires as $required_symbol) { if (!empty($map[$required_symbol])) { continue; } $this->resolveResource($map, $required_symbol); } } private function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $hash) { if (isset($handled[$symbol])) { continue; } if (empty($this->componentMap[$symbol])) { $packaged[] = $this->hashMap[$hash]; } else { $package_name = $this->componentMap[$symbol]; $packaged[] = $package_name; $package_symbols = $this->packageMap[$package_name]; foreach ($package_symbols as $package_symbol) { $handled[$package_symbol] = true; } } } return $packaged; } public function getResourceDataForName($resource_name) { return $this->resources->getResourceData($resource_name); } public function getResourceNamesForPackageName($package_name) { $package_symbols = idx($this->packageMap, $package_name); if (!$package_symbols) { return null; } $resource_names = array(); foreach ($package_symbols as $symbol) { $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]]; } return $resource_names; } /** * Get the epoch timestamp of the last modification time of a symbol. * * @param string Resource symbol to lookup. * @return int Epoch timestamp of last resource modification. */ public function getModifiedTimeForName($name) { if ($this->isPackageResource($name)) { $names = array(); foreach ($this->packageMap[$name] as $symbol) { $names[] = $this->getResourceNameForSymbol($symbol); } } else { $names = array($name); } $mtime = 0; foreach ($names as $name) { $mtime = max($mtime, $this->resources->getResourceModifiedTime($name)); } return $mtime; } /** * Return the absolute URI for the resource associated with a symbol. This * method is fairly low-level and ignores packaging. * * @param string Resource symbol to lookup. * @return string|null Resource URI, or null if the symbol is unknown. */ public function getURIForSymbol($symbol) { $hash = idx($this->symbolMap, $symbol); return $this->getURIForHash($hash); } /** * Return the absolute URI for the resource associated with a resource name. * This method is fairly low-level and ignores packaging. * * @param string Resource name to lookup. * @return string|null Resource URI, or null if the name is unknown. */ public function getURIForName($name) { $hash = idx($this->nameMap, $name); return $this->getURIForHash($hash); } + public function getHashForName($name) { + return idx($this->nameMap, $name); + } + + /** * Return the absolute URI for a resource, identified by hash. * This method is fairly low-level and ignores packaging. * * @param string Resource hash to lookup. * @return string|null Resource URI, or null if the hash is unknown. */ private function getURIForHash($hash) { if ($hash === null) { return null; } return $this->resources->getResourceURI($hash, $this->hashMap[$hash]); } /** * Return the resource symbols required by a named resource. * * @param string Resource name to lookup. * @return list|null List of required symbols, or null if the name * is unknown. */ public function getRequiredSymbolsForName($name) { $hash = idx($this->nameMap, $name); if ($hash === null) { return null; } return idx($this->requiresMap, $hash, array()); } /** * Return the resource name for a given symbol. * * @param string Resource symbol to lookup. * @return string|null Resource name, or null if the symbol is unknown. */ public function getResourceNameForSymbol($symbol) { $hash = idx($this->symbolMap, $symbol); return idx($this->hashMap, $hash); } public function isPackageResource($name) { return isset($this->packageMap[$name]); } public function getResourceTypeForName($name) { return $this->resources->getResourceType($name); } } diff --git a/src/applications/celerity/controller/CelerityPhabricatorResourceController.php b/src/applications/celerity/controller/CelerityPhabricatorResourceController.php index c5f878b61e..5751996db8 100644 --- a/src/applications/celerity/controller/CelerityPhabricatorResourceController.php +++ b/src/applications/celerity/controller/CelerityPhabricatorResourceController.php @@ -1,53 +1,57 @@ library); } public function handleRequest(AphrontRequest $request) { $this->path = $request->getURIData('path'); $this->hash = $request->getURIData('hash'); $this->library = $request->getURIData('library'); $this->postprocessorKey = $request->getURIData('postprocessor'); // Check that the resource library exists before trying to serve resources // from it. try { $this->getCelerityResourceMap(); } catch (Exception $ex) { return new Aphront400Response(); } - return $this->serveResource($this->path); + return $this->serveResource( + array( + 'path' => $this->path, + 'hash' => $this->hash, + )); } protected function buildResourceTransformer() { $minify_on = PhabricatorEnv::getEnvConfig('celerity.minify'); $developer_on = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); $should_minify = ($minify_on && !$developer_on); return id(new CelerityResourceTransformer()) ->setMinify($should_minify) ->setPostprocessorKey($this->postprocessorKey) ->setCelerityMap($this->getCelerityResourceMap()); } protected function getCacheKey($path) { return parent::getCacheKey($path.';'.$this->postprocessorKey); } } diff --git a/src/applications/celerity/controller/CelerityResourceController.php b/src/applications/celerity/controller/CelerityResourceController.php index 4e6feeac6c..debb37c1ca 100644 --- a/src/applications/celerity/controller/CelerityResourceController.php +++ b/src/applications/celerity/controller/CelerityResourceController.php @@ -1,173 +1,184 @@ getCelerityResourceMap(); + $expect_hash = $map->getHashForName($path); + + // Test if the URI hash is correct for our current resource map. If it + // is not, refuse to cache this resource. This avoids poisoning caches + // and CDNs if we're getting a request for a new resource to an old node + // shortly after a push. + $is_cacheable = ($hash === $expect_hash) && + $this->isCacheableResourceType($type); + if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) { // Return a "304 Not Modified". We don't care about the value of this // field since we never change what resource is served by a given URI. return $this->makeResponseCacheable(new Aphront304Response()); } - $is_cacheable = (!$dev_mode) && - $this->isCacheableResourceType($type); - $cache = null; $data = null; - if ($is_cacheable) { + if ($is_cacheable && !$dev_mode) { $cache = PhabricatorCaches::getImmutableCache(); $request_path = $this->getRequest()->getPath(); $cache_key = $this->getCacheKey($request_path); $data = $cache->getKey($cache_key); } if ($data === null) { - $map = $this->getCelerityResourceMap(); - if ($map->isPackageResource($path)) { $resource_names = $map->getResourceNamesForPackageName($path); if (!$resource_names) { return new Aphront404Response(); } try { $data = array(); foreach ($resource_names as $resource_name) { $data[] = $map->getResourceDataForName($resource_name); } $data = implode("\n\n", $data); } catch (Exception $ex) { return new Aphront404Response(); } } else { try { $data = $map->getResourceDataForName($path); } catch (Exception $ex) { return new Aphront404Response(); } } $xformer = $this->buildResourceTransformer(); if ($xformer) { $data = $xformer->transformResource($path, $data); } if ($cache) { $cache->setKey($cache_key, $data); } } $response = new AphrontFileResponse(); $response->setContent($data); $response->setMimeType($type_map[$type]); // NOTE: This is a piece of magic required to make WOFF fonts work in // Firefox and IE. Possibly we should generalize this more. $cross_origin_types = array( 'woff' => true, 'woff2' => true, 'eot' => true, ); if (isset($cross_origin_types[$type])) { // We could be more tailored here, but it's not currently trivial to // generate a comprehensive list of valid origins (an install may have // arbitrarily many Phame blogs, for example), and we lose nothing by // allowing access from anywhere. $response->addAllowOrigin('*'); } - return $this->makeResponseCacheable($response); + if ($is_cacheable) { + $response = $this->makeResponseCacheable($response); + } + + return $response; } public static function getSupportedResourceTypes() { return array( 'css' => 'text/css; charset=utf-8', 'js' => 'text/javascript; charset=utf-8', 'png' => 'image/png', 'svg' => 'image/svg+xml', 'gif' => 'image/gif', 'jpg' => 'image/jpeg', 'swf' => 'application/x-shockwave-flash', 'woff' => 'font/woff', 'woff2' => 'font/woff2', 'eot' => 'font/eot', 'ttf' => 'font/ttf', 'mp3' => 'audio/mpeg', ); } private function makeResponseCacheable(AphrontResponse $response) { $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setLastModified(time()); $response->setCanCDN(true); return $response; } /** * Is it appropriate to cache the data for this resource type in the fast * immutable cache? * * Generally, text resources (which are small, and expensive to process) * are cached, while other types of resources (which are large, and cheap * to process) are not. * * @param string Resource type. * @return bool True to enable caching. */ private function isCacheableResourceType($type) { $types = array( 'js' => true, 'css' => true, ); return isset($types[$type]); } protected function getCacheKey($path) { return 'celerity:'.PhabricatorHash::digestToLength($path, 64); } }