<?php

/**
 * @file
 * Implements the core hooks, public and private functions.
 */


require_once('cdn.constants.inc');


//----------------------------------------------------------------------------
// Drupal core.

/**
 * Implements hook_file_url_alter().
 */
function cdn_file_url_alter(&$original_uri) {
  $mode                  = variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC);
  $farfuture             = variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT);
  $stats                 = variable_get(CDN_STATS_VARIABLE, FALSE) && user_access(CDN_PERM_ACCESS_STATS);
  $https_support         = variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE);
  $maintenance_mode      = variable_get('maintenance_mode', FALSE);
  $is_https_page         = cdn_request_is_https();

  // Don't alter file URLs when running update.php.
  if (defined('MAINTENANCE_MODE')) {
    return;
  }

  if (cdn_status_is_enabled()) {
    $scheme = file_uri_scheme($original_uri);

    // If the current URI is an absolute or protocol-relative URI, return
    // immediately.
    if (($scheme && ($scheme == 'http' || $scheme == 'https'))
        || drupal_substr($original_uri, 0, 2) == '//'
       )
    {
      return;
    }
    elseif ($scheme) {
      // Only alter URLs for local stream wrappers. If a file is served
      // remotely, it doesn't make sense to serve it from a CDN.
      $local_schemes = array_keys(file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL));
      if (!in_array($scheme, $local_schemes)) {
        return;
      }

      // Attempt to get an external URL using the appropriate wrapper.
      if ($wrapper = file_stream_wrapper_get_instance_by_uri($original_uri)) {
        $uri = str_replace($GLOBALS['base_url'] . '/', '', $wrapper->getExternalUrl());
      }
      else {
        return;
      }
    }
    else {
      $uri = $original_uri;
    }

    if (!cdn_check_protocol()) {
      return;
    }
    if (!cdn_check_drupal_path(current_path())) {
      return;
    }
    if (!cdn_check_file($uri)) {
      return;
    }

    if ($stats) {
      cdn_load_include('stats');
      $start = microtime();
    }

    // Alter the file path when using Origin Pull mode and using that mode's
    // Far Future setting.
    if ($mode == CDN_MODE_BASIC && $farfuture && !$maintenance_mode) {
      cdn_load_include('basic.farfuture');
      // We need the unescaped version of the URI to perform file operations.
      $uri = urldecode($uri);
      // If the file does not yet exist, perform a normal HTTP request to this
      // file, to generate it. (E.g. when ImageCache is used, this will
      // generate the derivative file.) When that fails, don't serve it from
      // the CDN.
      if (!file_exists($uri) && !_cdn_basic_farfuture_generate_file($uri, $original_uri)) {
        $path = drupal_encode_path($uri);
        return;
      }
      // Generate a unique file identifier (UFI).
      $ufi = cdn_basic_farfuture_get_identifier($uri);
      // Now that file operations have been performed, re-encode the URI.
      $uri = drupal_encode_path($uri);
      // Generate the new path.
      $uri_before_farfuture = $uri;

      // Generate a unique token to verify that the request was generated by
      // CDN. We cannot use drupal_get_token() since it depends on the user
      // session.
      $path_info = pathinfo(urldecode($uri));
      $token = drupal_hmac_base64($ufi . $path_info['filename'], drupal_get_private_key() . drupal_get_hash_salt());
      $uri = "cdn/farfuture/$token/$ufi/$uri";
    }

    // Load the include file that contains the logic for the mode that's
    // currently enabled.
    cdn_load_include(($mode == CDN_MODE_BASIC) ? 'basic' : 'advanced');

    // Depending on the mode, use a different function to get the servers on
    // which the file is available.
    $servers = ($mode == CDN_MODE_BASIC) ? cdn_basic_get_servers($uri) : cdn_advanced_get_servers($uri);

    // The file is not available on any server.
    if (count($servers) == 0) {
      $cdn_url    = FALSE;
      $server = FALSE;
    }
    // If the file is available from multiple servers, then allow a special
    // function to pick the desired server. The decision can be made on any
    // desired criteria: user's location, user's role, current date …
    elseif (count($servers) > 1 && function_exists('cdn_pick_server')) {
      $picked_server = cdn_pick_server($servers);
      $cdn_url = $picked_server['url'];
      $server  = $picked_server['server'];
    }
    // The file is available from multiple servers; pick a server randomly
    // (yet consistently: this uses consistent hashing!), so that each server
    // will serve approximately the same number of files.
    elseif (count($servers > 1)) {
      $filename = basename($servers[0]['url']);
      $unique_file_id = hexdec(substr(md5($filename), 0, 5));
      $choice = $unique_file_id % count($servers);
      $cdn_url = $servers[$choice]['url'];
      $server  = $servers[$choice]['server'];
    }
    // The file is only avaible from one server.
    else {
      $cdn_url = $servers[0]['url'];
      $server  = $servers[0]['server'];
    }

    // If the current page is being served via HTTPS, and the CDN supports
    // HTTPS, then use the HTTPS file URL.
    if ($is_https_page && $https_support && !empty($cdn_url)) {
      $cdn_url = preg_replace('/^http:/', 'https:', $cdn_url);
    }

    // If the user can access it, add this to the per-page statistics.
    if ($stats) {
      $end = microtime();
      $source_uri = ($mode == CDN_MODE_BASIC && $farfuture && !$maintenance_mode) ? $uri_before_farfuture : $original_uri;
      _cdn_devel_page_stats($source_uri, $cdn_url, $server, $end - $start);
    }

    // Override the path with the corresponding CDN URL, *if* the file is
    // available on the CDN (it may only be not available in advanced mode).
    if ($cdn_url !== FALSE) {
      $original_uri = $cdn_url;
    }
  }
}

