Advanced aggregation relocate module.

File

advagg_relocate/advagg_relocate.advagg.inc

View source
<?php


/**
 * @file
 * Advanced aggregation relocate module.
 */

/**
 * @addtogroup advagg_hooks
 * @{
 */

/**
 * Implements hook_advagg_get_info_on_files_alter().
 */
function advagg_relocate_advagg_get_info_on_files_alter(&$return, $cached_data, $bypass_cache) {
    $aggregate_settings = advagg_current_hooks_hash_array();
    // Check external js setting.
    if (empty($aggregate_settings['variables']['advagg_relocate_js'])) {
        return;
    }
    foreach ($return as $key => &$info) {
        // Skip if not a js file.
        if (empty($info['fileext']) || $info['fileext'] !== 'js') {
            continue;
        }
        // Get the file contents.
        $file_contents = (string) @advagg_file_get_contents($info['data']);
        if (empty($file_contents)) {
            continue;
        }
        $value['data'] = $file_contents;
        $scripts_found = advagg_relocate_js_script_rewrite_list($key, $value, $aggregate_settings);
        if (!empty($scripts_found)) {
            $info['advagg_relocate'] = $scripts_found;
        }
    }
}

/**
 * Implements hook_advagg_get_js_file_contents_alter().
 */
function advagg_relocate_advagg_get_js_file_contents_alter(&$contents, $filename, $aggregate_settings) {
    // Do nothing if this is disabled.
    if (empty($aggregate_settings['variables']['advagg_relocate_js'])) {
        return;
    }
    module_load_include('inc', 'advagg', 'advagg');
    // Get info on file.
    $info = advagg_get_info_on_file($filename);
    if (!empty($info['advagg_relocate'])) {
        // Set values so it thinks its an inline script.
        $value['data'] =& $contents;
        $key = $filename;
        $scripts_found = advagg_relocate_js_script_rewrite_list($key, $value, $aggregate_settings);
        // Do rewrite of the data.
        if (!empty($scripts_found)) {
            $js[$key] =& $value;
            advagg_relocate_js_script_rewrite($js, $scripts_found);
        }
    }
}

/**
 * Implements hook_advagg_get_css_aggregate_contents_alter().
 */
function advagg_relocate_advagg_get_css_aggregate_contents_alter(&$data, $files, $aggregate_settings) {
    // Set variables if needed.
    if (!isset($aggregate_settings['variables']['advagg_relocate_css_inline_import'])) {
        $aggregate_settings['variables']['advagg_relocate_css_inline_import'] = variable_get('advagg_relocate_css_inline_import', ADVAGG_RELOCATE_CSS_INLINE_IMPORT);
    }
    // Do nothing if this is disabled.
    if (empty($aggregate_settings['variables']['advagg_relocate_css_inline_import'])) {
        return;
    }
    if (strpos($data, '@import') !== FALSE) {
        // Set values that will be used when preg_replace_callback is ran.
        _advagg_relocate_callback(array(), $files, $aggregate_settings);
        // Replace external import statements with the contents of them.
        $data = preg_replace_callback('%@import\\s*+(?:url\\(\\s*+)?+[\'"]?+((?:http:\\/\\/|https:\\/\\/|\\/\\/)(?:[^\'"()\\s]++))[\'"]?+\\s*+\\)?+\\s*+;%i', '_advagg_relocate_callback', $data);
        // 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.
        $matches = array();
        $regexp = '/@import[^;]+;/i';
        preg_match_all($regexp, $data, $matches);
        $data = preg_replace($regexp, '', $data);
        // Add import statements to the top of the stylesheet.
        $data = implode("\n", $matches[0]) . $data;
    }
}

/**
 * Implements hook_advagg_relocate_process_http_request_alter().
 */
