Code coverage for /20081101/modules/aggregator/aggregator.module

Line #Times calledCode
1
<?php
2
// $Id: aggregator.module,v 1.398 2008/11/01 19:51:06 dries Exp $
3
4
/**
5
 * @file
6
 * Used to aggregate syndicated content (RSS, RDF, and Atom).
7
 */
8
9
/**
10
 * Implementation of hook_help().
11
 */
12126
function aggregator_help($path, $arg) {
13
  switch ($path) {
1478
    case 'admin/help#aggregator':
1555
      $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>';
1655
      $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>';
1755
      $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>';
1855
      return $output;
1977
    case 'admin/content/aggregator':
2015
      $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>';
2115
      $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>';
2215
      return $output;
2362
    case 'admin/content/aggregator/add/feed':
2414
      return '<p>' . t('Add a feed in RSS, RDF or Atom format. A feed may
only have one entry.') . '</p>';
2548
    case 'admin/content/aggregator/add/category':
262
      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>';
2746
    case 'admin/content/aggregator/add/opml':
2812
      return '<p>' . t('<acronym title="Outline Processor Markup
Language">OPML</acronym> is an XML format used to exchange multiple feeds
between aggregators. A single OPML document may contain a collection of
many feeds. Drupal can parse such a file and import all feeds at once,
saving you the effort of adding them manually. You may either upload a
local file from your computer or enter a URL where Drupal can download
it.') . '</p>';
290
  }
3034
}
31
32
/**
33
 * Implementation of hook_theme().
34
 */
35126
function aggregator_theme() {
36
  return array(
37
    'aggregator_wrapper' => array(
3810
      'arguments' => array('content' => NULL),
3910
      'file' => 'aggregator.pages.inc',
4010
      'template' => 'aggregator-wrapper',
4110
    ),
42
    'aggregator_categorize_items' => array(
4310
      'arguments' => array('form' => NULL),
4410
      'file' => 'aggregator.pages.inc',
4510
    ),
46
    'aggregator_feed_source' => array(
4710
      'arguments' => array('feed' => NULL),
4810
      'file' => 'aggregator.pages.inc',
4910
      'template' => 'aggregator-feed-source',
5010
    ),
51
    'aggregator_block_item' => array(
5210
      'arguments' => array('item' => NULL, 'feed' => 0),
5310
    ),
54
    'aggregator_summary_items' => array(
5510
      'arguments' => array('summary_items' => NULL, 'source' => NULL),
5610
      'file' => 'aggregator.pages.inc',
5710
      'template' => 'aggregator-summary-items',
5810
    ),
59
    'aggregator_summary_item' => array(
6010
      'arguments' => array('item' => NULL),
6110
      'file' => 'aggregator.pages.inc',
6210
      'template' => 'aggregator-summary-item',
6310
    ),
64
    'aggregator_item' => array(
6510
      'arguments' => array('item' => NULL),
6610
      'file' => 'aggregator.pages.inc',
6710
      'template' => 'aggregator-item',
6810
    ),
69
    'aggregator_page_opml' => array(
7010
      'arguments' => array('feeds' => NULL),
7110
      'file' => 'aggregator.pages.inc',
7210
    ),
73
    'aggregator_page_rss' => array(
7410
      'arguments' => array('feeds' => NULL, 'category' => NULL),
7510
      'file' => 'aggregator.pages.inc',
7610
    ),
7710
  );
780
}
79
80
/**
81
 * Implementation of hook_menu().
82
 */
83126
function aggregator_menu() {
8411
  $items['admin/content/aggregator'] = array(
8511
    'title' => 'Feed aggregator',
8611
    'description' => "Configure which content your site aggregates from
other sites, how often it polls them, and how they're categorized.",
8711
    'page callback' => 'aggregator_admin_overview',
8811
    'access arguments' => array('administer news feeds'),
89
  );
9011
  $items['admin/content/aggregator/add/feed'] = array(
9111
    'title' => 'Add feed',
9211
    'page callback' => 'drupal_get_form',
9311
    'page arguments' => array('aggregator_form_feed'),
9411
    'access arguments' => array('administer news feeds'),
9511
    'type' => MENU_LOCAL_TASK,
9611
    'parent' => 'admin/content/aggregator',
97
  );
9811
  $items['admin/content/aggregator/add/category'] = array(
9911
    'title' => 'Add category',
10011
    'page callback' => 'drupal_get_form',
10111
    'page arguments' => array('aggregator_form_category'),
10211
    'access arguments' => array('administer news feeds'),
10311
    'type' => MENU_LOCAL_TASK,
10411
    'parent' => 'admin/content/aggregator',
105
  );
10611
  $items['admin/content/aggregator/add/opml'] = array(
10711
    'title' => 'Import OPML',
10811
    'page callback' => 'drupal_get_form',
10911
    'page arguments' => array('aggregator_form_opml'),
11011
    'access arguments' => array('administer news feeds'),
11111
    'type' => MENU_LOCAL_TASK,
11211
    'parent' => 'admin/content/aggregator',
113
  );
11411
  $items['admin/content/aggregator/remove/%aggregator_feed'] = array(
11511
    'title' => 'Remove items',
11611
    'page callback' => 'drupal_get_form',
11711
    'page arguments' => array('aggregator_admin_remove_feed', 4),
11811
    'access arguments' => array('administer news feeds'),
11911
    'type' => MENU_CALLBACK,
120
  );
12111
  $items['admin/content/aggregator/update/%aggregator_feed'] = array(
12211
    'title' => 'Update items',
12311
    'page callback' => 'aggregator_admin_refresh_feed',
12411
    'page arguments' => array(4),
12511
    'access arguments' => array('administer news feeds'),
12611
    'type' => MENU_CALLBACK,
127
  );
12811
  $items['admin/content/aggregator/list'] = array(
12911
    'title' => 'List',
13011
    'type' => MENU_DEFAULT_LOCAL_TASK,
13111
    'weight' => -10,
132
  );
13311
  $items['admin/content/aggregator/settings'] = array(
13411
    'title' => 'Settings',
13511
    'page callback' => 'drupal_get_form',
13611
    'page arguments' => array('aggregator_admin_settings'),
13711
    'type' => MENU_LOCAL_TASK,
13811
    'weight' => 10,
13911
    'access arguments' => array('administer news feeds'),
140
  );
14111
  $items['aggregator'] = array(
14211
    'title' => 'Feed aggregator',
14311
    'page callback' => 'aggregator_page_last',
14411
    'access arguments' => array('access news feeds'),
14511
    'weight' => 5,
146
  );
14711
  $items['aggregator/sources'] = array(
14811
    'title' => 'Sources',
14911
    'page callback' => 'aggregator_page_sources',
15011
    'access arguments' => array('access news feeds'),
151
  );
15211
  $items['aggregator/categories'] = array(
15311
    'title' => 'Categories',
15411
    'page callback' => 'aggregator_page_categories',
15511
    'access callback' => '_aggregator_has_categories',
156
  );
15711
  $items['aggregator/rss'] = array(
15811
    'title' => 'RSS feed',
15911
    'page callback' => 'aggregator_page_rss',
16011
    'access arguments' => array('access news feeds'),
16111
    'type' => MENU_CALLBACK,
162
  );
16311
  $items['aggregator/opml'] = array(
16411
    'title' => 'OPML feed',
16511
    'page callback' => 'aggregator_page_opml',
16611
    'access arguments' => array('access news feeds'),
16711
    'type' => MENU_CALLBACK,
168
  );
16911
  $items['aggregator/categories/%aggregator_category'] = array(
17011
    'title callback' => '_aggregator_category_title',
17111
    'title arguments' => array(2),
17211
    'page callback' => 'aggregator_page_category',
17311
    'page arguments' => array(2),
17411
    'access callback' => 'user_access',
17511
    'access arguments' => array('access news feeds'),
176
  );
17711
  $items['aggregator/categories/%aggregator_category/view'] = array(
17811
    'title' => 'View',
17911
    'type' => MENU_DEFAULT_LOCAL_TASK,
18011
    'weight' => -10,
181
  );
18211
  $items['aggregator/categories/%aggregator_category/categorize'] = array(
18311
    'title' => 'Categorize',
18411
    'page callback' => 'drupal_get_form',
18511
    'page arguments' => array('aggregator_page_category', 2),
18611
    'access arguments' => array('administer news feeds'),
18711
    'type' => MENU_LOCAL_TASK,
188
  );
18911
  $items['aggregator/categories/%aggregator_category/configure'] = array(
19011
    'title' => 'Configure',
19111
    'page callback' => 'drupal_get_form',
19211
    'page arguments' => array('aggregator_form_category', 2),
19311
    'access arguments' => array('administer news feeds'),
19411
    'type' => MENU_LOCAL_TASK,
19511
    'weight' => 1,
196
  );
19711
  $items['aggregator/sources/%aggregator_feed'] = array(
19811
    'page callback' => 'aggregator_page_source',
19911
    'page arguments' => array(2),
20011
    'access arguments' => array('access news feeds'),
20111
    'type' => MENU_CALLBACK,
202
  );
20311
  $items['aggregator/sources/%aggregator_feed/view'] = array(
20411
    'title' => 'View',
20511
    'type' => MENU_DEFAULT_LOCAL_TASK,
20611
    'weight' => -10,
207
  );
20811
  $items['aggregator/sources/%aggregator_feed/categorize'] = array(
20911
    'title' => 'Categorize',
21011
    'page callback' => 'drupal_get_form',
21111
    'page arguments' => array('aggregator_page_source', 2),
21211
    'access arguments' => array('administer news feeds'),
21311
    'type' => MENU_LOCAL_TASK,
214
  );
21511
  $items['aggregator/sources/%aggregator_feed/configure'] = array(
21611
    'title' => 'Configure',
21711
    'page callback' => 'drupal_get_form',
21811
    'page arguments' => array('aggregator_form_feed', 2),
21911
    'access arguments' => array('administer news feeds'),
22011
    'type' => MENU_LOCAL_TASK,
22111
    'weight' => 1,
222
  );
22311
  $items['admin/content/aggregator/edit/feed/%aggregator_feed'] = array(
22411
    'title' => 'Edit feed',
22511
    'page callback' => 'drupal_get_form',
22611
    'page arguments' => array('aggregator_form_feed', 5),
22711
    'access arguments' => array('administer news feeds'),
22811
    'type' => MENU_CALLBACK,
229
  );
23011
  $items['admin/content/aggregator/edit/category/%aggregator_category'] =
array(
23111
    'title' => 'Edit category',
23211
    'page callback' => 'drupal_get_form',
23311
    'page arguments' => array('aggregator_form_category', 5),
23411
    'access arguments' => array('administer news feeds'),
23511
    'type' => MENU_CALLBACK,
236
  );
237
23811
  return $items;
2390
}
240
241
/**
242
 * Menu callback.
243
 *
244
 * @return
245
 *   An aggregator category title.
246
 */
247126
function _aggregator_category_title($category) {
2480
  return $category['title'];
2490
}
250
251
/**
252
 * Implementation of hook_init().
253
 */
254126
function aggregator_init() {
255117
  drupal_add_css(drupal_get_path('module', 'aggregator') .
'/aggregator.css');
256117
}
257
258
/**
259
 * Find out whether there are any aggregator categories.
260
 *
261
 * @return
262
 *   TRUE if there is at least one category and the user has access to
them, FALSE otherwise.
263
 */
264126
function _aggregator_has_categories() {
2652
  return user_access('access news feeds') && db_query('SELECT COUNT(*) FROM
{aggregator_category}')->fetchField();
2660
}
267
268
/**
269
 * Implementation of hook_perm().
270
 */
271126
function aggregator_perm() {
272
  return array(
273
    'administer news feeds' => array(
2747
      'title' => t('Administer news feeds'),
2757
      'description' => t('Add, edit or delete news feeds that are
aggregated to your site.'),
2767
    ),
277
    'access news feeds' => array(
2787
      'title' => t('Access news feeds'),
2797
      'description' => t('View aggregated news feed items.'),
2807
    ),
2817
  );
2820
}
283
284
/**
285
 * Implementation of hook_cron().
286
 *
287
 * Checks news feeds for updates once their refresh interval has elapsed.
288
 */
289126
function aggregator_cron() {
2900
  $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked +
refresh < :time', array(':time' => REQUEST_TIME));
2910
  foreach ($result as $feed) {
2920
    aggregator_refresh((array)$feed);
2930
  }
2940
}
295
296
/**
297
 * Implementation of hook_block().
298
 *
299
 * Generates blocks for the latest news items in each category and feed.
300
 */
301126
function aggregator_block($op = 'list', $delta = '', $edit = array()) {
3020
  if (user_access('access news feeds')) {
3030
    if ($op == 'list') {
3040
      $result = db_query('SELECT cid, title FROM {aggregator_category}
ORDER BY title');
3050
      foreach ($result as $category) {
3060
        $block['category-' . $category->cid]['info'] = t('!title category
latest items', array('!title' => $category->title));
3070
      }
3080
      $result = db_query('SELECT fid, title FROM {aggregator_feed} WHERE
block <> 0 ORDER BY fid');
3090
      foreach ($result as $feed) {
3100
        $block['feed-' . $feed->fid]['info'] = t('!title feed latest
items', array('!title' => $feed->title));
3110
      }
3120
    }
3130
    elseif ($op == 'configure') {
3140
      list($type, $id) = explode('-', $delta);
3150
      if ($type == 'category') {
3160
        $value = db_query('SELECT block FROM {aggregator_category} WHERE
cid = :cid', array(':cid' => $id))->fetchField();
3170
        $form['block'] = array(
3180
          '#type' => 'select',
3190
          '#title' => t('Number of news items in block'),
3200
          '#default_value' => $value,
3210
          '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20))
3220
        );
3230
        return $form;
3240
      }
3250
    }
3260
    elseif ($op == 'save') {
3270
      list($type, $id) = explode('-', $delta);
3280
      if ($type == 'category') {
3290
        db_merge('aggregator_category')
3300
          ->key(array('cid' => $id))
3310
          ->fields(array('block' => $edit['block']))
3320
          ->execute();
3330
      }
3340
    }
3350
    elseif ($op == 'view') {
3360
      list($type, $id) = explode('-', $delta);
337
      switch ($type) {
3380
        case 'feed':
3390
          if ($feed = db_query('SELECT fid, title, block FROM
{aggregator_feed} WHERE block <> 0 AND fid = :fid', array(':fid' =>
$id))->fetchObject()) {
3400
            $block['subject'] = check_plain($feed->title);
3410
            $result = db_query_range("SELECT * FROM {aggregator_item} WHERE
fid = :fid ORDER BY timestamp DESC, iid DESC", array(':fid' => $id), 0,
$feed->block);
3420
            $read_more = theme('more_link', url('aggregator/sources/' .
$feed->fid), t("View this feed's recent news."));
3430
          }
3440
          break;
345
3460
        case 'category':
3470
          if ($category = db_query('SELECT cid, title, block FROM
{aggregator_category} WHERE cid = :cid', array(':cid' =>
$id))->fetchObject()) {
3480
            $block['subject'] = check_plain($category->title);
3490
            $result = db_query_range('SELECT i.* FROM
{aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON ci.iid =
i.iid WHERE ci.cid = :cid ORDER BY i.timestamp DESC, i.iid DESC',
array(':cid' => $category->cid), 0, $category->block);
3500
            $read_more = theme('more_link', url('aggregator/categories/' .
$category->cid), t("View this category's recent news."));
3510
          }
3520
          break;
3530
      }
3540
      $items = array();
3550
      foreach ($result as $item) {
3560
        $items[] = theme('aggregator_block_item', $item);
3570
      }
358
359
      // Only display the block if there are items to show.
3600
      if (count($items) > 0) {
3610
        $block['content'] = theme('item_list', $items) . $read_more;
3620
      }
3630
    }
3640
    if (isset($block)) {
3650
      return $block;
3660
    }
3670
  }
3680
}
369
370
/**
371
 * Add/edit/delete aggregator categories.
372
 *
373
 * @param $edit
374
 *   An associative array describing the category to be
added/edited/deleted.
375
 */
376126
function aggregator_save_category($edit) {
3771
  $link_path = 'aggregator/categories/';
3781
  if (!empty($edit['cid'])) {
3790
    $link_path .= $edit['cid'];
3800
    if (!empty($edit['title'])) {
3810
      db_merge('aggregator_category')
3820
        ->key(array('cid' => $edit['cid']))
3830
        ->fields(array(
3840
          'title' => $edit['title'],
3850
          'description' => $edit['description'],
3860
        ))
3870
        ->execute();
3880
      $op = 'update';
3890
    }
390
    else {
3910
      db_delete('aggregator_category')
3920
        ->condition('cid', $edit['cid'])
3930
        ->execute();
394
      // Make sure there is no active block for this category.
3950
      db_delete('blocks')
3960
        ->condition('module', 'aggregator')
3970
        ->condition('delta', 'category-' . $edit['cid'])
3980
        ->execute();
3990
      $edit['title'] = '';
4000
      $op = 'delete';
401
    }
4020
  }
4031
  elseif (!empty($edit['title'])) {
404
    // A single unique id for bundles and feeds, to use in blocks.
4051
    $link_path .= db_insert('aggregator_category')
4061
      ->fields(array(
4071
        'title' => $edit['title'],
4081
        'description' => $edit['description'],
4091
      ))
4101
      ->execute();
4111
    $op = 'insert';
4121
  }
4131
  if (isset($op)) {
4141
    menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
4151
  }
4161
}
417
418
/**
419
 * Add/edit/delete an aggregator feed.
420
 *
421
 * @param $edit
422
 *   An associative array describing the feed to be added/edited/deleted.
423
 */
424126
function aggregator_save_feed($edit) {
42515
  if (!empty($edit['fid'])) {
426
    // An existing feed is being modified, delete the category listings.
4277
    db_delete('aggregator_category_feed')
4287
      ->condition('fid', $edit['fid'])
4297
      ->execute();
4307
  }
43115
  if (!empty($edit['fid']) && !empty($edit['title'])) {
4321
    db_update('aggregator_feed')
4331
      ->condition('fid', $edit['fid'])
4341
      ->fields(array(
4351
        'title' => $edit['title'],
4361
        'url' => $edit['url'],
4371
        'refresh' => $edit['refresh'],
4381
        'block' => $edit['block'],
4391
      ))
4401
      ->execute();
4411
  }
44214
  elseif (!empty($edit['fid'])) {
4436
    $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid',
array(':fid' => $edit['fid']))->fetchCol();
4446
    if ($iids) {
4450
      db_delete('aggregator_category_item')
4460
        ->condition('iid', $iids, 'IN')
4470
        ->execute();
4480
    }
4496
    db_delete('aggregator_feed')->
4506
      condition('fid', $edit['fid'])
4516
      ->execute();
4526
    db_delete('aggregator_item')
4536
      ->condition('fid', $edit['fid'])
4546
      ->execute();
455
    // Make sure there is no active block for this feed.
4566
    db_delete('blocks')
4576
      ->condition('module', 'aggregator')
4586
      ->condition('delta', 'feed-' . $edit['fid'])
4596
      ->execute();
4606
  }
4618
  elseif (!empty($edit['title'])) {
4628
    $edit['fid'] = db_insert('aggregator_feed')
4638
      ->fields(array(
4648
        'title' => $edit['title'],
4658
        'url' => $edit['url'],
4668
        'refresh' => $edit['refresh'],
4678
        'block' => $edit['block'],
4688
        'description' => '',
4698
        'image' => '',
4708
      ))
4718
      ->execute();
472
    
4738
  }
47415
  if (!empty($edit['title'])) {
475
    // The feed is being saved, save the categories as well.
4769
    if (!empty($edit['category'])) {
4772
      foreach ($edit['category'] as $cid => $value) {
4782
        if ($value) {
4791
          db_merge('aggregator_category_feed')
4801
            ->fields(array(
4811
              'fid' => $edit['fid'],
4821
              'cid' => $cid,
4831
            ))
4841
            ->execute();
4851
        }
4862
      }
4872
    }
4889
  }
48915
}
490
491
/**
492
 * Removes all items from a feed.
493
 *
494
 * @param $feed
495
 *   An associative array describing the feed to be cleared.
496
 */
497126
function aggregator_remove($feed) {
4982
  $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid',
array(':fid' => $feed['fid']))->fetchCol();
4992
  if ($iids) {
5000
    db_delete('aggregator_category_item')
5010
      ->condition('iid', $iids, 'IN')
5020
      ->execute();
5030
  }
504
  
5052
  db_delete('aggregator_item')
5062
    ->condition('fid', $feed['fid'])
5072
    ->execute();
5082
  db_merge('aggregator_feed')
5092
    ->key(array('fid' => $feed['fid']))
5102
    ->fields(array(
5112
      'checked' => 0,
5122
      'hash' => '',
5132
      'modified' => 0,
5142
      'description' => $feed['description'],
5152
      'image' => $feed['image'],
5162
    ))
5172
    ->execute();
5182
  drupal_set_message(t('The news items from %site have been removed.',
array('%site' => $feed['title'])));
5192
}
520
521
/**
522
 * Callback function used by the XML parser.
523
 */
524126
function aggregator_element_start($parser, $name, $attributes) {
5255
  global $item, $element, $tag, $items, $channel;
526
527
  switch ($name) {
5285
    case 'IMAGE':
5295
    case 'TEXTINPUT':
5305
    case 'CONTENT':
5315
    case 'SUMMARY':
5325
    case 'TAGLINE':
5335
    case 'SUBTITLE':
5345
    case 'LOGO':
5355
    case 'INFO':
5362
      $element = $name;
5372
      break;
5385
    case 'ID':
5390
      if ($element != 'ITEM') {
5400
        $element = $name;
5410
      }
5425
    case 'LINK':
5435
      if (!empty($attributes['REL']) && $attributes['REL'] == 'alternate')
{
5440
        if ($element == 'ITEM') {
5450
          $items[$item]['LINK'] = $attributes['HREF'];
5460
        }
547
        else {
5480
          $channel['LINK'] = $attributes['HREF'];
549
        }
5500
      }
5515
      break;
5525
    case 'ITEM':
5532
      $element = $name;
5542
      $item += 1;
5552
      break;
5565
    case 'ENTRY':
5570
      $element = 'ITEM';
5580
      $item += 1;
5590
      break;
5600
  }
561
5625
  $tag = $name;
5635
}
564
565
/**
566
 * Call-back function used by the XML parser.
567
 */
568126
function aggregator_element_end($parser, $name) {
5695
  global $element;
570
571
  switch ($name) {
5725
    case 'IMAGE':
5735
    case 'TEXTINPUT':
5745
    case 'ITEM':
5755
    case 'ENTRY':
5765
    case 'CONTENT':
5775
    case 'INFO':
5782
      $element = '';
5792
      break;
5805
    case 'ID':
5810
      if ($element == 'ID') {
5820
        $element = '';
5830
      }
5840
  }
5855
}
586
587
/**
588
 * Callback function used by the XML parser.
589
 */
590126
function aggregator_element_data($parser, $data) {
5915
  global $channel, $element, $items, $item, $image, $tag;
5925
  $items += array($item => array());
593
  switch ($element) {
5945
    case 'ITEM':
5952
      $items[$item] += array($tag => '');
5962
      $items[$item][$tag] .= $data;
5972
      break;
5985
    case 'IMAGE':
5995
    case 'LOGO':
6002
      $image += array($tag => '');
6012
      $image[$tag] .= $data;
6022
      break;
6035
    case 'LINK':
6040
      if ($data) {
6050
        $items[$item] += array($tag => '');
6060
        $items[$item][$tag] .= $data;
6070
      }
6080
      break;
6095
    case 'CONTENT':
6100
      $items[$item] += array('CONTENT' => '');
6110
      $items[$item]['CONTENT'] .= $data;
6120
      break;
6135
    case 'SUMMARY':
6140
      $items[$item] += array('SUMMARY' => '');
6150
      $items[$item]['SUMMARY'] .= $data;
6160
      break;
6175
    case 'TAGLINE':
6185
    case 'SUBTITLE':
6190
      $channel += array('DESCRIPTION' => '');
6200
      $channel['DESCRIPTION'] .= $data;
6210
      break;
6225
    case 'INFO':
6235
    case 'ID':
6245
    case 'TEXTINPUT':
625
      // The sub-element is not supported. However, we must recognize
626
      // it or its contents will end up in the item array.
6270
      break;
6285
    default:
6295
      $channel += array($tag => '');
6305
      $channel[$tag] .= $data;
6315
  }
6325
}
633
634
/**
635
 * Checks a news feed for new items.
636
 *
637
 * @param $feed
638
 *   An associative array describing the feed to be refreshed.
639
 */
640126
function aggregator_refresh($feed) {
6415
  global $channel, $image;
642
643
  // Generate conditional GET headers.
6445
  $headers = array();
6455
  if ($feed['etag']) {
6460
    $headers['If-None-Match'] = $feed['etag'];
6470
  }
6485
  if ($feed['modified']) {
6490
    $headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s',
$feed['modified']) . ' GMT';
6500
  }
651
652
  // Request feed.
6535
  $result = drupal_http_request($feed['url'], $headers);
654
655
  // Process HTTP response code.
6565
  switch ($result->code) {
6575
    case 304:
6580
      db_update('aggregator_feed')
6590
        ->fields(array('checked' => REQUEST_TIME))
6600
        ->condition('fid', $feed['fid'])
6610
        ->execute();
6620
      drupal_set_message(t('There is no new syndicated content from
%site.', array('%site' => $feed['title'])));
6630
      break;
6645
    case 301:
6650
      $feed['url'] = $result->redirect_url;
666
      // Do not break here.
6675
    case 200:
6685
    case 302:
6695
    case 307:
670
      // We store the md5 hash of feed data in the database. When
refreshing a
671
      // feed we compare stored hash and new hash calculated from
downloaded
672
      // data. If both are equal we say that feed is not updated.
6735
      $md5 = md5($result->data);
6745
      if ($feed['hash'] == $md5) {
6750
        db_update('aggregator_feed')
6760
          ->condition('fid', $feed['fid'])
6770
          ->fields(array('checked' => REQUEST_TIME))
6780
          ->execute();
6790
        drupal_set_message(t('There is no new syndicated content from
%site.', array('%site' => $feed['title'])));
6800
        break;
6810
      }
682
683
      // Filter the input data.
6845
      if (aggregator_parse_feed($result->data, $feed)) {
6855
        $modified = empty($result->headers['Last-Modified']) ? 0 :
strtotime($result->headers['Last-Modified']);
686
687
        // Prepare the channel data.
6885
        foreach ($channel as $key => $value) {
6895
          $channel[$key] = trim($value);
6905
        }
691
692
        // Prepare the image data (if any).
6935
        foreach ($image as $key => $value) {
6942
          $image[$key] = trim($value);
6952
        }
696
6975
        if (!empty($image['LINK']) && !empty($image['URL']) &&
!empty($image['TITLE'])) {
698
          $image = l(theme('image', $image['URL'], $image['TITLE']),
$image['LINK'], array('html' => TRUE));
699
        }
7002
        else {
7012
          $image = '';
702
        }
7033
704
        $etag = empty($result->headers['ETag']) ? '' :
$result->headers['ETag'];
705
        // Update the feed data.
7065
        db_merge('aggregator_feed')
707
          ->key(array('fid' => $feed['fid']))
7085
          ->fields(array(
7095
            'url' => $feed['url'],
7105
            'checked' => REQUEST_TIME,
7115
            'link' => $channel['LINK'],
7125
            'description' => $channel['DESCRIPTION'],
7135
            'image' => $image,
7145
            'hash' => $md5,
7155
            'etag' => $etag,
7165
            'modified' => $modified,
7175
          ))
7185
          ->execute();
7195
7205
        // Clear the cache.
721
        cache_clear_all();
722
7235
        if (isset($result->redirect_url)) {
724
          watchdog('aggregator', 'Updated URL for feed %title to %url.',
array('%title' => $feed['title'], '%url' => $feed['url']));
7255
        }
7260
7270
        watchdog('aggregator', 'There is new syndicated content from
%site.', array('%site' => $feed['title']));
728
        drupal_set_message(t('There is new syndicated content from %site.',
array('%site' => $feed['title'])));
7295
      }
7305
      break;
7315
    default:
7325
      watchdog('aggregator', 'The feed from %site seems to be broken, due
to "%error".', array('%site' => $feed['title'], '%error' => $result->code .
' ' . $result->error), WATCHDOG_WARNING);
7330
      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)));
7340
      module_invoke('system', 'check_http_request');
7350
  }
7360
}
7370
7385
/**
739
 * Parse the W3C date/time format, a subset of ISO 8601.
740
 *
741
 * PHP date parsing functions do not handle this format.
742
 * See http://www.w3.org/TR/NOTE-datetime for more information.
743
 * Originally from MagpieRSS (http://magpierss.sourceforge.net/).
744
 *
745
 * @param $date_str
746
 *   A string with a potentially W3C DTF date.
747
 * @return
748
 *   A timestamp if parsed successfully or FALSE if not.
749
 */
750
function aggregator_parse_w3cdtf($date_str) {
751
  if
(preg_match('/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/',
$date_str, $match)) {
752126
    list($year, $month, $day, $hours, $minutes, $seconds) =
array($match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
7535
    // Calculate the epoch for current date assuming GMT.
7540
    $epoch = gmmktime($hours, $minutes, $seconds, $month, $day, $year);
755
    if ($match[10] != 'Z') { // Z is zulu time, aka GMT
7560
      list($tz_mod, $tz_hour, $tz_min) = array($match[8], $match[9],
$match[10]);
7570
      // Zero out the variables.
7580
      if (!$tz_hour) {
759
        $tz_hour = 0;
7600
      }
7610
      if (!$tz_min) {
7620
        $tz_min = 0;
7630
      }
7640
      $offset_secs = (($tz_hour * 60) + $tz_min) * 60;
7650
      // Is timezone ahead of GMT?  If yes, subtract offset.
7660
      if ($tz_mod == '+') {
767
        $offset_secs *= -1;
7680
      }
7690
      $epoch += $offset_secs;
7700
    }
7710
    return $epoch;
7720
  }
7730
  else {
7740
    return FALSE;
775
  }
7765
}
777
7780
/**
779
 * Parse a feed and store its items.
780
 *
781
 * @param $data
782
 *   The feed data.
783
 * @param $feed
784
 *   An associative array describing the feed to be parsed.
785
 * @return
786
 *   FALSE on error, TRUE otherwise.
787
 */
788
function aggregator_parse_feed(&$data, $feed) {
789
  global $items, $image, $channel;
790126
7915
  // Unset the global variables before we use them.
792
  unset($GLOBALS['element'], $GLOBALS['item'], $GLOBALS['tag']);
793
  $items = array();
7945
  $image = array();
7955
  $channel = array();
7965
7975
  // Parse the data.
798
  $xml_parser = drupal_xml_parser_create($data);
799
  xml_set_element_handler($xml_parser, 'aggregator_element_start',
'aggregator_element_end');
8005
  xml_set_character_data_handler($xml_parser, 'aggregator_element_data');
8015
8025
  if (!xml_parse($xml_parser, $data, 1)) {
803
    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);
8045
    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');
8050
    return FALSE;
8060
  }
8070
  xml_parser_free($xml_parser);
8080
8095
  // We reverse the array such that we store the first item last, and the
last
810
  // item first. In the database, the newest item should be at the top.
811
  $items = array_reverse($items);
812
8135
  // Initialize variables.
814
  $title = $link = $author = $description = $guid = NULL;
815
  foreach ($items as $item) {
8165
    unset($title, $link, $author, $description, $guid);
8175
8185
    // Prepare the item:
819
    foreach ($item as $key => $value) {
820
      $item[$key] = trim($value);
8215
    }
8222
8232
    // Resolve the item's title. If no title is found, we use up to 40
824
    // characters of the description ending at a word boundary, but not
825
    // splitting potential entities.
826
    if (!empty($item['TITLE'])) {
827
      $title = $item['TITLE'];
8285
    }
8292
    elseif (!empty($item['DESCRIPTION'])) {
8302
      $title = preg_replace('/^(.*)[^\w;&].*?$/', "\\1",
truncate_utf8($item['DESCRIPTION'], 40));
8315
    }
8320
    else {
8330
      $title = '';
834
    }
8355
836
    // Resolve the items link.
837
    if (!empty($item['LINK'])) {
838
      $link = $item['LINK'];
8395
    }
8402
    else {
8412
      $link = $feed['link'];
842
    }
8435
    $guid = isset($item['GUID']) ? $item['GUID'] : '';
844
8455
    // Atom feeds have a CONTENT and/or SUMMARY tag instead of a
DESCRIPTION tag.
846
    if (!empty($item['CONTENT:ENCODED'])) {
847
      $item['DESCRIPTION'] = $item['CONTENT:ENCODED'];
8485
    }
8490
    elseif (!empty($item['SUMMARY'])) {
8500
      $item['DESCRIPTION'] = $item['SUMMARY'];
8515
    }
8520
    elseif (!empty($item['CONTENT'])) {
8530
      $item['DESCRIPTION'] = $item['CONTENT'];
8545
    }
8550
8560
    // Try to resolve and parse the item's publication date.
857
    $date = '';
858
    foreach (array('PUBDATE', 'DC:DATE', 'DCTERMS:ISSUED',
'DCTERMS:CREATED', 'DCTERMS:MODIFIED', 'ISSUED', 'CREATED', 'MODIFIED',
'PUBLISHED', 'UPDATED') as $key) {
8595
      if (!empty($item[$key])) {
8605
        $date = $item[$key];
8615
        break;
8620
      }
8630
    }
8640
8655
    $timestamp = strtotime($date);
866
8675
    if ($timestamp === FALSE) {
868
      $timestamp = aggregator_parse_w3cdtf($date); //
Aggregator_parse_w3cdtf() returns FALSE on failure.
8695
    }
8705
8715
    // Save this item. Try to avoid duplicate entries as much as possible.
If
872
    // we find a duplicate entry, we resolve it and pass along its ID is
such
873
    // that we can update it if needed.
874
    if (!empty($guid)) {
875
      $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE
fid = :fid AND guid = :guid", array(':fid' => $feed['fid'], ':guid' =>
$guid))->fetchObject();
8765
    }
8770
    elseif ($link && $link != $feed['link'] && $link != $feed['url']) {
8780
      $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE
fid = :fid AND link = :link", array(':fid' => $feed['fid'], ':link' =>
$link))->fetchObject();
8795
    }
8802
    else {
8812
      $entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE
fid = :fid AND title = :title", array(':fid' => $feed['fid'], ':title' =>
$title))->fetchObject();
882
    }
8835
884
    if (!$timestamp) {
885
      $timestamp = isset($entry->timestamp) ? $entry->timestamp :
REQUEST_TIME;
8865
    }
8875
    $item += array('AUTHOR' => '', 'DESCRIPTION' => '');
8885
    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));
