Index: ad.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ad/ad.module,v
retrieving revision 1.2
diff -u -r1.2 ad.module
--- ad.module	26 Dec 2006 08:10:19 -0000	1.2
+++ ad.module	9 Mar 2008 19:57:01 -0000
@@ -1,120 +1,239 @@
 <?php
-// $Id: ad.module,v 1.2 2006/12/26 08:10:19 jeremy Exp $
+// $Id: ad.module,v 1.2.2.29.2.62 2008/01/17 20:04:14 jeremy Exp $
 
 /**
  * @file
  * An advertising system for Drupal powered websites.
  *
- * Ad Module 0.3.1
- * Copyright (c) 2005-2006.
+ * Copyright (c) 2005-2007.
  *   Jeremy Andrews <jeremy@kerneltrap.org>.  All rights reserved.
  */
 
 /**
- * Use this function to display an ad from a specified group.
+ * Use this function to display ads from a specified group.
+ *
+ * @param $group
+ *  The ad group tid to display ads from.
+ * @param $quantity
+ *  Optionally specify the number of unique ads to display.
+ * @param $options
+ *  Any number of options from this list:  hostid, nids.
  */
-function ad($group = 'default') {
+function ad($group = FALSE, $quantity = 1, $options = array()) {
   global $base_url;
 
-  /**
-   * This function will be called to display advertisements.  It will start
-   * simple but will rapidly evolve to handle the following list of TODO's.
-   *
-   *  TODO: support taxonomy, ie only display ad in specified taxonomies
-   *  TODO: support roles, display certain ads only to certain roles
-   *  TODO: support multiple display methods (local and remote)
-   *  TODO: track who is displaying the ad (local versus remote)
-   */
-
   $adserve = variable_get('adserve', '');
-  if (file_exists($adserve)) {
-    if (user_access('show advertisements')) {
-      // Count how many dirs above the adserve.php dir to the index.php dir.
-      $up = (sizeof(explode('/', $adserve)) - 1);
+  $adserveinc = variable_get('adserveinc', '');
+  if (empty($adserve) || empty($adserveinc)) {
+    // This is probably the first time ad() has been called.
+    _ad_check_install();
+    $adserve = variable_get('adserve', '');
+    $adserveinc = variable_get('adserveinc', '');
+  }
+  if (!file_exists($adserve) || !file_exists($adserveinc)) {
+    drupal_set_message(t('Ads cannot be displayed.  The ad module is <a href="@misconfigured">misconfigured</a>, failed to locate the required <em>serve.php</em> ond/or <em>adserve.inc</em> file.', array('@misconfigured' => url('admin/content/ad/configure'))), 'error');
+    _ad_check_install();
+    return (t('The ad module is <a href="@misconfigured">misconfigured</a>.', array('@misconfigured' => url('admin/content/ad/configure'))));
+  }
+
+  // Be sure a display method has been chosen.
+  if (!isset($options['ad_display'])) {
+    $options['ad_display'] = variable_get('ad_display', 'javascript');
+  }
+  $options['quantity'] = isset($quantity) ? $quantity : 1;
+  if (!isset($options['tids'])) { 
+    $options['tids'] = $group;
+  }
+  $options['cache'] = variable_get('ad_cache', 'none');
+
+  switch ($options['ad_display']) {
+    case 'raw':
+      require_once(drupal_get_path('module', 'ad') .'/adserve.inc');
+      $output = adserve_ad($options);
+      break;
+    case 'iframe':
+      $display_variables = "m=iframe";
+      // Fall through...
+    case 'javascript':
+    default:
+      if ($display_variables) {
+        $display_variables .= "&amp;q=$quantity";
+      }
+      else {
+        $display_variables = "q=$quantity";
+      }
+      if ($hostid = $options['hostid']) {
+        $display_variables .= "&amp;k=$hostid";
+      }
+      if ($options['cache'] != 'none') {
+        // Allow external cache files to define additional display variables.
+        $display_variables .= '&amp;c='. $options['cache'] . module_invoke('ad_cache_'. $options['cache'], 'adcacheapi', 'display_variables', array());
+      }
+      if ($nids = $options['nids']) {
+        // Choose ads from the provided list of node Id's.
+        $display_variables .= "&amp;n=$nids";
+        $group = "nids-$nids";
+      }
+      else if ($tids = $options['tids']) {
+        // Choose ads from the provided list of taxonomy terms.
+        $display_variables .= "&amp;t=$tids";
+        $group = "tids-$tids";
+      }
+      else {
+        // Choose ads from the specified group.
+        $display_variables .= "&amp;t=$group";
+        $options['tids'] = $group;
+      }
+      $src = url("$base_url/$adserve?$display_variables");
+      if ($options['ad_display'] == 'iframe') {
+        // TODO: We need to know the IFrame size before it is displayed.  This
+        // limits the flexibility of what can be displayed in these frames.
+        // For now we'll have a global value, later we'll add per-group
+        // over-rides.
+        $append = 'frameborder="'. variable_get('ad_iframe_frameborder', 0) .'" ';
+        $append .= 'scrolling="'. variable_get('ad_iframe_scroll', 'auto') .'" ';
+        $append .= "name=\"$group\" ";
+        if ($height = variable_get('ad_iframe_height', '')) {
+          $append .= "height=\"$height\" ";
+        }
+        if ($width = variable_get('ad_iframe_width', '')) {
+          $append .= "width=\"$width\" ";
+        }
+        $output = "<iframe src=\"$src\" $append></iframe>";
+      }
+      else {
+        $output = "<script type=\"text/javascript\" src=\"$src\"></script>";
+      }
+      break;
+  }
 
-      $cache = variable_get('ad_cache', 'database');
-      $src = url("$base_url/$adserve?group=$group&up=$up&cache=$cache");
-    }
+  if (user_access('show advertisements')) {
+    return theme('ad_display', $group, $output);
   }
   else {
-    drupal_set_message(t('Ads cannot be displayed.  The ad module is %misconfigured, failed to locate the required adserve.php file "<em>%adserve</em>".', array('%misconfigured' => l(t('misconfigured'), 'admin/ad/configure'), '%adserve' => $adserve)), 'error');
+    return theme('ad_display', 'none', "<!-- Enable 'show advertisements' permission if you wish to display ads here. -->");
   }
-
-  /**
-   * TODO:  Support other display methods than our own JavaScript.  For example,
-   * we may want to serve ads using externally provided JavaScripts, or straight
-   * urls, etc...
-   */
-  $output = "<script type=\"text/javascript\" src=\"$src\"></script>\n";
-
-  return theme('ad_display', $group, $output);
 }
-
+ /*new added hook_theme() d6*/
+ function ad_theme() {
+  return array(
+    'ad_display' => array(
+      'arguments' => array('content'),
+    ),
+  );
+}
+/*new added hook_theme() d6*/
 /**
  * Function to display the actual advertisement to the screen.  Wrap it in a 
  * theme function to make it possible to customize in your own theme.
  */
+
 function theme_ad_display($group, $display) {
-  $output = "<div class=\"advertisement\" id=\"group-$group\">\n";
-  $output .= $display;
-  $output .= "</div>";
-  return $output;
+  // The naming convention for the id attribute doesn't allow commas.
+  $group = preg_replace('/[,]/', '', $group);
+  return "\n<div class=\"advertisement\" id=\"group-$group\">$display</div>\n";
 }
 
 /**
  * Update click counter then redirect host to ad's target URL.
  */
-function ad_redirect($aid, $optional = NULL) {
+function ad_redirect($aid, $hostid = NULL) {
   global $user;
-  ad_statistics_increment($aid, 'click');
-  db_query("INSERT INTO {ad_clicks} (aid, uid, hostname, url, timestamp) VALUES (%d, %d, '%s', '%s', %d)", $aid, $user->uid, $_SERVER['REMOTE_ADDR'], referer_uri(), time());
+  if (function_exists('click_filter_status')) {
+    $status = click_filter_status($aid, $hostid);
+    if ($status == CLICK_VALID) {
+      ad_statistics_increment($aid, 'click', $hostid);
+    }
+  }
+  else {
+    // We're not filtering clicks, so all clicks are valid.
+    ad_statistics_increment($aid, 'click', $hostid);
+    $status = 0;
+  }
+  db_query("INSERT INTO {ad_clicks} (aid, uid, status, hostname, user_agent, hostid, url, timestamp) VALUES (%d, %d, %d, '%s', '%s', '%s', '%s', %d)", $aid, $user->uid, $status, ip_address(), $_SERVER['HTTP_USER_AGENT'], $hostid, referer_uri(), time());
 
   // Determine where we're supposed to redirect the user.
   $adtype = db_result(db_query('SELECT adtype FROM {ads} WHERE aid = %d', $aid));
-  $node->aid = $aid;
-  $node->optional = $optional;
+
+  $node->nid = $node->aid = $aid;
+  $node->hostid = $hostid;
   $url = module_invoke('ad_'. $adtype, 'adapi', 'redirect', $node);
   if (isset($url)) {
+    watchdog('ad', 'Clicked %type ad aid %aid hostid %hostid.', array('%type' => $adtype, '%aid' => $aid, '%hostid' => $hostid));
     header('Location: '. $url);
   }
   else {
+    watchdog('ad', 'Ad redirection failed for aid %aid hostid %hostid, failed to load destination URL. ', array('%aid' => $aid, '%hostid' => $hostid));
     drupal_goto('');
   }
 }
 
 /**
+ * Ad API Helper Function:
+ * Append all necessary attributes to <a> tags.
+ */
+function ad_link_attributes() {
+  $output = ad_link_target();
+  $output .= ad_link_nofollow();
+  return $output;
+}
+
+/**
+ * Ad API Helper Function:
+ * Append target="..." as globally configured.
+ */
+function ad_link_target() {
+  return ' target="'. variable_get('ad_link_target', '_self') .'"';
+}
+
+/**
+ * Ad API Helper Function:
+ * Append rel="nofollow" if globally enabled.
+ */
+function ad_link_nofollow() {
+  if (variable_get('ad_link_nofollow', 0)) {
+    return ' rel="nofollow"';
+  }
+  return; 
+}
+
+/**
  * Increment action counter.
  */
-function ad_statistics_increment($aid, $action) {
+function ad_statistics_increment($aid, $action, $hostid = NULL) {
   // Update action statistics.
-  db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE aid = %d AND action = '%s' AND date = %d", $aid, $action, date('YmdH'));
+  db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE date = %d AND aid = %d AND action = '%s' AND hostid = '%s'", date('YmdH'), $aid, $action, $hostid);
   // If column doesn't already exist, we need to add it.
   if (!db_affected_rows()) {
-    db_query("INSERT INTO {ad_statistics} (aid, date, action, count) VALUES(%d, %d, '%s', 1)", $aid, date('YmdH'), $action);
+    db_query("INSERT INTO {ad_statistics} (aid, hostid, date, action, count) VALUES(%d, '%s', %d, '%s', 1)", $aid, $hostid, date('YmdH'), $action);
     // If another process already added this row our INSERT will fail, if so we
     // still need to increment it so we don't loose an action.
     if (!db_affected_rows()) {
-      db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE aid = %d AND action = '%s' AND date = %d", $aid, $action, date('YmdH'));
+      db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE date = %d AND aid = %d AND action = '%s' AND hostid = '%s'", date('YmdH'), $aid, $action, $hostid);
     }
   }
+
+  $event = array('aid' => $aid, 'action' => $action, 'hostid' => $hostid);
+  module_invoke_all('adapi', 'statistics_increment', $event);
 }
 
 function ad_status_array($admin = TRUE) {
   if ($admin) {
     // status options for administrators
     return array(
-      t('pending') => t('This advertisement is currently waiting for adminsitrative approval.'), 
-      t('active') => t('This advertisement is actively being displayed.'), 
-      t('offline') => t('This advertisement has been temporarily disabled by its owner and is not currently being displayed.'), 
-      t('unpublished') => t('This advertisement has been unpublished and is not currently being displayed.'), 
-      t('expired') => t('This advertisement has expired or was otherwise disabled by an administrator.'), 
-      t('denied') => t('This advertisement was refused by the site administrator, it will not be displayed.'));
+      'pending' => t('This advertisement is currently waiting for administrative approval.'), 
+      'approved' => t('This advertisement has been approved and is currently waiting to be administratively activated.'), 
+      'active' => t('This advertisement is actively being displayed.'), 
+      'offline' => t('This advertisement has been temporarily disabled by its owner and is not currently being displayed.'), 
+      'unpublished' => t('This advertisement has been unpublished and is not currently being displayed.'), 
+      'expired' => t('This advertisement has expired.'), 
+      'denied' => t('This advertisement was refused by the site administrator, it will not be displayed.'));
   }
   else {
     // status options for advertisement owners
     return array(
-      t('active') => t('This advertisement is actively being displayed.'), 
-      t('offline') => t('This advertisement has been temporarily disabled and is not currently being displayed.'));
+      'active' => t('This advertisement is actively being displayed.'), 
+      'offline' => t('This advertisement has been temporarily disabled and is not currently being displayed.'));
   }
 }
 
@@ -152,7 +271,7 @@
     $last_month = date('Y') - 1 .'120000';
   }
   else {
-    $last_month = date('Y'). $last_month .'0000';
+    $last_month = date('Y') . ($last_month < 10 ? '0' : '') . $last_month .'0000';
   }
   $statistics['last_month']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d AND date <= %d", $aid, $last_month, $this_month));
   $statistics['last_month']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d AND date <= %d", $aid, $last_month, $this_month));
@@ -165,8 +284,9 @@
   }
 
   // Get statistics for this week.
-  $statistics['this_week']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date > %d AND date <= %d", $aid, date('Ymd00', time() - 60*60*24*7), date('YmdH', time())));
-  $statistics['this_week']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date > %d AND date <= %d", $aid, date('Ymd00', time() - 60*60*24*7), date('YmdH', time())));
+  $this_week_start = date('Ymd00', time() - 60*60*24*6);
+  $statistics['this_week']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date > %d", $aid, $this_week_start));
+  $statistics['this_week']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date > %d", $aid, $this_week_start));
 
   // No sense in making further queries if the ad has no statistics this week.
   if (!$statistics['this_week']['views'] && !$statistics['this_week']['clicks']) {
@@ -174,10 +294,13 @@
   }
 
   // Get statistics for yesterday and today.