function advagg_relocate_advagg_relocate_process_http_request_alter(&$response, $type) {
    if ($type !== 'js' || strpos($response->url, 'connect.facebook.net/en_US/fbevents.js') === FALSE) {
        return 1;
    }
    // Fix loader so it works if not loaded from connect.facebook.net.
    $base_fb_url = 'https://connect.facebook.net';
    $matches = array();
    $pattern = '/function\\s+([\\w]{1,2})\\(\\)\\s*\\{\\s*var\\s+([\\w]{1,2})\\s*=\\s*null,\\s*([\\w]{1,2})\\s*=\\s*null,\\s*([\\w]{1,2})\\s*=\\s*([\\w]{1,2})\\.getElementsByTagName\\([\'"]script[\'"]\\)/';
    preg_match($pattern, $response->data, $matches);
    // Bail out if not matched.
    if (empty($matches[0])) {
        return 2;
    }
    // Transform
    // function B(){var E=null,F=null,G=b.getElementsByTagName("script");
    // to
    // function B(){var E="https://connect.facebook.net",G=b.getElementsByTagName("script"),F=G[0].
    $response->data = str_replace($matches[0], "function {$matches[1]}(){var {$matches[2]}=\"{$base_fb_url}\",{$matches[4]}={$matches[5]}.getElementsByTagName(\"script\"),{$matches[3]}={$matches[4]}[0]", $response->data);
    // Get Facebook IDs.
    $fb_ids = array_filter(array_map('trim', explode("\n", variable_get('advagg_relocate_js_fbevents_local_ids', ADVAGG_RELOCATE_JS_FBEVENTS_LOCAL_IDS))));
    if (empty($fb_ids)) {
        return 3;
    }
    // Get Facebook Version.
    $matches = array();
    $pattern = '/fbq.version\\s*=\\s*[\'"]([\\.\\d]+)[\'"]/';
    preg_match($pattern, $response->data, $matches);
    if (empty($matches[1])) {
        return 4;
    }
    $version = $matches[1];
    // Get Release Segment.
    $segment = 'stable';
    $matches = array();
    $pattern = '/fbq._releaseSegment\\s*=\\s*[\'"](.+)[\'"]/';
    preg_match($pattern, $response->data, $matches);
    if (!empty($matches[1])) {
        $segment = $matches[1];
    }
    // Update local copies of the /signals/config/ js.
    $js = array();
    foreach ($fb_ids as $fb_id) {
        $url = "{$base_fb_url}/signals/config/{$fb_id}?v={$version}&r={$segment}";
        $js[$url]['data'] = $url;
        $js[$url]['type'] = 'external';
        $js[$url]['#fbid'] = "config{$fb_id}";
        $url = "{$base_fb_url}/signals/plugins/{$fb_id}?v={$version}&r={$segment}";
        $js[$url]['data'] = $url;
        $js[$url]['type'] = 'external';
        $js[$url]['#fbid'] = "plugins{$fb_id}";
    }
    if (!empty($js)) {
        advagg_relocate_js_post_alter($js, TRUE);
    }
    // Get a list of the local copies for this version.
    $local_copies = array();
    foreach ($js as $values) {
        if ($values['type'] === 'file') {
            // Create an aggregate just for this file.
            $values += drupal_js_defaults($values);
            $elements = array(
                $values,
            );
            $groups = advagg_group_js($elements);
            _advagg_aggregate_js($groups);
            if (isset($groups[0]['data'])) {
                $local_copies[$values['#fbid']] = advagg_file_create_url($groups[0]['data']);
            }
        }
    }
    if (empty($local_copies)) {
        return 5;
    }
    // Add the local copies to the js file.
    $local_copies = json_encode($local_copies);
    $matches = array();
    $pattern = '/return\\s*\\{\\s*baseURL:\\s*([\\w]{1,2}),\\s*scriptElement:\\s*([\\w]{1,2})\\s*\\}/';
    preg_match($pattern, $response->data, $matches);
    // Bail out if not matched.
    if (empty($matches[0])) {
        return 6;
    }
    // Transform
    // return{baseURL:E,scriptElement:F}
    // to
    // return{baseURL:E,scriptElement:F,localCopies:ARRAY_OF_LOCAL_JS_FILES}.
    $response->data = str_replace($matches[0], "return{baseURL:{$matches[1]},scriptElement:{$matches[2]},localCopies:{$local_copies}}", $response->data);
    // Change logic so it'll use the local copy if it exists.
    $matches = array();
    $pattern = '/([\\w]{1,2})\\s*=\\s*([\\w]{1,2})\\.baseURL;\\s*var\\s+([\\w]{1,2})\\s*=\\s*([\\w]{1,2})\\s*\\+\\s*[\'"]\\/signals\\/config\\/[\'"]\\s*\\+\\s*([\\w]{1,2})\\s*\\+\\s*[\'"]\\?v=[\'"]\\s*\\+\\s*([\\w]{1,2})\\s*\\+\\s*[\'"]\\&r=[\'"]\\s*\\+\\s*([\\w]{1,2}),\\s*([\\w]{1,2})\\s*=\\s*([\\w]{1,2})\\.createElement\\([\'"]script[\'"]\\);\\s*[\\w]{1,2}.src\\s*=\\s*[\\w]{1,2};/';
    preg_match($pattern, $response->data, $matches);
    // Bail out if not matched.
    if (empty($matches[0])) {
        return 7;
    }
    // Transform
    // 1 2             3 4                    5       6       7 8 9                         8     3
    // I=H.baseURL;var J=I+"/signals/config/"+E+"?v="+F+"&r="+G,K=b.createElement("script");K.src=J;
    // to
    // 1 2             3 4                    5       6       7 8 9                            2              2                      5   3 2                      5   8     3
    // I=H.baseURL;var J=I+"/signals/config/"+E+"?v="+F+"&r="+G,K=b.createElement("script");if(H.localCopies&&H.localCopies["config"+E]){J=H.localCopies["config"+E];}K.src=J;.
    $response->data = str_replace($matches[0], "{$matches[1]}={$matches[2]}.baseURL;var {$matches[3]}={$matches[4]}+\"/signals/config/\"+{$matches[5]}+\"?v=\"+{$matches[6]}+\"&r=\"+{$matches[7]},{$matches[8]}={$matches[9]}.createElement(\"script\");if({$matches[2]}.localCopies&&{$matches[2]}.localCopies[\"config\"+{$matches[5]}]){{$matches[3]}={$matches[2]}.localCopies[\"config\"+{$matches[5]}];}{$matches[8]}.src={$matches[3]};", $response->data);
    // Change logic so it'll use the local copy if it exists.
    $matches = array();
    $pattern = '/if\\s*\\(([\\w]{1,2})\\.baseURL\\s*\\&\\&\\s*([\\w]{1,2})\\.scriptElement\\)\\s*\\{\\s*var\\s+([\\w]{1,2})\\s*\\=\\s*([\\w]{1,2})\\.baseURL\\s*\\+\\s*[\'"]\\/signals\\/plugins\\/[\'"]\\s*\\+\\s*([\\w]{1,2})\\s*\\+\\s*[\'"]\\.js\\?v\\=[\'"]\\s*\\+\\s*([\\w]{1,2})\\.version;/';
    preg_match($pattern, $response->data, $matches);
    // Bail out if not matched.
    if (empty($matches[0])) {
        return 8;
    }
    // Transform
    // if(C.baseURL&&C.scriptElement){var D=C.baseURL+"/signals/plugins/"+A+".js?v="+g.version;
    // to
    // if(C.baseURL&&C.scriptElement){var D=C.baseURL+"/signals/plugins/"+A+".js?v="+g.version;if(C.localCopies&&C.localCopies["plugins"+A]){D=C.localCopies["plugins"+A];}.
    $response->data = str_replace($matches[0], "if({$matches[1]}.baseURL&&{$matches[2]}.scriptElement){var {$matches[3]}={$matches[4]}.baseURL+\"/signals/plugins/\"+{$matches[5]}+\".js?v=\"+{$matches[6]}.version;if({$matches[1]}.localCopies&&{$matches[1]}.localCopies[\"plugins\"+{$matches[5]}]){{$matches[3]}={$matches[1]}.localCopies[\"plugins\"+{$matches[5]}];}", $response->data);
}

