cdn.basic.farfuture.inc in CDN 7.2
Same filename and directory in other branches
Far Future expiration setting for basic mode.
File
cdn.basic.farfuture.incView source
<?php
/**
* @file
* Far Future expiration setting for basic mode.
*/
//----------------------------------------------------------------------------
// Menu system callbacks.
function cdn_basic_farfuture_download($token, $ufi, $path) {
// Validate the token to make sure this request originated from CDN.
$path_info = pathinfo($path);
$sec_token = drupal_hmac_base64($ufi . $path_info['filename'], drupal_get_private_key() . drupal_get_hash_salt());
if ($token != $sec_token) {
header('HTTP/1.1 403 Forbidden');
exit;
}
// Disallow downloading of files that are also not allowed to be downloaded
// by Drupal's .htaccess file.
if (preg_match("/\\.(engine|inc|info|install|make|module|profile|test|po|sh|php([3-6])?|phtml|.*sql|theme|tpl(\\.php)?|xtmpl)\$|^(\\..*|Entries.*|Repository|Root|Tag|Template)\$/", $path)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
if (!file_exists($path)) {
watchdog('cdn', 'CDN Far Future 404: %file.', array(
'%file' => $path,
), WATCHDOG_ALERT);
header('HTTP/1.1 404 Not Found');
exit;
}
// Remove some useless/unwanted headers.
$remove_headers = explode("\n", CDN_BASIC_FARFUTURE_REMOVE_HEADERS);
$current_headers = array();
foreach (headers_list() as $header) {
$parts = explode(':', $header);
$current_headers[] = $parts[0];
}
foreach ($remove_headers as $header) {
if (in_array($header, $current_headers)) {
// header_remove() only exists in PHP >=5.3
if (function_exists('header_remove')) {
header_remove($header);
}
else {
// In PHP <5.3, we cannot remove headers. At least shorten them to save
// every byte possible and to stop leaking information needlessly.
header($header . ':');
}
}
}
// Remove all previously set Cache-Control headers, because we're going to
// override it. Since multiple Cache-Control headers might have been set,
// simply setting a new, overriding header isn't enough: that would only
// override the *last* Cache-Control header. Yay for PHP!
if (function_exists('header_remove')) {
header_remove('Cache-Control');
}
else {
header("Cache-Control:");
header("Cache-Control:");
}
// Default caching rules: no caching/immediate expiration.
header("Cache-Control: private, must-revalidate, proxy-revalidate");
header("Expires: " . gmdate("D, d M Y H:i:s", time() - 86400) . "GMT");
// Instead of being powered by PHP, tell the world this resource was powered
// by the CDN module!
header("X-Powered-By: Drupal CDN module");
// Determine the content type.
header("Content-Type: " . _cdn_basic_farfuture_get_mimetype(basename($path)));
// Support partial content requests.
header("Accept-Ranges: bytes");
// Browsers that implement the W3C Access Control specification might refuse
// to use certain resources such as fonts if those resources violate the
// same-origin policy. Send a header to explicitly allow cross-domain use of
// those resources. (This is called Cross-Origin Resource Sharing, or CORS.)
header("Access-Control-Allow-Origin: *");
// If the extension of the file that's being served is one of the far future
// extensions (by default: images, fonts and flash content), then cache it
// in the far future.
$farfuture_extensions = variable_get(CDN_BASIC_FARFUTURE_EXTENSIONS_VARIABLE, CDN_BASIC_FARFUTURE_EXTENSIONS_DEFAULT);
$extension = drupal_strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($extension, explode("\n", $farfuture_extensions))) {
// Remove all previously set Cache-Control headers, because we're going to
// override it. Since multiple Cache-Control headers might have been set,
// simply setting a new, overriding header isn't enough: that would only
// override the *last* Cache-Control header. Yay for PHP!
if (function_exists('header_remove')) {
header_remove('Cache-Control');
}
else {
header("Cache-Control:");
header("Cache-Control:");
}
// Set a far future Cache-Control header (480 weeks), which prevents
// intermediate caches from transforming the data and allows any
// intermediate cache to cache it, since it's marked as a public resource.
// Finally, it's also marked as "immutable", which helps avoid revalidation,
// see:
// - https://bitsup.blogspot.be/2016/05/cache-control-immutable.html
// - https://tools.ietf.org/html/draft-mcmanus-immutable-00
header('Cache-Control: max-age=290304000, no-transform, public, immutable');
// Set a far future Expires header. The maximum UNIX timestamp is somewhere
// in 2038. Set it to a date in 2037, just to be safe.
header("Expires: Tue, 20 Jan 2037 04:20:42 GMT");
// Pretend the file was last modified a long time ago in the past, this will
// prevent browsers that don't support Cache-Control nor Expires headers to
// still request a new version too soon (these browsers calculate a
// heuristic to determine when to request a new version, based on the last
// time the resource has been modified).
// Also see http://code.google.com/speed/page-speed/docs/caching.html.
header("Last-Modified: Wed, 20 Jan 1988 04:20:42 GMT");
}
// GET requests with an "Accept-Encoding" header that lists "gzip".
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
// Only send gzipped files for some file extensions (it doesn't make sense
// to gzip images, for example).
if (in_array($extension, explode("\n", CDN_BASIC_FARFUTURE_GZIP_EXTENSIONS))) {
// Ensure a gzipped version of the file is stored on disk, instead of
// gzipping the file on every request.
$gzip_path = drupal_realpath('public://') . '/' . CDN_BASIC_FARFUTURE_GZIP_DIRECTORY . "/{$path}.{$ufi}.gz";
if (!file_exists($gzip_path)) {
_cdn_basic_farfuture_create_directory_structure(dirname($gzip_path));
file_put_contents($gzip_path, gzencode(file_get_contents($path), 9));
}
// Make sure zlib.output_compression does not gzip our gzipped output.
ini_set('zlib.output_compression', '0');
// Instruct intermediate HTTP caches to store both a compressed (gzipped)
// and uncompressed version of the resource.
header("Vary: Accept-Encoding");
// Prepare for gzipped output.
header("Content-Encoding: gzip");
$path = $gzip_path;
}
}
// Conditional GET requests (i.e. with If-Modified-Since header).
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
// All files served by this function are designed to expire in the far
// future. Hence we can simply always tell the client the requested file
// was not modified.
header("HTTP/1.1 304 Not Modified");
}
else {
_cdn_transfer_file($path);
}
exit;
}
function cdn_basic_farfuture_reverseproxy_test($token) {
$reference_token = variable_get('cdn_reverse_proxy_test');
if ($reference_token === FALSE || $token != $reference_token) {
header('HTTP/1.1 403 Forbidden');
exit;
}
if (function_exists('header_remove')) {
header_remove('Cache-Control');
}
else {
header("Cache-Control:");
header("Cache-Control:");
}
header("Cache-Control: max-age=290304000, no-transform, public");
header("Expires: Tue, 20 Jan 2037 04:20:42 GMT");
header("Last-Modified: Wed, 20 Jan 1988 04:20:42 GMT");
print REQUEST_TIME . '-' . md5(rand());
exit;
}
//----------------------------------------------------------------------------
// Public functions.
/**
* Get the UFI method for the file at a path.
*
* @param $path
* The path to get UFI method for the file at the given path.
* @param $mapping
* The UFI mapping to use.
*/
function cdn_basic_farfuture_get_ufi_method($path, $mapping) {
// Determine which UFI method should be used. Note that we keep on trying to
// find another method until the end: the order of rules matters!
// However, specificity also matters. The directory pattern "foo/bar/*"
// should *always* override the less specific pattern "foo/*".
$ufi_method = FALSE;
$current_specificity = 0;
foreach (array_keys($mapping) as $directory) {
if (drupal_match_path($path, $directory)) {
// Parse the file extension from the given path; convert it to lower case.
$file_extension = drupal_strtolower(pathinfo($path, PATHINFO_EXTENSION));
// Based on the file extension, determine which key should be used to find
// the CDN URLs in the mapping lookup table, if any.
$extension = NULL;
if (array_key_exists($file_extension, $mapping[$directory])) {
$extension = $file_extension;
}
elseif (array_key_exists('*', $mapping[$directory])) {
$extension = '*';
}
// If a matching extension was found, assign the corresponding UFI method.
if (isset($extension)) {
$specificity = $mapping[$directory][$extension]['specificity'];
if ($specificity > $current_specificity) {
$ufi_method = $mapping[$directory][$extension]['ufi method'];
$current_specificity = $specificity;
}
}
}
}
// Fall back to the default UFI method in case no UFI method is defined by
// the user.
if ($ufi_method === FALSE) {
$ufi_method = CDN_BASIC_FARFUTURE_UNIQUE_IDENTIFIER_DEFAULT;
}
return $ufi_method;
}
/**
* Get the UFI (Unique File Identifier) for the file at a path.
*
* @param $path
* The path to get a UFI for.
*/
function cdn_basic_farfuture_get_identifier($path) {
static $ufi_info;
static $mapping;
// Gather all unique file identifier info.
if (!isset($ufi_info)) {
$ufi_info = module_invoke_all('cdn_unique_file_identifier_info');
}
// We only need to parse the textual CDN mapping once into a lookup table.
if (!isset($mapping)) {
$mapping = _cdn_basic_farfuture_parse_raw_mapping(variable_get(CDN_BASIC_FARFUTURE_UNIQUE_IDENTIFIER_MAPPING_VARIABLE, CDN_BASIC_FARFUTURE_UNIQUE_IDENTIFIER_MAPPING_DEFAULT));
}
$ufi_method = cdn_basic_farfuture_get_ufi_method($path, $mapping);
$prefix = $ufi_info[$ufi_method]['prefix'];
if (isset($ufi_info[$ufi_method]['value'])) {
$value = $ufi_info[$ufi_method]['value'];
}
else {
$callback = $ufi_info[$ufi_method]['callback'];
$value = call_user_func_array($callback, array(
$path,
));
}
return "{$prefix}:{$value}";
}
//----------------------------------------------------------------------------
// Private functions.
/**
* Parse the raw (textual) mapping into a lookup table, where the key is the
* file extension and the value is a list of CDN URLs that serve the file.
*
* @param $mapping_raw
* A raw (textual) mapping.
* @return
* The corresponding mapping lookup table.
*/
function _cdn_basic_farfuture_parse_raw_mapping($mapping_raw) {
$mapping = array();
if (!empty($mapping_raw)) {
$lines = preg_split("/[\n\r]+/", $mapping_raw, -1, PREG_SPLIT_NO_EMPTY);
foreach ($lines as $line) {
// Parse this line. It may or may not limit the CDN URL to a list of
// file extensions.
$parts = explode('|', $line);
$directories = explode(':', $parts[0]);
$specificity = 0;
// There may be 2 or 3 parts:
// - part 1: directories
// - part 2: file extensions (optional)
// - part 3: unique file identifier method
if (count($parts) == 2) {
$extensions = array(
'*',
);
// Use the asterisk as a wildcard.
$ufi_method = drupal_strtolower(trim($parts[1]));
}
elseif (count($parts) == 3) {
// Convert to lower case, remove periods, whitespace and split on ' '.
$extensions = explode(' ', trim(str_replace('.', '', drupal_strtolower($parts[1]))));
$ufi_method = drupal_strtolower(trim($parts[2]));
}
// Create the mapping lookup table.
foreach ($directories as $directory) {
$directory_specificity = 10 * count(explode('/', $directory));
foreach ($extensions as $extension) {
$extension_specificity = $extension == '*' ? 0 : 1;
$mapping[$directory][$extension] = array(
'ufi method' => $ufi_method,
'specificity' => $directory_specificity + $extension_specificity,
);
}
}
}
}
return $mapping;
}
/**
* Variant of Drupal's file_transfer(), based on
* http://www.thomthom.net/blog/2007/09/php-resumable-download-server/
* to support ranged requests as well.
*
* Note: ranged requests that request multiple ranges are not supported. They
* are responded to with a 416. See
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
*/
function _cdn_transfer_file($path) {
$fp = @fopen($path, 'rb');
$size = filesize($path);
// File size
$length = $size;
// Content length
$start = 0;
// Start byte
$end = $size - 1;
// End byte
// In case of a range request, seek within the file to the correct location.
if (isset($_SERVER['HTTP_RANGE'])) {
$c_start = $start;
$c_end = $end;
// Extract the string containing the requested range.
list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
// If the client requested multiple ranges, repond with a 416.
if (strpos($range, ',') !== FALSE) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes {$start}-{$end}/{$size}");
exit;
}
// Case "Range: -n": final n bytes are requested.
if ($range[0] == '-') {
$c_start = $size - substr($range, 1);
}
else {
$range = explode('-', $range);
$c_start = intval($range[0]);
$c_end = isset($range[1]) && is_numeric($range[1]) ? intval($range[1]) : $size;
}
// Minor normalization: end bytes can not be larger than $end.
$c_end = $c_end > $end ? $end : $c_end;
// If the requested range is not valid, respond with a 416.
if ($c_start > $c_end || $c_start > $end || $c_end > $end) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header("Content-Range: bytes {$start}-{$end}/{$size}");
exit;
}
$start = $c_start;
$end = $c_end;
$length = $end - $start + 1;
fseek($fp, $start);
// The ranged request is valid and will be performed, respond with a 206.
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes {$start}-{$end}/{$size}");
}
header("Content-Length: {$length}");
// Start buffered download. Prevent reading too far for a ranged request.
$buffer = 1024 * 8;
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
set_time_limit(0);
// Reset time limit for big files.
echo fread($fp, $buffer);
flush();
// Free up memory, to prevent triggering PHP's memory limit.
}
fclose($fp);
}
/**
* Determine an Internet Media Type, or MIME type from a filename.
* Borrowed from Drupal 7.
*
* @param $path
* A string containing the file path.
* @return
* The internet media type registered for the extension or
* application/octet-stream for unknown extensions.
*/
function _cdn_basic_farfuture_get_mimetype($path) {
static $mapping;
if (!isset($mapping)) {
// The default file map, defined in file.mimetypes.inc is quite big.
// We only load it when necessary.
include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
$mapping = file_mimetype_mapping();
}
$extension = '';
$file_parts = explode('.', basename($path));
// Remove the first part: a full filename should not match an extension.
array_shift($file_parts);
// Iterate over the file parts, trying to find a match.
// For my.awesome.image.jpeg, we try:
// - jpeg
// - image.jpeg, and
// - awesome.image.jpeg
while ($additional_part = array_pop($file_parts)) {
$extension = drupal_strtolower($additional_part . ($extension ? '.' . $extension : ''));
if (isset($mapping['extensions'][$extension])) {
return $mapping['mimetypes'][$mapping['extensions'][$extension]];
}
}
return 'application/octet-stream';
}
/**
* Perform a nested HTTP request to generate a file.
*
* @param $path
* A path relative to the Drupal root (not urlencoded!) to the file that
* should be generated.
* @param $original_uri
* The original url if available.
* @return
* Whether the file was generated or not.
*/
function _cdn_basic_farfuture_generate_file($path, $original_uri = NULL) {
// Check if there's a file to generate in the first place!
if (!menu_get_item($path)) {
return FALSE;
}
// While it should already be impossible to enter recursion because of the
// above menu system check, we still want to detect this just to be safe.
// This really can only happen if a file is missing and we try to generate
// it and the request to generate the file itself triggers a 404, which
// again references the file that is missing, and would thus again trigger a
// 404, etc.
// @see http://drupal.org/node/1417616#comment-5694960
if (request_uri() == base_path() . $path) {
watchdog('cdn', 'Recursion detected for %file!', array(
'%file' => $path,
), WATCHDOG_ALERT);
header('HTTP/1.1 404 Not Found');
exit;
}
$url = $GLOBALS['base_url'] . '/' . drupal_encode_path($path);
// If this is an image style url ensure the image style token is set. Since we
// have just limited information to work with the evaluation is quite complex.
// The token query is added even if the 'image_allow_insecure_derivatives'
// variable is TRUE, so that the emitted links remain valid if it is changed
// back to the default FALSE.
if (($scheme = file_uri_scheme($original_uri)) && file_stream_wrapper_valid_scheme($scheme) && stripos($original_uri, $scheme . '://styles/') === 0) {
$parts = explode('/', $original_uri, 6);
$orinal_image_path = $scheme . '://' . $parts[5];
$token_query = array(
IMAGE_DERIVATIVE_TOKEN => image_style_path_token($parts[3], $orinal_image_path),
);
$url .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query);
}
$headers = array(
// Make sure we hit the server and do not end up with a stale
// cached version.
'Cache-Control' => 'no-cache',
'Pragma' => 'no-cache',
);
drupal_http_request($url, array(
'headers' => $headers,
));
$exists = file_exists($path);
watchdog('cdn', 'Nested HTTP request to generate %file: %result (URL: %url, time: !time).', array(
'!time' => (int) $_SERVER['REQUEST_TIME'],
'%file' => $path,
'%url' => $url,
'%result' => $exists ? 'success' : 'failure',
), $exists ? WATCHDOG_NOTICE : WATCHDOG_CRITICAL);
return $exists;
}
/**
* file_check_directory() doesn't support creating directory trees.
*/
function _cdn_basic_farfuture_create_directory_structure($path) {
// Create the directory structure in which the file will be stored. Because
// it's nested, file_check_directory() can't do this in one run.
$parts = explode('/', $path);
for ($i = 0; $i < count($parts); $i++) {
$directory = implode('/', array_slice($parts, 0, $i + 1));
if (!file_exists($directory)) {
file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
}
}
}
Functions
Name | Description |
---|---|
cdn_basic_farfuture_download | |
cdn_basic_farfuture_get_identifier | Get the UFI (Unique File Identifier) for the file at a path. |
cdn_basic_farfuture_get_ufi_method | Get the UFI method for the file at a path. |
cdn_basic_farfuture_reverseproxy_test | |
_cdn_basic_farfuture_create_directory_structure | file_check_directory() doesn't support creating directory trees. |
_cdn_basic_farfuture_generate_file | Perform a nested HTTP request to generate a file. |
_cdn_basic_farfuture_get_mimetype | Determine an Internet Media Type, or MIME type from a filename. Borrowed from Drupal 7. |
_cdn_basic_farfuture_parse_raw_mapping | Parse the raw (textual) mapping into a lookup table, where the key is the file extension and the value is a list of CDN URLs that serve the file. |
_cdn_transfer_file | Variant of Drupal's file_transfer(), based on http://www.thomthom.net/blog/2007/09/php-resumable-download-server/ to support ranged requests as well. |