/**
 * Implementation of hook_cdn_unique_file_identifier_info().
 */
function cdn_cdn_unique_file_identifier_info() {
  // Keys are machine names.
  return array(
    'md5_hash' => array(
      'label'        => t('MD5 hash'),
      'prefix'       => 'md5',
      'description'  => t('MD5 hash of the file.'),
      'filesystem'   => TRUE,
      'callback'     => 'md5_file',
    ),
    'mtime' => array(
      'label'        => t('Last modification time'),
      'prefix'       => 'mtime',
      'description'  => t('Last modification time of the file.'),
      'filesystem'   => TRUE,
      'callback'     => 'filemtime',
    ),
    'perpetual' => array(
      'label'        => t('Perpetual'),
      'prefix'       => 'perpetual',
      'description'  => t('Perpetual files never change (or are never cached
                          by the browser, e.g. video files).'),
      'filesystem'   => FALSE,
      'value'        => 'forever',
    ),
    'drupal_version' => array(
      'label'        => t('Drupal version'),
      'prefix'       => 'drupal',
      'description'  => t('Drupal core version — this should only be applied
                          to files that ship with Drupal core.'),
      'filesystem'   => FALSE,
      'value'        => VERSION,
    ),
    'drupal_cache' => array(
      'label'        => t('Drupal cache'),
      'prefix'       => 'drupal-cache',
      'description'  => t('Uses the current Drupal cache ID
                          (<code>css_js_query_string</code>). This ID is
                          updated automatically whenever the Drupal cache is
                          flushed (e.g. when you submit the modules form). Be
                          aware that this can change relatively often, forcing
                          redownloads by your visitors.'),
      'filesystem'   => FALSE,
      'value'        => variable_get('css_js_query_string', 0),
    ),
    'deployment_id' => array(
      'label'        => t('Deployment ID'),
      'prefix'       => 'deployment',
      'description'  => t('A developer-defined deployment ID. Can be an
                          arbitrary string or number, as long as it uniquely
                          identifies deployments and therefore the affected
                          files.<br />
                          Define this deployment ID in any enabled module or
                          in <code>settings.php</code> as the
                          <code>CDN_DEPLOYMENT_ID</code>
                          constant, and it will be picked up instantaneously.'),
      'filesystem'   => FALSE,
      'callback'     => '_cdn_ufi_deployment_id',
    ),
  );
}

/**
 * Implements hook_element_info_alter().
 */
function cdn_element_info_alter(&$type) {
  if (!cdn_status_is_enabled()) {
    return;
  }

  $mode = variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC);
  if ($mode == CDN_MODE_BASIC) {
    // Override Drupal's CSS aggregation system.
    cdn_load_include('basic.css');
    $type['styles']['#aggregate_callback'] = '_cdn_aggregate_css';
  }
}

/**
 * Implements hook_menu().
 */
function cdn_menu() {
  // Admin UI.
  $items['admin/config/development/cdn'] = array(
    'title'            => 'CDN',
    'description'      => 'Configure CDN integration.',
    'access arguments' => array(CDN_PERM_ADMIN),
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('cdn_admin_general_settings_form'),
    'type'             => MENU_NORMAL_ITEM,
    'file'             => 'cdn.admin.inc',
  );
  $items['admin/config/development/cdn/general'] = array(
    'title'            => 'General',
    'description'      => 'General settings.',
    'access arguments' => array(CDN_PERM_ADMIN),
    'weight'           => -10,
    'type'             => MENU_DEFAULT_LOCAL_TASK,
    'file'             => 'cdn.admin.inc',
  );
  $items['admin/config/development/cdn/details'] = array(
    'title'            => 'Details',
    'access arguments' => array(CDN_PERM_ADMIN),
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('cdn_admin_details_form'),
    'weight'           => -8,
    'type'             => MENU_LOCAL_TASK,
    'file'             => 'cdn.admin.inc',
  );
  $items['admin/config/development/cdn/other'] = array(
    'title'            => 'Other',
    'description'      => 'Other settings.',
    'access arguments' => array(CDN_PERM_ADMIN),
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('cdn_admin_other_settings_form'),
    'weight'           => -4,
    'type'             => MENU_LOCAL_TASK,
    'file'             => 'cdn.admin.inc',
  );

  // Statistics: file touch support.
  $items['admin/cdn/touch/%'] = array(
    'title'            => 'Touch file',
    'description'      => 'Touch a file to force a resync with File Conveyor.',
    'access arguments' => array(CDN_PERM_TOUCH),
    'page callback'    => 'drupal_get_form',
    'page arguments'   => array('cdn_touch_file_form', 3),
    'type'             => MENU_CALLBACK,
    'file'             => 'cdn.stats.inc',
  );

  // Origin Pull mode's Far Future expiration support.
  $items['cdn/farfuture/%/%/%menu_tail'] = array(
    'title'            => 'Download a far futured file',
    'access callback'  => TRUE,
    'page callback'    => 'cdn_basic_farfuture_download',
    'page arguments'   => array(2, 3, 4),
    'type'             => MENU_CALLBACK,
    'load arguments'   => array('%map', '%index'),
    'file'             => 'cdn.basic.farfuture.inc',
  );
  $items['cdn/farfuture/reverse-proxy-test/%'] = array(
    'title'            => 'Far Future reverse proxy test',
    'access callback'  => TRUE,
    'page callback'    => 'cdn_basic_farfuture_reverseproxy_test',
    'page arguments'   => array(3),
    'type'             => MENU_CALLBACK,
    'file'             => 'cdn.basic.farfuture.inc',
  );

  return $items;
}

/**
 * Implements hook_permission().
 */
function cdn_permission() {
  return array(
    CDN_PERM_ADMIN => array(
      'title'           => t('Administer CDN configuration settings'),
      'restrict access' => TRUE,
    ),
    CDN_PERM_ACCESS_STATS => array(
      'title' => t('Access per-page statistics'),
    ),
    CDN_PERM_ACCESS_TESTING => array(
      'title'       => t('Access files on the CDN when in testing mode'),
      'description' => t('Users with this permission will get files from the
                          CDN when testing mode is enabled.'),
    ),
    CDN_PERM_TOUCH => array(
      'title'       => t('Touch files'),
      'description' => t("'Touch' files through the links provided by the
                          per-page statistics. This will change the last
                          modification time of the file, and depending on your
                          set-up, may cause a resync of the file."),
    )
  );
}

/**
 * Implements hook_node_view_alter().
 */
function cdn_node_view_alter(&$build) {
  if (!cdn_status_is_enabled()) {
    return;
  }

  $build['#post_render'][] = 'cdn_post_render_html_alter';
}

/**
 * Implements hook_block_view_alter().
 */
function cdn_block_view_alter(&$data, $block) {
  if (!cdn_status_is_enabled()) {
    return;
  }

  if (isset($data['content'])) {
    // Blocks with render arrays.
    if (is_array($data['content']) && $data['content']) {
      $data['content']['#post_render'][] = 'cdn_post_render_html_alter';
    }
    // Blocks without render arrays (typically custom blocks).
    elseif (is_string($data['content'])) {
      $data['content'] = cdn_post_render_html_alter($data['content']);
    }
  }
}

/**
 * Implements hook_ctools_render_alter().
 */
function cdn_ctools_render_alter(&$info, $page, $context) {
  if (!cdn_status_is_enabled()) {
    return;
  }

  // Nodes in Panel panes.
  if ($context['task']['name'] === 'node_view' && !empty($info['content']) && is_string($info['content'])) {
    $info['content'] = cdn_post_render_html_alter($info['content']);
  }
}

/**
 * Implements hook_theme().
 */
function cdn_theme() {
  return array(
    'cdn_page_stats' => array(
      'file'      => 'theme.inc',
      'variables' => array(
        'file_count'                    => NULL,
        'cdn_file_count'                => NULL,
        'synced_files_per_server_count' => NULL,
        'total_time'                    => NULL,
        'synced_files'                  => NULL,
        'unsynced_files'                => NULL,
      ),
    ),
    'cdn_page_stats_file_link' => array(
      'file'      => 'theme.inc',
      'variables' => array(
        'file'          => NULL,
        'absolute_path' => NULL,
        'synced'        => NULL,
        'cdn_url'       => NULL,
        'server'        => NULL,
      ),
    ),
  );
}

/**
 * Implements hook_html_head_alter().
 *
 * Adding dns-prefetch <link> elements via hook instead of drupal_add_html_head
 * to place them as early in the head as possible so that the browser acts upon
 * them ASAP.
 *
 * @see https://github.com/h5bp/html5-boilerplate/blob/master/doc/extend.md
 */
function cdn_html_head_alter(&$head_elements) {
  if (!cdn_status_is_enabled()) {
    return;
  }

  $domains = cdn_get_domains();
  // Two sets of markups: because IE9 interprets rel="prefetch" as "dns-prefetch".
  // IE10 intreprets both dns-prefetch and prefetch as DNS pre-resolve.
  $markup = null;
  $ie_markup = null;

  if (count($domains)) {
    // Enable prefetching.
    $head_elements['cdn_dns_prefetch_meta'] = array(
      '#type' => 'html_tag',
      '#tag' => 'link',
      '#attributes' => array(
        'http-equiv' => 'x-dns-prefetch-control',
        'content'    => 'on',
      ),
      // System meta for content-type is at weight -1000. We'll insert the DNS tags
      // shortly after that.
      '#weight' => -900.9999,
    );
    // The domain names to prefetch. Use protocol-relative URLs.
    foreach ($domains as $domain) {
      $element = array(
        '#tag' => 'link',
        '#attributes' => array(
          'rel' => 'dns-prefetch',
          'href'    => '//' . $domain,
        )
      );

      $link_el = theme('html_tag', array('element' => $element));
      $markup .= $link_el;
      $ie_markup .= preg_replace('/rel="dns-prefetch"/', 'rel="prefetch"', $link_el);
    }
    $markup .= '<!--[if IE 9]>' . PHP_EOL;
    $markup .= $ie_markup;
    $markup .= '<![endif]-->' . PHP_EOL;

    $head_elements['cdn_dns_prefetch_block'] = array(
      '#type' => 'markup',
      '#markup' => $markup,
      '#weight' => -900,
    );
  }
}

/**
 * Implements hook_boot().
 */
function cdn_boot() {
  // Bail if the status is "disabled" (i.e. don't when "enabled" or "testing").
  if (variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED) === CDN_DISABLED) {
    return;
  }

  // Inspired by common.inc/_drupal_bootstrap_full().
  require_once DRUPAL_ROOT . '/includes/common.inc';
  require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
  require_once DRUPAL_ROOT . '/includes/unicode.inc';

  // Prevent the CDN from returning content pages. We only want the CDN to
  // return static files like images, CSS files, JavaScript files, etc. By
  // default it will return anything. Since those static files aren't served by
  // Drupal.
  $redirect_url = _cdn_seo_should_redirect(current_path());
  if ($redirect_url !== FALSE) {
    // A 301 is SEO friendly, as it tells the search engine what the canonical
    // URL is for this content.
    header('HTTP/1.0 301 Moved Permanently');
    // @see http://googlewebmastercentral.blogspot.com/2011/06/supporting-relcanonical-http-headers.html
    header('Link: <' . $redirect_url . '>; rel="canonical"');
    header('Location: ' . $redirect_url);

    // To ensure this redirect occurs immediately we don't use drupal_exit().
    exit();
  }
}

/**
 * Implements hook_init().
 */
function cdn_init() {
  if (!cdn_status_is_enabled()) {
    return;
  }

  // When per-page statistics are enabled, add the CSS that will be used to
  // make these statistics more usable.
  if (variable_get(CDN_STATS_VARIABLE, FALSE)
      && user_access(CDN_PERM_ACCESS_STATS)
  ) {
    drupal_add_css(drupal_get_path('module', 'cdn') . '/cdn.css', array('every_page' => TRUE));
  }
}

/**
 * Implements hook_exit().
 */
function cdn_exit($destination = NULL) {
  // When the _cdn_devel_page_stats() function does not exist, there are no
  // stats to show, hence we can return immediately.
  // This can happen when the stats are disabled (for the current user or
  // entirely), or when a cached page is being served.
  if (!function_exists('_cdn_devel_page_stats')) {
    return;
  }

  // Try not to break non-HTML pages.
  if (function_exists('drupal_get_http_header') && !strstr(drupal_get_http_header('Content-Type'), 'html')) {
    return;
  }

  if (!$destination
      && variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED) != CDN_DISABLED
      && variable_get(CDN_STATS_VARIABLE, FALSE)
      && user_access(CDN_PERM_ACCESS_STATS)
  ) {
    list(
      $file_count,
      $cdn_file_count,
      $synced_files_per_server_count,
      $total_time,
      $synced_files,
      $unsynced_files,
    ) = _cdn_devel_page_stats();
    print theme('cdn_page_stats', array(
      'file_count'                    => $file_count,
      'cdn_file_count'                => $cdn_file_count,
      'synced_files_per_server_count' => $synced_files_per_server_count,
      'total_time'                    => $total_time,
      'synced_files'                  => $synced_files,
      'unsynced_files'                => $unsynced_files
    ));
  }
}

/**
 * Implementation of hook_flush_caches().
 */
function cdn_flush_caches() {
  // Force refresh of the blacklist.
  cdn_get_blacklist(TRUE);

  // Clear aggregated CSS files for pages served via HTTP.
  variable_del('cdn_css_cache_files_http');
  file_scan_directory('public://cdn/css/http', '/.*/', array('callback' => 'drupal_delete_file_if_stale'));

  // Clear aggregated CSS files for pages served via HTTPS.
  variable_del('cdn_css_cache_files_https');
  file_scan_directory('public://cdn/css/https', '/.*/', array('callback' => 'drupal_delete_file_if_stale'));
}

/**
 * Implementation of hook_cdn_blacklist().
 */
function cdn_cdn_blacklist() {
  $blacklist = array();

  // Blacklist wysiwyg library files.
  if (module_exists('wysiwyg')) {
    foreach (wysiwyg_get_all_editors() as $editor) {
      if (!$editor['installed']) {
        continue;
      }
      $blacklist[] = $editor['library path'] . '/*';
    }
  }

  // Blacklist Image CAPTCHA' dynamically generated CAPTCHA images.
  $blacklist[] = 'image_captcha*';

  // Blacklist SimpleTest paths
  $blacklist[] = "*simpletest/verbose/*";

  return $blacklist;
}


//----------------------------------------------------------------------------
// Drush.

/**
 * Implements hook_cacheaudit().
 */
function cdn_cacheaudit() {
  $results = array(array('Settings', 'Value'));

  $status = variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED);
  if ($status == CDN_ENABLED) {
    $status_value = 'enabled';
  }
  elseif ($status == CDN_TESTING) {
    $status_value = 'testing';
  }
  else {
    $status_value = 'disabled';
  }
  $results[] = array('Status', $status_value);

  $mode = variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC);
  $results[] = array('Mode', $mode ? 'Origin Pull' : 'File Conveyor');

  if ($mode == CDN_MODE_BASIC) {
    cdn_load_include('basic');
    $results[] = array('    mappings', count(_cdn_basic_parse_raw_mapping(cdn_basic_get_mapping())));
    $results[] = array('    Far Future expiration', variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT));
  }

  $results[] = array('Domains', count(cdn_get_domains()));

  return array('cdn' => $results);
}


