You are here

adsense_injector.module in Content Injector (formerly AdSense Injector) 7.3

Inject adsense ads into node content automatically.

File

adsense_injector.module
View source
<?php

define('ADSENSE_INJECTOR_DEFAULT_MANUAL_INSERTION_TEMPLATE', '<div style="float: right; margin: 0; padding: 0 1em .25em 0;">[adsense:250x250:0123456789]</div>');
define('ADSENSE_INJECTOR_DEFAULT_AD_BODY_TEMPLATE', '<div style="float: right; margin: 0; padding: 0 1em .25em 0;">[adsense:250x250:0123456789]</div>');
define('ADSENSE_INJECTOR_MANUAL_INSERTION_TAG', '[ai:insertion:inline]');
define('ADSENSE_INJECTOR_ADSENSE_TAG_PREFIX', '[adsense:');
define('ADSENSE_INJECTOR_DEFAULT_XPATH_INLINE', '/html/body/p[3]');
define('ADSENSE_INJECTOR_DEFAULT_XPATH_FRONT', '!!FRONT');
define('ADSENSE_INJECTOR_DEFAULT_XPATH_BACK', '!!BACK');
define('ADSENSE_INJECTOR_NOINJECT', 'ai:noinject');

/**
 * @file
 * Inject adsense ads into node content automatically.
 *
 * @ingroup adsense_injector
 */

/**
 * Process the body, handle adsense injector tags.
 * Look for an insertion point based on paragraphs.
 * @param $node_body The node body HTML to process.
 * @param $node The node object (for field insertion)
 * @param $skipList An array of insertions to skip.
 * @return The processed HTML.
 */
function adsense_injector_process_body($node_body, &$node, $skipList = null) {

  // Process adsense module tags in the template text, if enabled and possible.
  // Determine the target paragraph # from tags.
  // TODO: Remove dependency on adsense module.  Only call _adsense_process_tags()
  // if module is present.
  $vars = _ai_get_body_insertions_vars();
  $special = array();
  foreach ($vars as $key => $insertion) {

    //
    // Skip any keys in skipList
    if ($skipList && in_array($key, $skipList)) {
      continue;
    }

    //
    // Find and hold all non-xpath insertions for last.
    // Non-xpath insertions start with !! (!!TOP, !!BOTTOM)
    if (strpos($insertion['xpath'], '!!') === 0) {

      // Add it to the list and continue.
      $special[] = $insertion;
      continue;
    }
    else {

      // Process current insertion text.
      $template = _adsense_injector_process_tags($insertion['text']);

      //
      // TODO: allow views, blocks render tags: [ci:block:xxx] and [ci:view:xyz]
      // Given: body, list of xpath expressions and associated html snippet
      // Produce: new body
      $xpath_expr = $insertion['xpath'];

      // TODO: Ignore empty paragraphs if possible.
      // TODO: Smart location: Count words in each 'paragraph' element?
      // TODO: This doesn't take into account the <br> stuff inserted by auto-line breaks
      $node_body = _ai_injectXPath($node_body, $template, $xpath_expr);
    }
  }
  foreach ($special as $specialInsertion) {

    // Process current insertion text.
    $template = _adsense_injector_process_tags($specialInsertion['text']);
    switch ($specialInsertion['xpath']) {
      case ADSENSE_INJECTOR_DEFAULT_XPATH_FRONT:
        $node->content['adsense_injector_front'] = array(
          '#markup' => $template,
          '#weight' => variable_get('adsense_injector_front_weight', -6),
        );
        break;
      case ADSENSE_INJECTOR_DEFAULT_XPATH_BACK:
        $node->content['adsense_injector_back'] = array(
          '#markup' => $template,
          '#weight' => variable_get('adsense_injector_back_weight', 10),
        );
        break;
      default:
        watchdog('adsense_injector', t('Unknown special insertion type in adsense_injector_process_body'), WATCHDOG_ERROR);
    }
  }
  return $node_body;
}

/**
 * Handle 'manual' insertion at the specified position
 * @param $node_body The node body text to process
 * @param $at The insertion point of the '[ad_here]' marker.
 */
function _ai_insert_text_at($node_body, $at, $insertion) {
  $front = substr($node_body, 0, $at);
  $end = substr($node_body, $at + strlen(ADSENSE_INJECTOR_MANUAL_INSERTION_TAG));
  return $front . _ai_html_comment('ManualInsertion', 'START') . $insertion . _ai_html_comment('ManualInsertion', 'END') . $end;
}

/**
 * Should we inject now?
 * @param object $node
 * @return True if should inject an ad.
 */