/**
 * @} End of "addtogroup advagg_hooks".
 */

/**
 * Gets external CSS files and puts the contents of it in the aggregate.
 *
 * @param array $matches
 *   Array of matched items from preg_replace_callback().
 * @param array $files
 *   List of files with the media type.
 * @param array $aggregate_settings
 *   Array of settings.
 *
 * @return string
 *   Contents of the import statement.
 */
function _advagg_relocate_callback(array $matches = array(), array $files = array(), array $aggregate_settings = array()) {
    // Store values for preg_replace_callback callback.
    $_args =& drupal_static(__FUNCTION__, array());
    if (!empty($files)) {
        $_args['files'] = $files;
    }
    if (!empty($aggregate_settings)) {
        $_args['aggregate_settings'] = $aggregate_settings;
    }
    // Short circuit if no matches were passed in.
    if (empty($matches)) {
        return '';
    }
    // Bail if not matched.
    if (empty($matches[1])) {
        return $matches[0];
    }
    // Check URL.
    if (!advagg_relocate_check_domain_of_font_url($matches[1], $_args['aggregate_settings'])) {
        return $matches[0];
    }
    // Check per file settings.
    if (!isset($_args['aggregate_settings']['variables']['advagg_relocate_css_file_settings'])) {
        $_args['aggregate_settings']['variables']['advagg_relocate_css_file_settings'] = variable_get('advagg_relocate_css_file_settings', array());
    }
    $key_to_check = str_replace(array(
        '=',
        '&',
        ' ',
    ), array(
        '_',
        '-',
        '-',
    ), $matches[1]);
    foreach ($_args['files'] as $filename => $values) {
        $form_api_filename = str_replace(array(
            '/',
            '.',
            '=',
            '&',
            ' ',
        ), array(
            '__',
            '--',
            '_',
            '-',
            '+',
        ), $filename);
        // All has been checked; good to go.
        if (!empty($_args['aggregate_settings']['variables']['advagg_relocate_css_file_settings']["all:{$form_api_filename}"])) {
            continue;
        }
        // This file is good to be inlined.
        if (!empty($_args['aggregate_settings']['variables']['advagg_relocate_css_file_settings'][$form_api_filename][$key_to_check])) {
            continue;
        }
        // No go, return unaltered.
        return $matches[0];
    }
    $font_faces = advagg_relocate_get_remote_font_data($matches[1], $_args['aggregate_settings']);
    return advagg_relocate_font_face_parser($font_faces);
}

/**
 * Given an array of font data output a new CSS string.
 *
 * @param array $font_faces
 *   Array of font data.
 *
 * @return string
 *   String of CSS font data.
 */
