You are here

advagg_relocate.advagg.inc in Advanced CSS/JS Aggregation 7.2

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

Namesort descending Description
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.