
Summary: Ref T7567. In T8266 I fixed a bunch of obscure "Range" issues, but only for file downloads -- not for Celerity. Extend all that stuff to Celerity, which is fortunately much easier. I believe this will fix Conpherence sounds in Safari. Test Plan: - Wrote out an HTTP request in a text file with `Range: bytes=0-1` and similar, piped it to localhost with `cat request.txt | nc localhost 80`, saw server return appropriate range responses consistent with file behavior after T8266, which all seems to work. - Also did that for files to try to make sure I wasn't breaking anything. Reviewers: chad, amckinley Reviewed By: chad Maniphest Tasks: T7567 Differential Revision: https://secure.phabricator.com/D17724
208 lines
5.8 KiB
PHP
208 lines
5.8 KiB
PHP
<?php
|
|
|
|
abstract class CelerityResourceController extends PhabricatorController {
|
|
|
|
protected function buildResourceTransformer() {
|
|
return null;
|
|
}
|
|
|
|
public function shouldRequireLogin() {
|
|
return false;
|
|
}
|
|
|
|
public function shouldRequireEnabledUser() {
|
|
return false;
|
|
}
|
|
|
|
public function shouldAllowPartialSessions() {
|
|
return true;
|
|
}
|
|
|
|
public function shouldAllowLegallyNonCompliantUsers() {
|
|
return true;
|
|
}
|
|
|
|
abstract public function getCelerityResourceMap();
|
|
|
|
protected function serveResource(array $spec) {
|
|
$path = $spec['path'];
|
|
$hash = idx($spec, 'hash');
|
|
|
|
// Sanity checking to keep this from exposing anything sensitive, since it
|
|
// ultimately boils down to disk reads.
|
|
if (preg_match('@(//|\.\.)@', $path)) {
|
|
return new Aphront400Response();
|
|
}
|
|
|
|
$type = CelerityResourceTransformer::getResourceType($path);
|
|
$type_map = self::getSupportedResourceTypes();
|
|
|
|
if (empty($type_map[$type])) {
|
|
throw new Exception(pht('Only static resources may be served.'));
|
|
}
|
|
|
|
$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
|
|
|
|
$map = $this->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);
|
|
$is_locally_cacheable = $this->isLocallyCacheableResourceType($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());
|
|
}
|
|
|
|
$cache = null;
|
|
$data = null;
|
|
if ($is_cacheable && $is_locally_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) {
|
|
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 = id(new AphrontFileResponse())
|
|
->setMimeType($type_map[$type]);
|
|
|
|
$range = AphrontRequest::getHTTPHeader('Range');
|
|
|
|
if (strlen($range)) {
|
|
$response->setContentLength(strlen($data));
|
|
|
|
list($range_begin, $range_end) = $response->parseHTTPRange($range);
|
|
|
|
if ($range_begin !== null) {
|
|
if ($range_end !== null) {
|
|
$data = substr($data, $range_begin, ($range_end - $range_begin));
|
|
} else {
|
|
$data = substr($data, $range_begin);
|
|
}
|
|
}
|
|
|
|
$response->setContentIterator(array($data));
|
|
} else {
|
|
$response
|
|
->setContent($data)
|
|
->setCompressResponse(true);
|
|
}
|
|
|
|
|
|
// 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('*');
|
|
}
|
|
|
|
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',
|
|
'ico' => 'image/x-icon',
|
|
);
|
|
}
|
|
|
|
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 isLocallyCacheableResourceType($type) {
|
|
$types = array(
|
|
'js' => true,
|
|
'css' => true,
|
|
);
|
|
|
|
return isset($types[$type]);
|
|
}
|
|
|
|
protected function getCacheKey($path) {
|
|
return 'celerity:'.PhabricatorHash::digestToLength($path, 64);
|
|
}
|
|
|
|
}
|