Path: blob/master/src/applications/celerity/controller/CelerityResourceController.php
12256 views
<?php12abstract class CelerityResourceController extends PhabricatorController {34protected function buildResourceTransformer() {5return null;6}78public function shouldRequireLogin() {9return false;10}1112public function shouldRequireEnabledUser() {13return false;14}1516public function shouldAllowPartialSessions() {17return true;18}1920public function shouldAllowLegallyNonCompliantUsers() {21return true;22}2324abstract public function getCelerityResourceMap();2526protected function serveResource(array $spec) {27$path = $spec['path'];28$hash = idx($spec, 'hash');2930// Sanity checking to keep this from exposing anything sensitive, since it31// ultimately boils down to disk reads.32if (preg_match('@(//|\.\.)@', $path)) {33return new Aphront400Response();34}3536$type = CelerityResourceTransformer::getResourceType($path);37$type_map = self::getSupportedResourceTypes();3839if (empty($type_map[$type])) {40throw new Exception(pht('Only static resources may be served.'));41}4243$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');4445$map = $this->getCelerityResourceMap();46$expect_hash = $map->getHashForName($path);4748// Test if the URI hash is correct for our current resource map. If it49// is not, refuse to cache this resource. This avoids poisoning caches50// and CDNs if we're getting a request for a new resource to an old node51// shortly after a push.52$is_cacheable = ($hash === $expect_hash);53$is_locally_cacheable = $this->isLocallyCacheableResourceType($type);54if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {55// Return a "304 Not Modified". We don't care about the value of this56// field since we never change what resource is served by a given URI.57return $this->makeResponseCacheable(new Aphront304Response());58}5960$cache = null;61$data = null;62if ($is_cacheable && $is_locally_cacheable && !$dev_mode) {63$cache = PhabricatorCaches::getImmutableCache();6465$request_path = $this->getRequest()->getPath();66$cache_key = $this->getCacheKey($request_path);6768$data = $cache->getKey($cache_key);69}7071if ($data === null) {72if ($map->isPackageResource($path)) {73$resource_names = $map->getResourceNamesForPackageName($path);74if (!$resource_names) {75return new Aphront404Response();76}7778try {79$data = array();80foreach ($resource_names as $resource_name) {81$data[] = $map->getResourceDataForName($resource_name);82}83$data = implode("\n\n", $data);84} catch (Exception $ex) {85return new Aphront404Response();86}87} else {88try {89$data = $map->getResourceDataForName($path);90} catch (Exception $ex) {91return new Aphront404Response();92}93}9495$xformer = $this->buildResourceTransformer();96if ($xformer) {97$data = $xformer->transformResource($path, $data);98}99100if ($cache) {101$cache->setKey($cache_key, $data);102}103}104105$response = id(new AphrontFileResponse())106->setMimeType($type_map[$type]);107108// The "Content-Security-Policy" header has no effect on the actual109// resources, only on the main request. Disable it on the resource110// responses to limit confusion.111$response->setDisableContentSecurityPolicy(true);112113$range = AphrontRequest::getHTTPHeader('Range');114115if ($range !== null && strlen($range)) {116$response->setContentLength(strlen($data));117118list($range_begin, $range_end) = $response->parseHTTPRange($range);119120if ($range_begin !== null) {121if ($range_end !== null) {122$data = substr($data, $range_begin, ($range_end - $range_begin));123} else {124$data = substr($data, $range_begin);125}126}127128$response->setContentIterator(array($data));129} else {130$response131->setContent($data)132->setCompressResponse(true);133}134135136// NOTE: This is a piece of magic required to make WOFF fonts work in137// Firefox and IE. Possibly we should generalize this more.138139$cross_origin_types = array(140'woff' => true,141'woff2' => true,142'eot' => true,143);144145if (isset($cross_origin_types[$type])) {146// We could be more tailored here, but it's not currently trivial to147// generate a comprehensive list of valid origins (an install may have148// arbitrarily many Phame blogs, for example), and we lose nothing by149// allowing access from anywhere.150$response->addAllowOrigin('*');151}152153if ($is_cacheable) {154$response = $this->makeResponseCacheable($response);155}156157return $response;158}159160public static function getSupportedResourceTypes() {161return 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'woff' => 'font/woff',170'woff2' => 'font/woff2',171'eot' => 'font/eot',172'ttf' => 'font/ttf',173'mp3' => 'audio/mpeg',174'ico' => 'image/x-icon',175);176}177178private function makeResponseCacheable(AphrontResponse $response) {179$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);180$response->setLastModified(time());181$response->setCanCDN(true);182183return $response;184}185186187/**188* Is it appropriate to cache the data for this resource type in the fast189* immutable cache?190*191* Generally, text resources (which are small, and expensive to process)192* are cached, while other types of resources (which are large, and cheap193* to process) are not.194*195* @param string Resource type.196* @return bool True to enable caching.197*/198private function isLocallyCacheableResourceType($type) {199$types = array(200'js' => true,201'css' => true,202);203204return isset($types[$type]);205}206207protected function getCacheKey($path) {208return 'celerity:'.PhabricatorHash::digestToLength($path, 64);209}210211}212213214