<?php
// $Id: adsense.module,v 1.68.2.20 2009/06/04 22:01:50 jcnventura Exp $

/**
 * @file
 * Displays Google AdSense ads on Drupal pages
 *
 * This is the core module of the AdSense package, with the Drupal hooks
 * and other administrative functions.
 */

// Ad types, link or ad
define('ADSENSE_TYPE_LINK',   1);
define('ADSENSE_TYPE_AD',     2);
define('ADSENSE_TYPE_SEARCH', 3);

define('ADSENSE_MAX_CHANNELS', 10);
define('ADSENSE_AD_CHANNEL_DEFAULT', '');

define('ADSENSE_ACCESS_PAGES_DEFAULT', '');
define('ADSENSE_BASIC_ID_DEFAULT', '');
define('ADSENSE_DISABLE_DEFAULT', 0);
define('ADSENSE_ID_MODULE_DEFAULT', 'adsense_basic');
define('ADSENSE_PLACEHOLDER_DEFAULT', 1);
define('ADSENSE_PLACEHOLDER_TEXT_DEFAULT', 'Google AdSense');
define('ADSENSE_SECTION_TARGETING_DEFAULT', 1);
define('ADSENSE_TEST_MODE_DEFAULT', 0);
define('ADSENSE_VISIBILITY_DEFAULT', 0);

define('ADSENSE_SECRET_ADTEST_DEFAULT', 0);
define('ADSENSE_SECRET_LANGUAGE_DEFAULT', '');

/**
 * This is the array that holds all ad formats.
 *
 * All it has is a multi-dimensional array indexed by a key, containing the ad type,
 * the description, Google's javascript ad code and the dimensions.
 *
 * To add a new code:
 * - Make sure the key is not in use by a different format
 * - Go to Google AdSense
 *   . Get the dimensions
 *   . Get the code
 * - Add it below
 *
 * @param $key
 *   (optional) ad key for which the format is needed
 * @return
 *   if no key is provided: array of supported ad formats as an array (type, desc(ription), code, width and height)
 *   if a key is provided, the array containing the ad format for that key, or NULL if there is no ad with that key
 */
function adsense_ad_formats($key = NULL) {
  $ads = array(
    '120x90'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 120x90'),   'code' => '120x90_0ads_al',   'width' => 120, 'height' =>  90),
    '160x90'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 160x90'),   'code' => '160x90_0ads_al',   'width' => 160, 'height' =>  90),
    '180x90'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 180x90'),   'code' => '180x90_0ads_al',   'width' => 180, 'height' =>  90),
    '200x90'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 200x90'),   'code' => '200x90_0ads_al',   'width' => 200, 'height' =>  90),
    '468x15'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 468x15'),   'code' => '468x15_0ads_al',   'width' => 468, 'height' =>  15),
    '728x15'   =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('4-links 728x15'),   'code' => '728x15_0ads_al',   'width' => 728, 'height' =>  15),
    '120x90_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 120x90'),   'code' => '120x90_0ads_al_s', 'width' => 120, 'height' =>  90),
    '160x90_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 160x90'),   'code' => '160x90_0ads_al_s', 'width' => 160, 'height' =>  90),
    '180x90_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 180x90'),   'code' => '180x90_0ads_al_s', 'width' => 180, 'height' =>  90),
    '200x90_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 200x90'),   'code' => '200x90_0ads_al_s', 'width' => 200, 'height' =>  90),
    '468x15_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 468x15'),   'code' => '468x15_0ads_al_s', 'width' => 468, 'height' =>  15),
    '728x15_5' =>  array('type' => ADSENSE_TYPE_LINK, 'desc' => t('5-links 728x15'),   'code' => '728x15_0ads_al_s', 'width' => 728, 'height' =>  15),
    '120x240'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Vertical banner'),  'code' => '120x240_as',       'width' => 120, 'height' => 240),
    '120x600'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Skyscraper'),       'code' => '120x600_as',       'width' => 120, 'height' => 600),
    '125x125'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Button'),           'code' => '125x125_as',       'width' => 125, 'height' => 125),
    '160x600'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Wide Skyscraper'),  'code' => '160x600_as',       'width' => 160, 'height' => 600),
    '180x150'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Small Rectangle'),  'code' => '180x150_as',       'width' => 180, 'height' => 150),
    '200x200'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Small Square'),     'code' => '200x200_as',       'width' => 200, 'height' => 200),
    '234x60'   =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Half Banner'),      'code' => '234x60_as',        'width' => 234, 'height' =>  60),
    '250x250'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Square'),           'code' => '250x250_as',       'width' => 250, 'height' => 250),
    '300x250'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Medium Rectangle'), 'code' => '300x250_as',       'width' => 300, 'height' => 250),
    '336x280'  =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Large Rectangle'),  'code' => '336x280_as',       'width' => 336, 'height' => 280),
    '468x60'   =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Banner'),           'code' => '468x60_as',        'width' => 468, 'height' =>  60),
    '728x90'   =>  array('type' => ADSENSE_TYPE_AD,   'desc' => t('Leaderboard'),      'code' => '728x90_as',        'width' => 728, 'height' =>  90),
  );

  if (!empty($key)) {
    if (array_key_exists($key, $ads)) {
      return $ads[$key];
    }
    elseif ($key == 'Search Box') {
      return array('type' => ADSENSE_TYPE_SEARCH, 'desc' => t('AdSense for Search'));
    }
    else {
      return NULL;
    }
  }

  return $ads;
}

