web/drupal/modules/aggregator/aggregator.module
branchdrupal
changeset 74 0ff3ba646492
equal deleted inserted replaced
73:fcf75e232c5b 74:0ff3ba646492
       
     1 <?php
       
     2 // $Id: aggregator.module,v 1.374.2.5 2009/03/30 12:15:53 goba Exp $
       
     3 
       
     4 /**
       
     5  * @file
       
     6  * Used to aggregate syndicated content (RSS, RDF, and Atom).
       
     7  */
       
     8 
       
     9 /**
       
    10  * Implementation of hook_help().
       
    11  */
       
    12 function aggregator_help($path, $arg) {
       
    13   switch ($path) {
       
    14     case 'admin/help#aggregator':
       
    15       $output = '<p>'. t('The aggregator is a powerful on-site syndicator and news reader that gathers fresh content from RSS-, RDF-, and Atom-based feeds made available across the web. Thousands of sites (particularly news sites and blogs) publish their latest headlines and posts in feeds, using a number of standardized XML-based formats. Formats supported by the aggregator include <a href="@rss">RSS</a>, <a href="@rdf">RDF</a>, and <a href="@atom">Atom</a>.', array('@rss' => 'http://cyber.law.harvard.edu/rss/', '@rdf' => 'http://www.w3.org/RDF/', '@atom' => 'http://www.atomenabled.org')) .'</p>';
       
    16       $output .= '<p>'. t('Feeds contain feed items, or individual posts published by the site providing the feed. Feeds may be grouped in categories, generally by topic. Users view feed items in the <a href="@aggregator">main aggregator display</a> or by <a href="@aggregator-sources">their source</a>. Administrators can <a href="@feededit">add, edit and delete feeds</a> and choose how often to check each feed for newly updated items. The most recent items in either a feed or category can be displayed as a block through the <a href="@admin-block">blocks administration page</a>. A <a href="@aggregator-opml">machine-readable OPML file</a> of all feeds is available. A correctly configured <a href="@cron">cron maintenance task</a> is required to update feeds automatically.', array('@aggregator' => url('aggregator'), '@aggregator-sources' => url('aggregator/sources'), '@feededit' => url('admin/content/aggregator'), '@admin-block' => url('admin/build/block'), '@aggregator-opml' => url('aggregator/opml'), '@cron' => url('admin/reports/status'))) .'</p>';
       
    17       $output .= '<p>'. t('For more information, see the online handbook entry for <a href="@aggregator">Aggregator module</a>.', array('@aggregator' => 'http://drupal.org/handbook/modules/aggregator/')) .'</p>';
       
    18       return $output;
       
    19     case 'admin/content/aggregator':
       
    20       $output = '<p>'. t('Thousands of sites (particularly news sites and blogs) publish their latest headlines and posts in feeds, using a number of standardized XML-based formats. Formats supported by the aggregator include <a href="@rss">RSS</a>, <a href="@rdf">RDF</a>, and <a href="@atom">Atom</a>.', array('@rss' => 'http://cyber.law.harvard.edu/rss/', '@rdf' => 'http://www.w3.org/RDF/', '@atom' => 'http://www.atomenabled.org')) .'</p>';
       
    21       $output .= '<p>'. t('Current feeds are listed below, and <a href="@addfeed">new feeds may be added</a>. For each feed or feed category, the <em>latest items</em> block may be enabled at the <a href="@block">blocks administration page</a>.', array('@addfeed' => url('admin/content/aggregator/add/feed'), '@block' => url('admin/build/block'))) .'</p>';
       
    22       return $output;
       
    23     case 'admin/content/aggregator/add/feed':
       
    24       return '<p>'. t('Add a feed in RSS, RDF or Atom format. A feed may only have one entry.') .'</p>';
       
    25     case 'admin/content/aggregator/add/category':
       
    26       return '<p>'. t('Categories allow feed items from different feeds to be grouped together. For example, several sport-related feeds may belong to a category named <em>Sports</em>. Feed items may be grouped automatically (by selecting a category when creating or editing a feed) or manually (via the <em>Categorize</em> page available from feed item listings). Each category provides its own feed page and block.') .'</p>';
       
    27   }
       
    28 }
       
    29 
       
    30 /**
       
    31  * Implementation of hook_theme()
       
    32  */
       
    33 function aggregator_theme() {
       
    34   return array(
       
    35     'aggregator_wrapper' => array(
       
    36       'arguments' => array('content' => NULL),
       
    37       'file' => 'aggregator.pages.inc',
       
    38       'template' => 'aggregator-wrapper',
       
    39     ),
       
    40     'aggregator_categorize_items' => array(
       
    41       'arguments' => array('form' => NULL),
       
    42       'file' => 'aggregator.pages.inc',
       
    43     ),
       
    44     'aggregator_feed_source' => array(
       
    45       'arguments' => array('feed' => NULL),
       
    46       'file' => 'aggregator.pages.inc',
       
    47       'template' => 'aggregator-feed-source',
       
    48     ),
       
    49     'aggregator_block_item' => array(
       
    50       'arguments' => array('item' => NULL, 'feed' => 0),
       
    51     ),
       
    52     'aggregator_summary_items' => array(
       
    53       'arguments' => array('summary_items' => NULL, 'source' => NULL),
       
    54       'file' => 'aggregator.pages.inc',
       
    55       'template' => 'aggregator-summary-items',
       
    56     ),
       
    57     'aggregator_summary_item' => array(
       
    58       'arguments' => array('item' => NULL),
       
    59       'file' => 'aggregator.pages.inc',
       
    60       'template' => 'aggregator-summary-item',
       
    61     ),
       
    62     'aggregator_item' => array(
       
    63       'arguments' => array('item' => NULL),
       
    64       'file' => 'aggregator.pages.inc',
       
    65       'template' => 'aggregator-item',
       
    66     ),
       
    67     'aggregator_page_opml' => array(
       
    68       'arguments' => array('feeds' => NULL),
       
    69       'file' => 'aggregator.pages.inc',
       
    70     ),
       
    71     'aggregator_page_rss' => array(
       
    72       'arguments' => array('feeds' => NULL, 'category' => NULL),
       
    73       'file' => 'aggregator.pages.inc',
       
    74     ),
       
    75   );
       
    76 }
       
    77 
       
    78 /**
       
    79  * Implementation of hook_menu().
       
    80  */
       
    81 function aggregator_menu() {
       
    82   $items['admin/content/aggregator'] = array(
       
    83     'title' => 'Feed aggregator',
       
    84     'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.",
       
    85     'page callback' => 'aggregator_admin_overview',
       
    86     'access arguments' => array('administer news feeds'),
       
    87     'file' => 'aggregator.admin.inc',
       
    88   );
       
    89   $items['admin/content/aggregator/add/feed'] = array(
       
    90     'title' => 'Add feed',
       
    91     'page callback' => 'drupal_get_form',
       
    92     'page arguments' => array('aggregator_form_feed'),
       
    93     'access arguments' => array('administer news feeds'),
       
    94     'type' => MENU_LOCAL_TASK,
       
    95     'parent' => 'admin/content/aggregator',
       
    96     'file' => 'aggregator.admin.inc',
       
    97   );
       
    98   $items['admin/content/aggregator/add/category'] = array(
       
    99     'title' => 'Add category',
       
   100     'page callback' => 'drupal_get_form',
       
   101     'page arguments' => array('aggregator_form_category'),
       
   102     'access arguments' => array('administer news feeds'),
       
   103     'type' => MENU_LOCAL_TASK,
       
   104     'parent' => 'admin/content/aggregator',
       
   105     'file' => 'aggregator.admin.inc',
       
   106   );
       
   107   $items['admin/content/aggregator/remove/%aggregator_feed'] = array(
       
   108     'title' => 'Remove items',
       
   109     'page callback' => 'drupal_get_form',
       
   110     'page arguments' => array('aggregator_admin_remove_feed', 4),
       
   111     'access arguments' => array('administer news feeds'),
       
   112     'type' => MENU_CALLBACK,
       
   113     'file' => 'aggregator.admin.inc',
       
   114   );
       
   115   $items['admin/content/aggregator/update/%aggregator_feed'] = array(
       
   116     'title' => 'Update items',
       
   117     'page callback' => 'aggregator_admin_refresh_feed',
       
   118     'page arguments' => array(4),
       
   119     'access arguments' => array('administer news feeds'),
       
   120     'type' => MENU_CALLBACK,
       
   121     'file' => 'aggregator.admin.inc',
       
   122   );
       
   123   $items['admin/content/aggregator/list'] = array(
       
   124     'title' => 'List',
       
   125     'type' => MENU_DEFAULT_LOCAL_TASK,
       
   126     'weight' => -10,
       
   127   );
       
   128   $items['admin/content/aggregator/settings'] = array(
       
   129     'title' => 'Settings',
       
   130     'page callback' => 'drupal_get_form',
       
   131     'page arguments' => array('aggregator_admin_settings'),
       
   132     'type' => MENU_LOCAL_TASK,
       
   133     'weight' => 10,
       
   134     'access arguments' => array('administer news feeds'),
       
   135     'file' => 'aggregator.admin.inc',
       
   136   );
       
   137   $items['aggregator'] = array(
       
   138     'title' => 'Feed aggregator',
       
   139     'page callback' => 'aggregator_page_last',
       
   140     'access arguments' => array('access news feeds'),
       
   141     'weight' => 5,
       
   142     'file' => 'aggregator.pages.inc',
       
   143   );
       
   144   $items['aggregator/sources'] = array(
       
   145     'title' => 'Sources',
       
   146     'page callback' => 'aggregator_page_sources',
       
   147     'access arguments' => array('access news feeds'),
       
   148     'file' => 'aggregator.pages.inc',
       
   149   );
       
   150   $items['aggregator/categories'] = array(
       
   151     'title' => 'Categories',
       
   152     'page callback' => 'aggregator_page_categories',
       
   153     'access callback' => '_aggregator_has_categories',
       
   154     'file' => 'aggregator.pages.inc',
       
   155   );
       
   156   $items['aggregator/rss'] = array(
       
   157     'title' => 'RSS feed',
       
   158     'page callback' => 'aggregator_page_rss',
       
   159     'access arguments' => array('access news feeds'),
       
   160     'type' => MENU_CALLBACK,
       
   161     'file' => 'aggregator.pages.inc',
       
   162   );
       
   163   $items['aggregator/opml'] = array(
       
   164     'title' => 'OPML feed',
       
   165     'page callback' => 'aggregator_page_opml',
       
   166     'access arguments' => array('access news feeds'),
       
   167     'type' => MENU_CALLBACK,
       
   168     'file' => 'aggregator.pages.inc',
       
   169   );
       
   170   $items['aggregator/categories/%aggregator_category'] = array(
       
   171     'title callback' => '_aggregator_category_title',
       
   172     'title arguments' => array(2),
       
   173     'page callback' => 'aggregator_page_category',
       
   174     'page arguments' => array(2),
       
   175     'access callback' => 'user_access',
       
   176     'access arguments' => array('access news feeds'),
       
   177     'file' => 'aggregator.pages.inc',
       
   178   );
       
   179   $items['aggregator/categories/%aggregator_category/view'] = array(
       
   180     'title' => 'View',
       
   181     'type' => MENU_DEFAULT_LOCAL_TASK,
       
   182     'weight' => -10,
       
   183   );
       
   184   $items['aggregator/categories/%aggregator_category/categorize'] = array(
       
   185     'title' => 'Categorize',
       
   186     'page callback' => 'drupal_get_form',
       
   187     'page arguments' => array('aggregator_page_category', 2),
       
   188     'access arguments' => array('administer news feeds'),
       
   189     'type' => MENU_LOCAL_TASK,
       
   190     'file' => 'aggregator.pages.inc',
       
   191   );
       
   192   $items['aggregator/categories/%aggregator_category/configure'] = array(
       
   193     'title' => 'Configure',
       
   194     'page callback' => 'drupal_get_form',
       
   195     'page arguments' => array('aggregator_form_category', 2),
       
   196     'access arguments' => array('administer news feeds'),
       
   197     'type' => MENU_LOCAL_TASK,
       
   198     'weight' => 1,
       
   199     'file' => 'aggregator.admin.inc',
       
   200   );
       
   201   $items['aggregator/sources/%aggregator_feed'] = array(
       
   202     'page callback' => 'aggregator_page_source',
       
   203     'page arguments' => array(2),
       
   204     'access arguments' => array('access news feeds'),
       
   205     'type' => MENU_CALLBACK,
       
   206     'file' => 'aggregator.pages.inc',
       
   207   );
       
   208   $items['aggregator/sources/%aggregator_feed/view'] = array(
       
   209     'title' => 'View',
       
   210     'type' => MENU_DEFAULT_LOCAL_TASK,
       
   211     'weight' => -10,
       
   212   );
       
   213   $items['aggregator/sources/%aggregator_feed/categorize'] = array(
       
   214     'title' => 'Categorize',
       
   215     'page callback' => 'drupal_get_form',
       
   216     'page arguments' => array('aggregator_page_source', 2),
       
   217     'access arguments' => array('administer news feeds'),
       
   218     'type' => MENU_LOCAL_TASK,
       
   219     'file' => 'aggregator.pages.inc',
       
   220   );
       
   221   $items['aggregator/sources/%aggregator_feed/configure'] = array(
       
   222     'title' => 'Configure',
       
   223     'page callback' => 'drupal_get_form',
       
   224     'page arguments' => array('aggregator_form_feed', 2),
       
   225     'access arguments' => array('administer news feeds'),
       
   226     'type' => MENU_LOCAL_TASK,
       
   227     'weight' => 1,
       
   228     'file' => 'aggregator.admin.inc',
       
   229   );
       
   230   $items['admin/content/aggregator/edit/feed/%aggregator_feed'] = array(
       
   231     'title' => 'Edit feed',
       
   232     'page callback' => 'drupal_get_form',
       
   233     'page arguments' => array('aggregator_form_feed', 5),
       
   234     'access arguments' => array('administer news feeds'),
       
   235     'type' => MENU_CALLBACK,
       
   236     'file' => 'aggregator.admin.inc',
       
   237   );
       
   238   $items['admin/content/aggregator/edit/category/%aggregator_category'] = array(
       
   239     'title' => 'Edit category',
       
   240     'page callback' => 'drupal_get_form',
       
   241     'page arguments' => array('aggregator_form_category', 5),
       
   242     'access arguments' => array('administer news feeds'),
       
   243     'type' => MENU_CALLBACK,
       
   244     'file' => 'aggregator.admin.inc',
       
   245   );
       
   246 
       
   247   return $items;
       
   248 }
       
   249 
       
   250 /**
       
   251  * Menu callback.
       
   252  *
       
   253  * @return
       
   254  *   An aggregator category title.
       
   255  */
       
   256 function _aggregator_category_title($category) {
       
   257   return $category['title'];
       
   258 }
       
   259 
       
   260 /**
       
   261  * Implementation of hook_init().
       
   262  */
       
   263 function aggregator_init() {
       
   264   drupal_add_css(drupal_get_path('module', 'aggregator') .'/aggregator.css');
       
   265 }
       
   266 
       
   267 /**
       
   268  * Find out whether there are any aggregator categories.
       
   269  *
       
   270  * @return
       
   271  *   TRUE if there is at least one category and the user has access to them, FALSE otherwise.
       
   272  */
       
   273 function _aggregator_has_categories() {
       
   274   return user_access('access news feeds') && db_result(db_query('SELECT COUNT(*) FROM {aggregator_category}'));
       
   275 }
       
   276 
       
   277 /**
       
   278  * Implementation of hook_perm().
       
   279  */
       
   280 function aggregator_perm() {
       
   281   return array('administer news feeds', 'access news feeds');
       
   282 }
       
   283 
       
   284 /**
       
   285  * Implementation of hook_cron().
       
   286  *
       
   287  * Checks news feeds for updates once their refresh interval has elapsed.
       
   288  */
       
   289 function aggregator_cron() {
       
   290   $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < %d', time());
       
   291   while ($feed = db_fetch_array($result)) {
       
   292     aggregator_refresh($feed);
       
   293   }
       
   294 }
       
   295 
       
   296 /**
       
   297  * Implementation of hook_block().
       
   298  *
       
   299  * Generates blocks for the latest news items in each category and feed.
       
   300  */
       
   301 function aggregator_block($op = 'list', $delta = 0, $edit = array()) {
       
   302   if (user_access('access news feeds')) {
       
   303     if ($op == 'list') {
       
   304       $result = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title');
       
   305       while ($category = db_fetch_object($result)) {
       
   306         $block['category-'. $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title));
       
   307       }
       
   308       $result = db_query('SELECT fid, title FROM {aggregator_feed} ORDER BY fid');
       
   309       while ($feed = db_fetch_object($result)) {
       
   310         $block['feed-'. $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
       
   311       }
       
   312     }
       
   313     else if ($op == 'configure') {
       
   314       list($type, $id) = explode('-', $delta);
       
   315       if ($type == 'category') {
       
   316         $value = db_result(db_query('SELECT block FROM {aggregator_category} WHERE cid = %d', $id));
       
   317       }
       
   318       else {
       
   319         $value = db_result(db_query('SELECT block FROM {aggregator_feed} WHERE fid = %d', $id));
       
   320       }
       
   321       $form['block'] = array('#type' => 'select', '#title' => t('Number of news items in block'), '#default_value' => $value, '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
       
   322       return $form;
       
   323     }
       
   324     else if ($op == 'save') {
       
   325       list($type, $id) = explode('-', $delta);
       
   326       if ($type == 'category') {
       
   327         $value = db_query('UPDATE {aggregator_category} SET block = %d WHERE cid = %d', $edit['block'], $id);
       
   328       }
       
   329       else {
       
   330         $value = db_query('UPDATE {aggregator_feed} SET block = %d WHERE fid = %d', $edit['block'], $id);
       
   331       }
       
   332     }
       
   333     else if ($op == 'view') {
       
   334       list($type, $id) = explode('-', $delta);
       
   335       switch ($type) {
       
   336         case 'feed':
       
   337           if ($feed = db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id))) {
       
   338             $block['subject'] = check_plain($feed->title);
       
   339             $result = db_query_range('SELECT * FROM {aggregator_item} WHERE fid = %d ORDER BY timestamp DESC, iid DESC', $feed->fid, 0, $feed->block);
       
   340             $read_more = theme('more_link', url('aggregator/sources/'. $feed->fid), t("View this feed's recent news."));
       
   341           }
       
   342           break;
       
   343 
       
   344         case 'category':
       
   345           if ($category = db_fetch_object(db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = %d', $id))) {
       
   346             $block['subject'] = check_plain($category->title);
       
   347             $result = db_query_range('SELECT i.* FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON ci.iid = i.iid WHERE ci.cid = %d ORDER BY i.timestamp DESC, i.iid DESC', $category->cid, 0, $category->block);
       
   348             $read_more = theme('more_link', url('aggregator/categories/'. $category->cid), t("View this category's recent news."));
       
   349           }
       
   350           break;
       
   351       }
       
   352       $items = array();
       
   353       while ($item = db_fetch_object($result)) {
       
   354         $items[] = theme('aggregator_block_item', $item);
       
   355       }
       
   356 
       
   357       // Only display the block if there are items to show.
       
   358       if (count($items) > 0) {
       
   359         $block['content'] = theme('item_list', $items) . $read_more;
       
   360       }
       
   361     }
       
   362     if (isset($block)) {
       
   363       return $block;
       
   364     }
       
   365   }
       
   366 }
       
   367 
       
   368 /**
       
   369  * Add/edit/delete aggregator categories.
       
   370  *
       
   371  * @param $edit
       
   372  *   An associative array describing the category to be added/edited/deleted.
       
   373  */
       
   374 function aggregator_save_category($edit) {
       
   375   $link_path = 'aggregator/categories/';
       
   376   if (!empty($edit['cid'])) {
       
   377     $link_path .= $edit['cid'];
       
   378     if (!empty($edit['title'])) {
       
   379       db_query("UPDATE {aggregator_category} SET title = '%s', description = '%s' WHERE cid = %d", $edit['title'], $edit['description'], $edit['cid']);
       
   380       $op = 'update';
       
   381     }
       
   382     else {
       
   383       db_query('DELETE FROM {aggregator_category} WHERE cid = %d', $edit['cid']);
       
   384       // Make sure there is no active block for this category.
       
   385       db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'aggregator', 'category-' . $edit['cid']);
       
   386       $edit['title'] = '';
       
   387       $op = 'delete';
       
   388     }
       
   389   }
       
   390   else if (!empty($edit['title'])) {
       
   391     // A single unique id for bundles and feeds, to use in blocks
       
   392     db_query("INSERT INTO {aggregator_category} (title, description, block) VALUES ('%s', '%s', 5)", $edit['title'], $edit['description']);
       
   393     $link_path .= db_last_insert_id('aggregator_category', 'cid');
       
   394     $op = 'insert';
       
   395   }
       
   396   if (isset($op)) {
       
   397     menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
       
   398   }
       
   399 }
       
   400 
       
   401 /**
       
   402  * Add/edit/delete an aggregator feed.
       
   403  *
       
   404  * @param $edit
       
   405  *   An associative array describing the feed to be added/edited/deleted.
       
   406  */
       
   407 function aggregator_save_feed($edit) {
       
   408   if (!empty($edit['fid'])) {
       
   409     // An existing feed is being modified, delete the category listings.
       
   410     db_query('DELETE FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
       
   411   }
       
   412   if (!empty($edit['fid']) && !empty($edit['title'])) {
       
   413     db_query("UPDATE {aggregator_feed} SET title = '%s', url = '%s', refresh = %d WHERE fid = %d", $edit['title'], $edit['url'], $edit['refresh'], $edit['fid']);
       
   414   }
       
   415   else if (!empty($edit['fid'])) {
       
   416     $items = array();
       
   417     $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
       
   418     while ($item = db_fetch_object($result)) {
       
   419       $items[] = "iid = $item->iid";
       
   420     }
       
   421     if (!empty($items)) {
       
   422       db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
       
   423     }
       
   424     db_query('DELETE FROM {aggregator_feed} WHERE fid = %d', $edit['fid']);
       
   425     db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
       
   426     // Make sure there is no active block for this feed.
       
   427     db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'aggregator', 'feed-' . $edit['fid']);
       
   428   }
       
   429   else if (!empty($edit['title'])) {
       
   430     db_query("INSERT INTO {aggregator_feed} (title, url, refresh, block, description, image) VALUES ('%s', '%s', %d, 5, '', '')", $edit['title'], $edit['url'], $edit['refresh']);
       
   431     // A single unique id for bundles and feeds, to use in blocks.
       
   432     $edit['fid'] = db_last_insert_id('aggregator_feed', 'fid');
       
   433   }
       
   434   if (!empty($edit['title'])) {
       
   435     // The feed is being saved, save the categories as well.
       
   436     if (!empty($edit['category'])) {
       
   437       foreach ($edit['category'] as $cid => $value) {
       
   438         if ($value) {
       
   439           db_query('INSERT INTO {aggregator_category_feed} (fid, cid) VALUES (%d, %d)', $edit['fid'], $cid);
       
   440         }
       
   441       }
       
   442     }
       
   443   }
       
   444 }
       
   445 
       
   446 /**
       
   447  * Removes all items from a feed.
       
   448  *
       
   449  * @param $feed
       
   450  *   An associative array describing the feed to be cleared.
       
   451  */
       
   452 function aggregator_remove($feed) {
       
   453   $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
       
   454   while ($item = db_fetch_object($result)) {
       
   455     $items[] = "iid = $item->iid";
       
   456   }
       
   457   if (!empty($items)) {
       
   458     db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
       
   459   }
       
   460   db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
       
   461   db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE fid = %d", $feed['fid']);
       
   462   drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed['title'])));
       
   463 }
       
   464 
       
   465 /**
       
   466  * Call-back function used by the XML parser.
       
   467  */
       
   468 function aggregator_element_start($parser, $name, $attributes) {
       
   469   global $item, $element, $tag, $items, $channel;
       
   470 
       
   471   switch ($name) {
       
   472     case 'IMAGE':
       
   473     case 'TEXTINPUT':
       
   474     case 'CONTENT':
       
   475     case 'SUMMARY':
       
   476     case 'TAGLINE':
       
   477     case 'SUBTITLE':
       
   478     case 'LOGO':
       
   479     case 'INFO':
       
   480       $element = $name;
       
   481       break;
       
   482     case 'ID':
       
   483       if ($element != 'ITEM') {
       
   484         $element = $name;
       
   485       }
       
   486     case 'LINK':
       
   487       if (!empty($attributes['REL']) && $attributes['REL'] == 'alternate') {
       
   488         if ($element == 'ITEM') {
       
   489           $items[$item]['LINK'] = $attributes['HREF'];
       
   490         }
       
   491         else {
       
   492           $channel['LINK'] = $attributes['HREF'];
       
   493         }
       
   494       }
       
   495       break;
       
   496     case 'ITEM':
       
   497       $element = $name;
       
   498       $item += 1;
       
   499       break;
       
   500     case 'ENTRY':
       
   501       $element = 'ITEM';
       
   502       $item += 1;
       
   503       break;
       
   504   }
       
   505 
       
   506   $tag = $name;
       
   507 }
       
   508 
       
   509 /**
       
   510  * Call-back function used by the XML parser.
       
   511  */
       
   512 function aggregator_element_end($parser, $name) {
       
   513   global $element;
       
   514 
       
   515   switch ($name) {
       
   516     case 'IMAGE':
       
   517     case 'TEXTINPUT':
       
   518     case 'ITEM':
       
   519     case 'ENTRY':
       
   520     case 'CONTENT':
       
   521     case 'INFO':
       
   522       $element = '';
       
   523       break;
       
   524     case 'ID':
       
   525       if ($element == 'ID') {
       
   526         $element = '';
       
   527       }
       
   528   }
       
   529 }
       
   530 
       
   531 /**
       
   532  * Call-back function used by the XML parser.
       
   533  */
       
   534 function aggregator_element_data($parser, $data) {
       
   535   global $channel, $element, $items, $item, $image, $tag;
       
   536   $items += array($item => array());
       
   537   switch ($element) {
       
   538     case 'ITEM':
       
   539       $items[$item] += array($tag => '');
       
   540       $items[$item][$tag] .= $data;
       
   541       break;
       
   542     case 'IMAGE':
       
   543     case 'LOGO':
       
   544       $image += array($tag => '');
       
   545       $image[$tag] .= $data;
       
   546       break;
       
   547     case 'LINK':
       
   548       if ($data) {
       
   549         $items[$item] += array($tag => '');
       
   550         $items[$item][$tag] .= $data;
       
   551       }
       
   552       break;
       
   553     case 'CONTENT':
       
   554       $items[$item] += array('CONTENT' => '');
       
   555       $items[$item]['CONTENT'] .= $data;
       
   556       break;
       
   557     case 'SUMMARY':
       
   558       $items[$item] += array('SUMMARY' => '');
       
   559       $items[$item]['SUMMARY'] .= $data;
       
   560       break;
       
   561     case 'TAGLINE':
       
   562     case 'SUBTITLE':
       
   563       $channel += array('DESCRIPTION' => '');
       
   564       $channel['DESCRIPTION'] .= $data;
       
   565       break;
       
   566     case 'INFO':
       
   567     case 'ID':
       
   568     case 'TEXTINPUT':
       
   569       // The sub-element is not supported. However, we must recognize
       
   570       // it or its contents will end up in the item array.
       
   571       break;
       
   572     default:
       
   573       $channel += array($tag => '');
       
   574       $channel[$tag] .= $data;
       
   575   }
       
   576 }
       
   577 
       
   578 /**
       
   579  * Checks a news feed for new items.
       
   580  *
       
   581  * @param $feed
       
   582  *   An associative array describing the feed to be refreshed.
       
   583  */
       
   584 function aggregator_refresh($feed) {
       
   585   global $channel, $image;
       
   586 
       
   587   // Generate conditional GET headers.
       
   588   $headers = array();
       
   589   if ($feed['etag']) {
       
   590     $headers['If-None-Match'] = $feed['etag'];
       
   591   }
       
   592   if ($feed['modified']) {
       
   593     $headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s', $feed['modified']) .' GMT';
       
   594   }
       
   595 
       
   596   // Request feed.
       
   597   $result = drupal_http_request($feed['url'], $headers);
       
   598 
       
   599   // Process HTTP response code.
       
   600   switch ($result->code) {
       
   601     case 304:
       
   602       db_query('UPDATE {aggregator_feed} SET checked = %d WHERE fid = %d', time(), $feed['fid']);
       
   603       drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed['title'])));
       
   604       break;
       
   605     case 301:
       
   606       $feed['url'] = $result->redirect_url;
       
   607       watchdog('aggregator', 'Updated URL for feed %title to %url.', array('%title' => $feed['title'], '%url' => $feed['url']));
       
   608       // Deliberate no break.
       
   609     case 200:
       
   610     case 302:
       
   611     case 307:
       
   612       // Filter the input data:
       
   613       if (aggregator_parse_feed($result->data, $feed)) {
       
   614         $modified = empty($result->headers['Last-Modified']) ? 0 : strtotime($result->headers['Last-Modified']);
       
   615 
       
   616         // Prepare the channel data.
       
   617         foreach ($channel as $key => $value) {
       
   618           $channel[$key] = trim($value);
       
   619         }
       
   620 
       
   621         // Prepare the image data (if any).
       
   622         foreach ($image as $key => $value) {
       
   623           $image[$key] = trim($value);
       
   624         }
       
   625 
       
   626         if (!empty($image['LINK']) && !empty($image['URL']) && !empty($image['TITLE'])) {
       
   627           // Note, we should really use theme_image() here but that only works with local images it won't work with images fetched with a URL unless PHP version > 5
       
   628           $image = '<a href="'. check_url($image['LINK']) .'" class="feed-image"><img src="'. check_url($image['URL']) .'" alt="'. check_plain($image['TITLE']) .'" /></a>';
       
   629         }
       
   630         else {
       
   631           $image = NULL;
       
   632         }
       
   633 
       
   634         $etag = empty($result->headers['ETag']) ? '' : $result->headers['ETag'];
       
   635         // Update the feed data.
       
   636         db_query("UPDATE {aggregator_feed} SET url = '%s', checked = %d, link = '%s', description = '%s', image = '%s', etag = '%s', modified = %d WHERE fid = %d", $feed['url'], time(), $channel['LINK'], $channel['DESCRIPTION'], $image, $etag, $modified, $feed['fid']);
       
   637 
       
   638         // Clear the cache.
       
   639         cache_clear_all();
       
   640 
       
   641         watchdog('aggregator', 'There is new syndicated content from %site.', array('%site' => $feed['title']));
       
   642         drupal_set_message(t('There is new syndicated content from %site.', array('%site' => $feed['title'])));
       
   643         break;
       
   644       }
       
   645       $result->error = t('feed not parseable');
       
   646       // Deliberate no break.
       
   647     default:
       
   648       watchdog('aggregator', 'The feed from %site seems to be broken, due to "%error".', array('%site' => $feed['title'], '%error' => $result->code .' '. $result->error), WATCHDOG_WARNING);
       
   649       drupal_set_message(t('The feed from %site seems to be broken, because of error "%error".', array('%site' => $feed['title'], '%error' => $result->code .' '. $result->error)));
       
   650   }
       
   651 }
       
   652 
       
   653 /**
       
   654  * Parse the W3C date/time format, a subset of ISO 8601. PHP date parsing
       
   655  * functions do not handle this format.
       
   656  * See http://www.w3.org/TR/NOTE-datetime for more information.
       
   657  * Originally from MagpieRSS (http://magpierss.sourceforge.net/).
       
   658  *
       
   659  * @param $date_str
       
   660  *   A string with a potentially W3C DTF date.
       
   661  * @return
       
   662  *   A timestamp if parsed successfully or FALSE if not.
       
   663  */
       
   664 function aggregator_parse_w3cdtf($date_str) {
       
   665   if (preg_match('/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/', $date_str, $match)) {
       
   666     list($year, $month, $day, $hours, $minutes, $seconds) = array($match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
       
   667     // calc epoch for current date assuming GMT
       
   668     $epoch = gmmktime($hours, $minutes, $seconds, $month, $day, $year);
       
   669     if ($match[10] != 'Z') { // Z is zulu time, aka GMT
       
   670       list($tz_mod, $tz_hour, $tz_min) = array($match[8], $match[9], $match[10]);
       
   671       // zero out the variables
       
   672       if (!$tz_hour) {
       
   673         $tz_hour = 0;
       
   674       }
       
   675       if (!$tz_min) {
       
   676         $tz_min = 0;
       
   677       }
       
   678       $offset_secs = (($tz_hour * 60) + $tz_min) * 60;
       
   679       // is timezone ahead of GMT?  then subtract offset
       
   680       if ($tz_mod == '+') {
       
   681         $offset_secs *= -1;
       
   682       }
       
   683       $epoch += $offset_secs;
       
   684     }
       
   685     return $epoch;
       
   686   }
       
   687   else {
       
   688     return FALSE;
       
   689   }
       
   690 }
       
   691 
       
   692 /**
       
   693  * Parse a feed and store its items.
       
   694  *
       
   695  * @param $data
       
   696  *   The feed data.
       
   697  * @param $feed
       
   698  *   An associative array describing the feed to be parsed.
       
   699  * @return
       
   700  *   0 on error, 1 otherwise.
       
   701  */
       
   702 function aggregator_parse_feed(&$data, $feed) {
       
   703   global $items, $image, $channel;
       
   704 
       
   705   // Unset the global variables before we use them:
       
   706   unset($GLOBALS['element'], $GLOBALS['item'], $GLOBALS['tag']);
       
   707   $items = array();
       
   708   $image = array();
       
   709   $channel = array();
       
   710 
       
   711   // parse the data:
       
   712   $xml_parser = drupal_xml_parser_create($data);
       
   713   xml_set_element_handler($xml_parser, 'aggregator_element_start', 'aggregator_element_end');
       
   714   xml_set_character_data_handler($xml_parser, 'aggregator_element_data');
       
   715 
       
   716   if (!xml_parse($xml_parser, $data, 1)) {
       
   717     watchdog('aggregator', 'The feed from %site seems to be broken, due to an error "%error" on line %line.', array('%site' => $feed['title'], '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser)), WATCHDOG_WARNING);
       
   718     drupal_set_message(t('The feed from %site seems to be broken, because of error "%error" on line %line.', array('%site' => $feed['title'], '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser))), 'error');
       
   719     return 0;
       
   720   }
       
   721   xml_parser_free($xml_parser);
       
   722 
       
   723   // We reverse the array such that we store the first item last, and the last
       
   724   // item first. In the database, the newest item should be at the top.
       
   725   $items = array_reverse($items);
       
   726 
       
   727   // Initialize variables.
       
   728   $title = $link = $author = $description = $guid = NULL;
       
   729   foreach ($items as $item) {
       
   730     unset($title, $link, $author, $description, $guid);
       
   731 
       
   732     // Prepare the item:
       
   733     foreach ($item as $key => $value) {
       
   734       $item[$key] = trim($value);
       
   735     }
       
   736 
       
   737     // Resolve the item's title. If no title is found, we use up to 40
       
   738     // characters of the description ending at a word boundary but not
       
   739     // splitting potential entities.
       
   740     if (!empty($item['TITLE'])) {
       
   741       $title = $item['TITLE'];
       
   742     }
       
   743     elseif (!empty($item['DESCRIPTION'])) {
       
   744       $title = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", truncate_utf8($item['DESCRIPTION'], 40));
       
   745     }
       
   746     else {
       
   747       $title = '';
       
   748     }
       
   749 
       
   750     // Resolve the items link.
       
   751     if (!empty($item['LINK'])) {
       
   752       $link = $item['LINK'];
       
   753     }
       
   754     else {
       
   755       $link = $feed['link'];
       
   756     }
       
   757     $guid = isset($item['GUID']) ? $item['GUID'] : '';
       
   758 
       
   759     // Atom feeds have a CONTENT and/or SUMMARY tag instead of a DESCRIPTION tag.
       
   760     if (!empty($item['CONTENT:ENCODED'])) {
       
   761       $item['DESCRIPTION'] = $item['CONTENT:ENCODED'];
       
   762     }
       
   763     else if (!empty($item['SUMMARY'])) {
       
   764       $item['DESCRIPTION'] = $item['SUMMARY'];
       
   765     }
       
   766     else if (!empty($item['CONTENT'])) {
       
   767       $item['DESCRIPTION'] = $item['CONTENT'];
       
   768     }
       
   769 
       
   770     // Try to resolve and parse the item's publication date. If no date is
       
   771     // found, we use the current date instead.
       
   772     $date = 'now';
       
   773     foreach (array('PUBDATE', 'DC:DATE', 'DCTERMS:ISSUED', 'DCTERMS:CREATED', 'DCTERMS:MODIFIED', 'ISSUED', 'CREATED', 'MODIFIED', 'PUBLISHED', 'UPDATED') as $key) {
       
   774       if (!empty($item[$key])) {
       
   775         $date = $item[$key];
       
   776         break;
       
   777       }
       
   778     }
       
   779 
       
   780     $timestamp = strtotime($date); // As of PHP 5.1.0, strtotime returns FALSE on failure instead of -1.
       
   781     if ($timestamp <= 0) {
       
   782       $timestamp = aggregator_parse_w3cdtf($date); // Returns FALSE on failure
       
   783       if (!$timestamp) {
       
   784         $timestamp = time(); // better than nothing
       
   785       }
       
   786     }
       
   787 
       
   788     // Save this item. Try to avoid duplicate entries as much as possible. If
       
   789     // we find a duplicate entry, we resolve it and pass along its ID is such
       
   790     // that we can update it if needed.
       
   791     if (!empty($guid)) {
       
   792       $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND guid = '%s'", $feed['fid'], $guid));
       
   793     }
       
   794     else if ($link && $link != $feed['link'] && $link != $feed['url']) {
       
   795       $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND link = '%s'", $feed['fid'], $link));
       
   796     }
       
   797     else {
       
   798       $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND title = '%s'", $feed['fid'], $title));
       
   799     }
       
   800     $item += array('AUTHOR' => '', 'DESCRIPTION' => '');
       
   801     aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid:  ''), 'fid' => $feed['fid'], 'timestamp' => $timestamp, 'title' => $title, 'link' => $link, 'author' => $item['AUTHOR'], 'description' => $item['DESCRIPTION'], 'guid' => $guid));
       
   802   }
       
   803 
       
   804   // Remove all items that are older than flush item timer.
       
   805   $age = time() - variable_get('aggregator_clear', 9676800);
       
   806   $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
       
   807 
       
   808   $items = array();
       
   809   $num_rows = FALSE;
       
   810   while ($item = db_fetch_object($result)) {
       
   811     $items[] = $item->iid;
       
   812     $num_rows = TRUE;
       
   813   }
       
   814   if ($num_rows) {
       
   815     db_query('DELETE FROM {aggregator_category_item} WHERE iid IN ('. implode(', ', $items) .')');
       
   816     db_query('DELETE FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
       
   817   }
       
   818 
       
   819   return 1;
       
   820 }
       
   821 
       
   822 /**
       
   823  * Add/edit/delete an aggregator item.
       
   824  *
       
   825  * @param $edit
       
   826  *   An associative array describing the item to be added/edited/deleted.
       
   827  */
       
   828 function aggregator_save_item($edit) {
       
   829   if ($edit['iid'] && $edit['title']) {
       
   830     db_query("UPDATE {aggregator_item} SET title = '%s', link = '%s', author = '%s', description = '%s', guid = '%s', timestamp = %d WHERE iid = %d", $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['guid'], $edit['timestamp'], $edit['iid']);
       
   831   }
       
   832   else if ($edit['iid']) {
       
   833     db_query('DELETE FROM {aggregator_item} WHERE iid = %d', $edit['iid']);
       
   834     db_query('DELETE FROM {aggregator_category_item} WHERE iid = %d', $edit['iid']);
       
   835   }
       
   836   else if ($edit['title'] && $edit['link']) {
       
   837     db_query("INSERT INTO {aggregator_item} (fid, title, link, author, description, timestamp, guid) VALUES (%d, '%s', '%s', '%s', '%s', %d, '%s')", $edit['fid'], $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['timestamp'], $edit['guid']);
       
   838     $edit['iid'] = db_last_insert_id('aggregator_item', 'iid');
       
   839     // file the items in the categories indicated by the feed
       
   840     $categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
       
   841     while ($category = db_fetch_object($categories)) {
       
   842       db_query('INSERT INTO {aggregator_category_item} (cid, iid) VALUES (%d, %d)', $category->cid, $edit['iid']);
       
   843     }
       
   844   }
       
   845 }
       
   846 
       
   847 /**
       
   848  * Load an aggregator feed.
       
   849  *
       
   850  * @param $fid
       
   851  *   The feed id.
       
   852  * @return
       
   853  *   An associative array describing the feed.
       
   854  */
       
   855 function aggregator_feed_load($fid) {
       
   856   static $feeds;
       
   857   if (!isset($feeds[$fid])) {
       
   858     $feeds[$fid] = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', $fid));
       
   859   }
       
   860   return $feeds[$fid];
       
   861 }
       
   862 
       
   863 /**
       
   864  * Load an aggregator category.
       
   865  *
       
   866  * @param $cid
       
   867  *   The category id.
       
   868  * @return
       
   869  *   An associative array describing the category.
       
   870  */
       
   871 function aggregator_category_load($cid) {
       
   872   static $categories;
       
   873   if (!isset($categories[$cid])) {
       
   874     $categories[$cid] = db_fetch_array(db_query('SELECT * FROM {aggregator_category} WHERE cid = %d', $cid));
       
   875   }
       
   876   return $categories[$cid];
       
   877 }
       
   878 
       
   879 /**
       
   880  * Format an individual feed item for display in the block.
       
   881  *
       
   882  * @param $item
       
   883  *   The item to be displayed.
       
   884  * @param $feed
       
   885  *   Not used.
       
   886  * @return
       
   887  *   The item HTML.
       
   888  * @ingroup themeable
       
   889  */
       
   890 function theme_aggregator_block_item($item, $feed = 0) {
       
   891   global $user;
       
   892 
       
   893   $output = '';
       
   894   if ($user->uid && module_exists('blog') && user_access('create blog entries')) {
       
   895     if ($image = theme('image', 'misc/blog.png', t('blog it'), t('blog it'))) {
       
   896       $output .= '<div class="icon">'. l($image, 'node/add/blog', array('attributes' => array('title' => t('Comment on this news item in your personal blog.'), 'class' => 'blog-it'), 'query' => "iid=$item->iid", 'html' => TRUE)) .'</div>';
       
   897     }
       
   898   }
       
   899 
       
   900   // Display the external link to the item.
       
   901   $output .= '<a href="'. check_url($item->link) .'">'. check_plain($item->title) ."</a>\n";
       
   902 
       
   903   return $output;
       
   904 }
       
   905 
       
   906 /**
       
   907  * Safely render HTML content, as allowed.
       
   908  *
       
   909  * @param $value
       
   910  *   The content to be filtered.
       
   911  * @return
       
   912  *   The filtered content.
       
   913  */
       
   914 function aggregator_filter_xss($value) {
       
   915   return filter_xss($value, preg_split('/\s+|<|>/', variable_get('aggregator_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'), -1, PREG_SPLIT_NO_EMPTY));
       
   916 }
       
   917 
       
   918 /**
       
   919  * Helper function for drupal_map_assoc.
       
   920  *
       
   921  * @param $count
       
   922  *   Items count.
       
   923  * @return
       
   924  *   Plural-formatted "@count items"
       
   925  */
       
   926 function _aggregator_items($count) {
       
   927   return format_plural($count, '1 item', '@count items');
       
   928 }