-  $statistics['yesterday']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d AND date <= %d", $aid, date('Ymd00', time() - 60*60*24), date('Ymd00', time())));
-  $statistics['yesterday']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d AND date <= %d", $aid, date('Ymd00', time() - 60*60*24), date('Ymd00', time())));
-  $statistics['today']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, date('Ymd00', time())));
-  $statistics['today']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, date('Ymd00', time())));
+  $yesterday_start = date('Ymd00', time() - 60*60*24);
+  $yesterday_end = date('Ymd24', time() - 60*60*24);
+  $today_start = date('Ymd00', time());
+  $statistics['yesterday']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d AND date <= %d", $aid, $yesterday_start, $yesterday_end));
+  $statistics['yesterday']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d AND date <= %d", $aid, $yesterday_start, $yesterday_end));
+  $statistics['today']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, $today_start));
+  $statistics['today']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, $today_start));
 
   // No sense in making further queries if the ad has no statistics today.
   if (!$statistics['today']['views'] && !$statistics['today']['clicks']) {
@@ -185,16 +308,58 @@
   }
 
   // Get statistics for this hour and the last hour.
-  $statistics['this_hour']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date = %d", $aid, date('YmdH', time())));
-  $statistics['this_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date = %d", $aid, date('YmdH', time())));
-  $statistics['last_hour']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date = %d", $aid, date('YmdH', time() - (60*60))));
-  $statistics['last_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date = %d", $aid, date('YmdH', time() - (60*60))));
+  $last_hour = date('YmdH', time() - 60*60);
+  $this_hour = date('YmdH', time());
+  $statistics['last_hour']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date = %d", $aid, $last_hour));
+  $statistics['last_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date = %d", $aid, $last_hour));
+  $statistics['this_hour']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date = %d", $aid, $this_hour));
+  $statistics['this_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date = %d", $aid, $this_hour));
 
   return $statistics;
 }
 
-function theme_ad_statistics_display($aid) {
-  $statistics = ad_statistics($aid);
+/**
+ * Display the status of the currently viewed ad.
+ */
+function theme_ad_status_display($node) {
+  $status_array = ad_status_array();
+  $output  = '<div class="adstatus">';
+  $output .= '<p>'. t($status_array["$node->adstatus"]) .'</p>';
+  switch ($node->adstatus) {
+    case 'approved':
+      if ($node->autoactivate) {
+        $output .= '<p>'. t('This advertisement will be automatically activated on %timestamp, in %time.', array('%timestamp' => format_date($node->autoactivate, 'large'), '%time' => format_interval($node->autoactivate - time()))) .'</p>';
+      }
+      break;
+    case 'active':
+      $activated = db_result(db_query("SELECT activated FROM {ads} WHERE aid = %d", $node->nid));
+      if ($activated) {
+        $output .= '<p>'. t('This advertisement has been active since %date.', array('%date' => format_date($activated, 'large'))) .'</p>';
+      }
+      if ($node->autoexpire) {
+        $output .= '<p>'. t('This advertisement will expire on %timestamp, in %time.', array('%timestamp' => format_date($node->autoexpire, 'large'), '%time' => format_interval($node->autoexpire - time()))) .'</p>';
+      }
+      if ($node->maxviews) {
+        $views = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $node->nid, date('YmdH', $node->activated)));
+        $output .= '<p>'. t('This advertisement will expire after %left more views.', array('%left' => $node->maxviews - $views)) .'</p>';
+      }
+      if ($node->maxclicks) {
+        $clicks = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $node->nid, date('YmdH', $node->activated)));
+        $output .= '<p>'. t('This advertisement will expire after %left more clicks.', array('%left' => $node->maxclicks - $clicks)) .'</p>';
+      }
+      break;
+    case 'expired':
+      $expired = db_result(db_query("SELECT expired FROM {ads} WHERE aid = %d", $node->nid));
+      if ($expired) {
+        $output .= '<p>'. t('This advertisement expired %date.', array('%date' => format_date($expired, 'large'))) .'</p>';
+      }
+      break;
+  }
+  $output .= '</div>';
+  return theme('box', t('Status'), $output);
+}
+
+function theme_ad_statistics_display($statistics) {
   $headers = array('', t('Views'), t('Clicks'), t('Click-thru'));
   $rows = array();
 
@@ -210,35 +375,100 @@
       );
     }
   }
-  return theme('box', '', theme('table', $headers, $rows));
+  return theme('box', t('Statistics'), theme('table', $headers, $rows));
 }
 
 /****************
  * Drupal hooks *
  ****************/
 
+function ad_init() {
+  if (function_exists('drupal_set_content')) {
+    if (module_exists('views')) {
+      include drupal_get_path('module', 'ad') .'/ad_views.inc';
+    }
+  }
+}
+
 /**
- * Drupal _help hook.  Provides help and information text about the ad module.
- *
- * @path   Current display path.
- * @return Text appropriate for the current $path.
+ * Implementation of hook_help().
  */
-function ad_help($path) {
+function ad_help($path, $arg=NULL) {
   switch ($path) {
-    case 'admin/modules#description':
-      $output = t('An advertising system for Drupal powered websites.');
-      break;
     case 'admin/help#ad':
       $output = '<p>'. t('The ad module provides a complete advertising system for Drupal powered websites.  It does this through an API that allow other modules to handle various types of advertising content.  For example, if enabled together with the ad_image module you will be able to display image based advertisements such as banner ads.') .'</p>';
       break;
-    case 'node/add#ad':
-      $output = t('Advertisements can be randomly displayed to visitors of your website.  Statistics for how often each advertisement is viewed and clicked are collected.');
-      break;
   }
   return $output;
 }
 
 /**
+ * Drupal _cron hook.
+ */
+function ad_cron() {
+  if (time() - variable_get('ad_cron_timestamp', 0) >= 60) {
+    // Locate ads that need to be activated or expired.
+    $result = db_query('SELECT aid, adstatus, adtype, autoactivate, autoactivated, autoexpire, autoexpired FROM {ads} WHERE autoactivate <> 0 OR autoexpire <> 0');
+    while ($ad = db_fetch_object($result)) {
+      switch ($ad->adstatus) {
+        case 'approved': {
+          // See if this ad is ready to be activated.
+          if ($ad->autoactivate && $ad->autoactivate <= time()) {
+            $node = node_load($ad->aid);
+
+            // Activate the ad.
+            db_query("UPDATE {ads} SET adstatus = 'active', autoactivate = 0, autoactivated = %d, activated = %d WHERE aid = %d", time(), time(), $ad->aid);
+            ad_statistics_increment($ad->aid, 'autoactivated');
+            ad_statistics_increment($ad->aid, 'active');
+
+            watchdog('ad', 'Automatically activated ad %title with nid %nid.', array('%title' => $node->title, '%nid' => $node->nid));
+
+            // Allow modules to do special processing to automatically
+            // activated advertisements.
+            module_invoke('ad_'. $ad->adtype, 'adapi', 'autoactivate', $node);
+          }
+          else if (!$ad->autoactivate) {
+            // Once daily warn that there's an ad stuck in approved state.
+            if (time() - variable_get("ad_autoactivate_warning_$ad->aid", 0) >= 8600) {
+              watchdog('ad', 'Warning: ad %title with nid %nid in approved state has no autoactivate date set.', array('%title' => $node->title, '%nid' => $node->nid));
+              variable_set("ad_autoactivate_warning_$ad->aid", time());
+            }
+          }
+          break;
+        }
+        case 'active': {
+          // See if this ad is ready to be activated.
+          if ($ad->autoexpire && $ad->autoexpire <= time()) {
+            $node = node_load($ad->aid);
+
+            // Expire the ad.
+            db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $ad->aid);
+            ad_statistics_increment($ad->aid, 'autoexpired');
+            ad_statistics_increment($ad->aid, 'expired');
+
+            watchdog('ad', 'Automatically expired ad %title with nid %nid.', array('%title' => $node->title, '%nid' => $node->nid));
+
+            // Allow modules to do special processing to automatically
+            // activated advertisements.
+            module_invoke('ad_'. $ad->adtype, 'adapi', 'autoexpire', $node);
+          }
+          else if (!$ad->autoexpire) {
+            // Ad is already activated, but has autoactivate timestamp set.
+            db_query("UPDATE {ads} SET autoactivate = 0 WHERE aid = %d", $ad->aid);
+          }
+          break;
+        }
+        default:
+          $node = node_load($ad->aid);
+          db_query('UPDATE {ads} SET autoactivate = 0, autoexpire = 0 WHERE aid = %d', $ad->aid);
+          watchdog('ad', 'Warning: reset %type timestamp on advertisement %title with nid %nid because it is in %state state.', array('%title' => $node->title, '%nid' => $node->nid, '%type' => $ad->autoactivate ? 'autoactivate' : 'autoexpire', '%state' => $ad->adstatus));
+      }
+    }
+    variable_set('ad_cron_timestamp', time());
+  }
+}
+
+/**
  * Drupal _perm hook.  Establishes permissions used by this module.
  *
  * @return  An array of permissions used by this module.
@@ -247,7 +477,7 @@
   return ( 
           array('administer advertisements',
                 'create advertisements',
-                'edit own advertisements',
+                'manage advertisements',
                 'show advertisements')
          );
 }
@@ -255,20 +485,25 @@
 /**
  */
 function ad_node_info() {
-  return array('ad' => array('name' => t('advertisement'), 'base' => 'ad'));
+  return array('ad' => array(
+    'name' => t('Advertisement'),
+    'module' => 'ad',
+    'description' => t('Advertisements can be randomly displayed to visitors of your website.'),
+    'help' => t('Advertisements can be randomly displayed to visitors of your website.'),
+  ));
 }
 
 /**
  */
-function ad_access($op, $node) {
-  global $user;
-
+function ad_access($op, $node, $account) {
   if ($op == 'create') {
-    return user_access('create advertisements');
+    return user_access('create advertisements', $account);
   }
 
   if ($op == 'update' || $op == 'delete') {
-    return (user_access('administer advertisements') || ($node->uid == $user->uid && user_access('edit own advertisements')));
+    if (user_access('administer advertisements', $account) || (ad_is_owner($node->nid) && user_access('manage advertisements', $account))) {
+      return TRUE;
+    }
   }
 }
 
@@ -280,7 +515,7 @@
   $edit = $_POST['edit'];
 
   $type = arg(3);
-  if (function_exists("ad_$type". '_type')) {
+  if (function_exists("ad_$type".'_type')) {
     $adtype = $type;
   }
   else {
@@ -298,17 +533,17 @@
     '#required' => TRUE, 
     '#default_value' => $node->title,
   );
-  $form['body'] = array(
+  $form['body_filter']['body'] = array(
     '#type' => 'textarea', 
     '#title' => t('Description'), 
-    '#required' => TRUE, 
     '#default_value' => $node->body, 
     '#rows' => 3
   );
+  $form['body_filter']['format'] = filter_form($node->format);
 
   // determine the current ad type
   if (!isset($adtype)) {
-    $adtypes = module_invoke_all('adapi', 'type');
+    $adtypes = module_invoke_all('adapi', 'type', array());
     switch (sizeof($adtypes)) {
       case 0:
         drupal_set_message(t('At least one ad type module must be enabled before you can create advertisements.  For example, try %enabling the ad_text or ad_image module.', array('%enabling' => l('enabling', 'admin/modules'))), 'error');
@@ -317,15 +552,17 @@
         $adtype = $adtypes[0];
         break;
       default:
-        $adtype = arg(3) ? arg(3) : $edit['adtype'];
-        $form['adtype'] = array(
-          '#type' => 'radios', 
-          '#title' => t('Style of ad'), 
-          '#options' => drupal_map_assoc($adtypes), 
-          '#default_value' => $adtype ? $adtype : $adtypes[0], 
-          '#required' => TRUE, 
-          '#description' => t('Select the type of ad that you wish to create from the above options.')
-        );
+        if (arg(0) == 'node' && arg(1) == 'add' && arg(2) == 'ad') {
+          $adtype = arg(3) ? arg(3) : $edit['adtype'];
+          $form['adtype'] = array(
+            '#type' => 'radios', 
+            '#title' => t('Style of ad'), 
+            '#options' => drupal_map_assoc($adtypes), 
+            '#default_value' => $adtype ? $adtype : $adtypes[0], 
+            '#required' => TRUE, 
+            '#description' => t('Select the type of ad that you wish to create from the above options.')
+          );
+        }
         break;
     }
   }
@@ -342,19 +579,6 @@
     );
   }
 
-  // display group selection menu
-  $form['group'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Group'),
-    '#collapsible' => TRUE,
-  );
-  $form['group']['gid'] = array(
-    '#type' => 'radios',
-    '#default_value' => $node->gid ? $node->gid : 1,
-    '#options' => ad_groups_list(),
-    '#description' => t('Assign your advertisement to a group.  When you wish to display advertisements, you will choose a group of advertisements to display in one position on your screen.'),
-  );
-
   if (user_access('administer advertisements')) {
     // admins can set any status on advertisements
     $form['adstatus'] = array(
@@ -373,10 +597,27 @@
       );
     }
   }