/**
 * Implementation of hook_perm().
 */
function adsense_perm() {
  return array('administer adsense', 'hide adsense', 'use PHP for ad visibility');
}

/**
 * Implementation of hook_theme().
 */
function adsense_theme() {
  return array(
    'adsense_ad' => array(
      'arguments' => array('ad' => NULL, 'module' => NULL),
    ),
  );
}

/**
 * Implementation of hook_menu().
 */
function adsense_menu() {
  $items = array();

  $items['admin/settings/adsense'] = array(
    'title' => 'AdSense',
    'description' => 'Configure Google AdSense Ads.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('adsense_main_settings'),
    'access arguments'  => array('administer adsense'),
    'file' => 'adsense.admin.inc',
  );
  $items['admin/settings/adsense/main'] = array(
    'title' => 'Settings',
    'weight' => 10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/settings/adsense/publisher'] = array(
    'title' => 'Publisher ID',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('adsense_id_settings'),
    'access arguments'  => array('administer adsense'),
    'weight' => 0,
    'type' => MENU_LOCAL_TASK,
    'file' => 'adsense.admin.inc',
  );
  $items['admin/settings/adsense/publisher/site'] = array(
    'title' => 'Site ID',
    'weight' => -1,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );

  return $items;
}

/**
 * Implementation of hook_requirements().
 */
function adsense_requirements($phase) {
  $requirements = array();
  $t = get_t();
  switch ($phase) {
    // At runtime, make sure that we have a publisher ID
    case 'runtime':
      $basic_id = variable_get('adsense_basic_id', ADSENSE_BASIC_ID_DEFAULT);
      if (empty($basic_id)) {
        $requirements['adsense_basic_id'] = array(
          'title' => $t('AdSense'),
          'value' => $t('Publisher ID is not set.'),
          'description' => $t('Please configure it in the <a href="@url">AdSense settings page</a>.', array('@url' => url('admin/settings/adsense/publisher'))),
          'severity' => REQUIREMENT_ERROR,
        );
      }
      break;
  }
  return $requirements;
}

/**
 * Implementation of hook_filter().
 */
