changeset 0:d8a3998dac8e ad

ajout module ad
author pierre
date Fri, 20 Feb 2009 14:04:09 +0000
parents
children 948362c2a207
files LICENSE.txt MAINTAINER.txt README.txt ad.admin.inc ad.info ad.install ad.module ad.pages.inc ad_flash-5.x-1.x-dev.tar.gz adserve.inc cache/file/ad_cache_file.inc cache/file/ad_cache_file.info cache/file/ad_cache_file.module cache/memcache/ad_cache_memcache.inc cache/memcache/ad_cache_memcache.info cache/memcache/ad_cache_memcache.module documentation/ADSERVE_CONFIGURATION.txt documentation/CHANGELOG.txt documentation/CONTRIBUTE.txt documentation/DEBUG.txt documentation/INSTALL.txt documentation/README.txt documentation/ad_types.txt embed/ad_embed.info embed/ad_embed.module external/ad_external.info external/ad_external.install external/ad_external.module external/external.inc html/ad_html.info html/ad_html.install html/ad_html.module image/ad_image.info image/ad_image.install image/ad_image.module imageserve.inc notify/ad_notify.info notify/ad_notify.install notify/ad_notify.module owners/ad_owners.info owners/ad_owners.install owners/ad_owners.module remote/ad_remote.info remote/ad_remote.module report/ad_report.info report/ad_report.module serve.php statistics/click_filter/click_filter.info statistics/click_filter/click_filter.install statistics/click_filter/click_filter.module text/ad_text.info text/ad_text.install text/ad_text.module translations/ad.pot weight/percent/ad_weight_percent.inc weight/percent/ad_weight_percent.install weight/percent/ad_weight_percent.module
diffstat 57 files changed, 12763 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,274 @@
+GNU GENERAL PUBLIC LICENSE
+
+              Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
+Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
+verbatim copies of this license document, but changing it is not allowed.
+
+                  Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public License
+applies to most of the Free Software Foundation's software and to any other
+program whose authors commit to using it. (Some other Free Software
+Foundation software is covered by the GNU Library General Public License
+instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the
+freedom to distribute copies of free software (and charge for this service if
+you wish), that you receive source code or can get it if you want it, that you
+can change the software or use pieces of it in new free programs; and that
+you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to
+deny you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for
+a fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show
+them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2)
+offer you this license which gives you legal permission to copy, distribute
+and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients
+to know that what they have is not the original, so that any problems
+introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We
+wish to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+           GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
+               MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms
+of this General Public License. The "Program", below, refers to any such
+program or work, and a "work based on the Program" means either the
+Program or any derivative work under copyright law: that is to say, a work
+containing the Program or a portion of it, either verbatim or with
+modifications and/or translated into another language. (Hereinafter, translation
+is included without limitation in the term "modification".) Each licensee is
+addressed as "you".
+
+Activities other than copying, distribution and modification are not covered
+by this License; they are outside its scope. The act of running the Program is
+not restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made
+by running the Program). Whether that is true depends on what the Program
+does.
+
+1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the
+Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you
+may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you
+also meet all of these conditions:
+
+a) You must cause the modified files to carry prominent notices stating that
+you changed the files and the date of any change.
+
+b) You must cause any work that you distribute or publish, that in whole or in
+part contains or is derived from the Program or any part thereof, to be
+licensed as a whole at no charge to all third parties under the terms of this
+License.
+
+c) If the modified program normally reads commands interactively when run,
+you must cause it, when started running for such interactive use in the most
+ordinary way, to print or display an announcement including an appropriate
+copyright notice and a notice that there is no warranty (or else, saying that
+you provide a warranty) and that users may redistribute the program under
+these conditions, and telling the user how to view a copy of this License.
+(Exception: if the Program itself is interactive but does not normally print such
+an announcement, your work based on the Program is not required to print
+an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be
+reasonably considered independent and separate works in themselves, then
+this License, and its terms, do not apply to those sections when you distribute
+them as separate works. But when you distribute the same sections as part
+of a whole which is a work based on the Program, the distribution of the
+whole must be on the terms of this License, whose permissions for other
+licensees extend to the entire whole, and thus to each and every part
+regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to
+work written entirely by you; rather, the intent is to exercise the right to
+control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of a
+storage or distribution medium does not bring the other work under the scope
+of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1
+and 2 above provided that you also do one of the following:
+
+a) Accompany it with the complete corresponding machine-readable source
+code, which must be distributed under the terms of Sections 1 and 2 above
+on a medium customarily used for software interchange; or,
+
+b) Accompany it with a written offer, valid for at least three years, to give
+any third party, for a charge no more than your cost of physically performing
+source distribution, a complete machine-readable copy of the corresponding
+source code, to be distributed under the terms of Sections 1 and 2 above on
+a medium customarily used for software interchange; or,
+
+c) Accompany it with the information you received as to the offer to distribute
+corresponding source code. (This alternative is allowed only for
+noncommercial distribution and only if you received the program in object
+code or executable form with such an offer, in accord with Subsection b
+above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source code
+means all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation and
+installation of the executable. However, as a special exception, the source
+code distributed need not include anything that is normally distributed (in
+either source or binary form) with the major components (compiler, kernel,
+and so on) of the operating system on which the executable runs, unless that
+component itself accompanies the executable.
+
+If distribution of executable or object code is made by offering access to
+copy from a designated place, then offering equivalent access to copy the
+source code from the same place counts as distribution of the source code,
+even though third parties are not compelled to copy the source along with the
+object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy,
+modify, sublicense or distribute the Program is void, and will automatically
+terminate your rights under this License. However, parties who have received
+copies, or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you
+do not accept this License. Therefore, by modifying or distributing the
+Program (or any work based on the Program), you indicate your acceptance
+of this License to do so, and all its terms and conditions for copying,
+distributing or modifying the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution
+of the Program by all those who receive copies directly or indirectly through
+you, then the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and
+the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose
+that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will be
+similar in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that
+version or of any later version published by the Free Software Foundation. If
+the Program does not specify a version number of this License, you may
+choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software
+Foundation, write to the Free Software Foundation; we sometimes make
+exceptions for this. Our decision will be guided by the two goals of
+preserving the free status of all derivatives of our free software and of
+promoting the sharing and reuse of software generally.
+
+               NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE,
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT
+PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR
+AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR
+ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE
+LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
+SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE
+PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
+OR DATA BEING RENDERED INACCURATE OR LOSSES
+SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN
+IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGES.
+
+          END OF TERMS AND CONDITIONS
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MAINTAINER.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,4 @@
+Written by Jeremy Andrews (1/2006)
+Maintainers:
+ - Jeremy Andrews <jeremy@tag1consulting.com>
+ - Alexandr Shvets <neochief@drupaldance.com>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,1 @@
+Documentation can be found in the documentation/ subdirectory.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ad.admin.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,918 @@
+<?php
+// $Id: ad.admin.inc,v 1.1.2.9 2009/02/17 19:22:45 jeremy Exp $
+
+/**
+ * @file
+ * Advertisement admin pages and functions.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Build default ad administration page.
+ */
+function ad_admin_list() {
+  _ad_check_installation();
+
+  $output = drupal_get_form('ad_filter_form');
+
+  if (isset($_POST['operation']) && ($_POST['operation'] == 'delete') && isset($_POST['ads'])) {
+    return drupal_get_form('ad_multiple_delete_confirm');
+  }
+  $output .= drupal_get_form('ad_admin_ads');
+
+  return $output;
+}
+
+/**
+ * Provide a filterable list of advertisements.
+ */
+function ad_admin_ads() {
+  $filter = ad_build_filter_query();
+  $result = pager_query('SELECT a.*, n.* FROM {ads} a INNER JOIN {node} n ON a.aid = n.nid '. $filter['join'] .' '. $filter['where'] .' ORDER BY n.changed DESC', 50, 0, NULL, $filter['args']);
+
+  $form['options'] = array('#type' => 'fieldset',
+    '#title' => t('Update options'),
+    '#prefix' => '<div class="container-inline">',
+    '#suffix' => '</div>',
+  );
+  $options = array();
+  foreach (module_invoke_all('ad_operations') as $operation => $array) {
+    $options[$operation] = $array['label'];
+  }
+  $form['options']['operation'] = array('#type' => 'select', '#options' => $options,  '#default_value' => 'approve');
+  $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
+
+  $destination = drupal_get_destination();
+  $ads = array();
+  while ($ad = db_fetch_object($result)) {
+    $ads[$ad->aid] = '';
+    $form['title'][$ad->aid] = array('#value' => l($ad->title, 'node/'. $ad->aid));
+    $form['group'][$ad->aid] = array('#value' => _ad_get_group($ad->aid));
+    $form['adtype'][$ad->aid] = array('#value' => t(check_plain($ad->adtype)));
+    $form['adstatus'][$ad->aid] = array('#value' => t(check_plain($ad->adstatus)));
+    $form['operations'][$ad->aid] = array('#value' => l(t('edit'), 'node/'. $ad->aid .'/edit', array('query' => $destination)));
+  }
+  $form['ads'] = array('#type' => 'checkboxes', '#options' => $ads);
+  $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
+  return $form;
+}
+
+/**
+ * Implementation of hook_ad_operations().
+ */
+function ad_ad_operations() {
+  $operations = array(
+    'approved' => array(
+      'label' => t('Mark as approved'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('approved'),
+    ),
+    'active' => array(
+      'label' => t('Mark as active'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('active'),
+    ),
+    'expired' => array(
+      'label' => t('Mark as expired'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('expired'),
+    ),
+    'pending' => array(
+      'label' => t('Mark as pending'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('pending'),
+    ),
+    'offline' => array(
+      'label' => t('Mark as offline'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('offline'),
+    ),
+    'unpublished' => array(
+      'label' => t('Mark as unpublished'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('unpublished'),
+    ),
+    'denied' => array(
+      'label' => t('Mark as denied'),
+      'callback' => 'ad_operations_callback',
+      'callback arguments' => array('denied'),
+    ),
+    'delete' => array(
+      'label' => t('Delete'),
+    ),
+  );
+  return $operations;
+}
+
+/**
+ * Callback function for admin mass approving ads.
+ * TODO: Update activated and expired when appropriate.
+ * TODO: Publish/unpublish nodes when appropriate.
+ */
+function ad_operations_callback($ads, $action) {
+  $placeholders = implode(',', array_fill(0, count($ads), '%d'));
+  db_query("UPDATE {ads} SET adstatus = '". $action ."' WHERE aid IN(". $placeholders .')', $ads);
+  foreach ($ads as $aid) {
+    $node = node_load($aid);
+    ad_statistics_increment($aid, 'update');
+    ad_statistics_increment($aid, $action);
+    // Allow ad type module to act on nodeapi events.  The adapi hook provides
+    // access to additional variables not available in the nodeapi hook.
+    if (isset($node->adtype)) {
+      // 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('update', $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($action, $node);
+      }
+    }
+  }
+}
+
+/**
+ * Display a form to confirm whether to really delete the selected ads.
+ */
+function ad_multiple_delete_confirm($form_state) {
+  $form['ads'] = array('#prefix' => '<ul>', '#suffix' => '</ul>', '#tree' => TRUE);
+  // array_filter returns only elements with TRUE values
+  foreach (array_filter($form_state['post']['ads']) as $aid => $value) {
+    $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $aid));
+    $form['ads'][$aid] = array('#type' => 'hidden', '#value' => $aid, '#prefix' => '<li>', '#suffix' => check_plain($title) ."</li>\n");
+  }
+  $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
+
+  return confirm_form($form,
+                      t('Are you sure you want to delete these ads?'),
+                      'admin/content/ad', t('This action cannot be undone.'),
+                      t('Delete all'), t('Cancel'));
+}
+
+/**
+ * Perform the actual ad deletions.
+ */
+function ad_multiple_delete_confirm_submit($form, &$form_state) {
+  if ($form_state['values']['confirm']) {
+    foreach ($form_state['values']['ads'] as $aid => $value) {
+      node_delete($aid);
+    }
+    drupal_set_message(t('The ads have been deleted.'));
+  }
+  $form_state['redirect'] = 'admin/content/ad';
+}
+
+/**
+ * Theme ad administration overview.
+ */
+function theme_ad_admin_ads($form) {
+  // Overview table:
+  $header = array(theme('table_select_header_cell'), t('Title'), t('Group'), t('Type'), t('Status'), t('Operations'));
+
+  $output = drupal_render($form['options']);
+  if (isset($form['title']) && is_array($form['title'])) {
+    foreach (element_children($form['title']) as $key) {
+      $row = array();
+      $row[] = drupal_render($form['ads'][$key]);
+      $row[] = drupal_render($form['title'][$key]);
+      $row[] = drupal_render($form['group'][$key]);
+      $row[] = drupal_render($form['adtype'][$key]);
+      $row[] = drupal_render($form['adstatus'][$key]);
+      $row[] = drupal_render($form['operations'][$key]);
+      $rows[] = $row;
+    }
+
+  }
+  else  {
+    $rows[] = array(array('data' => t('No ads available.'), 'colspan' => '6'));
+  }
+
+  $output .= theme('table', $header, $rows);
+  if ($form['pager']['#value']) {
+    $output .= drupal_render($form['pager']);
+  }
+
+  $output .= drupal_render($form);
+
+  return $output;
+}
+
+/**
+ * Must select an ad if performing an operation.
+ */
+function ad_admin_ads_validate($form, &$form_state) {
+  $ads = array_filter($form_state['values']['ads']);
+  if (count($ads) == 0) {
+    form_set_error('', t('No ads selected.'));
+  }
+}
+
+/**
+ * Submit the ad administration update form.
+ */
+function ad_admin_ads_submit($form, &$form_state) {
+  $operations = module_invoke_all('ad_operations');
+  $operation = $operations[$form_state['values']['operation']];
+  // Filter out unchecked nodes
+  $ads = array_filter($form_state['values']['ads']);
+  if ($function = $operation['callback']) {
+    // Add in callback arguments if present.
+    if (isset($operation['callback arguments'])) {
+      $args = array_merge(array($ads), $operation['callback arguments']);
+    }
+    else {
+      $args = array($ads);
+    }
+    call_user_func_array($function, $args);
+
+    cache_clear_all();
+    drupal_set_message(t('The update has been performed.'));
+  }
+}
+
+/**
+ * Build query for ad administration filters based on session.
+ */
+function ad_build_filter_query() {
+  $filters = ad_filters();
+
+  // Build query
+  $where = $args = array();
+  $join = '';
+  foreach ($_SESSION['ad_overview_filter'] as $index => $filter) {
+    list($key, $value) = $filter;
+    switch ($key) {
+      case 'status':
+        list($value, $key) = explode('-', $value, 2);
+        $op = $key == 1 ? '=' : '!=';
+        $where[] = "a.adstatus $op '%s'";
+        break;
+      case 'group':
+        $table = "tn$index";
+        $where[] = "$table.tid = %d";
+        $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid ";
+        break;
+      case 'type':
+        $where[] = "a.adtype = '%s'";
+    }
+    $args[] = $value;
+  }
+  $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
+
+  return array('where' => $where, 'join' => $join, 'args' => $args);
+}
+
+/**
+ * List ad administration filters that can be applied.
+ */
+function ad_filters() {
+  $session = &$_SESSION['ad_overview_filter'];
+  $session = is_array($session) ? $session : array();
+  // Regular filters
+  $options = array();
+  $options = array(
+    'pending-1' => t('pending'),
+    'approved-1' => t('approved'),
+    'active-1' => t('active'),
+    'offline-1' => t('offline'),
+    'unpublished-1' => t('unpublished'),
+    'expired-1' => t('expired'),
+    'denied-1' => t('denied'),
+    'pending-0' => t('not pending'),
+    'approved-0' => t('not approved'),
+    'active-0' => t('not active'),
+    'offline-0' => t('not offline'),
+    'unpublished-0' => t('not unpublished'),
+    'expired-0' => t('not expired'),
+    'denied-0' => t('not denied')
+  );
+
+  $filters['status'] = array(
+    'title' => t('status'),
+    'options' => $options
+  );
+  $adtypes = ad_get_types();
+  $filters['type'] = array(
+    'title' => t('type'),
+    'options' => $adtypes,
+  );
+  // The taxonomy filter
+  if ($taxonomy = module_invoke('taxonomy', 'get_tree', _ad_get_vid())) {
+    $options = array();
+    // TODO: Add support for the default group.
+    //$options[0] = t('default');
+    foreach ($taxonomy as $term) {
+      $options[$term->tid] = check_plain($term->name);
+    }
+    $filters['group'] = array('title' => t('group'), 'options' => $options);
+  }
+
+  return $filters;
+}
+
+/**
+ * Theme ad administration filter selector.
+ */
+function theme_ad_filters($form) {
+  $output = '<ul class="clear-block">';
+  if (isset($form['current']) && sizeof($form['current'])) {
+    foreach (element_children($form['current']) as $key) {
+      $output .= '<li>'. drupal_render($form['current'][$key]) .'</li>';
+    }
+  }
+
+  $output .= '<li><dl class="multiselect">'. (isset($form['current']) && sizeof($form['current']) ? '<dt><em>'. t('and') .'</em> '. t('where') .'</dt>' : '') .'<dd class="a">';
+  foreach (element_children($form['filter']) as $key) {
+    $output .= drupal_render($form['filter'][$key]);
+  }
+  $output .= '</dd>';
+
+  $output .= '<dt>'. t('is') .'</dt><dd class="b">';
+
+  if (isset($form['status'])) {
+    foreach (element_children($form['status']) as $key) {
+      $output .= drupal_render($form['status'][$key]);
+    }
+  }
+  $output .= '</dd>';
+
+  $output .= '</dl>';
+  $output .= '<div class="container-inline" id="ad-admin-buttons">'. drupal_render($form['buttons']) .'</div>';
+  $output .= '</li></ul>';
+
+  return $output;
+}
+
+/**
+ * Return form for advertisement administration filters.
+ */
+function ad_filter_form($form_state) {
+  $session = &$_SESSION['ad_overview_filter'];
+  $session = is_array($session) ? $session : array();
+  $filters = ad_filters();
+
+  $i = 0;
+  $form['filters'] = array('#type' => 'fieldset',
+    '#title' => t('Show only ads where'),
+    '#theme' => 'ad_filters',
+  );
+  foreach ($session as $filter) {
+    list($type, $value) = $filter;
+    if ($type == 'category') {
+      // Load term name from DB rather than search and parse options array.
+      $value = module_invoke('taxonomy', 'get_term', $value);
+      $value = $value->name;
+    }
+    else if ($type == 'status') {
+      $value = $filters['status']['options'][$value];
+    }
+    else {
+      $value = $filters[$type]['options'][$value];
+    }
+    $string = ($i++ ? '<em>and</em> where <strong>%a</strong> is <strong>%b</strong>' : '<strong>%a</strong> is <strong>%b</strong>');
+    $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value)));
+    if ($type == 'type') {
+      // Remove the type option if it is already being filtered on.
+      unset($filters['type']);
+    }
+    else if ($type == 'group') {
+      unset($filters['group']);
+    }
+    if ($type == 'status') {
+      foreach ($session as $option) {
+        if ($option[0] == 'status') {
+          list($value, $key) = explode('-', $option[1], 2);
+          if ($key) {
+            // One postive key means we can't have any more.
+            // Remove the status option if we're already filtering on a positive
+            // key (ie, 'active', as an ad can't be 'active' and 'pending')
+            unset($filters['status']);
+          }
+          else {
+            // When a key is selected, remove it and its inverse as there's
+            // no logic in selecting the same key multiple times, and selecting
+            // two opposite keys will always return 0 results.
+            $inverse = $key == 1 ? 0 : 1;
+            unset($filters['status']['options'][$option[1]]);
+            unset($filters['status']['options'][$value .'-'. $inverse]);
+          }
+        }
+      }
+    }
+  }
+
+  $names = array();
+  foreach ($filters as $key => $filter) {
+    $names[$key] = $filter['title'];
+    $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
+  }
+
+  $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
+  $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
+  if (count($session)) {
+    $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
+    $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
+  }
+
+  return $form;
+}
+
+/**
+ * Theme ad administration filter form.
+ */
+function theme_ad_filter_form($form) {
+  $output  = '<div id="ad-admin-filter">';
+  $output .= drupal_render($form['filters']);
+  $output .= '</div>';
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ * Process result from ad administration filter form.
+ */
+function ad_filter_form_submit($form, &$form_state) {
+  $filters = ad_filters();
+/* TODO The 'op' element in the form values is deprecated.
+   Each button can have #validate and #submit functions associated with it.
+   Thus, there should be one button that submits the form and which invokes
+   the normal form_id_validate and form_id_submit handlers. Any additional
+   buttons which need to invoke different validate or submit functionality
+   should have button-specific functions. */
+  switch ($form_state['values']['op']) {
+    case t('Filter'):
+    case t('Refine'):
+      if (isset($form_state['values']['filter'])) {
+        $filter = $form_state['values']['filter'];
+
+        // Flatten the options array to accommodate hierarchical/nested options.
+        $flat_options = form_options_flatten($filters[$filter]['options']);
+
+        if (isset($form_state['values'][$filter]) && isset($flat_options[$form_state['values'][$filter]])) {
+          $_SESSION['ad_overview_filter'][] = array($filter, $form_state['values'][$filter]);
+        }
+      }
+      break;
+    case t('Undo'):
+      array_pop($_SESSION['ad_overview_filter']);
+      break;
+    case t('Reset'):
+      $_SESSION['ad_overview_filter'] = array();
+      break;
+  }
+}
+
+
+/**
+ *
+ */
+function ad_admin_statistics($form_state) {
+  $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' => check_plain($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 = 'view' AND n.tid %s", $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 %s", $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 %s", $last_year, $this_year, $filter));
+    $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 %s", $last_year, $this_year, $filter));
+    $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 %s", $this_year, $filter));
+    $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 %s", $this_year, $filter));
+
+    // 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 %s", $last_month, $this_month, $filter));
+    $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 %s", $last_month, $this_month, $filter));
+    $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 %s", $this_month, $filter));
+    $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 %s", $this_month, $filter));
+
+    // 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 %s", $this_week_start, $filter));
+    $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 %s", $this_week_start, $filter));
+
+    // 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 %s", $yesterday_start, $yesterday_end, $filter));
+    $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 %s", $yesterday_start, $yesterday_end, $filter));
+    $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 %s", $today_start, $filter));
+    $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 %s", $today_start, $filter));
+
+    // 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 %s", $last_hour, $filter));
+    $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 %s", $last_hour, $filter));
+    $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 %s", $this_hour, $filter));
+    $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 %s", $this_hour, $filter));
+
+    // 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>'. format_plural($ads, 'There is <a href="!url">1 active ad</a> in this group.', 'There are <a href="!url">%count</a> active ads in this group.', array('!url' => url('ad/'. $group->tid .'/group'))) .'</p>'. theme('ad_statistics_display', $statistics),
+      );
+    }
+    else {
+      $form[$group->name]['statistics'] = array(
+        '#type' => 'markup',
+        '#value' => '<p>'. format_plural($ads, 'There is 1 active ad in this group.', 'There are @count active ads in this group.') .'</p>'. theme('ad_statistics_display', $statistics),
+      );
+    }
+  }
+
+  if (!isset($form) || 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($form_state) {
+  _ad_check_installation();
+
+  $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 scripts: %adserve, %adserveinc', array('%adserve' => ($adserve ? $adserve : t('not found')), '%adserveinc' => ($adserveinc ? $adserveinc : t('not found')))),
+  );
+
+  $form['general'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('General'),
+    '#collapsible' => TRUE,
+    '#collapsed' => FALSE,
+  );
+
+  // 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'), 'jquery' => t('jQuery'), '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, jQuery, and IFrame display methods allows you to display random ads and track impressions even on cached pages.  When using the Raw display method together with Drupal\'s page cache, impressions will be properly tracked but advertisements will only change when the page cache is updated.');
+  $return = module_invoke_all('displayapi', 'display_description', array());
+  foreach ($return as $describe) {
+    $description .= ' '. $describe;
+  }
+
+  $form['general']['ad_display'] = array(
+    '#type' => 'radios',
+    '#title' => t('Display type'),
+    '#default_value' => variable_get('ad_display', 'javascript'),
+    '#options' => $display_options,
+    '#description' => $description,
+  );
+
+  $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_cache'] = array(
+    '#type' => 'radios',
+    '#title' => t('Type'),
+    '#default_value' => variable_get('ad_cache', 'none'),
+    '#options' => $cache_options,
+    '#description' => $description,
+  );
+
+  // Provide hook for ad_cache_TYPE modules to add inline settings.
+  $form['cache'] = array_merge($form['cache'], module_invoke_all('adcacheapi', 'settings'));
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate form settings, calling attention to any illogical configurations.
+ */
+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(t('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_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['values']);
+  }
+/*
+ // 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_state['values']);
+  }*/
+}
+
+/**
+ * Empty page for ad_type modules that don't define a global settings page.
+ * This way admins can still set default permissions for this ad type.
+ */
+function ad_no_global_settings($form_state) {
+  $form = array();
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+function ad_admin_groups_list() {
+  _ad_check_installation();
+
+  $header = array(
+      array('data' => t('Name'), 'field' => 'name'),
+      array('data' => t('Description'), 'field' => 'description'),
+      array('data' => t('Options')),
+    );
+
+  $groups = taxonomy_get_tree(_ad_get_vid());
+
+  if ($groups != array()) {
+    foreach ($groups as $group) {
+      $row = array();
+      $row[] = check_plain($group->name);
+      $row[] = check_plain($group->description);
+      $row[] = l(t('edit'), "admin/content/ad/groups/$group->tid/edit");
+      $rows[] = $row;
+    }
+  }
+  else {
+    $rows[] = array(array('data' => t('No groups have been created.'), 'colspan' => 3));
+  }
+
+  $output = theme('table', $header, $rows);
+  $output .= theme('pager', NULL, 15, 0);
+
+  return $output;
+}
+
+/**
+ * Returns a form for adding an ad group.
+ */
+function ad_admin_group_form($form_state, $group = NULL) {
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Group name'),
+    '#default_value' => isset($group->name) ? check_plain($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' => isset($group->description) ? check_plain($group->description) : '',
+    '#required' => TRUE,
+    '#description' => t('Describe this ad group.')
+  );
+
+  $form['weight'] = array(
+    '#type' => 'weight',
+    '#title' => t('Weight'),
+    '#default_value' => isset($group->weight) ? $group->weight : 0,
+    '#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(),
+  );
+
+
+  if (isset($group->tid)) {
+    $form['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+    );
+    $form['delete'] = array(
+      '#type' => 'submit',
+      '#value' => t('Delete'),
+    );
+    $form['tid'] = array(
+      '#type' => 'value',
+      '#value' => $group->tid
+    );
+  }
+  else {
+    $form['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Create group'),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Save a newly created ad group.
+ */
+function ad_admin_group_form_validate($form, &$form_state) {
+  if ($form_state['values']['op'] == t('Delete')) {
+    drupal_goto('admin/content/ad/groups/'. $form_state['values']['tid'] .'/delete');
+  }
+}
+
+
+/**
+ * Save a newly created ad group.
+ */
+function ad_admin_group_form_submit($form, &$form_state) {
+  $status = taxonomy_save_term($form_state['values']);
+  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'])));
+  }
+  $form_state['redirect'] = 'admin/content/ad/groups';
+}
+
+/**
+ * Returns a confirmation page when deleting an ad group and all of its ads.
+ */
+function ad_confirm_group_delete($form_state, $group = NULL) {
+  $form['tid'] = array(
+    '#type' => 'value',
+    '#value' => $group->tid,
+  );
+  $form['name'] = array(
+    '#type' => 'value',
+    '#value' => check_plain($group->name),
+  );
+
+  return confirm_form(
+    $form,
+    t('Are you sure you want to delete the ad group %name?', array('%name' => $group->name)),
+    'admin/content/ad/groups',
+    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']));
+
+  $form_state['redirect'] = 'admin/content/ad/groups';
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ad.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad.info,v 1.1.2.2.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = Ad
+package = Ad
+dependencies[] = taxonomy
+description = An advertising system for Drupal powered websites.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ad.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,445 @@
+<?php
+// $Id: ad.install,v 1.2.2.4.2.27.2.7 2009/02/17 18:56:25 jeremy Exp $
+
+/**
+ * @file
+ * Advertisement module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Ad module database schema.
+ */
+function ad_schema() {
+ /**
+  * The ad table stores administrative information about each ad.  The
+  * actual ad itself can be found in the appropriate ad type table.
+  */
+  $schema['ads'] = array(
+    'description' => 'The ad table stores administrative information about each ad.  The actual ad itself can be found in the appropriate ad type table.',
+    'fields' => array(
+      'aid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Unique ad ID. Equals to ad nid.',
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'The {users}.uid that owns this node; initially, this is the user that created it.',
+      ),
+      'adstatus' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Ad status',
+      ),
+      'adtype' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Ad type',
+      ),
+      'redirect' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Ad redirect URL',
+      ),
+      'autoactivate' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad autoactivating?',
+      ),
+      'autoactivated' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad autoactivated?',
+      ),
+      'autoexpire' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad autoexpiring?',
+      ),
+      'autoexpired' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad autoexpired?',
+      ),
+      'activated' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad activated?',
+      ),
+      'maxviews' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Maximum ad impressions',
+      ),
+      'maxclicks' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Maximum ad clicks',
+      ),
+      'expired' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Is ad expired?',
+      ),
+    ),
+    'primary key' => array('aid'),
+    'indexes' => array(
+      'uid' => array('uid'),
+      'autoactivate' => array('autoactivate'),
+      'autoactivate' => array('autoactivate'),
+    ),
+  );
+
+ /**
+  * This table counts each time a given action occurs on an ad.  Actions
+  * include when the ad is viewed, clicked, enabled and disabled.
+  * Statistics are collected at an hourly granularity.
+  *
+  * The source column is used for tracking statistics for externally
+  * hosted ads.
+  *
+  * Actions:
+  *  'view', 'click', 'enable', 'disable'
+  */
+  $schema['ad_statistics'] = array(
+    'description' => 'Stores ad statistics.',
+    'fields' => array(
+      'sid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'description' => 'Statistics entry ID.',
+      ),
+      'aid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Ad id.',
+      ),
+      'date' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Date when action was made.',
+      ),
+      'action' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Actions: "view", "click", "enable", "disable".',
+      ),
+      'adgroup' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE,
+        'default' => '',
+        'description' => 'Ad group.',
+      ),
+      'hostid' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      'count' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Count of actions triggered.',
+      ),
+    ),
+    'primary key' => array('sid'),
+    'indexes' => array(
+      'aid' => array('aid'),
+      'date' => array('date'),
+      'action' => array('action'),
+      'adgroup' => array('adgroup'),
+      'hostid' => array('hostid'),
+    ),
+  );
+
+ /**
+  * The ad_clicks table tracks when a given advertisement was clicked,
+  * who clicked it (uid if any and IP address), and what page they were
+  * on when they clicked it.
+  */
+  $schema['ad_clicks'] = array(
+    'description' => 'The ad_clicks table tracks when a given advertisement was clicked, who clicked it (uid if any and IP address), and what page they were on when they clicked it.',
+    'fields' => array(
+      'cid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'description' => 'Statistics entry ID.',
+      ),
+      'aid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Ad id.',
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      'status' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      'hostname' => array(
+        'type' => 'varchar',
+        'length' => 128,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      'user_agent' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Clicker\'s browser agent.',
+      ),
+      'adgroup' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Ad group.',
+      ),
+      'hostid' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      'url' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE,
+        'default' => '',
+        'description' => 'Clicked URL.',
+      ),
+      'timestamp' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Date when action was made.',
+      ),
+    ),
+    'primary key' => array('cid'),
+    'indexes' => array(
+      'aid' => array('aid'),
+      'status' => array('status'),
+      'hostname' => array('hostname'),
+      'user_agent' => array('user_agent'),
+      'adgroup' => array('adgroup'),
+      'hostid' => array('hostid'),
+      'url' => array('url'),
+    ),
+  );
+
+ /**
+  * The ad_hosts table is used to configure users that can display ads
+  * remotely.
+  */
+  $schema['ad_hosts'] = array(
+    'description' => 'The ad_hosts table is used to configure users that can display ads remotely. ',
+    'fields' => array(
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      'hostid' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      'status' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      'description' => array(
+        'type' => 'text',
+        'not null' => FALSE,
+        'description' => 'Host from which acion was made.',
+      ),
+    ),
+    'primary key' => array('uid'),
+    'indexes' => array(
+      'status' => array('status'),
+      'hostid' => array('hostid'),
+    ),
+  );
+
+  return $schema;
+}
+
+/**
+ * Ad module installation.
+ */
+function ad_install() {
+  // Create tables.
+  drupal_install_schema('ad');
+}
+
+/**
+ * Allow complete uninstallation of the ad module.
+ */
+function ad_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('ad');
+
+  // Delete all ad content.
+  $result = db_query("SELECT nid FROM {node} WHERE type = 'ad'");
+  while ($node = db_fetch_object($result)) {
+    node_delete($node->nid);
+    variable_del("ad_autoactivate_warning_$node->nid");
+  }
+
+  // Delete all remaining ad module variables.
+  $variables = array('ad_cron_timestamp', 'ad_link_target', 'ad_cache', 'ad_cache_file', 'adserve', 'ad_group_vid', 'ad_groups', 'ad_validate_url', 'ad_display');
+  foreach ($variables as $variable) {
+    variable_del($variable);
+  }
+  db_query("DELETE FROM {variable} WHERE name LIKE 'ad_block_quantity_%'");
+}
+
+/**
+ * Convert some things from absolete dev. schema to new schema API
+ */
+function ad_update_6001() {
+  $ret = array();
+  // When we touching index columns, we should first remove it from schema
+  db_drop_index($ret, 'ad_clicks', 'status');
+  db_change_field($ret, 'ad_clicks', 'status', 'status',
+      array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      array('indexes' => array(
+        'status' => array('status'),
+      ),
+    )
+  );
+  db_drop_index($ret, 'ad_hosts', 'status');
+  db_change_field($ret, 'ad_hosts', 'status', 'status',
+      array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => '',
+      ),
+      array('indexes' => array(
+        'status' => array('status'),
+      ),
+    )
+  );
+
+  db_drop_index($ret, 'ad_statistics', 'hostid');
+  db_change_field($ret, 'ad_statistics', 'hostid', 'hostid',
+      array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      array('indexes' => array(
+        'hostid' => array('hostid'),
+      ),
+    )
+  );
+
+  db_drop_index($ret, 'ad_hosts', 'hostid');
+  db_change_field($ret, 'ad_hosts', 'hostid', 'hostid',
+      array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Host from which acion was made.',
+      ),
+      array('indexes' => array(
+        'hostid' => array('hostid'),
+      ),
+    )
+  );
+  return $ret;
+}
+
+/**
+ * Rebuild menu for anyone using the ad_embed module.
+ */
+function ad_update_6002() {
+  menu_rebuild();
+  return array();
+}
+
+/**
+ * Flush all caches for new themeable ad display functions.
+ */
+function ad_update_6003() {
+  drupal_flush_all_caches();
+  return array();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ad.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,1506 @@
+<?php
+// $Id: ad.module,v 1.2.2.29.2.83.2.16 2009/02/17 18:56:26 jeremy Exp $
+
+/**
+ * @file
+ * An advertising system for Drupal powered websites.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_theme() {
+  return array(
+    'ad_display' => array(
+      'file' => 'ad.module',
+      'arguments' => array(
+        'group' => NULL,
+        'display' => NULL,
+        'method' => 'javascript',
+      ),
+    ),
+    'ad_status_display' => array(
+      'file' => 'ad.module',
+      'arguments' => array(
+        'node' => NULL,
+      ),
+    ),
+    'ad_statistics_display' => array(
+      'file' => 'ad.pages.inc',
+      'arguments' => array(
+        'statistics' => NULL,
+      ),
+    ),
+    'node_ad' => array(
+      'file' => 'ad.pages.inc',
+      'arguments' => array(
+        'node' => NULL,
+        'yield_form' => TRUE,
+      ),
+    ),
+    'ad_admin_ads' => array(
+      'file' => 'ad.admin.inc',
+      'arguments' => array(
+        'form' => NULL,
+      ),
+    ),
+    'ad_filters' => array(
+      'file' => 'ad.admin.inc',
+      'arguments' => array(
+        'form' => NULL,
+      ),
+    ),
+    'ad_filter_form' => array(
+      'file' => 'ad.admin.inc',
+      'arguments' => array(
+        'form' => NULL,
+      ),
+    ),
+  );
+};
+
+/**
+ * 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 = FALSE, $quantity = 1, $options = array()) {
+  global $base_url;
+
+  $adserve = variable_get('adserve', '');
+  $adserveinc = variable_get('adserveinc', '');
+  if (empty($adserve) || empty($adserveinc)) {
+    // This is probably the first time ad() has been called.
+    _ad_check_installation();
+    $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_installation();
+    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':
+    case 'jquery':
+      $query['m'] = $options['ad_display'];
+      // Fall through...
+    case 'javascript':
+    default:
+      $query['q'] = $quantity;
+      if (isset($options['hostid'])) {
+        $query['k'] = $options['hostid'];
+      }
+      // Allow external cache files to define additional display variables.
+      if ($options['cache'] != 'none') {
+        $query['c'] =  $options['cache'];
+        $cache_variables = module_invoke('ad_cache_'. $options['cache'], 'adcacheapi', 'display_variables', array());
+        if (is_array($cache_variables)) {
+          foreach ($cache_variables as $key => $value) {
+            $query[$key] = $value;
+          }
+        }
+      }
+      // Allow ad_type modules to define additional display variables.
+      $type_variables = module_invoke_all('adapi', 'display_variables', array());
+      if (is_array($type_variables)) {
+        foreach ($type_variables as $key => $value) {
+          $query[$key] = $value;
+        }
+      }
+      if (isset($options['nids'])) {
+        // Choose ads from the provided list of node Id's.
+        $nids = $options['nids'];
+        $query['n'] = $nids;
+        $group = "nids-$nids";
+      }
+      else if (isset($options['tids'])) {
+        // Choose ads from the provided list of taxonomy terms.
+        $tids = $options['tids'];
+        $query['t'] = $tids;
+        $group = "tids-$tids";
+      }
+      else {
+        // Choose ads from the specified group.
+        $query['t'] = $group;
+        $options['tids'] = $group;
+      }
+      $src = url($base_url .'/'. $adserve, array('query' => $query));
+      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 if ($options['ad_display'] == 'jquery') {
+        // The theme function uses this to generate a CSS id for jQuery to use.
+        $output = $src;
+      }
+      else {
+        $output = '<script type="text/javascript" src="'. $src .'"></script>';
+      }
+      break;
+  }
+
+  if (user_access('show advertisements')) {
+    return theme('ad_display', $group, $output, $options['ad_display']);
+  }
+  else {
+    return theme('ad_display', 'none', "<!-- Enable 'show advertisements' permission if you wish to display ads here. -->");
+  }
+}
+
+/**
+ * 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, $method = 'javascript') {
+  static $id = 0;
+
+  // The naming convention for the id attribute doesn't allow commas.
+  $group = preg_replace('/[,]/', '+', $group);
+
+  if ($method == 'jquery') {
+    drupal_add_js('misc/jquery.js', 'core');
+    return "\n<div class=\"advertisement group-$group\" id=\"group-id-$id\">\n <script type=\"text/javascript\">\n //<!--[CDATA[\n  $(document).ready(function(){ jQuery(\"div#group-id-$id\").load(\"$display\"); });\n //]]>\n </script>\n</div>\n";
+  }
+  else {
+    return "\n<div class=\"advertisement group-$group\" id=\"group-id-$group\">$display</div>\n";
+  }
+}
+
+/**
+ * Update click counter then redirect host to ad's target URL.
+ */
+function ad_redirect($aid, $group, $hostid = 0) {
+  global $user;
+  if (function_exists('click_filter_status')) {
+    $status = click_filter_status($aid, $hostid);
+    if ($status == CLICK_VALID) {
+      ad_statistics_increment($aid, 'click', $group, $hostid);
+    }
+  }
+  else {
+    // We're not filtering clicks, so all clicks are valid.
+    ad_statistics_increment($aid, 'click', $group, $hostid);
+    $status = 0;
+  }
+  // Allow source url to be passed in.
+  $url = isset($_GET['u']) ? $_GET['u'] : '';
+  if (!isset($url) || !valid_url($url)) {
+    $url = referer_uri();
+  }
+  db_query("INSERT INTO {ad_clicks} (aid, uid, status, hostname, user_agent, adgroup, hostid, url, timestamp) VALUES (%d, %d, %d, '%s', '%s', '%s', '%s', '%s', %d)", $aid, $user->uid, $status, ip_address(), $_SERVER['HTTP_USER_AGENT'], $group, $hostid, $url, time());
+
+  // Determine where we're supposed to redirect the user.
+  $adtype = db_result(db_query('SELECT adtype FROM {ads} WHERE aid = %d', $aid));
+
+  $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() {
+  return array_merge(ad_link_target(TRUE), ad_link_nofollow(TRUE));
+}
+
+/**
+ * Ad API Helper Function:
+ * Provide XHTML-strict-compatible target window onclick-handlers based on
+ * global configuration.
+ */
+function ad_link_target() {
+  switch (variable_get('ad_link_target', '_self')) {
+    case '_blank':
+      $target = array('onclick' => 'window.open(this.href); return false;');
+      break;
+    case '_parent':
+      $target = array('onclick' => 'window.parent.location = this.href; return false;');
+      break;
+    case '_top':
+      $target = array('onclick' => 'window.top.location = this.href; return false;');
+      break;
+    default:
+      $target = array();
+      break;
+  }
+  return $target;
+}
+
+/**
+ * Ad API Helper Function:
+ * Append rel="nofollow" if globally enabled.
+ */
+function ad_link_nofollow() {
+  if (variable_get('ad_link_nofollow', 0)) {
+    $nofollow = array('rel' => 'nofollow');
+  }
+  else {
+    $nofollow = array();
+  }
+  return $nofollow;
+}
+
+/**
+ * Increment action counter.
+ */
+function ad_statistics_increment($aid, $action, $group = NULL, $hostid = NULL) {
+  // Update action statistics.
+  db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE date = %d AND aid = %d AND action = '%s' AND adgroup = '%s' AND hostid = '%s'", date('YmdH'), $aid, $action, $group, $hostid);
+  // If column doesn't already exist, we need to add it.
+  if (!db_affected_rows()) {
+    db_query("INSERT INTO {ad_statistics} (aid, adgroup, hostid, date, action, count) VALUES(%d, '%s', '%s', %d, '%s', 1)", $aid, $group, $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 date = %d AND aid = %d AND action = '%s' AND adgroup = '%s' AND hostid = '%s'", date('YmdH'), $aid, $action, $group, $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(
+      '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(
+      'active' => t('This advertisement is actively being displayed.'),
+      'offline' => t('This advertisement has been temporarily disabled and is not currently being displayed.'));
+  }
+}
+
+/**
+ * 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 impressions.', 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);
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    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 = '<p>'. t('Advertisements can be randomly displayed to visitors of your website.') .'</p>';
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_cron().
+ */
+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());
+  }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function ad_perm() {
+  return array('administer advertisements',
+                'create advertisements',
+                'edit own advertisements',
+                'show advertisements');
+}
+
+/**
+ * Implementation of hook_node_info().
+ */
+function ad_node_info() {
+  $items['ad'] = array(
+    'name' => t('Advertisement'),
+    'module' => 'ad',
+    'description' => t('Advertisements can be randomly displayed to visitors of your website.'),
+  );
+
+  $adtypes = ad_get_types('data');
+  // New 'type' notation returns everything we need to define content type
+  foreach ($adtypes as $type => $data) {
+    $items['ad/'. $type] = $data;
+  }
+  return $items;
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function ad_access($op, $node, $account) {
+  if ($op == 'create') {
+    return user_access('create advertisements', $account);
+  }
+
+  if ($op == 'update' || $op == 'delete') {
+    return (user_access('administer advertisements', $account) ||
+           (module_exists('ad_owners') && ad_is_owner($node->nid) && user_access('edit own advertisements', $account)));
+  }
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function ad_form($node, &$form_state) {
+  $form = array();
+
+  $type = arg(3);
+  if (function_exists('ad_'. $type .'_display_ad')) {
+    $adtype = $type;
+  }
+  else {
+    $adtype = isset($node->adtype) ? $node->adtype : '';
+  }
+
+  $form['aid'] = array(
+    '#type' => 'value',
+    '#value' => isset($node->nid) ? $node->nid : 0,
+  );
+
+  $form['title'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Title'),
+    '#required' => TRUE,
+    '#default_value' => isset($node->title) ? $node->title : '',
+  );
+  $form['body_filter']['body'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Description'),
+    '#default_value' => isset($node->body) ? $node->body : '',
+    '#rows' => 3
+  );
+  $form['body_filter']['format'] = filter_form($node->format);
+
+  // determine the current ad type
+  if (!isset($adtype)) {
+    $adtypes = ad_get_types();
+    if (sizeof($adtypes) == 1) {
+      $adtype = key($adtypes);
+    }
+    else if (!sizeof($adtypes)) {
+      drupal_set_message(t('At least one ad type module must be enabled before you can create advertisements.  For example, try <a href="!url">enabling</a> the ad_text or ad_image module.', array('!url' => url('admin/build/modules'))), 'error');
+    }
+  }
+
+  // display type-specific options
+  if (isset($adtype)) {
+    $elements = module_invoke('ad_'. $adtype, 'adapi', 'form', $node);
+    if (is_array($elements)) {
+      foreach ($elements as $element => $values) {
+        $form[$element] = $values;
+      }
+    }
+    $form['adtype'] = array(
+      '#type' => 'hidden',
+      '#value' => $adtype,
+    );
+  }
+
+  if (user_access('administer advertisements')) {
+    // admins can set any status on advertisements
+    $form['adstatus'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Status'),
+      '#collapsible' => TRUE
+    );
+    foreach (ad_status_array() as $status => $description) {
+      $form['adstatus']["ad$status"] = array(
+        '#type' => 'radio',
+        '#title' => t($status),
+        '#return_value' => $status,
+        '#default_value' => isset($node->adstatus) ? $node->adstatus : 'pending',
+        '#description' => $description,
+        '#parents' => array('adstatus')
+      );
+    }
+  }
+  else if (ad_adaccess($node, 'manage status')) {
+    if (!$node->adstatus || $node->adstatus == 'pending') {
+      $adstatus = ad_status_array();
+      $node->adstatus = 'pending';
+      $form['adstatus'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Status'),
+        '#collapsible' => TRUE
+      );
+      $form['adstatus']['display'] = array(
+        '#type' => 'markup',
+        '#value' => '<p><strong>'. t('Status') .':</strong> '. 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'),
+        '#collapsible' => TRUE
+      );
+      foreach ($adstatus as $status => $description) {
+        $form['adstatus']["ad$status"] = array(
+          '#type' => 'radio',
+          '#title' => t($status),
+          '#return_value' => $status,
+          '#default_value' => $node->adstatus ? $node->adstatus : 'pending',
+          '#description' => $description,
+          '#parents' => array("adstatus")
+        );
+      }
+    }
+  }
+  else {
+    $adstatus = ad_status_array();
+    if (!isset($node->adstatus)) {
+      $node->adstatus = 'pending';
+    }
+    $form['ad_adstatus'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Status'),
+      '#collapsible' => TRUE
+    );
+    $form['ad_adstatus']['adstatus_display'] = array(
+      '#type' => 'markup',
+      '#value' => '<p><strong>'. t('Status') .':</strong> '. t($node->adstatus) .'<br />'. t($adstatus[$node->adstatus]),
+    );
+    $form['adstatus'] = array(
+      '#type' => 'value',
+      '#value' => $node->adstatus
+    );
+  }
+
+  // display scheduling options
+  $form['schedule'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Scheduling'),
+    '#collapsible' => TRUE,
+    // Collapse if there isn't any scheduling data set.
+    '#collapsed' => (
+      isset($node->autoactivate) ||
+      isset($form_state['values']['autoactivate']) ||
+      isset($node->autoexpire) ||
+      isset($form_state['values']['autoexpire']) ||
+      isset($node->maxviews) ||
+      isset($form_state['values']['maxviews']) ||
+      isset($node->maxclicks) ||
+      isset($form_state['values']['maxclicks']))
+      ? FALSE : TRUE,
+  );
+
+  if (ad_adaccess($node, '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' => isset($node->autoactivate) && $node->autoactivate > 0 ? format_date((int)$node->autoactivate, 'custom', 'F j, Y H:i') : 0,
+      '#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(
+      '#type' => 'textfield',
+      '#title' => t('Automatically expire ad'),
+      '#required' => FALSE,
+      '#default_value' => isset($node->autoexpire) && $node->autoexpire > 0 ? format_date((int)$node->autoexpire, 'custom', 'F j, Y H:i') : 0,
+      '#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 impressions'),
+      '#required' => FALSE,
+      '#size' => 10,
+      '#maxlength' => 11,
+      '#default_value' => isset($node->maxviews) ? $node->maxviews : 0,
+      '#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 impressions, 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' => isset($node->maxclicks) ? $node->maxclicks : 0,
+      '#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
+    $form['schedule']['autoexpire_display'] = array(
+      '#type' => 'markup',
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => theme('ad_status_display', $node),
+    );
+    $form['schedule']['autoexpire'] = array(
+      '#type' => 'hidden',
+      '#value' => isset($node->autoexpire) ? $node->autoexpire : 0,
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function ad_form_alter(&$form, &$form_state, $form_id) {
+  if ($form_id == 'ad_node_form') {
+    $adtypes = ad_get_types('data');
+    if (isset($form['adtype']) && isset($form['adtype']['#value'])) {
+      $adtype = $form['adtype']['#value'];
+      if (isset($adtypes[$adtype])) {
+        // Valid advertisement type selected.
+        return;
+      }
+    }
+    if (sizeof($adtypes) == 1) {
+      // Auto select the appropriate advertisement type.
+      drupal_goto('node/add/ad/'. key($adtypes));
+    }
+    else {
+      foreach ($adtypes as $key => $adtype) {
+        $out = '<dt>'. l($adtype['name'], "node/add/ad/$key", array('title' => t('Add a !key', array('!key' => $adtype['name'])))) .'</dt>';
+        $out .= '<dd>'. $adtype['description'] .'</dd>';
+        $item[$key] = $out;
+      }
+      if (isset($item)) {
+        $output = t('Choose from the following advertisement types:');
+        $output .= '<dl>'. implode('', $item) .'</dl>';
+      }
+      else {
+        $output = t('You are not allowed to create advertisements.');
+      }
+
+      $form = array();
+      $form['select'] = array(
+        '#value' => $output,
+      );
+      $form['#tree'] = FALSE;
+      $form['#programmed'] = FALSE;
+    }
+  }
+  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['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']);
+    }
+  }
+}
+
+/**
+ * Submit handler for global settings of all ad types.
+ *
+ * @see ad_form_alter()
+ */
+function ad_global_settings_submit($form, &$form_state) {
+  variable_set('ad_'. $form_state['values']['adtype'] .'_default_permissions', $form_state['values']['default_permissions']);
+  unset($form_state['values']['adtype'], $form_state['values']['default_permissions']);
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function ad_nodeapi(&$node, $op, $teaser, $page) {
+  global $user;
+
+  switch ($op) {
+
+    case 'load':
+      $ad = db_fetch_array(db_query('SELECT * FROM {ads} WHERE aid = %d', $node->nid));
+      $merge = array_merge((array)$node, (array)$ad);
+      $adtype = module_invoke('ad_'. $ad['adtype'], 'adapi', 'load', $merge);
+      if (is_array($adtype)) {
+        return array_merge($ad, $adtype);
+      }
+      else {
+        return $ad;
+      }
+      break;
+
+    case 'insert':
+      if (isset($node->adtype)) {
+        if ($node->status != 1 && $node->adstatus == 'active') {
+          $node->adstatus = 'unpublished';
+        }
+        $activated = $node->adstatus == 'active' ? time() : 0;
+        if (!isset($node->autoactive)) {
+          $node->autoactivate = 0;
+        }
+        if (!isset($node->maxviews)) {
+          $node->maxviews = 0;
+        }
+        if (!isset($node->maxclicks)) {
+          $node->maxclicks = 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)", $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, $node->maxviews, $node->maxclicks);
+        ad_statistics_increment($node->nid, 'create');
+      }
+      break;
+
+    case 'update':
+      if (isset($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 = 'unpublished';
+        }
+        // If a previously unpublished node has been published, reactivate the
+        // the ad.
+        else if ($node->status == 1 && $node->adstatus == 'unpublished') {
+          $node->adstatus = 'active';
+          // Special "publish" event, may as well track it even though we'll
+          // next also record an "active" event.
+          ad_statistics_increment($node->nid, 'publish');
+        }
+        // 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->adstatus != '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', redirect = '%s', autoactivate = %d, autoexpire = %d, activated = %d, maxviews = %d, maxclicks = %d, expired = %d WHERE aid = %d", $node->uid, $node->adstatus, $node->adtype, url('ad/redirect/'. $node->nid, array('absolute' => TRUE)), isset($node->autoactivate) && $node->autoactivate > 0 ? strtotime($node->autoactivate) : '', isset($node->autoexpire) && $node->autoexpire > 0 ? strtotime($node->autoexpire) : '', $activated, isset($node->maxviews) ? (int)$node->maxviews : 0, isset($node->maxclicks) ? (int)$node->maxclicks : 0, isset($expired) ? $expired : 0, $node->nid);
+        ad_statistics_increment($node->nid, 'update');
+      }
+      break;
+
+    case 'delete':
+      db_query("DELETE FROM {ads} WHERE aid = %d", $node->nid);
+      db_query("DELETE FROM {ad_statistics} 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 (isset($node->adtype)) {
+        $node = node_prepare($node, $teaser);
+        $node->content['body'] = array(
+          '#value' => $teaser ? $node->teaser : theme('node_ad', $node, $page),
+          '#weight' => 1,
+        );
+      }
+      break;
+  }
+  // Allow ad type module to act on nodeapi events.  The adapi hook provides
+  // access to additional variables not available in the nodeapi hook.
+  if (isset($node->adtype)) {
+    // 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 ad_adapi($op, $node = NULL) {
+  switch ($op) {
+    case 'permissions':
+      return array('access statistics', 'access click history', 'manage status');
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_menu() {
+  $items = array();
+
+  $items['admin/content/ad'] = array(
+    'title' => 'Ads',
+    'page callback' => 'ad_admin_list',
+    'access arguments' => array('administer advertisements'),
+    'description' => 'Configure and manage your advertising system.',
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/list'] = array(
+    'title' => 'List',
+    'page callback' => 'ad_admin_list',
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/statistics'] = array(
+    'title' => 'Statistics',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_admin_statistics'),
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 1,
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/configure'] = array(
+    'title' => 'Settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_admin_configure_settings'),
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 3,
+    'file' => 'ad.admin.inc',
+  );
+
+  ad_menu_add_global_settings($items);
+
+  $items['admin/content/ad/groups'] = array(
+    'title' => 'Ad groups',
+    'page callback' => 'ad_admin_groups_list',
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 5,
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/groups/list'] = array(
+    'title' => 'List',
+    'page callback' => 'ad_admin_groups_list',
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => 0,
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/groups/add'] = array(
+    'title' => 'Create group',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_admin_group_form'),
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 3,
+    'file' => 'ad.admin.inc',
+  );
+  $items["admin/content/ad/groups/%ad_group/edit"] = array(
+    'title' => 'Edit group',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_admin_group_form', 4),
+    'access arguments' => array('administer advertisements'),
+    'weight' => 1,
+    'file' => 'ad.admin.inc',
+  );
+  $items["admin/content/ad/groups/%ad_group/delete"] = array(
+    'title' => 'Delete group',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_confirm_group_delete', 4),
+    'access arguments' => array('administer advertisements'),
+    'weight' => 2,
+    'file' => 'ad.admin.inc',
+  );
+  $items['admin/content/ad/configure/global'] = array(
+    'title' => 'Global settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_admin_configure_settings'),
+    'access arguments' => array('administer advertisements'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => 0,
+    'file' => 'ad.admin.inc',
+  );
+  $items["node/%node/details/%"] = array(
+    'title' => 'Click details',
+    'page callback' => 'ad_click_details',
+    'page arguments' => array(1, 3),
+    'access arguments' => array(1, 'access click history'),
+    'access callback' => 'ad_adaccess',
+    'type' => MENU_CALLBACK,
+    'file' => 'ad.pages.inc',
+  );
+  $items["ad/redirect/%/%/%"] = array(
+    'access arguments' => array('show advertisements'),
+    'type' => MENU_CALLBACK,
+    'page callback' => 'ad_redirect',
+    'page arguments' => (array(2, 3, 4)),
+  );
+  $items["ad/redirect/%/%"] = array(
+    'access arguments' => array('show advertisements'),
+    'type' => MENU_CALLBACK,
+    'page callback' => 'ad_redirect',
+    'page arguments' => (array(2, 3)),
+  );
+
+  return $items;
+}
+
+/**
+ * Load settings for all ad modules. Those modules, who don't
+ * have their settings form, will get a standard one.
+ */
+function ad_menu_add_global_settings(&$menu_items) {
+  $adtypes = ad_get_types();
+  foreach ($adtypes as $type => $name) {
+    // Ad type global settings.
+    $settings = 'ad_'. $type .'_global_settings';
+    if (!function_exists($settings)) {
+      $settings = 'ad_no_global_settings';
+    }
+    $menu_items['admin/content/ad/configure/'. $type] = array(
+      'title' => $name,
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array($settings),
+      'access arguments' => array('administer advertisements'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 2,
+      'file' => 'ad.admin.inc',
+    );
+  }
+}
+
+/**
+ * Drupal menu wildcard Ad group loader
+ */
+function ad_group_load($tid) {
+  if (!is_numeric($tid)) {
+    return FALSE;
+  }
+  $group = ad_groups_list(TRUE, $tid);
+  if (!isset($group)) {
+    return FALSE;
+  }
+  return $group;
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function ad_block($op = 'list', $delta = 0, $edit = array()) {
+  switch ($op) {
+    case 'list':
+      $blocks = array();
+      $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':
+      $groups = ad_groups_list();
+      $block['content'] = ad($delta, variable_get('ad_block_quantity_'. $delta, 1));
+      return $block;
+  }
+}
+
+/**
+ * Determine whether the user has a given privilege.
+ *
+ * @param $ad
+ *   Node object or aid of advertisement.
+ * @param $permission
+ *   Special Ad owners permission which should be checked (such as 'manage owners')
+ * @param $account
+ *   User object, which are accessing the ad or current user by default.
+ */
+function ad_adaccess($ad, $permission, $account = NULL) {
+  global $user;
+  static $permissions = array();
+
+  if (!isset($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;
+  }
+
+  // Handle ad owners access
+  if (module_exists('ad_owners')) {
+    return ad_owners_adaccess($ad, $permission, $account);
+  }
+
+  return FALSE;
+}
+
+/**
+ * Returns ad types data.
+ *
+ * @param $op
+ *  If set to 'name', will only return array of type names ($type => $name).
+ *  If set to 'data', will return all of the type's data
+ * @param $type
+ *  If not specified, will return array of all available types, else will
+ *  return specific type's name or data.
+ */
+function ad_get_types($op = 'name', $type = NULL) {
+  $adtypes = module_invoke_all('adapi', 'type', array());
+  switch ($op) {
+    case 'name':
+      if (isset($type)) {
+        return $adtypes[$type]['name'];
+      }
+      else {
+        foreach ($adtypes as $type => $data) {
+          $adtypes[$type] = $data['name'];
+        }
+        return $adtypes;
+      }
+    case 'data':
+      if (isset($type)) {
+        return $adtypes[$type];
+      }
+      else {
+        return $adtypes;
+      }
+  }
+}
+
+/**
+ * Return an array of all groups, or a specific group.
+ *
+ * @param $object
+ *  If FALSE, will return only name of group(s).  If TRUE, will return full
+ *  group object including ->name, ->description, and ->tid.
+ * @param $tid
+ *  If set to an integer >0, will only return group info about that specific
+ *  group.
+ */
+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());
+      if (is_array($tids)) {
+        foreach ($tids as $group) {
+          $groups[$group->tid]->name = $group->name;
+          $groups[$group->tid]->description = $group->description;
+          $groups[$group->tid]->tid = $group->tid;
+          $groups[$group->tid]->weight = $group->weight;
+        }
+      }
+      // 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;
+      $groups[0]->weight = 0;
+    }
+    // Return a specific group object.
+    if ((int)$tid) {
+      return $groups[$tid];
+    }
+    // Return an array of all group objects.
+    else {
+      return $groups;
+    }
+  }
+  // Return only the group name(s).
+  else {
+    if (empty($names)) {
+      $tids = taxonomy_get_tree(_ad_get_vid());
+      if (is_array($tids)) {
+        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;
+    }
+  }
+}
+
+/**
+ * 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($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($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;
+  }
+}
+
+function _ad_check_installation() {
+  // 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);
+  }
+  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');
+  }
+  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);
+  }
+  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');
+  }
+
+  // Validate vid in vocabulary table.
+  $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');
+    if ($vid) {
+      db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d OR type = 'ad'", variable_get('ad_group_vid', ''));
+      variable_set('ad_group_vid_restore', variable_get('ad_group_vid', ''));
+    }
+    variable_del('ad_group_vid');
+  }
+  else {
+    // Validate vid in vocabulary_node_types table.
+    $result = db_query("SELECT vid FROM {vocabulary_node_types} WHERE type = 'ad'");
+    $found = FALSE;
+    while ($vocab = db_fetch_object($result)) {
+      if ($vocab->vid == variable_get('ad_group_vid', '')) {
+        $found = TRUE;
+      }
+    }
+    if (!$found) {
+      drupal_set_message(t('Missing vocabulary node type for advertisements, attempting to auto-fix.'), 'error');
+      db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d OR type = 'ad'", variable_get('ad_group_vid', ''));
+      db_query("DELETE FROM {vocabulary} WHERE vid = %d", variable_get('ad_group_vid', ''));
+      variable_set('ad_group_vid_restore', variable_get('ad_group_vid', ''));
+      variable_del('ad_group_vid');
+    }
+  }
+
+  _ad_get_vid();
+  // Preserve old ad groups, if any.
+  if (($old = variable_get('ad_group_vid_restore', '')) &&
+       $vid = variable_get('ad_group_vid', '')) {
+    drupal_set_message(t('Restoring orphaned ad group configuration.'));
+    db_query('UPDATE {term_data} SET vid = %d WHERE vid = %d', $vid, $old);
+    variable_set('ad_group_vid_restore', '');
+  }
+
+  $rid = db_result(db_query_range("SELECT rid FROM {permission} WHERE perm LIKE '%%show advertisements%%'", 1));
+  if (!$rid) {
+    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'))));
+  }
+
+  // Allow modules to define an action to take each time an ad is served.
+  // When modules define 'adserve_select' or 'adserve_filter', they must set
+  // the 'function' and 'path' parameters.  The 'weight' parameter can
+  // optionally be set.
+  //  function: the function to call when serving an add
+  //  path: the path to the include file where $function is defined
+  // Modules can define actions that happen when advertisements are served.
+  // Currently support actions are:
+  //  - init_text   (display content before displaying ads) // TODO
+  //  - select      (alter which ads are selected to be displayed) // TODO
+  //  - filter      (filter selected ads before they are displayed) // TODO
+  //  - exit_text   (display content after displaying ads) // TODO
+  $hooks = array('init_text', 'select', 'filter', 'exit_text');
+  foreach ($hooks as $hook) {
+    $adserve_actions = module_invoke_all('adapi', "adserve_$hook", array());
+    $actions = array();
+    foreach ($adserve_actions as $name => $action) {
+      if (is_numeric($action['weight'])) {
+        $weight = $action['weight'];
+      }
+      else {
+        // weight is an optional, defaults to 0
+        $weight = $action['weight'] = 0;
+      }
+      $actions[$weight .'.'. $name] = $action;
+      $actions[$weight .'.'. $name]['name'] = $name;
+    }
+    // order actions by weight (multiple same-weight actions sorted by alpha)
+    ksort($actions);
+    variable_set("adserve_$hook", serialize($actions));
+  }
+
+  module_invoke_all('adapi', 'check_install', array());
+}
+
+/**
+ * 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' => t('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;
+}
+
+/**
+ * Builds the necessary HTML to display an image-based impressions counter.
+ */
+function ad_display_image($ad, $css = TRUE) {
+  global $base_url;
+  $adserve = variable_get('adserve', '');
+  $cache = variable_get('ad_cache', 'none');
+  $variables = "?o=image";
+  if (is_object($ad)) {
+    $aid = $ad->aid;
+  }
+  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;
+  }
+}
+
+/**
+ * Retrieve the group name from the nid.
+ */
+function _ad_get_group($nid) {
+  static $groups = array();
+
+  if (!isset($groups[$nid])) {
+    $result = db_query('SELECT d.name FROM {term_data} d LEFT JOIN {term_node} n ON d.tid = n.tid WHERE n.nid = %d AND d.vid = %d', $nid, _ad_get_vid());
+    while ($term = db_fetch_object($result)) {
+      $terms[] = $term->name;
+    }
+    if (!empty($terms)) {
+      $groups[$nid] = implode(', ', $terms);
+    }
+    else {
+      $groups[$nid] = t('default');
+    }
+  }
+
+  return $groups[$nid];
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ad.pages.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,251 @@
+<?php
+// $Id: ad.pages.inc,v 1.1.2.6 2009/02/16 23:12:28 jeremy Exp $
+
+/**
+ * @file
+ * Advertisement nodes pages and forms.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>
+ */
+
+function theme_node_ad($node, $yield_form = TRUE) {
+  $output = '';
+  if (ad_adaccess($node, 'access statistics')) {
+    $output = theme('ad_status_display', $node);
+    $output .= theme('ad_statistics_display', ad_statistics($node->nid));
+  }
+  if (ad_adaccess($node, 'access click history')) {
+    $header = array(
+      array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
+      array('data' => t('User'), 'field' => 'uid'),
+      array('data' => t('URL where clicked'), 'field' => 'url'),
+    );
+    if (function_exists('click_filter_status_text') && user_access('view filtered clicks')) {
+      $header[] = array('data' => t('Status'), 'field' => 'status');
+    }
+    $header[] = '';
+
+    if ($node->nid) {
+      $sql = "SELECT cid, timestamp, uid, status, url FROM {ad_clicks} WHERE aid = %d";
+      $sql .= tablesort_sql($header);
+      $result = pager_query($sql, 25, 0, NULL, $node->nid);
+
+      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', 'M j H:i');
+        $row[] = theme('username', $click_user);
+        $row[] = l($url, $ad->url);
+        if (function_exists('click_filter_status_text') && user_access('view filtered clicks')) {
+          $row[] = click_filter_status_text($ad->status);
+        }
+        $row[] = '['. l(t('details'), 'node/'. $node->nid .'/details/'. $ad->cid) .']';
+        $rows[] = $row;
+      }
+
+      if (empty($rows)) {
+        $click_history = '<p>'. t('There are no clicks yet.') .'</p>';
+      }
+      else {
+        $click_history = theme('table', $header, $rows);
+      }
+      $click_history .= theme('pager', NULL, 25, 0);
+      $output .= theme('box', t('Click history'), $click_history);
+    }
+  }
+  return $output;
+}
+
+/**
+ * Calculate statistics for the given advertisements.
+ * TODO: Introduce caching to make this more efficient.
+ */
+function ad_statistics($aid) {
+  // Get global statistics.
+  $statistics['global']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view'", $aid));
+  $statistics['global']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click'", $aid));
+
+  // No sense in making further queries if the ad has no global statistics.
+  if (!$statistics['global']['views'] && !$statistics['global']['clicks']) {
+    return $statistics;
+  }
+
+  // Get 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(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d AND date <= %d", $aid, $last_year, $this_year));
+  $statistics['last_year']['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_year, $this_year));
+  $statistics['this_year']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, $this_year));
+  $statistics['this_year']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, $this_year));
+
+  // No sense in making further queries if the ad has no statistics this year.
+  if (!$statistics['this_year']['views'] && !$statistics['this_year']['clicks']) {
+    return $statistics;
+  }
+
+  // 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(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));
+  $statistics['this_month']['views'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, $this_month));
+  $statistics['this_month']['clicks'] = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, $this_month));
+
+  // No sense in making further queries if the ad has no statistics this month.
+  if (!$statistics['this_month']['views'] && !$statistics['this_month']['clicks']) {
+    return $statistics;
+  }
+
+  // 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(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']) {
+    return $statistics;
+  }
+
+  // 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(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']) {
+    return $statistics;
+  }
+
+  // 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(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($statistics) {
+  $header = array('', t('Impressions'), t('Clicks'), t('Click-thru'));
+  $rows = array();
+
+  $data = array(
+   'this_hour' => t('This hour'),
+   'last_hour' => t('Last hour'),
+   'today' => t('Today'),
+   'yesterday' => t('Yesterday'),
+   'this_week' => t('Last seven days'),
+   'this_month' => t('This month'),
+   'last_month' => t('Last month'),
+   'this_year' => t('This year'),
+   'last_year' => t('Last year'),
+   'global' => t('All time')
+  );
+
+  foreach ($data as $key => $value) {
+    if (isset($statistics[$key]) && (isset($statistics[$key]['views']) || isset($statistics[$key]['clicks']) || $key == 'global')) {
+      $rows[] = array(
+        array('data' => $value),
+        array('data' => (int)$statistics[$key]['views']),
+        array('data' => (int)$statistics[$key]['clicks']),
+        array('data' => $statistics[$key]['views'] ? sprintf('%1.2f', ((int)$statistics[$key]['clicks'] / (int)$statistics[$key]['views']) * 100) .'%' : '0.00%'),
+      );
+    }
+  }
+  if (empty($rows) || (!$statistics['global']['views'] && !$statistics['global']['clicks'])) {
+    $statistics = '<p>'. t('There are no any statistics yet.') .'</p>';
+  }
+  else {
+    $statistics = theme('table', $header, $rows);
+  }
+
+  return theme('box', t('Statistics'), $statistics);
+}
+
+
+function ad_click_details($node, $cid) {
+  drupal_set_breadcrumb(array(l(t('Home'), NULL), l(check_plain($node->title), 'node/'. $node->nid)));
+  if ($click = db_fetch_object(db_query('SELECT * FROM {ad_clicks} WHERE cid = %d', $cid))) {
+    $ad = node_load($click->aid);
+    $account = user_load(array('uid' => $click->uid));
+    $rows = array(
+      array(
+        array('data' => t('Time'), 'header' => TRUE),
+        format_date($click->timestamp, 'custom', 'D F j, Y h:i a'),
+      ),
+      array(
+        array('data' => t('User'), 'header' => TRUE),
+        theme('username', $account),
+      ),
+      array(
+        array('data' => t('IP Address'), 'header' => TRUE),
+        $click->hostname,
+      ),
+      array(
+        array('data' => t('User Agent'), 'header' => TRUE),
+        check_plain($click->user_agent),
+      ),
+      array(
+        array('data' => t('URL'), 'header' => TRUE),
+        l($click->url, $click->url),
+      ),
+      array(
+        array('data' => t('Advertisement'), 'header' => TRUE),
+        $ad->ad,
+      )
+    );
+    if (function_exists('click_filter_status_text') && user_access('view filtered clicks')) {
+      switch ($click->status) {
+        case 0:
+        default:
+          $status = t('Not valid: this click has not been counted for unknown reasons.  This is an unexpected error.');
+          break;
+        case 1:
+          $status = t('Valid: this is a valid click.');
+          break;
+        case 2:
+          $status = t('Not valid: this click has not been counted because another click by the same IP address was already counted.');
+          break;
+        case 3:
+          $status = t('Not valid: this click has not been counted because it was generated by an owner of the advertisement.');
+          break;
+        case 4:
+          $status = t('Not valid: this click has not been counted because it was generated by a user in a filtered role.');
+          break;
+        case 5:
+          $status = t('Not valid: this click has not been counted because it was generated by an automated "bot".');
+          break;
+      }
+      $rows[] = array(array('data' => t('Status'), 'header' => TRUE), $status);
+    }
+    $output = theme('table', array(), $rows);
+  }
+  return $output;
+}
+
+
Binary file ad_flash-5.x-1.x-dev.tar.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/adserve.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,732 @@
+<?php
+// $Id: adserve.inc,v 1.1.2.31.2.8 2009/02/17 19:22:45 jeremy Exp $
+
+/**
+ * @file
+ * Configuration.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ *
+ * By default, adserve configuration happens dynamically as ads are served.
+ * However, it is possible to override dynamic settings with static defaults.
+ * Refer to the documentation/ADSERVE_CONFIGURATION.txt for details on adding
+ * adserve overrides to settings.php.
+ *
+ * Note that the path to Drupal's root directory can not be overriden in
+ * settings.php as adserve needs this path to find settings.php in the first
+ * place.  To hard code the path to Drupal's root directory, uncomment the
+ * following define statement, and set the correct path.  This is not generally
+ * required.  On a Unix server this path will be something like '/path/to/web'.
+ * On a Windows server this path will be something like 'D:\path\to\web'.
+ */
+//define('DRUPAL_ROOT', '/var/www/html');
+
+/**
+ * The main adserve logic.
+ */
+function adserve_ad($options = array()) {
+  static $displayed_count = 0;
+
+  // If no $options are passed in, assume we're using JavaScript.
+  if (!empty($options)) {
+    adserve_variable('variable_load', $options);
+  }
+  else {
+    adserve_variable('variable_load');
+  }
+  adserve_bootstrap(0);
+
+  adserve_debug();
+
+  adserve_variable('error', FALSE);
+  $output = NULL;
+  if (adserve_variable('adcache') != 'none') {
+    /**
+     * Ad caches are defined through external modules.  Ad caches are composed
+     * of a module 'ad_cache_TYPE.module' and an include file
+     * 'ad_cache_TYPE.inc' that live in the 'cache/TYPE' subdirectory where
+     * 'TYPE' is replaced with the type of cache.  For example, the included
+     * file cache lives in 'cache/file'.
+     *
+     * The ad_cache_TYPE.inc file must have a function named ad_cache_TYPE()
+     * which is used to display ads.  It can optionally include a function
+     * titled ad_cache_TYPE_variables used to extract any necessary
+     * variables from the global $_GET array (this can also be used to override
+     * values that would normally be set from $_GET).  Any functions used
+     * by this code without bootstrapping Drupal should also be in this file.
+     *
+     * The ad_cache_TYPE.module file should define the drupal _help() hook
+     * so the module can be enabled.  It should also define the _adcacheapi()
+     * hook allowing for configuration and processing.  Any functions used by
+     * this code after bootstrapping Drupal should also be in this module.
+     *
+     * Refer to cache/file/* for an implementation example.
+     */
+    $function = 'ad_cache_'. adserve_variable('adcache');
+    $output = adserve_invoke_file($function);
+
+  }
+
+  // If there's no output, we assume either there's no cache enabled, or the
+  // cache failed.
+  // TODO: Log failures with the watchdog.
+  if ($output == NULL) {
+    if (adserve_variable('debug')) {
+      echo "No cache enabled.<br />\n";
+    }
+
+    adserve_bootstrap();
+
+    if (adserve_variable('nids')) {
+      $id = adserve_variable('nids');
+      $type = 'nids';
+      adserve_variable('group', "n$id");
+
+      // Retrieve all active advertisements from the provided nid list.
+      $sql = "SELECT aid FROM {ads} WHERE adstatus = 'active' AND aid IN (%s)";
+      $result = db_query($sql, $id);
+
+      if (adserve_variable('debug')) {
+        echo "Searching for ad from nid list: $id.<br />\n";
+        echo "Query: \"$sql;\"<br />\n";
+      }
+    }
+    else if (adserve_variable('tids')) {
+      $id = adserve_variable('tids');
+      $type = 'tids';
+      adserve_variable('group', "t$id");
+
+      // Retrieve all active advertisements from the provided tid list.
+      $sql = "SELECT a.aid FROM {ads} a INNER JOIN {term_node} n ON a.aid = n.nid WHERE a.adstatus = 'active' AND n.tid IN (%s)";
+      $result = db_query($sql, $id);
+
+      if (adserve_variable('debug')) {
+        echo "Searching for ad from tid list: $id.<br />\n";
+        echo "Query: \"$sql;\"<br />\n";
+      }
+    }
+    else {
+      $id = 0;
+      $type = 'default';
+      adserve_variable('group', "$id");
+
+      // Randomly determine which ad to display from those that do not have
+      // any tid assigned to them.
+      $sql = "SELECT a.aid FROM {ads} a LEFT JOIN {term_node} n ON a.aid = n.nid WHERE a.adstatus = 'active' AND n.tid IS NULL";
+      $result = db_query($sql);
+
+      if (adserve_variable('debug')) {
+        echo "Searching for ads with no tids.<br />\n";
+        echo "Query: \"$sql;\"<br />\n";
+      }
+    }
+
+    // Build list of all available ads to choose from.
+    $available = array();
+    while ($ad = db_fetch_object($result)) {
+      $available[$ad->aid] = $ad->aid;
+    }
+    if (adserve_variable('debug')) {
+      echo 'Available ads: ';
+      if (sizeof($ads)) {
+        echo implode(', ', $available) ."<br />";
+      }
+      else {
+        echo 'none<br />';
+      }
+    }
+
+    // Randomly select from available advertisements.
+    $selected = adserve_select_ad($available, adserve_variable('quantity'));
+
+    $output = '';
+    $ads = 0;
+    $details = array();
+    $ids = array();
+    // Include appropriate module for displaying selected ad.
+    foreach ($selected as $aid) {
+      $ids[$aid] = $aid;
+      $ads++;
+      $detail = $details[$aid] = node_load($aid);
+      if (!isset($modules[$detail->adtype])) {
+        $modules[$detail->adtype] = db_result(db_query("SELECT filename FROM {system} WHERE name = '%s'", 'ad_'. $detail->adtype));
+      }
+      if (adserve_variable('debug')) {
+        echo 'ad: <pre>';
+        print_r($detail);
+        echo '</pre>';
+        echo "Loading module '". $modules[$detail->adtype] ."'.<br />\n";
+      }
+      include_once $modules[$detail->adtype];
+
+      if ($output) {
+        // Add a div between ads that themers can use to arrange ads when
+        // displaying more than one at a time.
+        $displayed_count++;
+        $output .= "<div class=\"advertisement-space\" id=\"space-$id-$displayed_count\"></div>";
+      }
+      $output .= module_invoke("ad_$detail->adtype", 'display_ad', $detail);
+
+      // Update the ad's impressions counter.
+      if (adserve_variable('ad_display') == 'raw') {
+        $output .= ad_display_image($detail);
+      }
+      else {
+        adserve_increment($detail);
+      }
+    }
+    adserve_variable("$type-ids", $ids);
+    if (empty($ads)) {
+      adserve_variable('error', TRUE);
+      $output = 'No active ads were found in the '. (empty($nids) ? 'tids' : 'nids') ." '$id'.";
+      adserve_increment(NULL, 'count');
+    }
+    if (adserve_variable('debug')) {
+      echo "Ads displayed: $ads<br />";
+    }
+  }
+
+  $hostid = adserve_variable('hostid');
+  $group = adserve_variable('group');
+  $replace = "/$group";
+  if (!empty($hostid)) {
+    $replace .= "/$hostid";
+  }
+  if ($url = htmlentities(adserve_variable('url'))) {
+    $replace .= "?u=$url";
+  }
+
+  $output = preg_replace('&/@HOSTID___&', $replace, $output);
+  if (adserve_variable('error')) {
+    $output = "<!-- $output -->";
+  }
+
+  /**
+   * Modules can add custom code to be displayed before or after ads are
+   * displayed.  For example, you many want to add a tagline, "Powered by
+   * Drupal".  To do so, define 'adserve_exit_text' within your module's
+   * adapi hook.
+   *
+   * Code sample for adserve_exit_text example:
+   *
+   *   sample_adapi($op, $ad) {
+   *     case 'adserve_exit_text':
+   *       return array(
+   *         'sample' => array(
+   *           'text' => t('Powered by Drupal'),
+   *         )
+   *       );
+   *   }
+   *
+   * As another example use case, you could also use the _init_text and
+   * _exit_text hooks to wrap all advertisements in a custom div.
+   */
+  $init = TRUE;
+  foreach (array('adserve_init_text', 'adserve_exit_text') as $hook) {
+    $result = adserve_invoke_hook($hook);
+    if (is_array($result)) {
+      $append = '';
+      foreach ($result as $text) {
+        if ($text['text']) {
+          $append .= $text['text'];
+        }
+      }
+      if ($init) {
+        $output = $append . $output;
+      }
+      else {
+        $output .= $append;
+      }
+    }
+    $init = FALSE;
+  }
+
+  switch (adserve_variable('ad_display')) {
+    case 'iframe':
+    case 'jquery':
+      if (!adserve_variable('debug')) {
+        // Tell the web browser not to cache this frame so the ad refreshes
+        // each time the page is viewed.
+
+        // Expires in the past:
+        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+        // Last load:
+        header('Last-Modified: '. gmdate('D, d M Y H:i:s') .' GMT');
+        // HTTP 1.1:
+        header('Cache-Control: no-store, no-cache, must-revalidate');
+        header('Cache-Control: post-check=0, pre-check=0', FALSE);
+        // HTTP 1.0:
+        header('Pragma: no-cache');
+      }
+      print "$output";
+      exit(0);
+    case 'javascript':
+    default:
+      $output = str_replace(array("\r", "\n", "<", ">", "&"),
+                            array('\r', '\n', '\x3c', '\x3e', '\x26'),
+                            addslashes($output));
+      if (!adserve_variable('debug')) {
+        // Tell the web browser not to cache this script so the ad refreshes
+        // each time the page is viewed.
+        // Expires in the past:
+        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+        // Last load:
+        header('Last-Modified: '. gmdate('D, d M Y H:i:s') .' GMT');
+        // HTTP 1.1:
+        header('Cache-Control: no-store, no-cache, must-revalidate');
+        header('Cache-Control: post-check=0, pre-check=0', FALSE);
+        // HTTP 1.0:
+        header('Pragma: no-cache');
+        // Output is a JavaScript:
+        header('Content-Type: application/x-javascript; charset=utf-8');
+      }
+      print "document.write('$output');";
+      exit(0);
+    case 'raw':
+      chdir(adserve_variable('ad_dir'));
+      return $output;
+  }
+}
+
+/**
+ * Retrieve variables from $_GET array or from passed in $value array.
+ */
+function adserve_variable($variable, $value = NULL) {
+  global $conf;
+  static $variables = NULL, $overridden = NULL, $cache_loaded = array();
+
+  // Update the value, if set.
+  if (isset($value)) {
+    $variables->$variable = $value;
+  }
+
+  if (!isset($variables->loaded) || $variable == 'variable_load') {
+    if ($variable == 'variable_load' && isset($value)) {
+      $values['debug'] = isset($value['debug']) ? $value['debug'] : '';
+      $values['c'] = isset($value['adcache']) ? $value['adcache'] : '';
+      $values['n'] = isset($value['nids']) ? $value['nids'] : '';
+      $values['t'] = isset($value['tids']) ? $value['tids'] : '';
+      $values['k'] = isset($value['hostid']) ? $value['hostid'] : '';
+      $values['q'] = isset($value['quantity']) ? $value['quantity'] : 1;
+      $values['m'] = isset($value['ad_display']) ? $value['ad_display'] : 0;
+      unset($value);
+    }
+    else {
+      $values = $_GET;
+    }
+
+    // Don't use getcwd as path may involve symbolic links
+    $variables->ad_dir = dirname($_SERVER['SCRIPT_FILENAME']);
+    // 'debug' is an integer.
+    $variables->debug = isset($values['debug']) ? (int)$values['debug'] : 0;
+    // Cache types are comprised of only letters.
+    $variables->adcache = isset($values['c']) ? preg_replace('/[^a-zA-Z]/', '', $values['c']) : 'none';
+    // Nids is an integer or a ",".
+    $variables->nids = isset($values['n']) ? preg_replace('/[^0-9,]/', '', $values['n']) : '';
+    // Tids is an integer or a ",".
+    $variables->tids = isset($values['t']) ? preg_replace('/[^0-9,]/', '', $values['t']) : '';
+    // Hostid is an md5() which is comprised of numbers and letters a-f.
+    $variables->hostid = isset($values['k']) ? preg_replace('/[^0-9a-f]/', '', $values['k']) : '';
+    // Click url
+    $variables->url = isset($values['u']) ? $values['u'] : '';
+    // Quantity is an integer.
+    $variables->quantity = isset($values['q']) ? (int)$values['q'] : 0;
+    // Ad ID is an integer.
+    $variables->aid = isset($values['a']) ? (int)$values['a'] : 0;
+    // Method is compriese of only letters.
+    $variables->ad_display = isset($values['m']) ? preg_replace('/[^a-zA-Z]/', '', $values['m']) : 'javascript';
+
+    // Set defaults.
+    $variables->quantity = $variables->quantity ? $variables->quantity : 1;
+
+    if ($variables->debug) {
+      foreach ($variables as $variable => $val) {
+        echo "$variable: '$val'<br />\n";
+      }
+      if ($variables->debug == 1) exit;
+    }
+    $variables->loaded = TRUE;
+
+    // Override the value, if set during initialization.
+    if (isset($value)) {
+      $variables->$variable = $value;
+    }
+  }
+
+  if (!$overridden) {
+    if (isset($conf)) {
+      foreach ($conf as $var => $val) {
+        $variables->$var = $val;
+        if ($variables->debug) {
+          echo "Override $var: '$val'<br />\n";
+        }
+      }
+      $overridden = TRUE;
+    }
+  }
+
+  if (!isset($cache_loaded[$variables->adcache])) {
+    // Retrieve variables defined by cache plugin, if enabled.
+    if ($variables->adcache != 'none') {
+      $include = $variables->ad_dir ."/cache/$variables->adcache/ad_cache_$variables->adcache.inc";
+      if (file_exists($include)) {
+        if ($variables->debug) {
+          echo "Attempting to include cache include file '$include'.<br />\n";
+        }
+        require_once($include);
+      }
+      else if ($variables->debug) {
+        echo "Failed to find cache include file '$include'.<br />\n";
+      }
+      $function = 'ad_cache_'. $variables->adcache .'_variables';
+      if (function_exists($function)) {
+        $external_variables = $function();
+        foreach ($external_variables as $key => $val) {
+          if (!isset($variables->$key)) {
+            $variables->$key = $val;
+          }
+        }
+      }
+    }
+    $cache_loaded[$variables->adcache] = TRUE;
+  }
+
+  if ($variable == 'variable_dump') {
+    echo "Dumping \$variables:<br />\n";
+    echo '<pre>';
+    foreach ($variables as $var => $val) {
+      echo "  $var($val)<br />\n";
+    }
+    echo '</pre>';
+  }
+
+  if (isset($variables->$variable)) {
+    return $variables->$variable;
+  }
+  else {
+    return NULL;
+  }
+}
+
+/**
+ * Invoke a function in the specified file.
+ */
+function adserve_invoke_file($function, $arg1 = NULL, $arg2 = NULL) {
+  $output = '';
+  if (function_exists($function)) {
+    $output = $function($arg1, $arg2);
+  }
+  else if (adserve_variable('debug')) {
+    echo "Function '$function' does not exist.<br />\n";
+  }
+  return $output;
+}
+
+/**
+ * Invoke adserve hooks, defined in adapi with adserve_HOOK.
+ */
+function adserve_invoke_hook($hook, $a1 = NULL, $a2 = NULL) {
+  if (adserve_variable('adcache') != 'none') {
+    $cache = adserve_variable('adcache');
+    _debug_echo("Invoking adserve hook '$hook' in $cache cache.");
+    // Get information from cache.
+    return adserve_invoke_file("ad_cache_{$cache}_$hook", $a1, $a2);
+  }
+  else {
+    _debug_echo("Invoking adserve hook '$hook'.");
+    // Get information from Drupal variable table.
+    $actions = variable_get($hook, '');
+    $return = array();
+    if (!empty($actions)) {
+      $actions = unserialize($actions);
+      foreach ($actions as $name => $action) {
+        if ($action['function']) {
+          $function = $action['function'];
+          if (!function_exists($function)) {
+            if ($action['path']) {
+              _debug_echo("Including file '". $action['path'] ."'.");
+              include_once($action['path']);
+            }
+          }
+          if (function_exists($function)) {
+            _debug_echo("Invoking function '$function'.");
+            $return[] = $function($a1, $a2);
+          }
+          else if (adserve_variable('debug')) {
+            echo "Function '$function' does not exist.<br />\n";
+          }
+        }
+        else {
+          $return[] = $action;
+        }
+      }
+    }
+    return $return;
+  }
+  // Retreive hook definition from cache if using, or from variable_get
+  // return hook action.
+}
+
+function _debug_echo($text) {
+  if (adserve_variable('debug')) {
+    echo "$text<br />\n";
+  }
+}
+
+/**
+ * Remove one or more ids from array.
+ * TODO: Optimize.  Perhaps something like array_flip, unset, array_flip.
+ * @param $ids An array of ID's, ie array(5, 8, 9, 11).
+ * @param $remove An ID or an array of ID's to be removed from $ids.
+ */
+function adserve_select_reindex($ids, $remove) {
+  $new = array();
+  // Walk through array of IDs and decide what to keep.
+  foreach ($ids as $id) {
+    // If $remove is an array, walk through array to decide fate of ID.
+    if (is_array($remove)) {
+      $keep = TRUE;
+      foreach ($remove as $rem) {
+        // Loop until we find one that matches or reach end of array.
+        if ($id == $rem) {
+          $keep = FALSE;
+          break;
+        }
+      }
+      if ($keep) {
+        $new[] = $id;
+      }
+    }
+    else {
+      if ($id != $remove) {
+        $new[] = $id;
+      }
+    }
+  }
+  return $new;
+}
+
+/**
+ * Disabled: will be re-implemented with new adserve hooks introduced for
+ * geotargeting.
+function adserve_invoke_weight($ads, $quantity = 1, $invalid = array()) {
+  $parent = adserve_variable('ad_dir') .'/weight';
+  if (is_dir($parent) && $handle = opendir($parent)) {
+    while ($dir = readdir($handle)) {
+      if (is_dir("$parent/$dir") && !in_array($dir, array('.', '..', 'CVS'))) {
+        $include = "$parent/$dir/ad_weight_$dir.inc";
+        if (file_exists($include)) {
+          require_once($include);
+          $function = "ad_weight_{$dir}_select_ad";
+          if (function_exists($function)) {
+            $return = $function($ads, $quantity, $invalid);
+            // First come, first serve.  We found an ad_weight function that
+            // returned something, so we'll take it.
+            if ($return) {
+              return $return;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+ */
+
+/**
+ * Simple default function to randomly select an ad.  Provides a hook to allow
+ * the definition of external display methods.
+ * @param An array of valid ad IDs, ie array(5, 8, 9, 11).
+ * @param Optional, how many unique ads to select.
+ * @param Optional, an array of invalid IDs.
+ */
+function adserve_select_ad($ads, $quantity = 1, $invalid = array()) {
+  //adserve_invoke_weight($ads, $quantity, $invalid);
+
+  $ids = array();
+  $id = 0;
+  $total = sizeof($ads);
+  _debug_echo("Selecting $quantity ad(s) from $total total ad(s).");
+  if (is_array($ads)) {
+    $ads = adserve_select_reindex($ads, $invalid);
+    $total = sizeof($ads);
+    for ($i = 0; $i < $quantity; $i++) {
+      _debug_echo('Randomly selecting ad: '. ($i + 1) ." of $quantity.");
+      $id = 0;
+      // Randomly select a unique banner to display.  We subtract 1 as arrays
+      // start at 0.
+      $return = adserve_invoke_hook('adserve_select', $ads, $invalid);
+      if (is_array($return) && !empty($return)) {
+        foreach ($return as $id) {
+          // First come first serve.
+          if ((int)$id) break;
+        }
+      }
+      if ($id >= 0 && sizeof($ads)) {
+        if ($id == 0) {
+          _debug_echo("Default ID selection in adserve.inc.");
+          $id = $total > 1 ? $ads[mt_rand(0, $total - 1)] : $ads[0];
+          _debug_echo("Randomly selected ID: $id.");
+        }
+        if ($id > 0) {
+          $ids[] = $id;
+        }
+      }
+      else {
+        // There are no more valid advertisements left to display.
+        break;
+      }
+      $invalid[] = $id;
+      $ads = adserve_select_reindex($ads, $id);
+      $total = sizeof($ads);
+      // We're out of ads to display.
+      if ($total <= 0) {
+        break;
+      }
+    }
+  }
+  return $ids;
+}
+
+/**
+ * Include Drupal's bootstrap.inc.
+ */
+function adserve_include_drupal() {
+  // For optimal performance set DRUPAL_ROOT at the top of this file.
+  if (defined('DRUPAL_ROOT')) {
+    if (is_dir(DRUPAL_ROOT) && file_exists(DRUPAL_ROOT .'/includes/bootstrap.inc')) {
+      chdir(DRUPAL_ROOT);
+      adserve_variable('root_dir', DRUPAL_ROOT);
+    }
+    else {
+      echo 'Invalid DRUPAL_ROOT ('. DRUPAL_ROOT .') defined in adserve.inc';
+    }
+  }
+  else {
+    $path = explode('/', adserve_variable('ad_dir'));
+    while (!empty($path)) {
+      // Search for top level Drupal directory to perform bootstrap.
+      chdir(implode('/', $path));
+      if (file_exists('./includes/bootstrap.inc')) {
+        adserve_variable('root_dir', getcwd());
+        break;
+      }
+      array_pop($path);
+    }
+  }
+  require_once adserve_variable('root_dir') .'/includes/bootstrap.inc';
+}
+
+/**
+ * Include the necessary files and call the Drupal bootstrap.
+ */
+function adserve_bootstrap($bootstrap = NULL) {
+  adserve_include_drupal();
+
+  // If no specific bootstrap is specified, do a full bootstrap.
+  if (!isset($bootstrap)) {
+    $bootstrap = DRUPAL_BOOTSTRAP_FULL;
+  }
+
+  if (adserve_variable('debug')) {
+    echo "Drupal bootstrap '". $bootstrap ."'.<br />\n";
+  }
+
+  drupal_bootstrap($bootstrap);
+}
+
+/**
+ * Increment ad counters.  Increment in cache if enabled.
+ */
+function adserve_increment($ad, $action = 'view') {
+  $cache = adserve_variable('adcache');
+  if (adserve_variable('debug')) {
+    echo "adserve_increment action($action) cache($cache)<br />\n";
+  }
+  if (is_object($ad) && isset($ad->aid)) {
+    $aid = $ad->aid;
+  }
+  else {
+    $aid = 0;
+  }
+  if ($cache != 'none') {
+    $rc = adserve_invoke_file("ad_cache_{$cache}_increment", $action, $aid);
+    if ($rc) return;
+  }
+  adserve_bootstrap();
+  // Update impressions statistics.
+  db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE aid = %d AND action = '%s' AND date = %d AND adgroup = '%s' AND hostid = '%s'", $aid, $action, date('YmdH'), adserve_variable('group'), adserve_variable('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, adgroup, hostid, count) VALUES(%d, %d, '%s', '%s', '%s', 1)", $aid, date('YmdH'), $action, adserve_variable('hostid'), adserve_variable('hostid'));
+    // 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 impression.
+    if (!db_affected_rows()) {
+      db_query("UPDATE {ad_statistics} SET count = count + 1 WHERE aid = %d AND action = '%s' AND date = %d AND adgroup = '%s' AND hostid = '%s'", $aid, $action, date('YmdH'), adserve_variable('group'), adserve_variable('hostid'));
+    }
+  }
+
+  if ($action == 'view') {
+    // See if we need to perform additional queries.
+    if (isset($ad->maxviews) && $ad->maxviews > 0) {
+      $views = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, date('YmdH', $ad->activated)));
+      if ($views >= $ad->maxviews) {
+        db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+        ad_statistics_increment($aid, 'autoexpired');
+        ad_statistics_increment($aid, 'expired');
+      }
+    }
+  }
+  // TODO: Do we need to do this here?  Can it happen when a new click is
+  // registered?
+  if (isset($ad->maxclicks) && $ad->maxclicks > 0) {
+    $clicks = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, date('YmdH', $ad->activated)));
+    if ($clicks >= $ad->maxclicks) {
+      db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+      ad_statistics_increment($aid, 'autoexpired');
+      ad_statistics_increment($aid, 'expired');
+    }
+  }
+}
+
+/**
+ * Display additional debug information.
+ */
+function adserve_debug() {
+  if (adserve_variable('debug')) {
+    echo "Root drupal directory detected as '". adserve_variable('root_dir') ."'.<br />\n<br />\n";
+
+    $ad_dir = adserve_variable('ad_dir');
+    $files = array("$ad_dir/serve.php", "$ad_dir/ad.module");
+    if (adserve_variable('debug') > 2) {
+      $files = array_merge($files, array("$ad_dir/ad.install"));
+    }
+    if (adserve_variable('debug') > 3) {
+      $files = array_merge($files, array("$ad_dir/image/ad_image.module", "$ad_dir/image/ad_image.install", "$ad_dir/text/ad_text.module", "$ad_dir/text/ad_text.install", "$ad_dir/embed/ad_embed.module", "$ad_dir/report/ad_report.module", "$ad_dir/notify/ad_notify.module", "$ad_dir/notify/ad_notify.install"));
+    }
+    foreach ($files as $file) {
+      if (!file_exists($file)) {
+        echo "Error: '$file' does not exist!<br />\n";
+      }
+      else if (!is_readable($file)) {
+        echo "Error: '$file' is not readable!<br />\n";
+      }
+      else {
+        $fd = fopen($file, 'r');
+        while (!feof($fd)) {
+          $line = fgets($fd);
+          if (substr($line, 0, 5) == "<?php") {
+            continue;
+          }
+          else {
+            echo "$file: $line<br />";
+            break;
+          }
+        }
+      }
+    }
+    echo "<br />\n";
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/file/ad_cache_file.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,426 @@
+<?php
+// $Id: ad_cache_file.inc,v 1.1.4.23.2.5 2009/02/16 23:12:28 jeremy Exp $
+
+/**
+ * @file
+ * A plug in for the ad.module, providing a file cache mechanism for improved
+ * performance when displaying ads.
+ *
+ * TODO: Add support for tracking impressions/clicks with group-level
+ * granularity.
+ *
+ * Copyright (c) 2007-2009.
+ *  Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * This is the actual cache function called by adserve.php that displays ads
+ * without bootstrapping Drupal.
+ */
+function ad_cache_file() {
+  static $displayed_count = 0;
+
+  _debug_echo('File cache: entering.');
+
+  $cache_file = ad_cache_file_get_lock();
+
+  $output = '';
+  if ($cache_file) {
+    // Read entire cache file into memory.
+    $cache = unserialize(fread(adserve_variable('fd'), filesize($cache_file)));
+    // Store cache in a static variable for re-use by other functions.
+    ad_cache_file_ro_cache($cache);
+    /**
+     * The cache structure looks like this:
+     * $cache['ad'][$aid]['display'] = $ad
+     * $cache['hostid'][$hostid] = TRUE
+     * $cache['ad'][$aid][$hostid]['counts'][$action][$timestamp] = $counter
+     * $cache['last_sync'] = $timestamp
+     */
+
+    /**
+     * Initial support for over-riding default functionality when serving ads.
+     */
+    $includes = array('include_file_init', 'include_file_select');
+    foreach ($includes as $include) {
+      if (isset($cache[$include])) {
+        $include_file = adserve_variable('root_dir') .'/'. $cache[$include];
+        if (file_exists($include_file) && is_file($include_file)) {
+          _debug_echo("File cache: including external file: '$include_file'.");
+          include_once($include_file);
+        }
+        else {
+          _debug_echo("File cache: unable to find external file: '$include_file'.");
+        }
+
+        switch ($include) {
+          case 'include_file_init':
+            $include_func_init = $cache['include_func_init'];
+            _debug_echo("File cache: include_func_init: '$include_func_init'.");
+            break;
+          case 'include_file_select':
+            adserve_variable('include_func_select', $cache['include_func_select']);
+            _debug_echo("File cache: include_func_select: '". adserve_variable('include_func_select') ."'");
+            break;
+        }
+      }
+    }
+
+    $hostid = adserve_variable('hostid') ? adserve_variable('hostid') : 'none';
+    _debug_echo("File cache: using hostid: '$hostid'.");
+    if ($hostid != 'none' && !isset($cache['hostid'][$hostid])) {
+      _debug_echo("File cache: invalid hostid: '$hostid'.");
+      $output = 'You do not have permission to display ads.';
+    }
+    else {
+      $last_sync = $cache['last_sync'];
+      $lifetime = $cache['lifetime'];
+      $time = time();
+      $timestamp = date('YmdH');
+
+      // Allow external plug-ins to initialize these values...
+      if (isset($include_func_init) && function_exists($include_func_init)) {
+        $init = $include_func_init($cache, $hostid);
+      }
+      if (!empty($init)) {
+        _debug_echo('File cache: initialized externally.');
+        $quantity = $init['quantity'];
+        $id = $init['id'];
+        $type = $init['type'];
+        $aids = explode(',', $id);
+        $cache_size = sizeof($aids);
+        adserve_variable('group', "e");
+      }
+      else {
+        $quantity = adserve_variable('quantity');
+        if (isset($cache['hostid'][$hostid]['aids'])) {
+          $id = $cache['hostid'][$hostid]['aids'];
+          $type = 'host';
+          $aids = explode(',', $id);
+          $cache_size = sizeof($aids);
+          adserve_variable('group', "h$id");
+        }
+        else if (adserve_variable('nids')) {
+          $id = adserve_variable('nids');
+          $type = 'nids';
+          $aids = explode(',', $id);
+          $cache_size = sizeof($aids);
+          adserve_variable('group', "n$id");
+        }
+        else if (adserve_variable('tids')) {
+          $id = adserve_variable('tids');
+          $type = 'tids';
+          if (!isset($cache['tids'][$id])) {
+            $cache['tids'][$id] = array();
+            $tids = explode(',', $id);
+            foreach ($tids as $tid) {
+              if (is_array($cache['tid'][$tid]['aid'])) {
+                $cache['tids'][$id] += $cache['tid'][$tid]['aid'];
+              }
+            }
+            // Rebuild keys from 0, cache it for re-use on next ad display
+            $cache['tids'][$id] = array_values($cache['tids'][$id]);
+          }
+          $cache_size = sizeof($cache['tids'][$id]);
+          $aids = $cache['tids'][$id];
+          adserve_variable('group', "t$id");
+        }
+        else {
+          $id = 0;
+          $type = 'default';
+          $cache_size = sizeof($cache['tid'][0]['aid']);
+          $aids = $cache['tid'][0]['aid'];
+          adserve_variable('group', "$id");
+        }
+      }
+
+      if (adserve_variable('debug')) {
+        echo "File cache: last sync: $last_sync<br />\n";
+        echo "File cache: current time: $time<br />\n";
+        if ($time - $lifetime >= $last_sync) {
+          echo "File cache: will rebuild cache now.<br />\n";
+        }
+        else {
+          $seconds = $last_sync - $time + $lifetime;
+          echo "File cache: will rebuild cache in $seconds seconds.<br/>\n";
+        }
+        echo "File cache: timestamp: $timestamp<br />\n";
+        echo "File cache: cache_size($cache_size)<br />\n";
+      }
+
+      $ids = adserve_variable("$type-ids");
+      if ($ids == NULL) {
+        $ids = array();
+      }
+      _debug_echo('File cache: size of $ids: '. sizeof($ids));
+
+      // Only include aids that are in our cache, others are not valid in our
+      // context.
+      $search = array();
+      if (is_array($aids)) {
+        foreach ($aids as $aid) {
+          if (isset($cache['ad'][$aid])) {
+            $search[] = $aid;
+          }
+        }
+      }
+
+      $selected = adserve_select_ad($search, $quantity, $ids);
+      adserve_variable("$type-ids", array_merge($selected, $ids));
+      foreach ($selected as $aid) {
+        $aid = (int)$aid;
+        $ad = $cache['ad'][$aid];
+
+        if (!empty($output)) {
+          // Add a div between ads that themers can use to arrange ads when
+          // displaying more than one at a time.
+          $displayed_count++;
+          $output .= "<div class=\"advertisement-space\" id=\"$id-$displayed_count\"></div>";
+        }
+        $output .= $ad['display'];
+
+        _debug_echo("File cache: displaying AID: $aid");
+
+        // If displaying an ad, increment appropriate impressions counter.
+        // Otherwise, simply increment a counter.
+        $action = $aid ? 'view' : 'count';
+
+        // Increment counter.
+        if (isset($cache['ad'][$aid][$hostid]) &&
+            isset($cache['ad'][$aid][$hostid]['counts'][$action]) &&
+            isset($cache['ad'][$aid][$hostid]['counts'][$action][$timestamp])) {
+          $cache['ad'][$aid][$hostid]['counts'][$action][$timestamp]++;
+        }
+        else {
+          $cache['ad'][$aid][$hostid]['counts'][$action][$timestamp] = 1;
+        }
+      }
+
+      // Write updated cache back to file and release the lock.
+      $cache = serialize($cache);
+      // Store updated cache in a static variable for re-use by other functions.
+      ad_cache_file_ro_cache($cache);
+      rewind(adserve_variable('fd'));
+      ftruncate(adserve_variable('fd'), 0);
+      fwrite(adserve_variable('fd'), $cache, strlen($cache));
+      flock(adserve_variable('fd'), LOCK_UN);
+      fclose(adserve_variable('fd'));
+      adserve_variable('fd', '');
+
+      // Every $lifetime seconds we flush the cache files to the database.
+      if ($last_sync < time() - $lifetime) {
+        ad_cache_file_rebuild();
+      }
+    }
+  }
+  else {
+    $output = 'Configuration error, failed to lock cache file.';
+    if (ad_cache_file_rebuild()) {
+      // Required function was missing, the file cache must be disabled, so
+      // return nothing allowing adserve.inc to use the default display method.
+      return;
+    }
+  }
+  if (empty($output)) {
+    adserve_variable('error', TRUE);
+    $output = 'No active ads were found in the '. (empty($nids) ? 'tids' : 'nids') ." '$id'.";
+    if (adserve_variable('debug')) {
+      echo "$output<br />\n";
+    }
+  }
+  return $output;
+}
+
+function ad_cache_file_get_lock() {
+  static $lock = FALSE;
+  static $cache_file = '';
+
+  if ($lock) {
+    _debug_echo('File cache: already have lock.');
+    return $cache_file;
+  }
+
+  // We'll loop through all possible cache files until we obtain an
+  // exclusive lock.
+  for ($i = 1; $i <= adserve_variable('files'); $i++) {
+    // Prefix the filename with a '.' to hide it on Unix systems.
+    $cache_file = adserve_variable('root_dir') .'/'. adserve_variable('path') .'/.'. $i .'.ad.cache';
+
+    if (adserve_variable('debug')) {
+      echo "Trying cache_file '$cache_file'.<br />\n";
+    }
+
+    if (!$fd = @fopen($cache_file, 'r+')) {
+      if (adserve_variable('debug')) {
+        echo "Failed to open cache_file '$cache_file'.<br />\n";
+      }
+      // We failed to open the cache file, try the next one.
+      continue;
+    }
+    if ($i < adserve_variable('files')) {
+      // This isn't the last available cache file so we'll use a
+      // non-blocking lock for best performance.  If we fail to lock this
+      // cache file, we'll quickly move on to the next until we find an
+      // available one.
+      if (!flock($fd, LOCK_EX|LOCK_NB)) {
+        if (adserve_variable('debug')) {
+          echo "Failed to obtain non-blocking lock.<br />\n";
+        }
+        // We failed to obtain an exclusive lock, close the file and try the
+        // next one.
+        @fclose($fd);
+        continue;
+      }
+      if (adserve_variable('debug')) {
+        echo "Obtained lock.<br />\n";
+        $stat = fstat($fd);
+        echo 'File size: '. $stat['size'] .'<br />';
+      }
+      $lock = TRUE;
+      break;
+    }
+    else {
+      // This is the last available cache file, we'll use a blocking lock
+      // as we have to wait until we have exclusive write permissions.
+      if (!flock($fd, LOCK_EX)) {
+        if (adserve_variable('debug')) {
+          echo "Failed to obtain blocking lock.<br />\n";
+        }
+        // A blocking exclusive lock shouldn't ever fail, so something has
+        // gone very wrong.  Perhaps the file was deleted out from under us?
+        @fclose($fd);
+        continue;
+      }
+      if (adserve_variable('debug')) {
+        echo "Obtained lock on final cache file.<br />\n";
+      }
+      $lock = TRUE;
+      break;
+    }
+  }
+  if ($lock) {
+    adserve_variable('fd', $fd);
+    return $cache_file;
+  }
+  else {
+    return NULL;
+  }
+}
+
+function ad_cache_file_increment($action = 'view', $aid) {
+  _debug_echo("File cache: incrementing '$action'.");
+  $cache_file = ad_cache_file_get_lock();
+  if ($cache_file) {
+    $cache = unserialize(fread(adserve_variable('fd'), filesize($cache_file)));
+
+    $hostid = adserve_variable('hostid') ? adserve_variable('hostid') : 'none';
+    $aid = adserve_variable('aid');
+    if ($action == 'view') {
+      if ($hostid != 'none' && !isset($cache['hostid'][$hostid])) {
+        _debug_echo("File cache: invalid hostid: $hostid");
+        $output = 'You do not have permission to display images.';
+        // TODO: We should still log this.  Perhaps a new $action type?
+        return -1;
+      }
+    }
+    $last_sync = $cache['last_sync'];
+    $lifetime = $cache['lifetime'];
+    $time = time();
+    $timestamp = date('YmdH');
+
+    // Increment action counter.
+    if (isset($cache['ad'][$aid][$hostid]) &&
+        isset($cache['ad'][$aid][$hostid]['counts'][$action]) &&
+        isset($cache['ad'][$aid][$hostid]['counts'][$action][$timestamp])) {
+      $cache['ad'][$aid][$hostid]['counts'][$action][$timestamp]++;
+    }
+    else {
+      $cache['ad'][$aid][$hostid]['counts'][$action][$timestamp] = 1;
+    }
+
+    // Write updated cache back to file and release the lock.
+    $cache = serialize($cache);
+    rewind(adserve_variable('fd'));
+    ftruncate(adserve_variable('fd'), 0);
+    fwrite(adserve_variable('fd'), $cache, strlen($cache));
+    flock(adserve_variable('fd'), LOCK_UN);
+    fclose(adserve_variable('fd'));
+    adserve_variable('fd', '');
+
+    // Every $lifetime seconds we flush the cache files to the database.
+    if ($last_sync < time() - $lifetime) {
+      ad_cache_file_rebuild();
+    }
+    return 1;
+  }
+
+  return 0;
+}
+
+/**
+ *  Additional variables required by the filecache.
+ */
+function ad_cache_file_variables() {
+  // paths are comprised of alphanumerics, underscores, dashes, periods and
+  // slashes.
+  $variables = array();
+  $variables['path'] = isset($_GET['p']) ? preg_replace('/[^_\-\.\/\\0-9a-zA-Z]/', '', $_GET['p']) : 'files';
+  // files is an integer.
+  $variables['files'] = isset($_GET['f']) ? (int)$_GET['f'] : 1;
+  if ($variables['files'] > 15) {
+      echo "Invalid value 'f=". $variables['files'] ."', exiting.<br />\n";
+      exit();
+  }
+  return $variables;
+}
+
+/**
+ * Bootstrap drupal, then run ad_cache_file_build() from ad.module which will
+ * rebuild all cache files.
+ */
+function ad_cache_file_rebuild() {
+  adserve_bootstrap();
+  if (function_exists('ad_cache_file_build')) {
+    ad_cache_file_build();
+    return 0;
+  }
+  else {
+    return -1;
+  }
+}
+
+/**
+ *
+ */
+function ad_cache_file_ro_cache($cache = array()) {
+  static $ro_cache = array();
+
+  if (!empty($cache)) {
+    $ro_cache = $cache;
+  }
+  return $ro_cache;
+}
+
+/**
+ *
+ */
+function ad_cache_file_adserve_select($ads, $invalid) {
+  if ($include_func_select = adserve_variable('include_func_select')) {
+    _debug_echo("File cache: adserve_select: invoking '$include_func_select()'");
+    if (function_exists($include_func_select)) {
+      $cache = ad_cache_file_ro_cache();
+      if (!empty($cache)) {
+        return $include_func_select($ads, $invalid, $cache);
+      }
+      else {
+        _debug_echo("File cache: unexpected error: cache variable empty.");
+      }
+    }
+    else {
+      _debug_echo("File cache: adserve_select: '$include_func_select()' not found");
+    }
+  }
+  else {
+    _debug_echo("File cache: adserve_select: no select function defined");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/file/ad_cache_file.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_cache_file.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = File cache
+package = Ad
+dependencies[] = ad
+description = Provides a file-caching mechanism to improve ad serving performance.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/file/ad_cache_file.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,232 @@
+<?php
+// $Id: ad_cache_file.module,v 1.1.4.18.2.5 2009/02/16 23:12:28 jeremy Exp $
+
+/**
+ * @file
+ * A plug in for the ad.module, providing a file cache mechanism for improved
+ * performance when displaying ads.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_cache_file_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'admin/modules#description':
+      $output = t('Can improve the performance of the ad module utilizing file caching.');
+      break;
+
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_adcacheapi().
+ */
+function ad_cache_file_adcacheapi($op, &$node = array()) {
+  switch ($op) {
+    case 'display_variables':
+      return array(
+        'f' => variable_get('ad_files', 3),
+        'p' => file_create_path(),
+      );
+    case 'method':
+      return array('file' => t('File'));
+    case 'description':
+      return t('File based caching will usually offer better performance, however, some find it difficult to enable and it may not offer valid statistics if you are using multiple load balanced web servers.');
+    case 'settings':
+      $form = array();
+      $form['file'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('File cache settings'),
+        '#collapsible' => TRUE,
+        '#collapsed' => (variable_get('ad_cache', 'none') == 'file') ? FALSE : TRUE,
+      );
+      $form['file']['ad_files'] = array(
+        '#type' => 'select', 
+        '#title' => t('Number of cache files'), 
+        '#default_value' => variable_get('ad_files', 3), 
+        '#options' => drupal_map_assoc(array(1, 3, 5, 10, 15)), 
+        '#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.  This configuration option is only relevant if the file cache is enabled.')
+      );
+      $period = drupal_map_assoc(array(15,30,60,600,1800,3600,21600,43200,86400), 'format_interval');
+      $form['file']['ad_cache_file_lifetime'] = array(
+        '#type' => 'select', 
+        '#title' => t('Cache lifetime'), 
+        '#default_value' => variable_get('ad_cache_file_lifetime', 60), 
+        '#options' => $period,
+        '#description' => t('Specify how long information should be cached before ad statistics are updated in the database.  Increasing the cache lifetime can improve overall performance.  This configuration options is only relevant if the file cache is enabled.'),
+      );
+      return $form;
+    case 'settings_submit':
+      variable_set('ad_cache_file_lifetime', $node['ad_cache_file_lifetime']);
+      if ($node['ad_cache'] != 'file') {
+        ad_cache_file_build(0, variable_get('ad_files', 3));
+      }
+      else {
+        ad_cache_file_build($node['ad_files'], variable_get('ad_files', 3));
+      }
+      variable_set('ad_files', $node['ad_files']);
+      break;
+
+    case 'insert':
+    case 'update':
+    case 'delete':
+      if (variable_get('ad_cache', 'none') == 'file') {
+        ad_cache_file_build();
+      }
+      break;
+  }
+}
+
+/**
+ * Build all required cache files when using the file cache.
+ */
+function ad_cache_file_build($new_files = 0, $old_files = 0) {
+  $files = max($new_files, $old_files);
+  $files = $files ? $files : variable_get('ad_files', 3);
+  $new_cache = serialize(_ad_build_cache());
+  for ($i = 1; $i <= $files; $i++) {
+    $cache_file = file_create_path(".$i.ad.cache");
+    if (!file_exists($cache_file)) {
+      // Create the cache file.
+      file_save_data($new_cache, $cache_file, FILE_EXISTS_REPLACE);
+    }
+    else {
+      if (!$fd = @fopen($cache_file, 'r+')) {
+        drupal_set_message(t('Ad module failed to access cache <em>%file</em>.  Verify file permissions.', array('%file' => $cache_file)), 'error');
+        continue;
+      }
+      // Block until we get an exclusive lock on the cache file.
+      flock($fd, LOCK_EX);
+
+      // Read the entire cache file into memory.
+      $cache = unserialize(file_get_contents($cache_file));
+      if ($cache && isset($cache['ad'])) {
+        foreach ($cache['ad'] as $aid => $host) {
+          foreach ($host as $hostid => $ad) {
+            $hostid = ($hostid == 'none') ? '' : $hostid;
+            if (isset($ad['counts']) && is_array($ad['counts'])) {
+              foreach ($ad['counts'] as $action => $counts) {
+                foreach ($counts as $timestamp => $count) {
+                  db_query("UPDATE {ad_statistics} SET count = count + %d WHERE aid = %d AND action = '%s' AND date = %d AND hostid = '%s'", $count, $aid, $action, $timestamp, $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, hostid, count) VALUES(%d, %d, '%s', '%s', %d)", $aid, $timestamp, $action, $hostid, $count);
+                    // If another process already added this row our INSERT will
+                    // fail, if so we still need to increment it so we don't 
+                    // loose a count.
+                    if (!db_affected_rows()) {
+                      db_query("UPDATE {ad_statistics} SET count = count + %d WHERE aid = %d AND action = '%s' AND date = %d AND hostid = '%s'", $count, $aid, $action, $timestamp, $hostid);
+                    }
+                  }
+                }
+              }
+              // If counting ad impressions, see if we've hit a limit
+              if ($action = 'view') {
+                $limits = db_fetch_object(db_query('SELECT activated, maxviews, maxclicks, adstatus FROM {ads} WHERE aid = %d', $aid));
+                if ($limits->adstatus == 'active') {
+                  if ($limits->maxviews) {
+                    $views = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, date('YmdH', $limits->activated)));
+                    if ($views >= $limits->maxviews) {
+                      db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+                      ad_statistics_increment($aid, 'autoexpired');
+                      ad_statistics_increment($aid, 'expired');
+                    }
+                  }
+                  if ($limits->maxclicks) {
+                    $clicks = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, date('YmdH', $limits->activated)));
+                    if ($clicks >= $limits->maxclicks) {
+                      db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+                      ad_statistics_increment($aid, 'autoexpired');
+                      ad_statistics_increment($aid, 'expired');
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+      // This will rebuild a new fresh cache file, and release the lock
+      if ($old_files && $i > $new_files) {
+        unlink($cache_file);
+      }
+      else {
+        file_save_data($new_cache, $cache_file, FILE_EXISTS_REPLACE);
+      }
+    }
+  }
+}
+
+/**
+ * Returns the cache structure:
+ *
+ *   // The ad html.
+ *   $cache['ad'][$aid]['display'] = $ad
+ *   // Impressions counter.
+ *   $cache['ad'][$aid][$hostid]['view']
+ *   // Ad type.
+ *   $cache['ad'][$aid]['adtype'] = $adtype
+ *   // Synchronization timestamp.
+ *   $cache['last_sync'] = $timestamp
+ *
+ *   // Owner ID index.
+ *   $cache['uid'][$uid]['aid'][] = $aid
+ *   $cache['ad'][$aid]['uid'][] = $uid;
+ *   // Host ID index.
+ *   $cache['uid'][$uid]['hostid'] = $hostid
+ */
+function _ad_build_cache() {
+  $cache = array();
+
+  $result = db_query("SELECT aid FROM {ads} WHERE adstatus = 'active' OR adstatus = 'approved' OR adstatus = 'offline'");
+  while ($ad = db_fetch_object($result)) {
+    $node = node_load($ad->aid);
+    // Ad information.
+    $cache['ad'][$ad->aid]['display'] = module_invoke("ad_$node->adtype", 'display_ad', $node);
+    $cache['ad'][$ad->aid]['adtype'] = $node->adtype;
+    $cache['ad']['aid'][] = $node->aid;
+
+    // Owner indexes.
+    // TODO: Disable this query if ad_remote isn't enabled?
+    $owners = db_query('SELECT o.uid, h.hostid FROM {ad_owners} o LEFT JOIN {ad_hosts} h ON o.uid = h.uid WHERE aid = %d', $ad->aid);
+    $counter = 0;
+    while ($owner = db_fetch_object($owners)) {
+      $cache['uid'][$owner->uid]['aid'][] = $ad->aid;
+      $cache['ad'][$ad->aid]['uid'][] = $owner->uid;
+      $cache['ad'][$ad->aid][$owner->hostid]['view'] = array();
+    }
+
+    // Taxonomy index.
+    $terms = db_query('SELECT tid FROM {term_node} WHERE nid = %d', $ad->aid);
+    $match = FALSE;
+    while ($term = db_fetch_object($terms)) {
+      $cache['tid'][$term->tid]['aid'][$ad->aid] = $ad->aid;
+      $match = TRUE;
+    }
+    if (!$match) {
+      $cache['tid'][0]['aid'][] = $ad->aid;
+    }
+  }
+
+  // HostID index
+  $owners = db_query('SELECT uid, hostid FROM {ad_hosts}');
+  while ($owner = db_fetch_object($owners)) {
+    $cache['uid'][$owner->uid]['hostid'] = $owner->hostid;
+    $cache['ad'][0][$owner->hostid]['count'] = array();
+    if (($user = user_load(array('uid' => $owner->uid))) &&
+        (user_access('host remote advertisements', $user))) {
+      $cache['hostid'][$owner->hostid] = TRUE;
+    }
+  }
+
+  $cache = array_merge($cache, module_invoke_all('ad_build_cache'));
+  $cache['last_sync'] = time();
+  $cache['lifetime'] = variable_get('ad_cache_file_lifetime', 60);
+  return $cache;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/memcache/ad_cache_memcache.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,401 @@
+<?php
+// $Id: ad_cache_memcache.inc,v 1.1.2.6.2.6 2009/02/16 23:12:28 jeremy Exp $
+
+/**
+ * @file
+ * Memcache include.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * TODO: Debug Raw and IFrame display methods, neither currently seem to work
+ *       with this cache type.
+ */
+
+/**
+ * Called by adserve.inc, display an ad from memcache.
+ */
+function ad_cache_memcache() {
+  _debug_echo('Memcache: entering ad_cache_memcache().');
+
+  // TODO: Move the meat of this function into adserve.php, simplifying what
+  // cache plugins have to do and removing duplicated logic.
+  $init_cache = array();
+  $init_func = ad_cache_memcache_hook($init_cache, 'include_file_init', 'include_func_init');
+
+  $hostid = adserve_variable('hostid') ? adserve_variable('hostid') : 'none';
+  if ($hostid == 'none' || ad_memcache_get("ad-hostid-$hostid")) {
+    if (function_exists($init_func)) {
+      $init = $init_func($init_cache, $hostid);
+    }
+    if (!empty($init)) {
+      if (adserve_variable('debug')) {
+        echo "Memcache: initialized externally:<pre>\n";
+        print_r($init);
+        echo '</pre>';
+      }
+      $type = $init['type'];
+      $id = $init['id'];
+      $group = $init['group'];
+      $aids = explode(',', $id);
+      adserve_variable('quantity', $init['quantity']);
+    }
+    else {
+      if ($id = adserve_variable('nids')) {
+        $type = 'node';
+      }
+      else if ($id = adserve_variable('tids')) {
+        $type = 'taxonomy';
+      }
+      else {
+        $type = 'default';
+        $id = 0;
+      }
+      $aids = ad_cache_memcache_get_ids($type, $id);
+      $group = $id;
+    }
+    adserve_variable('group', $group);
+
+    if (adserve_variable('debug')) {
+      echo 'Memcache: selecting from the following ad id(s): ';
+      if (empty($aids)) {
+        echo 'none.<br />';
+      }
+      else {
+        echo implode(', ', $aids) .'.<br />';
+      }
+    }
+
+    $ids = adserve_variable("$type-ids");
+    if ($ids == NULL) {
+      $ids = array();
+    }
+
+    $output = '';
+    $selected = adserve_select_ad($aids, adserve_variable('quantity'), $ids);
+    adserve_variable("$type-ids", array_merge($selected, $ids));
+    foreach ($selected as $aid) {
+      if ($aid = (int)$aid) {
+        $ad = ad_cache_memcache_get_ad($aid);
+
+        if (!empty($output)) {
+          $display_count++;
+          $output .= "<div class=\"space\" id=\"$id-$displayed_count\"></div>";
+        }
+
+        $output .= $ad->display;
+      }
+      else {
+        $ad = array();
+      }
+
+      _debug_echo("Displaying AID: $aid.");
+      $action = $aid ? 'view' : 'count';
+      ad_cache_memcache_increment($action, $aid, $group, $hostid, $ad);
+    }
+    if (empty($output)) {
+      adserve_variable('error', TRUE);
+      $output = 'No active ads were found in the '. (empty($nids) ? 'tids' : 'nids') ." '$id'.";
+      _debug_echo("Memcache: {$output}");
+    }
+  }
+  else {
+    _debug_echo("Memcache: invalid hostid: '$hostid'.");
+    $output = 'You do not have permission to display ads.';
+  }
+
+  return $output;
+}
+
+function ad_cache_memcache_hook(&$cache, $hook, $func) {
+
+  if (empty($cache)) {
+    _debug_echo('Memcache: retrieving hook info from cache.');
+    $cache = ad_memcache_get('ad-cache-hook');
+  }
+  $include_func = NULL;
+  if (is_array($cache) && !empty($cache)) {
+    $include_file = adserve_variable('root_dir') .'/'. $cache[$hook];
+    if (file_exists($include_file) && is_file($include_file)) {
+      _debug_echo("Memcache: including external file: '$include_file'.");
+      include_once($include_file);
+    }
+    else if (is_file($include_file)) {
+      _debug_echo("Memcache: unable to find external file: '$include_file'.");
+    }
+    else {
+      _debug_echo('Memcache: no include file defined in cache.');
+    }
+    $include_func = $cache[$func];
+    if ($include_func) {
+      _debug_echo("Memcache: returning requested func($func): '$include_func'.");
+    }
+  }
+  return ($include_func);
+}
+
+function ad_cache_memcache_get_ids($op = 'default', $id = 0) {
+  switch ($op) {
+
+    case 'node': {
+      $ids = explode(',', $id);
+      break;
+    }
+
+    case 'taxonomy': {
+      $ids = ad_memcache_get("ad-taxonomy-cache-$id");
+      if (!$ids || empty($ids)) {
+        $taxonomy = ad_memcache_get('ad-taxonomy');
+        $cache = array();
+        $ids = explode(',', $id);
+        foreach ($ids as $tid) {
+          if (is_array($taxonomy[$tid])) {
+            $cache += $taxonomy[$tid];
+          }
+        }
+        // Rebuild keys from 0, cache for quick re-use on next ad display.
+        $ids = array_values($cache);
+        ad_memcache_set("ad-taxonomy-cache-$id", $ids);
+      }
+      break;
+    }
+
+    default: {
+      $taxonomy = ad_memcache_get('ad-taxonomy');
+      $ids = $taxonomy[0];
+      break;
+    }
+
+  }
+
+  return $ids;
+}
+
+function ad_cache_memcache_get_ad($aid) {
+  static $load = FALSE;
+
+  $ad = ad_memcache_get("ad-aid-$aid");
+
+  if (!$load && !is_object($ad)) {
+    $load = TRUE;
+    adserve_bootstrap();
+    $ad_memcache_build = variable_get('ad_memcache_build', '');
+    if ((time() - $ad_memcache_build) >= 60) {
+      ad_cache_memcache_build();
+    }
+  }
+
+  return $ad;
+}
+
+/**
+ * Increment impressions counter in memcache.
+ */
+function ad_cache_memcache_increment($action, $aid, $group = '', $hostid = '', $ad = array()) {
+  static $timestamp = NULL;
+
+  _debug_echo("Memcache: increment action($action) aid($aid) group($group) hostid($hostid).");
+
+  if ($aid && !is_object($ad)) {
+    _debug_echo("Invalid ad id: $aid.");
+    return (0);
+  }
+
+  if (!isset($timestamp)) {
+    $timestamp = date('YmdH');
+  }
+  $counters = ad_memcache_get("ad-counters-$aid");
+
+  $update = TRUE;
+  if (!is_array($counters) || !isset($counters["$action:$group:$hostid:$timestamp"])) {
+    _debug_echo("Memcache: adding map: action($action) aid($aid) group($group) hostid($hostid) timestamp($timestamp)");
+    ad_memcache_increment_map($action, $aid, $group, $hostid, $timestamp);
+  }
+
+  $rc = ad_memcache_increment("ad-$action-$aid-$group-$hostid-$timestamp");
+  _debug_echo("Memcache: incrementing ad-$action-$aid-$group-$hostid-$timestamp ($rc)");
+}
+
+/**
+ * The maximum time any process can hold a given lock, in seconds.
+ */
+define('AD_MEMCACHE_LOCK_LIMIT', 2);
+
+/**
+ * Store a value in memcache.
+ */
+function ad_memcache_set($key, $value, $timeout = 86400) {
+  $memcache = ad_memcache_init();
+
+  return $memcache->set($key, $value, MEMCACHE_COMPRESSED, $timeout);
+}
+
+/**
+ * Store a value in memcache.
+ */
+function ad_memcache_add($key, $value, $timeout = 86400) {
+  $memcache = ad_memcache_init();
+
+  return $memcache->add($key, $value, MEMCACHE_COMPRESSED, $timeout);
+}
+
+/**
+ * Get a value from memcache.
+ */
+function ad_memcache_get($key) {
+  $memcache = ad_memcache_init();
+
+  return $memcache->get($key);
+}
+
+/**
+ * Delete a value from memcache.
+ */
+function ad_memcache_delete($key) {
+  $memcache = ad_memcache_init();
+
+  return $memcache->delete($key);
+}
+
+/**
+ * Get a lock in memcache.
+ */
+function ad_memcache_lock($key, $wait = TRUE) {
+  $loop = 0;
+  $lock = FALSE;
+  while ($lock == FALSE) {
+    $lock = ad_memcache_add("LOCK-$key-LOCK", TRUE, AD_MEMCACHE_LOCK_LIMIT);
+    if (!$lock && $wait) {
+      if ($loop++ > 50) {
+        // Hard limit of 5 seconds, after which we fail to grab a lock.
+        return FALSE;
+      }
+      // Wait 1/10th of a second and try again.
+      usleep(100000);
+    }
+    else if (!$lock && !$wait) {
+      return FALSE;
+    }
+  }
+  return TRUE;
+}
+
+/**
+ * Release a lock in memcache.
+ */
+function ad_memcache_unlock($key) {
+  ad_memcache_delete("LOCK-$key-LOCK");
+}
+
+/**
+ * Increment a numerical value in memcache.
+ */
+function ad_memcache_increment($key, $value = 1) {
+  $memcache = ad_memcache_init();
+
+  $rc = $memcache->increment($key, $value);
+  if ($rc === FALSE) {
+    // We tried incrementing a counter that hasn't yet been initialized.
+    $rc = $memcache->set($key, $value);
+    if ($rc === FALSE) {
+      // Another process already initialized the counter, increment it.
+      $rc = $memcache->increment($key);
+    }
+  }
+  return $rc;
+}
+
+/**
+ * Decrement a numerical value in memcache.
+ */
+function ad_memcache_decrement($key, $value = 1) {
+  $memcache = ad_memcache_init();
+
+  $rc = $memcache->decrement($key, $value);
+  if ($rc === FALSE) {
+    // We tried incrementing a counter that hasn't yet been initialized.
+    $rc = $memcache->set($key, $value);
+    if ($rc === FALSE) {
+      // Another process already initialized the counter, increment it.
+      $rc = $memcache->decrement($key);
+    }
+  }
+  return $rc;
+}
+
+/**
+ * Update mapping which allows us to quickly find stats in memcache when
+ * feeding them into the database.
+ */
+function ad_memcache_increment_map($action, $aid, $group, $hostid, $timestamp) {
+  $key = "ad-counters-$aid";
+  if (ad_memcache_lock($key)) {
+    $counters = ad_memcache_get($key);
+    if (!is_array($counters) || !isset($counters["$action:$group:$hostid:$timestamp"])) {
+      $counters["$action:$group:$hostid:$timestamp"] = "$action:$group:$hostid:$timestamp";
+      ad_memcache_set($key, $counters);
+    }
+    ad_memcache_unlock($key);
+  }
+}
+
+/**
+ * Decrement a numerical value in memcache.
+ * TODO: Use the same configuration style as Drupal's memcache module,
+ * supporting multiple memcache servers, etc.
+ */
+function ad_memcache_init() {
+  static $memcache = NULL;
+
+  if (!$memcache) {
+    $memcache = new Memcache;
+    $memcache->addServer('localhost', 11211);
+  }
+  return $memcache;
+}
+
+/**
+ * Allow external ad selection logic.
+ */
+function ad_cache_memcache_adserve_select($ads, $invalid) {
+  $cache = array();
+  if ($select_func = ad_cache_memcache_hook($cache, 'include_file_select', 'include_func_select')) {
+    _debug_echo("Memcache: adserve_select: invoking '$select_func()'");
+    if (function_exists($select_func)) {
+      if (is_array($cache) && !empty($cache)) {
+        return $select_func($ads, $invalid, $cache);
+      }
+      else {
+        _debug_echo("Memcache: unexpected error: cache empty.");
+      }
+    }
+    else {
+      _debug_echo("Memcache: adserve_select: '$include_func_select()' not found");
+    }
+  }
+  else {
+    _debug_echo("Memcache: adserve_select: no select function defined");
+  }
+}
+
+/**
+ * Allow external exit text.
+ */
+function ad_cache_memcache_adserve_exit_text() {
+  $cache = array();
+  if ($exit_text_func = ad_cache_memcache_hook($cache, 'include_file_exit_text', 'include_func_exit_text')) {
+    _debug_echo("Memcache: adserve_exit_text: invoking '$exit_text_func()'");
+    if (function_exists($exit_text_func)) {
+      return $exit_text_func();
+    }
+    else {
+      _debug_echo("Memcache: adserve_exit_text: '$exit_text_func()' not found");
+    }
+  }
+  else {
+    _debug_echo("Memcache: adserve_exit_text: no exit_text function defined");
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/memcache/ad_cache_memcache.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_cache_memcache.info,v 1.1.2.2.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = Ad memcache cache
+package = Ad
+dependencies[] = ad
+description = Integrates the ad module with memcached, allowing ads to be served directly out of RAM.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cache/memcache/ad_cache_memcache.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,282 @@
+<?php
+// $Id: ad_cache_memcache.module,v 1.1.2.9.2.6 2009/02/16 23:12:28 jeremy Exp $
+
+/**
+ * @file
+ * A plug in for the ad.module, integrating the ad module with memcache.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_requirements().
+ */
+function ad_cache_memcache_requirements($phase = NULL) {
+  include_once './includes/install.inc';
+  // Connect to memcached so we can retrieve its version.
+  if (function_exists('memcache_add_server')) {
+    require_once(drupal_get_path('module', 'ad_cache_memcache') .'/ad_cache_memcache.inc');
+    $memcache = ad_memcache_init();
+    // Retrieve the version of memcache.
+    if (function_exists('memcache_get_version')) {
+      $severity = REQUIREMENT_OK;
+      $value = memcache_get_version($memcache);
+    }
+    else {
+      $severity = REQUIREMENT_ERROR;
+      $value = t('Memcache installation not valid, %function not found.', array('%function' => 'memcache_get_version'));
+    }
+  }
+  else {
+    $severity = REQUIREMENT_ERROR;
+    $value = t('Memcache is not installed, %function not found.', array('%function' => 'memcache_add_server'));
+  }
+
+  if ($phase) {
+    return array(
+      'memcache' => array(
+        'title' => t('Memcache'),
+        'value' => $value,
+        'severity' => $severity,
+      ),
+    );
+  }
+  else {
+    if ($severity == REQUIREMENT_OK) {
+      return TRUE;
+    }
+    else {
+      return $value;
+    }
+  }
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_cache_memcache_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'admin/modules#description':
+      $output = t('Utilize memcached to improve the performance of the ad module.');
+      break;
+
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_adcacheapi().
+ */
+function ad_cache_memcache_adcacheapi($op, &$node = array()) {
+  switch ($op) {
+    case 'method':
+      ad_cache_memcache_sync();
+      ad_cache_memcache_build();
+      return array('memcache' => t('Memcache'));
+    case 'description':
+      return t('Memcache allows improved performance by caching data directly in RAM.');
+    case 'settings':
+      $form['memcache'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Memcache settings'),
+        '#collapsible' => TRUE,
+        '#collapsed' => (variable_get('ad_cache', 'none') == 'memcache') ? FALSE : TRUE,
+      );
+      $period = drupal_map_assoc(array(60,120,180,240,300,600,900,1800,2700,3600,10800,21600,43200,86400), 'format_interval');
+      $form['memcache']['ad_cache_memcache_sync'] = array(
+        '#type' => 'select',
+        '#title' => t('Sync frequency'),
+        '#default_value' => variable_get('ad_cache_memcache_sync', 600),
+        '#options' => $period,
+        '#description' => t('Specify how often statistics stored in RAM should be synced to the database (requires cron runs with the same or greater frequency). The longer you store data in memcache, the more data you risk loosing in the event of a system failure. This configuration option is only relevant if memcache is enabled.'),
+      );
+      // Sanity tests...
+      if (variable_get('ad_cache', 'none') == 'memcache') {
+        $sync = variable_get('ad_cache_memcache_sync', 600);
+        $cron_last = variable_get('cron_last', NULL);
+        if (is_numeric($cron_last)) {
+          if (time() - $cron_last > $sync) {
+            drupal_set_message(t('Memcache warning:  your last cron run was !time ago.  Advertisement impressions data is only synchronized into the database when cron runs.  You are risking data loss.  To learn more about how Drupal cron works, please check the online help pages for <a href="@url">configuring cron jobs</a>.', array('@url' => 'http://drupal.org/cron', '!sync' => format_interval($sync), '!time' => format_interval(time() - $cron_last))), 'error');
+          }
+        }
+        else {
+          drupal_set_message(t('Memcache warning:  Cron has not run.  Advertisement impressions data is only synchronized into the database when cron runs.  You are risking data loss.  It appears cron jobs have not been setup on your system. Please check the help pages for <a href="@url">configuring cron jobs</a>.', array('@url' => 'http://drupal.org/cron')), 'error');
+        }
+      }
+      return $form;
+    case 'settings_submit':
+      variable_set('ad_cache_memcache_sync', $node['ad_cache_memcache_sync']);
+      break;
+
+    case 'insert':
+    case 'update':
+    case 'delete':
+      if (variable_get('ad_cache', 'none') == 'memcache') {
+        ad_cache_memcache_sync_ad($node->nid);
+        ad_cache_memcache_build($node);
+      }
+      break;
+  }
+}
+
+/**
+ * Regularily syncronize counters into RAM.
+ */
+function ad_cache_memcache_cron() {
+  $ad_memcache_timestamp = variable_get('ad_memcache_timestamp', '');
+  if ((time() - $ad_memcache_timestamp) >= variable_get('ad_cache_memcache_sync', 600)) {
+    ad_cache_memcache_sync();
+  }
+
+  $ad_memcache_build = variable_get('ad_memcache_build', '');
+  // rebuild cache every 12 hours
+  // TODO: Make configurable
+  if ((time() - $ad_memcache_build) >= 43200) {
+    ad_cache_memcache_build();
+  }
+}
+
+/**
+ * Load advertisements into memory.
+ */
+function ad_cache_memcache_sync() {
+  variable_set('ad_memcache_timestamp', time());
+  if (($error = ad_cache_memcache_requirements()) === TRUE) {
+    $result = db_query("SELECT aid, adtype FROM {ads} WHERE adstatus = 'active'");
+    while ($ad = db_fetch_object($result)) {
+      ad_cache_memcache_sync_ad($ad->aid);
+    }
+    // Sync counters.
+    ad_cache_memcache_sync_ad(0);
+  }
+  else {
+    drupal_set_message(t('!module: Unable to syncronize cache: !error', array('!module' => 'ad_cache_memcache.module', '!error' => $error)), 'error');
+  }
+}
+
+/**
+ * Syncronize counts for given advertisement with database.
+ */
+function ad_cache_memcache_sync_ad($aid) {
+  if (($error = ad_cache_memcache_requirements()) !== TRUE) {
+    drupal_set_message(t('!module: Unable to syncronize cache: !error', array('!module' => 'ad_cache_memcache.module', '!error' => $error)), 'error');
+    return;
+  }
+
+  if (!ad_memcache_lock("ad-counters-$aid")) {
+    // Another process is already updating these values.
+    return;
+  }
+  $counters = ad_memcache_get("ad-counters-$aid");
+  if (!is_array($counters)) {
+    ad_memcache_unlock("ad-counters-$aid");
+    // There's nothing currently in memory for this ad.
+    return;
+  }
+  ad_memcache_delete("ad-counters-$aid");
+  ad_memcache_unlock("ad-counters-$aid");
+  foreach ($counters as $map) {
+    list($action, $group, $hostid, $timestamp) = explode(':', $map);
+    if ($action && $group && $hostid && $timestamp) {
+      $count = ad_memcache_get("ad-$action-$aid-$group-$hostid-$timestamp");
+      if ($count) {
+        ad_memcache_decrement("ad-$action-$aid-$group-$hostid-$timestamp", $count);
+        db_query("UPDATE {ad_statistics} SET count = count + %d WHERE aid = %d AND action = '%s' AND date = %d AND adgroup = '%s' AND hostid = '%s'", $count, $aid, $action, $timestamp, $group, $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, adgroup, hostid, count) VALUES(%d, %d, '%s', '%s', '%s', %d)", $aid, $timestamp, $action, $group, $hostid, $count);
+          // If another process already added this row our INSERT will fail, if
+          // so we still need to increment it so we don't loose a count.
+          if (!db_affected_rows()) {
+            db_query("UPDATE {ad_statistics} SET count = count + %d WHERE aid = %d AND action = '%s' AND date = %d AND adgroup = '%s' AND hostid = '%s'", $count, $aid, $action, $timestamp, $group, $hostid);
+          }
+        }
+      }
+      // If counting ad impressions, see if we've hit a limit
+      if ($action = 'view') {
+        $limits = db_fetch_object(db_query('SELECT activated, maxviews, maxclicks, adstatus FROM {ads} WHERE aid = %d', $aid));
+        if ($limits->adstatus == 'active') {
+          if ($limits->maxviews) {
+            $views = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d", $aid, date('YmdH', $limits->activated)));
+            if ($views >= $limits->maxviews) {
+              db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+              ad_statistics_increment($aid, 'autoexpired');
+              ad_statistics_increment($aid, 'expired');
+            }
+          }
+          if ($limits->maxclicks) {
+            $clicks = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d", $aid, date('YmdH', $limits->activated)));
+            if ($clicks >= $limits->maxclicks) {
+              db_query("UPDATE {ads} SET adstatus = 'expired', autoexpire = 0, autoexpired = %d, expired = %d WHERE aid = %d", time(), time(), $aid);
+              ad_statistics_increment($aid, 'autoexpired');
+              ad_statistics_increment($aid, 'expired');
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Caches ad information into memory.
+*/
+function ad_cache_memcache_build($changed = NULL) {
+  variable_set('ad_memcache_build', time());
+  if (($error = ad_cache_memcache_requirements()) !== TRUE) {
+    drupal_set_message(t('!module: Unable to build cache: !error', array('!module' => 'ad_cache_memcache.module', '!error' => $error)), 'error');
+    return;
+  }
+
+  if (is_object($changed) && isset($changed->aid)) {
+    // An advertisement has changed, rebuild cache on next cron run.
+    variable_set('ad_memcache_build', '');
+  }
+  else {
+    // Rebuilding entire cache.
+    $result = db_query("SELECT aid, adtype, redirect FROM {ads} WHERE adstatus = 'active' OR adstatus = 'approved' OR adstatus = 'offline'");
+    while ($ad = db_fetch_object($result)) {
+      $node = node_load($ad->aid);
+      $ad->display = module_invoke("ad_$ad->adtype", 'display_ad', $node);
+      ad_memcache_set("ad-aid-$ad->aid", $ad);
+      $ads[] = $ad->aid;
+
+      // Owner indexes.
+      $ad_owners = db_query('SELECT o.uid, h.hostid FROM {ad_owners} o LEFT JOIN {ad_hosts} h ON o.uid = h.uid WHERE aid = %d', $ad->aid);
+      $counter = 0;
+      while ($owner = db_fetch_object($ad_owners)) {
+        $owners[$owner->uid][$ad->aid] = $ad->aid;
+        ad_memcache_set("ad-$ad->aid-uid", $owner->uid);
+      }
+
+      $match = FALSE;
+      // Taxonomy index.
+      $terms = db_query('SELECT tid FROM {term_node} WHERE nid = %d', $ad->aid);
+      while ($term = db_fetch_object($terms)) {
+        $taxonomy[$term->tid][$ad->aid] = $ad->aid;
+        $match = TRUE;
+      }
+      if (!$match) {
+        $taxonomy[0][] = $ad->aid;
+      }
+    }
+    ad_memcache_set("ad-ads", $ads);
+    ad_memcache_set("ad-owners", $owners);
+    ad_memcache_set("ad-taxonomy", $taxonomy);
+
+    // HostID index
+    $owners = db_query('SELECT uid, hostid FROM {ad_hosts}');
+    while ($owner = db_fetch_object($owners)) {
+      ad_memcache_set("ad-hosts-$owner->uid", $owner->hostid);
+      if (($user = user_load(array('uid' => $owner->uid))) &&
+          (user_access('host remote advertisements', $user))) {
+        ad_memcache_set("ad-hostid-$owner->hostid", TRUE);
+      }
+    }
+    // Always invoke hooks, they can decide to queue or act immediately.
+    ad_memcache_set('ad-cache-hook', module_invoke_all('ad_build_cache'));
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/ADSERVE_CONFIGURATION.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,18 @@
+It is possible to override adserve settings by adding them to the $conf array
+in settings.php.  Read more about how to override a variable in the comments
+within settings.php.
+
+Any adserve variable can be overridden in this way.
+
+What follows is a sample overidden settings.php conf array, including some
+non-adserve variables along with adserve variables:
+
+------------
+
+conf = array(
+  'site_name' => 'My Drupal site',
+  'theme_default' => 'minelli',
+  'anonymous' => 'Visitor',
+  'adcache' => 'none',
+  'method' => 'javascript',
+);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/CHANGELOG.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,163 @@
+$Id: CHANGELOG.txt,v 1.2.2.40.2.176.2.41 2009/02/17 19:26:51 jeremy Exp $
+
+February 17th, 2009 (1.1)
+ - ad.module
+    o bug #375742: support manual IFrame configuration
+    o feature #298608: return ad_link_attributes as array so ad_display_TYPE
+                   functions can call l() rather than custom building links
+ - ad.admin.inc
+    o feature #346454: replace "views" with "impressions"
+ - ad.install
+    o feature #298608: flush all caches for new ad display functions
+ - adserve.inc
+    o bug #375765: do not auto-expire ads unless configured to auto-expire
+    o bug #375377: remove references to no longer existing views files
+ - ad_external.module
+    o feature #298608: display external ad with themeable function
+ - ad_html.module
+    o feature #298608: display html ad with themeable function
+ - ad_image.module
+    o feature #298608: display image ad with themeable function
+ - ad_text.module
+    o feature #298608: display text ad with themeable function
+
+February 16th, 2009
+ - ad_text.module
+    o bug #375377: remove references to no longer existing ad_text_views.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_image.module
+    o bug #375377: remove references to no longer existing ad_image_views.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad.module
+    o bug #375270: cleanup code for E_ALL compliance (new patch by Webchick)
+    o bug #375474: don't automatically set autoactive/autoexpire date
+ - ad.admin.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_owners.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_embed.module
+    o bug #294035: fixup ad_embed module to work with Drupal 6
+ - ad.install
+    o bug #294035: rebuild menu so ad_embed settings page is visible
+
+February 16th, 2009 (1.0)
+ - ad.admin.inc
+    o bug #374044: when editing group, don't display "create group" button
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad.pages.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - adserve.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_cache_file.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_cache_file.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_cache_memcache.inc
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_cache_memcache.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_embed.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_remote.module
+    o bug #375221: generate valid HTML snippet
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_external.module
+    o bug #375221: generate valid HTML snippet
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_image.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_text.module
+    o bug #375241: allow ad owners to change ad status
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_html.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_remote.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_report.module
+    o bug #375270: cleanup code for E_ALL compliance
+ - ad_owners.module
+    o bug #375244: automatically make ad creator an ad owner with default perms
+    o bug #375270: cleanup code for E_ALL compliance
+ - click_filter.install
+    o bug #375270: cleanup code for E_ALL compliance
+ - click_filter.module
+    o bug #375270: cleanup code for E_ALL compliance
+
+February 12th, 2009 (1.0-rc2)
+ - ad_remote.info
+    o bug #294044: module dependent on ad_owners module
+ - ad.module
+    o bug #304326: fix permissions so user can view click details
+ - ad.pages.inc
+    o feature #304326: display username in overview and details
+ - ad_memcache.inc
+    o bug #294040: order of ad_memcache_set() parameters was swapped
+
+February 11th, 2009
+ - ad_text.module
+    o feature #373016: don't require destination URL, cleanup help text
+    o bug #373065: cleanup URL on ad node page
+    o bug #373046: allow roles with 'create advertisements' to create ads
+ - ad_image.module
+    o feature #373016: cleanup help text for destination URL
+    o bug #373065: cleanup URL on ad node page
+    o bug #373046: allow roles with 'create advertisements' to create ads
+ - ad_html.module
+    o bug #373046: allow roles with 'create advertisements' to create ads
+ - ad_external.module
+    o bug #373046: allow roles with 'create advertisements' to create ads
+ - ad_text.install
+    o bug #373057: uninstall fails to remove text ads
+ - ad_image.install
+    o bug #373057: uninstall fails to remove image ads
+ - ad_html.install
+    o bug #373057: uninstall fails to remove html ads
+ - adserve.inc
+    o bug #373065: do not add trailing slash if there is no hostid
+ - ad_cache_file.inc
+    o bug #373065: generate proper redirect link
+
+February 11th, 2009 (1.0-rc1)
+  SA-CONTRIB-2009-007: http://drupal.org/node/372977
+
+February 10th, 2009
+ - ad_notify.module
+    o bug #360432: don't display error if no notifications have been created
+ - ad.admin.inc
+    o bug #372211: fix multi-delete of advertisements via admin interface
+ - adserve.inc
+    o bug #277286: document how to set DRUPAL_ROOT for Windows servers, get
+      path directly from DURPAL_ROOT if set
+
+February 9th, 2009
+ - ad.module
+    o bug #369327: allow creation of advertisements
+ - ad_notify.info
+    o bug #370194: properly format dependencies
+ - ad_report.info
+    o properly format dependencies
+
+November 19th, 2008
+ - ad.module
+    o bug #329475: remove translations on internal status names
+ - ad_text.module
+    o bug #335466: fix unclosed div
+ - ad.install
+    o bug #330173: fix infinite loop when uninstalling the ad module
+ - click_filter.module
+    o bug #294037: remove calls to db_num_rows
+      (patch thanks to James Glasgow, aka jrglasgow)
+ - ad_report.module
+    o bug #330104: port module to Drupal 6
+      (patch by Mehmet Alkanlar, aka mehmeta)
+
+October 30th 2008
+ - ad.module
+    o bug #294029: Display theme, properly redirect
+ - click_filter.module
+    o bug #294037
+
+August 11th 2008
+Branched 6.x from 5.x-1.6
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/CONTRIBUTE.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,64 @@
+There are many ways that you can contribute to the development of the ad
+module:
+ 1. Testing.
+ 2. Documentation.
+ 3. Code.
+ 4. Money.
+
+
+1. Testing
+
+   The ad project is a large and complex collection of modules.  As new
+  features are added, unfortunately new bugs are sometimes added too.  And
+  unfortunately these bugs can go undetected in my development environement.
+  By installing the ad module on your server, testing it, and reporting any
+  problems that you run into, you help to improve the project.
+
+   Before submitting bug reports, please be sure that you are running the
+  latest version of the code, as your bug may have already been fixed.  Also,
+  please be sure to review the existing bug reports that other people have
+  already submitted so I don't have to waste time cleaning up duplicate bug
+  reports -- I maintain this project on a volunteer basis and already have
+  little enough time as is.  Finally, be sure to review the included
+  documentation to be sure that you have properly installed and configured the
+  module.
+
+   Bug reports can be viewed and submitted at the following URL:
+     http://drupal.org/project/issues/ad
+
+
+2. Documentation.
+
+   There is limited documentation built into the modules, and additional
+  documentation found in the documentation directory distributed with the ad
+  module.  However, documentation can always be improved.  You don't need to
+  know how to write code or create patches to help improve the documentation.
+  You can simply rewrite documentation files and attach them to the project's
+  issue queue at the following URL:
+     http://drupal.org/project/issues/ad
+
+
+3. Code.
+
+   There are plenty of issues listed in the project's queue waiting until I
+  find the time to code up fixes.  If you are a PHP coder yourself, it is
+  tremendously helpful if you track down bugs and submit fixes!  It's most
+  helpful if you submit fixes for both the 4.7.x and 5.x versions of the
+  module.
+
+   In addition to fixing bugs, you are also welcome to contribute new
+  features.  It's preferred when new features use the ad module's many hooks
+  and are implemented in external modules.
+
+
+4. Money.
+
+   I maintain the ad module in my spare time.  While cash donations are not in
+  any way required, they are certainly appreciated.  The simplest way to donate
+  money is to use PayPal and send the donation to my email address:
+  jeremy@tag1consulting.com.  If you are unable to use PayPal, drop me a note at
+  that same email address and we can work out something else.
+
+   I also offer consulting services, and can be contracted to add specific
+  functionality to the module.  Contact me at jeremy@tag1consulting.com for
+  more information.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/DEBUG.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,32 @@
+If you are having problems installing or configuring the ad module, please be
+sure to carefully follow the instructions in INSTALL.txt, and to read through
+README.txt.
+
+If after reviewing those files you are still having problems with the ad 
+module, please review the project's issue queue to see if you are experiencing
+a known bug:
+   http://drupal.org/project/issues/ad
+
+If you're still unable to find the information you need, you can submit a new
+issue by following the above link.  When doing so, it is important that you
+offer details on how to duplicate the bug that you are experiencing.  If you
+are unable to get ads to appear, be sure to explain what method you are using
+to try and display ads: blocks, views, ad_embed, etc...
+
+In the event that ads are not displaying, it is helpful if you collect the
+following debug information and inculde it in your bug report.  Using your
+web browser, view the html source of the page on which you expect 
+advertisements to show up.  Search for the string "serve.php", then copy and 
+paste that entire URL directly into your web browser.  It will look something 
+like:
+  http://sample.com/modules/ad/serve.php?q=1&t=23
+
+Load that URL and see if there are any indications as to why ads are not
+displaying.  If not, add "&debug=2" at the end of the URL:
+  http://sample.com/modules/ad/serve.php?q=1&t=23&debug=2
+
+(NOTE: you will need to convert "&amp;" to "&" in the url when you cut and
+paste it from your html source.  If you do not do this conversion, most of
+the URL will be ignored resulting in invalid output.)
+
+You can then submit the resulting debug output in your bug report.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/INSTALL.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,61 @@
+-------------
+Requirements:
+-------------
+ - Drupal 5.x
+ - upload.module (if using the ad_image module)
+
+-------------
+Installation:
+-------------
+1) Extract the ad tarball.  Move the resulting 'ad/' subdirectory within your
+   'modules/' directory.  Be sure the web server has read permissions to this
+   directory and the files within it.
+
+   NOTE:  For optimal performance you should set the DRUPAL_ROOT variable at
+   the top of the adserve.inc file.  Otherwise, the ad module will automatically
+   determine your top level Drupal directory each time it serves an
+   advertisement.
+
+2) Log in to your website and enable the ad.module.
+    (Goto :: administer >> modules :: then put a check next to 'ad')
+
+3) If you wish to display image (banner) ads, enable the ad_image.module.  If
+   it's not already enabled, you will also need to enable the upload.module.
+    (Goto :: administer >> modules :: then put a check next to 'ad_image' and
+     'upload'.)
+
+   You can set per-group limitations for image ads at administer >> modules >>
+   groups >> image ads.
+
+4) If you wish to display text ads, enable the ad_text.module.
+    (Goto :: administer >> modules :: then put a check next to 'ad_text')
+
+   You can set global limitations for text ads at administer >> modules >>
+   settings >> text ads.
+
+5) If you wish to embed ads into your site content, enable the ad_embed.module.
+    (Goto :: administer >> modules :: then put a check next to 'ad_embed')
+
+   You can configure embedded ads at administer >> modules >> settings >>
+   embedded ads.  There is also online help available if you enable the Drupal
+   help module and go to administer >> help >> ad_embed.
+
+6) If you wish to provide online graphical reports along with your ads, enable
+   the ad_report module.
+    (Goto :: administer >> modules :: then put a check next to 'ad_report')
+
+7) Assign the 'show advertisements' permission to all roles that you wish to
+   see advertisements.
+    (Goto :: administer >> user management >> access control :: then put a
+     check next to 'show advertisements' for all roles that should see ads)
+
+8) You can display ads in blocks by going to administer >> blocks and enabling
+   the per-ad-group blocks that are automatically generated.
+
+9) Optionally configure an input filter for your advertisements that allow
+   the html tags to display correctly.
+    (Goto :: administer >> site configuration >> input filters)
+   If you need to associate a specific input filter with the advertisement
+   content type, you may want to review the following projects:
+      http://drupal.org/project/better_formats
+      http://drupal.org/project/filterbynodetype
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/README.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,318 @@
+---------
+Overview:
+---------
+The ad module is a powerful advertising system for Drupal-powered websites. It 
+supports the random display and tracking of graphical (banner), text and raw
+html ads.  Ads can easily be displayed in themes, blocks, or embedded in site 
+content. The module records comprehensive statistics about when and how often 
+ads are viewed and clicked, including a plug-in module for generating graphical
+time-based reports. Ads can be assigned to multiple owners, each of which can 
+be assigned their own set of permissions.  Installation is simple by design. An
+API is provided allowing the development of additional functionality and 
+integration with other Drupal modules.
+
+Features:
+    * auto-generated ad blocks supporting a configurable number of ads
+    * automatically or manually embed ads into site content
+    * collection of comprehensive statistics allowing time-based reporting and 
+      analysis
+    * tracking of when and where ads are clicked, by which user and which IP
+    * advertisements can have multiple owners
+    * granular per-advertisement/per-owner permissions system
+    * activation/expiration scheduling based on time, clicks or impressions
+    * an ad_image plug-in for image (aka banner) ads
+    * an ad_text plug-in for simple text ads
+    * an ad_html plug-in for raw html ads
+    * an externally maintained ad_geoip plug-in provides support for 
+      geotargeting ads
+    * an ad_report plug-in for basic graphical reports
+    * an ad_notify plug-in for scheduling automatic email notifications
+    * an ad_remote plug-in for hosting ads on remote (non-Drupal) websites
+    * an administrative statistics overview page
+    * support for any number of configurable ad groups, utilizing Drupal's
+      taxonomy (category) subsystem
+    * display ads based on node ids (nids) or taxonomy terms (categories)
+    * file-based caching for improved performance
+    * memcache-based caching for improved performance
+    * support for external caching methods
+    * MySQL and PostgreSQL support
+
+
+------
+Usage:
+------
+Installation and requirements information can be found within the INSTALL.txt
+file included with this module.
+
+
+-------------
+Creating ads:
+-------------
+Once the ad module is properly installed an enabled, you can create
+advertisements by visiting 'create content' on your website and choosing
+'advertisement' as the content type.  If you have enabled multiple ad type
+modules, you can select one of them from the resulting menu, such as 'image
+advertiesment' or 'text advertisement'.
+
+  Text ads:
+  ---------
+  Text ads are very simple, requiring that you fill out only three fields of
+  information.  First, you need to specify the Destination URL where you want
+  people to be redirected when they click on your text ad.  Second, you enter
+  the a header for your ad.  The header will be linked to the Destination URL.
+  Finally, you need to specify the body of your ad.
+
+  Image ads:
+  ----------
+  Image ads or only slightly more complicated than text ads.  They depend on
+  the drupal core Upload module for managing the actual images.  As with text
+  ads, you first need to specify the Destination URL where you want people to
+  be redirected when they click on your image ad.  Second, you can optoinally
+  enter some Mouseover text.  If you enter text in the mouseover field this
+  text will be displayed when people hover their mouse pointer over the
+  advertisement.  Finally, you need to scroll down to the File attachements
+  section of the page and click the Browser button to select your image, then
+  click Attach to upload the image.
+
+  HTML ads:
+  ---------
+  HTML ads allow you to easily define your own custom ad type by simply pasting
+  in a block of HTML code.  At this time, however, the ad module is unable to
+  track when html ads are clicked.
+
+  Ad groups:
+  ----------
+  Ads can be organized into groups.  For example, you may have a group called
+  "Text Ads" and another group called "Image Ads."  You could then assign your 
+  text ads to the "Text Ads" group, and your image ads to your "Image Ads" 
+  groups.  (This is not required, it is perfectly valid to include both image 
+  ads and text ads in the same group.)  When displaying ads, you typically tell 
+  the ad module to display ads from a certain group and then the ad module 
+  randomly selects an active ad from the specified group.  Each ad can be a
+  member of any number of groups.
+
+  Ad status:
+  ----------
+  There are several states that an ad can exist in.  An ad in the Pending state
+  is one that has recently been uploaded and is waiting to be approved by a
+  privileged user.  An ad in the Approved state is one that has been approved
+  by a privileged user but is not actively being displayed, the ad could be
+  waiting on an autoactivation event.  An ad in the Active state is being 
+  actively displayed.  An ad in the Offline state is approved but is currently 
+  not being displayed.  An ad in the Unpublished state means that the ad node 
+  was unpublished so the ad is not any longer being displayed.  An ad in the 
+  Expired state is no longer being displayed.  An ad in the Denied state means 
+  it was not approved by the site administrator.
+
+  Scheduling:
+  -----------
+  If you put an ad into the Approved state and then enter a date and time in the
+  Automatically Activate Ad field, the ad will be automatically activated
+  on the date and time specified.  This feature requires that cron be functional
+  on your website.  If you enter a date and time in the Automatically Expire Ad
+  field, the ad will be automatically expired on the date and time specified.
+  Again, this feature requires that cron be functional on your website.
+
+  If you enter a number into the Maximum Impressions field, the ad will be
+  automatically expired once it has been displayed this number of times.
+
+  If you enter a number into the Maximum Clicks field, the ad will be
+  automatically expired once it has been clicked this number of times.
+  
+
+---------------
+Displaying ads:
+---------------
+There are many ways to display ads with the ad module.  The simplest way
+is to enable one of the automatically generated ad blocks.  It is also possible
+to use the ad_embed module to automatically embed ads within your site content.
+And you can even display ads from within the PHP of your theme or another 
+module.
+
+  ----------
+  Ad blocks:
+  ----------
+  The ad module automatically generates on ad block for every ad group that you
+  create.  For example, if you visit "Administer >> Site building >> Blocks" 
+  you will find a block named "ad group: default".  If you enable this block, 
+  it will display a random ad from all active ads in the default group.
+
+  You can optionally configure the block to display more than one ad at a time
+  by clicking the 'configure' link on the block administration page.
+
+  -------------
+  Embedded ads:
+  -------------
+  If you enabled the ad_embed module, it is possible to embed ads into your
+  site content.  To configure the ad_embed module go to "Administer >> Content
+  Management >> Ads >> Settings >> Embedded ads".  
+
+  If you wish to manually embed ads in your content, check the box next to 
+  "Replace ad bracket tags" or "Replace ad comment tags".  This will cause the 
+  ad_embed module to replace embedded [[add]] or <!--ad--> tags respectively.  
+  Instructions on how to specify specific ad groups or even specific ads and
+  how many ads to display at a time can be found by visiting "Administer >> 
+  Help >> Embed".
+
+  If you wish to automatically embed ads in your content, configure this on a
+  per-content-type basis in the lower configuration section.
+  embeded ads".
+
+  ------------------------
+  Displaying ads from PHP:
+  ------------------------
+  To display an ad from within PHP code, make a call to the ad() function.
+  You can optionally specify an ad group, the number of ads to display, and
+  several other options.  For example, to display one random ad from all ads
+  that have not been assigned to any group you don't have to pass in any 
+  parameters:
+      <?php print ad(); ?>
+
+  To display two ads from all ads that have not been assigned to any group
+  you would execute the following code:
+      <?php print ad(0, 2); ?>
+
+  (The first parameter specifies the ad group to display ads from.  By
+  specifying 0, we are telling the ad module to display ads that are not
+  assigned to any group.  The second parameter specifies the number of ads
+  to display at one time.)
+
+  To randomly display an ad from a specific group of nids, for example with 
+  the node ID 69 or 76, you would pass in the following parameters:
+      <?php print ad(NULL, 1, array('nids' => '69,76')); ?>
+
+  (When specifying specific nids, any specified ad group is ignored, so we
+  leave the first parameter as NULL.  The second parameter causes only one
+  ad to be displayed at a time.  And the third parameter is an array that
+  tells the ad module to randomly display either ad 69 or ad 76.)
+
+  To display and ad randomly selected from multiple groups you can simply
+  specify multiple groups separated by commas.  For example, to display 3
+  ads from groups 24, 56 and 98 you would pass in the following parameters:
+      <?php print ad('24,56,98', 2); ?>
+
+  You can also specify how to display a given ad.  Current display methods are
+  'javascript' and 'raw'.  When using the 'javascript' method, ads will
+  randomly change even when the Drupal pagecache is enabled.  When using the
+  'raw' method, ads will only change when the Drupal pagecache is flushed.
+  To force one ad with a tid of 76 to display using JavaScript you would pass
+  in the following parameters:
+      <?php print ad(76, 1, array('method' => 'javascript'));
+
+  To force two ads with a tid of 101 or 102 to display using the Raw method
+  you would pass in the following parameters:
+      <?php print ad('101,102', 76, array('method' => 'raw'));
+
+  ------------
+  Theming ads:
+  ------------
+  All ads are wrapped in the following tags:
+    <div class="advertisement" id="group-#"></div>
+
+  All ads are in the same "advertisement" class.  Each ad group gets a unique
+  id.  This makes it possible to create generic advertisement formatting as
+  well as specific advertisement formatting.
+
+    CSS Example 1:
+    --------------
+    This sample code can be added to your theme's custom style.css.  It adds
+    padding to advertisements, wrapping them in a dashed border and giving
+    them a background color.  It also adds the text "Advertisement:" above
+    the ad:
+
+       .advertisement {
+         padding: 5px;
+         border: dashed;
+         background-color: #ffd;
+       }
+
+       .advertisement:before {
+         content: "Advertisement:";
+       }
+  
+    CSS Example 2:
+    --------------
+    Here is more sample code that could be added to your theme's custom
+    style.css.  It will cause multiple image ads to be displayed horizontally 
+    one beside the other (space permitting), rather than vertically one
+    below the other.  The ads are aligned to the left side of the screen,
+    if you'd prefer them to be aligned to the right side of the screen change
+    the word "left" to "right" in the snippet:
+
+       .image-advertisement {
+         float: left;
+         padding: 3px;
+       }
+
+
+-------------------
+Ad module settings:
+-------------------
+The ad module and related modules provide a number of configuration options.
+
+  ----------------
+  Global settings:
+  ----------------
+  The ad module allows you to specify some global settings at "Administer >>
+  Content management >> Ads >> Settings >> Global settings".
+
+  Even if you do not plan to make any changes, you should visit this page at
+  least one time as the ad module will perform a series of sanity tests when
+  you visit this page, alerting you to any installation problems.
+
+  This page begins by displaying the path to serve.php.  If you see an error
+  here, then the ad module is not properly installed.  Review the directions
+  in INSTALL.txt to properly install the ad module.
+
+  In the General section you can specify what happens when an advertisement is
+  clicked.  For example, you can choose to have the resulting Destination URL 
+  opened in the same browser window, or in a new browser window.  You can also
+  enable the "nofollow" check box causing ads served by the ad module to 
+  include rel="nofollow" in automatically generated links.  You can select
+  to display advertisements using JavaScript, jQuery, IFrames, or Raw HTML.
+  Finally, you can disable URL validation, and enable advertisement filtering.
+
+  Checking 'Filter ads' allows you to apply Drupal's standard input filters
+  to your advertisements.  If you enable this option, be sure to apply a
+  compatible filter that doesn't strip away the <div> and other tags used by
+  the various advertisement types.
+
+  The next section allows you to configure IFrames, if you are using them to
+  display advertisements.
+
+  For better performance, you can also enable a cache.  At this time there is
+  only a file cache available.  When the file cache is enabled, ads are quickly
+  displayed from a cache file without bootstrapping Drupal.  On very busy
+  websites you may need to increase the number of cache files to prevent
+  cache file contention.
+
+  ---------
+  Text ads:
+  ---------
+  The ad_text module allows you to specify some minimum and maximum lengths for
+  text ads at "Administer >> Content management >> Ads >> Settings >> Text ads".
+
+  ----------
+  Image ads:
+  ----------
+  The ad_image module allows you to specify some image constraints on a 
+  per-group basis at "Administer >> Content management >> Ads >> Settings >> 
+  Image ads".
+
+  -------------
+  Embedded ads:
+  -------------
+  Manually and automatically embedded ads can be configured at "Administer >> 
+  Content management >> Ads >> Settings >> Embedded ads".
+
+
+-----------
+Statistics:
+-----------
+The ad module tracks how often each ad is viewed and clicked.  The statistics
+are tracked to an hourly granularity.
+
+When an advertisement is first enabled, the statistics for 'This hour', 'Today',
+'Last seven days', 'This month', 'This year' and 'All time' will all be the 
+same.  When available, statistics will also include 'Last hour', 'Yesterday',
+'Last month', and 'Last year'.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/documentation/ad_types.txt	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,73 @@
+---------
+Overview:
+---------
+This document is intended to help someone write a module for the ad api to
+introduce a new ad type. The core ad module includes two ad type modules, one
+for text ads and another for image ads. You can use the same api used by these
+two ad type modules to create your own custom ad type module, for example you
+may wish to write a module to ad support for flash ads. Some familiarity with
+Drupal and PHP is required.
+
+--------------------------
+Naming your new ad module:
+--------------------------
+There are two ad types included with the core ad module, text ads and image
+ads. Each ad type lives in its own module. Text ads are defined in the
+ad_text module, and image ads are defined in the ad_image module. All
+additional types of ads should be defined in modules following the same naming
+scheme which is 'ad_' followed by the type of ad. For example, if you are
+creating a module to add support for flash-based ads, you would call your
+module ad_flash.
+
+------------------------------
+Registering a new style of ad:
+------------------------------
+Within the Drupal framework, ads are nodes. To create a new ad, a user
+navigates to "create content >> ad", on which page they will be prompted
+to select the type of the ad they wish to create. Your new ad type can
+be added to this list by using the _adapi() hook and the 'type' operator. 
+For example, if creating a module for flash ads, you might add the following 
+function:
+
+ad_flash_adapi($op, $ad = NULL) {
+  switch ($op) {
+    case 'type':
+      return array(
+        'flash' => array(
+          'name' => t('Flash ad'),
+          'module' => 'ad_flash',
+          'description' => t('A flash advertisement.'),
+          'help' => t('A flash advertisement.'),
+        ),
+      );
+  }
+}
+
+------------------
+Displaying your ad
+------------------
+To display an ad, your ad type module needs to define the _display_ad() hook.
+Be aware that when this function is called, many standard Drupal functions
+are not available to you as the adtype.php script will only bootstrap to
+DRUPAL_BOOTSTRAP_DATABASE.
+
+You are passed in an object which contains the ad ID (aid) and the redirect
+URL (ie 'ad/redirect/13'). Utilize this information to return the complete
+advertisement. For example:
+
+function ad_static_display_ad($ad) {
+  $return "<a href=\"$ad->redirect\">Static ad #$ad->aid</a>";
+}
+
+--------------------------------
+Sharing your new ad_type module:
+--------------------------------
+The drupal.org CVS contributions repository requires that all included modules
+be licesensed under the GPL. The ad module is dual licensed under both the
+GPL and the BSD license, meeting this requirement. You can license your
+new ad_style module under just the GPL, or under a dual license as long as
+one of the licenses is the GPL.
+
+If your module meets the above licensing requirements and you feel other Drupal
+users could benefit from your work, contact the ad module maintainer to have
+your module reviewed for inclusion.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/embed/ad_embed.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_embed.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = Embed
+package = Ad
+dependencies[] = ad
+description = Embed ads in content.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/embed/ad_embed.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,326 @@
+<?php
+// $Id: ad_embed.module,v 1.1.2.4.2.12.2.6 2009/02/17 01:13:16 jeremy Exp $
+
+/**
+ * @file
+ * Embed ads in content.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_embed_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'admin/help#ad_embed':
+      $output = '<p>'. t('The ad_embed module uses the ad module to replace special tags with random advertisements, or to automatically insert ads after a number of paragraphs in your content.  The module allows you to specify which ad group to select ads from and how many ads to display.  Instead of selecting ads from ad groups, it also allows you to specify individual ads by their node id.') .'</p>';
+      $output .= '<p><b>'. t('Replace tags:') .'</b><br />';
+      $output .= t('Content must have been created by a user with <em>embed ads in content</em> permissions for tags to be replaced.  The ad_embed module supports two different types of tags.  One uses double brackets, looking like <em>[[ad]]</em>, the other uses an html comment, looking like <em>&lt;!--ad--&gt;</em>.  Individual parameters are then separated by the | (pipe) character.  For individual parameters with multiple values, the values are separated by commas.') .'</p>';
+      $output .= '<p>'. t('Using html comments has the advantage that if you decide to disable the ad_embed module, the ads will simply disappear (as they will be html coments.  The downside to using html comments is that you need to configure your Input Filter to not filter them out.  Using brackets can be simpler to get working.') .'</p>';
+      $output .= '<p><em>'. t('Parameters:') .'</em><br />';
+      $output .= t('The following parameters can be used within embedded advertisement tages:') .'</p>';
+      $output .= '<ul>';
+      $output .= '  <li>'. t('<em>groups</em>: specify which group to select random ads from.');
+      $output .= '  <li>'. t('<em>quantity</em>: specify how many ads to display at once.');
+      $output .= '  <li>'. t('<em>hostid</em>: specify a unqiue when logging impressions and clicks.  (This is primarily intended for use when hosting ads on remote websites.)');
+      $output .= '  <li>'. t('<em>nids</em>: a list of ads from which to select from.  If <em>nids</em> is specified, the <em>groups</em> parameter will be ignored.');
+      $output .= '</ul></p>';
+      $output .= '<p><em>'. t('Examples:') .'</em><br />';
+      $output .= t('The following examples should help you to understand how to embed ads within your content.');
+      $output .= '<ul>';
+      $output .= '  <li>'. t('<em>[[ad]]</em><br />This will display one random ad from ads that are not assigned to any group.') .'</li>';
+      $output .= '  <li>'. t('<em>&lt;!--ad--&gt;</em><br />This will display one random ad from ads that are not assigned to any group.') .'</li>';
+      $output .= '  <li>'. t('<em>[[ad|quantity=2]]</em><br />This will display two random ads from ads that are not assigned to any group.') .'</li>';
+      $output .= '  <li>'. t('<em>[[ad|group=23]]</em><br />This will display one random ad from the group with a tid of 23.') .'</li>';
+      $output .= '  <li>'. t('<em>&lt!--ad|nids=1,13,42,76|quantity=4]]</em><br />This will display all four listed ads in a random order.') .'</li>';
+      $output .= '</ul>';
+      $output .= '<p><b>'. t('Automatically embedding ads:') .'</b><br />';
+      $output .= '<p>'. t('You can enable the functionality to automatically embed ads in your content at <em>administer >> ads >> settings >> embed ads</em>.  On this configuration page, you can choose which content types should contain automatically embedded ads, after which paragraph the ad should appear, and from which ad group the advertisement should be selected.') .'</p>';
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function ad_embed_perm() {
+  return array('embed ads in content');
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_embed_menu() {
+  $items = array();
+
+  $items['admin/content/ad/configure/embed'] = array(
+    'title' => 'Embedded ads',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_embed_admin_configure_settings'),
+    'access arguments' => array('embed ads in content'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 5,
+  );
+
+  return $items;
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function ad_embed_nodeapi(&$node, $op, $teaser, $page) {
+  switch ($op) {
+    case 'view':
+      $node = (object)$node;
+      // Only replace tags if content creator has 'embed' permission
+      if (user_access('embed ads in content', user_load(array('uid' => $node->uid)))) {
+        if (isset($node->content['teaser']['#value'])) {
+          $node->content['teaser']['#value'] = ad_embed_replace($node->content['teaser']['#value']);
+        }
+        if (isset($node->content['body']['#value'])) {
+          $node->content['body']['#value'] = ad_embed_replace($node->content['body']['#value']);
+        }
+      }
+      if (!($teaser) && variable_get("embed-ad-$node->type", 0)) {
+        $ad = ad(variable_get("embed-ad-$node->type".'-group', array()), variable_get("embed-ad-$node->type".'-quantity', 1));
+        $node->content['body']['#value'] = ad_embed_auto($node->content['body']['#value'], $ad, variable_get("embed-ad-$node->type".'-count', 3), variable_get("embed-ad-$node->type".'-force', 0));
+      }
+  }
+}
+
+/**
+ * Module settings page.
+ */
+function ad_embed_admin_configure_settings($form_state) {
+  $form = array();
+
+  $form['manual'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+    '#title' => t('Manually embedded ads'),
+    '#description' => t('Configuration options for manually embedded ads using [[ad]] and &lt!--ad--&gt; tags.'),
+  );
+
+  $form['manual']['ad_embed_replace_brackets'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Replace ad bracket tags'),
+    '#default_value' => variable_get('ad_embed_replace_brackets', 1),
+    '#description' => t('Replace [[ad]] style tags in site content with advertisements.'),
+  );
+
+  $form['manual']['ad_embed_replace_comments'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Replace ad comment tags'),
+    '#default_value' => variable_get('ad_embed_replace_comments', 1),
+    '#description' => t('Replace &lt;!--ad--&gt; style tags in site content with advertisements.'),
+  );
+
+  $form['automatic'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+    '#title' => t('Automatically embedded ads'),
+    '#description' => t('Enable automatically embedded ads for any of the following content types, causing ads to be automatically inserted when users are viewing the content.  Configure where the ad should be placed, from which ad group(s) the ads should be selected, and how many ads to display.'),
+  );
+
+  $types = node_get_types();
+  foreach ($types as $key => $type) {
+    $form['automatic'][$key] = array(
+      '#type' => 'fieldset',
+      '#collapsible' => TRUE,
+      '#collapsed' => variable_get("embed-ad-$key", 0) ? FALSE : TRUE,
+      '#title' => $type->name,
+    );
+
+    $form['automatic'][$key]["embed-ad-$key"] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Automatically embed ads in %type content', array('%type' => $type->name)),
+      '#default_value' => variable_get("embed-ad-$key", 0),
+    );
+
+    $form['automatic'][$key]["embed-ad-$key-count"] = array(
+      '#type' => 'select',
+      '#title' => t('Location', array('%type' => $type->name)),
+      '#options' => array(
+        0 => t('Before the first paragraph'),
+        1 => t('After the first paragraph'),
+        2 => t('After the second paragraph'),
+        3 => t('After the third paragraph'),
+        4 => t('After the fourth paragraph'),
+        5 => t('After the fifth paragraph'),
+        6 => t('After the sixth paragraph'),
+        7 => t('After the seventh paragraph'),
+        8 => t('After the eighth paragraph'),
+        9 => t('After the ninth paragraph'),
+        10 => t('After the tenth paragraph'),
+        -1 => t('After the last paragraph'),
+      ),
+      '#default_value' => variable_get("embed-ad-$key-count", array()),
+      '#description' => t('Specify where you would like to automatically embed the advertisement for this content type.'),
+    );
+
+    $form['automatic'][$key]["embed-ad-$key-force"] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Always display ad'),
+      '#default_value' => variable_get("embed-ad-$key-force", 0),
+      '#description' => t('Check this box to display an ad even if the content doesn\'t have as many paragraphs as you specified above.  In these cases the ad will appear at the end of the content.'),
+    );
+
+    $form['automatic'][$key]["embed-ad-$key-group"] = array(
+      '#type' => 'select',
+      '#multiple' => TRUE,
+      '#title' => t('Ad group'),
+      '#options' => ad_groups_list(FALSE),
+      '#default_value' => explode(',', variable_get("embed-ad-$key-group", '')),
+      '#description' => t('Specify from which ad group(s) you\'d like to display advertisements.'),
+    );
+
+    $form['automatic'][$key]["embed-ad-$key-quantity"] = array(
+      '#type' => 'select',
+      '#title' => t('Quantity'),
+      '#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)),
+      '#default_value' => variable_get("embed-ad-$key-quantity", 1),
+      '#description' => t('Specify how many advertisements to display at the same time.'),
+    );
+  }
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Save changes from module settings page.
+ */
+function ad_embed_admin_configure_settings_submit($form, &$form_state) {
+  variable_set('ad_embed_replace_brackets', $form_state['values']['ad_embed_replace_brackets']);
+  variable_set('ad_embed_replace_comments', $form_state['values']['ad_embed_replace_comments']);
+
+  $types = node_get_types();
+  foreach ($types as $key => $type) {
+    variable_set("embed-ad-$key", $form_state['values']["embed-ad-$key"]);
+    variable_set("embed-ad-$key-force", $form_state['values']["embed-ad-$key-force"]);
+    if (isset($form_state['values']["embed-ad-$key-group"])) {
+      variable_set("embed-ad-$key-group", (implode(',', $form_state['values']["embed-ad-$key-group"])));
+    }
+    else {
+      variable_set("embed-ad-$key-group", '');
+    }
+    variable_set("embed-ad-$key-count", $form_state['values']["embed-ad-$key-count"]);
+    variable_set("embed-ad-$key-quantity", $form_state['values']["embed-ad-$key-quantity"]);
+  }
+  drupal_set_message(t('Embedded ad configuration options saved.'));
+}
+
+/**
+ * Replaces [[ad]] and <!--ad--> style tags with JavaScript for displaying ads.
+ */
+function ad_embed_replace($text) {
+  $tags = array(
+    '[[ad' => ']]',
+    '<!--ad' => '-->',
+  );
+
+  if (!$text) { return; }
+
+  foreach ($tags as $open => $close) {
+
+    if ($open == '[[ad' && !variable_get('ad_embed_replace_brackets', 1)) 
+      continue;
+    if ($open == '<!--ad' && !variable_get('ad_embed_replace_comments', 1)) 
+      continue;
+    if (strlen($open) > strlen($text))
+      continue;
+
+    $pos = 0;
+    while ($pos !== FALSE) {
+      // Locate the first open embed tag.
+      $pos = strpos($text, $open, $pos+1);
+      if ($pos) {
+        $start = $pos + strlen($open);
+        // Locate the matching close embed tag.
+        $pos = strpos($text, $close, $start);
+        if ($pos) {
+          // Extract the contents within the tags.
+          $contents = substr($text, $start, $pos - $start);
+          // Parse the options within the contents.
+          $options = implode('&', explode('|', $contents));
+          parse_str($options, $args);
+          // Set defaults
+          $quantity = 1;
+          $options = array();
+          $group = '';
+          foreach ($args as $key => $value) {
+            switch ($key) {
+              case 'quantity':
+                // Must be at least 1
+                $quantity = (int)$value ? (int)$value : 1;
+                break;
+              case 'group':
+                $group = $value;
+                break;
+              case 'hostid':
+                $options['hostid'] = $value;
+                break;
+              case 'nids':
+                $options['nids'] = $value;
+                break;
+            }
+          }
+          $replace = $open . $contents . $close;
+          $ad = ad($group, $quantity, $options);
+          $text = str_replace($replace, $ad, $text);
+          // Adjust position to compensate for difference in length of what
+          // we are replacing and length of what we are replacing it with.
+          if (strlen($replace) > strlen($ad)) {
+            $pos -= (strlen($replace) - strlen($ad)) - 1;
+          }
+          else if (strlen($replace) < strlen($ad)) {
+            $pos += (strlen($ad) - strlen($replace)) + 1;
+          }
+        }
+      }
+    }
+  }
+  return $text;
+}
+
+/**
+ * Automatically embed advertisement into content.
+ */
+function ad_embed_auto($text, $ad, $count, $force = FALSE) {
+  if (!$text) { return; }
+
+  if ($count == 0) {
+    $text = $ad . $text;
+  }
+  else if ($count == -1) {
+    $text = $text . $ad;
+  }
+
+  $pos = $paragraph = 0;
+  while ($pos !== FALSE) {
+    $pos = strpos($text, "\n", $pos + 1);
+    if ($pos) {
+      $paragraph++;
+      if ($paragraph == $count) {
+        $part1 = substr($text, 0, $pos);
+        $part2 = substr($text, $pos + 1, strlen($text));
+        $text = $part1 . $ad . $part2;
+        break;
+      }
+    }
+  }
+  // Not enough paragraphs to display ad, unless forced.
+  if (($paragraph < $count) && $force) {
+    $text = $text . $ad;
+  }
+
+  return $text;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/external/ad_external.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_external.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = External Ad
+package = Ad
+dependencies[] = ad
+description = Enhances the ad module to support externally hosted ads.  Typically combined with IFrames.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/external/ad_external.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,50 @@
+<?php
+// $Id: ad_external.install,v 1.1.2.1.2.2 2009/02/16 17:06:48 jeremy Exp $
+
+/**
+ * @file
+ * Ad_external module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_external_schema() {
+  $schema['ad_external'] = array(
+    'description' => 'The ad_external table stores source URLs for external ads.',
+    'fields' => array(
+      'aid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'url' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      )
+    ),
+    'primary key' => array('aid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * ad_external module installation.
+ */
+function ad_external_install() {
+  drupal_install_schema('ad_external');
+}
+
+/**
+ * Allow complete uninstallation of the ad_external module.
+ */
+function ad_external() {
+  drupal_uninstall_schema('ad_external');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/external/ad_external.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,207 @@
+<?php
+// $Id: ad_external.module,v 1.1.2.6.2.7 2009/02/17 18:56:26 jeremy Exp $
+
+/**
+ * @file
+ * Enhances the ad module to support externally hosted ads, served via IFrames.
+ * It is recommended that you configure "Administer >> Content management >>
+ *  Ads >> Settings >> Global settings >> Display type" to "Raw".
+ * If you configure the "Display type" to IFrame, you will be serving IFrames
+ * within IFrames.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Function used to display the selected ad.
+ */
+function ad_external_display_ad($ad) {
+  return theme('ad_external_ad', $ad);
+}
+
+/**
+ * Return a themed ad of type ad_remote.
+ *
+ * @param @ad
+ *  The ad object.
+ * @return
+ *  A string containing the ad markup.
+ */
+function theme_ad_external_ad($ad) {
+  global $base_url;
+
+  if (isset($ad->aid)) {
+    $output = '<div class="external-advertisement" id="ad-'. $ad->aid .'">';
+    // For now, we hard code this to only support caching with the file cache.
+    // As more cache types are introduced, we'll expand this.  Ideally it should
+    // be cache type agnostic, but that seems to be unrealistic.
+    if (variable_get('ad_cache', 'none') == 'file') {
+      $adserve = variable_get('adserve', '');
+      $display_variables = '&amp;c=file'. module_invoke('ad_cache_file', 'adcacheapi', 'display_variables', array());
+      $external = url("$base_url/$adserve?o=external&amp;n=$ad->aid&amp;$display_variables");
+    }
+    else {
+      $external = db_result(db_query('SELECT url FROM {ad_external} WHERE aid = %d', $ad->aid));
+    }
+    $append = 'frameborder="'. variable_get('ad_iframe_frameborder', 0) .'" ';
+    $append .= 'scrolling="'. variable_get('ad_iframe_scroll', 'auto') .'" ';
+    if ($height = variable_get('ad_iframe_height', '')) {
+      $append .= "height=\"$height\" ";
+    }
+    if ($width = variable_get('ad_iframe_width', '')) {
+      $append .= "width=\"$width\" ";
+    }
+    $output .= "<iframe src=\"$external\" $append></iframe>";
+    $output .= '</div>';
+    return $output;
+  }
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_external_theme() {
+  return array(
+    'ad_external_ad' => array(
+      'file' => 'ad_external.module',
+      'arguments' => array(
+        'ad' => NULL,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_external_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'node/add/ad#external':
+      $output = t('An external advertisement, displayed in an IFrame.');
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function ad_external_access($op, $node, $account) {
+  return ad_access($op, $node, $account);
+}
+
+/**
+ * Implementation of hook_adapi().
+ */
+function ad_external_adapi($op, &$node) {
+  switch ($op) {
+    case 'load':
+      return db_fetch_array(db_query('SELECT * FROM {ad_external} WHERE aid = %d', $node['aid']));
+
+    case 'insert':
+      db_query("INSERT INTO {ad_external} (aid, url) VALUES(%d, '%s')", $node->nid, $node->url);
+      break;
+
+    case 'update':
+      db_query("UPDATE {ad_external} SET url = '%s' WHERE aid = %d", $node->url, $node->nid);
+      break;
+
+    case 'delete':
+      db_query('DELETE FROM {ad_external} WHERE aid = %d', $node->nid);
+      break;
+
+    case 'form':
+      return ad_external_node_form($node);
+
+    case 'view':
+      return ad_external_node_view($node);
+
+    case 'redirect':
+      // TODO: Would it ever make sense to have redirects for this ad type?
+      watchdog('ad', 'Unexpected redirect attempt in external ad type.');
+      return;
+
+    case 'type':
+      return array(
+        'external' => array(
+          'name' => t('External ad'),
+          'module' => 'ad_external',
+          'description' => t('An external advertisement, displayed in an IFrame.'),
+          'help' => t('An external advertisement, displayed in an IFrame.'),
+        ),
+      );
+  }
+}
+
+/**
+ * Adapi helper function for displaying a node form.
+ */
+function ad_external_node_form(&$node) {
+  $form = array();
+
+  $form['ad_external'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('External'),
+    '#collapsible' => TRUE,
+  );
+
+  $form['ad_external']['preview'] = array(
+    '#type' => 'markup',
+    '#value' => ad_external_display_ad($node),
+  );
+
+  if ((arg(1) == 'add' || arg(2) == 'edit') &&
+       user_access('create advertisements')) {
+    $form['ad_external']['url'] = array(
+      '#type' => 'textfield',
+      '#title' => t('External Source URL'),
+      '#required' => TRUE,
+      '#default_value' => isset($node->url) ? $node->url : '',
+      '#description' => t('Enter the complete URL where your external ad his hosted.  The URL must begin with http:// or https://.  For example, %url.', array('%url' => t('http://www.sample.org/external/ad.php'))),
+    );
+
+  }
+
+  return $form;
+}
+
+/**
+ * Adapi helper function for displaying ad itself.
+ */
+function ad_external_node_view(&$node) {
+  $node->content['ad'] = array(
+    '#value' => theme('box', '', stripslashes(ad_external_display_ad($node))),
+    '#weight' => -1,
+  );
+  $link = t('Links to !url.', array('!url' => $node->url));
+  if (variable_get('ad_filter', 0)) {
+    $link = check_markup($link, $node->format, FALSE);
+  }
+  $node->content['ad-link'] = array(
+    '#value' => "<div class=\"links-to\">$link<div>",
+    '#weight' => 1,
+  );
+}
+
+/**
+ * Download external pages and store them in the filecache.
+ */
+function ad_external_ad_build_cache() {
+  $cache = array();
+  $result = db_query('SELECT * FROM {ad_external}');
+  while ($external = db_fetch_object($result)) {
+    $contents = file_get_contents($external->url);
+    if ($contents === FALSE) {
+      // Failed to download external url, don't cache the error page.
+      // TODO: Watchdog log this.
+      // TODO: Would it be possible to obtain the old version from the cache,
+      //       if we already downloaded this page once?
+      continue;
+    }
+    $cache['ad_external'][$external->aid]['url'] = $external->url;
+    $cache['ad_external'][$external->aid]['contents'] = $contents;
+  }
+  return $cache;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/external/external.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,37 @@
+<?php
+// $Id: external.inc,v 1.1.2.1.2.2 2009/02/16 17:06:48 jeremy Exp $
+
+/**
+ * @file
+ * Loads external ad.
+ *
+ * Copyright (c) 2006-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+adserve_variable('ad_dir', getcwd() .'/');
+
+// Attempt to utilize the file cache, if enabled.
+if (function_exists('ad_cache_file_get_lock')) {
+
+  // Find the top level Drupal directory so we can access the cache files.
+  for ($i = 1; $i <= 20; $i++) {
+    if (file_exists('./includes/bootstrap.inc')) {
+      break;
+    }
+    chdir('..');
+  }
+  adserve_variable('root_dir', getcwd());
+
+  $cache_file = ad_cache_file_get_lock();
+  if ($cache_file) {
+    $cache = unserialize(fread(adserve_variable('fd'), filesize($cache_file)));
+    if (isset($cache['ad_external'])) {
+      $aid = adserve_variable('nids');
+      if ($aid && isset($cache['ad_external'][$aid]['contents'])) {
+        print $cache['ad_external'][$aid]['contents'];
+        exit(0);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/ad_html.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_html.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = HTML Ad
+package = Ad
+dependencies[] = ad
+description = Enhances the ad module to support html-based ads, such as those defined by many advertising affiliate websites.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/ad_html.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,54 @@
+<?php
+// $Id: ad_html.install,v 1.1.2.1.2.4 2009/02/16 17:06:49 jeremy Exp $
+
+/**
+ * @file
+ * Ad_html module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_html_schema() {
+  $schema['ad_html'] = array(
+    'description' => 'The ad_html table stores HTML code of html ads.',
+    'fields' => array(
+      'aid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'html' => array(
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+    ),
+    'primary key' => array('aid'),
+  );
+  return $schema;
+}
+
+/**
+ * ad_html module installation.
+ */
+function ad_html_install() {
+  drupal_install_schema('ad_html');
+}
+
+/**
+ * Allow complete uninstallation of the ad_html module.
+ */
+function ad_html_uninstall() {
+  // Delete all ad_html content.
+  $result = db_query("SELECT aid FROM {ad_html}");
+  while ($aid = db_result($result)) {
+    node_delete($aid);
+  }
+
+  // Remove tables.
+  drupal_uninstall_schema('ad_html');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/html/ad_html.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,154 @@
+<?php
+// $Id: ad_html.module,v 1.1.2.5.2.8 2009/02/17 18:56:26 jeremy Exp $
+
+/**
+ * @file
+ * Enhances the ad module to support html ads.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+
+/**
+ * Function used to display the selected ad.
+ */
+function ad_html_display_ad($ad) {
+  return theme('ad_html_ad', $ad);
+}
+
+/**
+ * Return a themed ad of type ad_html.
+ *
+ * @param @ad
+ *  The ad object.
+ * @return
+ *  A string containing the ad markup.
+ */
+function theme_ad_html_ad($ad) {
+  if (isset($ad->aid)) {
+    $output  = '<div class="html-advertisement" id="ad-'. $ad->aid .'">';
+    $output .= check_markup($ad->html, $ad->format, FALSE);
+    $output .= '</div>';
+    return $output;
+  }
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_html_theme() {
+  return array(
+    'ad_html_ad' => array(
+      'file' => 'ad_html.module',
+      'arguments' => array(
+        'ad' => NULL,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_html_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'node/add/ad#html':
+      $output = t('A html advertisement.');
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function ad_html_access($op, $node, $account) {
+  return ad_access($op, $node, $account);
+}
+
+/**
+ * Implementation of the ad module's _adapi hook.
+ */
+function ad_html_adapi($op, &$node) {
+  switch ($op) {
+    case 'load':
+      $return = db_fetch_array(db_query('SELECT html FROM {ad_html} WHERE aid = %d', $node['aid']));
+      $return['ad'] = check_markup($return['html'], $node['format'], FALSE);
+      return $return;
+
+    case 'insert':
+      db_query("INSERT INTO {ad_html} (aid, html) VALUES(%d, '%s')", $node->nid, $node->html);
+      break;
+
+    case 'update':
+      db_query("UPDATE {ad_html} SET html = '%s' WHERE aid = %d", $node->html, $node->nid);
+      break;
+
+    case 'delete':
+      db_query('DELETE FROM {ad_html} WHERE aid = %d', $node->nid);
+      break;
+
+    case 'form':
+      return ad_html_node_form($node);
+
+    case 'view':
+      return ad_html_node_view($node);
+
+    case 'type':
+      return array(
+        'html' => array(
+          'name' => t('HTML ad'),
+          'module' => 'ad_html',
+          'description' => t('A html advertisement.'),
+          'help' => t('A html advertisement.'),
+        ),
+      );
+    case 'permissions':
+      if (!isset($node->adtype) || $node->adtype == 'html') {
+        return array('manage ad html');
+      }
+  }
+}
+
+/**
+ * Adapi helper function for displaying a node form.
+ */
+function ad_html_node_form(&$node) {
+  $form = array();
+
+  $form['ad_html'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('HTML'),
+    '#collapsible' => TRUE,
+  );
+
+  $form['ad_html']['display'] = array(
+    '#type' => 'markup',
+    '#value' => ad_html_display_ad($node),
+  );
+
+  if (ad_adaccess($node, 'manage ad html') || arg(1) == 'add' && user_access('create advertisements')) {
+    $form['ad_html']['html'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Ad HTML'),
+      '#required' => TRUE,
+      '#default_value' => isset($node->html) ? $node->html : '',
+      '#description' => t('Paste the complete HTML provided by your advertising affiliate.'),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Helper function, display the html ad as a node.
+ */
+function ad_html_node_view(&$node) {
+  $node->content['ad'] = array(
+    '#value' => theme('box', '', stripslashes(ad_html_display_ad($node))),
+    '#weight' => -1,
+  );
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/ad_image.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,13 @@
+; $Id: ad_image.info,v 1.1.2.5.2.1 2008/08/11 21:15:26 jeremy Exp $
+name = Image Ad
+package = Ad
+dependencies[] = ad
+dependencies[] = upload
+description = Enhances the ad module to support banner ads.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/ad_image.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,119 @@
+<?php
+// $Id: ad_image.install,v 1.2.2.2.2.6.2.4 2009/02/16 17:06:49 jeremy Exp $
+
+/**
+ * @file
+ * Ad_image module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_image_schema() {
+  $schema['ad_image'] = array(
+    'description' => 'The ad_image table stores image information such as file ID, title, width, height of corresponding image ads.',
+    'fields' => array(
+      'aid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'fid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'url' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'tooltip' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'width' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'height' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'unique keys' => array(
+      'aid' => array('aid')
+    ),
+  );
+  $schema['ad_image_format'] = array(
+    'description' => 'The ad_image_format table stores dimensions for image ads.',
+    'fields' => array(
+      'gid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'min_width' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'max_width' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'min_height' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'max_height' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('gid'),
+  );
+
+  return $schema;
+}
+
+
+/**
+ * ad_image module installation.
+ */
+function ad_image_install() {
+  drupal_install_schema('ad_image');
+}
+
+/**
+ * Allow complete uninstallation of the ad_image module.
+ */
+function ad_image_uninstall() {
+  // Delete all ad_image content.
+  $result = db_query("SELECT aid FROM {ad_image}");
+  while ($aid = db_result($result)) {
+    node_delete($aid);
+  }
+
+  // Remove tables.
+  drupal_uninstall_schema('ad_image');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/image/ad_image.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,528 @@
+<?php
+// $Id: ad_image.module,v 1.2.2.13.2.40.2.11 2009/02/17 18:56:26 jeremy Exp $
+
+/**
+ * @file
+ * Enhances the ad module to support banner ads.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Function used to display the selected ad.
+ */
+function ad_image_display_ad($ad) {
+  return theme('ad_image_ad', $ad);
+}
+
+/**
+ * Return a themed ad of type ad_image.
+ *
+ * @param @ad
+ *   The ad object.
+ * @return
+ *   A string containing the ad markup.
+ */
+function theme_ad_image_ad($ad) {
+  if (isset($ad->aid) && isset($ad->filepath)) {
+    $output = '<div class="image-advertisement" id="ad-'. $ad->aid .'">';
+    if (isset($ad->url) && !empty($ad->url)) {
+      $image = theme('ad_image_image', $ad->filepath, check_plain($ad->tooltip), check_plain($ad->tooltip));
+      $output .= l($image, $ad->redirect .'/@HOSTID___', array('attributes' => ad_link_attributes(), 'absolute' => TRUE, 'html' => TRUE));
+    }
+    else {
+      $output .= theme('ad_image_image', $ad->filepath, check_plain($ad->tooltip), check_plain($ad->tooltip));
+    }
+    $output .= '</div>';
+    return $output;
+  }
+}
+
+/**
+ * Return a themed ad image.
+ *
+ * @param $path
+ *   Either the path of the ad image file (relative to base_path()) or a full
+ *   URL.
+ * @param $alt
+ *   The alternative text for text-based browsers.
+ * @param $tooltip
+ *   The tooltip text is displayed when the image is hovered in some popular
+ *   browsers.
+ * @param $attributes
+ *   Associative array of attributes to be placed in the img tag.
+ * @param $getsize
+ *   If set to TRUE, the image's dimension are fetched and added as width/height
+ *   attributes.
+ * @return
+ *   A string containing the image tag.
+*/
+function theme_ad_image_image($path, $alt = '', $tooltip = '', $attributes = NULL, $getsize = TRUE) {
+  if (!$getsize || (is_file($path) && (list($width, $height, $type, $image_attributes) = @getimagesize($path)))) {
+    $attributes = drupal_attributes($attributes);
+    $url = preg_replace('&'. drupal_get_path('module', 'ad') .'/&', '', file_create_url($path));
+    return '<img src="'. check_url($url) .'" alt="'. check_plain($alt) .'" title="'. check_plain($tooltip) .'" '. $image_attributes . $attributes .' />';
+  }
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_image_theme() {
+  return array(
+    'ad_image_ad' => array(
+      'file' => 'ad_image.module',
+      'arguments' => array(
+        'ad' => NULL,
+      ),
+    ),
+    'ad_image_image' => array(
+      'file' => 'ad_image.module',
+      'arguments' => array(
+        'path' => NULL,
+        'alt' => '',
+        'tooltip' => '',
+        'attributes' => NULL,
+        'getsize' => TRUE,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_image_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'node/add/ad#image':
+      $output = t('An image or banner advertisement.');
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function ad_image_access($op, $node, $account) {
+  return ad_access($op, $node, $account);
+}
+
+/**
+ * Image ad settings form.
+ */
+function ad_image_global_settings($edit = array()) {
+  $form = array();
+
+  $groups = module_invoke('ad', 'groups_list', TRUE);
+  foreach ($groups as $tid => $group) {
+    $form["group-$tid"] = array(
+      '#type' => 'fieldset',
+      '#title' => $group->name,
+      '#collapsible' => TRUE,
+    );
+
+    $form["group-$tid"]["description-$tid"] = array(
+      '#type' => 'markup',
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => theme_placeholder("$group->description"),
+    );
+
+    $format = db_fetch_object(db_query('SELECT * FROM {ad_image_format} WHERE gid = %d', $tid));
+    $form["group-$tid"]["min-height-$tid"] = array(
+      '#type' => 'textfield',
+      '#title' => t('Minimum height'),
+      '#size' => 5,
+      '#maxlength' => 5,
+      '#default_value' => isset($format->min_height) ? $format->min_height : 0,
+      '#description' => t('Optionally specify a minimum height in pixels for images in this group.  To specify no minimum height, enter <em>0</em>.'),
+    );
+    $form["group-$tid"]["min-width-$tid"] = array(
+      '#type' => 'textfield',
+      '#title' => t('Minimum width'),
+      '#size' => 5,
+      '#maxlength' => 5,
+      '#default_value' => isset($format->min_width) ? $format->min_width : 0,
+      '#description' => t('Optionally specify a minimum width in pixels for images in this group.  To specify no minimum width, enter <em>0</em>.'),
+    );
+    $form["group-$tid"]["max-height-$tid"] = array(
+      '#type' => 'textfield',
+      '#title' => t('Maximum height'),
+      '#size' => 5,
+      '#maxlength' => 5,
+      '#default_value' => isset($format->max_height) ? $format->max_height : 0,
+      '#description' => t('Optionally specify a maximum height in pixels for images in this group.  To specify no maximum height, enter <em>0</em>.'),
+    );
+    $form["group-$tid"]["max-width-$tid"] = array(
+      '#type' => 'textfield',
+      '#title' => t('Maximum width'),
+      '#size' => 5,
+      '#maxlength' => 5,
+      '#default_value' => isset($format->max_width) ? $format->max_width : 0,
+      '#description' => t('Optionally specify a maximum width in pixels for images in this group.  To specify no maximum width, enter <em>0</em>.'),
+    );
+  }
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Save min and max image width and height values for ad groups.
+ */
+function ad_image_global_settings_submit($form, &$form_state) {
+  $groups = module_invoke('ad', 'groups_list', TRUE);
+  foreach ($groups as $group) {
+    // TODO: Update the database schema, convert gid to tid.
+    $gid = db_result(db_query('SELECT gid FROM {ad_image_format} WHERE gid = %d', $group->tid));
+    if (is_numeric($gid)) {
+      db_query("UPDATE {ad_image_format} SET min_width = %d, max_width = %d, min_height = %d, max_height = %d WHERE gid = %d", $form_state['values']["min-width-$group->tid"], $form_state['values']["max-width-$group->tid"], $form_state['values']["min-height-$group->tid"], $form_state['values']["max-height-$group->tid"], $group->tid);
+    }
+    else {
+      db_query("INSERT INTO {ad_image_format} (gid, min_width, max_width, min_height, max_height) VALUES (%d, %d, %d, %d, %d)", $group->tid, $form_state['values']["min-width-$group->tid"], $form_state['values']["max-width-$group->tid"], $form_state['values']["min-height-$group->tid"], $form_state['values']["max-height-$group->tid"]);
+    }
+  }
+  drupal_set_message('Image ad global settings updated.');
+}
+
+/**
+ * Implementation of hook_adapi().
+ */
+function ad_image_adapi($op, &$node) {
+  $output = NULL;
+  switch ($op) {
+
+    case 'load':
+      $return = db_fetch_array(db_query("SELECT a.*, f.filepath FROM {ad_image} a LEFT JOIN {files} f ON a.fid = f.fid WHERE aid = %d", $node['aid']));
+      $return['ad'] = '<img src="'. file_create_url($return['filepath']) .'" width="'. $return['width'] .'" height="'. $return['height'] .'" alt="'. check_plain($return['tooltip']) .'" />';
+      return $return;
+
+    case 'insert':
+    case 'update':
+      $image = ad_image_load_image($node);
+      $fid = (int)ad_image_active_file($node->files);
+      // This is ugly, but as "a" comes before "u" we don't seem to be able
+      // to modify the upload module's form.  Instead, we check after the fact
+      // if someone is editing images when they're not allowed, and if so we
+      // prevent the ad from being saved.
+      if ($op == 'update' && !ad_adaccess($node, 'manage active ad')) {
+        // See if fid is changing -- it's okay if new images are uploaded, it's
+        // just not okay if the active fid is changed.
+        if ($fid != $image->fid) {
+          drupal_set_message('You do not have the necessary permissions to change the active advertisement.', 'error');
+          // This causes upload_save() to simply return without making any 
+          // changes to the files attached to this node.
+          unset($node->files);
+        }
+      }
+      else {
+        // Check that all values are valid -- this is a kludge to work around
+        // bug #146147 until the problem is better understood.
+        $width = isset($image->width) ? $image->width : 0;
+        $height = isset($image->height) ? $image->height : 0;
+        $fid = isset($image->fid) ? $image->fid : 0;
+        if ($image !== FALSE && $width != 0 && $height != 0 && $fid != 0) {
+          $node->fid = $image->fid;
+          $node->width = $image->width;
+          $node->height = $image->height;
+        }
+        else {
+          $image = FALSE;
+        }
+      }
+      if ($op == 'insert') {
+        db_query("INSERT INTO {ad_image} (aid, fid, url, tooltip, width, height) VALUES(%d, %d, '%s', '%s', %d, %d)", $node->nid, $node->fid, $node->url, $node->tooltip, $node->width, $node->height);
+      }
+      else {
+        db_query("UPDATE {ad_image} SET fid = %d, url = '%s', tooltip = '%s', width = %d, height = %d WHERE aid = %d", $fid, $node->url, $node->tooltip, $node->width, $node->height, $node->nid);
+      }
+      // No valid image has been uploaded, don't allow ad to be 'active'.
+      if ($image === FALSE || !ad_image_active_file(($node->files))) {
+        db_query("UPDATE {ads} SET adstatus = '%s' WHERE aid = %d AND adstatus = '%s'", t('pending'), $node->nid, t('active'));
+        if (db_affected_rows()) {
+          drupal_set_message(t('Image validation failed, unable to mark ad as %active.  Setting ad as %pending.  If you do not see any more errors, you should now be able to set your ad as %active.', array('%active' => t('active'), '%pending' => t('pending'))), 'error');
+        }
+      }
+      else if (!$fid) {
+        db_query("UPDATE {ads} SET adstatus = '%s' WHERE aid = %d AND adstatus = '%s'", t('pending'), $node->nid, t('active'));
+        if (db_affected_rows()) {
+          drupal_set_message(t('Unable to mark ad as <em>active</em> until uploaded image is validated.  If you do not see any more errors, you should now be able to set your ad as <em>active</em>.'), 'error');
+        }
+      }
+      break;
+
+    case 'validate':
+      if ($node->url && variable_get('ad_validate_url', 1) && (!valid_url($node->url, TRUE))) {
+        form_set_error('url', t('You must specify a valid %field.', array('%field' => t('Destination URL'))));
+      }
+      if (!isset($node->files) || !ad_image_active_file($node->files)) {
+        form_set_error('upload', t('It is required that you upload an image for your image advertisement.'));
+      }
+      break;
+
+    case 'delete':
+      db_query('DELETE FROM {ad_image} WHERE aid = %d', $node->nid);
+      break;
+
+    case 'form':
+      return ad_image_node_form($node);
+
+    case 'view':
+      return ad_image_node_view($node);
+
+    case 'redirect':
+      return db_result(db_query('SELECT url FROM {ad_image} WHERE aid = %d', $node->nid));
+
+    case 'type':
+      return array(
+        'image' => array(
+          'name' => t('Image ad'),
+          'module' => 'ad_image',
+          'description' => t('An image or banner advertisement.'),
+          'help' => t('An image or banner advertisement.'),
+        ),
+      );
+    case 'permissions':
+      if (!isset($node->adtype) || $node->adtype == 'image') {
+        return array('manage active ad');
+      }
+
+    case 'check_install':
+      if (!module_exists('upload')) {
+        drupal_set_message(t("The required <em>upload module</em> is not enabled, you will not be able to upload image ads.  Please %enable the upload module, or %disable the ad_image module.", array('%enable' => l('enable', 'admin/modules'), '%disable' => l('disable', 'admin/modules'))), 'error');
+      }
+      if (is_object($node) && !variable_get("upload_$node->type", TRUE)) {
+        drupal_set_message(t('You will not be able to upload image ads until you !enable for the advertisement content type.', array('!enable' => l(t('enable attachments'), 'admin/content/types/ad'))), 'error');
+      }
+      if (empty($node)) {
+        if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) ==  FILE_DOWNLOADS_PRIVATE) {
+          drupal_set_message(t('Your website is configured to use Drupal\'s private !method.  You have to enable "!view" permissions in addition to the ad module\'s standard "!show" permissions for all roles that you wish to view image advertisements.', array('!method' => l(t('download method'), 'admin/settings/file-system'), '!view' => l(t('view uploaded files'), 'admin/user/access'), '!show' => l(t('show advertisements'), 'admin/user/access'))));
+        }
+      }
+
+      break;
+
+  }
+
+  return $output;
+}
+
+/**
+ * Determine the currently active ad.
+ */
+function ad_image_active_file($files = array()) {
+  if (is_array($files)) {
+    foreach ($files as $fid => $data) {
+      if (is_array($data)) {
+        if ($data['list'] && !$data['remove']) {
+          return $fid;
+        }
+      }
+      else if ($data->list && !isset($data->remove)) {
+        return $fid;
+      }
+    }
+  }
+  return 0;
+}
+
+/**
+ * Loads image format object from DB.
+ */
+function ad_image_format_load($gid) {
+  static $format;
+  if (isset($format[$gid])) {
+    return $format[$gid];
+  }
+  $format[$gid] = db_fetch_object(db_query('SELECT min_width, max_width, min_height, max_height FROM {ad_image_format} WHERE gid = %d', $gid));
+  return $format[$gid];
+}
+
+/**
+ * Validate that the size of the uploaded image is within the defined limits.
+ */
+function ad_image_validate_size($file, $nid) {
+  $size = NULL;
+  $error = FALSE;
+  $edit = isset($_POST['edit']) ? $_POST['edit'] : array();
+  if (is_object($file)) {
+    // TODO: Detect if new terms have been set, and if so validate against
+    // them, not the old ones.  See what's in $edit['taxonomy'].
+    $node = node_load($nid);
+    $terms = module_invoke('taxonomy', 'node_get_terms', $node);
+    if (count($terms) == 0) {
+      // We need at least a single (NULL) term to be ensure we still get the
+      // default image size.
+      $terms[] = NULL;
+    }
+    foreach ($terms as $tid => $term) {
+      list($size->width, $size->height) = getimagesize($file->filepath);
+      if ($format = ad_image_format_load($tid)) {
+        if ($size->width < $format->min_width) {
+          drupal_set_message(t('The image <em>%name</em> is only %current pixels wide, which is less than the minimum of %minimum pixels allowed in the %group ad group.', array('%name' => $file->filename, '%current' => $size->width, '%minimum' => $format->min_width, '%group' => theme_placeholder($term->name))), 'error');
+          $error = TRUE;
+        }
+        else if ($format->max_width && ($size->width > $format->max_width)) {
+          drupal_set_message(t('The image <em>%name</em> is %current pixels wide, which is more than the maximum of %maximum pixels allowed in the %group ad group.', array('%name' => $file->filename, '%current' => $size->width, '%maximum' => $format->max_width, '%group' => theme_placeholder($term->name))), 'error');
+          $error = TRUE;
+        }
+        if ($size->height < $format->min_height) {
+          drupal_set_message(t('The image <em>%name</em> is only %current pixels high, which is less than the minimum of %minimum pixels allowed in the %group ad group.', array('%name' => $file->filename, '%current' => $size->height, '%minimum' => $format->min_height, '%group' => theme_placeholder($term->name))), 'error');
+          $error = TRUE;
+        }
+        else if ($format->max_height && $size->height > $format->max_height) {
+          drupal_set_message(t('The image <em>%name</em> is %current pixels high, which is more than the maximum of %maximum pixels allowed in the %group ad group.', array('%name' => $file->filename, '%current' => $size->height, '%maximum' => $format->max_height, '%group' => theme_placeholder($term->name))), 'error');
+          $error = TRUE;
+        }
+      }
+    }
+  }
+  else {
+    $error = TRUE;
+    drupal_set_message('Please report error: $file is not an object, bug #146147.');
+  }
+  if ($error) {
+    return FALSE;
+  }
+  else {
+    return $size;
+  }
+}
+
+/**
+ * Returns image object from given ad node.
+ */
+function ad_image_load_image($node) {
+  if (is_array($node->files)) {
+    foreach ($node->files as $file) {
+      if (is_array($file)) {
+        if ($file['list'] && file_exists($file['filepath'])) {
+          $image = ad_image_validate_size((object)$file, $node->nid);
+          if ($image !== FALSE) {
+            $image->fid = $file['fid'];
+            return $image;
+          }
+        }
+      }
+      else {
+        if ($file->list && file_exists($file->filepath)) {
+          $image = ad_image_validate_size($file, $node->nid);
+          if ($image !== FALSE) {
+            $image->fid = $file->fid;
+            return $image;
+          }
+        }
+      }
+    }
+  }
+  return FALSE;
+}
+
+/**
+ * Adapi helper function for displaying a node form.
+ */
+function ad_image_node_form(&$node) {
+  $form = array();
+
+  ad_image_adapi('check_install', $node);
+
+  $form['ad_image'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Image'),
+    '#collapsible' => TRUE,
+  );
+
+  if (isset($node->files)) {
+    $files = $node->files;
+  }
+  else {
+    if (!isset($node->vid)) {
+      $node->vid = '';
+    }
+    $files = module_invoke('upload', 'load', $node);
+  }
+  $num = sizeof($files);
+
+  $path = NULL;
+  $active = 0;
+  if ($num) {
+    foreach ($files as $file) {
+      if ($file->list && file_exists($file->filepath)) {
+        $path .= '<img src="'. file_create_url($file->filepath) .'" alt="'. check_plain($file->filename) .'" /> ';
+        $image = ad_image_validate_size($file, $node->nid);
+        if ($image === FALSE) {
+          $path .= t('(invalid image)'). '<br />';
+        }
+        else if (!$active++) {
+          $path .= t('(active)'). '<br />';
+        }
+        else {
+          $path .= t('(inactive)'). '<br />';
+        }
+      }
+      else if (!file_exists($file->filepath)) {
+        drupal_set_message(t('Unable to locate image %image.', array('%image' => "$file->filepath")));
+        $path .= t('Unable to locate the uploaded image.');
+      }
+    }
+  }
+  if ($path == NULL) {
+    $path = t('No images have been uploaded.  Please upload an image via the <em>File attachments</em> form section below.<br />');
+    // Only set error if node has been previewed or submitted.
+    if (isset($_POST['edit'])) {
+      form_set_error('upload', t('It is required that you upload an image for your image advertisement.'));
+    }
+  }
+
+  $path .= t('<br />Only the first uploaded image that has <em>List</em> checked in the <em>File attachments</em> form section below will be displayed as an advertisement.  The image that will be displayed is marked as <em>active</em> above.');
+
+  $form['ad_image']['image'] = array(
+    '#type' => 'markup',
+    '#value' => $path,
+    '#prefix' => '<div class="container-inline">',
+    '#suffix' => '</div>',
+  );
+
+  $form['ad_image']['url'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Destination URL'),
+    '#required' => FALSE,
+    '#default_value' => isset($node->url) ? $node->url : '',
+    '#description' => t('Enter the complete URL where you want people to be redirected when they click on this advertisement.  The URL must be valid and begin with http:// or https://, for example %url, unless you !disable.  If you do not enter a URL, the advertisement will not be clickable.', array('%url' => t('http://www.sample.org/'), '!disable' => l(t('disable URL validation'), 'admin/content/ad/configure', array('fragment' => 'edit-ad-validate-url-wrapper')))),
+  );
+
+  $form['ad_image']['tooltip'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Mouseover'),
+    '#required' => FALSE,
+    '#default_value' => isset($node->tooltip) ? $node->tooltip : '',
+    '#description' => t('Optionally enter text to appear when a mouse pointer hovers over the ad image.'),
+  );
+
+  return $form;
+}
+
+/**
+ * Adapi helper function for displaying ad itself.
+ */
+function ad_image_node_view(&$node) {
+  $node->content['ad'] = array(
+    '#value' => preg_replace('&@HOSTID___&', '0', ad_image_display_ad($node)),
+    '#weight' => -1,
+  );
+  if (!empty($node->url)) {
+    $link = t('Links to !url.', array('!url' => $node->url));
+    $link = check_plain($link, $node->format, FALSE);
+    $node->content['ad-link'] = array(
+      '#value' => "<div class=\"links-to\">$link</div>",
+      '#weight' => 0,
+    );
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/imageserve.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,61 @@
+<?php
+// $Id: imageserve.inc,v 1.1.2.8.2.2 2009/02/16 17:06:47 jeremy Exp $
+
+/**
+ * @file
+ * Image serving lib.
+ *
+ * Copyright (c) 2008-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Generate a tiny image with GD, used to count when an ad has been displayed
+ * on a cached page.
+ */
+function adserve_counter_image() {
+  adserve_variable('variable_load');
+  adserve_bootstrap(0);
+
+  $ad->aid = adserve_variable('aid');
+  if ($ad->aid) {
+    adserve_increment($ad);
+  }
+  else {
+    adserve_increment($ad, 'count');
+  }
+
+  if (function_exists('imagecreate')) {
+    $image = imagecreate(1, 1);
+    // Tell the web browser not to cache this image so we register a count each
+    // time the page is viewed.
+    // Expires in the past:
+    header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+    // Last loud:
+    header('Last-Modified: '. gmdate('D, d M Y H:i:s') .' GMT');
+    // HTTP 1.1:
+    header('Cache-Control: no-store, no-cache, must-revalidate');
+    header('Cache-Control: post-check=0, pre-check=0', FALSE);
+    // HTTP 1.0:
+    header('Pragma: no-cache');
+  }
+  else {
+    // GD not installed, report error and exit.
+    exit();
+  }
+
+  if (function_exists('imagejpeg')) {
+    header("Content-type: image/jpeg");
+    imagejpeg($image);
+  }
+  else if (function_exists('imagepng')) {
+    header("Content-type: image/png");
+    imagepng($image);
+  }
+  else if (function_exists('imagegif')) {
+    header("Content-type: image/gif");
+    imagegif($image);
+  }
+  imagedestroy($image);
+  exit(0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notify/ad_notify.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,13 @@
+; $Id: ad_notify.info,v 1.1.2.1.2.3 2009/02/09 20:39:09 jeremy Exp $
+name = Notify
+package = Ad
+dependencies[] = ad_owners
+description = Receive email notifications regarding ads.
+core = 6.x
+
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notify/ad_notify.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,132 @@
+<?php
+// $Id: ad_notify.install,v 1.1.2.2.2.6.2.3 2009/02/16 17:06:49 jeremy Exp $
+
+/**
+ * @file
+ * Ad_notify module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_notify_schema() {
+  $schema['ad_notify'] = array(
+    'description' => 'The ad_notify table stores notifications data such as recepient, message body, event, etc.',
+    'fields' => array(
+      'notid' => array(
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'aid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'oid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'event' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => ''
+      ),
+      'delay' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'queued' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'sent' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'counter' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'locked' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'expire' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'status' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'address' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'subject' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'body' => array(
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+    ),
+    'primary key' => array('notid'),
+    'unique keys' => array(
+      'oid' => array('oid', 'event', 'delay'),
+    ),
+    'indexes' => array(
+      'delay' => array('delay'),
+      'event' => array('event'),
+      'oid_2' => array('oid'),
+      'queued' => array('queued'),
+      'sent' => array('sent'),
+      'status' => array('status'),
+    ),
+  );
+
+  return $schema;
+}
+
+/**
+ * ad_notify module installation.
+ */
+function ad_notify_install() {
+  drupal_install_schema('ad_notify');
+}
+
+
+/**
+ * Allow complete uninstallation of the ad_notify module.
+ */
+function ad_notify_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('ad_notify');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/notify/ad_notify.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,617 @@
+<?php
+// $Id: ad_notify.module,v 1.1.2.2.2.13.2.6 2009/02/16 23:12:29 jeremy Exp $
+
+/**
+ * @file
+ * Receive email notifications regarding ads.
+ *
+ * Copyright (c) 2005-2008.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+define('AD_NOTIFY_DISABLED', 0);
+define('AD_NOTIFY_ENABLED', 1);
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_notify_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'admin/help#ad_notify':
+      $output = '<p>'. t('The ad_notify modules provides email notifications for the ad module.') .'</p>';
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_notify_menu() {
+  $items = array();
+  
+  $items['node/%node/adowners/%user/notifications'] = array(
+    'title callback' => 'owner_notifications_title',
+    'title arguments' => array('!owner' => 3),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_notify_overview_form', 1, 3),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, 'manage owners'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 4,
+  );
+  $items['node/%node/notifications'] = array(
+    'title' => 'My notifications',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_notify_overview_form', 1),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, array('manage owners', 'manage own notifications')),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 4,
+  );
+  $items['node/%node/adowners/%user/notifications/%ad_notification/delete'] = array(
+    'title' => 'Delete notification',
+    'page callback' => 'ad_notify_confirm_delete_page',
+    'page arguments' => array(1, 3, 5),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, array('manage owners', 'manage own notifications')),
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ * Menu item title callback - use the user name
+ */
+function owner_notifications_title($account) {
+  return t('!owner\'s notifications', array('!owner' => $account->name));
+}
+
+/**
+ * Implementation of hook_cron().
+ * For performance reasons, all notifications are actually sent via this cron 
+ * hook.
+ */
+function ad_notify_cron() {
+  // Walk through all configured notifications and determine if any need to be
+  // emailed. 
+  $result = db_query('SELECT n.notid, o.aid, n.oid, n.event, n.queued, n.delay, n.sent, n.expire, n.address, n.subject, n.body FROM {ad_notify} n INNER JOIN {ad_owners} o ON n.oid = o.oid WHERE n.status = %d', AD_NOTIFY_ENABLED);
+  while ($notification = db_fetch_object($result)) {
+    $send = FALSE;
+    // Handle special case 'regular' notification that is simply a time-based
+    // status email.
+    if ($notification->event == 'regular') {
+      if ((time() - $notification->delay) >= $notification->sent) {
+        $send = TRUE;
+        $count = 1;
+      }
+    }
+    // Handle event based notifications based on information stored in the 
+    // ad_statistics table.
+    else {
+      if (($event = trim($notification->event, '-')) != $notification->event) {
+        // Event was prefixed by a -, so time is negative.  We can't pull a
+        // future event out of the statistics table, so we let the module that
+        // defined this event tell us whether or not it's happened.
+        $event_count = module_invoke_all('adnotifyapi', $notification->event, $notification);
+        if (isset($event_count[$notification->event])) {
+          $send = TRUE;
+        }
+      }
+
+      else {
+        $count = db_result(db_query("SELECT COUNT(aid) AS count FROM {ad_statistics} WHERE aid = %d AND date > %d AND action = '%s'", $notification->aid, date('YmdH', $notification->sent), $notification->event));
+        if ($count) {
+          // See if the notification has been queued long enough to be sent.
+          if (!$notification->delay || ($notification->queued &&
+              (time() > ($notification->queued + $notification->delay)))) {
+            $send = TRUE;
+          }
+          else if (!$notification->queued) {
+            // Queue up the notification to send it at a later time.
+            db_query('UPDATE {ad_notify} SET queued = %d WHERE notid = %d', time(), $notification->notid);
+          }
+        }
+      }
+    }
+
+    if ($send) {
+      ad_notify_send_mail($notification, $count);
+      if ($notification->expire) {
+        // Update the sent timestamp and counter, and auto-expire the 
+        // notification so it is not sent again.
+        db_query('UPDATE {ad_notify} SET queued = 0, sent = %d, counter = counter + 1, status = %d WHERE notid = %d', time(), AD_NOTIFY_DISABLED, $notification->notid);
+      }
+      else {
+        // Simply update the sent timestamp and counter.
+        db_query('UPDATE {ad_notify} SET queued = 0, sent = %d, counter = counter + 1 WHERE notid = %d', time(), $notification->notid);
+      }
+    }
+  }
+}
+
+/**
+ * Send email notifications using PHP mail() function.
+ */
+function ad_notify_send_mail($notification, $count = 0) {
+  $uid = db_result(db_query('SELECT uid FROM {ad_owners} WHERE oid = %d', $notification->oid));
+  $node = node_load(array('nid' => $notification->aid));
+  $owner = user_load(array('uid' => $uid));
+  $statistics = ad_statistics($notification->aid);
+  $notifications = module_invoke_all('adnotifyapi', 'register');
+  $variables = array(
+    '%owner_name' => $owner->name,
+    '%owner_mail' => $owner->mail,
+    '%owner_uid' => $owner->uid,
+    '%sitename' => variable_get('site_name', 'drupal'),
+    '%status' => $node->adstatus,
+    '%type' => $node->adtype,
+    '%event' => $notification->event,
+    '%frequency' => t(strtolower($notifications[$notification->event]), array('@when' => format_interval($notification->delay))),
+    '%redirect' => url($node->redirect, array('absolute' => TRUE)),
+    '%aid' => $notification->aid,
+    '%title' => $node->title,
+    '%url' => url("node/$node->nid", array('absolute' => TRUE)),
+    '%siteurl' => url('', array('absolute' => TRUE)),
+    '%comments' => $node->comment_count,
+    '%count' => $count,
+    '%created_small' => format_date($node->created, 'small'),
+    '%created_medium' => format_date($node->created, 'medium'),
+    '%created_large' => format_date($node->created, 'large'),
+    '%activated_small' => $node->activated ? format_date($node->activated, 'small') : t('never'),
+    '%activated_medium' => $node->activated ? format_date($node->activated, 'medium') : t('never'),
+    '%activated_large' => $node->activated ? format_date($node->activated, 'large') : t('never'),
+    '%expired_small' => $node->expired ? format_date($node->expired, 'small') : t('never'),
+    '%expired_medium' => $node->expired ? format_date($node->expired, 'medium') : t('never'),
+    '%expired_large' => $node->expired ? format_date($node->expired, 'large') : t('never'),
+    '%autoactivate_small' => $node->autoactivate ? format_date($node->autoactivate, 'small') : t('never'),
+    '%autoactivate_medium' => $node->autoactivate ? format_date($node->autoactivate, 'medium') : t('never'),
+    '%autoactivate_large' => $node->autoactivate ? format_date($node->autoactivate, 'large') : t('never'),
+    '%autoexpire_small' => $node->autoexpire ? format_date($node->autoexpire, 'small') : t('never'),
+    '%autoexpire_medium' => $node->autoexpire ? format_date($node->autoexpire, 'medium') : t('never'),
+    '%autoexpire_large' => $node->autoexpire ? format_date($node->autoexpire, 'large') : t('never'),
+    '%maxviews' => $node->maxviews,
+    '%maxclicks' => $node->maxclicks,
+    '%global_views' => $statistics['global']['views'],
+    '%global_clicks' => $statistics['global']['clicks'],
+    '%last_year_views' => $statistics['last_year']['views'],
+    '%last_year_clicks' => $statistics['last_year']['clicks'],
+    '%this_year_views' => $statistics['this_year']['views'],
+    '%this_year_clicks' => $statistics['this_year']['clicks'],
+    '%last_month_views' => $statistics['last_month']['views'],
+    '%last_month_clicks' => $statistics['last_month']['clicks'],
+    '%this_month_views' => $statistics['this_month']['views'],
+    '%this_month_clicks' => $statistics['this_month']['clicks'],
+    '%this_week_views' => $statistics['this_week']['views'],
+    '%this_week_clicks' => $statistics['this_week']['clicks'],
+    '%yesterday_views' => $statistics['yesterday']['views'],
+    '%yesterday_clicks' => $statistics['yesterday']['clicks'],
+    '%today_views' => $statistics['today']['views'],
+    '%today_clicks' => $statistics['today']['clicks'],
+    '%last_hour_views' => $statistics['last_hour']['views'],
+    '%last_hour_clicks' => $statistics['last_hour']['clicks'],
+    '%this_hour_views' => $statistics['this_hour']['views'],
+    '%this_hour_clicks' => $statistics['this_hour']['clicks'],
+  );
+  // TODO: Add hook to allow other modules to define variables.
+
+  // TODO: Should the from_address be configurable?
+  drupal_mail(
+    'ad_notify_mail',                                   // mail key
+    $notification->address,                             // to address
+    strtr($notification->subject, $variables),          // subject
+    wordwrap(strtr($notification->body, $variables), 72),      // message
+    variable_get('site_mail', ini_get('sendmail_from')) // from address
+  );
+}
+
+/**
+ * Implementation of hook_adapi().
+ */
+function ad_notify_adapi($op, &$node) {
+  $output = NULL;
+  switch ($op) {
+    case 'statistics_increment':
+      break;
+
+    case 'permissions':
+      return array('manage own notifications', 'edit notification email');
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_adnotifyapi().
+ */
+function ad_notify_adnotifyapi($op, $arg1 = NULL, $arg2 = NULL) {
+  switch ($op) {
+    case 'register':
+      return array(
+        'regular' => t('Email every @when as long as the ad is active.'),
+      );
+      break;
+  }
+}
+
+/**
+ * Notification overview form.
+ */
+function ad_notify_overview_form($form_state, $node, $owner = NULL, $notid = 0) {
+  global $user;
+  if (arg(2) == 'notifications') {
+    drupal_set_title('My notifications');
+  }
+  else {
+    drupal_set_title('Notifications');
+  }
+  if (!isset($owner)) {
+    $owner = $user;
+  }
+
+  $oid = db_result(db_query('SELECT oid FROM {ad_owners} WHERE aid = %d AND uid = %d', $node->nid, $owner->uid));
+  if (isset($oid)) {
+    $notifications = module_invoke_all('adnotifyapi', 'register');
+
+    $header = array(
+      array('data' => t('Last sent'), 'field' => 'sent', 'sort' => 'desc'),
+      array('data' => t('Notification'), 'field' => 'event'),
+      array('data' => t('Status'), 'field' => 'status'),
+      array('data' => t('Action'))
+    );
+
+    $sql = "SELECT notid, event, delay, sent, address, status FROM {ad_notify} WHERE oid = %d";
+    $sql .= tablesort_sql($header);
+    $result = pager_query($sql, 25, 0, NULL, $oid);
+
+    $rows = array();
+    while ($notify = db_fetch_object($result)) {
+      $row = array();
+      $row[] = $notify->sent ? t('!time ago', array('!time' => format_interval(time() - $notify->sent))) : t('Never');
+      $row[] = t($notifications[$notify->event], array('@when' => format_interval($notify->delay)));
+      $row[] = $notify->status == AD_NOTIFY_ENABLED ? t('enabled') : t('disabled');
+      $row[] = l(t('edit'), 'node/'. $node->nid .'/adowners/'. $owner->uid .'/notifications/' .$notify->notid. '/edit') .' '. l(t('delete'), 'node/'. $node->nid .'/adowners/'. $owner->uid .'/notifications/'. $notify->notid .'/delete');
+      $rows[] = $row;
+    }
+    $output = theme('table', $header, $rows);
+    $output .= theme('pager', NULL, 25, 0);
+  }
+
+  $form = array();
+
+  if ($notid) {
+    $notification = ad_notification_load($notid);
+  }
+
+  $help = '<p>'. t('You can configure one or more notifications for your advertisement using the drop down menus below.  For example, to receive a weekly notification with information about how often your advertisement was viewed and clicked, select the <em>email every @when as long as the ad is active</em> event, and <em>1 week</em> for when.  Or, to receive a reminder that your advertisement will expire in 24 hours select the <em>email @when before the advertisement will expire</em>, and <em>1 day</em> for when.') .'</p>';
+  $help .= '<p>'. t('If you schedule a delay between an event and when you are notified and the event happens multiple times, only one notification will be sent.  For example, if you create a notification for <em>email 1 day after the advertisement is clicked</em> and the ad is clicked 42 more times in the next 24 hours, you will only receive one email 24 hours after your ad was first clicked that notes that your ad was clicked a total of 43 times in the past 24 hours.') .'</p>';
+  $form['create'] = array(
+    '#type' => 'fieldset',
+    '#description' => $help,
+    '#title' => $notid ? t('Edit notification') : t('Create new notification'),
+    '#collapsible' => TRUE,
+    '#collapsed' => ($rows == array() || $notid) ? FALSE : TRUE,
+  );
+
+  $form['create']['event'] = array(
+    '#type' => 'select',
+    '#title' => t('Event'),
+    '#options' => $notifications,
+    '#description' => t('Select an event for which you would like to receive a notification.'),
+    '#default_value' => $notid ? $notification->event : 1,
+  );
+
+  $form['create']['delay'] = array(
+    '#type' => 'select',
+    '#title' => t('When'),
+    '#options' => drupal_map_assoc(array(0,3600,10800,21600,43200,86400,259200,432000,604800,1209600,1814400,2419200,4838400,9676800,31536000), 'format_interval'),
+    '#description' => t('Select a value to replace @when in the event notification you selected above.'),
+    '#default_value' => $notid ? $notification->delay : 0,
+  );
+
+  $form['create']['expire'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('One-time'),
+    '#description' => t('Check this box if this notification email should only be sent one time.  If not checked, an email will be sent each time the event happens.  If checked, an email will only be sent the first time the event happens, then the notification will be automatically disabled.'),
+    '#default_value' => $notid ? $notification->expire : 0,
+  );
+
+  if (ad_adaccess($node->nid, 'manage owners') && arg(2) == 'adowners' && 
+      $user->uid != arg(3)) {
+    $form['create']['locked'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Locked'),
+      '#description' => t('Check this box if you are setting up a notification for someone else, and you don\'t want them to be able to disable the notification.  Only users with the <em>manage owners</em> permission for this ad can edit or delete a locked notification.'),
+      '#default_value' => $notid ? $notification->locked : 0,
+    );
+  }
+  else {
+    $form['create']['locked'] = array(
+      '#type' => 'hidden',
+      '#value' => $notid ? $notification->locked : 0,
+    );
+  }
+
+  if ($notid) {
+    $form['create']['mail'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Message'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+    );
+  }
+
+  // TODO: Make it possible for admins to modify email address, and even to
+  // enter multiple email addresses.  Wrap this in a special permission, as 
+  // it involves trust to configure notifications to unverified addresses.
+  $form['create']['mail']['address-display'] = array(
+    '#type' => 'markup',
+    '#value' => '<b>'. t('Notify address') .':</b><br />'. t('The email will be sent to %address.', array('%address' => $owner->mail)),
+    '#prefix' => '<div class="container-inline">',
+    '#suffix' => '</div>',
+  );
+
+  $form['create']['mail']['address'] = array(
+    '#type' => 'hidden',
+    '#value' => $owner->mail,
+  );
+
+  if ($notid) {
+    $form['create']['mail']['subject'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Subject'),
+      '#required' => TRUE,
+      '#default_value' => $notification->subject,
+    );
+
+    $form['create']['mail']['body'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Body'),
+      '#required' => TRUE,
+      '#default_value' => $notification->body,
+      '#description' => t('Enter the body of your notification email.  The following variables can be used in your message and will be automatically replaced before the email is sent:') .'<ul>'
+          .'<li>'. t('%sitename: the name of this website.') 
+          .'<li>'. t('%owner_name: the username of the ad owner.') 
+          .'<li>'. t('%owner_mail: the email address of the ad owner.') 
+          .'<li>'. t('%owner_uid: the user ID of the ad owner.') 
+          .'<li>'. t('%event: the type of event that has triggered this notification.')
+          .'<li>'. t('%count: the number of times the event happened.') 
+          .'<li>'. t('%frequency: a complete sentence describing the frequency this notification will be sent.') 
+          .'<li>'. t('%type: the type of ad.') 
+          .'<li>'. t('%status: the status of the ad.') 
+          .'<li>'. t('%url: the url of the advertisement.') 
+          .'<li>'. t('%siteurl: the url of the website.') 
+          .'<li>'. t('%redirect: the redirection url of the advertisement.') 
+          .'<li>'. t('%title: the title of the advertisement.') 
+          .'<li>'. t('%aid: the ID of the advertisement.') 
+          .'<li>'. t('%comments: the number of comments attached to the advertisement.') 
+          .'<li>'. t('%created_small, %created_medium, %created_large: various formats of when the advertisement was created.') 
+          .'<li>'. t('%activated_small, %activated_medium, %activated_large: various formats of when the advertisement was activated.') 
+          .'<li>'. t('%expired_small, %expired_medium, %expired_large: various formats of when the advertisement was expired.') 
+          .'<li>'. t('%autoactivate_small, %autoactivate_medium, %autoactivate_large: various formats of when the advertisement was automatically activated.') 
+          .'<li>'. t('%autoexpire_small, %autoexpire_medium, %autoexpire_large: various formats of when the advertisement was automatically expired.') 
+          .'<li>'. t('%maxviews: the maximum number of times this advertisement is allowed to be viewed.') 
+          .'<li>'. t('%maxclicks: the maximum number of times this advertisement is allowed to be clicked.') 
+          .'<li>'. t('%global_views, %global_clicks, %last_year_views, %last_year_clicks, %this_year_views, %this_year_clicks, %last_month_views, %last_month_clicks, %this_month_views, %this_month_clicks, %this_week_views, %this_week_clicks, %yesterday_views, %yesterday_clicks, %today_views, %today_clicks, %last_hour_views, %last_hour_clicks, %this_hour_views, %this_hour_clicks: various advertisement statistics')
+          .'</ul>',
+    );
+  }
+
+  $form['create']['oid'] = array(
+    '#type' => 'hidden',
+    '#value' => $oid,
+  );
+
+  $form['create']['aid'] = array(
+    '#type' => 'hidden',
+    '#value' => $node->nid,
+  );
+
+  $form['create']['uid'] = array(
+    '#type' => 'hidden',
+    '#value' => $owner->uid,
+  );
+
+  if ($notid) {
+    $form['create']['notid'] = array(
+      '#type' => 'hidden',
+      '#value' => $notid,
+    );
+    $form['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+    );
+  }
+  else {
+    $form['create']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Create notification'),
+    );
+  }
+
+  if ($rows != array()) {
+    if (!$notid) {
+      $form['notifications'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Notifications'),
+        '#collapsible' => TRUE,
+      );
+      $form['notifications']['current'] = array(
+        '#type' => 'markup',
+        '#value' => $output,
+      );
+    }
+  }
+  else {
+    $form['notifications'] = array(
+      '#type' => 'markup',
+      '#value' => '<p>'. t('There are no notifications configured for %owner.', array('%owner' => $owner->name)) .'</p>',
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Validate ad notifications before saving to database.
+ */
+function ad_notify_overview_form_validate($form, &$form_state) {
+  $redirect = FALSE;
+  if ($form_state['values']['event'] == 'regular' && $form_state['values']['delay'] < 3600) {
+    drupal_set_message(t('You are not allowed to schedule a regular notification more frequently than once an hour.'), 'error');
+    $redirect = TRUE;
+  }
+  else if (!isset($form_state['values']['notid'])) {
+    if (db_result(db_query("SELECT notid FROM {ad_notify} WHERE oid = %d AND event = '%s' AND delay = %d", $form_state['values']['oid'], $form_state['values']['event'], $form_state['values']['delay']))) {
+      drupal_set_message(t('You have already scheduled that notification.'), 'error');
+      $redirect = TRUE;
+    }
+  }
+  else if ($form_state['values']['locked'] && !ad_adaccess($form_state['values']['aid'], 'manage owners')) {
+    $redirect = TRUE;
+    drupal_set_message(t('This notification is locked, you will need to contact the site administrator to edit this notification for you.'), 'error');
+  }
+
+  if ($redirect) {
+    if (arg(2) == 'adowners' && arg(4) == 'notifications') {
+      drupal_goto('node/'. $form_state['values']['aid'] .'/adowners/'. $form_state['values']['uid'] .'/notifications');
+    }
+    else {
+      drupal_goto('node/'. $form_state['values']['aid'] .'/notifications');
+    }
+  }
+}
+
+/**
+ * Save notifications to database.
+ */
+function ad_notify_overview_form_submit($form, &$form_state) {
+  if (isset($form_state['values']['notid'])) {
+    db_query("UPDATE {ad_notify} SET aid = %d, oid = %d, event = '%s', delay = %d, expire = %d, locked = %d, status = %d, address = '%s', subject = '%s', body = '%s' WHERE notid = %d", $form_state['values']['aid'], $form_state['values']['oid'], $form_state['values']['event'], $form_state['values']['delay'], $form_state['values']['expire'], $form_state['values']['locked'], AD_NOTIFY_ENABLED, $form_state['values']['address'], $form_state['values']['subject'], $form_state['values']['body'], $form_state['values']['notid']);
+    drupal_set_message('Notification updated.');
+  }
+  else {
+    // Retrieve the default mail subject and body.
+    $mail = module_invoke_all('adnotifyapi', 'mail_text', $form_state['values']['event']);
+    if ($mail == array()) {
+      // Default message text.
+      $mail = array(
+        'subject' => t('[%sitename ad] %event notification'),
+        'body' => t("Hello %owner_name,\n\n  This is an automatically generated notification about your advertisement \"%title\" that is being displayed on the %sitename website.\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"),
+      );
+    }
+    db_query("INSERT INTO {ad_notify} (aid, oid, event, delay, expire, locked, status, address, subject, body) VALUES(%d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s')", $form_state['values']['aid'], $form_state['values']['oid'], $form_state['values']['event'], $form_state['values']['delay'], $form_state['values']['expire'], $form_state['values']['locked'], AD_NOTIFY_ENABLED, $form_state['values']['address'], $mail['subject'], $mail['body']);
+    drupal_set_message('Notification created.');
+  }
+  $form_state['redirect'] = 'node/'. $form_state['values']['aid'] .'/adowners/'. $form_state['values']['uid'] .'/notifications';
+}
+
+/**
+ * Load a specified notification from the database, return as an object.
+ */
+function ad_notification_load($notid) {
+  return db_fetch_object(db_query('SELECT * FROM {ad_notify} WHERE notid = %d', $notid));
+}
+
+/**
+ * Display confirm form.
+ */
+function ad_notify_confirm_delete_page($node, $owner, $notification) {
+  return drupal_get_form('ad_notify_confirm_delete', $node, $owner, $notification);
+}
+
+/**
+ * Confirm deletion of a specified notification from the database.
+ */
+function ad_notify_confirm_delete(&$form_state, $node, $owner, $notification) {
+  $form = array();
+
+  $form['oid'] = array(
+    '#type' => 'hidden',
+    '#value' => $notification->oid,
+  );
+
+  $form['aid'] = array(
+    '#type' => 'hidden',
+    '#value' => $node->nid,
+  );
+
+  $form['uid'] = array(
+    '#type' => 'hidden',
+    '#value' => $owner->uid,
+  );
+
+  $form['notid'] = array(
+    '#type' => 'hidden',
+    '#value' => $notification->notid,
+  );
+
+  $form['locked'] = array(
+    '#type' => 'hidden',
+    '#value' => $notification->locked,
+  );
+
+  $form['event'] = array(
+    '#type' => 'fieldset',
+    '#collapsible' => FALSE,
+  );
+  $notifications = module_invoke_all('adnotifyapi', 'register');
+  $form['event']['type'] = array(
+    '#type' => 'markup',
+    '#value' => t($notifications[$notification->event], array('@when' => format_interval($notification->delay))),
+    '#prefix' => '<div class="container-inline">',
+    '#suffix' => '</div>',
+  );
+
+  $form = confirm_form(
+    $form,
+    'Are you sure you want to delete this notification?',
+    'node/'. $node->nid .'/adowners/'. $owner->uid .'/notifications',
+    'This action cannot be undone.',
+    'Delete',
+    'Cancel'
+  );
+  return $form;
+}
+
+
+/**
+ * Validate that the selected notification can be deleted.
+ */
+function ad_notify_confirm_delete_validate($form, &$form_state) {
+  if ($form_state['values']['locked'] && !ad_adaccess($form_state['values']['aid'], 'manage owners')) {
+    drupal_set_message(t('This notification is locked, you will need to contact the site administrator to delete this notification for you.'), 'error');
+    if (arg(2) == 'adowners' && arg(4) == 'notifications') {
+      drupal_goto('node/'. $form_state['values']['aid'] .'/adowners/'. $form_state['values']['uid'] .'/notifications');
+    }
+    else {
+      drupal_goto('node/'. $form_state['values']['aid'] .'/notifications');
+    }
+  }
+}
+
+/**
+ * Delete a specified notification from the database.
+ */
+function ad_notify_confirm_delete_submit($form, &$form_state) {
+  db_query('DELETE FROM {ad_notify} WHERE notid = %d', $form_state['values']['notid']);
+  drupal_set_message('Notification deleted.');
+  $form_state['redirect'] = 'node/'. $form_state['values']['aid'] .'/adowners/'. $form_state['values']['uid'] .'/notifications';
+}
+
+/**
+ * Implementation of hook_adowners().
+ */
+function ad_notify_adowners($op, $arg1 = NULL, $arg2 = NULL) {
+  switch ($op) {
+    case 'overview':
+      return l(t('notifications'), 'node/'. $arg1 .'/adowners/'. $arg2 .'/notifications');
+
+    case 'delete':
+      if ($arg1) {
+        db_query('DELETE FROM {ad_notify} WHERE oid = %d', $arg1);
+      }
+      break;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/owners/ad_owners.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,13 @@
+; $Id: ad_owners.info,v 1.1.2.1 2009/01/29 16:38:02 neochief Exp $
+name = Ad Owners
+package = Ad
+dependencies[] = ad
+description = Enhances the ad module to support ad owners.
+core = 6.x
+
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/owners/ad_owners.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,86 @@
+<?php
+// $Id: ad_owners.install,v 1.1.2.2 2009/02/16 17:06:49 jeremy Exp $
+
+/**
+ * @file
+ * Ad_owners module database schema.
+ *
+ * Copyright (c) 2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_owners_schema() {
+  $schema['ad_owners'] = array(
+    'description' => 'Stores information about ad owners. Every ad can have one or more owners.',
+    'fields' => array(
+      'oid' => array(
+        'type' => 'serial',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'description' => 'Unique owner pair ID.',
+      ),
+      'aid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Ad id.',
+      ),
+      'uid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'The {users}.uid that owns ad.',
+      ),
+    ),
+    'primary key' => array('oid'),
+    'indexes' => array(
+      'aid' => array('aid'),
+      'uid' => array('uid'),
+    ),
+  );
+
+ /**
+  * Permissions can be granted to each owner of each ad.  The same owner
+  * can own multiple ads, and can have different permissions for each ad.
+  */
+  $schema['ad_permissions'] = array(
+    'description' => 'Permissions can be granted to each owner of each ad. The same owner can own multiple ads, and can have different permissions for each ad.',
+    'fields' => array(
+      'oid' => array(
+        'type' => 'int',
+        'not null' => TRUE,
+        'unsigned' => TRUE,
+        'default' => 0,
+        'description' => 'Owner pair ID.',
+      ),
+      'permissions' => array(
+        'type' => 'text',
+        'not null' => FALSE,
+        'size' => 'big',
+        'description' => 'Ad permission info.',
+      ),
+    ),
+    'primary key' => array('oid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * ad_external module installation.
+ */
+function ad_owners_install() {
+  drupal_install_schema('ad_owners');
+}
+
+/**
+ * Allow complete uninstallation of the ad_external module.
+ */
+function ad_owners() {
+  drupal_uninstall_schema('ad_owners');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/owners/ad_owners.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,493 @@
+<?php
+// $Id: ad_owners.module,v 1.1.2.8 2009/02/17 04:34:27 jeremy Exp $
+
+/**
+ * @file
+ * Enhances the ad module to support ad owners.
+ *
+ * Copyright (c) 2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_owners_theme() {
+  return array(
+    'ad_owner_permissions_form' => array(
+      'arguments' => array(
+        'form' => NULL,
+      ),
+    ),
+  );
+};
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_owners_menu() {
+  $items = array();
+
+  $items['node/%node/adowners'] = array(
+    'title' => 'Ad owners',
+    'page callback' => 'ad_owners_overview',
+    'page arguments' => array(1),
+    'access callback' => 'ad_owners_access',
+    'access arguments' => array(1),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 5,
+  );
+  $items['node/%node/adowners/list'] = array(
+    'title' => 'List',
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, 'manage owners'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => 0,
+  );
+  $items['node/%node/adowners/%user/permissions'] = array(
+    'title callback' => 'owner_permissions_title',
+    'title arguments' => array('!owner' => 3),
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_owner_permissions_form', 1, 3),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, 'manage owners'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 2,
+  );
+  $items['node/%node/adowners/%user/remove'] = array(
+    'title' => 'Remove owner',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_owner_remove_form', 1, 3),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, 'manage owners'),
+    'type' => MENU_CALLBACK,
+    'weight' => 6,
+  );
+  $items['node/%node/adowners/add'] = array(
+    'title' => 'Add owner',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_owners_add_form', 1),
+    'access callback' => 'ad_adaccess',
+    'access arguments' => array(1, 'manage owners'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 4,
+  );
+
+  return $items;
+}
+
+/**
+ * Menu item access callback.
+ */
+function ad_owners_access($node) {
+  return ($node->type == 'ad') && ad_adaccess($node, 'manage owners');
+}
+
+/**
+ * Menu item title callback - use the user name
+ */
+function owner_permissions_title($account) {
+  return t('!owner\'s permissions', array('!owner' => $account->name));
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function ad_owners_form_alter(&$form, &$form_state, $form_id) {
+  if ($form_id == 'ad_'. arg(4) .'_global_settings' || $form_id == 'ad_no_global_settings') {
+    if (!isset($form['adtype'])) {
+      $form['adtype'] = array('#type' => 'value', '#value' => arg(4));
+    }
+    $permissions = module_invoke_all('adapi', 'permissions', NULL);
+    $form['permissions'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Permissions'),
+      '#collapsible' => TRUE,
+      '#description' => t('Select which permissions will be automatically granted to new owners of <em>!type</em> advertisements.', array('!type' => ad_get_types('name', arg(4)))),
+    );
+    $form['permissions']['default_permissions'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Default permissions for <em>!type</em> owners', array('!type' => ad_get_types('name', arg(4)))),
+      '#options' => drupal_map_assoc($permissions),
+      '#default_value' => variable_get('ad_'. arg(4) .'_default_permissions', array('access statistics', 'access click history', 'manage status')),
+    );
+    if (isset($form['save'])) {
+      $form['save']['#weight'] = 10;
+    }
+    if (isset($form['#submit']) && is_array($form['#submit'])) {
+      $form['#submit'] = array('ad_global_settings_submit') + $form['#submit'];
+    }
+    else {
+      $form['#submit'] = array('ad_global_settings_submit');
+    }
+  }
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function ad_owners_nodeapi(&$node, $op, $teaser, $page) {
+  global $user;
+
+  switch ($op) {
+    case 'insert':
+    case 'update':
+      if (isset($node->adtype)) {
+        // Be sure ad owner has at least default ad permissions.
+        ad_owners_add($node, $node->uid);
+        ad_host_id_create($node->uid);
+      }
+      break;
+    case 'delete':
+      // 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);
+      break;
+  }
+}
+
+/**
+ * Implementation of hook_adapi().
+ */
+function ad_owners_adapi($op, $node = NULL) {
+  switch ($op) {
+    case 'permissions':
+      return array('manage owners');
+      break;
+  }
+}
+
+/**
+ * Determine whether the ad owner has a given privilege.
+ *
+ * @param $ad
+ *   Node object or aid of advertisement.
+ * @param $permission
+ *   Special Ad owners permission which should be checked (such as 'manage owners')
+ * @param $account
+ *   User object, which are accessing the ad or current user by default.
+ */
+function ad_owners_adaccess($ad, $permission, $account = NULL) {
+  global $user;
+  static $permissions = array();
+
+  if (!isset($account)) {
+    $account = $user;
+  }
+
+  $aid = 0;
+  if (isset($ad)) {
+    if (is_numeric($ad)) {
+      $aid = $ad;
+    }
+    else if (is_object($ad) && isset($ad->nid)) {
+      $aid = $ad->nid;
+    }
+  }
+
+  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)));
+  }
+  $access = '';
+  if (is_array($permission)) {
+    foreach ($permission as $perm) {
+      $access |= in_array($perm, $permissions[$aid][$account->uid]);
+    }
+  }
+  else {
+    $access = in_array($permission, $permissions[$aid][$account->uid]);
+  }
+
+  return $access;
+}
+
+
+/**
+ * TODO: Make this themeable.
+ * TODO: Group permissions by module.
+ * TODO: Allow modules to define default value for permission.
+ */
+function ad_owners_overview($node) {
+  drupal_set_title(t('Ad owners'));
+
+  // Be sure the node owner is listed as an ad owner
+  if (!db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $node->uid, $node->nid))) {
+    ad_owners_add($node, $node->uid);
+  }
+
+  $header = array(
+    array('data' => t('Username'), 'field' => 'uid'),
+    array('data' => t('Options')),
+  );
+
+  $sql = "SELECT a.uid, u.name FROM {ad_owners} a INNER JOIN {users} u ON a.uid = u.uid WHERE aid = %d";
+  $sql .= tablesort_sql($header);
+  $result = pager_query($sql, 25, 0, NULL, $node->nid);
+
+  $rows = array();
+  while ($owner = db_fetch_object($result)) {
+    $row = array();
+    $row[] = $owner->name;
+    $options = array();
+    // first option is 'permissions', plug-ins come afterwards
+    $options[] = l(t('permissions'), 'node/'. $node->nid .'/adowners/'. $owner->uid .'/permissions');
+    $options = array_merge($options, module_invoke_all('adowners', 'overview', $node->nid, $owner->uid));
+    // node owner has to remain an ad owner
+    if ($node->uid != $owner->uid) {
+      $options[] = l(t('remove'), 'node/'. $node->nid .'/adowners/'. $owner->uid .'/remove');
+    }
+    $options = implode(' | ', $options);
+    $row[] = $options;
+    $rows[] = $row;
+  }
+
+  $output = theme('table', $header, $rows);
+  $output .= theme('pager', NULL, 25, 0);
+
+  return $output;
+}
+
+/**
+ * A simple form for adding new users as owners of ads.
+ */
+function ad_owners_add_form($form_state, $node) {
+  $form = array();
+  drupal_set_title(t('Add owner'));
+
+  $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) {
+  $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('edit own advertisements', $owner) &&
+           !user_access('administer advertisements', $owner)) {
+    form_set_error('username', t('The specified user %username does not have <em>edit own 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']));
+  $node = node_load($form_state['values']['aid']);
+  if (!(ad_owners_add($node, $owner->uid))) {
+    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 (!isset($account)) {
+    $account = $user;
+  }
+  if (db_result(db_query('SELECT oid FROM {ad_owners} WHERE uid = %d AND aid = %d', $account->uid, $aid))) {
+    return 1;
+  }
+  else {
+    return 0;
+  }
+}
+
+/**
+ * Add an owner to an ad.
+ */
+function ad_owners_add($node, $owner, $permissions = array()) {
+  $rc = 0;
+  $uid = is_numeric($owner) ? $owner : $owner->uid;
+  if (!db_result(db_query('SELECT oid FROM {ad_owners} WHERE aid = %d AND uid = %d', $node->nid, $uid))) {
+    db_query('INSERT INTO {ad_owners} (aid, uid) VALUES(%d, %d)', $node->nid, $uid);
+    $rc = db_affected_rows() ? 1 : 0;
+
+    if (!$permissions) {
+      $permissions = variable_get('ad_'. $node->adtype .'_default_permissions', array('access statistics', 'access click history', 'manage status'));
+    }
+
+    $oid = db_result(db_query("SELECT oid FROM {ad_owners} WHERE aid = %d and uid = %d", $node->nid, $uid));
+    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' => $node->nid));
+  }
+  return $rc;
+}
+
+/**
+ * Create a unique host id for each ad owner, used when displaying ads remotely.
+ */
+function ad_host_id_create($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;
+}
+
+/**
+ * Removes ad owner from an ad.
+ */
+function ad_owner_remove_form($form_state, $node, $owner) {
+  $form['aid'] = array(
+    '#type' => 'value',
+    '#value' => $node->nid,
+  );
+  $form['uid'] = array(
+    '#type' => 'value',
+    '#value' => $owner->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')
+  );
+}
+
+/**
+ * Don't allow the removal of the primary owner of the advertisement.
+ */
+function ad_owner_remove_form_validate($form, &$form_state) {
+  $node = node_load($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');
+
+    $form_state['redirect'] = 'node/'. $form_state['values']['aid'] .'/adowners';
+  }
+}
+
+/**
+ * Remove the ad owner, and all associated permissions.
+ */
+function ad_owner_remove_form_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)));
+
+  $form_state['redirect'] = 'node/'. $form_state['values']['aid'] .'/adowners';
+}
+
+
+/**
+ * Display a form with all available permissions and their status for the
+ * selected ad and ad owner.
+ */
+function ad_owner_permissions_form($form_state, $node, $user) {
+  drupal_set_title(t('Permissions'));
+
+  $oid = db_result(db_query("SELECT oid FROM {ad_owners} WHERE aid = %d and uid = %d", $node->nid, $user->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();
+
+  $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' => $node->nid,
+  );
+
+  $form['uid'] = array(
+    '#type' => 'hidden',
+    '#value' => $user->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($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_form_submit($form, &$form_state) {
+  $permissions = module_invoke_all('adapi', 'permissions', array());
+  $perms = array();
+  foreach ($permissions as $permission) {
+    $perm = str_replace(' ', '_', $permission);
+    if (isset($form_state['values'][$perm]) && $form_state['values'][$perm] > 0) {
+      $perms[] = $permission;
+    }
+  }
+  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));
+
+  drupal_set_message(t('The permissions have been saved.'));
+  $form_state['redirect'] = 'node/'. $form_state['values']['aid'] .'/adowners';
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/remote/ad_remote.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,13 @@
+; $Id: ad_remote.info,v 1.1.2.1.2.2 2009/02/12 12:47:55 jeremy Exp $
+name = Remote
+package = Ad
+dependencies[] = ad_owners
+description = Generates cut-and-paste source snippets allowing ads to be easily displayed on remote websites.
+core = 6.x
+
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/remote/ad_remote.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,127 @@
+<?php
+// $Id: ad_remote.module,v 1.1.4.5.2.5 2009/02/16 23:12:29 jeremy Exp $
+
+/**
+* @file
+ * Enhances the ad module to providing cut-and-paste source snippets allowing
+ * ads to be easily displayed on remote websites.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_perm().
+ */
+function ad_remote_perm() {
+  return array('host remote advertisements');
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_remote_menu() {
+  $items = array();
+
+  $items['admin/content/ad/ad_remote'] = array(
+    'title' => 'Remote ads',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('ad_remote_form'),
+    'access arguments' => array('host remote advertisements'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 1,
+  );
+  return $items;
+}
+
+/**
+ * A simple page providing source snippets for displaying ads on remote 
+ * websites. When form is being submitted, it rebuilds with needed code snippet.
+ */
+function ad_remote_form($form_state) {
+  global $user;
+
+  $form['settings'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Settings'),
+    '#description' => t('Use the following options to build a source snippet for displaying ads on your website.'),
+    '#collapsible' => TRUE,
+    '#collapsed' => isset($form_state['values']['group']),
+    '#weight' => -1,
+  );
+
+  $form['settings']['group'] = taxonomy_form(_ad_get_vid(), 0, t('Select one or more groups to display ads from.'));
+  $form['settings']['group']['#default_value'] = isset($form_state['values']['group']) ? $form_state['values']['group'] : '';
+
+  if (isset($form_state['values']['quantity'])) {
+    // Sanity check, be sure quantity is an integer.
+    $quantity = (int)$form_state['values']['quantity'];
+  }
+  if (!isset($quantity)) {
+    // Must display at least one advertisement.
+    $quantity = 1;
+  }
+
+  $form['settings']['quantity'] = array(
+    '#type' => 'select',
+    '#title' => t('Quantity'),
+    '#options' => drupal_map_assoc(array(1,2,3,4,5,6,7,8,9,10,15,20,25,50)),
+    '#default_value' => $quantity,
+    '#description' => t('Select the maximum number of unique ads that should be displayed together.'),
+  );
+  
+  if (isset($form_state['values']['group'])) {
+    $form['code'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Code snippet'),
+      '#description' => t('Insert the following source snippet into your web page to display ads hosted on this web site.  Include the entire snippet, and do not modify it in any way.'),
+    );
+
+    $hostid = ad_host_id_create($user->uid);
+    $group = NULL;
+    if (is_array($form_state['values']['group']) && !empty($form_state['values']['group'])) {
+      if (isset($form_state['values']['group'][0]) && $form_state['values']['group'][0] == 0) {
+        unset($form_state['values']['group'][0]);
+      }
+      $group = implode(',', $form_state['values']['group']);
+      // Sanity check, be sure group is only numbers and commas.
+      $group = preg_replace('/[^0-9,]/', '', $group);
+    }
+    if (!$group) {
+      $group = 0;
+    }
+
+    $output = '<!--'. t('start') .'-->'. ad($group, $quantity, array('raw' => 1, 'hostid' => $hostid)) .'<!--'. t('end') .'-->';
+    $form['code']['snippet'] = array(
+      '#type' => 'textarea',
+      '#value' => $output,
+      '#attributes' => array(
+        'onclick' => 'this.select();',
+        'onfocus' => 'this.select();',
+      ),
+    );
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Generate code snippet'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form validator.
+ */
+function ad_remote_form_validate($form, &$form_state) {
+  if (empty($form_state['values']['group'])) {
+    form_set_error('group', t('At least one group should be selected'));
+  }
+}
+/**
+ * Tell the form to rebuild.
+ */
+function ad_remote_form_submit($form, &$form_state) {
+  $form_state['rebuild'] = TRUE;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/report/ad_report.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,13 @@
+; $Id: ad_report.info,v 1.1.2.1.2.2 2009/02/09 20:39:09 jeremy Exp $
+name = Report
+package = Ad
+dependencies[] = ad
+description = Provides comprehensive charts and reports about advertising statistics.
+core = 6.x
+
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/report/ad_report.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,289 @@
+<?php
+// $Id: ad_report.module,v 1.1.2.3.2.7.2.6 2009/02/16 23:12:29 jeremy Exp $
+
+/**
+ * @file
+ * Provides comprehensive charts and reports about advertising statistics.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_menu().
+ */
+function ad_report_menu() {
+  $items = array();
+
+  $items['node/%node/report'] = array(
+    'title' => 'Reports',
+    'page callback' => 'ad_report_bargraph',
+    'page arguments' => array(1),
+    'type' => MENU_LOCAL_TASK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  );
+  $items['node/%node/report/monthly'] = array(
+    'title' => 'Monthly Reports',
+    'page callback' => 'ad_report_bargraph',
+    'page arguments' => array(1,3),
+    'type' => MENU_LOCAL_TASK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  );
+  $items['node/%node/report/weekly'] = array(
+    'title' => 'Weekly Reports',
+    'page callback' => 'ad_report_bargraph',
+    'page arguments' => array(1,3),
+    'type' => MENU_LOCAL_TASK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  );
+  $items['node/%node/report/daily'] = array(
+    'title' => 'Daily Reports',
+    'page callback' => 'ad_report_bargraph',
+    'page arguments' => array(1,3),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  ); 
+  $items['node/%node/report/hourly'] = array(
+    'title' => 'Hourly Reports',
+    'page callback' => 'ad_report_bargraph',
+    'page arguments' => array(1,3),
+    'type' => MENU_LOCAL_TASK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  );
+  $items['ad_report/%node/bargraph'] = array(
+    'title' => 'Bar graph',
+    'page callback' => 'ad_report_generate_bargraph',
+    'page arguments' => array(1),
+    'type' => MENU_CALLBACK,
+    'access callback' => 'ad_report_access',
+    'access arguments' => array(1),
+  );
+
+  return $items;
+}
+/**
+* Implementation of access callback.
+* 
+* @param mixed $node
+*  Ad object.
+*/
+function ad_report_access($node){
+    return ($node->type == 'ad') && ad_adaccess($node, 'access statistics');
+}
+/**
+ * Page to display ad with bargraph.
+ */
+function ad_report_bargraph($node, $granularity = 'daily', $type = 'node') {
+  switch ($granularity) {
+    case 'hourly':
+      drupal_set_title(t('Past twelve hours'));
+      break;
+    case 'daily':
+      drupal_set_title(t('Past twelve days'));
+      break;
+    case 'weekly':
+      drupal_set_title(t('Past twelve weeks'));
+      break;
+    case 'monthly':
+      drupal_set_title(t('Past twelve months'));
+      break;
+  }
+
+  switch ($type) {
+    case 'node':
+      if ($node->aid) {
+        $output = '<img src="'. url("ad_report/$node->nid/bargraph/$granularity/node") .'" />';
+        $ad_link = module_invoke('ad_' . $node->adtype, 'display_ad', $node); 
+        $output .= theme('box', $node->title, $ad_link);
+      }
+      break;
+    default:
+      $output = '<img src="'. url("ad_report/$node->uid/bargraph/$granularity/$type") .'" />';
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Page that utilizes gd to generate a bargraph.
+ *
+ * TODO: Make this more dynamic, allowing to move through time, etc.
+ */
+function ad_report_generate_bargraph($node, $granularity = 'daily', $type = 'node') {
+    $id = $node->nid;
+  header("Content-type: image/png");
+
+  // Preperation.
+  $views = array();
+  $max_views = 0;
+  $statistics = array();
+  $clicks = array();
+  $max_clicks = 0;
+  $time = time();
+
+  $increments = 12;
+  $end_add = 0;
+  switch ($granularity) {
+    case 'hourly':
+      $start_time = (60 * 60 * 11);
+      // Increment hourly.
+      $increment_time = (60 * 60);
+
+      $format_start = 'YmdH';
+      $format_end = 'YmdH';
+      $format_end_append = '';
+      $format_upper = 'M d';
+      $format_lower = 'ga';
+      $graph_height = 250;
+      break;
+    case 'daily':
+    default:
+      $start_time = (60 * 60 * 24 * 11);
+      // Increment daily.
+      $increment_time = (60 * 60 * 24);
+
+      $format_start = 'Ymd00';
+      $format_end = 'Ymd';
+      $format_end_append = '24';
+      $format_upper = 'D';
+      $format_lower = 'M d';
+      break;
+    case 'weekly':
+      $start_time = (60 * 60 * 24 * 7 * 11);
+      // Increment weekly.
+      $increment_time = (60 * 60 * 24 * 7);
+
+      $format_start = 'Ymd00';
+      $format_end = 'Ymd';
+      $format_end_append = '24';
+      $end_add = (60 * 60 * 24 * 6);
+      //$end_add = 600;
+      $format_upper = 'M d -';
+      $format_lower = '';
+      break;
+    case 'monthly':
+      $start_time = ((60 * 60 * 24 * 2) + (60 * 60 * 24 * 7 * 4)) * 11;
+      // Increment monthly (every 30 days).
+      $increment_time = (60 * 60 * 24 * 2) + (60 * 60 * 24 * 7 * 4);
+
+      $format_start = 'Ymd00';
+      $format_end = 'Ymd';
+      $format_end_append = '24';
+      $end_add = (60 * 60 * 24 * 29);
+      $format_upper = 'M d -';
+      $format_lower = '';
+      break;
+  }
+
+  // Retrive data from database.
+  for ($i = $time - $start_time; $i <= $time; $i = $i + $increment_time) {
+    $day_start = date($format_start, $i);
+    $day_end = date($format_end, $i + $end_add). $format_end_append;
+    if ($type == 'node') {
+      $view = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'view' AND date >= %d AND date <= %d", $id, $day_start, $day_end));
+      $click = (int)db_result(db_query("SELECT SUM(count) FROM {ad_statistics} WHERE aid = %d AND action = 'click' AND date >= %d AND date <= %d", $id, $day_start, $day_end));
+    }
+    else if ($type == 'user') {
+      $view = (int)db_result(db_query("SELECT SUM(a.count) FROM {ad_statistics} a LEFT JOIN {node} n ON a.aid = n.nid WHERE uid = %d AND type = 'ad' AND (action = 'view' OR action = 'count') AND date >= %d AND date <= %d", $id, $day_start, $day_end));
+      $click = (int)db_result(db_query("SELECT SUM(a.count) FROM {ad_statistics} a LEFT JOIN {node} n ON a.aid = n.nid WHERE uid = %d AND type = 'ad' AND action = 'click' AND date >= %d AND date <= %d", $id, $day_start, $day_end));
+    }
+    else { 
+      $function = "ad_report_views_$type";
+      if (function_exists("$function")) {
+        $view = $function($id, $day_start, $day_end);
+      }
+      $function = "ad_report_clicks_$type";
+      if (function_exists("$function")) {
+        $click = $function($id, $day_start, $day_end);
+      }
+    }
+    if ($view > $max_views) {
+      $max_views = $view;
+    }
+    $statistics[] = array(
+      'upper' => date($format_upper, $i),
+      'lower' => date($format_lower, $i),
+      'views' => $view,
+      'clicks' => $click
+    );
+  }
+
+  // Build graph image.
+  $image_width = 50 * $increments + 1;
+  $image_height = 300;
+  $graph_width = 50 * $increments;
+  $graph_height = 250;
+
+  $graph = imagecreate($image_width, $image_height);
+
+  // Configure colors to use in chart.
+  $color = array(
+    'white' => imagecolorallocate($graph, 255, 255, 255),
+    'black' => imagecolorallocate($graph, 0, 0, 0),
+    'grey' => imagecolorallocate($graph, 192, 192, 192),
+    'blue' => imagecolorallocate($graph, 0, 0, 255),
+    'orange' => imagecolorallocate($graph, 220, 210, 60),
+  );
+
+  // Draw the outside edges of the graph.
+  imageline($graph, 0, 0, 0, $graph_height, $color['grey']);
+  imageline($graph, 0, 0, $graph_width, 0, $color['grey']);
+  imageline($graph, $graph_width - 1, 0, $graph_width - 1, $graph_height, $color['grey']);
+  imageline($graph, 0, $graph_height - 1, $graph_width - 1, $graph_height - 1, $color['grey']);
+
+  // Draw a grid.
+  for ($i = 0; $i < ($increments + 1); $i++) {
+    imageline($graph, $i*50, 0, $i*50, $graph_height, $color['grey']);
+  }
+  for ($i = 0; $i < 11; $i++) {
+    imageline($graph, 0, $i*25, $graph_width, $i*25, $color['grey']);
+  }
+
+  $multiply = 0;
+  if ($max_views > $graph_height) {
+    if (!$multiply) {
+      $multiply = .9;
+    }
+    while (($max_views * $multiply) >= $graph_height) {
+      $multiply *= .9;
+    }
+  }
+  else if ($max_views) {
+    while (($max_views * ($multiply + 1)) <= $graph_height) {
+      $multiply++;
+    }
+  }
+
+  // Display impressions.
+  for ($i = 0; $i < $increments ; $i++) {
+    $view = $multiply ? $statistics[$i]['views'] * $multiply : $statistics[$i]['views'];
+    if ($view) {
+      imagefilledrectangle($graph, $i*50 + 4, $graph_height-$view, ($i+1)*50, $graph_height, $color['grey']);
+      $string_height = $view < 10 ? $graph_height - 10 : $graph_height - $view;
+      imagestring($graph, 2, $i*50 + 15, $string_height, $statistics[$i]['views'], $color['black']);
+    }
+    // Display timestamp
+    imagestring($graph, 2, $i*50 + 2, 255, $statistics[$i]['upper'], $color['black']);
+    imagestring($graph, 2, $i*50 + 3, 265, $statistics[$i]['lower'], $color['black']);
+  }
+
+  // Display clicks.
+  for ($i = 0; $i < $increments; $i++) {
+    $click = $multiply ? $statistics[$i]['clicks'] * $multiply : $statistics[$i]['clicks'];
+    if ($click) {
+      imagefilledrectangle($graph, $i*50 + 10, $graph_height-$click, ($i+1)*50, $graph_height, $color['blue']);
+      $string_height = $click < 10 ? $graph_height - 10 : $graph_height - $click;
+      imagestring($graph, 2, $i*50 + 20, $string_height, $statistics[$i]['clicks'], $color['white']);
+    }
+  }
+
+  imagepng($graph);
+  imagedestroy($graph);
+  
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/serve.php	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,43 @@
+<?php
+// $Id: serve.php,v 1.1.2.2.2.1 2009/02/16 17:06:47 jeremy Exp $
+
+/**
+ * @file
+ * Serve advertisements.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+// Provide adserve functions to external plugins.
+require_once('adserve.inc');
+
+if (isset($_GET['o'])) {
+  $output = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['o']);
+  if ($output == 'image') {
+    require_once('imageserve.inc');
+    adserve_counter_image();
+    exit(0);
+  }
+  else if ($output) {
+    // TODO: Document how this hook allows external modules to use serve.php
+    $files = array(
+      "$output.inc", // search for file in main ad directory
+      "$output/$output.inc", // search for file in subdirectory
+      "../$output/$output.inc", // search for file in higher subdirectory
+    );
+    foreach ($files as $file) {
+      $function = $output .'_serve';
+      if (file_exists($file)) {
+        require_once("$file");
+        if (function_exists($function)) {
+          $function();
+          exit(0);
+        }
+      }
+    }
+  }
+}
+
+// Default action.
+adserve_ad();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statistics/click_filter/click_filter.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: click_filter.info,v 1.1.2.1.2.1 2008/08/11 21:15:27 jeremy Exp $
+name = Click filter
+package = Ad
+dependencies[] = ad
+description = Filter duplicate click statistics.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statistics/click_filter/click_filter.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,67 @@
+<?php
+// $Id: click_filter.install,v 1.1.2.3.2.6 2009/02/16 23:12:30 jeremy Exp $
+
+/**
+ * @file
+ * Click_filter module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+if (!defined('CLICK_NEW')) {
+  define('CLICK_NEW', 0);
+  define('CLICK_VALID', 1);
+  define('CLICK_DUPLICATE', 2);
+  define('CLICK_OWNER', 3);
+  define('CLICK_FILTER_ROLE', 4);
+  define('CLICK_BOT', 5);
+}
+
+/**
+ * Currently there is no click_filter schema.  However, we do need to review
+ * existing clicks for duplicates
+ */
+function click_filter_install() {
+  $valid = $duplicate = $owner = $bot = 0;
+  // Find all unfiltered clicks.
+  $result = db_query("SELECT DISTINCT(aid) FROM {ad_clicks} WHERE status = %d", CLICK_NEW);
+  while ($ad = db_fetch_object($result)) {
+    $clicks = db_query('SELECT cid, uid, hostname, timestamp, user_agent FROM {ad_clicks} WHERE status = %d AND aid = %d', CLICK_NEW, $ad->aid);
+    while ($click = db_fetch_object($clicks)) {
+      if (preg_match("/bot/i", $click->user_agent)) {
+        db_query('UPDATE {ad_clicks} SET status = %d WHERE cid = %d', CLICK_BOT, $click->cid);
+        $bot++;
+        continue;
+      }
+      // Search for clicks by one of the ad's owners.
+      $result = db_query('SELECT oid FROM {ad_owners} WHERE aid = %d AND uid = %d', $ad->aid, $click->uid);
+      if (db_result($result)) {
+        // Mark as clicked by ad owner.
+        db_query('UPDATE {ad_clicks} SET status = %d WHERE cid = %d', CLICK_OWNER, $click->cid);
+        // Decrement click from ad_statistics table.
+        db_query("UPDATE {ad_statistics} SET count = count - 1 WHERE date = %d AND aid = %d AND action = 'click' AND hostid = '%s'", date('YmdH', $click->timestamp), $ad->aid, $click->hostid);
+        $owner++;
+        continue;
+      }
+      // Search for duplicate clicks.
+      $result = db_query("SELECT cid FROM {ad_clicks} WHERE aid = %d AND status = %d AND cid <> %d AND (uid = %d OR hostname = '%s')", $ad->aid, CLICK_VALID, $click->cid, $click->uid, $click->hostname);
+      if (db_result($result)) {
+        // Mark as duplicate click.
+        db_query('UPDATE {ad_clicks} SET status = %d WHERE cid = %d', CLICK_DUPLICATE, $click->cid);
+        // Decrement click from ad_statistics table.
+        db_query("UPDATE {ad_statistics} SET count = count - 1 WHERE date = %d AND aid = %d AND action = 'click' AND hostid = '%s'", date('YmdH', $click->timestamp), $ad->aid, $click->hostid);
+        $duplicate++;
+      }
+      else {
+        db_query('UPDATE {ad_clicks} SET status = %d WHERE cid = %d', CLICK_VALID, $click->cid);
+        $valid++;
+      }
+    }
+  }
+  drupal_set_message(st('Updated existing clicks: %valid valid, filtered %duplicate duplicate, %owner by owner and %bot by search engine bots.', array('%valid' => $valid, '%duplicate' => $duplicate, '%owner' => $owner, '%bot' => $bot)));
+}
+
+/**
+ * Filter clicks from search engine bots.
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/statistics/click_filter/click_filter.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,92 @@
+<?php
+// $Id: click_filter.module,v 1.1.2.3.2.6 2009/02/16 23:12:30 jeremy Exp $
+
+/**
+* @file
+ * Filter duplicate and other unwanted clicks from ad statistics.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * TODO:
+ *  - Filter out bots and spiders using user_agent field.
+ *  - Add administrative ability to enter IP or range to filter.
+ */
+
+if (!defined('CLICK_NEW')) {
+  define('CLICK_NEW', 0);
+  define('CLICK_VALID', 1);
+  define('CLICK_DUPLICATE', 2);
+  define('CLICK_OWNER', 3);
+  define('CLICK_FILTER_ROLE', 4);
+  define('CLICK_BOT', 5);
+}
+/**
+ * Provide text version for statistics output.
+ */
+function click_filter_status_text($status) {
+  $output = '<div class="click-status">';
+  switch ($status) {
+    case 0:
+      $output .= '<div class="click-new">'. t('New') .'</div>';
+      break;
+    case 1:
+      $output .= '<div class="click-valid">'. t('Valid') .'</div>';
+      break;
+    case 2:
+      $output .= '<div class="click-duplicate">'. t('Duplicate') .'</div>';
+      break;
+    case 3:
+      $output .= '<div class="click-owner">'. t('Ad owner') .'</div>';
+      break;
+    case 4:
+      $output .= '<div class="click-filtered-role">'. t('Filtered role') .'</div>';
+      break;
+    case 5:
+      $output .= '<div class="click-bot">'. t('Bot') .'</div>';
+      break;
+    default:
+      $output .= '<div class="click-unknown">'. t('Unknown') .'</div>';
+      break;
+  }
+  $output .= '</div>';
+  return $output;
+}
+
+/**
+ * Perform on-the-fly click filtering.
+ */
+function click_filter_status($aid, $hostid) {
+  global $user;
+  if (user_access('filter clicks')) {
+    return CLICK_FILTER_ROLE;
+  }
+  if (preg_match("/bot/i", $_SERVER['HTTP_USER_AGENT'])) {
+    return CLICK_BOT;
+  }
+  // See if the click came from an owner of the ad.
+  $result = db_query('SELECT oid FROM {ad_owners} WHERE aid = %d AND uid = %d', $aid, $user->uid);
+  if (db_result($result)) {
+    return CLICK_OWNER;
+  }
+  // See if the click came from a duplicate uid or ip address.
+  if ($user->uid) {
+    $result = db_query("SELECT cid FROM {ad_clicks} WHERE aid = %d AND status = %d AND (uid = %d OR hostname = '%s')", $aid, CLICK_VALID, $user->uid, ip_address());
+  }
+  else {
+    $result = db_query("SELECT cid FROM {ad_clicks} WHERE aid = %d AND status = %d AND hostname = '%s'", $aid, CLICK_VALID, ip_address());
+  }
+  if (db_result($result)) {
+    return CLICK_DUPLICATE;
+  }
+  return CLICK_VALID;
+}
+
+/** 
+ * Implementation of hook_perm().
+ */
+function click_filter_perm() {
+  return array('view filtered clicks', 'filter clicks');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/text/ad_text.info	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,12 @@
+; $Id: ad_text.info,v 1.1.2.2.2.1 2008/08/11 21:15:27 jeremy Exp $
+name = Text Ad
+package = Ad
+dependencies[] = ad
+description = Enhances the ad module to support static text ads.
+core = 6.x
+; Information added by drupal.org packaging script on 2009-02-17
+version = "6.x-1.1"
+core = "6.x"
+project = "ad"
+datestamp = "1234899607"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/text/ad_text.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,67 @@
+<?php
+// $Id: ad_text.install,v 1.2.2.2.2.4.2.4 2009/02/16 17:06:50 jeremy Exp $
+
+/**
+ * @file
+ * Ad_text module database schema.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function ad_text_schema() {
+  $schema['ad_text'] = array(
+    'description' => 'The ad_text table stores sources of text ads.',
+    'fields' => array(
+      'aid' => array(
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'url' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'adheader' => array(
+        'type' => 'varchar',
+        'length' => '255',
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'adbody' => array(
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+    ),
+    'primary key' => array('aid'),
+  );
+
+  return $schema;
+}
+
+/**
+ * ad_text module installation.
+ */
+function ad_text_install() {
+  drupal_install_schema('ad_text');
+}
+
+/**
+ * Allow complete uninstallation of the ad_text module.
+ */
+function ad_text_uninstall() {
+  // Delete all ad_text content.
+  $result = db_query("SELECT aid FROM {ad_text}");
+  while ($aid = db_result($result)) {
+    node_delete($aid);
+  }
+
+  // Remove tables.
+  drupal_uninstall_schema('ad_text');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/text/ad_text.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,343 @@
+<?php
+// $Id: ad_text.module,v 1.2.2.7.2.24.2.13 2009/02/17 18:56:26 jeremy Exp $
+
+/**
+ * @file
+ * Enhances the ad module to support static text ads.
+ *
+ * Copyright (c) 2005-2009.
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Function used to display the selected ad.
+ */
+function ad_text_display_ad($ad) {
+  return theme('ad_text_ad', $ad);
+}
+
+/**
+ * Return a themed ad of type ad_text.
+ *
+ * @param @ad
+ *   The ad object.
+ * @return
+ *   A string containing the ad markup.
+ */
+function theme_ad_text_ad($ad) {
+  if (isset($ad->aid)) {
+    $output = '<div class="text-advertisement" id="ad-'. $ad->aid .'">';
+    if (isset($ad->url) && !empty($ad->url)) {
+      $output .= theme('ad_text_text', ad_text_display_prepare($ad->adheader, $ad->format), ad_text_display_prepare($ad->adbody, $ad->format), $ad->redirect .'/@HOSTID___');
+    }
+    else {
+      $output .= theme('ad_text_text', ad_text_display_prepare($ad->adheader, $ad->format), ad_text_display_prepare($ad->adbody, $ad->format));
+    }
+    $output .= '</div>';
+    return $output;
+  }
+}
+
+/**
+ * Return a themed text ad.
+ *
+ * @param $header
+ *  The header of the text ad.
+ * @param $body
+ *  The body of the text ad.
+ * @param $link
+ *  Optional link URL for header.
+ * @return
+ *  A string containing the text ad.
+ */
+function theme_ad_text_text($header, $body, $link = NULL) {
+  $output  = '<div class="ad-header">';
+  if (isset($link) && !empty($link)) {
+    $output .= l($header, $link, array('attributes' => ad_link_attributes(), 'html' => TRUE));
+  }
+  else {
+    $output .= $header;
+  }
+  $output .= '</div>';
+  $output .= '<div class="ad-body">'. $body .'</div>';
+  return $output;
+}
+
+/**
+ * Strip illegal characters, apply input filters, then encode the rest UTF-8.
+ */
+function ad_text_display_prepare($string, $format) {
+  $string = preg_replace("/[\b\f\n\r\t]/", ' ', $string);
+  $string = check_markup($string, $format, FALSE);
+  return $string;
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function ad_text_theme() {
+  return array(
+    'ad_text_ad' => array(
+      'file' => 'ad_text.module',
+      'arguments' => array(
+        'ad' => NULL,
+      ),
+    ),
+    'ad_text_text' => array(
+      'file' => 'ad_text.module',
+      'arguments' => array(
+        'header' => NULL,
+        'body' => NULL,
+        'link' => NULL,
+      ),
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function ad_text_help($path, $arg) {
+  $output = '';
+  switch ($path) {
+    case 'node/add/ad#text':
+      $output = t('A text advertisement.');
+      break;
+  }
+  return $output;
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function ad_text_access($op, $node, $account) {
+  return ad_access($op, $node, $account);
+}
+
+/**
+ * Text ad settings form.
+ */
+function ad_text_global_settings($form_state) {
+  $form = array();
+
+  $form['header_min'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Header minimum length'),
+    '#size' => 3,
+    '#maxlength' => 3,
+    '#default_value' => variable_get('header_min', 0),
+    '#description' => t('Optionally specify the minimum number of characters allowed in the header of a text ad.  Set to <em>0</em> to specify no minimum length.'),
+  );
+  $form['header_max'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Header maximum length'),
+    '#size' => 3,
+    '#maxlength' => 3,
+    '#default_value' => variable_get('header_max', 0),
+    '#description' => t('Optionally specify the maximum number of characters allowed in the header of a text ad.  Set to <em>0</em> to specify no maximum length.'),
+  );
+  $form['body_min'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Body minimum length'),
+    '#size' => 10,
+    '#maxlength' => 10,
+    '#default_value' => variable_get('body_min', 0),
+    '#description' => t('Optionally specify the minimum number of characters allowed in the body of a text ad.  Set to <em>0</em> to specify no minimum length.'),
+  );
+  $form['body_max'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Body maximum length'),
+    '#size' => 10,
+    '#maxlength' => 10,
+    '#default_value' => variable_get('body_max', 0),
+    '#description' => t('Optionally specify the maximum number of characters allowed in the body of a text ad.  Set to <em>0</em> to specify no maximum length.'),
+  );
+
+
+  $form['save'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * Settings form validation handler.
+ */
+function ad_text_global_settings_validate($form, &$form_state) {
+  if ((int)$form_state['values']['header_min'] > (int)$form_state['values']['header_max']) {
+    form_set_error('header_min', t('The header minimum length can not be greater than the header maximum length.'));
+  }
+  if ((int)$form_state['values']['body_min'] > (int)$form_state['values']['body_max']) {
+    form_set_error('body_min', t('The body minimum length can not be greater than the body maximum length.'));
+  }
+}
+
+/**
+ * Settings form submit handler.
+ */
+function ad_text_global_settings_submit($form, &$form_state) {
+  variable_set('header_min', $form_state['values']['header_min']);
+  variable_set('header_max', $form_state['values']['header_max']);
+  variable_set('body_min', $form_state['values']['body_min']);
+  variable_set('body_max', $form_state['values']['body_max']);
+}
+
+/**
+ * Implementation of hook_adapi().
+ */
+function ad_text_adapi($op, &$node) {
+  switch ($op) {
+    case 'load':
+      $return = db_fetch_array(db_query('SELECT aid, url, adheader, adbody FROM {ad_text} WHERE aid = %d', $node['aid']));
+      $return['ad'] = ad_text_display_prepare($return['adheader'], $node['format']) .'<br />'. ad_text_display_prepare($return['adbody'], $node['format']);
+      return $return;
+
+    case 'insert':
+      db_query("INSERT INTO {ad_text} (aid, url, adheader, adbody) VALUES(%d, '%s', '%s', '%s')", $node->nid, $node->url, $node->adheader, $node->adbody);
+      break;
+
+    case 'update':
+      if (ad_adaccess($node, 'manage ad text')) {
+        db_query("UPDATE {ad_text} SET url = '%s', adheader = '%s', adbody = '%s' WHERE aid = %d", $node->url, $node->adheader, $node->adbody, $node->nid);
+      }
+      break;
+
+    case 'delete':
+      db_query('DELETE FROM {ad_text} WHERE aid = %d', $node->nid);
+      break;
+
+    case 'form':
+      return ad_text_node_form($node);
+
+    case 'view':
+      return ad_text_node_view($node);
+
+    case 'redirect':
+      return db_result(db_query('SELECT url FROM {ad_text} WHERE aid = %d', $node->nid));
+
+    case 'validate':
+      $todo = array();
+      return ad_text_node_validate($node, $todo);
+
+    case 'type':
+      return array(
+        'text' => array(
+          'name' => t('Text ad'),
+          'module' => 'ad_text',
+          'description' => t('A text advertisement.'),
+          'help' => t('A text advertisement.'),
+        ),
+      );
+
+    case 'permissions':
+      if (!isset($node->adtype) || $node->adtype == 'text') {
+        return array('manage ad text');
+      }
+  }
+}
+
+/**
+ * Adapi helper function for displaying a node form.
+ */
+function ad_text_node_form(&$node) {
+  $form = array();
+
+  $form['ad_text'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Text'),
+    '#collapsible' => TRUE,
+  );
+
+  if (ad_adaccess($node, 'manage ad text') || arg(1) == 'add' && user_access('create advertisements')) {
+    $access = TRUE;
+  }
+  else {
+    $access = FALSE;
+    $form['ad_text']['notice'] = array(
+      '#type' => 'markup',
+      '#value' => '<p>'. t('You do not have permission to edit this advertisement.') .'</p>',
+    );
+  }
+
+  $form['ad_text']['text'] = array(
+    '#type' => 'markup',
+    '#value' => ad_text_display_ad($node),
+  );
+
+  $form['ad_text']['url'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Destination URL'),
+    '#maxlength' => 255,
+    '#default_value' => isset($node->url) ? $node->url : '',
+    '#description' => t('Enter the complete URL where you want people to be redirected when they click on this advertisement.  The URL must be valid and begin with http:// or https://, for example %url, unless you !disable.  If you do not enter a URL, the advertisement will not be clickable.', array('%url' => t('http://www.sample.org/'), '!disable' => l(t('disable URL validation'), 'admin/content/ad/configure', array('fragment' => 'edit-ad-validate-url-wrapper')))),
+    '#disabled' => !$access,
+  );
+
+  $form['ad_text']['adheader'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Ad header'),
+    '#required' => $access,
+    '#default_value' => isset($node->adheader) ? $node->adheader : '',
+    '#description' => t('This is the first line of the ad which will be linked to the URL entered above.'),
+    '#disabled' => !$access,
+  );
+  $form['ad_text']['adbody'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Ad body'),
+    '#required' => $access,
+    '#default_value' => isset($node->adbody) ? $node->adbody : '',
+    '#description' => t('This is the rest of the ad.'),
+    '#disabled' => !$access,
+  );
+
+  return $form;
+}
+
+/**
+ * Adapi helper function for displaying ad itself.
+ */
+function ad_text_node_view(&$node) {
+  $node->content['ad'] = array(
+    '#value' => theme('box', '', preg_replace('&@HOSTID___&', '0', (ad_text_display_ad($node)))),
+    '#weight' => -1,
+  );
+  if (!empty($node->url)) {
+    $link = t('Links to !url.', array('!url' => $node->url));
+    $link = check_plain($link, $node->format, FALSE);
+    $node->content['ad-link'] = array(
+      '#value' => "<div class=\"links-to\">$link</div>",
+      '#weight' => 1,
+    );
+  }
+}
+
+/**
+ * Text ads node validator.
+ */
+function ad_text_node_validate($node, &$form_state) {
+  // Enforce minimum and maximum lengths.
+  $header_len = isset($node->adheader) ? strlen($node->adheader) : 0;
+  $header_min = variable_get('header_min', 0);
+  $header_max = variable_get('header_max', 0);
+  if ($header_min && ($header_len < $header_min)) {
+    form_set_error('adheader', t('Your text ad header is only %cur characters but must be at least %min characters.', array('%cur' => $header_len, '%min' => $header_min)));
+  }
+  else if ($header_max && ($header_len > $header_max)) {
+    form_set_error('adheader', t('Your text ad header is %cur characters but can not be more than %max characters.', array('%cur' => $header_len, '%max' => $header_max)));
+  }
+  $body_len = strlen($node->adbody);
+  $body_min = variable_get('body_min', 0);
+  $body_max = variable_get('body_max', 0);
+  if ($body_min && ($body_len < $body_min)) {
+    form_set_error('adbody', t('Your text ad body is only %cur characters but must be at least %min characters.', array('%cur' => $body_len, '%min' => $body_min)));
+  }
+  else if ($body_max && ($body_len > $body_max)) {
+    form_set_error('adbody', t('Your text ad body is %cur characters but can not be more than %max characters.', array('%cur' => $body_len, '%max' => $body_max)));
+  }
+
+  if ($node->url && variable_get('ad_validate_url', 1) && (!valid_url($node->url, TRUE))) {
+    form_set_error('url', t('You must specify a valid %field.', array('%field' => t('Destination URL'))));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/translations/ad.pot	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,2202 @@
+# $Id: ad.pot,v 1.1.2.1 2009/01/30 10:40:25 neochief Exp $
+#
+# LANGUAGE translation of Drupal (general)
+# Copyright YEAR NAME <EMAIL@ADDRESS>
+# Generated from files:
+#  ad.admin.inc,v 1.1.2.1 2009/01/29 16:38:00 neochief
+#  ad_notify.module,v 1.1.2.2.2.13.2.3 2009/01/29 16:38:01 neochief
+#  ad_owners.module,v 1.1.2.1 2009/01/29 16:38:02 neochief
+#  ad.module,v 1.2.2.29.2.83.2.6 2009/01/29 16:38:00 neochief
+#  ad.pages.inc,v 1.1.2.1 2009/01/29 16:38:00 neochief
+#  ad_image.module,v 1.2.2.13.2.40.2.2 2009/01/29 16:38:01 neochief
+#  ad_embed.module,v 1.1.2.4.2.12.2.2 2009/01/29 16:38:01 neochief
+#  ad_text.module,v 1.2.2.7.2.24.2.3 2009/01/29 16:38:02 neochief
+#  ad_external.module,v 1.1.2.6.2.2 2009/01/29 16:38:01 neochief
+#  ad_remote.module,v 1.1.4.5.2.2 2009/01/29 16:38:02 neochief
+#  ad.info,v 1.1.2.2.2.1 2008/08/11 21:15:26 jeremy
+#  ad_cache_file.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy
+#  ad_cache_memcache.info,v 1.1.2.2.2.1 2008/08/11 21:15:26 jeremy
+#  ad_embed.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy
+#  ad_external.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy
+#  ad_html.info,v 1.1.2.1.2.1 2008/08/11 21:15:26 jeremy
+#  ad_image.info,v 1.1.2.5.2.1 2008/08/11 21:15:26 jeremy
+#  ad_notify.info,v 1.1.2.1.2.2 2009/01/29 16:38:01 neochief
+#  ad_owners.info,v 1.1.2.1 2009/01/29 16:38:02 neochief
+#  ad_remote.info,v 1.1.2.1.2.1 2008/08/11 21:15:27 jeremy
+#  ad_report.info,v 1.1.2.1.2.1 2008/08/11 21:15:27 jeremy
+#  click_filter.info,v 1.1.2.1.2.1 2008/08/11 21:15:27 jeremy
+#  ad_text.info,v 1.1.2.2.2.1 2008/08/11 21:15:27 jeremy
+#  ad_cache_file.module,v 1.1.4.18.2.2 2009/01/29 16:38:00 neochief
+#  ad_cache_memcache.module,v 1.1.2.9.2.2 2009/01/29 16:38:01 neochief
+#  ad_html.module,v 1.1.2.5.2.3 2009/01/29 16:38:01 neochief
+#  image/ad_image_views.inc: n/a
+#  text/ad_text_views.inc: n/a
+#  ad_report.module,v 1.1.2.3.2.7.2.3 2009/01/29 16:38:02 neochief
+#  click_filter.module,v 1.1.2.3.2.4 2009/01/29 16:38:02 neochief
+#  ad_weight_percent.module,v 1.1.2.4.2.1 2008/08/11 21:15:27 jeremy
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"POT-Creation-Date: 2009-01-29 21:42+0200\n"
+"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
+"Last-Translator: NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: ad.admin.inc:33
+msgid "Update options"
+msgstr ""
+
+#: ad.admin.inc:42
+msgid "Update"
+msgstr ""
+
+#: ad.admin.inc:51;787 notify/ad_notify.module:272
+msgid "edit"
+msgstr ""
+
+#: ad.admin.inc:64
+msgid "Mark as approved"
+msgstr ""
+
+#: ad.admin.inc:69
+msgid "Mark as active"
+msgstr ""
+
+#: ad.admin.inc:74
+msgid "Mark as expired"
+msgstr ""
+
+#: ad.admin.inc:79
+msgid "Mark as pending"
+msgstr ""
+
+#: ad.admin.inc:84
+msgid "Mark as offline"
+msgstr ""
+
+#: ad.admin.inc:89
+msgid "Mark as unpublished"
+msgstr ""
+
+#: ad.admin.inc:94
+msgid "Mark as denied"
+msgstr ""
+
+#: ad.admin.inc:99;842;857;899
+msgid "Delete"
+msgstr ""
+
+#: ad.admin.inc:151
+msgid "Are you sure you want to delete these ads?"
+msgstr ""
+
+#: ad.admin.inc:152 owners/ad_owners.module:370
+msgid "This action cannot be undone."
+msgstr ""
+
+#: ad.admin.inc:153
+msgid "Delete all"
+msgstr ""
+
+#: ad.admin.inc:153;900 owners/ad_owners.module:372
+msgid "Cancel"
+msgstr ""
+
+#: ad.admin.inc:164
+msgid "The ads have been deleted."
+msgstr ""
+
+#: ad.admin.inc:174 ad.module:509
+msgid "Title"
+msgstr ""
+
+#: ad.admin.inc:174
+msgid "Group"
+msgstr ""
+
+#: ad.admin.inc:174;699
+msgid "Type"
+msgstr ""
+
+#: ad.admin.inc:174;574 ad.pages.inc:22;240 ad.module:364;564;584;589;601;623;628 notify/ad_notify.module:259
+msgid "Status"
+msgstr ""
+
+#: ad.admin.inc:174
+msgid "Operations"
+msgstr ""
+
+#: ad.admin.inc:191
+msgid "No ads available."
+msgstr ""
+
+#: ad.admin.inc:210
+msgid "No ads selected."
+msgstr ""
+
+#: ad.admin.inc:233
+msgid "The update has been performed."
+msgstr ""
+
+#: ad.admin.inc:278 image/ad_image.module:187;189;193
+msgid "pending"
+msgstr ""
+
+#: ad.admin.inc:279
+msgid "approved"
+msgstr ""
+
+#: ad.admin.inc:280 image/ad_image.module:187;189;193
+msgid "active"
+msgstr ""
+
+#: ad.admin.inc:281
+msgid "offline"
+msgstr ""
+
+#: ad.admin.inc:282
+msgid "unpublished"
+msgstr ""
+
+#: ad.admin.inc:283
+msgid "expired"
+msgstr ""
+
+#: ad.admin.inc:284
+msgid "denied"
+msgstr ""
+
+#: ad.admin.inc:285
+msgid "not pending"
+msgstr ""
+
+#: ad.admin.inc:286
+msgid "not approved"
+msgstr ""
+
+#: ad.admin.inc:287
+msgid "not active"
+msgstr ""
+
+#: ad.admin.inc:288
+msgid "not offline"
+msgstr ""
+
+#: ad.admin.inc:289
+msgid "not unpublished"
+msgstr ""
+
+#: ad.admin.inc:290
+msgid "not expired"
+msgstr ""
+
+#: ad.admin.inc:291
+msgid "not denied"
+msgstr ""
+
+#: ad.admin.inc:295
+msgid "status"
+msgstr ""
+
+#: ad.admin.inc:300
+msgid "type"
+msgstr ""
+
+#: ad.admin.inc:311
+msgid "group"
+msgstr ""
+
+#: ad.admin.inc:328
+msgid "and"
+msgstr ""
+
+#: ad.admin.inc:328
+msgid "where"
+msgstr ""
+
+#: ad.admin.inc:334
+msgid "is"
+msgstr ""
+
+#: ad.admin.inc:358
+msgid "Show only ads where"
+msgstr ""
+
+#: ad.admin.inc:412;445
+msgid "Refine"
+msgstr ""
+
+#: ad.admin.inc:412;444
+msgid "Filter"
+msgstr ""
+
+#: ad.admin.inc:414;457
+msgid "Undo"
+msgstr ""
+
+#: ad.admin.inc:415;460
+msgid "Reset"
+msgstr ""
+
+#: ad.admin.inc:557
+msgid "There are no active ads."
+msgstr ""
+
+#: ad.admin.inc:578
+msgid "Using detected adserve scripts: %adserve, %adserveinc"
+msgstr ""
+
+#: ad.admin.inc:578;578
+msgid "not found"
+msgstr ""
+
+#: ad.admin.inc:583
+msgid "General"
+msgstr ""
+
+#: ad.admin.inc:592
+msgid "Click-through target"
+msgstr ""
+
+#: ad.admin.inc:594
+msgid "same browser window and frame"
+msgstr ""
+
+#: ad.admin.inc:595
+msgid "new browser window"
+msgstr ""
+
+#: ad.admin.inc:596
+msgid "parent frame"
+msgstr ""
+
+#: ad.admin.inc:597
+msgid "same browser window, removing all frames"
+msgstr ""
+
+#: ad.admin.inc:600
+msgid "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."
+msgstr ""
+
+#: ad.admin.inc:605
+msgid "nofollow"
+msgstr ""
+
+#: ad.admin.inc:607
+msgid "If enabled, %tag will be added to advertisement links generated by this module."
+msgstr ""
+
+#: ad.admin.inc:607
+msgid "rel=\"nofollow\""
+msgstr ""
+
+#: ad.admin.inc:611
+msgid "JavaScript"
+msgstr ""
+
+#: ad.admin.inc:611
+msgid "jQuery"
+msgstr ""
+
+#: ad.admin.inc:611;644
+msgid "IFrame"
+msgstr ""
+
+#: ad.admin.inc:611
+msgid "Raw"
+msgstr ""
+
+#: ad.admin.inc:614
+msgid "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, jQuery, and IFrame display methods allows you to display random ads and track impressions even on cached pages.  When using the Raw display method together with Drupal's page cache, view statistics will be properly tracked but advertisements will only change when the page cache is updated."
+msgstr ""
+
+#: ad.admin.inc:622
+msgid "Display type"
+msgstr ""
+
+#: ad.admin.inc:630
+msgid "Validate URLs"
+msgstr ""
+
+#: ad.admin.inc:632
+msgid "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."
+msgstr ""
+
+#: ad.admin.inc:637
+msgid "Filter ads"
+msgstr ""
+
+#: ad.admin.inc:639
+msgid "If enabled, the input format for each advertisement node will be applied to the displayed advertisement."
+msgstr ""
+
+#: ad.admin.inc:650
+msgid "Frameborder"
+msgstr ""
+
+#: ad.admin.inc:652
+msgid "If enabled, IFrames used for displaying ads will have a frameborder."
+msgstr ""
+
+#: ad.admin.inc:656
+msgid "Scrolling"
+msgstr ""
+
+#: ad.admin.inc:659
+msgid "Define whether or not scroll bars should be enabled for the ad IFrame."
+msgstr ""
+
+#: ad.admin.inc:663
+msgid "Width"
+msgstr ""
+
+#: ad.admin.inc:668
+msgid "The default width for advertisement IFrames"
+msgstr ""
+
+#: ad.admin.inc:672
+msgid "Height"
+msgstr ""
+
+#: ad.admin.inc:677
+msgid "The default height for advertisement IFrames"
+msgstr ""
+
+#: ad.admin.inc:682
+msgid "Cache"
+msgstr ""
+
+#: ad.admin.inc:688
+msgid "None"
+msgstr ""
+
+#: ad.admin.inc:691
+msgid "A cache can be used to efficiently track how many times advertisements are displayed and clicked."
+msgstr ""
+
+#: ad.admin.inc:710;765 embed/ad_embed.module:190 image/ad_image.module:113 notify/ad_notify.module:420 owners/ad_owners.module:449 text/ad_text.module:101
+msgid "Save"
+msgstr ""
+
+#: ad.admin.inc:725
+msgid "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."
+msgstr ""
+
+#: ad.admin.inc:775
+msgid "Name"
+msgstr ""
+
+#: ad.admin.inc:776;816 ad.module:515
+msgid "Description"
+msgstr ""
+
+#: ad.admin.inc:777 owners/ad_owners.module:202
+msgid "Options"
+msgstr ""
+
+#: ad.admin.inc:792
+msgid "No groups have been created."
+msgstr ""
+
+#: ad.admin.inc:807
+msgid "Group name"
+msgstr ""
+
+#: ad.admin.inc:811
+msgid "Specify a name for the ad group."
+msgstr ""
+
+#: ad.admin.inc:819
+msgid "Describe this ad group."
+msgstr ""
+
+#: ad.admin.inc:824
+msgid "Weight"
+msgstr ""
+
+#: ad.admin.inc:826
+msgid "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."
+msgstr ""
+
+#: ad.admin.inc:836 ad.module:946
+msgid "Create group"
+msgstr ""
+
+#: ad.admin.inc:873
+msgid "Created new ad group %term."
+msgstr ""
+
+#: ad.admin.inc:876
+msgid "The ad group %term has been updated."
+msgstr ""
+
+#: ad.admin.inc:896
+msgid "Are you sure you want to delete the ad group %name?"
+msgstr ""
+
+#: ad.admin.inc:898
+msgid "Ads that were within this group will not be deleted.  This action cannot be undone."
+msgstr ""
+
+#: ad.admin.inc:908
+msgid "The ad group %term has been deleted."
+msgstr ""
+
+#: ad.admin.inc:909 ad.module:233;237;398;407;423;438;0 external/ad_external.module:89
+msgid "ad"
+msgstr ""
+
+#: ad.admin.inc:909
+msgid "mailarchive: deleted %term ad group."
+msgstr ""
+
+#: ad.admin.inc:543
+msgid "There is <a href=\"!url\">1 active ad</a> in this group."
+msgid_plural "There are <a href=\"!url\">%count</a> active ads in this group."
+msgstr[0] ""
+msgstr[1] ""
+
+#: ad.admin.inc:549
+msgid "There is 1 active ad in this group."
+msgid_plural "There are @count active ads in this group."
+msgstr[0] ""
+msgstr[1] ""
+
+#: ad.pages.inc:17;198
+msgid "Time"
+msgstr ""
+
+#: ad.pages.inc:18
+msgid "IP address"
+msgstr ""
+
+#: ad.pages.inc:19
+msgid "URL where clicked"
+msgstr ""
+
+#: ad.pages.inc:52
+msgid "details"
+msgstr ""
+
+#: ad.pages.inc:57
+msgid "There are no clicks yet."
+msgstr ""
+
+#: ad.pages.inc:63
+msgid "Click history"
+msgstr ""
+
+#: ad.pages.inc:154
+msgid "Impressions"
+msgstr ""
+
+#: ad.pages.inc:154
+msgid "Clicks"
+msgstr ""
+
+#: ad.pages.inc:154
+msgid "Click-thru"
+msgstr ""
+
+#: ad.pages.inc:158
+msgid "This hour"
+msgstr ""
+
+#: ad.pages.inc:159
+msgid "Last hour"
+msgstr ""
+
+#: ad.pages.inc:160
+msgid "Today"
+msgstr ""
+
+#: ad.pages.inc:161
+msgid "Yesterday"
+msgstr ""
+
+#: ad.pages.inc:162
+msgid "Last seven days"
+msgstr ""
+
+#: ad.pages.inc:163
+msgid "Last week"
+msgstr ""
+
+#: ad.pages.inc:164
+msgid "This month"
+msgstr ""
+
+#: ad.pages.inc:165
+msgid "Last month"
+msgstr ""
+
+#: ad.pages.inc:166
+msgid "This year"
+msgstr ""
+
+#: ad.pages.inc:167
+msgid "Last year"
+msgstr ""
+
+#: ad.pages.inc:168
+msgid "All time"
+msgstr ""
+
+#: ad.pages.inc:182
+msgid "There are no any statistics yet."
+msgstr ""
+
+#: ad.pages.inc:188 ad.module:909
+msgid "Statistics"
+msgstr ""
+
+#: ad.pages.inc:202
+msgid "IP Address"
+msgstr ""
+
+#: ad.pages.inc:206
+msgid "User Agent"
+msgstr ""
+
+#: ad.pages.inc:210
+msgid "URL"
+msgstr ""
+
+#: ad.pages.inc:214 ad.module:460
+msgid "Advertisement"
+msgstr ""
+
+#: ad.pages.inc:222
+msgid "Not valid: this click has not been counted for unknown reasons.  This is an unexpected error."
+msgstr ""
+
+#: ad.pages.inc:225
+msgid "Valid: this is a valid click."
+msgstr ""
+
+#: ad.pages.inc:228
+msgid "Not valid: this click has not been counted because another click by the same IP address was already counted."
+msgstr ""
+
+#: ad.pages.inc:231
+msgid "Not valid: this click has not been counted because it was generated by an owner of the advertisement."
+msgstr ""
+
+#: ad.pages.inc:234
+msgid "Not valid: this click has not been counted because it was generated by a user in a filtered role."
+msgstr ""
+
+#: ad.pages.inc:237
+msgid "Not valid: this click has not been counted because it was generated by an automated \"bot\"."
+msgstr ""
+
+#: ad.module:88
+msgid "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."
+msgstr ""
+
+#: ad.module:90
+msgid "The ad module is <a href=\"@misconfigured\">misconfigured</a>."
+msgstr ""
+
+#: ad.module:310
+msgid "This advertisement is currently waiting for administrative approval."
+msgstr ""
+
+#: ad.module:311
+msgid "This advertisement has been approved and is currently waiting to be administratively activated."
+msgstr ""
+
+#: ad.module:312;321
+msgid "This advertisement is actively being displayed."
+msgstr ""
+
+#: ad.module:313
+msgid "This advertisement has been temporarily disabled by its owner and is not currently being displayed."
+msgstr ""
+
+#: ad.module:314
+msgid "This advertisement has been unpublished and is not currently being displayed."
+msgstr ""
+
+#: ad.module:315
+msgid "This advertisement has expired."
+msgstr ""
+
+#: ad.module:316
+msgid "This advertisement was refused by the site administrator, it will not be displayed."
+msgstr ""
+
+#: ad.module:322
+msgid "This advertisement has been temporarily disabled and is not currently being displayed."
+msgstr ""
+
+#: ad.module:336
+msgid "This advertisement will be automatically activated on %timestamp, in %time."
+msgstr ""
+
+#: ad.module:342
+msgid "This advertisement has been active since %date."
+msgstr ""
+
+#: ad.module:345
+msgid "This advertisement will expire on %timestamp, in %time."
+msgstr ""
+
+#: ad.module:349
+msgid "This advertisement will expire after %left more impressions."
+msgstr ""
+
+#: ad.module:353
+msgid "This advertisement will expire after %left more clicks."
+msgstr ""
+
+#: ad.module:359
+msgid "This advertisement expired %date."
+msgstr ""
+
+#: ad.module:373
+msgid "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."
+msgstr ""
+
+#: ad.module:462;463
+msgid "Advertisements can be randomly displayed to visitors of your website."
+msgstr ""
+
+#: ad.module:526
+msgid "At least one ad type module must be enabled before you can create advertisements.  For example, try <a href=\"!url\">enabling</a> the ad_text or ad_image module."
+msgstr ""
+
+#: ad.module:536
+msgid "Style of ad"
+msgstr ""
+
+#: ad.module:541
+msgid "Select the type of ad that you wish to create from the above options."
+msgstr ""
+
+#: ad.module:639
+msgid "Scheduling"
+msgstr ""
+
+#: ad.module:654
+msgid "The current date and time is \"%date\"."
+msgstr ""
+
+#: ad.module:658
+msgid "Automatically activate ad"
+msgstr ""
+
+#: ad.module:661
+msgid "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."
+msgstr ""
+
+#: ad.module:669
+msgid "Automatically expire ad"
+msgstr ""
+
+#: ad.module:672
+msgid "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."
+msgstr ""
+
+#: ad.module:676
+msgid "Maximum impressions"
+msgstr ""
+
+#: ad.module:681
+msgid "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 impressions, leave this field set to %zero."
+msgstr ""
+
+#: ad.module:685
+msgid "Maximum clicks"
+msgstr ""
+
+#: ad.module:690
+msgid "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."
+msgstr ""
+
+#: ad.module:718
+msgid "This vocabulary was automatically created for use by the ad module.  Only applicable options are available."
+msgstr ""
+
+#: ad.module:723;726
+msgid "ad group"
+msgstr ""
+
+#: ad.module:726
+msgid "Type %type is required to use this vocabulary."
+msgstr ""
+
+#: ad.module:728
+msgid "If enabled, ads are categorized by typing ad group names instead of choosing them from a list."
+msgstr ""
+
+#: ad.module:729
+msgid "If enabled, allows ads to have more than one ad group (always true for free tagging)."
+msgstr ""
+
+#: ad.module:730
+msgid "If enabled, every ad <strong>must</strong> be assigned to at least one ad group."
+msgstr ""
+
+#: ad.module:743
+msgid "Ad group name"
+msgstr ""
+
+#: ad.module:744
+msgid "The name for this ad group.  Example: \"Linux\"."
+msgstr ""
+
+#: ad.module:745
+msgid "A description of the ad group."
+msgstr ""
+
+#: ad.module:747
+msgid "In listings, the heavier ad groups will sink and the lighter ad groups will be positioned nearer the top."
+msgstr ""
+
+#: ad.module:803
+msgid "This ad will not be automatically activated at the scheduled time because it is not in the <em>approved</em> state."
+msgstr ""
+
+#: ad.module:1049
+msgid "ad group: @name"
+msgstr ""
+
+#: ad.module:1055
+msgid "Number of ads"
+msgstr ""
+
+#: ad.module:1058
+msgid "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."
+msgstr ""
+
+#: ad.module:1167;1191;1455
+msgid "default"
+msgstr ""
+
+#: ad.module:1168
+msgid "The default ad group is comprised of all ads not assigned to any other ad group."
+msgstr ""
+
+#: ad.module:1212
+msgid "Email @when before the advertisement will expire."
+msgstr ""
+
+#: ad.module:1213
+msgid "Email @when after the advertisement is expired."
+msgstr ""
+
+#: ad.module:1214
+msgid "Email @when before the advertisement will be activated (if scheduled)."
+msgstr ""
+
+#: ad.module:1215
+msgid "Email @when after the advertisement is activated."
+msgstr ""
+
+#: ad.module:1216
+msgid "Email @when after the advertisement is clicked."
+msgstr ""
+
+#: ad.module:1217
+msgid "Email @when after the advertisement is approved."
+msgstr ""
+
+#: ad.module:1218
+msgid "Email @when after the advertisement is denied."
+msgstr ""
+
+#: ad.module:1243;1253;1263;1268;1273 notify/ad_notify.module:497
+msgid "[%sitename ad] %event notification"
+msgstr ""
+
+#: ad.module:1244
+msgid "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"
+msgstr ""
+
+#: ad.module:1248
+msgid "[%sitename ad] expiration notification"
+msgstr ""
+
+#: ad.module:1249
+msgid "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"
+msgstr ""
+
+#: ad.module:1254
+msgid "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"
+msgstr ""
+
+#: ad.module:1258
+msgid "[%sitename ad] activation notification"
+msgstr ""
+
+#: ad.module:1259
+msgid "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"
+msgstr ""
+
+#: ad.module:1264
+msgid "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"
+msgstr ""
+
+#: ad.module:1269
+msgid "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"
+msgstr ""
+
+#: ad.module:1274
+msgid "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"
+msgstr ""
+
+#: ad.module:1292;1301
+msgid "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."
+msgstr ""
+
+#: ad.module:1307
+msgid "Invalid vocabulary defined for advertisements, attempting to auto-fix."
+msgstr ""
+
+#: ad.module:1324
+msgid "Missing vocabulary node type for advertisements, attempting to auto-fix."
+msgstr ""
+
+#: ad.module:1336
+msgid "Restoring orphaned ad group configuration."
+msgstr ""
+
+#: ad.module:1343
+msgid "Be sure to enable \"!show\" permissions for all roles that you wish to see advertisements."
+msgstr ""
+
+#: ad.module:1343;452 image/ad_image.module:245
+msgid "show advertisements"
+msgstr ""
+
+#: ad.module:1392;930
+msgid "Ad groups"
+msgstr ""
+
+#: ad.module:233
+msgid "Clicked %type ad aid %aid hostid %hostid."
+msgstr ""
+
+#: ad.module:237
+msgid "Ad redirection failed for aid %aid hostid %hostid, failed to load destination URL. "
+msgstr ""
+
+#: ad.module:398
+msgid "Automatically activated ad %title with nid %nid."
+msgstr ""
+
+#: ad.module:407
+msgid "Warning: ad %title with nid %nid in approved state has no autoactivate date set."
+msgstr ""
+
+#: ad.module:423
+msgid "Automatically expired ad %title with nid %nid."
+msgstr ""
+
+#: ad.module:438
+msgid "Warning: reset %type timestamp on advertisement %title with nid %nid because it is in %state state."
+msgstr ""
+
+#: ad.module:449
+msgid "administer advertisements"
+msgstr ""
+
+#: ad.module:450
+msgid "create advertisements"
+msgstr ""
+
+#: ad.module:451
+msgid "edit own advertisements"
+msgstr ""
+
+#: ad.module:895
+msgid "Ads"
+msgstr ""
+
+#: ad.module:898
+msgid "Configure and manage your advertising system."
+msgstr ""
+
+#: ad.module:902;938 owners/ad_owners.module:38
+msgid "List"
+msgstr ""
+
+#: ad.module:918 remote/ad_remote.module:46
+msgid "Settings"
+msgstr ""
+
+#: ad.module:955
+msgid "Edit group"
+msgstr ""
+
+#: ad.module:963
+msgid "Delete group"
+msgstr ""
+
+#: ad.module:971
+msgid "Global settings"
+msgstr ""
+
+#: ad.module:980
+msgid "Click details"
+msgstr ""
+
+#: ad.info:0;0 cache/file/ad_cache_file.info:0 cache/memcache/ad_cache_memcache.info:0 embed/ad_embed.info:0 external/ad_external.info:0 html/ad_html.info:0 image/ad_image.info:0 notify/ad_notify.info:0 owners/ad_owners.info:0 remote/ad_remote.info:0 report/ad_report.info:0 statistics/click_filter/click_filter.info:0 text/ad_text.info:0
+msgid "Ad"
+msgstr ""
+
+#: ad.info:0
+msgid "An advertising system for Drupal powered websites."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:19
+msgid "Can improve the performance of the ad module utilizing file caching."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:37
+msgid "File"
+msgstr ""
+
+#: cache/file/ad_cache_file.module:39
+msgid "File based caching will usually offer better performance, however, some find it difficult to enable and it may not offer valid statistics if you are using multiple load balanced web servers."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:44
+msgid "File cache settings"
+msgstr ""
+
+#: cache/file/ad_cache_file.module:50
+msgid "Number of cache files"
+msgstr ""
+
+#: cache/file/ad_cache_file.module:53
+msgid "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.  This configuration option is only relevant if the file cache is enabled."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:58
+msgid "Cache lifetime"
+msgstr ""
+
+#: cache/file/ad_cache_file.module:61
+msgid "Specify how long information should be cached before ad statistics are updated in the database.  Increasing the cache lifetime can improve overall performance.  This configuration options is only relevant if the file cache is enabled."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:100
+msgid "Ad module failed to access cache <em>%file</em>.  Verify file permissions."
+msgstr ""
+
+#: cache/file/ad_cache_file.module:0
+msgid "ad_cache_file"
+msgstr ""
+
+#: cache/file/ad_cache_file.info:0
+msgid "File cache"
+msgstr ""
+
+#: cache/file/ad_cache_file.info:0
+msgid "Provides a file-caching mechanism to improve ad serving performance."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:27
+msgid "Memcache installation not valid, %function not found."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:32
+msgid "Memcache is not installed, %function not found."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:38;75
+msgid "Memcache"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:60
+msgid "Utilize memcached to improve the performance of the ad module."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:77
+msgid "Memcache allows improved performance by caching data directly in RAM."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:81
+msgid "Memcache settings"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:88
+msgid "Sync frequency"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:91
+msgid "Specify how often statistics stored in RAM should be synced to the database (requires cron runs with the same or greater frequency). The longer you store data in memcache, the more data you risk loosing in the event of a system failure. This configuration option is only relevant if memcache is enabled."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:99
+msgid "Memcache warning:  your last cron run was !time ago.  Advertisement impressions data is only synchronized into the database when cron runs.  You are risking data loss.  To learn more about how Drupal cron works, please check the online help pages for <a href=\"@url\">configuring cron jobs</a>."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:103
+msgid "Memcache warning:  Cron has not run.  Advertisement impressions data is only synchronized into the database when cron runs.  You are risking data loss.  It appears cron jobs have not been setup on your system. Please check the help pages for <a href=\"@url\">configuring cron jobs</a>."
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:153;162
+msgid "!module: Unable to syncronize cache: !error"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:227
+msgid "!module: Unable to build cache: !error"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.module:0
+msgid "ad_cache_memcache"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.info:0
+msgid "Ad memcache cache"
+msgstr ""
+
+#: cache/memcache/ad_cache_memcache.info:0
+msgid "Integrates the ad module with memcached, allowing ads to be served directly out of RAM."
+msgstr ""
+
+#: embed/ad_embed.module:18
+msgid "The ad_embed module uses the ad module to replace special tags with random advertisements, or to automatically insert ads after a number of paragraphs in your content.  The module allows you to specify which ad group to select ads from and how many ads to display.  Instead of selecting ads from ad groups, it also allows you to specify individual ads by their node id."
+msgstr ""
+
+#: embed/ad_embed.module:19
+msgid "Replace tags:"
+msgstr ""
+
+#: embed/ad_embed.module:20
+msgid "Content must have been created by a user with <em>embed ads in content</em> permissions for tags to be replaced.  The ad_embed module supports two different types of tags.  One uses double brackets, looking like <em>[[ad]]</em>, the other uses an html comment, looking like <em>&lt;!--ad--&gt;</em>.  Individual parameters are then separated by the | (pipe) character.  For individual parameters with multiple values, the values are separated by commas."
+msgstr ""
+
+#: embed/ad_embed.module:21
+msgid "Using html comments has the advantage that if you decide to disable the ad_embed module, the ads will simply disappear (as they will be html coments.  The downside to using html comments is that you need to configure your Input Filter to not filter them out.  Using brackets can be simpler to get working."
+msgstr ""
+
+#: embed/ad_embed.module:22
+msgid "Parameters:"
+msgstr ""
+
+#: embed/ad_embed.module:23
+msgid "The following parameters can be used within embedded advertisement tages:"
+msgstr ""
+
+#: embed/ad_embed.module:25
+msgid "<em>groups</em>: specify which group to select random ads from."
+msgstr ""
+
+#: embed/ad_embed.module:26
+msgid "<em>quantity</em>: specify how many ads to display at once."
+msgstr ""
+
+#: embed/ad_embed.module:27
+msgid "<em>hostid</em>: specify a unqiue when logging impressions and clicks.  (This is primarily intended for use when hosting ads on remote websites.)"
+msgstr ""
+
+#: embed/ad_embed.module:28
+msgid "<em>nids</em>: a list of ads from which to select from.  If <em>nids</em> is specified, the <em>groups</em> parameter will be ignored."
+msgstr ""
+
+#: embed/ad_embed.module:30
+msgid "Examples:"
+msgstr ""
+
+#: embed/ad_embed.module:31
+msgid "The following examples should help you to understand how to embed ads within your content."
+msgstr ""
+
+#: embed/ad_embed.module:33
+msgid "<em>[[ad]]</em><br />This will display one random ad from ads that are not assigned to any group."
+msgstr ""
+
+#: embed/ad_embed.module:34
+msgid "<em>&lt;!--ad--&gt;</em><br />This will display one random ad from ads that are not assigned to any group."
+msgstr ""
+
+#: embed/ad_embed.module:35
+msgid "<em>[[ad|quantity=2]]</em><br />This will display two random ads from ads that are not assigned to any group."
+msgstr ""
+
+#: embed/ad_embed.module:36
+msgid "<em>[[ad|group=23]]</em><br />This will display one random ad from the group with a tid of 23."
+msgstr ""
+
+#: embed/ad_embed.module:37
+msgid "<em>&lt!--ad|nids=1,13,42,76|quantity=4]]</em><br />This will display all four listed ads in a random order."
+msgstr ""
+
+#: embed/ad_embed.module:39
+msgid "Automatically embedding ads:"
+msgstr ""
+
+#: embed/ad_embed.module:40
+msgid "You can enable the functionality to automatically embed ads in your content at <em>administer >> ads >> settings >> embed ads</em>.  On this configuration page, you can choose which content types should contain automatically embedded ads, after which paragraph the ad should appear, and from which ad group the advertisement should be selected."
+msgstr ""
+
+#: embed/ad_embed.module:102
+msgid "Manually embedded ads"
+msgstr ""
+
+#: embed/ad_embed.module:103
+msgid "Configuration options for manually embedded ads using [[ad]] and &lt!--ad--&gt; tags."
+msgstr ""
+
+#: embed/ad_embed.module:108
+msgid "Replace ad bracket tags"
+msgstr ""
+
+#: embed/ad_embed.module:110
+msgid "Replace [[ad]] style tags in site content with advertisements."
+msgstr ""
+
+#: embed/ad_embed.module:115
+msgid "Replace ad comment tags"
+msgstr ""
+
+#: embed/ad_embed.module:117
+msgid "Replace &lt;!--ad--&gt; style tags in site content with advertisements."
+msgstr ""
+
+#: embed/ad_embed.module:123
+msgid "Automatically embedded ads"
+msgstr ""
+
+#: embed/ad_embed.module:124
+msgid "Enable automatically embedded ads for any of the following content types, causing ads to be automatically inserted when users are viewing the content.  Configure where the ad should be placed, from which ad group(s) the ads should be selected, and how many ads to display."
+msgstr ""
+
+#: embed/ad_embed.module:138
+msgid "Automatically embed ads in %type content"
+msgstr ""
+
+#: embed/ad_embed.module:144
+msgid "Location"
+msgstr ""
+
+#: embed/ad_embed.module:146
+msgid "Before the first paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:147
+msgid "After the first paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:148
+msgid "After the second paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:149
+msgid "After the third paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:150
+msgid "After the fourth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:151
+msgid "After the fifth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:152
+msgid "After the sixth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:153
+msgid "After the seventh paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:154
+msgid "After the eighth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:155
+msgid "After the ninth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:156
+msgid "After the tenth paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:157
+msgid "After the last paragraph"
+msgstr ""
+
+#: embed/ad_embed.module:160
+msgid "Specify where you would like to automatically embed the advertisement for this content type."
+msgstr ""
+
+#: embed/ad_embed.module:165
+msgid "Always display ad"
+msgstr ""
+
+#: embed/ad_embed.module:167
+msgid "Check this box to display an ad even if the content doesn't have as many paragraphs as you specified above.  In these cases the ad will appear at the end of the content."
+msgstr ""
+
+#: embed/ad_embed.module:173
+msgid "Ad group"
+msgstr ""
+
+#: embed/ad_embed.module:176
+msgid "Specify from which ad group(s) you'd like to display advertisements."
+msgstr ""
+
+#: embed/ad_embed.module:181 remote/ad_remote.module:67
+msgid "Quantity"
+msgstr ""
+
+#: embed/ad_embed.module:184
+msgid "Specify how many advertisements to display at the same time."
+msgstr ""
+
+#: embed/ad_embed.module:216
+msgid "Embedded ad configuration options saved."
+msgstr ""
+
+#: embed/ad_embed.module:50
+msgid "embed ads in content"
+msgstr ""
+
+#: embed/ad_embed.module:60
+msgid "Embedded ads"
+msgstr ""
+
+#: embed/ad_embed.module:0
+msgid "ad_embed"
+msgstr ""
+
+#: embed/ad_embed.info:0
+msgid "Embed"
+msgstr ""
+
+#: embed/ad_embed.info:0
+msgid "Embed ads in content."
+msgstr ""
+
+#: external/ad_external.module:55;97;98
+msgid "An external advertisement, displayed in an IFrame."
+msgstr ""
+
+#: external/ad_external.module:95
+msgid "External ad"
+msgstr ""
+
+#: external/ad_external.module:112
+msgid "External"
+msgstr ""
+
+#: external/ad_external.module:125
+msgid "External Source URL"
+msgstr ""
+
+#: external/ad_external.module:128
+msgid "Enter the complete URL where your external ad his hosted.  The URL must begin with http:// or https://.  For example, %url."
+msgstr ""
+
+#: external/ad_external.module:128
+msgid "http://www.sample.org/external/ad.php"
+msgstr ""
+
+#: external/ad_external.module:144 image/ad_image.module:455 text/ad_text.module:235
+msgid "Links to !url."
+msgstr ""
+
+#: external/ad_external.module:89
+msgid "Unexpected redirect attempt in external ad type."
+msgstr ""
+
+#: external/ad_external.module:0
+msgid "ad_external"
+msgstr ""
+
+#: external/ad_external.info:0
+msgid "External Ad"
+msgstr ""
+
+#: external/ad_external.info:0
+msgid "Enhances the ad module to support externally hosted ads.  Typically combined with IFrames."
+msgstr ""
+
+#: html/ad_html.module:32;71;72
+msgid "A html advertisement."
+msgstr ""
+
+#: html/ad_html.module:69
+msgid "HTML ad"
+msgstr ""
+
+#: html/ad_html.module:90
+msgid "HTML"
+msgstr ""
+
+#: html/ad_html.module:102
+msgid "Ad HTML"
+msgstr ""
+
+#: html/ad_html.module:105
+msgid "Paste the complete HTML provided by your advertising affiliate."
+msgstr ""
+
+#: html/ad_html.module:0
+msgid "ad_html"
+msgstr ""
+
+#: html/ad_html.info:0
+msgid "HTML Ad"
+msgstr ""
+
+#: html/ad_html.info:0
+msgid "Enhances the ad module to support html-based ads, such as those defined by many advertising affiliate websites."
+msgstr ""
+
+#: image/ad_image_views.inc:23 text/ad_text_views.inc:23
+msgid "As link"
+msgstr ""
+
+#: image/ad_image_views.inc:24 text/ad_text_views.inc:24
+msgid "Not as link"
+msgstr ""
+
+#: image/ad_image_views.inc:34
+msgid "Linked Image"
+msgstr ""
+
+#: image/ad_image_views.inc:35
+msgid "Unlinked Image"
+msgstr ""
+
+#: image/ad_image_views.inc:36
+msgid "Image path"
+msgstr ""
+
+#: image/ad_image.module:49;227;228
+msgid "An image or banner advertisement."
+msgstr ""
+
+#: image/ad_image.module:79
+msgid "Minimum height"
+msgstr ""
+
+#: image/ad_image.module:83
+msgid "Optionally specify a minimum height in pixels for images in this group.  To specify no minimum height, enter <em>0</em>."
+msgstr ""
+
+#: image/ad_image.module:87
+msgid "Minimum width"
+msgstr ""
+
+#: image/ad_image.module:91
+msgid "Optionally specify a minimum width in pixels for images in this group.  To specify no minimum width, enter <em>0</em>."
+msgstr ""
+
+#: image/ad_image.module:95
+msgid "Maximum height"
+msgstr ""
+
+#: image/ad_image.module:99
+msgid "Optionally specify a maximum height in pixels for images in this group.  To specify no maximum height, enter <em>0</em>."
+msgstr ""
+
+#: image/ad_image.module:103
+msgid "Maximum width"
+msgstr ""
+
+#: image/ad_image.module:107
+msgid "Optionally specify a maximum width in pixels for images in this group.  To specify no maximum width, enter <em>0</em>."
+msgstr ""
+
+#: image/ad_image.module:189
+msgid "Image validation failed, unable to mark ad as %active.  Setting ad as %pending.  If you do not see any more errors, you should now be able to set your ad as %active."
+msgstr ""
+
+#: image/ad_image.module:195
+msgid "Unable to mark ad as <em>active</em> until uploaded image is validated.  If you do not see any more errors, you should now be able to set your ad as <em>active</em>."
+msgstr ""
+
+#: image/ad_image.module:202 text/ad_text.module:270
+msgid "You must specify a valid %field."
+msgstr ""
+
+#: image/ad_image.module:202;429 text/ad_text.module:201;270
+msgid "Destination URL"
+msgstr ""
+
+#: image/ad_image.module:205;414
+msgid "It is required that you upload an image for your image advertisement."
+msgstr ""
+
+#: image/ad_image.module:225
+msgid "Image ad"
+msgstr ""
+
+#: image/ad_image.module:238
+msgid "The required <em>upload module</em> is not enabled, you will not be able to upload image ads.  Please %enable the upload module, or %disable the ad_image module."
+msgstr ""
+
+#: image/ad_image.module:241
+msgid "You will not be able to upload image ads until you !enable for the advertisement content type."
+msgstr ""
+
+#: image/ad_image.module:241
+msgid "enable attachments"
+msgstr ""
+
+#: image/ad_image.module:245
+msgid "Your website is configured to use Drupal's private !method.  You have to enable \"!view\" permissions in addition to the ad module's standard \"!show\" permissions for all roles that you wish to view image advertisements."
+msgstr ""
+
+#: image/ad_image.module:245
+msgid "download method"
+msgstr ""
+
+#: image/ad_image.module:245
+msgid "view uploaded files"
+msgstr ""
+
+#: image/ad_image.module:307
+msgid "The image <em>%name</em> is only %current pixels wide, which is less than the minimum of %minimum pixels allowed in the %group ad group."
+msgstr ""
+
+#: image/ad_image.module:311
+msgid "The image <em>%name</em> is %current pixels wide, which is more than the maximum of %maximum pixels allowed in the %group ad group."
+msgstr ""
+
+#: image/ad_image.module:315
+msgid "The image <em>%name</em> is only %current pixels high, which is less than the minimum of %minimum pixels allowed in the %group ad group."
+msgstr ""
+
+#: image/ad_image.module:319
+msgid "The image <em>%name</em> is %current pixels high, which is more than the maximum of %maximum pixels allowed in the %group ad group."
+msgstr ""
+
+#: image/ad_image.module:375
+msgid "Image"
+msgstr ""
+
+#: image/ad_image.module:395
+msgid "(invalid image)"
+msgstr ""
+
+#: image/ad_image.module:398
+msgid "(active)"
+msgstr ""
+
+#: image/ad_image.module:401
+msgid "(inactive)"
+msgstr ""
+
+#: image/ad_image.module:405
+msgid "Unable to locate image %image."
+msgstr ""
+
+#: image/ad_image.module:406
+msgid "Unable to locate the uploaded image."
+msgstr ""
+
+#: image/ad_image.module:411
+msgid "No images have been uploaded.  Please upload an image via the <em>File attachments</em> form section below.<br />"
+msgstr ""
+
+#: image/ad_image.module:418
+msgid "<br />Only the first uploaded image that has <em>List</em> checked in the <em>File attachments</em> form section below will be displayed as an advertisement.  The image that will be displayed is marked as <em>active</em> above."
+msgstr ""
+
+#: image/ad_image.module:432
+msgid "Enter the complete URL if you want people to be redirected when they click on this advertisement.  The URL must begin with http:// or https://.  For example, %url.  If you do not enter a URL, the advertisement will not be clickable."
+msgstr ""
+
+#: image/ad_image.module:432 text/ad_text.module:205
+msgid "http://www.sample.org/"
+msgstr ""
+
+#: image/ad_image.module:437
+msgid "Mouseover"
+msgstr ""
+
+#: image/ad_image.module:440
+msgid "Optionally enter text to appear when a mouse pointer hovers over the ad image."
+msgstr ""
+
+#: image/ad_image.module:0
+msgid "ad_image"
+msgstr ""
+
+#: image/ad_image.info:0
+msgid "Image Ad"
+msgstr ""
+
+#: image/ad_image.info:0
+msgid "Enhances the ad module to support banner ads."
+msgstr ""
+
+#: notify/ad_notify.module:21
+msgid "The ad_notify modules provides email notifications for the ad module."
+msgstr ""
+
+#: notify/ad_notify.module:68
+msgid "!owner's notifications"
+msgstr ""
+
+#: notify/ad_notify.module:162;163;164;165;166;167;168;169;170;171;172;173
+msgid "never"
+msgstr ""
+
+#: notify/ad_notify.module:231
+msgid "Email every @when as long as the ad is active."
+msgstr ""
+
+#: notify/ad_notify.module:257
+msgid "Last sent"
+msgstr ""
+
+#: notify/ad_notify.module:258
+msgid "Notification"
+msgstr ""
+
+#: notify/ad_notify.module:260
+msgid "Action"
+msgstr ""
+
+#: notify/ad_notify.module:269
+msgid "!time ago"
+msgstr ""
+
+#: notify/ad_notify.module:269
+msgid "Never"
+msgstr ""
+
+#: notify/ad_notify.module:271
+msgid "enabled"
+msgstr ""
+
+#: notify/ad_notify.module:271
+msgid "disabled"
+msgstr ""
+
+#: notify/ad_notify.module:272
+msgid "delete"
+msgstr ""
+
+#: notify/ad_notify.module:284
+msgid "You can configure one or more notifications for your advertisement using the drop down menus below.  For example, to receive a weekly notification with information about how often your advertisement was viewed and clicked, select the <em>email every @when as long as the ad is active</em> event, and <em>1 week</em> for when.  Or, to receive a reminder that your advertisement will expire in 24 hours select the <em>email @when before the advertisement will expire</em>, and <em>1 day</em> for when."
+msgstr ""
+
+#: notify/ad_notify.module:285
+msgid "If you schedule a delay between an event and when you are notified and the event happens multiple times, only one notification will be sent.  For example, if you create a notification for <em>email 1 day after the advertisement is clicked</em> and the ad is clicked 42 more times in the next 24 hours, you will only receive one email 24 hours after your ad was first clicked that notes that your ad was clicked a total of 43 times in the past 24 hours."
+msgstr ""
+
+#: notify/ad_notify.module:289
+msgid "Edit notification"
+msgstr ""
+
+#: notify/ad_notify.module:289
+msgid "Create new notification"
+msgstr ""
+
+#: notify/ad_notify.module:296
+msgid "Event"
+msgstr ""
+
+#: notify/ad_notify.module:298
+msgid "Select an event for which you would like to receive a notification."
+msgstr ""
+
+#: notify/ad_notify.module:304
+msgid "When"
+msgstr ""
+
+#: notify/ad_notify.module:306
+msgid "Select a value to replace @when in the event notification you selected above."
+msgstr ""
+
+#: notify/ad_notify.module:312
+msgid "One-time"
+msgstr ""
+
+#: notify/ad_notify.module:313
+msgid "Check this box if this notification email should only be sent one time.  If not checked, an email will be sent each time the event happens.  If checked, an email will only be sent the first time the event happens, then the notification will be automatically disabled."
+msgstr ""
+
+#: notify/ad_notify.module:321
+msgid "Locked"
+msgstr ""
+
+#: notify/ad_notify.module:322
+msgid "Check this box if you are setting up a notification for someone else, and you don't want them to be able to disable the notification.  Only users with the <em>manage owners</em> permission for this ad can edit or delete a locked notification."
+msgstr ""
+
+#: notify/ad_notify.module:336
+msgid "Message"
+msgstr ""
+
+#: notify/ad_notify.module:347
+msgid "Notify address"
+msgstr ""
+
+#: notify/ad_notify.module:347
+msgid "The email will be sent to %address."
+msgstr ""
+
+#: notify/ad_notify.module:360
+msgid "Subject"
+msgstr ""
+
+#: notify/ad_notify.module:367
+msgid "Body"
+msgstr ""
+
+#: notify/ad_notify.module:370
+msgid "Enter the body of your notification email.  The following variables can be used in your message and will be automatically replaced before the email is sent:"
+msgstr ""
+
+#: notify/ad_notify.module:371
+msgid "%sitename: the name of this website."
+msgstr ""
+
+#: notify/ad_notify.module:372
+msgid "%owner_name: the username of the ad owner."
+msgstr ""
+
+#: notify/ad_notify.module:373
+msgid "%owner_mail: the email address of the ad owner."
+msgstr ""
+
+#: notify/ad_notify.module:374
+msgid "%owner_uid: the user ID of the ad owner."
+msgstr ""
+
+#: notify/ad_notify.module:375
+msgid "%event: the type of event that has triggered this notification."
+msgstr ""
+
+#: notify/ad_notify.module:376
+msgid "%count: the number of times the event happened."
+msgstr ""
+
+#: notify/ad_notify.module:377
+msgid "%frequency: a complete sentence describing the frequency this notification will be sent."
+msgstr ""
+
+#: notify/ad_notify.module:378
+msgid "%type: the type of ad."
+msgstr ""
+
+#: notify/ad_notify.module:379
+msgid "%status: the status of the ad."
+msgstr ""
+
+#: notify/ad_notify.module:380
+msgid "%url: the url of the advertisement."
+msgstr ""
+
+#: notify/ad_notify.module:381
+msgid "%siteurl: the url of the website."
+msgstr ""
+
+#: notify/ad_notify.module:382
+msgid "%redirect: the redirection url of the advertisement."
+msgstr ""
+
+#: notify/ad_notify.module:383
+msgid "%title: the title of the advertisement."
+msgstr ""
+
+#: notify/ad_notify.module:384
+msgid "%aid: the ID of the advertisement."
+msgstr ""
+
+#: notify/ad_notify.module:385
+msgid "%comments: the number of comments attached to the advertisement."
+msgstr ""
+
+#: notify/ad_notify.module:386
+msgid "%created_small, %created_medium, %created_large: various formats of when the advertisement was created."
+msgstr ""
+
+#: notify/ad_notify.module:387
+msgid "%activated_small, %activated_medium, %activated_large: various formats of when the advertisement was activated."
+msgstr ""
+
+#: notify/ad_notify.module:388
+msgid "%expired_small, %expired_medium, %expired_large: various formats of when the advertisement was expired."
+msgstr ""
+
+#: notify/ad_notify.module:389
+msgid "%autoactivate_small, %autoactivate_medium, %autoactivate_large: various formats of when the advertisement was automatically activated."
+msgstr ""
+
+#: notify/ad_notify.module:390
+msgid "%autoexpire_small, %autoexpire_medium, %autoexpire_large: various formats of when the advertisement was automatically expired."
+msgstr ""
+
+#: notify/ad_notify.module:391
+msgid "%maxviews: the maximum number of times this advertisement is allowed to be viewed."
+msgstr ""
+
+#: notify/ad_notify.module:392
+msgid "%maxclicks: the maximum number of times this advertisement is allowed to be clicked."
+msgstr ""
+
+#: notify/ad_notify.module:393
+msgid "%global_views, %global_clicks, %last_year_views, %last_year_clicks, %this_year_views, %this_year_clicks, %last_month_views, %last_month_clicks, %this_month_views, %this_month_clicks, %this_week_views, %this_week_clicks, %yesterday_views, %yesterday_clicks, %today_views, %today_clicks, %last_hour_views, %last_hour_clicks, %this_hour_views, %this_hour_clicks: various advertisement statistics"
+msgstr ""
+
+#: notify/ad_notify.module:426
+msgid "Create notification"
+msgstr ""
+
+#: notify/ad_notify.module:434
+msgid "Notifications"
+msgstr ""
+
+#: notify/ad_notify.module:446
+msgid "There are no notifications configured for %owner."
+msgstr ""
+
+#: notify/ad_notify.module:459
+msgid "You are not allowed to schedule a regular notification more frequently than once an hour."
+msgstr ""
+
+#: notify/ad_notify.module:464
+msgid "You have already scheduled that notification."
+msgstr ""
+
+#: notify/ad_notify.module:470
+msgid "This notification is locked, you will need to contact the site administrator to edit this notification for you."
+msgstr ""
+
+#: notify/ad_notify.module:498
+msgid "Hello %owner_name,\n\n  This is an automatically generated notification about your advertisement \"%title\" that is being displayed on the %sitename website.\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"
+msgstr ""
+
+#: notify/ad_notify.module:581
+msgid "This notification is locked, you will need to contact the site administrator to delete this notification for you."
+msgstr ""
+
+#: notify/ad_notify.module:606
+msgid "notifications"
+msgstr ""
+
+#: notify/ad_notify.module:44
+msgid "My notifications"
+msgstr ""
+
+#: notify/ad_notify.module:53
+msgid "Delete notification"
+msgstr ""
+
+#: notify/ad_notify.module:0
+msgid "ad_notify"
+msgstr ""
+
+#: notify/ad_notify.info:0
+msgid "Notify"
+msgstr ""
+
+#: notify/ad_notify.info:0
+msgid "Receive email notifications regarding ads.core = 6.x"
+msgstr ""
+
+#: owners/ad_owners.module:80
+msgid "!owner's permissions"
+msgstr ""
+
+#: owners/ad_owners.module:94;409
+msgid "Permissions"
+msgstr ""
+
+#: owners/ad_owners.module:96
+msgid "Select which permissions will be automatically granted to new owners of <em>!type</em> advertisements."
+msgstr ""
+
+#: owners/ad_owners.module:100
+msgid "Default permissions for <em>!type</em> owners"
+msgstr ""
+
+#: owners/ad_owners.module:193;29
+msgid "Ad owners"
+msgstr ""
+
+#: owners/ad_owners.module:201;248
+msgid "Username"
+msgstr ""
+
+#: owners/ad_owners.module:215;416
+msgid "permissions"
+msgstr ""
+
+#: owners/ad_owners.module:219
+msgid "remove"
+msgstr ""
+
+#: owners/ad_owners.module:237;252;64
+msgid "Add owner"
+msgstr ""
+
+#: owners/ad_owners.module:245
+msgid "Enter the username of the user who should have ownership permissions on this advertisement."
+msgstr ""
+
+#: owners/ad_owners.module:261
+msgid "The specified username %username does not exist."
+msgstr ""
+
+#: owners/ad_owners.module:264
+msgid "The specified user %username is already an owner of this ad."
+msgstr ""
+
+#: owners/ad_owners.module:268
+msgid "The specified user %username does not have <em>edit own 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."
+msgstr ""
+
+#: owners/ad_owners.module:268
+msgid "assigned"
+msgstr ""
+
+#: owners/ad_owners.module:268
+msgid "role"
+msgstr ""
+
+#: owners/ad_owners.module:277
+msgid "The user is already an owner of the ad."
+msgstr ""
+
+#: owners/ad_owners.module:280
+msgid "The user %username has been added as an owner of this advertisement."
+msgstr ""
+
+#: owners/ad_owners.module:368
+msgid "Are you sure you want to remove user %name as an owner of this advertisement?"
+msgstr ""
+
+#: owners/ad_owners.module:371
+msgid "Remove"
+msgstr ""
+
+#: owners/ad_owners.module:383
+msgid "%name is the primary owner of this advertisement.  You cannot remove the primary owner."
+msgstr ""
+
+#: owners/ad_owners.module:398
+msgid "The ad owner %name has been removed."
+msgstr ""
+
+#: owners/ad_owners.module:416
+msgid "granted"
+msgstr ""
+
+#: owners/ad_owners.module:486
+msgid "The permissions have been saved."
+msgstr ""
+
+#: owners/ad_owners.module:55
+msgid "Remove owner"
+msgstr ""
+
+#: owners/ad_owners.module:0
+msgid "ad_owners"
+msgstr ""
+
+#: owners/ad_owners.info:0
+msgid "Ad Owners"
+msgstr ""
+
+#: owners/ad_owners.info:0
+msgid "Enhances the ad module to support ad owners."
+msgstr ""
+
+#: remote/ad_remote.module:47
+msgid "Use the following options to build a source snippet for displaying ads on your website."
+msgstr ""
+
+#: remote/ad_remote.module:53
+msgid "Select one or more groups to display ads from."
+msgstr ""
+
+#: remote/ad_remote.module:70
+msgid "Select the maximum number of unique ads that should be displayed together."
+msgstr ""
+
+#: remote/ad_remote.module:76
+msgid "Code snippet"
+msgstr ""
+
+#: remote/ad_remote.module:77
+msgid "Insert the following source snippet into your web page to display ads hosted on this web site.  Include the entire snippet, and do not modify it in any way."
+msgstr ""
+
+#: remote/ad_remote.module:94
+msgid "start"
+msgstr ""
+
+#: remote/ad_remote.module:94
+msgid "end"
+msgstr ""
+
+#: remote/ad_remote.module:107
+msgid "Generate code snippet"
+msgstr ""
+
+#: remote/ad_remote.module:118
+msgid "At least one group should be selected"
+msgstr ""
+
+#: remote/ad_remote.module:17
+msgid "host remote advertisements"
+msgstr ""
+
+#: remote/ad_remote.module:27
+msgid "Remote ads"
+msgstr ""
+
+#: remote/ad_remote.module:0
+msgid "ad_remote"
+msgstr ""
+
+#: remote/ad_remote.info:0
+msgid "Remote"
+msgstr ""
+
+#: remote/ad_remote.info:0
+msgid "Generates cut-and-paste source snippets allowing ads to be easily displayed on remote websites."
+msgstr ""
+
+#: report/ad_report.module:84
+msgid "Past twelve hours"
+msgstr ""
+
+#: report/ad_report.module:87
+msgid "Past twelve days"
+msgstr ""
+
+#: report/ad_report.module:90
+msgid "Past twelve weeks"
+msgstr ""
+
+#: report/ad_report.module:93
+msgid "Past twelve months"
+msgstr ""
+
+#: report/ad_report.module:19
+msgid "Reports"
+msgstr ""
+
+#: report/ad_report.module:27
+msgid "Monthly Reports"
+msgstr ""
+
+#: report/ad_report.module:35
+msgid "Weekly Reports"
+msgstr ""
+
+#: report/ad_report.module:43
+msgid "Daily Reports"
+msgstr ""
+
+#: report/ad_report.module:51
+msgid "Hourly Reports"
+msgstr ""
+
+#: report/ad_report.module:59
+msgid "Bar graph"
+msgstr ""
+
+#: report/ad_report.module:0
+msgid "ad_report"
+msgstr ""
+
+#: report/ad_report.info:0
+msgid "Report"
+msgstr ""
+
+#: report/ad_report.info:0
+msgid "Provides comprehensive charts and reports about advertising statistics.core = 6.x"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:31
+msgid "New"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:34
+msgid "Valid"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:37
+msgid "Duplicate"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:40
+msgid "Ad owner"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:43
+msgid "Filtered role"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:46
+msgid "Bot"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:49
+msgid "Unknown"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:89
+msgid "view filtered clicks"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:89
+msgid "filter clicks"
+msgstr ""
+
+#: statistics/click_filter/click_filter.module:0
+msgid "click_filter"
+msgstr ""
+
+#: statistics/click_filter/click_filter.info:0
+msgid "Click filter"
+msgstr ""
+
+#: statistics/click_filter/click_filter.info:0
+msgid "Filter duplicate click statistics."
+msgstr ""
+
+#: text/ad_text.module:53;169;170
+msgid "A text advertisement."
+msgstr ""
+
+#: text/ad_text.module:67
+msgid "Header minimum length"
+msgstr ""
+
+#: text/ad_text.module:71
+msgid "Optionally specify the minimum number of characters allowed in the header of a text ad.  Set to <em>0</em> to specify no minimum length."
+msgstr ""
+
+#: text/ad_text.module:75
+msgid "Header maximum length"
+msgstr ""
+
+#: text/ad_text.module:79
+msgid "Optionally specify the maximum number of characters allowed in the header of a text ad.  Set to <em>0</em> to specify no maximum length."
+msgstr ""
+
+#: text/ad_text.module:83
+msgid "Body minimum length"
+msgstr ""
+
+#: text/ad_text.module:87
+msgid "Optionally specify the minimum number of characters allowed in the body of a text ad.  Set to <em>0</em> to specify no minimum length."
+msgstr ""
+
+#: text/ad_text.module:91
+msgid "Body maximum length"
+msgstr ""
+
+#: text/ad_text.module:95
+msgid "Optionally specify the maximum number of characters allowed in the body of a text ad.  Set to <em>0</em> to specify no maximum length."
+msgstr ""
+
+#: text/ad_text.module:112
+msgid "The header minimum length can not be greater than the header maximum length."
+msgstr ""
+
+#: text/ad_text.module:115
+msgid "The body minimum length can not be greater than the body maximum length."
+msgstr ""
+
+#: text/ad_text.module:167
+msgid "Text ad"
+msgstr ""
+
+#: text/ad_text.module:189
+msgid "Text"
+msgstr ""
+
+#: text/ad_text.module:205
+msgid "Enter the complete URL where you want people to be redirected when they click on this advertisement.  The URL must begin with http:// or https://.  For example, %url."
+msgstr ""
+
+#: text/ad_text.module:210
+msgid "Ad header"
+msgstr ""
+
+#: text/ad_text.module:213
+msgid "This is the first line of the ad which will be linked to the URL entered above."
+msgstr ""
+
+#: text/ad_text.module:217
+msgid "Ad body"
+msgstr ""
+
+#: text/ad_text.module:220
+msgid "This is the rest of the ad."
+msgstr ""
+
+#: text/ad_text.module:254
+msgid "Your text ad header is only %cur characters but must be at least %min characters."
+msgstr ""
+
+#: text/ad_text.module:257
+msgid "Your text ad header is %cur characters but can not be more than %max characters."
+msgstr ""
+
+#: text/ad_text.module:263
+msgid "Your text ad body is only %cur characters but must be at least %min characters."
+msgstr ""
+
+#: text/ad_text.module:266
+msgid "Your text ad body is %cur characters but can not be more than %max characters."
+msgstr ""
+
+#: text/ad_text.module:0
+msgid "ad_text"
+msgstr ""
+
+#: text/ad_text.info:0
+msgid "Text Ad"
+msgstr ""
+
+#: text/ad_text.info:0
+msgid "Enhances the ad module to support static text ads."
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:75
+msgid "Enabled"
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:76
+msgid "If enabled, each ad in this group will be weighted per the percentages defined below."
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:96
+msgid "Display percent"
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:100
+msgid "Enter a percentage from 0 to 100.  The total percentages of all ads in this group must add up to 100.  For example, if you have two ads, and want one to be displayed 70% of the time and the other 30% of the time enter '70' in one and '30' in the other."
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:108
+msgid "Submit"
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:131
+msgid "The total percentage for all ads in the %group group combined must equal 100%.  It currently equals %percent."
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:41
+msgid "Weight Percent"
+msgstr ""
+
+#: weight/percent/ad_weight_percent.module:0
+msgid "ad_weight_percent"
+msgstr ""
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weight/percent/ad_weight_percent.inc	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,35 @@
+<?php
+// $Id: ad_weight_percent.inc,v 1.1.2.2.2.2 2009/02/16 17:06:50 jeremy Exp $
+
+/**
+ * @file
+ * A plug in for the ad.module, providing a percentage based weighting mechanism
+ * for the random selection of ads.
+ *
+ * Copyright (c) 2007-2009.
+ *  Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+function ad_weight_percent_select_ad($ads, $quantity, $invalid) {
+  $tid = NULL;
+  if (adserve_variable('tids')) {
+    $id = adserve_variable('tids');
+    $tids = explode(',', $id);
+    // A group can only be comprised of one tid.  If multiple tids are defined,
+    // then we're not working with a group.
+    if (sizeof($tids) == 1) {
+      $tid = $tids[0];
+    }
+  }
+  else if (!adserve_variable('nids')) {
+    // The default group.
+    $tid = 0;
+  }
+
+  // Only do something if we're currently processing a specific ad group.
+  if ($tid !== NULL) {
+    // TODO: The percentage information needs to be stored in the file cache,
+    // as obviously we don't want to bootstrap the database layer and perform
+    // a query at this point.
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weight/percent/ad_weight_percent.install	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,36 @@
+<?php
+// $Id: ad_weight_percent.install,v 1.1.2.2.2.2 2009/02/16 17:06:50 jeremy Exp $
+
+/**
+ * Ad weight percent module database schema.
+ * Copyright (c) 2007-2009
+ *   Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Installation of the ad_weight_percent schema.
+ */
+function ad_weight_percent_install() {
+  switch ($GLOBALS['db_type']) {
+    case 'mysql':
+    case 'mysqli':
+    default:
+      db_query("CREATE TABLE {ad_weight_percent} (
+        tid INT NOT NULL DEFAULT '0',
+        aid INT NOT NULL DEFAULT '0',
+        weight INT(3) NOT NULL DEFAULT '0',
+
+        UNIQUE KEY (tid, aid))");
+  }
+}
+
+/**
+ * Complete uninstallation of the ad_weight_percent module.
+ */
+function ad_weight_percent_uninstall() {
+  // Remove tables.
+  drupal_uninstall_schema('ad_weight_percent');
+
+  drupal_set_message('The ad_weight_percent module has been completely uninstalled.');
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/weight/percent/ad_weight_percent.module	Fri Feb 20 14:04:09 2009 +0000
@@ -0,0 +1,236 @@
+<?php
+// $Id: ad_weight_percent.module,v 1.1.2.4.2.2 2009/02/16 17:06:50 jeremy Exp $
+
+/**
+ * @file
+ * A plug in for the ad.module, providing a percentage based weighting mechanism
+ * for the random selection of ads.
+ *
+ * Copyright (c) 2007-2009.
+ *  Jeremy Andrews <jeremy@tag1consulting.com>.
+ */
+
+/**
+ * Drupal hook_menu().
+ */
+function ad_weight_percent_menu() {
+  $items = array();
+
+/* TODO
+   Non menu code that was placed in hook_menu under the '!$may_cache' block
+   so that it could be run during initialization, should now be moved to hook_init.
+   Previously we called hook_init twice, once early in the bootstrap process, second
+   just after the bootstrap has finished. The first instance is now called boot
+   instead of init.
+   
+   In Drupal 6, there are now two hooks that can be used by modules to execute code
+   at the beginning of a page request. hook_boot() replaces hook_boot() in Drupal 5
+   and runs on each page request, even for cached pages. hook_boot() now only runs
+   for non-cached pages and thus can be used for code that was previously placed in
+   hook_menu() with $may_cache = FALSE:
+   
+   Dynamic menu items under a '!$may_cache' block can often be simplified
+   to remove references to arg(n) and use of '%<function-name>' to check
+   conditions. See http://drupal.org/node/103114.
+   
+   The title and description arguments should not have strings wrapped in t(),
+   because translation of these happen in a later stage in the menu system.
+*/
+  if ($may_cache) {
+    $items['admin/content/ad/groups/percent'] = array(
+      'title' => 'Weight Percent',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('ad_weight_percent_settings'),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 5,
+    );
+  }
+
+  return $items;
+}
+
+/**
+ * Configure per-group percentage settings.
+ */
+function ad_weight_percent_settings() {
+  $form = array();
+
+  $groups = module_invoke('ad', 'groups_list', TRUE);
+  foreach ($groups as $tid => $group) {
+    $form["group-$tid"] = array(
+      '#type' => 'fieldset',
+      '#title' => $group->name,
+      '#collapsible' => TRUE,
+      '#collapsed' => variable_get("enable-$tid", 0) ? FALSE : TRUE,
+    );
+    $form["group-$tid"]["description-$tid"] = array(
+      '#type' => 'markup',
+      '#prefix' => '<div>',
+      '#suffix' => '</div>',
+      '#value' => theme_placeholder("$group->description"),
+    );
+    $form["group-$tid"]["enable-$tid"] = array(
+      '#type' => 'checkbox',
+      '#default_value' => variable_get("enable-$tid", 0),
+      '#title' => t('Enabled'),
+      '#description' => t('If enabled, each ad in this group will be weighted per the percentages defined below.'),
+    );
+
+    $result = db_query('SELECT nid FROM {term_node} WHERE tid = %d', $group->tid);
+    while ($nid = db_fetch_object($result)) {
+      $ad = node_load($nid->nid);
+      $percent = db_fetch_object(db_query('SELECT * FROM {ad_weight_percent} WHERE tid = %d AND aid = %d', $tid, $nid->nid));
+      $form["group-$tid"]["ad-$tid"]["$tid-$nid->nid"] = array(
+        '#type' => 'fieldset',
+        '#title' => $ad->title,
+        '#collapsible' => TRUE,
+      );
+      $form["group-$tid"]["ad-$tid"]["$tid-$nid->nid"]["ad-$tid-$nid->nid"] = array(
+        '#type' => 'markup',
+        '#prefix' => '<div>',
+        '#suffix' => '</div>',
+        '#value' => "$ad->ad<br />$ad->url",
+      );
+      $form["group-$tid"]["ad-$tid"]["$tid-$nid->nid"]["percent-$tid-$nid->nid"] = array(
+        '#type' => 'textfield',
+        '#title' => t('Display percent'), 
+        '#default_value' => $percent->weight, 
+        '#size' => 2,
+        '#maxlength' => 3,
+        '#description' => t("Enter a percentage from 0 to 100.  The total percentages of all ads in this group must add up to 100.  For example, if you have two ads, and want one to be displayed 70% of the time and the other 30% of the time enter '70' in one and '30' in the other."),
+      );
+    }
+
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+
+  return $form;
+}
+
+/**
+ * Be sure that all enabled groups add up to a total of 100%.
+ */
+function ad_weight_percent_settings_validate($form, &$form_state) {
+  $groups = module_invoke('ad', 'groups_list', TRUE);
+  foreach ($groups as $tid => $group) {
+    if ($form_state['values']["enable-$tid"]) {
+      $result = db_query('SELECT nid FROM {term_node} WHERE tid = %d', $group->tid);
+      $total = 0;
+      // Add up total percentages for all nids in group, confirm equals 100%.
+      $first = 0;
+      while ($nid = db_fetch_object($result)) {
+        if (!$first) $first = $nid->nid;
+        $total = $total + (int)$form_state['values']["percent-$tid-$nid->nid"];
+      }
+      // Confirmed that total equals 100%.
+      if ($total != 100) {
+        form_set_error("percent-$tid-$first", t('The total percentage for all ads in the %group group combined must equal 100%.  It currently equals %percent.', array('%group' => $group->name, '%percent' => "$total%")));
+      }
+    }
+  }
+}
+
+/**
+ * Save the weight percent settings in the database.
+ */
+function ad_weight_percent_settings_submit($form, &$form_state) {
+  $groups = module_invoke('ad', 'groups_list', TRUE);
+  foreach ($groups as $tid => $group) {
+    variable_set("enable-$tid", (int)$form_state['values']["enable-$tid"]);
+    db_query('DELETE FROM {ad_weight_percent} WHERE tid = %d', $tid);
+    $result = db_query('SELECT nid FROM {term_node} WHERE tid = %d', $group->tid);
+    while ($nid = db_fetch_object($result)) {
+      db_query('INSERT INTO {ad_weight_percent} (tid, aid, weight) VALUES(%d, %d, %d)', $tid, $nid->nid, (int)$form_state['values']["percent-$tid-$nid->nid"]);
+    }
+  }
+}
+
+/**
+ * Returns the greatest common divisor of an array of integers.
+ */
+function ad_weight_percent_gcd($integers) {
+  $gcd = array_shift($integers);
+
+  while (!empty($integers)) {
+    $gcd = _ad_weight_percent_gcd($gcd, array_shift($integers));
+  }
+  return $gcd;
+}
+
+/**
+ * Helper function to calculate the greatest common divisor using the Euclidean
+ * algorithm (http://en.wikipedia.org/wiki/Euclidean_algorithm).
+ */
+function _ad_weight_percent_gcd($a, $b) {
+  if ($b == 0) {
+    return $a;
+  }
+  else {
+    return _ad_weight_percent_gcd($b, $a % $b);
+  }
+}
+
+/**
+ * Ad module's adcacheapi _hook().
+ */
+/*
+function ad_cache_file_adcacheapi($op, &$node) {
+  switch ($op) {
+    case 'display_variables':
+      $files = variable_get('ad_files', 3);
+      $path = file_create_path();
+      return "&amp;f=$files&amp;p=$path";
+    case 'method':
+      return array('file' => t('File'));
+    case 'description':
+      return t('File based caching will usually offer better performance, however, some find it difficult to enable and it may not offer valid statistics if you are using multiple load balanced web servers.');
+    case 'settings':
+      $form = array();
+      $form['cache']['file'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('File cache settings'),
+        '#collapsible' => TRUE,
+        '#collapsed' => (variable_get('ad_cache', 'none') == 'file') ? FALSE : TRUE,
+      );
+      $form['cache']['file']['ad_files'] = array(
+        '#type' => 'select', 
+        '#title' => t('Number of cache files'), 
+        '#default_value' => variable_get('ad_files', 3), 
+        '#options' => drupal_map_assoc(array(1, 3, 5, 10, 15)), 
+        '#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.  This configuration option is only relevant if the file cache is enabled.')
+      );
+      $period = drupal_map_assoc(array(15,30,60,600,1800,3600,21600,43200,86400), 'format_interval');
+      $form['cache']['file']['ad_cache_file_lifetime'] = array(
+        '#type' => 'select', 
+        '#title' => t('Cache lifetime'), 
+        '#default_value' => variable_get('ad_cache_file_lifetime', 60), 
+        '#options' => $period,
+        '#description' => t('Specify how long information should be cached before ad statistics are updated in the database.  Increasing the cache lifetime can improve overall performance.  This configuration options is only relevant if the file cache is enabled.'),
+      );
+      return $form;
+    case 'settings_submit':
+      variable_set('ad_cache_file_lifetime', $node['ad_cache_file_lifetime']);
+      if ($node['ad_cache'] != 'file') {
+        ad_cache_file_build(0, variable_get('ad_files', 3));
+      }
+      else {
+        ad_cache_file_build($node['ad_files'], variable_get('ad_files', 3));
+      }
+      variable_set('ad_files', $node['ad_files']);
+      break;
+
+    case 'insert':
+    case 'update':
+    case 'delete':
+      if (variable_get('ad_cache', 'none') == 'file') {
+        ad_cache_file_build();
+      }
+      break;
+  }
+}
+*/
+