8895
  }
8905
8915
  // Remove all items that are older than flush item timer.
892
  $age = REQUEST_TIME - variable_get('aggregator_clear', 9676800);
893
  $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid AND
timestamp < :timestamp', array(':fid' => $feed['fid'], ':timestamp' =>
$age))->fetchCol();
8945
  if ($iids) {
8955
    db_delete('aggregator_category_item')
8965
      ->condition('iid', $iids, 'IN')
8970
      ->execute();
8980
    db_delete('aggregator_item')
8990
      ->condition('iid', $iids, 'IN')
9000
      ->execute();
9010
  }
9020
9030
  return TRUE;
904
}
9055
9060
/**
907
 * Add/edit/delete an aggregator item.
908
 *
909
 * @param $edit
910
 *   An associative array describing the item to be added/edited/deleted.
911
 */
912
function aggregator_save_item($edit) {
913
  if ($edit['title'] && empty($edit['iid'])) {
914126
    $edit['iid'] = db_insert('aggregator_item')
9155
      ->fields(array(
9161
        'title' => $edit['title'],
9171
        'link' => $edit['link'],
9181
        'author' => $edit['author'],
9191
        'description' => $edit['description'],
9201
        'guid' => $edit['guid'],
9211
        'timestamp' => $edit['timestamp'],
9221
        'fid' => $edit['fid'],
9231
      ))
9241
      ->execute();
9251
  }
9261
  if ($edit['iid'] && !$edit['title']) {
9271
    db_delete('aggregator_item')
9285
      ->condition('iid', $edit['iid'])
9290
      ->execute();
9300
    db_delete('aggregator_category_item')
9310
      ->condition('iid', $edit['iid'])
9320
      ->execute();
9330
  }
9340
  elseif ($edit['title'] && $edit['link']) {
9350
    // file the items in the categories indicated by the feed
9365
    $result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE
fid = :fid', array(':fid' => $edit['fid']));
937
    foreach ($result as $category) {
9382
      db_merge('aggregator_category_item')
9392
        ->fields(array(
9400
          'cid' => $category->cid,
9410
          'iid' => $edit['iid'],
9420
        ))
9430
        ->execute();
9440
    }
9450
  }
9460
}
9472
9485
/**
949
 * Load an aggregator feed.
950
 *
951
 * @param $fid
952
 *   The feed id.
953
 * @return
954
 *   An associative array describing the feed.
955
 */