//----------------------------------------------------------------------------
// Public functions.

/**
 * Get all domains from which files might be served. This information is
 * necessary for some modules, e.g. Boost.
 *
 * @return
 *   An array of domain names.
 */
function cdn_get_domains() {
  $domains = array();

  // Origin Pull mode domains.
  if (variable_get(CDN_MODE_VARIABLE, FALSE) == CDN_MODE_BASIC) {
    cdn_load_include('basic');
    $mapping = cdn_basic_get_mapping();
    $lines = preg_split("/[\n\r]+/", $mapping, -1, PREG_SPLIT_NO_EMPTY);
    foreach ($lines as $line) {
      // Ignore empty lines.
      $line = trim($line);
      if (empty($line)) {
        continue;
      }

      // Parse this line. It may or may not limit the CDN URL to a list of
      // file extensions.
      $parts = explode('|', $line);
      // It may also list more than one domain.
      foreach (explode(' ', $parts[0]) as $part) {
        // Remove white space.
        $part = trim($part);
        // Allow for protocol-relative domains. We prepend "http:", otherwise
        // parse_url() won't return anything.
        if (strpos($part, '//') === 0) {
          $part = 'http:' . $part;
        }
        $domains[] = parse_url($part, PHP_URL_HOST);
      }
    }
  }
  // File Conveyor mode domains.
  elseif (variable_get(CDN_MODE_VARIABLE, FALSE) == CDN_MODE_ADVANCED) {
    cdn_load_include('advanced');
    $db = _cdn_advanced_get_db_connection();

    // In case no connection to the database could be made, pretend no
    // domains are being used.
    if (!$db) {
      return array();
    }

    // Retrieve all unique domains (by retrieving one URL per server) and then
    // parsing the domain names in those URLs.
    $sql = "SELECT url
            FROM synced_files
            GROUP BY server";
    $stmt = $db->prepare($sql);
    $stmt->execute();
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    foreach ($rows as $row) {
      $domains[] = parse_url($row['url'], PHP_URL_HOST);
    }
  }

  return array_unique($domains);
}

