ad.module in Advertisement 7.3
Same filename and directory in other branches
Core code for the ad module.
File
ad.moduleView source
<?php
/**
* @file
* Core code for the ad module.
*/
define('AD_IMPRESSION_ID_PLACEHOLDER', 'AD_IMPRESSION_ID');
include_once 'ad.features.inc';
include_once 'ad.field.inc';
include_once 'ad.session.inc';
/**
* Implements hook_module_implements_alter().
*/
function ad_module_implements_alter(&$implementations, $hook) {
if ($hook === 'schema_alter' || $hook === 'modules_enabled') {
$group = $implementations['ad'];
unset($implementations['ad']);
$implementations['ad'] = $group;
}
}
/**
* Implements hook_schema_alter().
*/
function ad_schema_alter(&$schema) {
if (isset($schema['eck_tracked_event'])) {
foreach ([
'url',
'referrer',
'user_agent',
'page_title',
] as $field) {
$schema['eck_tracked_event']['fields'][$field] = array(
'description' => 'Text',
'type' => 'text',
'not null' => TRUE,
);
}
}
}
/**
* Implements hook_menu().
*/
function ad_menu() {
$items = array();
$items['ad/get'] = array(
'type' => MENU_CALLBACK,
'title' => 'Get ads',
'page callback' => 'ad_get_ads',
'access callback' => TRUE,
);
$items['ad/click/%/%'] = array(
'type' => MENU_CALLBACK,
'title' => 'Click ads',
'page callback' => 'ad_click',
'page arguments' => array(
2,
3,
),
'access callback' => TRUE,
);
return $items;
}
/**
* Implements hook_page_alter().
*/
function ad_page_alter(&$page) {
// Path to call to serve the ads.
// By default it calls the menu item this module defines. With proper
// configuration the ad_serve.php script can be used instead, possibly also
// linking it with another name, to mask it from ad blockers.
drupal_add_js(array(
'ad' => array(
'ServePath' => variable_get('ad_serve_path', 'ad/get'),
'UserID' => $GLOBALS['user']->uid,
),
), 'setting');
}
/**
* Implements hook_node_load().
*/
function ad_node_load($nodes, $types) {
$nids = array_keys($nodes);
$result = db_query('SELECT * FROM {ad_node} WHERE nid IN (:nids)', array(
':nids' => $nids,
));
foreach ($result as $record) {
$nid = $record->nid;
$nodes[$nid]->total_clicks = $record->total_clicks;
$nodes[$nid]->total_impressions = $record->total_impressions;
}
}
/**
* Implements hook_preprocess_node().
*/
function ad_preprocess_node(&$variables, $hook) {
$node = $variables['node'];
if (ad_is_node_ad($node)) {
$variables['ad_total_impressions'] = '<div>' . t('Total impressions: @total_impressions', array(
'@total_impressions' => !empty($node->total_impressions) ? $node->total_impressions : 0,
)) . '</div>';
$variables['ad_total_clicks'] = '<div>' . t('Total clicks: @total_clicks', array(
'@total_clicks' => !empty($node->total_clicks) ? $node->total_clicks : 0,
)) . '</div>';
}
}
/**
* Implements hook_menu_local_tasks_alter().
*
* Add a link to the stats to the node tabs, for ads only.
*/
function ad_menu_local_tasks_alter(&$data, $router_item, $root_path) {
if (strpos($root_path, 'node/%') === 0) {
foreach ($router_item['page_arguments'] as $item) {
if (!empty($item->type) && ad_is_node_ad($item)) {
$data['tabs'][0]['output'][] = array(
'#theme' => 'menu_local_task',
'#link' => array(
'title' => t('Ad Stats'),
'href' => 'admin/ad/stats/' . $item->nid,
'localized_options' => array(
'attributes' => array(
'title' => t('Ad Stats'),
),
),
),
);
}
}
}
}
/**
* Callback during a click event.
*
* @param int $nid
* The id of the ad being clicked.
* @param int $impression_unique_id
* The unique id of the impression that generated the click.
*/
function ad_click($nid, $impression_unique_id) {
$ad = ad_get_advertisement($nid);
if (!empty($ad)) {
if ($impression_unique_id != AD_IMPRESSION_ID_PLACEHOLDER) {
// Do not track clicks from the administration pages.
$impression = ad_load_tracked_event_by_unique_id($impression_unique_id);
ad_track_event('click', $ad['nid'], $impression_unique_id, $impression['url'], $impression['page_title'], $impression['page_unique_id'], NULL, NULL);
}
$destination = ad_get_destination($ad);
if (variable_get('ad_debug', FALSE)) {
drupal_set_message(t('click: ad @nid, destination @destination', array(
'@nid' => $nid,
'@destination' => $destination,
)), 'status');
drupal_goto($_SERVER['HTTP_REFERER']);
}
else {
drupal_goto($destination);
}
}
else {
// @todo: log invalid ad.
drupal_set_message(t('Invalid ad.'), 'warning');
drupal_goto();
}
}
/**
* Callback for the Ajax request to get ads.
*/
function ad_get_ads() {
$result = array();
$page_unique_id = ad_uniqid('page');
if (!empty($_POST['ads'])) {
if (!empty($_POST['page'])) {
$page_url = $_POST['page']['url'];
$page_title = $_POST['page']['title'];
}
$uid = (int) $_POST['uid'];
ad_session_start();
foreach ($_POST['ads'] as $element_id => $data) {
$view_name = $data['view'];
$display = isset($data['display']) ? $data['display'] : 'default';
// Be sure the view is specifically set for ads.
if (ad_is_view_ad_enabled($view_name, $display)) {
$view_arguments = array(
$view_name,
$display,
);
if (!empty($data['arguments'])) {
$view_arguments = array_merge($view_arguments, explode('/', $data['arguments']));
}
$view_result = ad_get_cache('views_get_view_result', $view_arguments);
// Finally, return a result.
if (!empty($view_result)) {
// Choose one.
$random_id = $view_result[array_rand($view_result)]['nid'];
$view_mode = ad_get_view_view_mode($view_name, $display);
$rendered_ad = ad_get_rendered_node($random_id, $view_mode);
// Track it.
$impression_unique_id = ad_track_event('impression', $random_id, '', $page_url, $page_title, $page_unique_id, NULL, $uid);
$rendered_ad = strtr($rendered_ad, array(
AD_IMPRESSION_ID_PLACEHOLDER => $impression_unique_id,
));
$result[$element_id] = array(
'rendered_ad' => $rendered_ad,
);
}
}
}
}
drupal_json_output($result);
}
/**
* Check whether a view is specifically set for ads.
*
* @param $view_name
* The name of the view.
* @param $display
* (optional) The name of the display to check. Defaults to "default".
*
* @return bool
* TRUE if the view is ad enabled; FALSE otherwise.
*/
function ad_is_view_ad_enabled($view_name, $display = 'default') {
$enabled = FALSE;
foreach (ad_get_info() as $node_type => $info) {
if ($info['display_view'] == $view_name) {
$enabled = TRUE;
continue;
}
}
return $enabled;
}
/**
* Return whether a node is used for ads.
*
* @param stdClass|array $node
* A node object or array.
*
* @return bool
* Whether the node is used for ads.
*/
function ad_is_node_ad($node) {
$type = is_object($node) ? $node->type : $node['type'];
return in_array($type, array_keys(ad_get_info()));
}
/**
* Return the info about the node type(s) that implement ads.
*
* @param bool $cache
* (optional) Whether to use the cache, if possible. Defaults to FALSE.
*
* @return array
* An array with the info about the ad node types.
*/
function ad_get_info($cache = TRUE) {
if (!$cache) {
$info = array();
foreach (module_implements('ad_info') as $module) {
$info = array_merge($info, module_invoke($module, 'ad_info'));
}
return $info;
}
else {
return ad_get_cache('ad_get_info', array(
FALSE,
));
}
}
/**
* Load an Ad node from the cache.
*
* This function is safe to call from a bootstrap phase lower than full, as it
* will perform a full bootstrap if needed.
*
* @param int $nid
* The ad node id.
* @param bool $cache
* (optional) Whether to use the cache, if possible. Defaults to TRUE.
*
* @return array
* The ad node, turned into an array.
*/
function ad_get_advertisement($nid, $cache = TRUE) {
if (!$cache) {
return node_load($nid);
}
else {
return ad_get_cache('ad_get_advertisement', array(
$nid,
FALSE,
));
}
}
/**
* Return a node rendered in a certain view mode, possibly from the cache.
*
* @param int $nid
* The ID of node to be rendered.
* @param string $view_mode
* The view mode to render the node into.
* @param bool $cache
* (optional) Whether to use the cache, if possible. Defaults to TRUE.
*
* @return string
* The rendered node using the provider view mode.
*/
function ad_get_rendered_node($nid, $view_mode, $cache = TRUE) {
if (!$cache) {
$node = node_load($nid);
$render_array = node_view($node, $view_mode);
return drupal_render($render_array);
}
else {
return ad_get_cache('ad_get_rendered_node', array(
$nid,
$view_mode,
FALSE,
));
}
}
/**
* Return the destination URL of an ad.
*
* This function supports simple text fields and link fields.
*
* @param stdClass|array $ad
* The ad node as an object or array.
*
* @return string
* The destination URL.
*/
function ad_get_destination($ad) {
// We need to cache objects as arrays, so we might get either an object or
// an array.
$node = is_object($ad) ? ad_object_to_array($ad) : $ad;
$node_type = $node['type'];
$ad_info = ad_get_info();
$link_field = $ad_info[$node_type]['link_field'];
if (!empty($node[$link_field][LANGUAGE_NONE][0]['url'])) {
return $node[$link_field][LANGUAGE_NONE][0]['url'];
}
elseif (!empty($node[$link_field][LANGUAGE_NONE][0]['value'])) {
return $node[$link_field][LANGUAGE_NONE][0]['value'];
}
else {
return '';
}
}
/**
* Return a view, eventually from the cache.
*
* Note that the view will be turned into an array, as its object properties.
* This function is safe to call from a bootstrap phase lower than full, and
* will throw a AdCacheNotFoundException eventually.
*
* @param $view_name
* The name of the view to be retrieved.
*
* @return array
* The view, turned into an array.
*/
function ad_get_cached_view($view_name) {
return ad_get_cache('views_get_view', $view_name);
}
/**
* Get the node view mode used in the ad view.
*
* @param string $view_name
* The name of the view to query.
* @param string $display
* The view display to query.
*
* @return string
* The node view mode used in the view, or FALSE if a view mode was not found.
*/
function ad_get_view_view_mode($view_name, $display) {
$view = ad_get_cached_view($view_name);
if ($found = _ad_recursive_find($view['display'][$display], 'view_mode')) {
return $found;
}
elseif ($found = _ad_recursive_find($view['display']['default'], 'view_mode')) {
return $found;
}
else {
return FALSE;
}
}
/**
* Recursively search an array for a key, and return its value.
*
* @param array $array
* The array to be searched.
* @param $needle
* The key to search.
*
* @return mixed
* The value of the key which was found.
*/
function _ad_recursive_find(array $array, $needle) {
$iterator = new RecursiveArrayIterator($array);
$recursive = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursive as $key => $value) {
if ($key === $needle) {
return $value;
}
}
}
/**
* Return the eventually cached results of a callback
*
* @param string $callback
* The callback to call if the result was not found in the cache.
* @param mixed $args
* (optional). The parameter(s) to be passed to the callback. Defaults to an
* empty array.
*
* @return array
* The result of the call. Any object will be turned into an array, to avoid
* issues with autoloaders when a full bootstrap was not performed.
*
* @throws AdCacheNotFoundException
* Throw a cache not found exception if the bootstrap phase is not full.
* This exception will be caught by the ad.php script, which will then
* initiate a full bootstrap.
*/
function ad_get_cache($callback, $args = array()) {
$results =& drupal_static(__FUNCTION__);
if (!is_array($args)) {
$args = array(
$args,
);
}
$cid = implode(':', array_merge(array(
'ad',
$callback,
), _ad_flatten($args)));
if (strlen($cid) > 255) {
// We're building the cid dynamically, so we can't be sure it will be less
// than 255 chars, which is the max length when using the db cache backend.
// Therefore cut the cid, and replace the last part with a hash; this will
// hide part of the cid, but it will avoid errors while guaranteeing
//// uniqueness.
$hash = hash('sha256', $cid);
$cid = substr($cid, 0, 255 - strlen($hash)) . $hash;
}
$cache_lifetime = variable_get('ad_cache_lifetime', 0);
if (!isset($results[$cid])) {
if ($cache_lifetime > 0) {
$cache = cache_get($cid);
if (empty($cache) || $cache->expire < REQUEST_TIME) {
ad_bootstrap_full();
$results[$cid] = ad_object_to_array(call_user_func_array($callback, $args));
cache_set($cid, $results[$cid], 'cache', REQUEST_TIME + $cache_lifetime);
}
else {
$results[$cid] = $cache->data;
}
}
else {
ad_bootstrap_full();
$result = call_user_func_array($callback, $args);
$results[$cid] = ad_object_to_array($result);
}
}
return $results[$cid];
}
/**
* Flatten a structure into an array suitable to be the base of a cache id.
*
* @param $parts
* A structure to flatten.
*
* @return array
* An array which can be imploded to get a cache id.
*/
function _ad_flatten($parts) {
$result = array();
if (is_array($parts)) {
foreach ($parts as $key => $value) {
$result[] = $key;
$result = array_merge($result, _ad_flatten($value));
}
}
elseif (is_object($parts)) {
$result = array_merge($result, _ad_flatten(ad_object_to_array($parts)));
}
else {
$result = array(
$parts,
);
}
return $result;
}
/**
* Track an event, like a click or an impression.
*
* @param string $type
* The type of the event; this is the bundle of the entity, so it needs to be
* defined as such.
* @param int $nid
* The ID of the ad node associated with the event.
* @param string $parent_unique_id
* (optional) The id of the parent event; e.g. impressions are parents of
* clicks. Defaults to ''.
* @param string $url
* (optional) The URL of the page. Defaults to NULL.
* @param string $page_title
* (optional) The title of the page. Defaults to NULL.
* @param string $page_unique_id
* (optional) A unique ID identifying a page view; all events (impressions and
* clicks) generated in a certain page view will have the same unique ID.
* Defaults to NULL.
* @param string $event_unique_id
* (optional) The unique id of the event; if NULL, a unique id will be
* generated. Defaults to NULL.
* @param int $uid
* (optional) The ID of the user associated with the event. If NULL, the
* current user will be used. Defaults to NULL.
*
* @return int
* The unique ID of the newly created event.
*/
function ad_track_event($type, $nid, $parent_unique_id = '', $url = NULL, $page_title = NULL, $page_unique_id = NULL, $event_unique_id = NULL, $uid = NULL) {
if (is_null($event_unique_id)) {
$event_unique_id = ad_uniqid($type);
}
if (is_null($uid)) {
$uid = $GLOBALS['user']->uid;
}
$data = array(
'type' => $type,
'uid' => $uid,
'created' => REQUEST_TIME,
'ad' => $nid,
'ip_address' => ip_address(),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'url' => $url,
// Drupal guarantees HTTP_REFERER is always populated (worst case it's
// empty.
'referrer' => $_SERVER['HTTP_REFERER'],
'page_title' => $page_title,
'page_unique_id' => $page_unique_id,
'parent_unique_id' => $parent_unique_id,
'session' => ad_session_get(),
'unique_id' => $event_unique_id,
);
$queue_info = variable_get('ad_queue_info', []);
if (!empty($queue_info[$type])) {
$queue = DrupalQueue::get('ad');
// Store the impression into a queue for later processing.
$queue
->createItem($data);
}
else {
ad_bootstrap_full();
$event = entity_create('tracked_event', $data);
$event
->save();
if (in_array($type, array(
'click',
'impression',
))) {
$counter = $type == 'click' ? 'total_clicks' : 'total_impressions';
ad_increase_denormalized_counter($nid, $counter);
}
}
// Another implementation might return UUIDs, if that's faster.
return $event_unique_id;
}
/**
* Increase one of the denormalized counters of an ad by one.
*
* @param $nid
* The ID of the affected ad.
* @param $field
* The field to increase: 'total_clicks' or 'total_impressions'.
*/
function ad_increase_denormalized_counter($nid, $field) {
// Denormalize these counters taking advantage of the properties of the base
// table. This allows to be quick while still easily avoiding race conditions.
$field = db_escape_field($field);
db_query("\n INSERT INTO {ad_node}\n (nid, {$field})\n VALUES (:nid, 1)\n ON DUPLICATE KEY UPDATE {$field} = {$field} + 1\n ", array(
':nid' => $nid,
));
}
/**
* Load a tracked event by its unique id.
*
* @param $unique_id
* The unique id to search for.
*
* @return array
* The tracked event as an array, or FALSE if it wasn't found.
*/
function ad_load_tracked_event_by_unique_id($unique_id) {
return db_query('SELECT * FROM {eck_tracked_event} WHERE unique_id = :unique_id', array(
'unique_id' => $unique_id,
))
->fetchAssoc();
}
/**
* Return a unique string which can be used to identify an event.
*
* @param string $type
* The type of unique id.
*
* @return string
* A unique string.
*/
function ad_uniqid($type) {
// If serving from multiple server, use a different value for each server to
// avoid duplicates.
$prefix = $type . '.' . variable_get('ad_uniqid_prefix', $_SERVER['HTTP_HOST']);
$uniqid = uniqid($prefix . '.', TRUE);
return hash('sha256', $uniqid);
}
/**
* Validate the link, eventually setting a form error.
*/
function _ad_eck_validate_url($element, &$form_state, $form) {
if (!empty($element['#value'])) {
$url = $element['#value'];
if (function_exists('link_validate_url')) {
$validation = link_validate_url($url);
if (!$validation) {
form_error($element, t('Invalid URL.'));
}
elseif ($validation == LINK_INTERNAL) {
// @todo: should we allow internal links?
form_error($element, t('The URL cannot be internal.'));
}
}
}
}
/**
* Implements hook_eck_entity_type_insert().
*
* Note that this is triggered when a type is added, not when an entity is
* created.
*/
function ad_eck_entity_type_insert($entity_type) {
// Add an index which is used in the stats view.
if ($entity_type->data['name'] == 'tracked_event') {
if (!db_index_exists('eck_tracked_event', 'ad')) {
// Be optimistic and use a BIGINT as event id.
$table = 'eck_tracked_event';
$definition = array(
'description' => 'The primary identifier for a(n) tracked_event.',
'type' => 'serial',
'unsigned' => TRUE,
'size' => 'big',
'not null' => TRUE,
);
db_change_field($table, 'id', 'id', $definition);
// Also add an index which is used often.
db_add_index($table, 'ad', array(
'ad',
));
db_add_index($table, 'unique_id', array(
'unique_id',
));
db_add_index($table, 'parent_unique_id', array(
'parent_unique_id',
));
}
}
}
/**
* Turn an object into an array, eventually also turning any embedded object.
*
* This is done because when a object is retrieved from the cache, and a full
* bootstrap was not performed, class autoloading fails, and the resulting
* object is incomplete.
*
* @param $object
* The object to be turned into the array.
* @return array
* The resulting array.
*/
function ad_object_to_array($object) {
static $known_objects;
if (is_object($object)) {
// Don't consider objects which can't be serialized, like Queries.
if ($object instanceof Query) {
return;
}
elseif (!empty($known_objects) && array_search(serialize($object), $known_objects)) {
return;
}
else {
// Avoid recursion.
$known_objects[] = serialize($object);
$object = (array) clone $object;
}
}
if (is_array($object)) {
$array = array();
foreach ($object as $key => $val) {
$array[$key] = ad_object_to_array($val);
}
}
else {
$array = $object;
}
return $array;
}
/**
* Eventually perform a full Drupal bootstrap.
*/
function ad_bootstrap_full() {
// Next line requires Drupal 7.33.
// See https://www.drupal.org/node/667098#comment-9301931
if (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL) {
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
drupal_add_http_header('X-Ad-bootstrap-phase', 'full');
}
}
/*
* Implements hook_cron_queue_info().
*/
function ad_cron_queue_info() {
$queues['ad'] = array(
'worker callback' => 'ad_delayed_track_impression',
'time' => 120,
);
return $queues;
}
/**
* Worker callback to track the impression from the queue.
*
* @param $data
*/
function ad_delayed_track_impression($data) {
$event = entity_create('tracked_event', $data);
$event
->save();
ad_increase_denormalized_counter($data['ad'], 'total_impressions');
// Also denormalize data in any existing click.
// Text fields are limited to 255 characters (see eck_property_type_schema())
// so truncate to avoid errors.
db_query("\n UPDATE eck_tracked_event SET\n url = :url,\n page_title = :page_title,\n page_unique_id = :page_unique_id\n WHERE\n type = 'click' AND parent_unique_id = :unique_id\n ", array(
':url' => substr($data['url'], 0, 255),
':page_title' => substr($data['page_title'], 0, 255),
':page_unique_id' => substr($data['page_unique_id'], 0, 255),
':unique_id' => substr($data['unique_id'], 0, 255),
));
}
Functions
Name | Description |
---|---|
ad_bootstrap_full | Eventually perform a full Drupal bootstrap. |
ad_click | Callback during a click event. |
ad_cron_queue_info | |
ad_delayed_track_impression | Worker callback to track the impression from the queue. |
ad_eck_entity_type_insert | Implements hook_eck_entity_type_insert(). |
ad_get_ads | Callback for the Ajax request to get ads. |
ad_get_advertisement | Load an Ad node from the cache. |
ad_get_cache | Return the eventually cached results of a callback |
ad_get_cached_view | Return a view, eventually from the cache. |
ad_get_destination | Return the destination URL of an ad. |
ad_get_info | Return the info about the node type(s) that implement ads. |
ad_get_rendered_node | Return a node rendered in a certain view mode, possibly from the cache. |
ad_get_view_view_mode | Get the node view mode used in the ad view. |
ad_increase_denormalized_counter | Increase one of the denormalized counters of an ad by one. |
ad_is_node_ad | Return whether a node is used for ads. |
ad_is_view_ad_enabled | Check whether a view is specifically set for ads. |
ad_load_tracked_event_by_unique_id | Load a tracked event by its unique id. |
ad_menu | Implements hook_menu(). |
ad_menu_local_tasks_alter | Implements hook_menu_local_tasks_alter(). |
ad_module_implements_alter | Implements hook_module_implements_alter(). |
ad_node_load | Implements hook_node_load(). |
ad_object_to_array | Turn an object into an array, eventually also turning any embedded object. |
ad_page_alter | Implements hook_page_alter(). |
ad_preprocess_node | Implements hook_preprocess_node(). |
ad_schema_alter | Implements hook_schema_alter(). |
ad_track_event | Track an event, like a click or an impression. |
ad_uniqid | Return a unique string which can be used to identify an event. |
_ad_eck_validate_url | Validate the link, eventually setting a form error. |
_ad_flatten | Flatten a structure into an array suitable to be the base of a cache id. |
_ad_recursive_find | Recursively search an array for a key, and return its value. |
Constants
Name | Description |
---|---|
AD_IMPRESSION_ID_PLACEHOLDER | @file Core code for the ad module. |