function _ai_should_inject($node) {
  $node_types = variable_get('adsense_injector_nodes', array());
  if (empty($node_types[$node->type])) {
    return false;
  }
  return true;
}

/**
 * Implements hook_node_view().
 *
 * If rendering a full page, and the node type one of the configured types,
 * inject configured adsense content using simple string concatenation.
 *
 * @todo: Evaluate efficiency of string concat vs. sprintf, other methods.
 */
function adsense_injector_node_view($node, $view_mode) {

  //return;

  // TODO Remaining code in this function needs to be moved to the appropriate new hook function.
  // We only consider content types which are enabled for inline adsense.
  if (!_ai_should_inject($node)) {
    return;
  }

  // Insert an ad into the body.
  if ($view_mode == 'full') {

    //
    // Any other reason not to inject?
    if (_ai_noinject($node)) {
      return;
    }

    //
    // If there are any manual insertions, perform them and exit if
    // desired.
    if (_ai_manual_insertion($node)) {
      return;
    }

    //
    // Now do the auto insertions.
    _ai_process_auto_insertions($node);
  }
  elseif ($view_mode == 'teaser' && variable_get('adsense_injector_list_view', FALSE)) {

    // Process adsense module tags in the template text, if enabled and possible.
    $template = variable_get('adsense_injector_list_view_template', '%teaser<br class="clear"/>[adsense:728x90:0123456789]');
    if (function_exists('_adsense_process_tags')) {
      $template = _adsense_process_tags($template, null);
    }
    $node->content['body'][0]['#markup'] = strtr($template, array(
      '%teaser' => $node->content['body'][0]['#markup'],
    ));
  }
}

/**
 * Process automatic insertions.
 * @param unknown_type $node The node to operate on.
 */
function _ai_process_auto_insertions(&$node, $skipList = null) {

  //
  // Get the minimum node body wordcount for insertion.
  $minwords = _ai_node_body_view_minwords();

  // Count words in a string.
  // lifted from node.module node_validate() function.
  $text = $node->content['body'][0]['#markup'];
  $wordcount = count(explode(' ', $text, $minwords));
  if ($wordcount >= $minwords) {
    $node->content['body'][0]['#markup'] = adsense_injector_process_body($text, $node, $skipList);
  }
  else {
    $node->content['body'][0]['#markup'] .= _ai_html_comment('AutoInsertion', "Body has {$wordcount} words ({$minwords} required), insertion skipped");
  }
}

/**
 * Perform manual insertions, if any.
 * @param unknown_type $node  The node to process.
 * @return true if we performed manual insertions, false otherwise.
 */
function _ai_manual_insertion(&$node) {

  //
  // Check for manual insertion marker(s)
  $text = $node->content['body'][0]['#markup'];
  $at = strpos($text, ADSENSE_INJECTOR_MANUAL_INSERTION_TAG);
  if (!($at === FALSE)) {
    $insertion = _ai_get_insertion_text_for('inline');

    // TODO: Grab item id from found tag.
    $insertion = _adsense_injector_process_tags($insertion);
    $node->content['body'][0]['#markup'] = _ai_insert_text_at($text, $at, $insertion);

    //
    // Now do auto insertions but skip the 'inline' insertion.
    adsense_injector_process_body($text, $node, array(
      'inline',
    ));
    return true;
  }
  return false;
}

/**
 * Determine whether there are any reasons not to inject ads.
 * 
 * @param unknown_type $node The node to process.
 * @return true if we should skip injection on this node.
 */
function _ai_noinject($node) {

  // Bail if node has 'ai_noinject' tag in first N characters.
  // TODO: make these regexp, configurable in admin.
  $text = $node->content['body'][0]['#markup'];
  $front = substr($text, 0, variable_get('adsense_injector_front_maxlen', 512));
  $skip = !(strpos($front, ADSENSE_INJECTOR_NOINJECT) === FALSE);

  /// Bail if we find [adsense: tags in body.
  if (!$skip) {
    $skip = !(strpos($text, ADSENSE_INJECTOR_ADSENSE_TAG_PREFIX) == FALSE);
  }
  return $skip;
}

/**
 * Implements hook_menu().
 */