/**
 * See if any installed modules need to exclude certain files from being
 * accessed from the CDN. List gets updated on cron runs.
 *
 * Typically, files that are loaded by JS through AJAX violate the same origin
 * policy browsers have to comply with. Hence the browser refuses to load the
 * file, causing a broken website. This blacklist allows module developers to
 * specify which files should not be loaded from the CDN.
 *
 * @param $reset
 *   Whether to force the stored blacklist to be regenerated.
 * @return string
 *   Every line is a path pattern (@see drupal_match_path()).
 */
function cdn_get_blacklist($reset = FALSE) {
  static $blacklist = NULL;

  if (is_null($blacklist) || $reset) {
    $cache = cache_get('cdn_blacklist');
    if (!isset($cache->data) || $reset) {
      // Query modules for a list of files to be included into the blacklist.
      $blacklist = module_invoke_all('cdn_blacklist');

      // Invoke hook_cdn_blacklist_alter.
      drupal_alter('cdn_blacklist', $blacklist);

      // Remove duplicates.
      $blacklist = array_unique($blacklist);

      // Convert array to string.
      $blacklist = implode("\n", $blacklist);

      // Save to the cache.
      cache_set('cdn_blacklist', $blacklist, 'cache', CACHE_TEMPORARY);
    }
    else {
      $blacklist = $cache->data;
    }
  }

  return $blacklist;
}