function advagg_relocate_font_face_parser(array $font_faces) {
    $new_css = '';
    foreach ($font_faces as $values => $src) {
        $output = '';
        $output .= str_replace('; ', ";\n", $values);
        if (isset($src['eot'])) {
            $output .= "src: {$src['eot']};\n";
        }
        $output .= 'src:';
        foreach ($src as $key => $location) {
            if (is_numeric($key)) {
                $output .= "{$location},";
            }
        }
        if (isset($src['eot'])) {
            $src['eot'] = str_replace('.eot', '.eot?#iefix', $src['eot']);
            $output .= "{$src['eot']} format('embedded-opentype'),";
        }
        if (isset($src['woff2'])) {
            $output .= "{$src['woff2']},";
        }
        if (isset($src['woff'])) {
            $output .= "{$src['woff']},";
        }
        if (isset($src['ttf'])) {
            $output .= "{$src['ttf']},";
        }
        if (isset($src['svg'])) {
            $output .= "{$src['svg']},";
        }
        $output = str_replace(array(
            '),l',
            '),u',
        ), array(
            "),\nl",
            "),\nu",
        ), trim($output, ',') . ';');
        $new_css .= "@font-face {\n{$output}\n}\n";
    }
    return $new_css;
}

/**
 * Gets external CSS and JS files; caches and returns response.
 *
 * @param string $urls
 *   URLs to get.
 * @param string $type
 *   Will be css or js.
 * @param array $options
 *   Array of settings for the http request.
 * @param bool $force_check
 *   TRUE if you want to force check the external source.
 *
 * @return array
 *   Array of http responses.
 */
function advagg_relocate_get_remote_data($urls, $type, array $options = array(), $force_check = FALSE) {
    // Set arguments for drupal_http_request().
    $options += array(
        'headers' => array(
            'Accept-Encoding' => 'gzip, deflate',
            'Connection' => 'close',
            'Referer' => $GLOBALS['base_root'] . request_uri(),
        ),
        'timeout' => 8,
        'version' => '1.0',
    );
    if (function_exists('brotli_uncompress')) {
        $options['headers']['Accept-Encoding'] .= ', br';
    }
    // Build CID.
    $cids = array();
    foreach ($urls as $k => $v) {
        $cids["advagg_relocate_{$type}_external:{$k}"] = "advagg_relocate_{$type}_external:{$k}";
    }
    // Try local cache.
    $return = array();
    $responses = array();
    $cached_data = cache_get_multiple($cids, 'cache_advagg_info');
    $cached_data = array_merge($cids, $cached_data);
    $url_to_cid = array();
    $request_sent = FALSE;
    foreach ($cached_data as $cid => $cache) {
        // CID not set, skip.
        if (empty($cid)) {
            continue;
        }
        // Set cid, filename and get url.
        $options['cid'] = $cid;
        $options['filename'] = substr($cid, 26 + strlen($type));
        // Filename lookup failure, skip.
        if (empty($urls[$options['filename']])) {
            continue;
        }
        // Add url to the lookup array.
        $url = advagg_force_https_path($urls[$options['filename']]);
        $url_to_cid[$url] = $cid;
        // Reset headers if needed.
        if (isset($options['headers']['If-None-Match'])) {
            unset($options['headers']['If-None-Match']);
        }
        if (isset($options['headers']['If-Modified-Since'])) {
            unset($options['headers']['If-Modified-Since']);
        }
        // Use cached data or setup for 304.
        if (!empty($cache->data)) {
            if ($cache->expire >= REQUEST_TIME && isset($cache->data->url) && empty($force_check)) {
                $return[$cache->data->url] = $cache->data;
                continue;
            }
            else {
                // Set header for 304 response.
                if (isset($cached_data->data->headers['etag'])) {
                    $options['headers']['If-None-Match'] = $cached_data->data->headers['etag'];
                }
                if (isset($cached_data->created)) {
                    $options['headers']['If-Modified-Since'] = gmdate('D, d M Y H:i:s T', $cached_data->created);
                }
            }
        }
        // Get data.
        if (module_exists('httprl')) {
            $request_sent = TRUE;
            httprl_request($url, $options);
        }
        else {
            $request_sent = TRUE;
            $responses[$url] = drupal_http_request($url, $options);
            if (!isset($responses[$url]->options)) {
                $responses[$url]->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses[$url]->url = $url;
            }
        }
    }
    if ($request_sent && module_exists('httprl')) {
        $responses = httprl_send_request();
    }
    if (empty($responses)) {
        return $return;
    }
    // Try failures again.
    advagg_relocate_try_failures_again($responses);
    // Process remote data.
    foreach ($responses as $url => $response) {
        // Content length does not match the response data.
        if (!empty($response->headers['content-length']) && $response->headers['content-length'] > strlen($response->data)) {
            continue;
        }
        // No url is a no go.
        if (empty($response->url)) {
            $response->url = $url;
        }
        if (isset($response->options['cid'])) {
            $cid = $response->options['cid'];
        }
        elseif (isset($url_to_cid[$response->url])) {
            $cid = $url_to_cid[$response->url];
        }
        else {
            // Can't match up url to the cid.
            continue;
        }
        // Update object.
        if (!isset($response->options['filename'])) {
            $response->options['filename'] = substr($cid, 26 + strlen($type));
        }
        if (!isset($response->options['cid'])) {
            $response->options['cid'] = $cid;
        }
        advagg_relocate_process_http_request($response, $type);
        if ($response->code == 304 && !empty($cached_data->data)) {
            // Update cache expire time.
            cache_set($cid, $cached_data->data, 'cache_advagg_info', REQUEST_TIME + $response->ttl);
            // Return cached data.
            $return[$cached_data->data->url] = $cached_data->data;
        }
        // Skip if not a 200.
        if ($response->code != 200 && $response->code != 201 && $response->code != 202 && $response->code != 206) {
            continue;
        }
        if (empty($response->data)) {
            continue;
        }
        $response->local_cache = FALSE;
        // Save data to the cache.
        if (!empty($response->data)) {
            $response->hash = drupal_hash_base64($response->data);
            $response->local_cache = TRUE;
            cache_set($cid, $response, 'cache_advagg_info', REQUEST_TIME + $response->ttl);
            $response->local_cache = FALSE;
        }
        $return[$response->url] = $response;
    }
    return $return;
}