-  else {
-    // display status options
-    $adstatus = ad_status_array(FALSE);
-    if (isset($node->adstatus) && isset($adstatus["$node->adstatus"])) {
+  else if (ad_adaccess($node->nid, 'manage status')) {
+    if (!$node->adstatus || $node->adstatus == 'pending') {
+      $adstatus = ad_status_array();
+      $node->adstatus = t('pending');
+      $form['adstatus'] = array(
+        '#type' => 'fieldset', 
+        '#title' => t('Status'), 
+        '#collapsible' => TRUE
+      );
+      $form['adstatus']['display'] = array(
+        '#type' => 'markup', 
+        '#value' => '<p><b>'. t('Status') .':</b> '. t($node->adstatus) .'<br />'. t($adstatus["$node->adstatus"]),
+      );
+      $form['adstatus']['adpending'] = array(
+        '#type' => 'value', 
+        '#value' => $node->adstatus
+      );
+    }
+    else {
+      $adstatus = ad_status_array(FALSE);
+      // display status options
       $form['adstatus'] = array(
         '#type' => 'fieldset', 
         '#title' => t('Status'), 
@@ -387,62 +628,78 @@
           '#type' => 'radio', 
           '#title' => t("$status"), 
           '#return_value' => $status, 
-          '#default_value' => $node->adstatus, 
-          '#description' => "$description", '#parents' => array("adstatus")
+          '#default_value' => $node->adstatus ? $node->adstatus : t('pending'), 
+          '#description' => "$description", 
+          '#parents' => array("adstatus")
         );
       }
     }
-    else {
-      $adstatus = ad_status_array();
-      if (!isset($node->adstatus)) {
-        $node->adstatus = t('pending');
-      }
-      $form['adstatus_display'] = array(
-        '#type' => 'markup', 
-        '#value' => '<p><b>'. t('Status') .':</b> '. $node->adstatus .'<br />'. $adstatus["$node->adstatus"]
-      );
-      $form['adstatus'] = array(
-        '#type' => 'value', 
-        '#value' => $node->adstatus
-      );
+  }
+  else {
+    $adstatus = ad_status_array();
+    if (!($node->adstatus)) {
+      $node->adstatus = t('pending');
     }
+    $form['ad_adstatus'] = array(
+      '#type' => 'fieldset', 
+      '#title' => t('Status'), 
+      '#collapsible' => TRUE
+    );
+    $form['ad_adstatus']['adstatus_display'] = array(
+      '#type' => 'markup', 
+      '#value' => '<p><b>'. t('Status') .':</b> '. t($node->adstatus) .'<br />'. t($adstatus["$node->adstatus"]),
+    );
+    $form['adstatus'] = array(
+      '#type' => 'value', 
+      '#value' => $node->adstatus
+    );
   }
 
-  // display statistics
-  $form['statistics'] = array(
-    '#type' => 'fieldset', 
-    '#title' => t('Statistics'), 
-    '#collapsible' => TRUE,
-  );
-
-  $form['statistics']['data'] = array(
-    '#type' => 'markup',
-    '#prefix' => '<div>',
-    '#suffix' => '</div>',
-    '#value' => theme_ad_statistics_display($node->nid),
-  );
-
+  if (ad_adaccess($node->nid, 'access statistics')) {
+    // display statistics
+    $form['statistics'] = array(
+      '#type' => 'fieldset', 
+      '#title' => t('Statistics'), 
+      '#collapsible' => TRUE,
+    );
+  
+    $form['statistics']['data'] = array(
+      '#type' => 'markup',
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => theme('ad_statistics_display', ad_statistics($node->nid)),
+    );
+  }
+  
   // display scheduling options
   $form['schedule'] = array(
     '#type' => 'fieldset', 
     '#title' => t('Scheduling'), 
     '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-  );
-  $form['schedule']['current'] = array( 
-    '#type' => 'markup', 
-    '#prefix' => '<div>',
-    '#suffix' => '</div>',
-    '#value' => t('The current date and time is "%date".', array('%date' => format_date(time(), 'custom', 'F j, Y H:i')))
-  );
-  $form['schedule']['autoactivate'] = array( 
-    '#type' => 'textfield', 
-    '#title' => t('Automatically activate ad'), 
-    '#required' => FALSE, 
-    '#default_value' => $node->autoactivate ? format_date((int)$node->autoactivate, 'custom', 'F j, Y H:i') : '', 
-    '#description' => t('You can specify a date and time for this advertisement to be automatically activated.  The advertisement needs to be in an "offline" state before it can be automatically activated.  If you prefer to activate the advertisement immediately, leave this field empty.')
+    // Collapse if there isn't any scheduling data set.
+    '#collapsed' => ($node->autoactivate || $edit['autoactivate'] ||
+                     $node->autoexpire || $edit['autoexpire'] ||
+                     $node->maxviews || $edit['maxviews'] ||
+                     $node->maxclicks || $edit['maxclicks']) 
+                     ? FALSE : TRUE,
   );
 
+  if (ad_adaccess($node->nid, 'manage status')) {
+    $form['schedule']['current'] = array( 
+      '#type' => 'markup', 
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => t('The current date and time is "%date".', array('%date' => format_date(time(), 'custom', 'F j, Y H:i')))
+    );
+    $form['schedule']['autoactivate'] = array( 
+      '#type' => 'textfield', 
+      '#title' => t('Automatically activate ad'), 
+      '#required' => FALSE, 
+      '#default_value' => $node->autoactivate ? format_date((int)$node->autoactivate, 'custom', 'F j, Y H:i') : '', 
+      '#description' => t('You can specify a date and time for this advertisement to be automatically activated.  The advertisement needs to be in an <em>approved</em> state before it can be automatically activated.  If you prefer to activate the advertisement immediately, leave this field empty.')
+    );
+  }
+
   if (user_access('administer advertisements')) {
     // admins can expire advertisements
     $form['schedule']['autoexpire'] = array(
@@ -451,23 +708,81 @@
       '#required' => FALSE, 
       '#default_value' => $node->autoexpire ? format_date((int)$node->autoexpire, 'custom', 'F j, Y H:i') : '', 
       '#description' => t('You can specify a date and time for this advertisement to be automatically expired.  If you don\'t want the advertisement to expire, leave this field empty.')
-     );
+    );
+    $form['schedule']['maxviews'] = array(
+      '#type' => 'textfield', 
+      '#title' => t('Maximum views'), 
+      '#required' => FALSE, 
+      '#size' => 10,
+      '#maxlength' => 11,
+      '#default_value' => $node->maxviews,
+      '#description' => t('You can specify the maximum number of times this advertisement should be displayed, after which it will be automatically expired.  If you don\'t want this advertisement to expire after a certain number of views, leave this field set to %zero.', array('%zero' => '0')),
+    );
+    $form['schedule']['maxclicks'] = array(
+      '#type' => 'textfield', 
+      '#title' => t('Maximum clicks'), 
+      '#required' => FALSE, 
+      '#size' => 10,
+      '#maxlength' => 11,
+      '#default_value' => $node->maxclicks,
+      '#description' => t('You can specify the maximum number of times this advertisement should be clicked, after which it will be automatically expired.  If you don\'t want this advertisement to expire after a certain number of clicks leave this field set to %zero.', array('%zero' => '0')),
+    );
   }
   else {
     // display expiration time
-    if (isset($node->autoexpire)) {
-      $form['schedule']['autoexpire_display'] = array(
-        '#type' => 'markup', 
-        '#value' => t('This ad will automatically expire in %date.', array('%date' => format_interval($node->autoexpire - time())))
+    $form['schedule']['autoexpire_display'] = array(
+      '#type' => 'markup', 
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => theme('ad_status_display', $node),
+    );
+    $form['schedule']['autoexpire'] = array(
+      '#type' => 'hidden', 
+      '#value' => $node->autoexpire
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Drupal _form_alter() hook.
+ */
+function ad_form_alter(&$form, $form_state, $form_id) {
+  if ($form_id == 'taxonomy_form_vocabulary') {
+    // Remove taxonomy form options not applicable for ad groups.
+    if ($form['vid']['#value'] == _ad_get_vid()) {
+      $form['help_ad_vocab'] = array(
+        '#value' => t('This vocabulary was automatically created for use by the ad module.  Only applicable options are available.'),
+        '#weight' => -1
       );
-      $form['schedule']['autoexpire'] = array(
-        '#type' => 'hidden', 
-        '#value' => $node->autoexpire
+      $form['nodes']['ad'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('ad group'),
+        '#value' => 1,
+        '#attributes' => array('disabled' => ''),
+        '#description' => t('Type %type is required to use this vocabulary.', array('%type' => t('ad group')))
       );
+      $form['tags']['#description'] = t('If enabled, ads are categorized by typing ad group names instead of choosing them from a list.');
+      $form['multiple']['#description'] = t('If enabled, allows ads to have more than one ad group (always true for free tagging).');
+      $form['required']['#description'] = t('If enabled, every ad <strong>must</strong> be assigned to at least one ad group.');
+      $form['hierarchy'] = array('#type' => 'value', '#value' => 0);
+      unset($form['relations']);
+    }
+    else {
+      unset($form['nodes']['ad']);
+    }
+  }
+  else if ($form_id == 'taxonomy_form_term') {
+    if ($form['vid']['#value'] == _ad_get_vid()) {
+      $form['name']['#title'] = t('Ad group name');
+      $form['name']['#description'] = t('The name for this ad group.  Example: "Linux".');
+      $form['description']['#description'] = t('A description of the ad group.');
+      $form['description']['#required'] = TRUE;
+      $form['weight']['#description'] = t('In listings, the heavier ad groups will sink and the lighter ad groups will be positioned nearer the top.');
+      unset($form['synonyms']);
     }
   }
-
-  return $form;
 }
 
 /**
@@ -493,9 +808,10 @@
       if ($node->adtype) {
         if ($node->status != 1 && $node->adstatus == 'active') {
           $node->adstatus = t('unpublished');
-          ad_statistics_increment($node->nid, 'unpublish');
         }
-        db_query("INSERT INTO {ads} (aid, uid, gid, adstatus, adtype, redirect, autoactivate, autoexpire) VALUES(%d, %d, %d, '%s', '%s', '%s', %d, %d)", $node->nid, $node->uid, $node->gid, $node->adstatus, $node->adtype, url("ad/redirect/$node->nid"), strtotime($node->autoactivate), strtotime($node->autoexpire));
+        $activated = $node->adstatus == 'active' ? time() : 0;
+        db_query("INSERT INTO {ads} (aid, uid, adstatus, adtype, redirect, autoactivate, autoexpire, activated, maxviews, maxclicks) VALUES (%d, %d, '%s', '%s', '%s', %d, %d, %d, %d, %d)", array($node->nid, $node->uid, $node->adstatus, $node->adtype, url("ad/redirect/$node->nid", array('absolute' => TRUE)), $node->autoactivate ? strtotime($node->autoactivate) : '', $node->autoexpire ? strtotime($node->autoexpire) : '', $activated, (int)$node->maxviews, (int)$node->maxclicks));
+        ad_owners_add($node->nid, $node->uid);
         ad_host_id_create($node->uid);
         ad_statistics_increment($node->nid, 'create');
       }
@@ -503,15 +819,52 @@
 
     case 'update':
       if ($node->adtype) {
+        $ad = db_fetch_object(db_query('SELECT * FROM {ads} WHERE aid = %d', $node->nid));
+        // Ad must be in approved state to be able to autoactivate it.
+        if ($node->adstatus != 'approved' && $node->autoactivate) {
+          if ($node->adstatus == 'active') {
+            // This ad is already active, no need to autoactivate it.
+            $node->autoactivate = 0;
+          }
+          else {
+            drupal_set_message(t('This ad will not be automatically activated at the scheduled time because it is not in the <em>approved</em> state.'), 'error');
+          }
+        }
+        // If this node has been upublished, the ad should no longer be active.
         if ($node->status != 1 && $node->adstatus == 'active') {
           $node->adstatus = t('unpublished');
-          ad_statistics_increment($node->nid, 'unpublish');
         }
+        // If a previously unpublished node has been published, reactivate the
+        // the ad.
         else if ($node->status == 1 && $node->adstatus == 'unpublished') {
           $node->adstatus = t('active');
+          db_query("UPDATE {ads} SET enabled = %d WHERE aid = %d", time(), $node->nid);
+          // Special "publish" event, may as well track it even though we'll
+          // next also record an "active" event.
           ad_statistics_increment($node->nid, 'publish');
         }
-        db_query("UPDATE {ads} SET uid = %d, gid = %d, adstatus = '%s', adtype = '%s', autoactivate = %d, autoexpire = %d WHERE aid = %d", $node->uid, $node->gid, $node->adstatus, $node->adtype, strtotime($node->autoactivate), strtotime($node->autoexpire), $node->nid);
+        // Check if ad is being manually activated.
+        if ($ad->adstatus != 'active' && $node->adstatus == 'active') {
+          $activated = time();
+        }
+        // Check if ad is being manually expired.
+        else if ($ad->status != 'expired' && $node->adstatus == 'expired') {
+          // Ad has been manually expired.
+          $expired = time();
+        }
+        // Ad has not been manually activated or expired, preserve timestamps.
+        else {
+          $activated = $ad->activated;
+          $expired = $ad->expired;
+        }
+        // Ad status has changed, record the event.
+        if ($ad->adstatus != $node->adstatus) {
+          ad_statistics_increment($node->nid, $node->adstatus);
+        }
+        // Update ads table with new information.
+        db_query("UPDATE {ads} SET uid = %d, adstatus = '%s', adtype = '%s', autoactivate = %d, autoexpire = %d, activated = %d, maxviews = %d, maxclicks = %d, expired = %d WHERE aid = %d", $node->uid, $node->adstatus, $node->adtype, $node->autoactivate ? strtotime($node->autoactivate) : '', $node->autoexpire ? strtotime($node->autoexpire) : '', $activated, (int)$node->maxviews, (int)$node->maxclicks, $expired, $node->nid);
+        // Be sure ad owner has at least default ad permissions.
+        ad_owners_add($node->nid, $node->uid);
         ad_host_id_create($node->uid);
         ad_statistics_increment($node->nid, 'update');
       }
@@ -520,41 +873,27 @@
     case 'delete':
       db_query("DELETE FROM {ads} WHERE aid = %d", $node->nid);
       db_query("DELETE FROM {ad_statistics} WHERE aid = %d", $node->nid);
+      // Clean up ad_permissions and any other per-ad tables.
+      $result = db_query('SELECT oid, uid FROM {ad_owners} WHERE aid = %d', $node->nid);
+      while ($id = db_fetch_object($result)) {
+        db_query('DELETE FROM {ad_permissions} WHERE oid = %d', $id->oid);
+        $owner = user_load(array('uid' => $id->uid));
+        // Tell plug-in modules to clean up.
+        module_invoke_all('adowners', 'remove', $id->oid, $owner);
+      }
+      db_query('DELETE FROM {ad_owners} WHERE aid = %d', $node->nid);
+      // All that's left of the ad is a single timestamp as to when it was 
+      // deleted.
+      ad_statistics_increment($node->nid, 'delete');
       break;
 
     case 'view':
       if ($node->adtype) {
         $node = node_prepare($node, $teaser);
-        if (user_access('administer advertisements')) {
-          $node->body .= theme_ad_statistics_display($node->nid);
-
-          $header = array(
-            array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
-            array('data' => t('Username'), 'field' => 'uid'),
-            array('data' => t('IP address'), 'field' => 'hostname'),
-            array('data' => t('URL where clicked'), 'field' => 'url'),
-          );
- 
-          if ($node->nid) {
-            $sql = "SELECT timestamp,uid,hostname,url FROM {ad_clicks} WHERE aid = $node->nid";
-            $sql .= tablesort_sql($header);
-            $result = pager_query($sql, 15);
-
-            while ($ad = db_fetch_object($result)) {
-              $row = array();
-              $click_user = user_load(array('uid' => $ad->uid));
-              $row[] = format_date($ad->timestamp, 'custom', 'F j, Y H:i');
-              $row[] = $ad->uid ? l($click_user->name, "user/$ad->uid") : variable_get('anonymous', 'Anonymous');
-              $row[] = $ad->hostname;
-              $row[] = l($ad->url, $ad->url);
-              $rows[] = $row;
-            }
-
-            $output = theme('table', $header, $rows);
-            $output .= theme('pager', NULL, 50, 0);
-            $node->body .= theme('box', t('Click history'), $output);
-          }
-        }
+        $node->content['body'] = array(
+          '#value' => $teaser ? $node->teaser : theme('node_ad', $node, $page),
+          '#weight' => 1,
+        );
       }
       break;
 
@@ -563,111 +902,248 @@
       break;
 
   }
-  // insert ad type data
+  // Allow ad type module to act on nodeapi events.  The adapi hook provides 
+  // access to additional variables not available in the nodeapi hook.
   if ($node->adtype) {
-    module_invoke('ad_'. $node->adtype, 'adapi', $op, $node);
+    // Don't use module_invoke, as in pre-PHP5 the changes to $node won't be
+    // passed back.
+    $function = "ad_$node->adtype" .'_adapi';
+    if (function_exists($function)) {
+      $function($op, $node);
+    }
+  }
+  // Allow ad cache module to act on nodeapi events.
+  $cache = variable_get('ad_cache', 'none');
+  if ($cache != 'none') {
+    $function = "ad_cache_$cache" .'_adcacheapi';
+    if (function_exists($function)) {
+      $function($op, $node);
+    }
+  }
+}
+
+function theme_node_ad($node, $yield_form = TRUE) {
+  $output = '';
+  if (ad_adaccess($node->nid, 'access statistics')) {
+    $output = theme('ad_status_display', $node);
+    $output .= theme('ad_statistics_display', ad_statistics($node->nid));
+  }
+  if (ad_adaccess($node->nid, 'access click history')) {
+    $header = array(
+      array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
+      array('data' => t('Username'), 'field' => 'uid'),
+      array('data' => t('IP address'), 'field' => 'hostname'),
+      array('data' => t('URL where clicked'), 'field' => 'url'),
+    );
+    if (function_exists('click_filter_status_text')) {
+      $header[] = array('data' => t('Status'), 'field' => 'status');
+    }
+ 
+    if ($node->nid) {
+      $sql = "SELECT timestamp, uid, status, hostname, url FROM {ad_clicks} WHERE aid = $node->nid";
+      $sql .= tablesort_sql($header);
+      $result = pager_query($sql, 25);
+
+      while ($ad = db_fetch_object($result)) {
+        if (module_exists('click_filter') && $ad->status != CLICK_VALID) {
+          // Only show filtered clicks to users with permission to view them.
+          if (!user_access('view filtered clicks')) {
+            continue;
+          }
+        }
+        if (strlen($ad->url) > 40) {
+          $url = substr($ad->url, 0, 37) .'...';
+        }
+        else {
+          $url = $ad->url;
+        }
+        $row = array();
+        $click_user = user_load(array('uid' => $ad->uid));
+        $row[] = format_date($ad->timestamp, 'custom', 'F j, Y H:i');
+        $row[] = $ad->uid ? l($click_user->name, "user/$ad->uid") : variable_get('anonymous', 'Anonymous');
+        $row[] = $ad->hostname;
+        $row[] = l($url, $ad->url);
+        if (function_exists('click_filter_status_text')) {
+          $row[] = click_filter_status_text($ad->status);
+        }
+        $rows[] = $row;
+      }
+
+      $click_history = theme('table', $header, $rows);
+      $click_history .= theme('pager', NULL, 25, 0);
+      $output .= theme('box', t('Click history'), $click_history);
+    }
+  }
+  return $output;
+}
+
+function ad_adapi($op, $node = NULL) {
+  switch ($op) {
+    case 'permissions':
+      return array('access statistics', 'access click history', 'manage status', 'manage owners');
+      break;
   }
 }
 
 /**
- * Drupal _menu hook.
+ * Implementation of hook_menu().
  */
-function ad_menu($may_cache) {
+function ad_menu() {
   global $user;
-  $items = array();
-
-  if ($may_cache) {
+ 
     // menu items
-    $items[] = array('path' => 'admin/ad', 
-                     'title' => t('ads'),
-                     'callback' => 'ad_admin_list',
-                     'access' => user_access('administer advertisements'));
+    $items['admin/content/ad'] = array(
+                     'title' => 'Ads',
+                     'page callback' => 'ad_admin_list',
+   'access arguments' => array('administer advertisements'),
+                     'description' => t('Configure and manage your advertising system.'));
 
     // tabs
-    $items[] = array('path' => 'admin/ad/list', 
-                     'title' => t('list'),
-                     'callback' => 'ad_admin_list',
-                     'type' => MENU_DEFAULT_LOCAL_TASK);
-    $items[] = array('path' => 'admin/ad/configure', 
-                     'title' => t('settings'),
-                     'callback' => 'ad_admin_configure_settings', 
-                     'type' => MENU_LOCAL_TASK, 
-                     'weight' => 3);
-    $items[] = array('path' => 'admin/ad/groups', 
-                     'title' => t('groups'),
-                     'callback' => 'ad_admin_groups_configure', 
+    $items['admin/content/ad/list'] = array(
+                     'title' => 'List',
+                     'page callback' => 'ad_admin_list',
+                     'type' => MENU_DEFAULT_LOCAL_TASK
+  );
+    $items['admin/content/ad/statistics'] = array(
+                     'title' => 'Statistics',
+                     'page callback' => 'drupal_get_form',
+                     'page arguments' => array('ad_admin_statistics'),
+                     'type' => MENU_LOCAL_TASK,
+                     'weight' => 1
+  );
+    $items['admin/content/ad/configure'] = array(
+                     'title' => 'Settings',
+                     'page callback' => 'drupal_get_form',
+                     'page arguments' => array('ad_admin_configure_settings'),
+                     'type' => MENU_LOCAL_TASK,
+                     'weight' => 3
+  );
+    $items['admin/content/ad/groups'] = array(
+                     'title' => 'Groups',
+                     'page callback' => 'ad_admin_groups_list',
                      'type' => MENU_LOCAL_TASK,
-                     'weight' => 5);
+                     'weight' => 5
+  );
 
-    // configure sub tabs
-    $items[] = array('path' => 'admin/ad/configure/global', 
-                     'title' => t('global settings'),
-                     'callback' => 'ad_admin_configure_settings', 
+    // groups sub tabs
+    $items['admin/content/ad/groups/list'] = array(
+                     'title' => 'List',
+                     'page callback' => 'ad_admin_groups_list',
                      'type' => MENU_DEFAULT_LOCAL_TASK, 
-                     'weight' => 0);
-    $items[] = array('path' => 'admin/ad/groups/list', 
-                     'title' => t('list'),
-                     'callback' => 'ad_admin_groups_configure', 
-                     'type' => MENU_DEFAULT_LOCAL_TASK, 
-                     'weight' => 0);
+                     'weight' => 0
+  );
+    $items['admin/content/ad/groups/add'] = array(
+                     'title' => 'Create group',
+                     'page callback' => 'drupal_get_form',
+                     'page arguments' => array('ad_admin_group_form'),
+                     'type' => MENU_LOCAL_TASK,
+                     'weight' => 3
+  );
 
+    // configure sub tabs
+    $items['admin/content/ad/configure/global'] = array(
+                     'title' => 'Global settings',
+                     'page callback' => 'drupal_get_form',
+                     'page arguments' => array('ad_admin_configure_settings'),
+                     'type' => MENU_DEFAULT_LOCAL_TASK,
+                     'weight' => 0
+  );
+    // wont delete this until complete transferd
+  $items['node/add'] = array(
+                     'title' => 'Ad',
+                     'page callback' => 'ad_add',
+                     'access arguments' => array('create advertisements'),
+           );          
+    $adtypes = module_invoke_all('adapi', 'type', array());
+    foreach ($adtypes as $adtype) {
+      $items['node/add'] = array(
+       'title' => t('!type advertisement', array('!type' => t($adtype))),
+    'access arguments' => array('create advertisements'),
+    );
+       }
 
-    $items[] = array('path' => 'node/add/ad', 
-                     'title' => t('ad'),
-                     'callback' => 'ad_add',
-                     'access' => user_access('create advertisements'));
-  }
-  else {
+ // }
+  //else {
     // callbacks
-
     if (arg(0) == 'ad' && arg(1) == 'redirect' && is_numeric(arg(2))) {
       $aid = arg(2);
-      $optional = arg(3);
-      $items[] = array('path' => "ad/redirect/$aid",
-                       'access' => user_access('show advertisements'),
+      $hostid = arg(3);
+      $items['ad/redirect/%aid'] = array(
+  'access arguments' => array('show advertisements'),
                        'type' => MENU_CALLBACK, 
-                       'callback' => 'ad_redirect',
-                       'callback arguments' => array($aid, $optional));
-    }
-    elseif (arg(1) == 'ad' && arg(2) == 'groups' && is_numeric(arg(3))) {
-      $gid =  arg(3);
-      $items[] = array('path' => "admin/ad/groups/$gid/delete", 
-                       'title' => t('delete'),
-                       'callback' => 'ad_admin_groups_delete', 
-                       'type' => MENU_CALLBACK,
-                       'weight' => 1);
+                       'page callback' => 'ad_redirect',
+                       'page arguments' => array($aid, 2),
+   );
+   
+    }
+elseif(arg(2) == 'ad' && arg(3) == 'groups' && is_numeric(arg(4))) {
+      
+  if ($term = taxonomy_get_term(arg(4))) {
+        $items['admin/content/ad/groups/%_taxonomy_term/edit'] = array(
+                         'title' => 'edit',
+                         'page callback' => 'drupal_get_form',
+                         'page arguments' => array('ad_admin_group_form', (array)$term),
+                        
+  'access arguments' => array('administer advertisements'),
+                         'type' => MENU_CALLBACK,
+                         'weight' => 1);
+        $items['admin/content/ad/groups/$term->tid/delete'] = array( 
+                         'title' => 'delete',
+                         'page callback' => 'drupal_get_form',
+                         'page arguments' => array('ad_confirm_group_delete', (array)$term),
+                         'access callback' => 'user_access',
+  'access arguments' => array('administer advertisements'),
+                         'type' => MENU_CALLBACK,
+                         'weight' => 2);
+      }
     }
-    elseif (arg(0) == 'node' && is_numeric(arg(1)) &&
-      (user_access('administer advertisements') || user_access('edit own advertisements'))) {
+elseif (arg(0) == 'node' && is_numeric(arg(1)) && ad_adaccess(arg(1), 'manage owners')) {
       $node = node_load(array('nid' => arg(1)));
-
-      $items[] = array('path' => "node/$node->nid/ad",
-                       'access' => user_access('administer advertisements') ||
-       ($node->uid == $user->uid && user_access('edit own advertisements')),
-                       'title' => t('ad owners'),
-                       'callback' => 'ad_owners_overview',
-                       'callback arguments' => array($node),
-                       'type' => MENU_LOCAL_TASK,
-                       'weight' => 5);
-
-      if (is_numeric(arg(3))) {
-        $uid = arg(3);
-        $ad_user = user_load(array('uid' => $uid));
-        $items[] = array('path' => "node/$node->nid/ad/$uid/permissions",
-                         'title' => t('%owner\'s permissions', array('%owner' => $ad_user->name)),
-                         'callback' => 'ad_owner_permissions',
-                         'callback arguments' => array($node->nid, $uid),
+      if ($node->adtype) {
+        $items[ "node/$node->nid/adowners"] = array(
+                         'access callback' => 'ad_adaccess',
+                         'access arguments' => array($node->nid, 'manage owners'),
+  'title' => 'Ad owners',
+                         'page callback' => 'ad_owners_overview',
+                         'page arguments' => array($node),
                          'type' => MENU_LOCAL_TASK,
+                         'weight' => 5);
+        $items[ "node/$node->nid/adowners/list"] = array(
+                         'access callback' => 'ad_adaccess',
+   'access arguments' => array($node->nid, 'manage owners'),
+                         'title' => 'List',
+                         'type' => MENU_DEFAULT_LOCAL_TASK,
                          'weight' => 0);
-        $items[] = array('path' => "node/$node->nid/ad/$uid/notifications",
-                         'title' => t('%owner\'s notifications', array('%owner' => $ad_user->name)),
-                         'callback' => 'ad_owner_notifications',
-                         'callback arguments' => array($node->nid, $uid),
-                         'type' => MENU_LOCAL_TASK,
-                         'weight' => 1);
+        if (is_numeric(arg(3))) {
+          $uid = arg(3);
+          $ad_user = user_load(array('uid' => $uid));
+          $items["node/$node->nid/adowners/$uid/permissions"] = array(
+                           'title' => $ad_user->name .'!owner\'s permissions',
+                           'access callback' => 'ad_adaccess',
+    'access arguments' => array($node->nid, 'manage owners'),
+                           'page callback' => 'drupal_get_form',
+                           'page arguments' => array('ad_owner_permissions', $node->nid, $uid),
+                           'type' => MENU_LOCAL_TASK,
+                           'weight' => 2);
+          $items["node/$node->nid/adowners/$uid/remove"] = array(
+                           'page callback' => 'drupal_get_form',
+                           'page arguments' => array('ad_owner_remove', $node->nid, $uid),
+                           'type' => MENU_CALLBACK,
+                           'weight' => 6);
+        }
+ else {
+          $items['node/%node_load_node/adowners/add'] = array(
+                           'access callback' => 'ad_adaccess',
+    'access arguments' => array($node->nid, 'manage owners'),
+                           'title' => 'Add',
+                           'page callback' => 'drupal_get_form',
+                           'page arguments' => array('ad_owners_add_form', 1),
+                           'type' => MENU_LOCAL_TASK,
+                           'weight' => 4);
+        }
       }
     }
-  }
+//}
 
   return $items;
 }
@@ -679,14 +1155,26 @@
   switch ($op) {
     case 'list':
       $blocks = array();
-      $result = db_query('SELECT gid,name FROM {ad_groups}');
-      while ($ad = db_fetch_object($result)) {
-        $blocks[$ad->gid]['info'] = t('ad group: <em>%name</em>', array('%name' => $ad->name));
+      $groups = ad_groups_list();
+      foreach ($groups as $tid => $name) {
+        $blocks[$tid]['info'] = t('ad group: @name', array('@name' => $name));
       }
       return $blocks;
+    case 'configure':
+      $form["ad_block_quantity_$delta"] = array(
+        '#type' => 'select',
+        '#title' => t('Number of ads'),
+        '#default_value' => variable_get("ad_block_quantity_$delta", 1),
+        '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)),
+        '#description' => t('Select the maximum number of unique ads that should be displayed together in this block.  If you specify a number larger than the maximum number of ads in this ad group, all ads will be displayed once.'),
+      );
+      return $form;
+    case 'save':
+      variable_set("ad_block_quantity_$delta", $edit["ad_block_quantity_$delta"]);
+      break;
     case 'view':
-      $group = db_result(db_query('SELECT name FROM {ad_groups} WHERE gid = %d', $delta));
-      $block['content'] = ad($group);
+      $groups = ad_groups_list();
+      $block['content'] = ad($delta, variable_get("ad_block_quantity_$delta", 1));
       return $block;
   }
 }
@@ -700,7 +1188,7 @@
   global $user;
 
   $edit = isset($_POST['edit']) ? $_POST['edit'] : '';
-  $adtypes = module_invoke_all('adapi', 'type');
+  $adtypes = module_invoke_all('adapi', 'type', array());
   if (arg(3) && is_array($adtypes) && in_array(arg(3), array_keys($adtypes))) {
     $adtype = arg(3);
 
@@ -716,7 +1204,7 @@
         $node[$field] = $_GET['edit'][$field];
       }
     }
-    $output = node_form($node);
+    $output = drupal_get_form('ad_node_form', $node);
     drupal_set_title(t('Submit %name', array('%name' => $adtype)));
   }
   else {
@@ -726,9 +1214,13 @@
       drupal_goto('node/add/ad/'. $adtypes[0]);
     }
     else if (sizeof($adtypes)) {
+
       foreach ($adtypes as $type) {
-        $output .= '<dt>'. l(t('%type advertisement', array('%type' => $type)), "node/add/ad/$type") .'</dt>';
-        $output .= '<dd>'. implode("\n", module_invoke_all('help', 'node/add/ad#'. $type)) .'</dd>';
+        $output .= '<dt>'. l(t('!type advertisement', array('!type' => $type)), "node/add/ad/$type") .'</dt>';
+        // Bugfix #166097: don't array_pop the module_invoke_all directly.
+        // See: http://drupal.org/node/166097
+        $help = module_invoke_all('help', 'node/add/ad#'. $type);
+        $output .= '<dd>'. array_pop($help) .'</dd>';
       }
     }
     else {
@@ -741,7 +1233,9 @@
 }
 
 /**
- *
+ * TODO: Make this themeable.
+ * TODO: Group permissions by module.
+ * TODO: Allow modules to define default value for permission.
  */
 function ad_owners_overview($node) {
   // Be sure the node owner is listed as an ad owner
@@ -754,6 +1248,8 @@
     array('data' => t('Options')),
   );
 
+  drupal_set_title('owners');
+
   $sql = "SELECT uid FROM {ad_owners} WHERE aid = $node->nid";
   $sql .= tablesort_sql($header);
   $result = pager_query($sql, 25);
@@ -763,47 +1259,282 @@
     $row = array();
     $user = user_load(array('uid' => $ad->uid));
     $row[] = $user->name;
-    $row[] = l(t('permissions'), "node/$node->nid/ad/$user->uid/permissions") .' | '. l(t('notifications'), "node/$node->nid/ad/$user->uid/notifications");
+    $options = array();
+    // first option is 'permissions', plug-ins come afterwards
+    $options[] = l(t('permissions'), "node/$node->nid/adowners/$user->uid/permissions");
+    $options = array_merge($options, module_invoke_all('adowners', 'overview', $node->nid, $user->uid));
+    // node owner has to remain an ad owner
+    if ($ad->uid != $node->uid) {
+      $options[] = l(t('remove'), "node/$node->nid/adowners/$user->uid/remove");
+    }
+    $options = implode(' | ', $options);
+    $row[] = $options;
     $rows[] = $row;
   }
 
   $output = theme('table', $header, $rows);
   $output .= theme('pager', NULL, 25, 0);
 
-  $output .= '<p></p>';
-  $output .= t('Ad owners are not implemented yet.');
-
   return $output;
 }
 
 /**
- * Add an owner to an ad.
+ * A simple form for adding new users as owners of ads.
  */
-function ad_owners_add($aid, $uid) {
-  db_query('LOCK TABLES {ad_owners} WRITE');
-  if (!db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $node->uid, $aid))) {
-    db_query('INSERT INTO {ad_owners} (aid, uid) VALUES(%d, %d)', $aid, $uid);
-  }
-  db_query('UNLOCK TABLES');
-}
-
-function ad_owner_permissions($aid, $uid) {
-  $output = t('Ad permissions are not implemented yet.');
-  return $output;
-}
+function ad_owners_add_form($node) {
+  $form = array();
+  drupal_set_title('Add owner');
 
-function ad_owner_notifications($aid, $uid) {
-  $output = t('Ad notifications are not implemented yet.');
-  return $output;
-}
+  $form['aid'] = array(
+    '#type' => 'value',
+    '#value' => $node->nid,
+  );
+  $form['username'] = array(
+    '#autocomplete_path' => 'user/autocomplete',
+    '#description' => t('Enter the username of the user who should have ownership permissions on this advertisement.'),
+    '#required' => TRUE, 
+    '#type' => 'textfield',
+    '#title' => t('Username'),
+  );
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Add owner'),
+  );
+
+  return $form;
+}
+
+function ad_owners_add_form_validate($form, &$form_state) {
+//$form_id, $form_values
+ 
+  $owner = user_load(array('name' => $form_state['values']['username']));
+  if (!is_object($owner)) {
+    form_set_error('username', t('The specified username %username does not exist.', array('%username' => $form_state['values']['username'])));
+  }
+  else if (db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $owner->uid, $form_state['values']['aid']))) {
+    form_set_error('username', t('The specified user %username is already an owner of this ad.', array('%username' => $form_state['values']['username'])));
+  }
+  else if (!user_access('manage advertisements', $owner) &&
+           !user_access('administer advertisements', $owner)) {
+    form_set_error('username', t('The specified user %username does not have <em>manage advertisements</em> nor <em>administer advertisements</em> permissions.  The user must be !assigned to a !role with these privileges before you can add them as an ad owner.', array('%username' => $form_state['values']['username'], '!assigned' => l(t('assigned'), "user/$owner->uid/edit"), '!role' => l(t('role'), 'admin/user/permissions'))));
+  }
+  module_invoke_all('adowners', 'validate', $owner, $form_state['values']['aid']);
+}
+
+function ad_owners_add_form_submit($form, &$form_state) {
+  $owner = user_load(array('name' => $form_state['values']['username']));
+  if (!(ad_owners_add($form_state['values']['aid'], $owner->uid, array('access statistics', 'access click history')))) {
+    form_set_error('username', t('The user is already an owner of the ad.'));
+  }
+  else {
+    drupal_set_message(t('The user %username has been added as an owner of this advertisement.', array('%username' => $form_state['values']['username'])));
+    drupal_goto('node/'. $form_state['values']['aid'] ."/adowners/$owner->uid/permissions");
+  }
+}
+
+function ad_is_owner($aid, $account = NULL) {
+  global $user;
+  if (is_null($account)) {
+    $account = $user;
+  }
+  if (db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $user->uid, $aid))) {
+    return 1;
+  }
+  else {
+    return 0;
+  }
+}
+
+/**
+ * Add an owner to an ad.
+ */
+function ad_owners_add($aid, $uid, $default = array('access statistics', 'access click history', 'manage status')) {
+  $node = node_load(array('nid' => $aid));
+  if ($GLOBALS['db_type'] == 'pgsql') {
+    db_query('START TRANSACTION;');
+  }
+  else {
+    // MySQL, MySQLi
+    db_query('LOCK TABLES {ad_owners} WRITE');
+  }
+
+  if (!db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $uid, $aid))) {
+    db_query('INSERT INTO {ad_owners} (aid, uid) VALUES(%d, %d)', $aid, $uid);
+    $rc = db_affected_rows() ? 1 : 0;
+
+    $permissions = array();
+    foreach ($default as $permission) {
+      if ($permission == ALL) {
+        $permissions = module_invoke_all('adapi', 'permissions', $node);
+        break;
+      }
+      else {
+        $permissions[] = $permission;
+      }
+    }
+
+    $oid = db_result(db_query("SELECT oid FROM {ad_owners} WHERE aid = %d and uid = %d", $aid, $uid));
+    if ($GLOBALS['db_type'] == 'pgsql') {
+      db_query('START TRANSACTION;');
+    }
+    else {
+      // MySQL, MySQLi
+      db_query('LOCK TABLES {ad_permissions} WRITE');
+    }
+    db_query('DELETE FROM {ad_permissions} WHERE oid = %d', $oid);
+    db_query("INSERT INTO {ad_permissions} VALUES(%d, '%s')", $oid, implode('|,|', $permissions));
+    module_invoke_all('adowners', 'add', $node, array('oid' => $oid, 'uid' => $uid, 'aid' => $aid));
+  }
+  if ($GLOBALS['db_type'] == 'pgsql') {
+    db_query('COMMIT;');
+  }
+  else {
+    // MySQL, MySQLi
+    db_query('UNLOCK TABLES');
+  }
+  return $rc;
+}
+
+/**
+ * Display a form with all available permissions and their status for the 
+ * selected ad and ad owner.
+ */
+function ad_owner_permissions($aid, $uid) {
+  drupal_set_title('Permissions');
+
+  $oid = db_result(db_query("SELECT oid FROM {ad_owners} WHERE aid = %d and uid = %d", $aid, $uid));
+  $granted = explode('|,|', db_result(db_query("SELECT permissions FROM {ad_permissions} WHERE oid = %d", $oid)));
+
+  $form['header'] = array(
+    '#type' => 'value',
+    '#value' => array(t('permissions'), t('granted'))
+  );
+
+  $rows = array();
+
+  $node = node_load(array('nid' => $aid));
+  $permissions = module_invoke_all('adapi', 'permissions', $node);
+  foreach ($permissions as $permission) {
+    $form['permission']["$permission"] = array(
+      '#value' => t("$permission"),
+    );
+    $form['grant'][str_replace(' ', '_', "$permission")] = array(
+      '#type' => 'checkbox',
+      '#default_value' => in_array("$permission", $granted) ? 1 : 0,
+    );
+  }
+
+  $form['oid'] = array(
+    '#type' => 'hidden',
+    '#value' => $oid,
+  );
+
+  $form['aid'] = array(
+    '#type' => 'hidden',
+    '#value' => $aid,
+  );
+
+  $form['uid'] = array(
+    '#type' => 'hidden',
+    '#value' => $uid,
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Display ad owner permissions in a simple table.
+ */
+function theme_ad_owner_permissions($form) {
+  $output = drupal_render($form['options']);
+  foreach (element_children($form['permission']) as $key) {
+    $row = array();
+    $row[] = drupal_render($form['permission']["$key"]);
+    $row[] = drupal_render($form['grant'][str_replace(' ', '_', "$key")]);
+    $rows[] = $row;
+  }
+
+  $output = theme('table', $form['header']['#value'], $rows);
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ * Store the ad owner's updated permissions in the ad_permissions table.
+ */
+function ad_owner_permissions_submit($form, &$form_state) {
+  $permissions = module_invoke_all('adapi', 'permissions', array());
+  $perms = array();
+  foreach ($permissions as $permission) {
+    if ($form_state['values'][str_replace(' ', '_', "$permission")]) {
+      $perms[] = $permission;
+    }
+  }
+  if ($GLOBALS['db_type'] == 'pgsql') {
+    db_query('START TRANSACTION;');
+  }
+  else {
+    // MySQL, MySQLi
+    db_query('LOCK TABLES {ad_permissions} WRITE');
+  }
+  db_query('DELETE FROM {ad_permissions} WHERE oid = %d', $form_state['values']['oid']);
+  db_query("INSERT INTO {ad_permissions} VALUES(%d, '%s')", $form_state['values']['oid'], implode('|,|', $perms));
+  if ($GLOBALS['db_type'] == 'pgsql') {
+    db_query('COMMIT;');
+  }
+  else {
+    // MySQL, MySQLi
+    db_query('UNLOCK TABLES');
+  }
+  drupal_set_message(t('The permissions have been saved.'));
+  return "node/$form_values[aid]/adowners";
+}
+
+/**
+ * Determine whether the user has a given privilege.
+ */
+function ad_adaccess($aid, $string, $account = NULL) {
+  global $user;
+  static $permissions = array();
+
+  if (is_null($account)) {
+    $account = $user;
+  }
+
+  // User #1 has all privileges:
+  if ($account->uid == 1) {
+    return TRUE;
+  }
+
+  // If you have administer permissions, you have all permissions.
+  if (user_access('administer advertisements', $account)) {
+    return TRUE;
+  }
+
+  if (!isset($permissions[$aid][$account->uid])) {
+    $oid = db_result(db_query("SELECT oid FROM {ad_owners} WHERE aid = %d and uid = %d", $aid, $account->uid));
+    $permissions[$aid][$account->uid] = explode('|,|', db_result(db_query("SELECT permissions FROM {ad_permissions} WHERE oid = %d", $oid)));
+  }
+
+  return (in_array("$string", $permissions[$aid][$account->uid]));
+}
 
 /**
  * Create a unique host id for each ad owner, used when displaying ads remotely.
  */
 function ad_host_id_create($uid) {
-  if (!db_result(db_query('SELECT hostid FROM {ad_hosts} WHERE uid = %d', $uid))) {
+  $hostid = db_result(db_query('SELECT hostid FROM {ad_hosts} WHERE uid = %d', $uid));
+  if (!$hostid) {
+    $hostid = md5($uid . time());
     db_query("INSERT INTO {ad_hosts} (uid, hostid) VALUES (%d, '%s')", $uid, md5($uid . time()));
   }
+
+  return $hostid;
 }
 
 /**
@@ -817,20 +1548,18 @@
   $header = array(
     array('data' => t('Title'), 'field' => 'n.title'),
     array('data' => t('Type'), 'field' => 'a.adtype'),
-    array('data' => t('Group'), 'field' => 'a.gid'),
-    array('data' => t('Status'), 'field' => 'a.adstatus'),
+    array('data' => t('Status'), 'field' => 'a.adstatus', 'sort' => 'asc'),
     array('data' => t('Options')),
   );
 
-  $sql = 'SELECT a.aid, a.gid, a.adstatus, a.adtype, n.title FROM {ads} a LEFT JOIN {node} n ON a.aid = n.nid';
+  $sql = 'SELECT a.aid, a.adstatus, a.adtype, n.title FROM {ads} a LEFT JOIN {node} n ON a.aid = n.nid';
   $sql .= tablesort_sql($header);
   $result = pager_query($sql, 50);
 
   while ($ad = db_fetch_object($result)) {
     $row = array();
-    $row[] = $ad->title;
+    $row[] = l($ad->title, "node/$ad->aid");
     $row[] = $ad->adtype;
-    $row[] = ad_group_name($ad->gid);
     $row[] = $ad->adstatus;
     $row[] = l(t('edit'), "node/$ad->aid/edit");
     $rows[] = $row;
@@ -842,102 +1571,348 @@
 }
 
 /**
+ *
+ */
+function ad_admin_statistics() {
+  $groups = ad_groups_list(TRUE);
+  foreach ($groups as $tid => $group) {
+    if ($tid) {
+      $ads = db_result(db_query("SELECT count(aid) FROM {ads} a JOIN {term_node} t ON a.aid = t.nid WHERE t.tid = %d AND adstatus = 'active'", $tid));
+      $filter = "= $tid";
+    }
+    else {
+      $ads = db_result(db_query("SELECT count(aid) FROM {ads} a LEFT JOIN {term_node} t ON a.aid = t.nid WHERE t.tid IS NULL AND adstatus = 'active'"));
+      $filter = "IS NULL";
+    }
+    if (!$ads) {
+      continue;
+    }
+
+    $form[$group->name] = array(
+      '#type' => 'fieldset',
+      '#title' => $group->name,
+      '#collapsible' => TRUE,
+    );
+
+    // Get overall global statistics.
+    $statistics['global']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = '%s' AND n.tid '%s'"), 'view', $filter);
+    $statistics['global']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND n.tid $filter"));
+
+    // Get overall statistics for this year and last year.
+    $this_year = date('Y000000');
+    $last_year = date('Y') - 1 .'000000';
+    $statistics['last_year']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND date <= %d AND n.tid $filter", $last_year, $this_year));
+    $statistics['last_year']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND date <= %d AND n.tid $filter", $last_year, $this_year));
+    $statistics['this_year']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND n.tid $filter", $this_year));
+    $statistics['this_year']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND n.tid $filter", $this_year));
+  
+    // Get statistics for this month and last month.
+    $this_month = date('Ym0000');
+    $last_month = date('m') - 1;
+    if ($last_month == 0) {
+      $last_month = date('Y') - 1 .'120000';
+    }
+    else {
+      $last_month = date('Y') . ($last_month < 10 ? '0' : '') . $last_month .'0000';
+    }
+    $statistics['last_month']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND date <= %d AND n.tid $filter", $last_month, $this_month));
+    $statistics['last_month']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND date <= %d AND n.tid $filter", $last_month, $this_month));
+    $statistics['this_month']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND n.tid $filter", $this_month));
+    $statistics['this_month']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND n.tid $filter", $this_month));
+  
+    // Get statistics for this week.
+    $this_week_start = date('Ymd00', time() - 60*60*24*6);
+    $statistics['this_week']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND n.tid $filter", $this_week_start));
+    $statistics['this_week']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND n.tid $filter", $this_week_start));
+  
+    // Get statistics for yesterday and today.
+    $yesterday_start = date('Ymd00', time() - 60*60*24);
+    $yesterday_end = date('Ymd24', time() - 60*60*24);
+    $today_start = date('Ymd00', time());
+    $statistics['yesterday']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND date <= %d AND n.tid $filter", $yesterday_start, $yesterday_end));
+    $statistics['yesterday']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND date <= %d AND n.tid $filter", $yesterday_start, $yesterday_end));
+    $statistics['today']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date >= %d AND n.tid $filter", $today_start));
+    $statistics['today']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date >= %d AND n.tid $filter", $today_start));
+  
+    // Get statistics for this hour and the last hour.
+    $last_hour = date('YmdH', time() - 60*60);
+    $this_hour = date('YmdH', time());
+    $statistics['last_hour']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date = %d AND n.tid $filter", $last_hour));
+    $statistics['last_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date = %d AND n.tid $filter", $last_hour));
+    $statistics['this_hour']['views'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'view' AND date = %d AND n.tid $filter", $this_hour));
+    $statistics['this_hour']['clicks'] = (int)db_result(db_query("SELECT SUM(s.count) FROM {ad_statistics} s LEFT JOIN {ads} a ON s.aid = a.aid LEFT JOIN {term_node} n ON a.aid = n.nid WHERE action = 'click' AND date = %d AND n.tid $filter", $this_hour));
+
+    // TODO: Create this view and remove the && FALSE to enable this code.
+    if (module_exists('views') && FALSE) {
+      $form[$group->name]['statistics'] = array(
+        '#type' => 'markup',
+        '#value' => '<p>'. t('There %count in this group.', array('%count' => format_plural($ads, 'is '. l('1 active ad', "ad/$group->gid/group"), 'are '. l('%count active ads', "ad/$group->tid/group")))) .'</p>'. theme('ad_statistics_display', $statistics),
+      );
+    }
+    else {
+      $form[$group->name]['statistics'] = array(
+        '#type' => 'markup',
+        '#value' => '<p>'. t('There @count in this group.', array('@count' => format_plural($ads, 'is 1 active ad', 'are @count active ads'))) .'</p>'. theme('ad_statistics_display', $statistics),
+      );
+    }
+  }
+
+  if (count($form) == 0) {
+    $form['header'] = array(
+      '#type' => 'markup',
+      '#value' => '<p>'. t('There are no active ads.') .'</p>',
+    );
+  }
+
+  return $form;
+}
+
+/**
  * Display a form for the ad module settings.
  */
 function ad_admin_configure_settings($edit = array()) {
   _ad_check_install();
 
+  $edit = $_POST['edit'];
+
   $adserve = variable_get('adserve', '');
+  $adserveinc = variable_get('adserveinc', '');
   $form['configuration'] = array(
     '#type' => 'fieldset',
     '#title' => t('Status'),
   );
   $form['configuration']['adserve'] = array(
     '#type' => 'markup',
-    '#value' => t('Using detected adserve script</em>: <em>%adserve</em>', array('%adserve' => ($adserve ? $adserve : t('not found')))),
+    '#value' => t('Using detected adserve scripts: %adserve, %adserveinc', array('%adserve' => ($adserve ? $adserve : t('not found')), '%adserveinc' => ($adserveinc ? $adserveinc : t('not found')))),
   );
 
-  $form['cache'] = array(
+  $form['general'] = array(
     '#type' => 'fieldset', 
-    '#title' => t('Cache settings')
+    '#title' => t('General'),
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
   );
-  $form['cache']['ad_cache'] = array(
+
+  // TODO: This needs a per-group over-ride, in case some groups are IFrames,
+  // while others are JavaScript, etc.
+  $form['general']['ad_link_target'] = array(
+    '#type' => 'radios',
+    '#title' => t('Click-through target'),
+    '#options' => array(
+      '_self' => t('same browser window and frame'), 
+      '_blank' => t('new browser window'), 
+      '_parent' => t('parent frame'), 
+      '_top' => t('same browser window, removing all frames'),
+    ),
+    '#default_value' => variable_get('ad_link_target', '_self'),
+    '#description' => t('Select an option above to configure what happens when an ad is clicked.  These options set the <em>a target</em>, and are <em>_self</em>, <em>_blank</em>, <em>_parent</em> and <em>_top</em> respectively.'),
+  );
+
+  $form['general']['ad_link_nofollow'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('nofollow'),
+    '#default_value' => variable_get('ad_link_nofollow', 0),
+    '#description' => t('If enabled, %tag will be added to advertisement links generated by this module.', array('%tag' => t('rel="nofollow"'))),
+  );
+
+  // Provide hook for ad_display_TYPE modules to set display TYPE.
+  $display_options = array_merge(array('javascript' => t('JavaScript'), 'iframe' => t('IFrame'), 'raw' => t('Raw')), module_invoke_all('displayapi', 'display_method'), array());
+
+  // Provide hook for ad_display_TYPE modules to define inline description.
+  $description = t('This setting configures the default method for displaying advertisements on your website.  It is possible to override this setting when making direct calls to ad(), as described in the documentation.  Using the JavaScript display method allows you to display random ads and track views even on cached pages.  Using the Raw method does not work well together with Drupal\'s page cache enabled as the advertisements will not change until the page cache is updated, and statistics will not be incremented each time an advertisement is displayed.');
+  $return = module_invoke_all('displayapi', 'display_description', array());
+  foreach ($return as $describe) {
+    $description .= ' '. $describe;
+  }
+
+  $form['general']['ad_display'] = array(
     '#type' => 'radios', 
-    '#title' => t('Type'), 
-    '#default_value' => variable_get('ad_cache', 'database'), 
-    '#options' => array('database' => t('Database'), 'file' => t('File')), 
-    '#description' => t('The cache is used to efficiently track how many times advertisements are displayed and clicked.  File based caching will usually offer better performance, however, it can be difficult to configure and will not offer valid statistics if you are using multiple load balanced web servers.')
+    '#title' => t('Display type'), 
+    '#default_value' => variable_get('ad_display', 'javascript'), 
+    '#options' => $display_options,
+    '#description' => $description,
   );
 
-  // Only enable the 'files' option if the file cache is enabled.
-  if (variable_get('ad_cache', 'database') == 'file') {
-    $attributes = array('enabled' => 'enabled');
-    $description = t('Please select the number of cache files the ad module should use.  Select a smaller value for better accuracy when performaing automatic actions on advertisements at specified thresholds.  Select a larger value for better performance.');
-  }
-  else {
-    $attributes = array('disabled' => 'disabled');
-    $description = t('This configuration setting is only relevant if the file cache is enabled.');
+  $form['general']['ad_validate_url'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Validate URLs'), 
+    '#default_value' => variable_get('ad_validate_url', 1), 
+    '#description' => t('If enabled, any destination URLs entered in ads will be required to be complete URLs (including http:// or https:// at the beginning).  If you wish to include internal urls, you will need to disable this option.'),
+  );
+
+  $form['iframe'] = array(
+    '#type' => 'fieldset', 
+    '#title' => t('IFrame'),
+    '#collapsible' => TRUE,
+    '#collapsed' => variable_get('ad_display', 'javascript') == 'iframe' ? FALSE : TRUE
+  );
+  $form['iframe']['ad_iframe_frameborder'] = array(
+    '#type' => 'checkbox', 
+    '#title' => t('Frameborder'),
+    '#default_value' => variable_get('ad_iframe_frameborder', 0), 
+    '#description' => t('If enabled, IFrames used for displaying ads will have a frameborder.'),
+  );
+  $form['iframe']['ad_iframe_scroll'] = array(
+    '#type' => 'radios', 
+    '#title' => t('Scrolling'), 
+    '#default_value' => variable_get('ad_iframe_scroll', 'auto'), 
+    '#options' => array('auto' => 'auto', 'on' => 'on', 'off' => 'off'),
+    '#description' => t('Define whether or not scroll bars should be enabled for the ad IFrame.'),
+  );
+  $form['iframe']['ad_iframe_width'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Width'),
+    '#default_value' => variable_get('ad_iframe_width', ''), 
+    '#maxlength' => 8,
+    '#size' => 5,
+    '#required' => FALSE,
+    '#description' => t('The default width for advertisement IFrames'),
+  );
+  $form['iframe']['ad_iframe_height'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Height'),
+    '#default_value' => variable_get('ad_iframe_height', ''), 
+    '#maxlength' => 8,
+    '#size' => 5,
+    '#required' => FALSE,
+    '#description' => t('The default height for advertisement IFrames'),
+  );
+
+  $form['cache'] = array(
+    '#type' => 'fieldset', 
+    '#title' => t('Cache'),
+    '#collapsible' => TRUE,
+    '#collapsed' => variable_get('ad_cache', 'none') == 'none' ? TRUE : FALSE,
+  );
+
+  // Provide hook for ad_cache_TYPE modules to set cache TYPE.
+  $cache_options = array_merge(array('none' => t('None')), module_invoke_all('adcacheapi', 'method', array()));
+
+  // Provide hook for ad_cache_TYPE modules to define inline description.
+  $description = t('A cache can be used to efficiently track how many times advertisements are displayed and clicked.');
+  $return = module_invoke_all('adcacheapi', 'description', array());
+  foreach ($return as $describe) {
+    $description .= ' '. $describe;
   }
 
-  $form['cache']['ad_files'] = array(
-    '#type' => 'select', 
-    '#title' => t('Number of cache files'), 
-    '#default_value' => variable_get('ad_files', '1'), 
-    '#options' => drupal_map_assoc(array(1, 3, 5, 10, 15)), 
+  $form['cache']['ad_cache'] = array(
+    '#type' => 'radios', 
+    '#title' => t('Type'), 
+    '#default_value' => variable_get('ad_cache', 'none'), 
+    '#options' => $cache_options,
     '#description' => $description,
-    '#attributes' => $attributes,
   );
 
+  // Provide hook for ad_cache_TYPE modules to add inline settings.
+  $form['cache'] = array_merge($form['cache'], module_invoke_all('adcacheapi', 'settings', $edit));
+
   $form['save'] = array(
     '#type' => 'submit',
     '#value' => t('Save'),
   );
 
-  return drupal_get_form('ad_settings_form', $form);
+  return $form;
 }
 
 /**
- * Prevent people from enabling the file cache until it is implemented.
+ * Validate form settings, calling attention to any illogical configurations.
  */
-function ad_settings_form_validate($form_id, $form_values) {
-  if ($form_values['ad_cache'] == 'file') {
-    form_set_error('ad_cache', t('Sorry, the file cache is not support by this version of the ad module.   At this time you must use the database cache.'));
+function ad_admin_configure_settings_validate($form, &$form_state) {
+  if ($form_state['values']['ad_link_target'] == '_self' && 
+      $form_state['values']['ad_display'] == 'iframe') {
+    // We don't consider this an error, as this could be exactly what the
+    // administrator is trying to do.  But as for most people it is likely
+    // to be a misconfiguration, display a helpful warning...
+    drupal_set_message('You have configured your advertisements to be displayed in iframes, and you have configured your click-through target as "same browser window and frame".  This is an unusual configuration, as when you click your advertisements only the IFrame will be redirected.  Be sure that this is actually what you are trying to do.');
   }
 }
 
 /**
  * Save updated values from settings form.
  */
-function ad_settings_form_submit($form_id, $form_values) {
-  variable_set('ad_cache', $form_values['ad_cache']);
-  variable_set('ad_files', $form_values['ad_files']);
+function ad_admin_configure_settings_submit($form, &$form_state) {
+  variable_set('ad_link_target', $form_state['values']['ad_link_target']);
+  variable_set('ad_link_nofollow', $form_state['values']['ad_link_nofollow']);
+  variable_set('ad_cache', $form_state['values']['ad_cache']);
+  variable_set('ad_display', $form_state['values']['ad_display']);
+  variable_set('ad_validate_url', $form_state['values']['ad_validate_url']);
+  variable_set('ad_iframe_frameborder', $form_state['values']['ad_iframe_frameborder']);
+  variable_set('ad_iframe_scroll', $form_state['values']['ad_iframe_scroll']);
+  variable_set('ad_iframe_width', $form_state['values']['ad_iframe_width']);
+  variable_set('ad_iframe_height', $form_state['values']['ad_iframe_height']);
+  if (($cache = variable_get('ad_cache', 'none')) != 'none') {
+    // Allow external cache types to store their settings
+    module_invoke('ad_cache_'. $cache, 'adcacheapi', 'settings_submit', $form_state);
+  }
+/*
+ // TODO: Write an external display module and implement this.
+  $display = variable_get('ad_display', 'javascript');
+  if ($display != 'javascript' && $display != 'raw') {
+    // Allow external display types to store their settings
+    module_invoke('ad_cache_'. $cache, 'adcacheapi', 'settings_submit', $form_values);
+  }
+*/
 }
 
 /**
- * Return an array of all groups.
- */
-function ad_groups_list() {
-  static $groups = NULL;
-  if (is_array($groups)) {
-    return $groups;
-  }
-  $result = db_query('SELECT gid,name,description FROM {ad_groups}');
-  while ($group = db_fetch_object($result)) {
-    $groups[$group->gid] = "$group->name<br />$group->description";
+ * Return an array of all groups, or a specific group.
+ *
+ * @param $tid
+ *  If set to an integer >0, will only return group info about that specific
+ *   group.
+ * @object
+ *  If FALSE, will return only name of group(s).  If TRUE, will return full
+ *  group object including ->name, ->description, and ->tid.
+ */
+function ad_groups_list($object = FALSE, $tid = NULL) {
+  static $groups = array();
+  static $names = array();
+
+  // Return the full group object(s).
+  if ($object) {
+    if (empty($groups)) {
+      $tids = taxonomy_get_tree(_ad_get_vid());
+      foreach ($tids as $group) {
+        $groups[$group->tid]->name = "$group->name";
+        $groups[$group->tid]->description = "$group->description";
+        $groups[$group->tid]->tid = $group->tid;
+      }
+      // Hard coded "default" group with tid of 0.
+      $groups[0]->name = t('default');
+      $groups[0]->description = t('The default ad group is comprised of all ads not assigned to any other ad group.');
+      $groups[0]->tid = 0;
+    }
+    // Return a specific group object.
+    if ((int)$tid) {
+      return $groups[$tid];
+    }
+    // Return an array of all group objects.
+    else {
+      return $groups;
+    }
   }
-  return $groups;
-}
-
-function ad_group_name($gid) {
-  static $groups = NULL;
-  if (isset($groups[$gid])) {
-    return $groups[$gid];
+  // Return only the group name(s).
+  else {
+    if (empty($names)) {
+      $tids = taxonomy_get_tree(_ad_get_vid());
+      foreach ($tids as $group) {
+        $names[$group->tid] = "$group->name";
+      }
+      // Hard coded "default" group with tid of 0.
+      $names[0] = t('default');
+    }
+    // Return a specific group name.
+    if ((int)$tid) {
+      return $names[$tid];
+    }
+    // Return an array of all group names.
+    else {
+      return $names;
+    }
   }
-  $groups[$gid] = db_result(db_query('SELECT name FROM {ad_groups} WHERE gid = %d', $gid));
-  return $groups[$gid];
 }
 
-function ad_admin_groups_configure($gid = 0, $op = NULL) {
+function ad_admin_groups_list() {
   _ad_check_install();
 
   $header = array(
@@ -946,206 +1921,333 @@
       array('data' => t('Options')),
     );
 
-  $sql = 'SELECT gid, name, description FROM {ad_groups}';
-  $sql .= tablesort_sql($header);
-  $result = pager_query($sql, 15);
+  $groups = taxonomy_get_tree(_ad_get_vid());
 
-  if (db_num_rows($result)) {
-    while ($group = db_fetch_object($result)) {
+  if ($groups != array()) {
+    foreach ($groups as $group) {
       $row = array();
       $row[] = $group->name;
       $row[] = $group->description;
-      if ($group->gid == 1) {
-        $row[] = '';
-      }
-      else {
-        $row[] = l(t('edit'), "admin/ad/groups/$group->gid/edit");
-      }
+      $row[] = l(t('edit'), "admin/content/ad/groups/$group->tid/edit");
       $rows[] = $row;
     }
   }
   else {
-    $rows[] = array(array('data' => t('No groups available.'), 'colspan' => 3));
+    $rows[] = array(array('data' => t('No groups have been created.'), 'colspan' => 3));
   }
 
   $output = theme('table', $header, $rows);
   $output .= theme('pager', NULL, 15, 0);
 
-  if ($gid && $op == 'edit') {
-    $edit = TRUE;
-    $group = db_fetch_object(db_query('SELECT gid, name, description FROM {ad_groups} WHERE gid = %d', $gid));
-  }
-  else {
-    $edit = FALSE;
-  }
+  return $output;
+}
 
-  $form['group'] = array(
-    '#type' => 'fieldset', 
-    '#title' => $edit ? t('Save group') : t('Create new group'),
-  );
-  $form['group']['name'] = array(
-    '#type' => 'textfield', 
-    '#title' => t('Group name'),  
-    '#maxlength' => 255,  
-    '#default_value' => $group->name,
-    '#required' => TRUE,
-    '#description' => t('Specify a name for your new ad group.  The name can only contain numbers and letters.  It can not contain spaces or punctuation.'),
-  );
-  $form['group']['description'] = array(
-    '#type' => 'textarea',  
-    '#title' => t('Group desciption'), 
-    '#default_value' => $group->description,
-    '#required' => TRUE,
-    '#description' => t('Enter a description of your new ad group.  This description is displayed when new advertisements are created.'),
-  );
-  $form['group']['gid'] = array(
+function ad_owner_remove($aid, $uid) {
+  $form['aid'] = array(
     '#type' => 'value',
-    '#value' => $gid,
+    '#value' => $aid,
   );
-  $form['group']['actions'] = array(
-    '#prefix' => '<div class="container-inline">',
-    '#suffix' => '</div>',
+  $form['uid'] = array(
+    '#type' => 'value',
+    '#value' => $uid,
   );
-  $form['group']['actions']['submit'] = array(
-    '#type' => 'submit', 
-    '#value' => $edit ? t('Save group') : t('Create group'),
+  $owner = user_load(array('uid' => $uid));
+  return confirm_form($form,
+    t('Are you sure you want to remove user %name as an owner of this advertisement?', array('%name' => $owner->name)),
+    "node/$aid/adowners",
+    t('This action cannot be undone.'),
+    t('Remove'),
+    t('Cancel')
   );
-  if ($edit) {
-    $form['group']['actions']['delete'] = array(
-      '#type' => 'submit', 
-      '#value' => t('Delete group'),
-    );
-    $form['group']['actions']['cancel'] = array(
-      '#type' => 'markup',
-      '#value' => l(t('Cancel'), 'admin/ad/groups'),
-    );
+}
+
+/**
+ * Don't allow the removal of the primary owner of the advertisement.
+ */
+function ad_owner_remove_validate($form, &$form_state) {
+  $node = node_load(array('nid' => $form_state['values']['aid']));
+  if ($node->uid == $form_state['values']['uid']) {
+    $owner = user_load(array('uid' => $form_state['values']['uid']));
+    drupal_set_message(t('%name is the primary owner of this advertisement.  You cannot remove the primary owner.', array('%name' => $owner->name)), 'error');
+    drupal_goto('node/'. $form_state['values']['aid'] .'/adowners');
   }
-  $output .= drupal_get_form('ad_admin_group_create', $form);
+}
 
-  return $output;
+/**
+ * Remove the ad owner, and all associated permissions.
+ */
+function ad_owner_remove_submit($form, &$form_state) {
+  $oid = db_result(db_query('SELECT oid FROM {ad_owners} WHERE aid = %d AND uid = %d', $form_state['values']['aid'], $form_state['values']['uid']));
+  db_query('DELETE FROM {ad_owners} WHERE oid = %d', $oid);
+  db_query('DELETE FROM {ad_permissions} WHERE oid = %d', $oid);
+  $owner = user_load(array('uid' => $form_state['values']['uid']));
+  module_invoke_all('adowners', 'remove', $oid, $owner);
+  drupal_set_message(t('The ad owner %name has been removed.', array('%name' => $owner->name)));
+  drupal_goto('node/'. $form_state['values']['aid'] .'/adowners');
 }
 
-function ad_admin_group_create_validate($form_id, $form_values) {
-  $form = array();
-  $op = $_POST['op'];
+/**
+ * Implement ad notify api _hook.
+ */
+function ad_adnotifyapi($op, $arg1 = NULL, $arg2 = NULL) {
+  switch ($op) {
+    // Make the following events available for notification.
+    case 'register':
+      return array(
+        '-expired' => t('Email @when before the advertisement will expire.'),
+        'expired' => t('Email @when after the advertisement is expired.'),
+        '-active' => t('Email @when before the advertisement will be activated (if scheduled).'),
+        'active' => t('Email @when after the advertisement is activated.'),
+        'click' => t('Email @when after the advertisement is clicked.'),
+        'approved' => t('Email @when after the advertisement is approved.'),
+        'denied' => t('Email @when after the advertisement is denied.'),
+      );
+      break;
+    case '-expired':
+      $node = node_load(array('nid' => $arg1->aid));
+      if (isset($node->autoexpire) && $node->autoexpire) {
+        if ((time() + $arg1->delay >= $node->autoexpire) &&
+            ($arg1->sent + $arg1->delay < $node->autoexpire)) {
+          return array('-expired' => 1);
+        }
+      }
+      break;
+    case '-active':
+      $node = node_load(array('nid' => $arg1->aid));
+      if (isset($node->autoactivate) && $node->autoactivate) {
+        if ((time() + $arg1->delay >= $node->autoactivate) &&
+            ($arg1->sent + $arg1->delay < $node->autoactivate)) {
+          return array('-active' => 1);
+        }
+      }
+      break;
+    case 'mail_text':
+      switch ($arg1) {
+        case 'expired':
+          return array(
+            'subject' => t('[%sitename ad] %event notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" that was being displayed on the %sitename website has expired.\n\n  Your advertisement was viewed %global_views times and clicked %global_clicks times since it was activated on %activated_large.\n\n  You can view additional statistics about this advertisement or update this notification at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case '-expired':
+          return array(
+            'subject' => t('[%sitename ad] expiration notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" that is being displayed on the %sitename website will expire on %autoexpire_large.\n\n  Your advertisement has been viewed %today_views times and clicked %today_clicks times today.  It was viewed %yesterday_views times and clicked %yesterday_clicks times yesterday.  It has been viewed %global_views times and clicked %global_clicks times since it was activated on %activated_large.\n\n  You can view additional statistics about this advertisement or update this notification at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case 'active':
+          return array(
+            'subject' => t('[%sitename ad] %event notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" is now actively being displayed on the %sitename website.\n\n  Your advertisement has been viewed %global_views times and clicked %global_clicks times since it was activated on %activated_large.\n\n  You can view additional statistics about this advertisement or update this notification at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case '-active':
+          return array(
+            'subject' => t('[%sitename ad] activation notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" will be actively displayed on the %sitename website on %autoactivate_large.\n\n  You can view statistics about this advertisement or update this notification at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case 'click':
+          return array(
+            'subject' => t('[%sitename ad] %event notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" on the %sitename website has been clicked.\n\n  Your advertisement has been viewed %today_views times and clicked %today_clicks times today.  It was viewed %yesterday_views times and clicked %yesterday_clicks times yesterday.  It has been viewed %global_views times and clicked %global_clicks times since it was activated on %activated_large.\n\n  You will receive this %frequency  You can view additional statistics about this advertisement or update this notification at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case 'approved':
+          return array(
+            'subject' => t('[%sitename ad] %event notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" on the %sitename website has been approved.\n\n  You can view statistics about this advertisement at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+        case 'denied':
+          return array(
+            'subject' => t('[%sitename ad] %event notification'),
+            'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification to inform you that your advertisement \"%title\" on the %sitename website has been denied and will not be displayed.\n\n  You can view statistics about this advertisement at the following url:\n    %url\n\nRegards,\n The %sitename Team\n\n-\n%siteurl"),
+          );
+      }
+      break;
+  }
+}
 
-  if (preg_replace('/[^0-9a-zA-Z]/', '', $form_values['name']) != $form_values['name']) {
-    form_set_error('name', t('The group name <em>%group</em> is invalid.  It can only consist of letters and numbers.', array('%group' => $form_values['name'])));
+/****/
+
+function _ad_check_install() {
+  // Verify serve.php exists and is readable.
+  $adserve = variable_get('adserve', '');
+  $adserveinc = variable_get('adserveinc', '');
+  if (!file_exists($adserve)) {
+    // The serve.php file should be in the same directory as the ad.module.
+    $adserve = drupal_get_path('module', 'ad') .'/serve.php';
+    variable_set('adserve', $adserve);
   }
-  // Create group
-  if ($op == 'Create group') {
-    if (db_result(db_query("SELECT gid FROM {ad_groups} WHERE name = '%s'", $form_values['name']))) {
-      form_set_error('name', t('A group named <em>%group</em> already exists, you must enter a unique group name.', array('%group' => $form_values['name'])));
-    }
+  if (!is_readable($adserve)) {
+    variable_set('adserve', '');
+    drupal_set_message(t('Failed to read the required file %filename.  Please make the file readable by the webserver process.  No ads can be displayed until this problem is resolved.', array('%filename' => $adserve)), 'error');
   }
-  // Save group
-  else if ($op == 'Save group') {
-    $gid = db_result(db_query("SELECT gid FROM {ad_groups} WHERE name = '%s'", $form_values['name']));
-    if ($gid && $gid != $form_values['gid']) {
-      form_set_error('name', t('A group named <em>%group</em> already exists, you must enter a unique group name.', array('%group' => $form_values['name'])));
-    }
-    else if ($gid == 1) {
-      drupal_set_message('The <em>default</em> group can not be modified.', 'error');
-      drupal_goto('admin/ad/groups');
-    }
+  if (!file_exists($adserveinc)) {
+    // The adserve.inc file should be in the same directory as the ad.module.
+    $adserveinc = drupal_get_path('module', 'ad') .'/adserve.inc';
+    variable_set('adserveinc', $adserveinc);
   }
-  // Delete group
-  else if ($op == 'Delete group') {
-    drupal_goto('admin/ad/groups/'. $form_values['gid'] .'/delete');
+  if (!is_readable($adserveinc)) {
+    variable_set('adserveinc', '');
+    drupal_set_message(t('Failed to read the required file %filename.  Please make the file readable by the webserver process.  No ads can be displayed until this problem is resolved.', array('%filename' => $adserveinc)), 'error');
   }
-}
 
-function ad_admin_group_create_submit($form_id, $form_values) {
-  $op = $_POST['op'];
-  if ($op == 'Create group') {
-    db_query("INSERT INTO {ad_groups} (name, description) VALUES ('%s', '%s')", $form_values['name'], $form_values['description']);
-    drupal_set_message(t('Ad group <em>%group</em> created.', array('%group' => $form_values['name'])));
+  $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module = 'ad'"));
+  if ($vid != variable_get('ad_group_vid', '')) {
+    drupal_set_message(t('Invalid vocabulary defined for advertisements, attempting to auto-fix.'), 'error');
+    variable_del('ad_group_vid');
   }
-  else if ($op == 'Save group') {
-    db_query("UPDATE {ad_groups} SET name = '%s', description = '%s' WHERE gid = %d", $form_values['name'], $form_values['description'], $form_values['gid']);
-    drupal_set_message(t('Ad group <em>%group</em> updated.', array('%group' => $form_values['name'])));
+
+  $result = db_query("SELECT rid FROM {permission} WHERE perm LIKE '%%show advertisements%%'");
+  if (!db_result($result)) {
+    drupal_set_message(t('Be sure to enable "!show" permissions for all roles that you wish to see advertisements.', array('!show' => l(t('show advertisements'), 'admin/user/permissions'))));
   }
-  drupal_goto('admin/ad/groups');
+
+  module_invoke_all('adapi', 'check_install', array());
 }
 
-function ad_admin_groups_delete() {
-  $group = NULL;
-  $gid = is_numeric(arg(3)) ? arg(3) : '';
-  if ($gid) {
-    $group = db_fetch_object(db_query('SELECT gid, name FROM {ad_groups} WHERE gid = %d', $gid));
-  }
-  if ($group) {
-    // Don't allow deletion of default group.
-    if ($group->name == 'default') {
-      drupal_set_message(t('The <em>default</em> group can not be deleted.'), 'error');
-      drupal_goto('admin/ad/groups');
+/**
+ * Creates a vocabulary for use by ad groups if not already created.
+ */
+function _ad_get_vid() {
+  $vid = variable_get('ad_group_vid', '');
+  if (empty($vid)) {
+    // No vid stored in the variables table, check if one even exists.
+    $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module = '%s'", 'ad'));
+    if (!$vid) {
+      // No vid, so we create one.
+      $edit = array('name' => 'Ad groups', 'multiple' => 1, 'required' => 0, 'hierarchy' => 0, 'relations' => 0, 'module' => 'ad', 'nodes' => array('ad' => 1));
+      taxonomy_save_vocabulary($edit);
+      $vid = $edit['vid'];
     }
+    // Save the vid for next time.
+    variable_set('ad_group_vid', $vid);
+  }
+  return $vid;
+}
 
-    $form['gid'] = array(
-      '#type' => 'value',
-      '#value' => $group->gid,
-    );
-    $form['name'] = array(
-      '#type' => 'value',
-      '#value' => $gruop->name,
-    );
-    return confirm_form(
-      'ad_group_confirm_delete',
-      $form,
-      t('Are you sure you want to delete the group %name?', array('%name' => theme('placeholder', $group->name))),
-      'admin/ad/groups',
-      t('All ads currently in this group will be re-attributed to the default group.  This action cannot be undone.'),
-      t('Delete'),
-      t('Cancel')
-    );
+/**
+ * Returns a form for adding an ad group.
+ */
+function ad_admin_group_form($group = array()) {
+  if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
+    drupal_goto('admin/content/ad/groups/'. $group['tid'] .'/delete');
+  }
+
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Group name'),
+    '#default_value' => $group['name'],
+    '#maxlength' => 64,
+    '#required' => TRUE,
+    '#description' => t('Specify a name for the ad group.')
+  );
+
+  $form['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Description'),
+    '#default_value' => $group['description'],
+    '#required' => TRUE,
+    '#description' => t('Describe this ad group.')
+  );
+    
+  $form['parent']['#tree'] = FALSE;
+
+  $form['weight'] = array(
+    '#type' => 'weight',
+    '#title' => t('Weight'),
+    '#default_value' => $group['weight'],
+    '#description' => t('When listing ad groups, those with lighter (smaller) weights get listed before ad groups with heavier (larger) weights.  Ad groups with equal weights are sorted alphabetically.')
+  );
+
+  $form['vid'] = array(
+    '#type' => 'hidden',
+    '#value' => _ad_get_vid());
+
+  $form['submit'] = array(
+    '#type' => 'submit', 
+    '#value' => t('Submit'));
+
+  if ($group['tid']) {
+    $form['delete'] = array(
+      '#type' => 'submit', 
+      '#value' => t('Delete'));
+    $form['tid'] = array(
+      '#type' => 'value', 
+      '#value' => $group['tid']);
   }
-  drupal_goto('admin/ad/groups');
+
+  return $form;
 }
 
-function ad_group_confirm_delete_submit($form_id, $form_values) {
-  if ($form_values['gid'] == 1) {
-    drupal_set_message('Sorry, you can not delete the <em>default</em> group.', 'error');
-    drupal_goto('admin/ad/groups');
+/**
+ * Save a newly created ad group.
+ */
+function ad_admin_group_form_submit($form, &$form_state) {
+  $status = taxonomy_save_term($form_state);
+  switch ($status) {
+    case SAVED_NEW:
+      $groups = variable_get('ad_groups', array());
+      $groups[] = $form_state['values']['tid'];
+      variable_set('ad_groups', $groups);
+      drupal_set_message(t('Created new ad group %term.', array('%term' => $form_state['values']['name'])));
+      break;
+    case SAVED_UPDATED:
+      drupal_set_message(t('The ad group %term has been updated.', array('%term' => $form_state['values']['name'])));
   }
-  db_query('UPDATE {ads} SET gid = 1 WHERE gid = %d', $form_values['gid']);
-  db_query('DELETE FROM {ad_groups} WHERE gid = %d', $form_values['gid']);
-  drupal_goto('admin/ad/groups');
+  return 'admin/content/ad/groups';
 }
 
-/****/
+/**
+ * Returns a confirmation page when deleting an ad group and all of its ads.
+ */
+function ad_confirm_group_delete($term) {
+  $form['tid'] = array('#type' => 'value', '#value' => $term['tid']);
+  $form['name'] = array('#type' => 'value', '#value' => $term['name']);
 
-function _ad_check_install() {
-  // Verify adserve.php exists and is readable.
+  return confirm_form(
+    $form,
+    t('Are you sure you want to delete the ad group %name?', array('%name' => $term['name'])), 
+    'admin/content/ad/group', 
+    t('Ads that were within this group will not be deleted.  This action cannot be undone.'), 
+    t('Delete'), 
+    t('Cancel'));
+}
+
+/**
+ * Delete ad group.
+ */
+function ad_confirm_group_delete_submit($form, &$form_state) {
+  taxonomy_del_term($form_state['values']['tid']);
+  drupal_set_message(t('The ad group %term has been deleted.', array('%term' => $form_state['values']['name'])));
+  watchdog('ad', 'mailarchive: deleted %term ad group.', array('%term' => $form_state['values']['name']));
+
+  return 'admin/content/ad/groups';
+}
+
+/**
+ * Builds the necessary HTML to display an image-based view counter.
+ */
+function ad_display_image($ad, $css = TRUE) {
+  global $base_url;
   $adserve = variable_get('adserve', '');
-  if (!file_exists($adserve)) {
-    $files = file_scan_directory('modules', '^adserve.php$');
-    switch (sizeof($files)) {
-      case 1:
-        $file = array_pop($files);
-        $adserve = $file->filename;
-        variable_set('adserve', $adserve);
-        break;
-      case 0:
-        unset($adserve);
-        variable_set('adserve', '');
-        drupal_set_message(t('Failed to locate the required file <em>adserve.php</em>.  Please verify your ad module installation.  No ads can be displayed until this problem is resolved.'), 'error');
-        break;
-      default:
-        unset($adserve);
-        variable_set('adserve', '');
-        drupal_set_message(t('Located multiple files named <em>adserve.php</em>, unable to determine which copy of the file to use.  Please remove all but one copy of <em>adserve.php</em>.  Review the <em>INSTALL.txt</em> that was distributed with the ad module for instructions on how to properly install the ad module.  No ads can be displayed until this problem is resolved.'), 'error');
-        $i = 1;
-        foreach ($files as $file) {
-          drupal_set_message(t('Copy %number: <em>%adserve</em>', array('%number' => $i++, '%adserve' => $file->filename)), 'error');
-        }
-    }
+  $cache = variable_get('ad_cache', 'none');
+  $variables = "?o=image";
+  if (is_object($ad)) {
+    $aid = $ad->aid;
   }
-  if ($adserve && !is_readable($adserve)) {
-    variable_set('adserve', '');
-    drupal_set_message(t('Failed to read the required file <em>%filename</em>.  Please make the file readable by the webserver process.  No ads can be displayed until this problem is resolved.', array('%filename' => $adserve)), 'error');
+  else {
+    /**
+     * No ad is specified, so we're just tracking traffic.
+     */
+    $aid = 0;
+  }
+  $variables .= "&amp;a=$aid";
+  if ($cache != 'none') {
+    $variables .= '&amp;c='. $cache . module_invoke('ad_cache_'. $cache, 'adcacheapi', 'display_variables', array());
+  }
+  $output = '<img src="'. url("$base_url/$adserve$variables") .'" height="0" width="0" alt="view counter" />';
+  if ($css) {
+    return '<div class="ad-image-counter">'. $output .'</div>';
+  }
+  else {
+    return $output;
   }
-
-  module_invoke_all('adapi', 'check_install');
 }
-
-?>
+?>
\ No newline at end of file