/**
 * Check if CDN module should serve files from a HTTPS location.
 */
function cdn_serve_from_https() {
  return variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE)
         && cdn_request_is_https();
}

/**
 * Check if the current request is over HTTPS.
 */
function cdn_request_is_https() {
  return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
         ||
         (
           isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
           &&
           $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'
         )
         ||
         (
           isset($_SERVER['HTTP_X_FORWARDED_PROTOCOL'])
           &&
           $_SERVER['HTTP_X_FORWARDED_PROTOCOL'] == 'https'
         );
}

/**
 * Check if the CDN module is enabled.
 */
function cdn_status_is_enabled() {
  $status = variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED);
  return $status == CDN_ENABLED
         ||
         ($status == CDN_TESTING && user_access(CDN_PERM_ACCESS_TESTING));
}

/**
 * Check if the current protocol is supported by the CDN.
 *
 * Note: currently only checks HTTPS, in the future possibly also SPDY.
 */
function cdn_check_protocol() {
  $https_support = variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE);

  // If the current page is being served via HTTPS, and the CDN does not
  // support HTTPS, then don't rewrite the file URL, because it would make the
  // visit insecure.
  if (cdn_request_is_https() && !$https_support) {
    return FALSE;
  }

  return TRUE;
}

/**
 * Check if a Drupal path should serve files from the CDN (i.e.: is the Drupal
 * path blacklisted?).
 *
 * @param $path
 *   A Drupal path.
 */
