Protocol Relative URLs in Drupal 7

When I first started tackling the problem of https and Drupal caching I wanted an interim solution and started by making the database caching layer smarter. Neil Drumm had the idea to use protocol-relative (a.k.a, schemeless) URLs. This turns out to be a great idea and thanks to some hooks in Drupal it's something we can implement fairly easily.

Protocol-Relative URL

Protocol relative URLs don't contain the protocol. For example, http://example.com/foo/bar would be //example.com/foo/bar. Browsers will use the protocol for the page it's on when the protocol is absent. For a page that could be displayed under https or http this is a nice solution and can help us avoid caching for both situations.

Protocol-Relative URLs aren't new or novel. For example, Google has been using them for some time. Just take a look at the source of a Google Plus page (or many others).

The Setup

You first need to make sure your server is capable of serving both http and https requests. For URLs to not have a protocol it makes sense to serve with multiple protocols. There is also the case where your content could be served out of http on RSS then viewed in an RSS reader under https. You don't want problems to pop up in cases like this.

IE 7/8 and CSS

Yeah, so older versions of IE have a bug. IE 7 and 8 will download stylesheets twice when the protocol is missing. You can either code around this, not use this technique, or let those users suffer with slightly slower performance.

Implementation Options

What follows are two different implementation options depending on what you want to do and your scenario. You can pick the case you think works best for you or tweak to your hearts delight.

Images Only

Many of the images inside Drupal (e.g., those handled by image styles) have a full URLs. If you want to alter these it's possible through the use of hook_preprocess_image().

/**
 * Implementation of hook_preprocess_image().
 *
 * Make images that use a full url be protocol relative.
 */
function custom_preprocess_image(&$variables) {

  // If the image URL starts with a protocol remove it and use a
  // relative protocol.
  $scheme = file_uri_scheme($variables['path']);
  $protocols = array('http', 'https');
  if ($scheme && in_array($scheme, $protocols)) {
    $variables['path'] = '//' . file_uri_target($variables['path']);
  }
}

This code is available as a gist at https://gist.github.com/1523736

All File URLs Generated By Drupal

In Drupal file_create_url() is used to create the full URLs for everything from image to CSS and JavaScript. It handles public and private files, internal paths (which it turns into full URLs), and making sure everything is tidy. To accommodate CDNs there is hook_file_url_alter() which we can plug into in order to make protocol-relative URLs.

/**
 * Implements hook_file_url_alter().
 *
 * Make all URLs be protocol relative.
 * Note: protocol relatice URLs will cause IE7/8 to download stylesheets twice.
 */
function custom_file_url_alter(&$url) {

  global $base_url;

  static $relative_base_url = NULL, $relative_base_length = NULL;

  $scheme = file_uri_scheme($url);

  // For some things (e.g., images) hook_file_url_alter can be called multiple
  // times. So, we have to be sure not to alter it multiple times. If we already
  // are relative protocol we can just return.
  // Only setup the and parse this stuff once.
  if (!$relative_base_url || !$relative_base_length) {
    $relative_base_url = '//' . file_uri_target($base_url);
    $relative_base_length = strlen($relative_base_url);
  }
  if (!$scheme && substr($url, 0, $relative_base_length) == $relative_base_url) {
    return;
  }

  // Handle the case where we have public files with the scheme public:// or
  // the case the relative path doesn't start with a /. Internal relative urls
  // have the base url prepended to them.
  if (!$scheme || $scheme == 'public') {

    // Internal Drupal paths.
    if (!$scheme) {
      $path = $url;
    }
    else {
      $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme);
      $path = $wrapper->getDirectoryPath() . '/' . file_uri_target($url);
    }

    // Clean up Windows paths.
    $path = str_replace('\\', '/', $path);

    $url = $base_url . '/' . $path;
  }

  // Convert full URLs to relative protocol.
  $protocols = array('http', 'https');
  $scheme = file_uri_scheme($url);
  if ($scheme && in_array($scheme, $protocols)) {
    $url = '//' . file_uri_target($url);
  }
}

This code is available as a gist at https://gist.github.com/1524135

Be careful with this option as it will cause CSS to be downloaded twice in IE 7 and 8.