advagg.missing.inc in Advanced CSS/JS Aggregation 7.2
Same filename and directory in other branches
Advanced CSS/JS aggregation module.
Functions used to generate a file given the filename.
File
advagg.missing.incView source
<?php
/**
* @file
* Advanced CSS/JS aggregation module.
*
* Functions used to generate a file given the filename.
*/
/**
* Menu Callback; generates a missing CSS/JS file.
*/
function advagg_missing_aggregate($input = '') {
// Do not stop processing this request.
ignore_user_abort(TRUE);
// Generate missing file.
$msg = advagg_missing_generate($input);
if (module_exists('jquery_update')) {
$arg = arg();
$filename = array_pop($arg);
$filename = explode('?', $filename);
$filename = array_shift($filename);
if (strpos($filename, 'min.map') !== FALSE && strpos($filename, 'jquery') !== FALSE) {
// Get filename from request.
$wrong_pattern = t('Wrong pattern.');
if ($msg === $wrong_pattern) {
$version = variable_get('jquery_update_jquery_version', '1.10');
$trueversion = '1.9.1';
switch ($version) {
case '1.9':
$trueversion = '1.9.1';
break;
case '1.10':
$trueversion = '1.10.2';
break;
case '1.11':
$trueversion = '1.11.2';
break;
case '2.1':
$trueversion = '2.1.4';
break;
}
$url = "https://cdn.jsdelivr.net/gh/jquery/jquery@{$trueversion}/jquery.min.map";
drupal_goto($url, array(
'external' => TRUE,
), 301);
}
}
}
// If here send out fast 404.
advagg_missing_fast404($msg);
}
/**
* Generates a missing CSS/JS file and send it to client.
*
* @return string
* text if bundle couldn't be generated.
*/
function advagg_missing_generate($input = '') {
// Make sure we are not in a redirect loop.
$redirect_counter = isset($_GET['redirect_counter']) ? intval($_GET['redirect_counter']) : 0;
if ($redirect_counter > 5) {
watchdog('advagg', 'This request could not generate correctly. Loop detected. Request data: %info', array(
'%info' => $_GET['q'],
));
return t('In a loop.');
}
// Get filename from request.
$arg = arg();
$filename = array_pop($arg);
$filename = explode('?', $filename);
$filename = array_shift($filename);
// Quit ASAP if filename does not match the AdvAgg pattern.
$data = advagg_get_hashes_from_filename($filename);
if (is_string($data) || !is_array($data)) {
// Try again with the function input.
$filename = $input;
$data1 = advagg_get_hashes_from_filename($filename);
if (is_string($data1) || !is_array($data1)) {
return "{$data} {$data1}";
}
else {
$data = $data1;
}
}
// Check to see if the file exists.
list($css_path, $js_path) = advagg_get_root_files_dir();
if ($data[0] === 'css') {
$uri = $css_path[0] . '/' . $filename;
}
elseif ($data[0] === 'js') {
$uri = $js_path[0] . '/' . $filename;
}
if (file_exists($uri) && filesize($uri) >= 0) {
// File does exist and filesize is bigger than zero, 307 to it.
$uri = advagg_generate_location_uri($filename, $data[0], $data[3]);
++$redirect_counter;
$uri .= '?redirect_counter=' . $redirect_counter;
header('Location: ' . $uri, TRUE, 307);
exit;
}
// Get lock so only one process will do the work.
$lock_name = 'advagg_' . $filename;
$uri = $GLOBALS['base_path'] . $_GET['q'];
$created = FALSE;
$files_to_save = array();
if (variable_get('advagg_no_locks', ADVAGG_NO_LOCKS)) {
$return = advagg_missing_create_file($filename, FALSE, $data);
if (!is_array($return)) {
return $return;
}
else {
list($files_to_save, $type) = $return;
$created = TRUE;
}
}
elseif (lock_acquire($lock_name, 10) || $redirect_counter > 4) {
if ($redirect_counter > 4) {
$return = advagg_missing_create_file($filename, TRUE, $data);
}
else {
$return = advagg_missing_create_file($filename, FALSE, $data);
}
lock_release($lock_name);
if (!is_array($return)) {
return $return;
}
else {
list($files_to_save, $type) = $return;
$created = TRUE;
}
}
else {
// Wait for another request that is already doing this work.
// We choose to block here since otherwise the router item may not
// be available in menu_execute_active_handler() resulting in a 404.
lock_wait($lock_name, 10);
if (file_exists($uri) && filesize($uri) > 0) {
$type = $data[0];
$created = TRUE;
}
}
// Redirect and try again on failure.
if (empty($created)) {
$uri = advagg_generate_location_uri($filename, $data[0], $data[3]);
++$redirect_counter;
$uri .= '?redirect_counter=' . $redirect_counter;
header('Location: ' . $uri, TRUE, 307);
exit;
}
if ($redirect_counter > 4) {
watchdog('advagg', 'One of the alter hooks failed when generating this file: %uri. Thus this file was created without any alter hooks.', array(
'%uri' => $uri,
), WATCHDOG_ERROR);
}
// Output file's contents if creating the file was successful.
// This function will call exit.
advagg_missing_send_saved_file($files_to_save, $uri, $created, $filename, $type, $redirect_counter, $data[3]);
}
/**
* Given the filename, type, and settings, create absolute URL for 307 redirect.
*
* Due to url inbound alter we can not trust that the redirect will work if
* using $GLOBALS['base_path'] . $_GET['q']. Generate the uri as if it was
* going to be embedded in the html.
*
* @param string $filename
* Just the filename no path information.
* @param string $type
* String: css or js.
* @param array $aggregate_settings
* Array of settings.
*
* @return string
* String pointing to the URI of the file.
*/
function advagg_generate_location_uri($filename, $type, array $aggregate_settings = array()) {
list($css_path, $js_path) = advagg_get_root_files_dir();
if ($type === 'css') {
$uri_307 = $css_path[0] . '/' . $filename;
}
elseif ($type === 'js') {
$uri_307 = $js_path[0] . '/' . $filename;
}
if (empty($aggregate_settings)) {
$aggregate_settings = advagg_current_hooks_hash_array();
}
// 307s need to be absolute. RFC 2616 14.30.
$aggregate_settings['variables']['advagg_convert_absolute_to_relative_path'] = FALSE;
$aggregate_settings['variables']['advagg_convert_absolute_to_protocol_relative_path'] = FALSE;
// Make advagg_context_switch available.
module_load_include('inc', 'advagg', 'advagg');
advagg_context_switch($aggregate_settings, 0);
$uri_307 = advagg_file_create_url($uri_307, $aggregate_settings);
advagg_context_switch($aggregate_settings, 1);
return $uri_307;
}
/**
* Send the css/js file to the client.
*
* @param array $files_to_save
* Array of filenames and data.
* @param string $uri
* Requested filename.
* @param bool $created
* If file was created in a different thread.
* @param string $filename
* Just the filename no path information.
* @param string $type
* String: css or js.
* @param array $aggregate_settings
* Array of settings. Used to generate the 307 redirect location.
*/
function advagg_missing_send_saved_file(array $files_to_save, $uri, $created, $filename, $type, $redirect_counter, array $aggregate_settings = array()) {
// Send a 304 if this is a repeat request.
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= REQUEST_TIME) {
header("HTTP/1.1 304 Not Modified");
exit;
}
$return_compressed_br = FALSE;
$return_compressed_gz = FALSE;
// Negotiate whether to use gzip compression.
if (!empty($_SERVER['HTTP_ACCEPT_ENCODING'])) {
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br') !== FALSE) {
$return_compressed_br = TRUE;
}
if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
$return_compressed_gz = TRUE;
}
}
header('Vary: Accept-Encoding', FALSE);
if (!empty($created)) {
if ($return_compressed_br && file_exists($uri . '.br') && filesize($uri . '.br') > 0) {
$uri .= '.br';
}
elseif ($return_compressed_gz && file_exists($uri . '.gz') && filesize($uri . '.gz') > 0) {
$uri .= '.gz';
}
if (!isset($files_to_save[$uri]) && file_exists($uri) && filesize($uri)) {
// Do not use advagg_file_get_contents here.
$files_to_save[$uri] = (string) @file_get_contents($uri);
}
}
// Make sure zlib.output_compression does not compress the output.
ini_set('zlib.output_compression', '0');
header('Vary: Accept-Encoding', FALSE);
// Clear the output buffer.
if (ob_get_level()) {
ob_end_clean();
}
// Set generic far future headers.
if (variable_get('advagg_farfuture_php', ADVAGG_FARFUTURE_PHP)) {
advagg_missing_set_farfuture_headers();
}
// Return compressed content if we can.
if ($return_compressed_br || $return_compressed_gz) {
foreach ($files_to_save as $uri => $data) {
// See if this uri contains .br near the end of it.
$pos = strripos($uri, '.br', 91 + strlen(ADVAGG_SPACE) * 3);
if (!empty($pos)) {
$len = strlen($uri);
if ($pos == $len - 3) {
// .br file exists, send it out.
header('Content-Encoding: br');
break;
}
}
// See if this uri contains .gz near the end of it.
$pos = strripos($uri, '.gz', 91 + strlen(ADVAGG_SPACE) * 3);
if (!empty($pos)) {
$len = strlen($uri);
if ($pos == $len - 3) {
// .gz file exists, send it out.
header('Content-Encoding: gzip');
break;
}
}
}
}
else {
$data = trim(reset($files_to_save));
}
// Output file and exit.
if (!empty($data)) {
$strlen = strlen($data);
// Send a 304 if this is a repeat request.
if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
$etags = explode(' ', $_SERVER['HTTP_IF_NONE_MATCH']);
if ($etags[0] < REQUEST_TIME + 31 * 24 * 60 * 60 && isset($etags[1]) && $etags[1] == $strlen) {
header("HTTP/1.1 304 Not Modified");
exit;
}
}
// Send out a 200 OK status.
$default = ADVAGG_HTTP_200_CODE;
if (module_exists('httprl') && variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL) && (is_callable('httprl_is_background_callback_capable') && httprl_is_background_callback_capable() || !is_callable('httprl_is_background_callback_capable'))) {
// Use 203 instead of 200 if HTTPRL is being used.
$default = 203;
}
$number = variable_get('advagg_http_200_code', $default);
header("{$_SERVER['SERVER_PROTOCOL']} {$number} OK");
// Insure the Last-Modified header is set so 304's work correctly.
if (file_exists($uri) && ($filemtime = @filemtime($uri))) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \\G\\M\\T', $filemtime));
// Etags generation in php is broken due to millisecond precision for the
// files mtime; apache has it, php does not.
}
else {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \\G\\M\\T', REQUEST_TIME));
}
// Set the Expires date 1 month into the future.
if (variable_get('advagg_farfuture_php', ADVAGG_FARFUTURE_PHP)) {
header('Expires: ' . gmdate('D, d M Y H:i:s \\G\\M\\T', REQUEST_TIME + 31 * 24 * 60 * 60));
}
// Also send an etag out.
header('Etag: ' . REQUEST_TIME . ' ' . $strlen);
if ($type === 'css') {
header("Content-Type: text/css");
}
elseif ($type === 'js') {
header("Content-Type: application/javascript; charset=UTF-8");
}
header('X-AdvAgg: Generated file at ' . REQUEST_TIME);
print $data;
exit;
}
else {
// Redirect and try again on failure.
$uri = advagg_generate_location_uri($filename, $type, $aggregate_settings);
++$redirect_counter;
$uri .= '?redirect_counter=' . $redirect_counter;
header('Location: ' . $uri, TRUE, 307);
exit;
}
}
/**
* Set various headers so the browser will cache the file for a long time.
*/
function advagg_missing_set_farfuture_headers() {
// Hat tip to the CDN module for the far future headers.
//
// 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: *");
// 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');
header_remove('ETag');
header_remove('Set-Cookie');
}
else {
header('Cache-Control:');
header('Cache-Control:');
header('ETag:');
header('ETag:');
header('Set-Cookie:');
header('Set-Cookie:');
}
// Set a far future Cache-Control header (52 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.
if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) {
header('Cache-Control: max-age=31449600, public, immutable');
}
else {
header('Cache-Control: max-age=31449600, public');
}
}
/**
* Given a filename create that file.
*
* @param string $filename
* Just the filename no path information.
* @param bool $no_alters
* (optional) Set to TRUE to do the bare amount of processing on the file.
* @param mixed $data
* (optional) Output from advagg_get_hashes_from_filename().
*
* @return mixed
* On failure a string saying why it failed.
* On success the $files_to_save array.
*/
function advagg_missing_create_file($filename, $no_alters = FALSE, $data = array()) {
// Option to still delever the file if fatal error.
register_shutdown_function("advagg_missing_fatal_handler", $filename);
if (empty($data)) {
$data = advagg_get_hashes_from_filename($filename);
}
if (is_array($data)) {
list($type, $aggregate_filenames_hash, $aggregate_contents_hash, $aggregate_settings) = $data;
}
else {
return $data;
}
if (empty($aggregate_settings)) {
$aggregate_settings = advagg_current_hooks_hash_array();
}
// Set no alters if this is the last chance of generating the aggregate.
if ($no_alters) {
$aggregate_settings['settings']['no_alters'] = TRUE;
}
// Get a list of files.
$files = advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash);
if (empty($files)) {
return t('Hashes do not match database.');
}
// Save aggregate file.
list($files_to_save, $errors) = advagg_save_aggregate($filename, $files, $type, $aggregate_settings);
// Update atime.
advagg_multi_update_atime(array(
array(
'aggregate_filenames_hash' => $aggregate_filenames_hash,
'aggregate_contents_hash' => $aggregate_contents_hash,
),
));
// Make sure .htaccess file exists in the advagg dir.
if (variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE)) {
advagg_htaccess_check_generate($files_to_save, $type);
}
// Return data.
return array(
$files_to_save,
$type,
$aggregate_filenames_hash,
$aggregate_contents_hash,
$aggregate_settings,
$files,
$errors,
);
}
/**
* Generate .htaccess rules and place them in advagg dir.
*
* @param array $files_to_save
* Array of files that where saved.
* @param string $type
* String: css or js.
* @param bool $force
* (Optional) force recreate the .htaccess file.
*
* @return array
* Empty array if not errors happened, list of errors if the write had any
* issues.
*/
function advagg_htaccess_check_generate(array $files_to_save, $type, $force = FALSE) {
list($css_path, $js_path) = advagg_get_root_files_dir();
$content_type = $type;
if ($content_type === 'js') {
$content_type = 'javascript';
$advagg_dir = basename($js_path[1]);
}
elseif ($content_type === 'css') {
$advagg_dir = basename($css_path[1]);
}
$type_upper = strtoupper($type);
$data = "\n";
// Some hosting companies do not allow "FollowSymLinks" but will support
// "SymLinksIfOwnerMatch".
if (variable_get('advagg_htaccess_symlinksifownermatch', ADVAGG_HTACCESS_SYMLINKSIFOWNERMATCH)) {
$data .= "Options +SymLinksIfOwnerMatch\n";
}
else {
$data .= "Options +FollowSymLinks\n";
}
if ($GLOBALS['base_path'] !== '/') {
$data .= "\n ErrorDocument 404 {$GLOBALS['base_path']}index.php\n";
}
// See if RewriteBase is needed.
$advagg_htaccess_rewritebase = trim(variable_get('advagg_htaccess_rewritebase', ADVAGG_HTACCESS_REWRITEBASE));
if (!empty($advagg_htaccess_rewritebase) && !empty($advagg_dir)) {
$rewrite_base_rule = str_replace('//', '/', $advagg_htaccess_rewritebase . '/' . $advagg_dir);
$data .= "RewriteBase {$rewrite_base_rule}\n";
}
$data .= "\n";
$data .= "<IfModule mod_rewrite.c>\n";
$data .= " RewriteEngine on\n";
$data .= " <IfModule mod_headers.c>\n";
$data .= " # Serve brotli compressed {$type_upper} files if they exist and the client accepts br.\n";
$data .= " RewriteCond %{HTTP:Accept-encoding} br\n";
$data .= " RewriteCond %{REQUEST_FILENAME}\\.br -s\n";
$data .= " RewriteRule ^(.*)\\.{$type} " . '$1' . "\\.{$type}\\.br [QSA]\n";
if ($type === 'css') {
$data .= " RewriteRule \\.{$type}\\.br\$ - [T=text/{$content_type},E=no-gzip:1]\n";
}
else {
$data .= " RewriteRule \\.{$type}\\.br\$ - [T=application/{$content_type},E=no-gzip:1]\n";
}
$data .= "\n";
$data .= " <FilesMatch \"\\.{$type}\\.br\$\">\n";
$data .= " # Serve correct encoding type.\n";
$data .= " Header set Content-Encoding br\n";
$data .= " # Force proxies to cache gzipped & non-gzipped css/js files separately.\n";
$data .= " Header append Vary Accept-Encoding\n";
$data .= " </FilesMatch>\n";
$data .= "\n";
$data .= " # Serve gzip compressed {$type_upper} files if they exist and the client accepts gzip.\n";
$data .= " RewriteCond %{HTTP:Accept-encoding} gzip\n";
$data .= " RewriteCond %{REQUEST_FILENAME}\\.gz -s\n";
$data .= " RewriteRule ^(.*)\\.{$type} " . '$1' . "\\.{$type}\\.gz [QSA]\n";
if ($type === 'css') {
$data .= " RewriteRule \\.{$type}\\.gz\$ - [T=text/{$content_type},E=no-gzip:1]\n";
}
else {
$data .= " RewriteRule \\.{$type}\\.gz\$ - [T=application/{$content_type},E=no-gzip:1]\n";
}
$data .= "\n";
$data .= " <FilesMatch \"\\.{$type}\\.gz\$\">\n";
$data .= " # Serve correct encoding type.\n";
$data .= " Header set Content-Encoding gzip\n";
$data .= " # Force proxies to cache gzipped & non-gzipped css/js files separately.\n";
$data .= " Header append Vary Accept-Encoding\n";
$data .= " </FilesMatch>\n";
$data .= " </IfModule>\n";
$data .= "</IfModule>\n";
$data .= "\n";
$data .= "<FilesMatch \"^{$type}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}.{$type}(\\.gz|\\.br)?\">\n";
$data .= " # No mod_headers. Apache module headers is not enabled.\n";
$data .= " <IfModule !mod_headers.c>\n";
$data .= " # No mod_expires. Apache module expires is not enabled.\n";
$data .= " <IfModule !mod_expires.c>\n";
$data .= " # Use ETags.\n";
$data .= " FileETag MTime Size\n";
$data .= " </IfModule>\n";
$data .= " </IfModule>\n";
$data .= "\n";
$data .= " # Use Expires Directive if apache module expires is enabled.\n";
$data .= " <IfModule mod_expires.c>\n";
$data .= " # Do not use ETags.\n";
$data .= " FileETag None\n";
$data .= " # Enable expirations.\n";
$data .= " ExpiresActive On\n";
$data .= " # Cache all aggregated {$type} files for 52 weeks after access (A).\n";
$data .= " ExpiresDefault A31449600\n";
$data .= " </IfModule>\n";
$data .= "\n";
$data .= " # Use Headers Directive if apache module headers is enabled.\n";
$data .= " <IfModule mod_headers.c>\n";
$data .= " # Do not use etags for cache validation.\n";
$data .= " Header unset ETag\n";
$data .= " # Serve correct content type.\n";
if ($type === 'css') {
$data .= " Header set Content-Type text/{$content_type}\n";
}
else {
$data .= " Header set Content-Type application/{$content_type}\n";
}
$data .= " <IfModule !mod_expires.c>\n";
$data .= " # Set a far future Cache-Control header to 52 weeks.\n";
if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) {
$data .= " Header set Cache-Control \"max-age=31449600, public, immutable\"\n";
}
else {
$data .= " Header set Cache-Control \"max-age=31449600, public\"\n";
}
$data .= " </IfModule>\n";
$data .= " <IfModule mod_expires.c>\n";
if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) {
$data .= " Header append Cache-Control \"public, immutable\"\n";
}
else {
$data .= " Header append Cache-Control \"public\"\n";
}
$data .= " </IfModule>\n";
$data .= " </IfModule>\n";
if ($type === 'css') {
$data .= " ForceType text/{$content_type}\n";
}
else {
$data .= " ForceType application/{$content_type}\n";
}
$data .= "</FilesMatch>\n";
$errors = array();
foreach (array_keys($files_to_save) as $uri) {
$dir = dirname($uri);
$htaccess_file = $dir . '/.htaccess';
if (!$force && file_exists($htaccess_file)) {
continue;
}
$errors = advagg_save_data($htaccess_file, $data, $force);
}
return $errors;
}
/**
* Given a filename return the type and 2 hashes.
*
* @param string $filename
* Just the filename no path information.
* @param bool $skip_hash_settings
* Allows for the skipping of db lookup for required file hooks.
*
* @return mixed
* On failure a string saying why it failed.
* On success array($ext, $aggregate_hash, $files_hash).
*/
function advagg_get_hashes_from_filename($filename, $skip_hash_settings = FALSE) {
// Verify requested filename has the correct pattern.
if (!advagg_match_file_pattern($filename)) {
return t('Wrong pattern.');
}
// Get the extension.
$ext = substr($filename, strpos($filename, '.', 131 + strlen(ADVAGG_SPACE) * 3) + 1);
// Set extraction points.
if ($ext === 'css') {
$aggregate_filenames_start = 3 + strlen(ADVAGG_SPACE);
$aggregate_contents_start = 46 + strlen(ADVAGG_SPACE) * 2;
$hooks_hashes_start = 89 + strlen(ADVAGG_SPACE) * 3;
}
elseif ($ext === 'js') {
$aggregate_filenames_start = 2 + strlen(ADVAGG_SPACE);
$aggregate_contents_start = 45 + strlen(ADVAGG_SPACE) * 2;
$hooks_hashes_start = 88 + strlen(ADVAGG_SPACE) * 3;
}
else {
return t('Wrong file type.');
}
// Extract info from wanted filename.
$aggregate_filenames_hash = substr($filename, $aggregate_filenames_start, 43);
$aggregate_contents_hash = substr($filename, $aggregate_contents_start, 43);
$hooks_hashes_value = substr($filename, $hooks_hashes_start, 43);
$aggregate_settings = array();
if (!$skip_hash_settings) {
// Verify that the hooks hashes is valid.
$aggregate_settings = advagg_get_hash_settings($hooks_hashes_value);
if (empty($aggregate_settings)) {
if (!variable_get('advagg_weak_file_verification', ADVAGG_WEAK_FILE_VERIFICATION)) {
return t('Bad hooks hashes value.');
}
elseif (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) {
watchdog('advagg-debug', 'File @filename has an empty aggregate_settings variable; the 3rd hash is incorrect.', array(
'@filename' => $filename,
), WATCHDOG_DEBUG);
}
}
}
return array(
$ext,
$aggregate_filenames_hash,
$aggregate_contents_hash,
$aggregate_settings,
);
}
/**
* Get the files that belong inside of this aggregate.
*
* @param string $type
* String: css or js.
* @param string $aggregate_filenames_hash
* Hash of the groupings of files.
* @param string $aggregate_contents_hash
* Hash of the files contents.
*
* @return array
* List of files in the order they should be included.
*/
function advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash) {
// Create main query for the advagg_aggregates_versions table.
$query = db_select('advagg_aggregates_versions', 'aav')
->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash)
->condition('aav.aggregate_contents_hash', $aggregate_contents_hash);
// Create join query for the advagg_aggregates table.
$subquery_aggregates = $query
->join('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash =
aav.aggregate_filenames_hash AND aa.aggregate_filenames_hash = :aggregate_filenames_hash', array(
':aggregate_filenames_hash' => $aggregate_filenames_hash,
));
// Create join query for the advagg_files table.
$query
->join('advagg_files', 'af', 'af.filename_hash = aa.filename_hash AND
af.filetype = :type AND af.filesize > 0', array(
':type' => $type,
));
// Select fields and ordering of the query; add in query comment as well.
$query = $query
->fields('af', array(
'filename',
))
->fields($subquery_aggregates, array(
'settings',
))
->orderBy('porder', 'ASC');
$query
->comment('Query called from ' . __FUNCTION__ . '()');
$results = $query
->execute();
// Add in files that are included in this aggregate.
$files = array();
foreach ($results as $value) {
$files[$value->filename] = unserialize($value->settings);
}
// Try again with weak file verification.
if (empty($files) && variable_get('advagg_weak_file_verification', ADVAGG_WEAK_FILE_VERIFICATION)) {
if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) {
watchdog('advagg-debug', 'Filehash @filename of type @type has an aggregate_contents_hash variable; the 2rd hash is incorrect.', array(
'@filename' => $aggregate_filenames_hash,
'@type' => $type,
), WATCHDOG_DEBUG);
}
// Create main query for the advagg_aggregates_versions table.
$query = db_select('advagg_aggregates_versions', 'aav')
->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash);
// Create join query for the advagg_aggregates table.
$subquery_aggregates = $query
->join('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash =
aav.aggregate_filenames_hash AND aa.aggregate_filenames_hash = :aggregate_filenames_hash', array(
':aggregate_filenames_hash' => $aggregate_filenames_hash,
));
// Create join query for the advagg_files table.
$query
->join('advagg_files', 'af', 'af.filename_hash = aa.filename_hash AND
af.filetype = :type', array(
':type' => $type,
));
// Select fields and ordering of the query; add in query comment as well.
$query = $query
->fields('af', array(
'filename',
))
->fields($subquery_aggregates, array(
'settings',
))
->orderBy('porder', 'ASC');
$query
->comment('Query called from ' . __FUNCTION__ . '()');
$results = $query
->execute();
// Add in files that are included in this aggregate.
$files = array();
foreach ($results as $value) {
$files[$value->filename] = unserialize($value->settings);
}
}
return $files;
}
/**
* Given a list of files, grab their contents and glue it into one big string.
*
* @param array $files
* Array of filenames.
* @param array $aggregate_settings
* Array of settings.
* @param string $aggregate_filename
* Filename of the aggregeate.
*
* @return string
* String containing all the files.
*/
function advagg_get_css_aggregate_contents(array $files, array $aggregate_settings, $aggregate_filename = '') {
$write_aggregate = TRUE;
// Check if CSS compression is enabled.
$optimize = TRUE;
if (!empty($aggregate_settings['settings']['no_alters'])) {
$optimize = FALSE;
}
if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) {
$optimize = FALSE;
}
module_load_include('inc', 'advagg', 'advagg');
$info_on_files = advagg_load_files_info_into_static_cache(array_keys($files));
$data = '';
if (!empty($files)) {
$media_changes = FALSE;
$last_media = NULL;
foreach ($files as $settings) {
if (!isset($settings['media'])) {
continue;
}
if (is_null($last_media)) {
$last_media = $settings['media'];
continue;
}
if ($settings['media'] !== $last_media) {
$media_changes = TRUE;
break;
}
}
if ($media_changes) {
$global_file_media = 'all';
}
else {
$global_file_media = $last_media;
}
// https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Media_queries
$media_types = array(
'all',
'aural',
'braille',
'handheld',
'print',
'projection',
'screen',
'tty',
'tv',
'embossed',
);
$import_statements = array();
module_load_include('inc', 'advagg', 'advagg');
$original_settings = array(
$optimize,
$aggregate_settings,
);
foreach ($files as $file => $settings) {
$media_changes = FALSE;
if (!isset($settings['media'])) {
$settings['media'] = '';
}
if ($settings['media'] !== $global_file_media) {
$media_changes = TRUE;
}
list($optimize, $aggregate_settings) = $original_settings;
// Allow other modules to modify aggregate_settings optimize.
// Call hook_advagg_get_css_file_contents_pre_alter().
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_get_css_file_contents_pre', $file, $optimize, $aggregate_settings);
}
if (is_readable($file)) {
// Get the files contents.
$file_contents = (string) @advagg_file_get_contents($file);
// Get a hash of the file's contents.
$file_contents_hash = drupal_hash_base64($file_contents);
$cid = 'advagg:file:' . advagg_drupal_hash_base64($file);
if (empty($info_on_files[$cid]['content_hash'])) {
// If hash was not in the cache, get it from the DB.
$results = db_select('advagg_files', 'af')
->fields('af', array(
'content_hash',
'filename_hash',
))
->condition('filename', $file)
->execute();
foreach ($results as $row) {
$info_on_files['advagg:file:' . $row->filename_hash]['content_hash'] = $row->content_hash;
}
}
if (isset($info_on_files[$cid]) == FALSE || $info_on_files[$cid]['content_hash'] !== $file_contents_hash) {
// If the content hash doesn't match don't write the file.
$write_aggregate = advagg_missing_file_not_readable($file, $aggregate_filename, FALSE);
}
$contents = advagg_load_css_stylesheet($file, $optimize, $aggregate_settings, $file_contents);
}
else {
// File is not readable.
$write_aggregate = advagg_missing_file_not_readable($file, $aggregate_filename, TRUE);
}
// Allow other modules to modify this files contents.
// Call hook_advagg_get_css_file_contents_alter().
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_get_css_file_contents', $contents, $file, $aggregate_settings);
}
if ($media_changes) {
$media_blocks = advagg_parse_media_blocks($contents);
$contents = '';
$file_has_type = FALSE;
if (!empty($settings['media'])) {
foreach ($media_types as $media_type) {
if (stripos($settings['media'], $media_type) !== FALSE) {
$file_has_type = TRUE;
break;
}
}
}
foreach ($media_blocks as $css_rules) {
if (strpos($css_rules, '@media') !== FALSE) {
// Get start and end of the rules for this media query block.
$start = strpos($css_rules, '{');
if ($start === FALSE) {
continue;
}
$end = strrpos($css_rules, '}');
if ($end === FALSE) {
continue;
}
// Get current media queries for this media block.
$media_rules = substr($css_rules, 6, $start - 6);
// Get everything else besides top level media query.
$css_selectors_rules = substr($css_rules, $start + 1, $end - ($start + 1));
// Add in main media rule if needed.
if (!empty($settings['media']) && strpos($media_rules, $settings['media']) === FALSE && $settings['media'] !== $global_file_media) {
$rule_has_type = FALSE;
if ($file_has_type) {
foreach ($media_types as $media_type) {
if (stripos($media_rules, $media_type) !== FALSE) {
$rule_has_type = TRUE;
break;
}
}
}
if (!$rule_has_type) {
$media_rules = $settings['media'] . ' and ' . $media_rules;
}
}
}
else {
$media_rules = $settings['media'];
$css_selectors_rules = $css_rules;
}
$media_rules = trim($media_rules);
// Pul all @font-face defentions inside the @media declaration above.
$font_face_string = '';
$font_blocks = advagg_parse_media_blocks($css_selectors_rules, '@font-face');
$css_selectors_rules = '';
foreach ($font_blocks as $rules) {
if (strpos($rules, '@font-face') !== FALSE) {
$font_face_string .= "\n {$rules}";
}
else {
$css_selectors_rules .= $rules;
}
}
$css_selectors_rules = str_replace("\n", "\n ", $css_selectors_rules);
$font_face_string = str_replace("\n", "\n ", $font_face_string);
// Wrap css in dedicated media query if it differs from the global
// media query and there actually are media rules.
if (!empty($media_rules) && $media_rules !== $global_file_media) {
$output = "{$font_face_string} \n@media {$media_rules} {\n {$css_selectors_rules} \n}";
}
else {
$output = "{$font_face_string} \n {$css_selectors_rules}";
}
$contents .= trim($output);
}
}
// Per the W3C specification at
// http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules
// must proceed any other style, so we move those to the top.
$regexp = '/@import[^;]+;/i';
preg_match_all($regexp, $contents, $matches);
$contents = preg_replace($regexp, '', $contents);
// Add the import statements with the media query of the current file.
$import_media = isset($settings['media']) ? $settings['media'] : '';
$import_media = trim($import_media);
$import_statements[] = array(
$import_media,
$matches[0],
);
// Close any open comment blocks.
$contents .= "\n/*})'\"*/\n";
if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) {
$contents .= "\n/* Above code came from {$file} */\n\n";
}
$data .= $contents;
}
// Add import statements to the top of the stylesheet.
$import_string = '';
foreach ($import_statements as $values) {
if ($media_changes) {
foreach ($values[1] as $statement) {
$import_string .= str_replace(';', $values[0] . ';', $statement);
}
}
else {
$import_string .= implode('', $values[1]);
}
}
$data = $import_string . $data;
}
// Allow other modules to modify this aggregates contents.
// Call hook_advagg_get_css_aggregate_contents_alter().
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_get_css_aggregate_contents', $data, $files, $aggregate_settings);
}
return array(
$data,
$write_aggregate,
);
}
/**
* Given a list of files, grab their contents and glue it into one big string.
*
* @param array $files
* Array of filenames.
* @param array $aggregate_settings
* Array of settings.
* @param string $aggregate_filename
* Filename of the aggregeate.
*
* @return string
* String containing all the files.
*/
function advagg_get_js_aggregate_contents(array $files, array $aggregate_settings, $aggregate_filename = '') {
$write_aggregate = TRUE;
$data = '';
module_load_include('inc', 'advagg', 'advagg');
$info_on_files = advagg_load_files_info_into_static_cache(array_keys($files));
if (!empty($files)) {
// Build aggregate JS file.
foreach ($files as $filename => $settings) {
$contents = '';
// Append a ';' and a newline after each JS file to prevent them from
// running together. Also close any comment blocks.
if (is_readable($filename)) {
$file_contents = (string) @advagg_file_get_contents($filename);
$file_contents_hash = drupal_hash_base64($file_contents);
$cid = 'advagg:file:' . advagg_drupal_hash_base64($filename);
if (empty($info_on_files[$cid]['content_hash'])) {
$results = db_select('advagg_files', 'af')
->fields('af', array(
'content_hash',
'filename_hash',
))
->condition('filename', $filename)
->execute();
foreach ($results as $row) {
$info_on_files['advagg:file:' . $row->filename_hash]['content_hash'] = $row->content_hash;
}
}
if (isset($info_on_files[$cid]['content_hash']) && $info_on_files[$cid]['content_hash'] !== $file_contents_hash) {
// If the content hash doesn't match don't write the file.
$write_aggregate = advagg_missing_file_not_readable($filename, $aggregate_filename, FALSE);
}
// Make sure that the file is ended properly.
$file_contents = trim($file_contents);
if (!empty($file_contents)) {
$file_contents .= "\n;/*})'\"*/\n";
}
if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) {
$file_contents .= "/* Above code came from {$filename} */\n\n";
}
$contents .= $file_contents;
}
else {
// File is not readable.
$write_aggregate = advagg_missing_file_not_readable($filename, $aggregate_filename, TRUE);
}
// Allow other modules to modify this files contents.
// Call hook_advagg_get_js_file_contents_alter().
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_get_js_file_contents', $contents, $filename, $aggregate_settings);
}
// Make sure that the file is ended properly.
$contents = trim($contents);
if (!empty($contents)) {
$contents .= ";/*})'\"*/\n";
}
$data .= $contents;
}
}
// Allow other modules to modify this aggregates contents.
// Call hook_advagg_get_js_aggregate_contents_alter().
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_get_js_aggregate_contents', $data, $files, $aggregate_settings);
}
return array(
$data,
$write_aggregate,
);
}
/**
* Let other modules know that this file couldn't be found.
*
* @param string $filename
* Filename of the missing file.
* @param string $aggregate_filename
* Filename of the aggregate that is trying to be generated.
* @param bool $fs_read_failure
* Set to TRUE if the file system couldn't be read.
*/
function advagg_missing_file_not_readable($filename, $aggregate_filename = '', $fs_read_failure = FALSE) {
$write_aggregate = FALSE;
$config_path = advagg_admin_config_root_path();
list($css_path, $js_path) = advagg_get_root_files_dir();
// Get cache of this report.
$cid = 'advagg:file_issue:' . drupal_hash_base64($filename);
$cache = cache_get($cid, 'cache_advagg_info');
// Let other modules know about this missing file.
// Call hook_advagg_missing_root_file().
module_invoke_all('advagg_missing_root_file', $aggregate_filename, $filename, $cache);
// Report to watchdog if this is not cached and it does not start in the
// public dir and the advagg dirs.
if (empty($cache) && strpos($filename, 'public://') !== 0 && strpos($filename, $css_path[1]) !== 0 && strpos($filename, $js_path[1]) !== 0) {
if ($fs_read_failure) {
watchdog('advagg', 'Reading from the file system failed. This can sometimes happen during a deployment and/or a clear cache operation. Filename: %file Aggregate Filename: %aggregate. If this continues to happen go to the <a href="@operations">Operations page</a> and under Drastic Measures - Reset the AdvAgg Files table click the Truncate advagg_files button.', array(
'%file' => $filename,
'%aggregate' => $aggregate_filename,
'@operations' => url('admin/config/development/performance/advagg/operations', array(
'fragment' => 'edit-reset-advagg-files',
)),
), WATCHDOG_WARNING);
}
else {
watchdog('advagg', 'The content hash for %file does not match the stored content hash from the database. Please <a href="@url">flush the advagg cache</a> under Smart Cache Flush. This can sometimes happen during a deployment. Filename: %file Aggregate Filename: %aggregate', array(
'%file' => $filename,
'%aggregate' => $aggregate_filename,
'@url' => url($config_path . '/advagg/operations', array(
'fragment' => 'edit-smart-flush',
)),
), WATCHDOG_WARNING);
}
cache_set($cid, TRUE, 'cache_advagg_info', CACHE_TEMPORARY);
}
elseif (!empty($cache) && $cache->created < REQUEST_TIME - variable_get('advagg_file_read_failure_timeout', ADVAGG_FILE_READ_FAILURE_TIMEOUT)) {
// Write the aggregate if it's been in a failure state for over 30 minutes.
$write_aggregate = TRUE;
}
return $write_aggregate;
}
/**
* Save an aggregate given a filename, the files included in it, and the type.
*
* @param string $filename
* Just the filename no path information.
* @param array $files
* Array of filenames.
* @param string $type
* String: css or js.
* @param array $aggregate_settings
* Array of settings.
*
* @return array
* array($files_to_save, $errors).
*/
function advagg_save_aggregate($filename, array $files, $type, array $aggregate_settings = array()) {
list($css_path, $js_path) = advagg_get_root_files_dir();
$uri = '';
if ($type === 'css') {
$uri = $css_path[0] . '/' . $filename;
}
elseif ($type === 'js') {
$uri = $js_path[0] . '/' . $filename;
}
if (empty($aggregate_settings)) {
$aggregate_settings = advagg_current_hooks_hash_array();
}
// Allow other modules to alter the location, files included, and settings.
if (empty($aggregate_settings['settings']['no_alters'])) {
// Call hook_advagg_save_aggregate_pre_alter().
drupal_alter('advagg_save_aggregate_pre', $uri, $files, $aggregate_settings);
}
// Build the aggregates contents.
$contents = '';
if ($type === 'css') {
list($contents, $write_aggregate) = advagg_get_css_aggregate_contents($files, $aggregate_settings, $filename);
}
elseif ($type === 'js') {
list($contents, $write_aggregate) = advagg_get_js_aggregate_contents($files, $aggregate_settings, $filename);
}
if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) {
$contents = "/* This aggregate contains the following files:\n" . implode(",\n", array_keys($files)) . ". */\n\n" . $contents;
}
// List of files to save.
$files_to_save = array(
$uri => $contents,
);
// Allow other modules to alter the contents and add new files to save.
// Call hook_advagg_save_aggregate_alter().
$other_parameters = array(
$files,
$type,
);
if (empty($aggregate_settings['settings']['no_alters'])) {
drupal_alter('advagg_save_aggregate', $files_to_save, $aggregate_settings, $other_parameters);
}
$errors = array();
if ($write_aggregate) {
foreach ($files_to_save as $uri => $data) {
$errors = advagg_save_data($uri, $data);
if (!file_exists($uri) || filesize($uri) == 0) {
if ($type === 'css') {
$full_dir = DRUPAL_ROOT . '/' . $css_path[1];
}
elseif ($type === 'js') {
$full_dir = DRUPAL_ROOT . '/' . $js_path[1];
}
$free_space = @disk_free_space($full_dir);
if ($free_space !== FALSE && strlen($data) > $free_space) {
watchdog('advagg', 'Write to file system failed. Disk is full. %uri. !errors. %full_dir.', array(
'%uri' => $uri,
'!errors' => print_r($errors, TRUE),
'%full_dir' => $full_dir,
), WATCHDOG_ALERT);
}
elseif (!is_writable($full_dir)) {
watchdog('advagg', 'Write to file system failed. Check directory permissions. %uri. !errors. %full_dir.', array(
'%uri' => $uri,
'!errors' => print_r($errors, TRUE),
'%full_dir' => $full_dir,
), WATCHDOG_ERROR);
}
else {
watchdog('advagg', 'Write to file system failed. %uri. !errors. %full_dir.', array(
'%uri' => $uri,
'!errors' => print_r($errors, TRUE),
'%full_dir' => $full_dir,
), WATCHDOG_ERROR);
}
// If the file is empty, remove it. Serving via drupal is better than an
// empty aggregate being served.
if (file_exists($uri) && filesize($uri) == 0) {
@unlink($uri);
}
}
}
}
return array(
$files_to_save,
$errors,
);
}
/**
* Save data to a file.
*
* This will use the rename operation ensuring atomic file operations.
*
* @param string $uri
* A string containing the destination location. This must be a stream wrapper
* URI.
* @param string $data
* A string containing the contents of the file.
* @param bool $overwrite
* (optional) Bool, set to TRUE to overwrite a file.
*
* @return array
* Empty array if not errors happened, list of errors if the write had any
* issues.
*/
function advagg_save_data($uri, $data, $overwrite = FALSE) {
$t = get_t();
$errors = array();
// Clear the stat cache.
module_load_include('inc', 'advagg', 'advagg');
advagg_clearstatcache($uri);
// Prepare dir if needed.
$dir = dirname($uri);
$dir_good = file_prepare_directory($dir, FILE_CREATE_DIRECTORY);
if (!$dir_good) {
$errors[1] = $t('The directory for @file can not be created or is not writable.', array(
'@file' => $uri,
));
return $errors;
}
// File already exists.
if (!$overwrite && file_exists($uri) && filesize($uri) > 0) {
if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) {
watchdog('advagg-debug', 'File @uri exists and overwrite is false.', array(
'@uri' => $uri,
), WATCHDOG_DEBUG);
}
$errors[2] = $t('File (@file) already exits.', array(
'@file' => $uri,
));
return $errors;
}
// If data is empty, write a space.
if (empty($data)) {
$data = ' ';
}
// Perform the replace operation. Since there could be multiple processes
// writing to the same file, the best option is to create a temporary file in
// the same directory and then rename it to the destination. A temporary file
// is needed if the directory is mounted on a separate machine; thus ensuring
// the rename command stays local and atomic.
//
// Get a temporary filename in the destination directory.
$dir = $uri_dir = drupal_dirname($uri) . '/';
// Corect the bug with drupal_tempnam where it doesn't pass subdirs to
// tempnam() if the dir is a stream wrapper.
$scheme = file_uri_scheme($uri_dir);
if ($scheme && file_stream_wrapper_valid_scheme($scheme)) {
$wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
if ($wrapper && method_exists($wrapper, 'getDirectoryPath')) {
$wrapper_dir_path = $wrapper
->getDirectoryPath();
if (!empty($wrapper_dir_path)) {
$dir = $wrapper_dir_path . '/' . substr($uri_dir, strlen($scheme . '://'));
$uri = $dir . substr($uri, strlen($uri_dir));
}
}
}
// Get the extension of the original filename and append it to the temp file
// name. Preserves the mime type in different stream wrapper implementations.
$parts = pathinfo($uri);
if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) {
$variables = array(
'@uri' => $uri,
);
watchdog('advagg-debug', 'Creating URI @uri', $variables, WATCHDOG_DEBUG);
$variables = array(
'@parts' => print_r($parts, TRUE),
);
watchdog('advagg-debug', 'File Parts <pre>@parts</pre>', $variables, WATCHDOG_DEBUG);
}
$extension = '.' . $parts['extension'];
if ($extension === '.gz' || $extension === '.br') {
$parts = pathinfo($parts['filename']);
$extension = '.' . $parts['extension'] . $extension;
}
// Create temp filename.
$temporary_file = $dir . 'advagg_file_' . drupal_hash_base64(microtime(TRUE) . mt_rand()) . $extension;
// Save to temporary filename in the destination directory.
$filepath = file_unmanaged_save_data($data, $temporary_file, FILE_EXISTS_REPLACE);
if ($filepath) {
// Perform the rename operation.
if (!advagg_rename($filepath, $uri)) {
// Unlink and try again for windows. Rename on windows does not replace
// the file if it already exists.
if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) {
watchdog('advagg-debug', 'Rename failed. @to', array(
'@to' => $uri,
), WATCHDOG_WARNING);
}
@unlink($uri);
// Remove temporary_file if rename failed.
if (!advagg_rename($filepath, $uri)) {
$errors[20] = $t('Renaming the filename (@incorrect) to (@correct) failed.', array(
'@incorrect' => $filepath,
'@correct' => $uri,
));
@unlink($filepath);
if (file_exists($filepath)) {
$errors[22] = $t('unlinking @file failed.', array(
'@file' => $filepath,
));
}
watchdog('advagg', 'Rename 4 failed. Current: %current Target: %target', array(
'%current' => $filepath,
'%target' => $uri,
), WATCHDOG_ERROR);
}
}
// Check the filesize.
$file_size = @filesize($uri);
$expected_size = _advagg_string_size_in_bytes($data);
if ($file_size === 0) {
// Zero byte file.
$errors[26] = $t('Write successful, but the file is empty. @file', array(
'@file' => $filepath,
));
watchdog('advagg', 'Write successful, but the file is empty. Target: target. The empty file has been removed. If this error continues, performance will be greatly degraded.', array(
'%target' => $uri,
), WATCHDOG_ERROR);
// Better to serve straight from Drupal than have a broken file.
@unlink($uri);
}
elseif ($file_size > 0 && $file_size != $expected_size) {
// Data written to disk doesn't match.
$errors[28] = $t('Write successful, but the file is the wrong size. @file Expected size is @expected_size, actual size is @file_size', array(
'@file' => $uri,
'@expected_size' => $expected_size,
'@file_size' => $file_size,
));
watchdog('advagg', 'Write successful, but the file is the wrong size. %file Expected size is %expected_size, actual size is %file_size. The broken file has been removed. If this error continues, performance will be greatly degraded.', array(
'%file' => $uri,
'%expected_size' => $expected_size,
'%file_size' => $file_size,
), WATCHDOG_ERROR);
// Better to serve straight from Drupal than have a broken file.
@unlink($uri);
}
}
else {
$errors[24] = $t('Write failed. @file', array(
'@file' => $temporary_file,
));
watchdog('advagg', 'Write failed. Target: %target', array(
'%target' => $temporary_file,
), WATCHDOG_ERROR);
}
// Cleanup leftover files.
if (file_exists($temporary_file)) {
@unlink($temporary_file);
}
if (file_exists($filepath)) {
@unlink($filepath);
}
return $errors;
}
/**
* Given a string, what is the size that it should be as a file?
*
* Code from http://stackoverflow.com/a/3511239/231914.
*
* @param string $string
* Input data to be sized in bytes.
*
* @return int
* Number of bytes this string uses.
*/
function _advagg_string_size_in_bytes($string) {
if (function_exists('mb_strlen')) {
return mb_strlen($string, '8bit');
}
else {
return strlen($string);
}
}
/**
* Rename; fallback to copy delete if this fails.
*
* @param string $source
* A string containing the source location.
* @param string $destination
* A string containing the destination location.
*
* @return mixed
* Destination string on success, FALSE on failure.
*/
function advagg_rename($source, $destination) {
$real_source = drupal_realpath($source);
$real_source = $real_source ? $real_source : $source;
$real_destination = drupal_realpath($destination);
$real_destination = $real_destination ? $real_destination : $destination;
// Try php rename.
if (!@rename($real_source, $real_destination)) {
// Try drupal move.
if (!file_unmanaged_move($source, $destination)) {
// Try file scheme's rename method if it exists.
$fs_wrapper = file_stream_wrapper_get_instance_by_scheme(file_uri_scheme($source));
if (!$fs_wrapper || !method_exists($fs_wrapper, 'rename') || !$fs_wrapper
->rename($source, $destination)) {
return FALSE;
}
}
}
return $destination;
}
/**
* Send out a fast 404 and exit.
*
* @param string $msg
* (optional) Small message reporting why the file didn't get created.
*/
function advagg_missing_fast404($msg = '') {
drupal_page_is_cacheable(FALSE);
// Strip new lines & separators and limit header message to 512 characters.
$msg = substr(preg_replace("/[^\\w\\. ]+/", "", $msg), 0, 512);
// Add in headers if possible.
if (!headers_sent()) {
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
header('X-AdvAgg: Failed validation. ' . $msg);
}
// Output fast 404 message and exit.
print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n";
print '<html xmlns="http://www.w3.org/1999/xhtml">';
print '<head><title>404 Not Found</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head>';
print '<body><h1>Not Found</h1>';
print '<p>The requested URL was not found on this server.</p>';
print '<p><a href="' . $GLOBALS['base_path'] . '">Home</a></p>';
print '<!-- advagg_missing_fast404 -->';
print '</body></html>';
exit;
}
/**
* Read the atime value for the given aggregate.
*
* @param string $aggregate_filenames_hash
* Hash of the groupings of files.
* @param string $aggregate_contents_hash
* Hash of the files contents.
* @param string $uri
* URI pointing to the aggregate file.
*
* @return mixed
* File atime or FALSE if not found.
*/
function advagg_get_atime($aggregate_filenames_hash, $aggregate_contents_hash, $uri) {
// Try to use the cache to avoid hitting the database with a select query.
$cache_id = 'advagg:db:' . $aggregate_filenames_hash . ADVAGG_SPACE . $aggregate_contents_hash;
$cache = cache_get($cache_id, 'cache_advagg_info');
if ($cache) {
// If the atime in the cache is less than 12 hours old, use that.
if (!empty($cache->data['atime']) && $cache->data['atime'] > REQUEST_TIME - 12 * 60 * 60) {
return $cache->data['atime'];
}
}
// Try to get the atime from the DB.
$atime = db_select('advagg_aggregates_versions', 'aav')
->fields('aav', array(
'atime',
))
->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash)
->condition('aav.aggregate_contents_hash', $aggregate_contents_hash)
->execute()
->fetchField();
if (!empty($atime)) {
return $atime;
}
// Return the atime from disk as a last resort.
if (file_exists($uri)) {
return fileatime($uri);
}
// No atime was found, return FALSE.
return FALSE;
}
/**
* Split up as CSS string by @media queries.
*
* @param string $css
* String of CSS.
* @param string $starting_string
* What to look for when starting to parse the string.
*
* @return array
* array of css with only media queries.
*
* @see http://stackoverflow.com/a/14145856/125684
*/
function advagg_parse_media_blocks($css, $starting_string = '@media') {
$media_blocks = array();
$start = 0;
$last_start = 0;
// Using the string as an array throughout this function.
// http://php.net/types.string#language.types.string.substr
while (($start = strpos($css, $starting_string, $start)) !== FALSE) {
// Stack to manage brackets.
$s = array();
// Get the first opening bracket.
$i = strpos($css, "{", $start);
// If $i is false, then there is probably a css syntax error.
if ($i === FALSE) {
continue;
}
// Push bracket onto stack.
array_push($s, $css[$i]);
// Move past first bracket.
++$i;
// Find the closing bracket for the @media statement. But ensure we don't
// overflow if there's an error.
while (!empty($s) && isset($css[$i])) {
// If the character is an opening bracket, push it onto the stack,
// otherwise pop the stack.
if ($css[$i] === "{") {
array_push($s, "{");
}
elseif ($css[$i] === "}") {
array_pop($s);
}
++$i;
}
// Get CSS before @media and store it.
if ($last_start != $start) {
$insert = trim(substr($css, $last_start, $start - $last_start));
if (!empty($insert)) {
$media_blocks[] = $insert;
}
}
// Cut @media block out of the css and store.
$media_blocks[] = trim(substr($css, $start, $i - $start));
// Set the new $start to the end of the block.
$start = $i;
$last_start = $start;
}
// Add in any remaining css rules after the last @media statement.
if (strlen($css) > $last_start) {
$insert = trim(substr($css, $last_start));
if (!empty($insert)) {
$media_blocks[] = $insert;
}
}
return $media_blocks;
}
/**
* Given a filename create that file; usually works if PHP goes fatal.
*
* @param string $filename
* Just the filename no path information.
*
* @return mixed
* On failure a string saying why it failed.
* On success the $files_to_save array.
*/
function advagg_missing_fatal_handler($filename) {
static $counter = 0;
// Bail out if there is no error.
$error = error_get_last();
if ($error === NULL) {
return;
}
$counter++;
// Bail out if this is still in a loop.
if ($counter > 2) {
return;
}
// Bail out if the file already exists.
$data = advagg_get_hashes_from_filename($filename);
$type = $data[0];
list($css_path, $js_path) = advagg_get_root_files_dir();
$uri = '';
if ($type === 'css') {
$uri = $css_path[0] . '/' . $filename;
}
elseif ($type === 'js') {
$uri = $js_path[0] . '/' . $filename;
}
if (file_exists($uri)) {
return;
}
// Generate the file with no alters.
set_time_limit(0);
$return = advagg_missing_create_file($filename, TRUE);
if (is_array($return) && !headers_sent()) {
$redirect_counter = isset($_GET['redirect_counter']) ? intval($_GET['redirect_counter']) : 0;
// 307 if headers have not been sent yet.
$uri = advagg_generate_location_uri($filename, $data[0], $data[3]);
++$redirect_counter;
$uri .= '?redirect_counter=' . $redirect_counter;
header('Location: ' . $uri, TRUE, 307);
exit;
}
}
Functions
Name | Description |
---|---|
advagg_generate_location_uri | Given the filename, type, and settings, create absolute URL for 307 redirect. |
advagg_get_atime | Read the atime value for the given aggregate. |
advagg_get_css_aggregate_contents | Given a list of files, grab their contents and glue it into one big string. |
advagg_get_files_from_hashes | Get the files that belong inside of this aggregate. |
advagg_get_hashes_from_filename | Given a filename return the type and 2 hashes. |
advagg_get_js_aggregate_contents | Given a list of files, grab their contents and glue it into one big string. |
advagg_htaccess_check_generate | Generate .htaccess rules and place them in advagg dir. |
advagg_missing_aggregate | Menu Callback; generates a missing CSS/JS file. |
advagg_missing_create_file | Given a filename create that file. |
advagg_missing_fast404 | Send out a fast 404 and exit. |
advagg_missing_fatal_handler | Given a filename create that file; usually works if PHP goes fatal. |
advagg_missing_file_not_readable | Let other modules know that this file couldn't be found. |
advagg_missing_generate | Generates a missing CSS/JS file and send it to client. |
advagg_missing_send_saved_file | Send the css/js file to the client. |
advagg_missing_set_farfuture_headers | Set various headers so the browser will cache the file for a long time. |
advagg_parse_media_blocks | Split up as CSS string by @media queries. |
advagg_rename | Rename; fallback to copy delete if this fails. |
advagg_save_aggregate | Save an aggregate given a filename, the files included in it, and the type. |
advagg_save_data | Save data to a file. |
_advagg_string_size_in_bytes | Given a string, what is the size that it should be as a file? |