function cdn_check_drupal_path($path) {
  global $user;
  $blacklist       = variable_get(CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE, CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_DEFAULT);
  $auth_blacklist  = variable_get(CDN_EXCEPTION_AUTH_USERS_BLACKLIST_VARIABLE, CDN_EXCEPTION_AUTH_USERS_BLACKLIST_DEFAULT);

  // Check if the Drupal path matches one of the blacklisted Drupal paths.
  if (drupal_match_path($path, $blacklist)) {
    return FALSE;
  }

  // If logged in user, apply a secondary blacklist.
  if ($user->uid > 0 && drupal_match_path($path, $auth_blacklist)) {
    return FALSE;
  }

  return TRUE;
}

/**
 * Check if a file should be served from the CDN.
 *
 * @param $uri
 *   A file URI.
 */
function cdn_check_file($uri) {
  $file_path_blacklist = variable_get(CDN_EXCEPTION_FILE_PATH_BLACKLIST_VARIABLE, CDN_EXCEPTION_FILE_PATH_BLACKLIST_DEFAULT);
  $file_path_whitelist = variable_get(CDN_EXCEPTION_FILE_PATH_WHITELIST_VARIABLE, CDN_EXCEPTION_FILE_PATH_WHITELIST_DEFAULT);
  $module_blacklist    = cdn_get_blacklist();

  // A file should not be served from a CDN when it matches one of the
  // blacklists, except when it matches the whitelist.
  if ((
        drupal_match_path($uri, $file_path_blacklist)
        ||
        drupal_match_path($uri, $module_blacklist)
      )
      && !drupal_match_path($uri, $file_path_whitelist)
    )
  {
      return FALSE;
  }

  return TRUE;
}

