Advanced CSS/JS aggregation module.

Functions used to generate a file given the filename.

Fichier

./advagg.missing.inc

View 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

Titre Deprecated Résumé
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?