comparison modules/aggregator/aggregator.module @ 1:c1f4ac30525a 6.0

Drupal 6.0
author Franck Deroche <webmaster@defr.org>
date Tue, 23 Dec 2008 14:28:28 +0100
parents
children 2427550111ae
comparison
equal deleted inserted replaced
0:5a113a1c4740 1:c1f4ac30525a
1 <?php
2 // $Id: aggregator.module,v 1.374 2008/01/15 08:06:32 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 */
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' => 'hsttp://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 'type' => MENU_CALLBACK,
205 'file' => 'aggregator.pages.inc',
206 );
207 $items['aggregator/sources/%aggregator_feed/view'] = array(
208 'title' => 'View',
209 'type' => MENU_DEFAULT_LOCAL_TASK,
210 'weight' => -10,
211 );
212 $items['aggregator/sources/%aggregator_feed/categorize'] = array(
213 'title' => 'Categorize',
214 'page callback' => 'drupal_get_form',
215 'page arguments' => array('aggregator_page_source', 2),
216 'access arguments' => array('administer news feeds'),
217 'type' => MENU_LOCAL_TASK,
218 'file' => 'aggregator.pages.inc',
219 );
220 $items['aggregator/sources/%aggregator_feed/configure'] = array(
221 'title' => 'Configure',
222 'page callback' => 'drupal_get_form',
223 'page arguments' => array('aggregator_form_feed', 2),
224 'access arguments' => array('administer news feeds'),
225 'type' => MENU_LOCAL_TASK,
226 'weight' => 1,
227 'file' => 'aggregator.admin.inc',
228 );
229 $items['admin/content/aggregator/edit/feed/%aggregator_feed'] = array(
230 'title' => 'Edit feed',
231 'page callback' => 'drupal_get_form',
232 'page arguments' => array('aggregator_form_feed', 5),
233 'access arguments' => array('administer news feeds'),
234 'type' => MENU_CALLBACK,
235 'file' => 'aggregator.admin.inc',
236 );
237 $items['admin/content/aggregator/edit/category/%aggregator_category'] = array(
238 'title' => 'Edit category',
239 'page callback' => 'drupal_get_form',
240 'page arguments' => array('aggregator_form_category', 5),
241 'access arguments' => array('administer news feeds'),
242 'type' => MENU_CALLBACK,
243 'file' => 'aggregator.admin.inc',
244 );
245
246 return $items;
247 }
248
249 /**
250 * Menu callback.
251 *
252 * @return
253 * An aggregator category title.
254 */
255 function _aggregator_category_title($category) {
256 return $category['title'];
257 }
258
259 /**
260 * Implementation of hook_init().
261 */
262 function aggregator_init() {
263 drupal_add_css(drupal_get_path('module', 'aggregator') .'/aggregator.css');
264 }
265
266 /**
267 * Find out whether there are any aggregator categories.
268 *
269 * @return
270 * TRUE if there is at least one category and the user has access to them, FALSE otherwise.
271 */
272 function _aggregator_has_categories() {
273 return user_access('access news feeds') && db_result(db_query('SELECT COUNT(*) FROM {aggregator_category}'));
274 }
275
276 /**
277 * Implementation of hook_perm().
278 */
279 function aggregator_perm() {
280 return array('administer news feeds', 'access news feeds');
281 }
282
283 /**
284 * Implementation of hook_cron().
285 *
286 * Checks news feeds for updates once their refresh interval has elapsed.
287 */
288 function aggregator_cron() {
289 $result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < %d', time());
290 while ($feed = db_fetch_array($result)) {
291 aggregator_refresh($feed);
292 }
293 }
294
295 /**
296 * Implementation of hook_block().
297 *
298 * Generates blocks for the latest news items in each category and feed.
299 */
300 function aggregator_block($op = 'list', $delta = 0, $edit = array()) {
301 if (user_access('access news feeds')) {
302 if ($op == 'list') {
303 $result = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title');
304 while ($category = db_fetch_object($result)) {
305 $block['category-'. $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title));
306 }
307 $result = db_query('SELECT fid, title FROM {aggregator_feed} ORDER BY fid');
308 while ($feed = db_fetch_object($result)) {
309 $block['feed-'. $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
310 }
311 }
312 else if ($op == 'configure') {
313 list($type, $id) = explode('-', $delta);
314 if ($type == 'category') {
315 $value = db_result(db_query('SELECT block FROM {aggregator_category} WHERE cid = %d', $id));
316 }
317 else {
318 $value = db_result(db_query('SELECT block FROM {aggregator_feed} WHERE fid = %d', $id));
319 }
320 $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)));
321 return $form;
322 }
323 else if ($op == 'save') {
324 list($type, $id) = explode('-', $delta);
325 if ($type == 'category') {
326 $value = db_query('UPDATE {aggregator_category} SET block = %d WHERE cid = %d', $edit['block'], $id);
327 }
328 else {
329 $value = db_query('UPDATE {aggregator_feed} SET block = %d WHERE fid = %d', $edit['block'], $id);
330 }
331 }
332 else if ($op == 'view') {
333 list($type, $id) = explode('-', $delta);
334 switch ($type) {
335 case 'feed':
336 if ($feed = db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id))) {
337 $block['subject'] = check_plain($feed->title);
338 $result = db_query_range('SELECT * FROM {aggregator_item} WHERE fid = %d ORDER BY timestamp DESC, iid DESC', $feed->fid, 0, $feed->block);
339 $read_more = theme('more_link', url('aggregator/sources/'. $feed->fid), t("View this feed's recent news."));
340 }
341 break;
342
343 case 'category':
344 if ($category = db_fetch_object(db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = %d', $id))) {
345 $block['subject'] = check_plain($category->title);
346 $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);
347 $read_more = theme('more_link', url('aggregator/categories/'. $category->cid), t("View this category's recent news."));
348 }
349 break;
350 }
351 $items = array();
352 while ($item = db_fetch_object($result)) {
353 $items[] = theme('aggregator_block_item', $item);
354 }
355
356 // Only display the block if there are items to show.
357 if (count($items) > 0) {
358 $block['content'] = theme('item_list', $items) . $read_more;
359 }
360 }
361 if (isset($block)) {
362 return $block;
363 }
364 }
365 }
366
367 /**
368 * Add/edit/delete aggregator categories.
369 *
370 * @param $edit
371 * An associative array describing the category to be added/edited/deleted.
372 */
373 function aggregator_save_category($edit) {
374 $link_path = 'aggregator/categories/';
375 if (!empty($edit['cid'])) {
376 $link_path .= $edit['cid'];
377 if (!empty($edit['title'])) {
378 db_query("UPDATE {aggregator_category} SET title = '%s', description = '%s' WHERE cid = %d", $edit['title'], $edit['description'], $edit['cid']);
379 $op = 'update';
380 }
381 else {
382 db_query('DELETE FROM {aggregator_category} WHERE cid = %d', $edit['cid']);
383 $edit['title'] = '';
384 $op = 'delete';
385 }
386 }
387 else if (!empty($edit['title'])) {
388 // A single unique id for bundles and feeds, to use in blocks
389 db_query("INSERT INTO {aggregator_category} (title, description, block) VALUES ('%s', '%s', 5)", $edit['title'], $edit['description']);
390 $link_path .= db_last_insert_id('aggregator', 'cid');
391 $op = 'insert';
392 }
393 if (isset($op)) {
394 menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
395 }
396 }
397
398 /**
399 * Add/edit/delete an aggregator feed.
400 *
401 * @param $edit
402 * An associative array describing the feed to be added/edited/deleted.
403 */
404 function aggregator_save_feed($edit) {
405 if (!empty($edit['fid'])) {
406 // An existing feed is being modified, delete the category listings.
407 db_query('DELETE FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
408 }
409 if (!empty($edit['fid']) && !empty($edit['title'])) {
410 db_query("UPDATE {aggregator_feed} SET title = '%s', url = '%s', refresh = %d WHERE fid = %d", $edit['title'], $edit['url'], $edit['refresh'], $edit['fid']);
411 }
412 else if (!empty($edit['fid'])) {
413 $items = array();
414 $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
415 while ($item = db_fetch_object($result)) {
416 $items[] = "iid = $item->iid";
417 }
418 if (!empty($items)) {
419 db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
420 }
421 db_query('DELETE FROM {aggregator_feed} WHERE fid = %d', $edit['fid']);
422 db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
423 }
424 else if (!empty($edit['title'])) {
425 db_query("INSERT INTO {aggregator_feed} (title, url, refresh, block, description, image) VALUES ('%s', '%s', %d, 5, '', '')", $edit['title'], $edit['url'], $edit['refresh']);
426 // A single unique id for bundles and feeds, to use in blocks.
427 $edit['fid'] = db_last_insert_id('aggregator_feed', 'fid');
428 }
429 if (!empty($edit['title'])) {
430 // The feed is being saved, save the categories as well.
431 if (!empty($edit['category'])) {
432 foreach ($edit['category'] as $cid => $value) {
433 if ($value) {
434 db_query('INSERT INTO {aggregator_category_feed} (fid, cid) VALUES (%d, %d)', $edit['fid'], $cid);
435 }
436 }
437 }
438 }
439 }
440
441 /**
442 * Removes all items from a feed.
443 *
444 * @param $feed
445 * An associative array describing the feed to be cleared.
446 */
447 function aggregator_remove($feed) {
448 $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
449 while ($item = db_fetch_object($result)) {
450 $items[] = "iid = $item->iid";
451 }
452 if (!empty($items)) {
453 db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
454 }
455 db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
456 db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE fid = %d", $feed['fid']);
457 drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed['title'])));
458 }
459
460 /**
461 * Call-back function used by the XML parser.
462 */
463 function aggregator_element_start($parser, $name, $attributes) {
464 global $item, $element, $tag, $items, $channel;
465
466 switch ($name) {
467 case 'IMAGE':
468 case 'TEXTINPUT':
469 case 'CONTENT':
470 case 'SUMMARY':
471 case 'TAGLINE':
472 case 'SUBTITLE':
473 case 'LOGO':
474 case 'INFO':
475 $element = $name;
476 break;
477 case 'ID':
478 if ($element != 'ITEM') {
479 $element = $name;
480 }
481 case 'LINK':
482 if (!empty($attributes['REL']) && $attributes['REL'] == 'alternate') {
483 if ($element == 'ITEM') {
484 $items[$item]['LINK'] = $attributes['HREF'];
485 }
486 else {
487 $channel['LINK'] = $attributes['HREF'];
488 }
489 }
490 break;
491 case 'ITEM':
492 $element = $name;
493 $item += 1;
494 break;
495 case 'ENTRY':
496 $element = 'ITEM';
497 $item += 1;
498 break;
499 }
500
501 $tag = $name;
502 }
503
504 /**
505 * Call-back function used by the XML parser.
506 */
507 function aggregator_element_end($parser, $name) {
508 global $element;
509
510 switch ($name) {
511 case 'IMAGE':
512 case 'TEXTINPUT':
513 case 'ITEM':
514 case 'ENTRY':
515 case 'CONTENT':
516 case 'INFO':
517 $element = '';
518 break;
519 case 'ID':
520 if ($element == 'ID') {
521 $element = '';
522 }
523 }
524 }
525
526 /**
527 * Call-back function used by the XML parser.
528 */
529 function aggregator_element_data($parser, $data) {
530 global $channel, $element, $items, $item, $image, $tag;
531 $items += array($item => array());
532 switch ($element) {
533 case 'ITEM':
534 $items[$item] += array($tag => '');
535 $items[$item][$tag] .= $data;
536 break;
537 case 'IMAGE':
538 case 'LOGO':
539 $image += array($tag => '');
540 $image[$tag] .= $data;
541 break;
542 case 'LINK':
543 if ($data) {
544 $items[$item] += array($tag => '');
545 $items[$item][$tag] .= $data;
546 }
547 break;
548 case 'CONTENT':
549 $items[$item] += array('CONTENT' => '');
550 $items[$item]['CONTENT'] .= $data;
551 break;
552 case 'SUMMARY':
553 $items[$item] += array('SUMMARY' => '');
554 $items[$item]['SUMMARY'] .= $data;
555 break;
556 case 'TAGLINE':
557 case 'SUBTITLE':
558 $channel += array('DESCRIPTION' => '');
559 $channel['DESCRIPTION'] .= $data;
560 break;
561 case 'INFO':
562 case 'ID':
563 case 'TEXTINPUT':
564 // The sub-element is not supported. However, we must recognize
565 // it or its contents will end up in the item array.
566 break;
567 default:
568 $channel += array($tag => '');
569 $channel[$tag] .= $data;
570 }
571 }
572
573 /**
574 * Checks a news feed for new items.
575 *
576 * @param $feed
577 * An associative array describing the feed to be refreshed.
578 */
579 function aggregator_refresh($feed) {
580 global $channel, $image;
581
582 // Generate conditional GET headers.
583 $headers = array();
584 if ($feed['etag']) {
585 $headers['If-None-Match'] = $feed['etag'];
586 }
587 if ($feed['modified']) {
588 $headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s', $feed['modified']) .' GMT';
589 }
590
591 // Request feed.
592 $result = drupal_http_request($feed['url'], $headers);
593
594 // Process HTTP response code.
595 switch ($result->code) {
596 case 304:
597 db_query('UPDATE {aggregator_feed} SET checked = %d WHERE fid = %d', time(), $feed['fid']);
598 drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed['title'])));
599 break;
600 case 301:
601 $feed['url'] = $result->redirect_url;
602 watchdog('aggregator', 'Updated URL for feed %title to %url.', array('%title' => $feed['title'], '%url' => $feed['url']));
603 // Deliberate no break.
604 case 200:
605 case 302:
606 case 307:
607 // Filter the input data:
608 if (aggregator_parse_feed($result->data, $feed)) {
609 $modified = empty($result->headers['Last-Modified']) ? 0 : strtotime($result->headers['Last-Modified']);
610
611 // Prepare the channel data.
612 foreach ($channel as $key => $value) {
613 $channel[$key] = trim($value);
614 }
615
616 // Prepare the image data (if any).
617 foreach ($image as $key => $value) {
618 $image[$key] = trim($value);
619 }
620
621 if (!empty($image['LINK']) && !empty($image['URL']) && !empty($image['TITLE'])) {
622 // 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
623 $image = '<a href="'. check_url($image['LINK']) .'" class="feed-image"><img src="'. check_url($image['URL']) .'" alt="'. check_plain($image['TITLE']) .'" /></a>';
624 }
625 else {
626 $image = NULL;
627 }
628
629 $etag = empty($result->headers['ETag']) ? '' : $result->headers['ETag'];
630 // Update the feed data.
631 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']);
632
633 // Clear the cache.
634 cache_clear_all();
635
636 watchdog('aggregator', 'There is new syndicated content from %site.', array('%site' => $feed['title']));
637 drupal_set_message(t('There is new syndicated content from %site.', array('%site' => $feed['title'])));
638 break;
639 }
640 $result->error = t('feed not parseable');
641 // Deliberate no break.
642 default:
643 watchdog('aggregator', 'The feed from %site seems to be broken, due to "%error".', array('%site' => $feed['title'], '%error' => $result->code .' '. $result->error), WATCHDOG_WARNING);
644 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)));
645 module_invoke('system', 'check_http_request');
646 }
647 }
648
649 /**
650 * Parse the W3C date/time format, a subset of ISO 8601. PHP date parsing
651 * functions do not handle this format.
652 * See http://www.w3.org/TR/NOTE-datetime for more information.
653 * Originally from MagpieRSS (http://magpierss.sourceforge.net/).
654 *
655 * @param $date_str
656 * A string with a potentially W3C DTF date.
657 * @return
658 * A timestamp if parsed successfully or FALSE if not.
659 */
660 function aggregator_parse_w3cdtf($date_str) {
661 if (preg_match('/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/', $date_str, $match)) {
662 list($year, $month, $day, $hours, $minutes, $seconds) = array($match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
663 // calc epoch for current date assuming GMT
664 $epoch = gmmktime($hours, $minutes, $seconds, $month, $day, $year);
665 if ($match[10] != 'Z') { // Z is zulu time, aka GMT
666 list($tz_mod, $tz_hour, $tz_min) = array($match[8], $match[9], $match[10]);
667 // zero out the variables
668 if (!$tz_hour) {
669 $tz_hour = 0;
670 }
671 if (!$tz_min) {
672 $tz_min = 0;
673 }
674 $offset_secs = (($tz_hour * 60) + $tz_min) * 60;
675 // is timezone ahead of GMT? then subtract offset
676 if ($tz_mod == '+') {
677 $offset_secs *= -1;
678 }
679 $epoch += $offset_secs;
680 }
681 return $epoch;
682 }
683 else {
684 return FALSE;
685 }
686 }
687
688 /**
689 * Parse a feed and store its items.
690 *
691 * @param $data
692 * The feed data.
693 * @param $feed
694 * An associative array describing the feed to be parsed.
695 * @return
696 * 0 on error, 1 otherwise.
697 */
698 function aggregator_parse_feed(&$data, $feed) {
699 global $items, $image, $channel;
700
701 // Unset the global variables before we use them:
702 unset($GLOBALS['element'], $GLOBALS['item'], $GLOBALS['tag']);
703 $items = array();
704 $image = array();
705 $channel = array();
706
707 // parse the data:
708 $xml_parser = drupal_xml_parser_create($data);
709 xml_set_element_handler($xml_parser, 'aggregator_element_start', 'aggregator_element_end');
710 xml_set_character_data_handler($xml_parser, 'aggregator_element_data');
711
712 if (!xml_parse($xml_parser, $data, 1)) {
713 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);
714 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');
715 return 0;
716 }
717 xml_parser_free($xml_parser);
718
719 // We reverse the array such that we store the first item last, and the last
720 // item first. In the database, the newest item should be at the top.
721 $items = array_reverse($items);
722
723 // Initialize variables.
724 $title = $link = $author = $description = $guid = NULL;
725 foreach ($items as $item) {
726 unset($title, $link, $author, $description, $guid);
727
728 // Prepare the item:
729 foreach ($item as $key => $value) {
730 $item[$key] = trim($value);
731 }
732
733 // Resolve the item's title. If no title is found, we use up to 40
734 // characters of the description ending at a word boundary but not
735 // splitting potential entities.
736 if (!empty($item['TITLE'])) {
737 $title = $item['TITLE'];
738 }
739 elseif (!empty($item['DESCRIPTION'])) {
740 $title = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", truncate_utf8($item['DESCRIPTION'], 40));
741 }
742 else {
743 $title = '';
744 }
745
746 // Resolve the items link.
747 if (!empty($item['LINK'])) {
748 $link = $item['LINK'];
749 }
750 else {
751 $link = $feed['link'];
752 }
753 $guid = isset($item['GUID']) ? $item['GUID'] : '';
754
755 // Atom feeds have a CONTENT and/or SUMMARY tag instead of a DESCRIPTION tag.
756 if (!empty($item['CONTENT:ENCODED'])) {
757 $item['DESCRIPTION'] = $item['CONTENT:ENCODED'];
758 }
759 else if (!empty($item['SUMMARY'])) {
760 $item['DESCRIPTION'] = $item['SUMMARY'];
761 }
762 else if (!empty($item['CONTENT'])) {
763 $item['DESCRIPTION'] = $item['CONTENT'];
764 }
765
766 // Try to resolve and parse the item's publication date. If no date is
767 // found, we use the current date instead.
768 $date = 'now';
769 foreach (array('PUBDATE', 'DC:DATE', 'DCTERMS:ISSUED', 'DCTERMS:CREATED', 'DCTERMS:MODIFIED', 'ISSUED', 'CREATED', 'MODIFIED', 'PUBLISHED', 'UPDATED') as $key) {
770 if (!empty($item[$key])) {
771 $date = $item[$key];
772 break;
773 }
774 }
775
776 $timestamp = strtotime($date); // As of PHP 5.1.0, strtotime returns FALSE on failure instead of -1.
777 if ($timestamp <= 0) {
778 $timestamp = aggregator_parse_w3cdtf($date); // Returns FALSE on failure
779 if (!$timestamp) {
780 $timestamp = time(); // better than nothing
781 }
782 }
783
784 // Save this item. Try to avoid duplicate entries as much as possible. If
785 // we find a duplicate entry, we resolve it and pass along its ID is such
786 // that we can update it if needed.
787 if (!empty($guid)) {
788 $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND guid = '%s'", $feed['fid'], $guid));
789 }
790 else if ($link && $link != $feed['link'] && $link != $feed['url']) {
791 $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND link = '%s'", $feed['fid'], $link));
792 }
793 else {
794 $entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND title = '%s'", $feed['fid'], $title));
795 }
796 $item += array('AUTHOR' => '', 'DESCRIPTION' => '');
797 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));
798 }
799
800 // Remove all items that are older than flush item timer.
801 $age = time() - variable_get('aggregator_clear', 9676800);
802 $result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
803
804 $items = array();
805 $num_rows = FALSE;
806 while ($item = db_fetch_object($result)) {
807 $items[] = $item->iid;
808 $num_rows = TRUE;
809 }
810 if ($num_rows) {
811 db_query('DELETE FROM {aggregator_category_item} WHERE iid IN ('. implode(', ', $items) .')');
812 db_query('DELETE FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
813 }
814
815 return 1;
816 }
817
818 /**
819 * Add/edit/delete an aggregator item.
820 *
821 * @param $edit
822 * An associative array describing the item to be added/edited/deleted.
823 */
824 function aggregator_save_item($edit) {
825 if ($edit['iid'] && $edit['title']) {
826 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']);
827 }
828 else if ($edit['iid']) {
829 db_query('DELETE FROM {aggregator_item} WHERE iid = %d', $edit['iid']);
830 db_query('DELETE FROM {aggregator_category_item} WHERE iid = %d', $edit['iid']);
831 }
832 else if ($edit['title'] && $edit['link']) {
833 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']);
834 $edit['iid'] = db_last_insert_id('aggregator_item', 'iid');
835 // file the items in the categories indicated by the feed
836 $categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
837 while ($category = db_fetch_object($categories)) {
838 db_query('INSERT INTO {aggregator_category_item} (cid, iid) VALUES (%d, %d)', $category->cid, $edit['iid']);
839 }
840 }
841 }
842
843 /**
844 * Load an aggregator feed.
845 *
846 * @param $fid
847 * The feed id.
848 * @return
849 * An associative array describing the feed.
850 */
851 function aggregator_feed_load($fid) {
852 static $feeds;
853 if (!isset($feeds[$fid])) {
854 $feeds[$fid] = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', $fid));
855 }
856 return $feeds[$fid];
857 }
858
859 /**
860 * Load an aggregator category.
861 *
862 * @param $cid
863 * The category id.
864 * @return
865 * An associative array describing the category.
866 */
867 function aggregator_category_load($cid) {
868 static $categories;
869 if (!isset($categories[$cid])) {
870 $categories[$cid] = db_fetch_array(db_query('SELECT * FROM {aggregator_category} WHERE cid = %d', $cid));
871 }
872 return $categories[$cid];
873 }
874
875 /**
876 * Format an individual feed item for display in the block.
877 *
878 * @param $item
879 * The item to be displayed.
880 * @param $feed
881 * Not used.
882 * @return
883 * The item HTML.
884 * @ingroup themeable
885 */
886 function theme_aggregator_block_item($item, $feed = 0) {
887 global $user;
888
889 $output = '';
890 if ($user->uid && module_exists('blog') && user_access('create blog entries')) {
891 if ($image = theme('image', 'misc/blog.png', t('blog it'), t('blog it'))) {
892 $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>';
893 }
894 }
895
896 // Display the external link to the item.
897 $output .= '<a href="'. check_url($item->link) .'">'. check_plain($item->title) ."</a>\n";
898
899 return $output;
900 }
901
902 /**
903 * Safely render HTML content, as allowed.
904 *
905 * @param $value
906 * The content to be filtered.
907 * @return
908 * The filtered content.
909 */
910 function aggregator_filter_xss($value) {
911 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));
912 }
913
914 /**
915 * Helper function for drupal_map_assoc.
916 *
917 * @param $count
918 * Items count.
919 * @return
920 * Plural-formatted "@count items"
921 */
922 function _aggregator_items($count) {
923 return format_plural($count, '1 item', '@count items');
924 }