You are here

google_tag.module in GoogleTagManager 7

Same filename and directory in other branches
  1. 8 google_tag.module
  2. 7.2 google_tag.module

Provides primary Drupal hook implementations.

Adds a JavaScript snippet to selected page responses to trigger analytics and other tracking items configured using the Google Tag Manager.

@author Jim Berry ("solotandem", http://drupal.org/user/240748)

File

google_tag.module
View source
<?php

/**
 * @file
 * Provides primary Drupal hook implementations.
 *
 * Adds a JavaScript snippet to selected page responses to trigger analytics and
 * other tracking items configured using the Google Tag Manager.
 *
 * @author Jim Berry ("solotandem", http://drupal.org/user/240748)
 */

/**
 * Default for matching all items except listed.
 */
const GOOGLE_TAG_EXCLUDE_LISTED = 'exclude listed';

/**
 * Default for matching only listed items.
 */
const GOOGLE_TAG_INCLUDE_LISTED = 'include listed';

/**
 * Default list of relative paths.
 */
define('GOOGLE_TAG_PATHS', "admin*\nbatch*\nnode/add*\nnode/*/edit\nnode/*/delete\nuser/*/edit*\nuser/*/cancel*");

/**
 * Default list of HTTP response statuses that override path conditions.
 */
define('GOOGLE_TAG_STATUSES', "403 Forbidden\n404 Not Found");

/**
 * Default list of tag classes to allow.
 */
define('GOOGLE_TAG_WHITELIST_CLASSES', "google\nnonGooglePixels\nnonGoogleScripts\nnonGoogleIframes");

/**
 * Default list of tag classes to forbid.
 */
define('GOOGLE_TAG_BLACKLIST_CLASSES', "customScripts\ncustomPixels");

/**
 * Implements hook_help().
 */
function google_tag_help($path, $arg) {
  module_load_include('inc', 'google_tag', 'includes/info');
  return _google_tag_help($path, $arg);
}

/**
 * Implements hook_menu().
 */
function google_tag_menu() {
  module_load_include('inc', 'google_tag', 'includes/info');
  return _google_tag_menu();
}

/**
 * Implements hook_permission().
 */
function google_tag_permission() {
  module_load_include('inc', 'google_tag', 'includes/info');
  return _google_tag_permission();
}

/**
 * Implements hook_flush_caches().
 *
 * This is called from system_cron() and drupal_flush_all_caches().
 */
function google_tag_flush_caches() {
  google_tag_assets_create();
}

/**
 * Implements hook_variable_group_info().
 */
function google_tag_variable_group_info() {
  module_load_include('inc', 'google_tag', 'includes/variable');
  return _google_tag_variable_group_info();
}

/**
 * Implements hook_variable_info().
 */
function google_tag_variable_info($options) {
  module_load_include('inc', 'google_tag', 'includes/variable');
  return _google_tag_variable_info($options);
}

/**
 * Implements hook_form_FORM_ID_alter() for variable_edit_form().
 *
 * This applies to:
 * - individual variables with a path like:
 * admin/config/system/variable/edit/google_tag_role_toggle
 *
 * - all variables by group with a path like:
 * admin/config/system/variable/group/google_tag
 *
 * - all variables by module with a path like:
 * admin/config/system/variable/module/google_tag
 *
 * on the latter two forms:
 * - comment says 'These are global default variables.'
 * - regardless the highlighted value in realm switcher
 * - realm name:key = global:default
 * - $form[#variable_options][realm]->realm and key
 *
 * all of these forms have:
 * - element parents are none
 * - $form[#variable_edit_form] contains list of variables on the form
 * - with first form need to check the variable being in this module
 *
 * on all pages:
 * - variable_realm_key_[realm] may be a query argument in the url
 */
function google_tag_form_variable_edit_form_alter(&$form, &$form_state, $form_id) {

  // Also include these on group and module pages for element validation.
  form_load_include($form_state, 'inc', 'google_tag', 'includes/admin');
  form_load_include($form_state, 'inc', 'google_tag', 'includes/variable');
  if (strpos($_GET['q'], 'admin/config/system/variable/edit/') === 0) {
    if (strpos($form['#variable_edit_form'][0], 'google_tag_') === 0) {
      $form['#submit'][] = 'google_tag_settings_form_submit';
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for variable_realm_edit_variables_form().
 *
 * This applies to:
 * - selected variables by realm with a path like:
 * admin/config/system/variable/realm/language/edit
 * - $form[#variable_options][realm]->realm and key
 * - $form_state[build_info][args] = [ [0] => realm_name, [1] => realm_key ]
 * - element parents are [variables][google_tag]
 * - and tree is true on variables key
 * - $form[#variable_edit_form] contains list of variables on the form
 * - which includes variables from other modules
 */
function google_tag_form_variable_realm_edit_variables_form_alter(&$form, &$form_state, $form_id) {

  // @todo Why does variable_realm not set this item on this particular form?
  $form['#realm_keys'][$form['#variable_options']['realm']->realm] = $form['#variable_options']['realm']->key;
  $form['#submit'][] = 'google_tag_settings_form_submit';
  form_load_include($form_state, 'inc', 'google_tag', 'includes/admin');
  form_load_include($form_state, 'inc', 'google_tag', 'includes/variable');
}

/**
 * Implements hook_page_build().
 *
 * Adds the snippet to the page array if the insertion conditions are met.
 *
 * @see drupal_render_page()
 */
function google_tag_page_build(&$page) {
  if (!google_tag_insert_snippet()) {
    return;
  }

  // Call sequence:
  // - drupal_render_page()
  //   - hook_page_build()
  //   - hook_page_alter()
  //   - drupal_render()
  // - drupal_render()
  //   - callbacks in $elements['#theme_wrappers']
  //     - hook_preprocess_html(): 'html' is the wrapper for page
  //     - templates may add tags after body tag
  //   - callbacks in $elements['#post_render']
  //     - google_tag_page_process(): callback set here
  $base_path = 'public:/';
  $include_classes = variable_get('google_tag_include_classes', 0);
  list($realm_name, $realm_key) = google_tag_realm_values();
  $realm_name .= $realm_name ? '/' : '';
  $realm_key .= $realm_key ? '.' : '';
  $types = $include_classes ? array(
    'data_layer',
    'script',
  ) : array(
    'script',
  );
  $data_layer = variable_get('google_tag_data_layer', 'dataLayer');
  if ($include_classes && module_exists('datalayer') && $data_layer == 'dataLayer') {
    $classes = variable_get('google_tag_data_layer_classes', array());
    if ($classes) {

      // Add data_layer using dataLayer module.
      datalayer_add($classes);
    }
    $types = array(
      'script',
    );
  }

  // Add data_layer and script snippets to head (by default).
  $include_script_as_file = variable_get('google_tag_include_file', 1);
  if ($include_script_as_file) {
    foreach ($types as $type) {

      // A stream wrapper may be passed as $data to drupal_add_js($data) because
      // drupal_get_js() calls file_create_url($data). The latter will work with
      // whichever class implements the stream, e.g. local, S3, or App Engine.
      $path = "{$base_path}/google_tag/{$realm_name}google_tag.{$realm_key}{$type}.js";

      // @todo Will it matter if file is empty?
      drupal_add_js($path, array(
        'group' => JS_LIBRARY * 2,
        'requires_jquery' => FALSE,
        'defer' => 'true',
      ));
    }
  }
  else {
    foreach ($types as $type) {
      $url = "{$base_path}/google_tag/{$realm_name}google_tag.{$realm_key}{$type}.js";
      $contents = @file_get_contents($url);

      // @see drupal_get_js()
      // For inline JavaScript to validate as XHTML, all JavaScript containing
      // XHTML needs to be wrapped in CDATA.
      if ($contents) {
        drupal_add_js($contents, array(
          'type' => 'inline',
          'group' => JS_LIBRARY * 2,
          'requires_jquery' => FALSE,
        ));
      }
    }
  }

  // Add noscript snippet to page_top region.
  $type = 'noscript';
  $url = "{$base_path}/google_tag/{$realm_name}google_tag.{$realm_key}{$type}.js";
  $noscript = @file_get_contents($url);
  if ($noscript) {

    // Note: for any theme that follows the pattern of core html.tpl.php in the
    // system module (e.g. bootstrap theme), this does not place the snippet
    // immediately after the body tag but rather after the 'skip-link' div.
    $page['page_top']['google_tag'] = array(
      '#markup' => $noscript,
      '#weight' => -10,
    );
  }
}

/**
 * Returns applicable realm name and key for the request.
 *
 * @return array
 *   The realm name and key.
 */
function google_tag_realm_values() {
  $realm_name = $realm_key = '';
  if (module_exists('variable_realm') && module_exists('variable_store')) {

    // If realms exist, then the non-realm snippet files will not be loaded.
    // These are in the 'google_tag' directory beneath the site files directory.
    // If variable_realm module is later removed, then visit the module settings
    // page and update the settings.
    $realms = variable_realm_current();

    // Remove the global realm as this is always active.
    unset($realms['global']);
    if (empty($realms)) {
      $realm_name = 'global';
      $realm_key = 'default';
    }
    else {

      // The variable_realm module allows multiple realms to be 'active' on a
      // page request. The realms are ordered with 'greater' weight having
      // precedence. Select the last realm but allow other modules to override.
      $realm_name = key(array_reverse($realms));
      $realm_key = variable_realm_status($realm_name);
    }

    // Allow other modules to alter the realm name and key.
    $realm_values = array(
      'name' => $realm_name,
      'key' => $realm_key,
    );
    drupal_alter('google_tag_realm', $realm_values);
    $realm_name = $realm_values['name'];
    $realm_key = $realm_values['key'];
    $debug = variable_get('google_tag_debug_output', 0);
    $debug ? drupal_set_message(t('realm:key = @realm:@key', array(
      '@realm' => $realm_name,
      '@key' => $realm_key,
    ))) : '';
  }
  return array(
    $realm_name,
    $realm_key,
  );
}

/**
 * Determines whether to insert the snippet on the response.
 *
 * @return bool
 *   TRUE if the conditions are met; FALSE otherwise.
 */
function google_tag_insert_snippet() {
  $id = variable_get('google_tag_container_id', '');
  if (empty($id)) {

    // No container ID.
    return FALSE;
  }
  $satisfied = TRUE;
  if (!_google_tag_status_check() || !_google_tag_path_check() || !_google_tag_role_check()) {

    // Omit snippet if any condition is not met.
    $satisfied = FALSE;
  }

  // Allow other modules to alter the insertion criteria.
  drupal_alter('google_tag_insert', $satisfied);
  $debug = variable_get('google_tag_debug_output', 0);
  $debug ? drupal_set_message(t('after alter @satisfied', array(
    '@satisfied' => $satisfied,
  ))) : '';
  return $satisfied;
}

/**
 * Determines whether to insert the snippet based on status code settings.
 *
 * @return bool
 *   TRUE if the status conditions are met; FALSE otherwise.
 */
function _google_tag_status_check() {
  static $satisfied;
  if (!isset($satisfied)) {
    $debug = variable_get('google_tag_debug_output', 0);
    $toggle = variable_get('google_tag_status_toggle', GOOGLE_TAG_EXCLUDE_LISTED);
    $statuses = variable_get('google_tag_status_list', GOOGLE_TAG_STATUSES);
    if (empty($statuses)) {
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
    }
    else {

      // Get the HTTP response status.
      $status = drupal_get_http_header('status');
      $satisfied = $status && strpos($statuses, $status) !== FALSE;
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
    }
    $debug ? drupal_set_message(t('google_tag')) : '';
    $debug ? drupal_set_message(t('status check @satisfied', array(
      '@satisfied' => $satisfied,
    ))) : '';
  }
  return $satisfied;
}

/**
 * Determines whether to insert the snippet based on the path settings.
 *
 * @return bool
 *   TRUE if the path conditions are met; FALSE otherwise.
 */
function _google_tag_path_check() {
  static $satisfied;
  if (!isset($satisfied)) {
    $debug = variable_get('google_tag_debug_output', 0);
    $toggle = variable_get('google_tag_path_toggle', GOOGLE_TAG_EXCLUDE_LISTED);
    $paths = variable_get('google_tag_path_list', GOOGLE_TAG_PATHS);
    if (empty($paths)) {
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
    }
    else {

      // @todo Are not some paths case sensitive???
      // Convert the paths to lowercase before comparison.
      $paths = drupal_strtolower($paths);
      $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
      $satisfied = drupal_match_path($path, $paths);

      // @todo Lowercase $_GET['q'] before comparison? What is purpose of this check?
      if ($path != $_GET['q']) {
        $satisfied = $satisfied || drupal_match_path($_GET['q'], $paths);
      }
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
    }
    $debug ? drupal_set_message(t('path check @satisfied', array(
      '@satisfied' => $satisfied,
    ))) : '';
  }
  return $satisfied;
}

/**
 * Determines whether to insert the snippet based on the user role settings.
 *
 * @return bool
 *   TRUE if the role conditions are met; FALSE otherwise.
 */
function _google_tag_role_check() {
  global $user;
  static $satisfied;
  if (!isset($satisfied)) {
    $debug = variable_get('google_tag_debug_output', 0);
    $toggle = variable_get('google_tag_role_toggle', GOOGLE_TAG_EXCLUDE_LISTED);
    $roles = variable_get('google_tag_role_list', array());
    $roles = array_filter($roles);
    if (empty($roles)) {
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED;
    }
    else {
      $satisfied = FALSE;

      // Check user roles against listed roles.
      $satisfied = (bool) array_intersect($roles, $user->roles);
      $satisfied = $toggle == GOOGLE_TAG_EXCLUDE_LISTED ? !$satisfied : $satisfied;
    }
    $debug ? drupal_set_message(t('role check @satisfied', array(
      '@satisfied' => $satisfied,
    ))) : '';
  }
  return $satisfied;
}

/**
 * Saves snippet files and data layer classes based on current settings.
 */
function google_tag_assets_create() {
  module_load_include('inc', 'google_tag', 'includes/admin');
  _google_tag_assets_create();
}

Functions

Namesort descending Description
google_tag_assets_create Saves snippet files and data layer classes based on current settings.
google_tag_flush_caches Implements hook_flush_caches().
google_tag_form_variable_edit_form_alter Implements hook_form_FORM_ID_alter() for variable_edit_form().
google_tag_form_variable_realm_edit_variables_form_alter Implements hook_form_FORM_ID_alter() for variable_realm_edit_variables_form().
google_tag_help Implements hook_help().
google_tag_insert_snippet Determines whether to insert the snippet on the response.
google_tag_menu Implements hook_menu().
google_tag_page_build Implements hook_page_build().
google_tag_permission Implements hook_permission().
google_tag_realm_values Returns applicable realm name and key for the request.
google_tag_variable_group_info Implements hook_variable_group_info().
google_tag_variable_info Implements hook_variable_info().
_google_tag_path_check Determines whether to insert the snippet based on the path settings.
_google_tag_role_check Determines whether to insert the snippet based on the user role settings.
_google_tag_status_check Determines whether to insert the snippet based on status code settings.

Constants

Namesort descending Description
GOOGLE_TAG_BLACKLIST_CLASSES Default list of tag classes to forbid.
GOOGLE_TAG_EXCLUDE_LISTED Default for matching all items except listed.
GOOGLE_TAG_INCLUDE_LISTED Default for matching only listed items.
GOOGLE_TAG_PATHS Default list of relative paths.
GOOGLE_TAG_STATUSES Default list of HTTP response statuses that override path conditions.
GOOGLE_TAG_WHITELIST_CLASSES Default list of tag classes to allow.