/**
 * Get the TTL and fix UTF-8 encoding.
 *
 * @param object $response
 *   Response from http request.
 * @param string $type
 *   Can be css, js, or font.
 */
function advagg_relocate_process_http_request(&$response, $type) {
    // Get ttl from response.
    $ttl = 0;
    if ($type === 'css') {
        $ttl = variable_get('advagg_relocate_css_min_ttl', ADVAGG_RELOCATE_CSS_MIN_TTL);
    }
    if ($type === 'js') {
        $ttl = variable_get('advagg_relocate_js_min_ttl', ADVAGG_RELOCATE_JS_MIN_TTL);
    }
    $now = REQUEST_TIME;
    if (isset($response->headers['expires'])) {
        $expires = strtotime($response->headers['expires']);
        if (isset($response->headers['date'])) {
            $now = max($now, strtotime($response->headers['date']));
        }
        $ttl = max($ttl, $expires - $now);
    }
    if (isset($response->headers['cache-control'])) {
        $cache_control_array = advagg_relocate_parse_cache_control($response->headers['cache-control']);
        if (isset($cache_control_array['max-age']) && is_numeric($cache_control_array['max-age'])) {
            $ttl = max($ttl, $cache_control_array['max-age']);
        }
        if (isset($cache_control_array['s-maxage']) && is_numeric($cache_control_array['s-maxage'])) {
            $ttl = max($ttl, $cache_control_array['s-maxage']);
        }
    }
    $response->ttl = $ttl;
    // If a BOM is found, convert the string to UTF-8.
    if (isset($response->data)) {
        $encoding = advagg_get_encoding_from_bom($response->data);
        if (!empty($encoding)) {
            $response->data = advagg_convert_to_utf8($response->data, $encoding);
        }
    }
    // Call hook_advagg_relocate_process_http_request_alter().
    drupal_alter('advagg_relocate_process_http_request', $response, $type);
}

/**
 * Decompress the data.
 *
 * @param object $response
 *   Response from http request.
 *
 * @return bool
 *   FALSE if something went wrong.
 */
function advagg_relocate_uncompress_data(&$response) {
    // Uncompress.
    if (!empty($response->headers['content-encoding']) && !empty($response->data) && (!isset($response->chunk_size) || !empty($response->headers['content-length']) && $response->headers['content-length'] == strlen($response->data)) && ($response->headers['content-encoding'] === 'gzip' || $response->headers['content-encoding'] === 'deflate' || $response->headers['content-encoding'] === 'br')) {
        // Do the first level of decoding if not already done.
        if ($response->headers['content-encoding'] === 'gzip') {
            $chunk = @gzinflate(substr($response->data, 10));
        }
        elseif ($response->headers['content-encoding'] === 'deflate') {
            $chunk = @gzinflate($response->data);
        }
        elseif ($response->headers['content-encoding'] === 'br' && is_callable('brotli_uncompress')) {
            $chunk = @brotli_uncompress($response->data);
        }
        if (isset($chunk)) {
            if ($chunk !== FALSE) {
                $response->data = $chunk;
            }
            else {
                return FALSE;
            }
        }
    }
    return TRUE;
}

/**
 * Detect failures and try again.
 *
 * @param array $responses
 *   An array of $response objects from a http request.
 */
