Mercurial > defr > drupal > core
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 } |