956
function aggregator_feed_load($fid) {
957
  static $feeds;
958126
  if (!isset($feeds[$fid])) {
95926
    $feeds[$fid] = db_query('SELECT * FROM {aggregator_feed} WHERE fid =
:fid', array(':fid' => $fid))->fetchAssoc();
96026
  }
96126
96226
  return $feeds[$fid];
963
}
96426
9650
/**
966
 * Load an aggregator category.
967
 *
968
 * @param $cid
969
 *   The category id.
970
 * @return
971
 *   An associative array describing the category.
972
 */
973
function aggregator_category_load($cid) {
974
  static $categories;
975126
  if (!isset($categories[$cid])) {
9760
    $categories[$cid] = db_query('SELECT * FROM {aggregator_category} WHERE
cid = :cid', array(':cid' => $cid))->fetchAssoc();
9770
  }
9780
9790
  return $categories[$cid];
980
}
9810
9820
/**
983
 * Format an individual feed item for display in the block.
984
 *
985
 * @param $item
986
 *   The item to be displayed.
987
 * @param $feed
988
 *   Not used.
989
 * @return
990
 *   The item HTML.
991
 * @ingroup themeable
992
 */
993
function theme_aggregator_block_item($item, $feed = 0) {
994
995126
  // Display the external link to the item.
9960
  $output .= '<a href="' . check_url($item->link) . '">' .
check_plain($item->title) . "</a>\n";
997
9980
  return $output;
9990
}
10000
10010
/**
10020
 * Safely render HTML content, as allowed.
10030
 *
1004
 * @param $value
1005
 *   The content to be filtered.
10060
 * @return
1007
 *   The filtered content.
10080
 */
10090
function aggregator_filter_xss($value) {
1010
  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));
1011
}
1012
1013
/**
1014
 * Helper function for drupal_map_assoc.
1015
 *
1016
 * @param $count
1017
 *   Items count.
1018
 * @return
1019126
 *   Plural-formatted "@count items"
10202
 */
10210
function _aggregator_items($count) {
1022
  return format_plural($count, '1 item', '@count items');
1023
}
1024