function advagg_relocate_try_failures_again(array &$responses) {
    // Try failures again.
    foreach ($responses as $key => &$response) {
        // Strlen doesn't match.
        if (!empty($response->headers['content-length']) && $response->headers['content-length'] > strlen($response->data)) {
            $response->code = 0;
            if (isset($response->options['headers']['connection'])) {
                unset($response->options['headers']['connection']);
            }
        }
        // Decode data.
        $decode_ok = advagg_relocate_uncompress_data($response);
        if (!$decode_ok) {
            // If decoding failed try again.
            if (isset($response->options['headers']['Accept-Encoding'])) {
                unset($response->options['headers']['Accept-Encoding']);
            }
            $response->code = 0;
        }
        // Try again if code is empty.
        if (empty($response->code)) {
            $url = $response->url;
            $options = $response->options;
            $responses[$key] = drupal_http_request($url, $options);
            if (!isset($responses[$key]->options)) {
                $responses[$key]->options = $options;
            }
            if (!isset($responses[$key]->url)) {
                $responses[$key]->url = $url;
            }
        }
    }
    // Try failures again, but force http.
    foreach ($responses as $key => $response) {
        if (empty($response->code)) {
            $url = advagg_force_http_path($response->url);
            $options = $response->options;
            $responses[$key] = drupal_http_request($url, $options);
            if (!isset($responses[$key]->options)) {
                $responses[$key]->options = $options;
            }
            if (!isset($responses[$key]->url)) {
                $responses[$key]->url = $url;
            }
        }
    }
}

/**
 * Gets external CSS files; caches it and returns css font rules.
 *
 * @param string $url
 *   URL of the CSS file to import.
 * @param array $aggregate_settings
 *   Array of settings.
 *
 * @return array
 *   Array of font data.
 */
function advagg_relocate_get_remote_font_data($url, array $aggregate_settings) {
    // Set default settings if needed.
    $font_type_defaults = array(
        'woff2' => 'woff2',
        'woff' => 'woff',
        'ttf' => 'ttf',
    );
    if (!isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'])) {
        $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'] = variable_get('advagg_relocate_css_inline_import_browsers', $font_type_defaults);
    }
    // Make sure advagg_relocate_css_inline_import_browsers is an array.
    if (!is_array($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'])) {
        $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'] = array(
            $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'] => $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'],
        );
    }
    // Use defaults if no matches for known font types.
    if (!isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff2']) && !isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff']) && !isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['ttf']) && !isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['eot']) && !isset($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['svg'])) {
        $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'] += $font_type_defaults;
    }
    // Set arguments for drupal_http_request().
    $options = array(
        'headers' => array(
            'Accept-Encoding' => 'gzip, deflate',
            'Connection' => 'close',
            'Referer' => $GLOBALS['base_root'] . request_uri(),
        ),
        'timeout' => 8,
        'version' => '1.0',
    );
    if (function_exists('brotli_uncompress')) {
        $options['headers']['Accept-Encoding'] .= ', br';
    }
    // If protocol relative, force https.
    if (strpos($url, '//') === 0) {
        $url = advagg_force_https_path($url);
    }
    // Build CID.
    $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers'] = array_filter($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']);
    $fonts = implode(',', $aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']);
    $cid = "advagg_relocate_css_inline_import:{$fonts}:{$url}";
    // Try local cache.
    $cached_data = cache_get($cid, 'cache_advagg_info');
    if (!empty($cached_data->data[0])) {
        if ($cached_data->expire >= REQUEST_TIME) {
            return $cached_data->data[0];
        }
        else {
            // Set header for 304 response.
            // $options['headers']['If-None-Match'] = $response->headers['etag'];.
            $options['headers']['If-Modified-Since'] = gmdate('D, d M Y H:i:s T', $cached_data->created);
        }
    }
    // Get external data.
    $responses = array();
    if (module_exists('httprl')) {
        // Get ttf.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['ttf'])) {
            $options['#font-type'] = 'ttf';
            httprl_request($url . '#ttf', $options);
        }
        // Get eot.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['eot'])) {
            $options['#font-type'] = 'eot';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)';
            httprl_request($url . '#eot', $options);
        }
        // Get svg.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['svg'])) {
            $options['#font-type'] = 'svg';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (iPad; U; CPU OS 3_2_2 like Mac OS X; nl-nl) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B500 Safari/531.21.10';
            httprl_request($url . '#svg', $options);
        }
        // Get woff.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff'])) {
            $options['#font-type'] = 'woff';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)';
            httprl_request($url . '#woff', $options);
        }
        // Get woff2.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff2'])) {
            $options['#font-type'] = 'woff2';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1';
            httprl_request($url . '#woff2', $options);
        }
        $responses = httprl_send_request();
    }
    if (empty($responses)) {
        // Get ttf.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['ttf'])) {
            $options['#font-type'] = 'ttf';
            $responses['ttf'] = drupal_http_request($url . '#ttf', $options);
            if (!isset($responses['ttf']->options)) {
                $responses['ttf']->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses['ttf']->url = $url . '#ttf';
            }
        }
        // Get eot.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['eot'])) {
            $options['#font-type'] = 'eot';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)';
            $responses['eot'] = drupal_http_request($url . '#eot', $options);
            if (!isset($responses['eot']->options)) {
                $responses['eot']->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses['eot']->url = $url . '#eot';
            }
        }
        // Get svg.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['svg'])) {
            $options['#font-type'] = 'svg';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (iPad; U; CPU OS 3_2_2 like Mac OS X; nl-nl) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B500 Safari/531.21.10';
            $responses['svg'] = drupal_http_request($url . '#svg', $options);
            if (!isset($responses['svg']->options)) {
                $responses['svg']->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses['svg']->url = $url . '#svg';
            }
        }
        // Get woff.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff'])) {
            $options['#font-type'] = 'woff';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)';
            $responses['woff'] = drupal_http_request($url . '#woff', $options);
            if (!isset($responses['woff']->options)) {
                $responses['woff']->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses['woff']->url = $url . '#woff';
            }
        }
        // Get woff2.
        if (!empty($aggregate_settings['variables']['advagg_relocate_css_inline_import_browsers']['woff2'])) {
            $options['#font-type'] = 'woff2';
            $options['headers']['User-Agent'] = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1';
            $responses['woff2'] = drupal_http_request($url . '#woff2', $options);
            if (!isset($responses['woff2']->options)) {
                $responses['woff2']->options = $options;
            }
            if (!isset($responses[$url]->url)) {
                $responses['woff2']->url = $url . '#woff2';
            }
        }
    }
    // Try failures again.
    advagg_relocate_try_failures_again($responses);
    // Parse data.
    $font_faces = array();
    $ttl = 0;
    foreach ($responses as $key => $response) {
        if ($response->code == 304 && !empty($cached_data->data[0])) {
            // This might need to be better handled in the future.
            return $cached_data->data[0];
        }
        // Set the font type if not set.
        if (empty($response->options['#font-type'])) {
            if (!is_numeric($key)) {
                $response->options['#font-type'] = $key;
            }
            else {
                continue;
            }
        }
        if ($response->code != 200 && $response->code != 201 && $response->code != 202 && $response->code != 206) {
            return array();
        }
        if (empty($response->data)) {
            return array();
        }
        advagg_relocate_process_http_request($response, 'font');
        $ttl = max($ttl, $response->ttl);
        // Parse the CSS.
        $font_face = advagg_relocate_parse_css_font_face($response->data, array(
            'font-family',
            'font-style',
            'font-weight',
            'src',
        ), $response->options['#font-type']);
        // Format into a better data structure and combine.
        foreach ($font_face as $k => $values) {
            if (!isset($font_faces[$k])) {
                $font_faces[$k] = $font_face[$k];
                continue;
            }
            foreach ($values as $index => $value) {
                if (!in_array($value, $font_faces[$k])) {
                    if ($index === $response->options['#font-type']) {
                        $font_faces[$k][$index] = $values[$index];
                    }
                    else {
                        $font_faces[$k][] = $values[$index];
                    }
                }
            }
        }
    }
    // Save data to the cache.
    if (!empty($font_faces)) {
        cache_set($cid, array(
            $font_faces,
            $responses,
        ), 'cache_advagg_info', REQUEST_TIME + $ttl);
    }
    return $font_faces;
}