/**
 * Helper function to efficiently load include files for this module.
 */
function cdn_load_include($basename) {
  module_load_include('inc', 'cdn', "cdn.$basename");
}

/**
 * Alter file URLs in the given HTML (currently only image file URLs).
 *
 * Can be used as a #post_render callback.
 */
function cdn_post_render_html_alter($html, $elements = array()) {
  cdn_load_include('fallback');
  cdn_html_alter_image_urls($html);
  return $html;
}


//----------------------------------------------------------------------------
// Private functions.

/**
 * Callback for generating a unique file identifier.
 *
 * @param $path
 *   The file path to the file for which to generate a  unique identifier.
 */
function _cdn_ufi_deployment_id($path) {
  return CDN_DEPLOYMENT_ID;
}

/**
 * Determines whether a redirect should be performed for the given path for SEO
 * considerations (prevent duplicate HTML content on the CDN), and if so, the
 * URL to which the requesting User Agent should be redirected.
 *
 * @param $path
 *   The path for which to determine the redirect URL.
 * @return
 *   FALSE if no redirect should occur, or the URL to redirect to.
 */
function _cdn_seo_should_redirect($path) {
  if (variable_get(CDN_SEO_REDIRECT_VARIABLE, CDN_SEO_REDIRECT_DEFAULT)) {
    // If the path ends in an extension that is not in the list of forbidden
    // extensions, then return FALSE to indicate that no redirect should occur.
    // The rationale is: menu_get_item() doesn't allow us to detect whether a
    // page callback will generate a file; hence we (ab)use this heuristic.
    // An added benefit is that we don't need the menu system to be loaded,
    // meaning that we can do all of this during hook_boot(), meaning we can use
    // this same code for cached pages, which we need to support anyway.
    // @todo: improve Drupal core so that contrib modules can know whether
    // certain menu callbacks generate files or not.
    $forbidden_extensions = variable_get(CDN_SEO_FORBIDDEN_EXTENSIONS_VARIABLE, CDN_SEO_FORBIDDEN_EXTENSIONS_DEFAULT);
    $extension = drupal_strtolower(pathinfo($path, PATHINFO_EXTENSION));
    if (!empty($extension) && !in_array($extension, explode("\n", $forbidden_extensions))) {
      return FALSE;
    }

    // Use case-insensitive substring matching to match the current User-Agent
    // to the list of CDN user agents.
    if (isset($_SERVER['HTTP_USER_AGENT'])) {
      $ua = drupal_strtolower($_SERVER['HTTP_USER_AGENT']);
      $cdn_user_agents = explode("\n", drupal_strtolower(variable_get(CDN_SEO_USER_AGENTS_VARIABLE, CDN_SEO_USER_AGENTS_DEFAULT)));
      foreach ($cdn_user_agents as $cdn_ua) {
        if (strstr($ua, trim($cdn_ua))) {
          return url($path, array('absolute' => TRUE));
        }
      }
    }
  }
  return FALSE;
}