function adsense_filter($op, $delta = 0, $format = -1, $text = '') {
  switch ($op) {
    case 'list':
      return array(0 => t('AdSense tag'));
    case 'no cache':
      return TRUE;
    case 'description':
      return t('Substitutes an AdSense special tag with an ad.');
    case 'process':
      return _adsense_process_tags($text);
    default:
      return $text;
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function adsense_nodeapi(&$node, $op = 'view', $teaser, $page) {
  switch ($op) {
    case 'view':
      if (variable_get('adsense_section_targeting', ADSENSE_SECTION_TARGETING_DEFAULT)) {
        $node->content['adsense_start'] = array(
          '#value' => '<!-- google_ad_section_start -->',
          '#weight' => -5,
          );
        $node->content['adsense_end'] = array(
          '#value' => '<!-- google_ad_section_end -->',
          '#weight' => 5,
          );
      }
  }
}

/**
 * Implementation of hook_form_filter_admin_format_form_alter().
 */
function adsense_form_filter_admin_format_form_alter(&$form, $form_state) {
  // In Drupal <= 6.9 (or later) the HTML corrector has a bug that causes problems with the use of the AdSense tag filter
  sscanf(VERSION, "%d.%d", $major, $minor);
  if (($major == 6) && ($minor <= 99)) {
    if ((empty($form_state['post']) && $form['filters']['adsense/0']['#default_value'] && $form['filters']['filter/3']['#default_value']) ||
        ((!empty($form_state['post'])) && ($form_state['post']['filters']['adsense/0'] == '1') && ($form_state['post']['filters']['filter/3'] == '1'))) {
      drupal_set_message(t('The HTML corrector filter has a bug that causes problems with the use of the AdSense tag. Disabling the HTML corrector filter is recommended.'), 'warning', TRUE);
    }
  }
}

/**
 * Implementation of hook_filter_tips().
 */
function adsense_filter_tips($delta, $format, $long = FALSE) {
  return t('Use the special tag [adsense:<em>format</em>:<em>slot</em>] or [adsense:<em>format</em>:<em>[group]</em>:<em>[channel]</em><em>[:slot]</em>] or [adsense:block:<em>location</em>] to display Google AdSense ads.');
}

/**
 * Helper function to process the adsense input filter
 *
 * @param $text
 *   text of the node being processed
 * @return
 *   modified text with the adsense tags replaced by Google AdSense ads
 * @see adsense_filter()
 * @see adsense_display()
 */
function _adsense_process_tags($text) {
  $patterns = array(
    'block'  => '/\[adsense:block:([^\]]+)\]/x',
    'oldtag' => '/\[adsense:([^:]+):(\d*):(\d*):?(\w*)\]/x',
    'tag'    => '/\[adsense:([^:]+):([^\]]+)\]/x',
  );

  foreach ($patterns as $mode => $pattern) {
    if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) {
      foreach ($matches as $match) {
        switch ($mode) {
          case 'block':
            $mods = array(
              'adsense_managed',
              'adsense_cse',
              'adsense_oldcode',
              'adsense_search',
            );
            foreach ($mods as $module) {
              $module_blocks = module_invoke($module, 'block', 'list');
              if ($module_blocks) {
                foreach ($module_blocks as $delta => $block) {
                  if ($block['info'] == $match[1]) {
                    // Found the block with the same name as the passed arg
                    $block = module_invoke($module, 'block', 'view', $delta);
                    $ad = $block['content'];
                  }
                }
              }
            }
            break;
          case 'oldtag':
            // If not specified, default group and channel to 1
            if (empty($match[2])) {
              $match[2] = 1;
            }
            if (empty($match[3])) {
              $match[3] = 1;
            }
            $args = array(
              'format'  => $match[1],
              'group'   => $match[2],
              'channel' => $match[3],
              'slot'    => $match[4],
            );
            $ad = adsense_display($args);
            unset($args);
            break;
          case 'tag':
            $args = array(
              'format'  => $match[1],
              'slot'    => $match[2],
            );
            $ad = adsense_display($args);
            unset($args);
            break;
        }
        // Replace the first occurance of the tag, in case we have the same
        // tag more than once.
        $str = '/\\'. $match[0] .'/';
        $text = preg_replace($str, $ad, $text, 1);
      }
    }
  }

  return $text;
}

/**
 * Provides the Google AdSense Publisher ID / slot ID to be used in the ad
 *
 * If revenue sharing modules are installed, this function will call the
 * appropriate function in those modules.
 *
 * @param $format
 *   format of the ad being generated (optional)
 * @return
 *   If the format parameter is supplied, array with 'client' and 'slot'
 *   fields, otherwise just the Publisher ID string is returned
 */
function adsense_get_client_slot_id($format = NULL) {
  // Get the configured function
  $function = variable_get('adsense_id_module', ADSENSE_ID_MODULE_DEFAULT);

  if ($function != ADSENSE_ID_MODULE_DEFAULT) {
    // Call the function
    if (function_exists($function)) {
      $client_id = $function('client_id', $format);
      if ($client_id) {
        return $client_id;
      }
    }
  }
  return variable_get('adsense_basic_id', ADSENSE_BASIC_ID_DEFAULT);
}

/**
 * Generates the Google AdSense Ad
 *
 * This function is capable of handling two types of arguments:
 * 1. an array of arguments (format, group, channel or slot)
 * 2. 0 to 4 arguments:
 *   - 1st arg: format  (default '160x600')
 *   - 2nd arg: group   (default 1)
 *   - 3rd arg: channel (default 1)
 *   - 4th arg: slot    (default '')
 *
 * A valid format must always be provided. If a slot is provided, the ad is generated by the
 * new format modules, if not then the 'old' format modules are attempted.
 *
 * @return
 *   Publisher ID string
 * @see adsense_ad_formats()
 * @see _adsense_page_match()
 * @see _adsense_check_if_enabled()
 * @see _adsense_format_box()
 * @see _adsense_can_insert_another()
 * @see _adsense_cse_get_searchbox()
 * @see _adsense_search_get_searchbox()
 * @see _adsense_managed_get_ad()
 * @see _adsense_oldcode_get_ad()
 */
function adsense_display() {
  $numargs = func_num_args();
  if (($numargs == 1) && is_array(func_get_arg(0))) {
    $args = func_get_arg(0);
  }
  else {
    // handle the 'old' method of calling this function
    // adsense_display($format = '160x600', $group = 1, $channel = 1, $slot = '', $referral = 0, $cpa = '')

    $args['format'] = '160x600';
    $args['group'] = 1;
    $args['channel'] = 1;
    switch ($numargs) {
      case 6:
        // cpa isn't used anymore
      case 5:
        // referral is obsolete
      case 4:
        $args['slot'] = func_get_arg(3);
      case 3:
        $args['channel'] = func_get_arg(2);
      case 2:
        $args['group'] = func_get_arg(1);
      case 1:
        $args['format'] = func_get_arg(0);
    }
  }

  $ad = adsense_ad_formats($args['format']);

  if ($ad === NULL) {
    $ad = '<!--adsense: invalid format: '. $args['format'] .'-->';
  }
  elseif (!_adsense_page_match()) {
    // Check first if disabled or if we are at adsense limit or if this page doesn't allow adsense
    $ad = '<!--adsense: page not in match list-->';
  }
  elseif (!_adsense_check_if_enabled()) {
    global $user;

    // Ads are disabled
    if (variable_get('adsense_placeholder', ADSENSE_PLACEHOLDER_DEFAULT) || ($user->uid == 1)) {
      $width = array_key_exists('width', $ad) ? $ad['width'] : 0;
      $height = array_key_exists('height', $ad) ? $ad['height'] : 0;
      $text = variable_get('adsense_placeholder_text', ADSENSE_PLACEHOLDER_TEXT_DEFAULT) ." ${ad['desc']}";
      if ($user->uid == 1) {
        $text = 'Ads disabled for site admin<br />'. $text;
      }

      $ad = "<!--adsense: placeholder-->\n". _adsense_format_box($text, $width, $height);
    }
    else {
      $ad = '<!--adsense: ads disabled -->';
    }
  }
  elseif (!_adsense_can_insert_another($ad['type'])) {
    $ad = '<!--adsense: ad limit reached for type-->';
  }
  else {
    // If site Slot ID for this ad was passed, pass the format as argument
    // in case Publisher ID modules are enabled that can return different
    // Slot IDs per ad format
    $client_id_arg = !empty($args['slot']) ? $args['format'] : NULL;
    $client = adsense_get_client_slot_id($client_id_arg);

    if (is_array($client)) {
      // An array was received, use that Slot ID
      $slot = $client['slot'];
      $client = $client['client'];
    }
    elseif (isset($args['slot'])) {
      // Use the original site Slot ID
      $slot = $args['slot'];
    }

    // Ad should be displayed
    switch ($args['format']) {
      case 'Search Box':
        if (!empty($slot) && module_exists('adsense_cse')) {
          $ad = _adsense_cse_get_searchbox($client, $slot);
          $module = 'adsense_cse';
        }
        elseif (module_exists('adsense_search')) {
          $ad = _adsense_search_get_searchbox($client, $args['channel']);
          $module = 'adsense_search';
        }
        else {
          $ad = '<!--adsense: no AdSense for Search module found-->';
        }
        break;
      default:
        if (!empty($slot) && module_exists('adsense_managed')) {
          $ad = _adsense_managed_get_ad($args['format'], $client, $slot);
          $module = 'adsense_managed';
        }
        elseif (module_exists('adsense_oldcode')) {
          $ad = _adsense_oldcode_get_ad($args['format'], $client, $args['group'], $args['channel']);
          $module = 'adsense_oldcode';
        }
        else {
          $ad = '<!--adsense: no AdSense for Content module found-->';
        }

        $ad = theme('adsense_ad', $ad, $module);

        break;
    }
    // Remove empty lines
    $ad = str_replace("\n\n", "\n", $ad);
  }

  return $ad;
}

/**
 * Default AdSense ad unit theming. Simply add a div with the adsense and $module classes
 *
 * @param $ad
 *   string with the generated ad unit
 * @param $module
 *   module used to generate the ad
 *
 * @return
 *   string with the modified ad unit
 * @ingroup themeable
 */
function theme_adsense_ad($ad, $module) {
  return "<div class='adsense $module'>\n$ad\n</div>";
}

/**
 * Helper function to verify if ads are currently enabled
 *
 * @return
 *   TRUE if ad display is enabled, FALSE otherwise
 */
function _adsense_check_if_enabled() {
  if (!variable_get('adsense_basic_id', ADSENSE_BASIC_ID_DEFAULT)) {
    // Google AdSense Publisher ID is not configured
    return FALSE;
  }
  if (variable_get('adsense_disable', ADSENSE_DISABLE_DEFAULT)) {
    return FALSE;
  }
  if (variable_get('adsense_test_mode', ADSENSE_TEST_MODE_DEFAULT)) {
    return TRUE;
  }
  if (variable_get('adsense_secret_adtest', ADSENSE_SECRET_ADTEST_DEFAULT)) {
    return TRUE;
  }
  if (user_access('hide adsense')) {
    return FALSE;
  }

  return TRUE;
}

/**
 * Determine if AdSense has reached limit on this page. As per Google's
 * policies, a page can have up to 3 ad units and 3 link units.
 *
 * @return
 *   TRUE if we can insert another ad, FALSE if not allowed.
 */
function _adsense_can_insert_another($type = ADSENSE_TYPE_AD) {
  static $num_ads = array(
    ADSENSE_TYPE_AD     => 0,
    ADSENSE_TYPE_LINK   => 0,
    ADSENSE_TYPE_SEARCH => 0,
  );

  $max_ads = array(
    ADSENSE_TYPE_AD     => 3,
    ADSENSE_TYPE_LINK   => 3,
    ADSENSE_TYPE_SEARCH => 2,
  );

  if ($num_ads[$type] < $max_ads[$type]) {
    $num_ads[$type]++;
    return TRUE;
  }

  return FALSE;
}

/**
 * Determine if AdSense has permission to be used on the current page.
 *
 * @return
 *   TRUE if can render, FALSE if not allowed.
 */
function _adsense_page_match() {
  // Do not show ads on secure pages.
  // This is for two reasons:
  // Google would most probably not have indexed secure pages
  // and it also prevents warnings about mixed-content
  // Thanks to Brad Konia http://drupal.org/node/29585
  // Should be restricted when running on Apache only
  if (isset($_SERVER['SERVER_SOFTWARE']) && (stripos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== FALSE) && 
      isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on')) {
    return FALSE;
  }

  $pages = variable_get('adsense_access_pages', ADSENSE_ACCESS_PAGES_DEFAULT);
  $visibility = variable_get('adsense_visibility', ADSENSE_VISIBILITY_DEFAULT);

  if ($pages) {
    if ($visibility == 2) {
      return drupal_eval($pages);
    }
    $path = drupal_get_path_alias($_GET['q']);
    $page_match = drupal_match_path($path, $pages);
    if ($path != $_GET['q']) {
      $page_match = $page_match || drupal_match_path($_GET['q'], $pages);
    }

    return !($visibility xor $page_match);
  }
  else {
    return !$visibility;
  }
}

/**
 * Generate a box to display instead of the ad when it is disabled
 *
 * @return
 *   string with the HTML text to create the box
 */
function _adsense_format_box($text, $width, $height) {
  $dimensions = ((!empty($width)) && (!empty($height)))
                ? " width:". $width ."px; height:". $height ."px;" : "";

  return "<div class='adsense' style='text-align:center;display: table-cell;vertical-align:middle;border:solid 1px;${dimensions}'>${text}</div>";
}