/**
 * Parse the cache-control string into a key value array.
 *
 * @param string $cache_control
 *   The cache-control string.
 *
 * @return array
 *   Returns a key value array.
 */
function advagg_relocate_parse_cache_control($cache_control) {
    $cache_control_array = explode(',', $cache_control);
    $cache_control_array = array_map('trim', $cache_control_array);
    $cache_control_parsed = array();
    foreach ($cache_control_array as $value) {
        if (strpos($value, '=') !== FALSE) {
            $temp = array();
            parse_str($value, $temp);
            $cache_control_parsed += $temp;
        }
        else {
            $cache_control_parsed[$value] = TRUE;
        }
    }
    return $cache_control_parsed;
}

/**
 * Parse the font family string into a structured array.
 *
 * @param string $css_string
 *   The raw css string.
 * @param array $properties
 *   The css properties to get.
 * @param string $type
 *   The type of font file.
 *
 * @return array
 *   Returns a key value array.
 */
function advagg_relocate_parse_css_font_face($css_string, array $properties, $type) {
    // Get the CSS that contains a font-family rule.
    $length = strlen($css_string);
    $property_position = 0;
    $lower = strtolower($css_string);
    $attributes = array();
    foreach ($properties as $property) {
        while (($property_position = strpos($lower, $property, $property_position)) !== FALSE) {
            // Find the start of the values for the property.
            $start_of_values = strpos($css_string, ':', $property_position);
            // Get the property at this location of the css.
            $property_in_loop = trim(substr($css_string, $property_position, $start_of_values - $property_position));
            // Make sure this property is one of the ones we're looking for.
            if ($property_in_loop !== $property) {
                $property_position += strlen($property);
                continue;
            }
            // Get position of last closing bracket plus 1 (start of this section).
            $start = strrpos($css_string, '}', -($length - $property_position));
            if ($start === FALSE) {
                // Property is in the first selector and a declaration block (full rule
                // set).
                $start = 0;
            }
            else {
                // Add one to start after the }.
                $start++;
            }
            // Get closing bracket (end of this section).
            $end = strpos($css_string, '}', $property_position);
            if ($end === FALSE) {
                // The end is the end of this file.
                $end = $length;
            }
            // Get closing ; in order to get end of the declaration of the property.
            $declaration_end_a = strpos($css_string, ';', $property_position);
            $declaration_end_b = strpos($css_string, '}', $property_position);
            if ($declaration_end_a === FALSE) {
                $declaration_end = $declaration_end_b;
            }
            else {
                $declaration_end = min($declaration_end_a, $declaration_end_b);
            }
            if ($declaration_end > $end) {
                $declaration_end = $end;
            }
            // Add one in order to capture the } when we ge the full rule set.
            $end++;
            // Advance position for the next run of the while loop.
            $property_position = $end;
            // Get values assigned to this property.
            $values_string = substr($css_string, $start_of_values + 1, $declaration_end - ($start_of_values + 1));
            // Parse values string into an array of values.
            $values_array = explode(',', $values_string);
            $values_array = array_map('trim', $values_array);
            foreach ($values_array as $key => $value) {
                if (stripos($value, "'{$type}'") !== FALSE || stripos($value, ".{$type}") !== FALSE) {
                    unset($values_array[$key]);
                    $values_array[$type] = $value;
                }
            }
            $attributes[$property][] = $values_array;
        }
    }
    // Make sure src is the last one.
    $temp = $attributes['src'];
    unset($attributes['src']);
    $attributes['src'] = $temp;
    // Parse attributes into an output array.
    $temp = array();
    $output = array();
    foreach ($attributes as $property => $values) {
        foreach ($values as $key => $value) {
            if ($property !== 'src') {
                $value = implode(',', $value);
                if (!isset($temp[$key])) {
                    $temp[$key] = '';
                }
                $temp[$key] .= "{$property}: {$value}; ";
            }
            else {
                $output[$temp[$key]] = $value;
            }
        }
    }
    return $output;
}