function adsense_injector_menu() {

  // debug('here');
  $items = array();
  $items['admin/config/content/adsense_injector'] = array(
    'title' => 'AdSense Injector',
    'description' => 'Insert Google AdSense ads content automatically.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'adsense_injector_admin_settings',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'adsense_injector.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Inject an HTML fragment into the node body, using XPath expression.
 * @param $body The node body to operate on.
 * @param $fragment The HTML fragment to inject.
 * @param $xpath_expression The XPath expression to use
 * @param $encoding The character encoding to use.  Defaults to 'UTF-8'.
 * @todo Fix entity encoding problem! 
 *   http://www.php.net/manual/en/domdocument.loadhtml.php#95251
 *   http://devzone.zend.com/article/8855
 *   
 */
function _ai_injectXPath($body, $fragment, $xpath_expression, $encoding = 'UTF-8') {
  try {
    $doc = new DomDocument();

    // HACK: Fix entity encoding http://www.php.net/manual/en/domdocument.loadhtml.php#95251 et al.
    $encoding_string = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset={$encoding}\">";
    $prehtml = "<html><head>{$encoding_string}</head><body>";
    $posthtml = "</body></html>";
    @$doc
      ->loadHTML($prehtml . $body . $posthtml);
    $xpath = new DOMXpath($doc);
    $ent = $xpath
      ->query($xpath_expression);
    if (!empty($ent) && $ent->length > 0) {

      // http://stackoverflow.com/questions/2255158/how-do-i-insert-html-into-a-php-dom-object
      $frag = $doc
        ->createDocumentFragment();
      $frag
        ->appendXML($fragment);
      $ent
        ->item(0)
        ->appendChild($frag);

      // DOMDocument->saveHTML() doesn't accept a child element unless in PHP
      // 5.3.6 or above.  So we'll have to do this the ugly way (as described
      // in http://php.net/manual/en/domdocument.savehtml.php#85165
      // $html_fragment = $doc->saveHTML($doc->getElementsByTagName('body')->item(0));
      //
      // If needed, we can do a manual encoding fixup
      // $doc = _ai_fixup_html_encoding($doc);
      $html_fragment = preg_replace('/^<!DOCTYPE.+?>/', '', str_replace(array(
        $encoding_string,
        '<html>',
        '</html>',
        '<head>',
        '</head>',
        '<body>',
        '</body>',
      ), '', $doc
        ->saveHTML()));
    }
    else {
      $html_fragment = $body . _ai_html_comment('injectXPath', "XPath query '{$xpath_expression}' result empty; insertion skipped ");
    }
  } catch (Exception $e) {
    $html_fragment = $body . _ai_html_comment('injectXPath error', $e
      ->getMessage());
  }
  return $html_fragment;
}

/**
 * // From: http://devzone.zend.com/article/8855
 * @param unknown_type $dom
 */
function _ai_fixup_html_encoding($dom) {
  $xpath = new DOMXPath($dom);
  $metaTags = $xpath
    ->query('/html/head/meta');

  // Unfortunately DOMXPath supports only XPath 1.0 and we have to iterate
  // through meta tags instead of selecting the node using
  // '/html/head/meta[lower-case(@http-equiv)="content-type"]'
  // (fn:lower-case() function came with XPath 2.0)
  for ($count = 0; $count < $metaTags->length; $count++) {
    $httpEquivAttribute = $metaTags
      ->item($count)->attributes
      ->getNamedItem('http-equiv');
    if ($httpEquivAttribute !== null && strtolower($httpEquivAttribute->value) == 'content-type') {
      $fragment = $dom
        ->createDocumentFragment();
      $fragment
        ->appendXML('<meta http-equiv="Content-type" content="text/html; charset=UTF-8"/>');
      $metaTags
        ->item($count)->parentNode
        ->replaceChild($fragment, $metaTags
        ->item($count));
      break;
    }
  }

  // Do nothing if meta tag is not found
  return $dom;
}

/**
 * Get the raw insertion text for the specified tag.
 * @param string $tag The insertion 'tag' ([top, inline, bottom]).
 *   No validation is performed on this parameter.  If the tag is unknown,
 *   the return value will be empty.
 * @return The insertion text, if 'tag' is known, empty otherwise.
 */
function _ai_get_insertion_text_for($tag) {
  $insertions = _ai_get_body_insertions_vars();
  return $insertions[$tag]['text'];
}

/**
 * Get the default body insertions variables.
 * @return An array of default body insertion variables.
 */
function _ai_get_default_body_insertions_vars() {
  return array(
    'body_insertions' => array(
      'top' => array(
        'name' => 'Top',
        'text' => '<!--FRONT-->',
        'xpath' => ADSENSE_INJECTOR_DEFAULT_XPATH_FRONT,
        'adminui' => false,
      ),
      'inline' => array(
        'name' => 'Inline',
        'text' => ADSENSE_INJECTOR_DEFAULT_AD_BODY_TEMPLATE,
        'xpath' => ADSENSE_INJECTOR_DEFAULT_XPATH_INLINE,
        'adminui' => true,
      ),
      'bottom' => array(
        'name' => 'Bottom',
        'text' => '<!--BACK-->',
        'xpath' => ADSENSE_INJECTOR_DEFAULT_XPATH_BACK,
        'adminui' => false,
      ),
    ),
  );
}

/**
 * Build list of body insertion variables with proper defaults.
 * This is necessary because not all elements may be stored in the variable
 * table. 
 * @return The body insertions variables array.
 */
function _ai_get_body_insertions_vars() {

  /** List of default body insertion templates **/
  $default_body_insertions = _ai_get_default_body_insertions_vars();

  //  Now build the list of insertions...
  $body_view = variable_get('adsense_injector_body_view', $default_body_insertions);
  $defitems = array(
    'name',
    'xpath',
    'adminui',
  );
  foreach ($body_view['body_insertions'] as $key => $item) {
    foreach ($defitems as $check) {

      // Get defaults for items that might be missing from variables table...
      if (!array_key_exists($check, $item)) {
        $body_view['body_insertions'][$key][$check] = $default_body_insertions['body_insertions'][$key][$check];
      }
    }
  }
  return $body_view['body_insertions'];
}

/**
 * Insert at front of body.
 * @param string $node_body The node body to process.
 * @param string $text The raw text to insert.  This should already be
 * processed for any macros/tags/tokens etc.
 * @return The processed node body.
 */
function _adsense_injector_insert_front($node_body, $text) {
  return $text . $node_body;
}

/**
 * Insert at end of boddy. 
 * @param string $node_body The node body to process.
 * @param string $text The raw text to insert.  This should already be
 * processed for any macros/tags/tokens etc.
 * @return The processed node body.
 */
function _adsense_injector_insert_back($node_body, $text) {
  return $node_body . $text;
}

/**
 * Process template text tags.  At present, we support only the adsense module
 * tags.  Later, we wish to support views and blocks rendering if current user
 * has permissions.
 * @param unknown_type $text
 * @return The processed template text. 
 */
function _adsense_injector_process_tags($text) {
  if (function_exists('_adsense_process_tags')) {
    $text = _adsense_process_tags($text, null);
  }
  return $text;
}

/**
 * Helper: get min words variable.
 */
function _ai_node_body_view_minwords() {
  return _ai_admin_nested_vget(variable_get('adsense_injector_body_view', array()), 'minwords', 75);
}

/**
 * Helper: Get node body insertion enabled variable.
 */
function _ai_node_body_view_enabled() {
  return _ai_admin_nested_vget(variable_get('adsense_injector_body_view', array()), 'enable', TRUE);
}

/**
 * Get a nested variable from a #tree structured variable set in the 
 * admin pages for this module.  Used in Admin pages but also needed by this
 * module so we'll define it here.
 * @param unknown_type $fields 
 * @param unknown_type $name
 * @param unknown_type $def
 */
function _ai_admin_nested_vget($fields, $name, $def) {
  return isset($fields[$name]) ? $fields[$name] : $def;
}

/**
 * Generate a formatted HTML comment.
 * @param unknown_type $id
 * @param unknown_type $text
 */
function _ai_html_comment($id, $text) {
  return '<!-- adsense_injector: (' . $id . ') ' . $text . '-->';
}

Functions

Namesort descending Description
adsense_injector_menu Implements hook_menu().
adsense_injector_node_view Implements hook_node_view().
adsense_injector_process_body Process the body, handle adsense injector tags. Look for an insertion point based on paragraphs.
_adsense_injector_insert_back Insert at end of boddy.
_adsense_injector_insert_front Insert at front of body.
_adsense_injector_process_tags Process template text tags. At present, we support only the adsense module tags. Later, we wish to support views and blocks rendering if current user has permissions.
_ai_admin_nested_vget Get a nested variable from a #tree structured variable set in the admin pages for this module. Used in Admin pages but also needed by this module so we'll define it here.
_ai_fixup_html_encoding // From: http://devzone.zend.com/article/8855
_ai_get_body_insertions_vars Build list of body insertion variables with proper defaults. This is necessary because not all elements may be stored in the variable table.
_ai_get_default_body_insertions_vars Get the default body insertions variables.
_ai_get_insertion_text_for Get the raw insertion text for the specified tag.
_ai_html_comment Generate a formatted HTML comment.
_ai_injectXPath Inject an HTML fragment into the node body, using XPath expression.
_ai_insert_text_at Handle 'manual' insertion at the specified position
_ai_manual_insertion Perform manual insertions, if any.
_ai_node_body_view_enabled Helper: Get node body insertion enabled variable.
_ai_node_body_view_minwords Helper: get min words variable.
_ai_noinject Determine whether there are any reasons not to inject ads.
_ai_process_auto_insertions Process automatic insertions.
_ai_should_inject Should we inject now?

Constants