/**
 * Parse the font family string into a structured array.
 *
 * @param string $filename
 *   The filename to save.
 * @param string $data
 *   The data to save to the file.
 * @param bool $local_cache
 *   TRUE if the data came from the cache bin.
 * @param string $hash
 *   Contents hash; if different resave data.
 *
 * @return array
 *   Returns an array of errors that might have happened.
 */
function _advagg_relocate_save_remote_asset($filename, $data, $local_cache, $hash) {
    // Save remote data.
    $errors = array();
    $saved = FALSE;
    $dir = variable_get('advagg_relocate_directory', ADVAGG_RELOCATE_DIRECTORY);
    $full_filename = $dir . $filename;
    $local_hash = '';
    if (!is_readable($full_filename)) {
        $errors = advagg_save_data($full_filename, $data);
        $saved = TRUE;
    }
    elseif (empty($local_cache)) {
        $file_contents = @advagg_file_get_contents($full_filename);
        if (!empty($file_contents)) {
            $local_hash = drupal_hash_base64($file_contents);
        }
        if ($hash !== $local_hash) {
            $errors = advagg_save_data($full_filename, $data, TRUE);
            $saved = TRUE;
        }
    }
    return array(
        $full_filename,
        $errors,
        $saved,
    );
}

Functions

Title Deprecated Summary
advagg_relocate_advagg_get_css_aggregate_contents_alter Implements hook_advagg_get_css_aggregate_contents_alter().
advagg_relocate_advagg_get_info_on_files_alter Implements hook_advagg_get_info_on_files_alter().
advagg_relocate_advagg_get_js_file_contents_alter Implements hook_advagg_get_js_file_contents_alter().
advagg_relocate_advagg_relocate_process_http_request_alter Implements hook_advagg_relocate_process_http_request_alter().
advagg_relocate_font_face_parser Given an array of font data output a new CSS string.
advagg_relocate_get_remote_data Gets external CSS and JS files; caches and returns response.
advagg_relocate_get_remote_font_data Gets external CSS files; caches it and returns css font rules.
advagg_relocate_parse_cache_control Parse the cache-control string into a key value array.
advagg_relocate_parse_css_font_face Parse the font family string into a structured array.
advagg_relocate_process_http_request Get the TTL and fix UTF-8 encoding.
advagg_relocate_try_failures_again Detect failures and try again.
advagg_relocate_uncompress_data Decompress the data.
_advagg_relocate_callback Gets external CSS files and puts the contents of it in the aggregate.
_advagg_relocate_save_remote_asset Parse the font family string into a structured array.