/[drupal]/contributions/modules/aggregator2/aggregator2.module
ViewVC logotype

Contents of /contributions/modules/aggregator2/aggregator2.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.43 - (show annotations) (download) (as text)
Thu Jul 20 10:52:04 2006 UTC (3 years, 4 months ago) by ahwayakchih
Branch: MAIN
CVS Tags: HEAD
Changes since 1.42: +2 -2 lines
File MIME type: text/x-php
Aggregator2:
- don't set feed->image to empty string if it's already set to some
value.
1 <?php
2 /* $Id: aggregator2.module,v 1.42 2006/07/20 10:46:58 ahwayakchih Exp $ */
3
4 /**
5 * @file
6 * Used to aggregate syndicated content (RSS, RDF, Atom).
7 * Sponsored by Sandro Feuillet.
8 * Sponsored by John Bransford.
9 * Sponsored by Development Seed.
10 */
11
12 /*
13 Copyright (C) 2005 by Marcin Konicki <ahwayakchih@gmail.com> and Sandro Feuillet <feuillet aat fastmail ddot fm>
14 Based on parts of Node Aggregator module by Bèr Kessels <ber aat webschuur ddot com>,
15 and Aggregator module by Drupal team - http://www.drupal.org
16 Also depends on other modules from Drupal basic distribution and, in some cases, contains parts of their code.
17
18 This program is free software; you can redistribute it and/or modify
19 it under the terms of the GNU General Public License.
20 This program is distributed in the hope that it will be useful,
21 but WITHOUT ANY WARRANTY.
22
23 See the LICENSE file for more details.
24 */
25
26 /**
27 * Some definitions, to make code a bit more readable, and easier to maintain
28 */
29 define("AGGREGATOR2_PERM_CREATE_FEED", "create feeds");
30 define("AGGREGATOR2_PERM_EDIT_OWN_FEED", "edit own feeds");
31 define("AGGREGATOR2_PERM_EDIT_OWN_ITEM", "edit own feed items");
32 define("AGGREGATOR2_PERM_REFRESH_OWN_FEED", "refresh own feed items");
33
34 define("AGGREGATOR2_PERM_ACCESS_FEED", "access feeds");
35 define("AGGREGATOR2_PERM_ACCESS_ITEM", "access feed items");
36
37 define("AGGREGATOR2_ITEM_DATE_SNIFFED", 0);
38 define("AGGREGATOR2_ITEM_DATE_CURRENT", 1);
39
40 define("AGGREGATOR2_SHOW_LINK_ALWAYS", 0);
41 define("AGGREGATOR2_SHOW_LINK_NEVER", 1);
42 define("AGGREGATOR2_SHOW_LINK_TEASER_ONLY", 2);
43 define("AGGREGATOR2_SHOW_LINK_PAGE_ONLY", 3);
44
45 // OR'ed deleting mode
46 define("AGGREGATOR2_ITEM_DELETE_ANY", 0);
47 define("AGGREGATOR2_ITEM_DELETE_UNPUBLISHED", 1);
48
49
50 static $AGGREGATOR2_REFRESH_FEED_RUNNING = FALSE;
51
52 /**
53 * Implementation of hook_help().
54 */
55 function aggregator2_help($section) {
56 switch ($section) {
57 case 'admin/help#aggregator2':
58 return t('
59 <h3>Background</h3>
60 Thousands of sites (particularly news sites and weblogs) publish their latest headlines and/or stories in a machine-readable format so that other sites can easily link to them. This content is usually in the form of an <a href="http://blogs.law.harvard.edu/tech/rss">RSS</a> feed (which is an XML-based syndication standard). Aggregator2 module can download such feeds, and add news from them to Your site.<br />
61 <h3>Setting up Aggregator2</h3>
62 <p><b>1.</b> First You need to setup permissions for aggregator2 module on %admin-&gt;%access page:
63 <ul>
64 <li><b>Access news</b> - User can view news.</li>
65 <li><b>Administer News Feed</b> - User can contribute/administer Aggregator2 News feed.</li>
66 <li><b>Administer News Items</b> - User can contribute/adminster News Items.</li>
67 </ul>
68 </p>
69 <p><b>2.</b> Next, if You want to associate categories with aggregator2 content, go to %admin-&gt;%categories page and edit one of already exiting vocabularies or create new one, and make sure under "types" that aggregator2 news feed and aggregator2 news feed item are checked.</p>
70 <h3>Creating Content</h3>
71 <p>After setting up Aggregator2 module, You can start generating content with it. For that You have to create %feed.
72 <ul>
73 <li><b>Title</b> - Title of the News Feed.</li>
74 <li><b>Categories</b> - Select categories that you want the feed itself to be categorized in.</li>
75 <li><b>URL</b> - url of RSS feed.</li>
76 <li><b>Update interval</b> - Amount of time before news feed is updated.</li>
77 <li><b>Discard Feed Items older than</b> - Whether or not you want the feed items to be discarded after a certain time interval.</li>
78 <li><b>Item Categories</b> - Category or Categories you want the aggregated feed items to be associated with.</li>
79 </ul>
80 For the feed to update automatically you must run %cron on a regular basis.
81 </p>
82 <h3>Administrating Content</h3>
83 <p>You can administer aggregator2 feeds and aggregator2 items as You administer any other drupal content. Just go to %admin-&gt;%content and use "edit" link at node You want to edit. Or You can use "edit" tab, when viewing specific node.</p>
84 ', array('%admin' => l(t('Administer'), 'admin'), '%access' => l(t('access control'), 'admin/access'), '%categories' => l(t('categories'), 'admin/taxonomy'), '%feed' => l(t('aggregator2 news feed'), 'node/add/aggregator2_feed'), '%cron' => l('cron.php', 'admin/help/system#cron'), '%content' => l(t('content'), 'admin/node')));
85 case 'admin/modules#description':
86 return t('Aggregates syndicated content (RSS and ATOM formats) as regular Drupal content.');
87 case 'admin/aggregator2':
88 return '<p>' . t('Thousands of sites (particularly news sites and weblogs) publish their latest headlines and/or stories in a machine-readable format so that other sites can easily link to them. This content is usually in the form of an %rssurl feed (which is an XML-based syndication standard).', array('%rssurl' => '<a href="http://blogs.law.harvard.edu/tech/rss">RSS</a>') ) . '</p><p>'. l(t('create news feed'), 'node/add/aggregator2_feed') .'</p>';
89 case 'node/add#aggregator2_feed':
90 return t('A news feed is a source of news from other site(s). If you add one from this page aggregator2 module will automatically add news feed items (nodes) in configured intervals. You will also be able to edit those items later. The URL is the full path to the RSS feed file. For the feed to update automatically you must run "cron.php" on a regular basis. If you already have a feed with the URL you are planning to use, the system will not accept another feed with the same URL.');
91 case 'node/add#aggregator2_item':
92 return t('A news feed item is an item that is part of a feed. They are added automatically when feed is updated.');
93 }
94 }
95
96 /**
97 * Implementation of hook_perm().
98 */
99 function aggregator2_perm() {
100 return array(AGGREGATOR2_PERM_CREATE_FEED, AGGREGATOR2_PERM_EDIT_OWN_FEED, AGGREGATOR2_PERM_EDIT_OWN_ITEM, AGGREGATOR2_PERM_REFRESH_OWN_FEED, AGGREGATOR2_PERM_ACCESS_FEED, AGGREGATOR2_PERM_ACCESS_ITEM);
101 }
102
103 /**
104 * Implementation of hook_node_info().
105 */
106 function aggregator2_node_info() {
107 return array(
108 'aggregator2_feed' => array('name' => t('feed'), 'base' => 'aggregator2_feed'),
109 'aggregator2_item' => array('name' => t('feed item'), 'base' => 'aggregator2_item')
110 );
111 }
112
113 /**
114 * Implementation of hook_menu().
115 */
116 function aggregator2_menu($may_cache) {
117 $items = array();
118
119 if ($may_cache) {
120 $items[] = array('path' => 'node/add/aggregator2_feed', 'title' => t('feed'),
121 'access' => user_access(AGGREGATOR2_PERM_CREATE_FEED));
122
123 $items[] = array('path' => 'aggregator2/sources', 'title' => t('aggregator2'),
124 'callback' => 'aggregator2_page_default', 'access' => user_access(AGGREGATOR2_PERM_ACCESS_FEED),
125 'type' => MENU_CALLBACK);
126
127 $items[] = array('path' => 'admin/aggregator2', 'title' => t('aggregator2'),
128 'callback' => 'aggregator2_admin_overview', 'access' => user_access(AGGREGATOR2_PERM_CREATE_FEED));
129
130 // TODO: find a nice way to allow refresh only to feed owner
131 $items[] = array('path' => 'admin/aggregator2/refresh', 'title' => t('aggregator2'),
132 'callback' => 'aggregator2_admin_refresh_feed', 'access' => user_access(AGGREGATOR2_PERM_REFRESH_OWN_FEED),
133 'type' => MENU_CALLBACK);
134
135 // TODO: find a nice way to allow remove only to items owner?
136 $items[] = array('path' => 'admin/aggregator2/remove', 'title' => t('remove items'),
137 'callback' => 'aggregator2_admin_remove_feed_items', 'access' => user_access(AGGREGATOR2_PERM_EDIT_OWN_ITEM),
138 'type' => MENU_CALLBACK);
139
140 $items[] = array('path' => 'aggregator2/opml', 'title' => t('opml'),
141 'callback' => 'aggregator2_page_opml', 'access' => user_access(AGGREGATOR2_PERM_ACCESS_FEED),
142 'type' => MENU_CALLBACK);
143 }
144
145 return $items;
146 }
147
148 /**
149 * Implementation of hook_settings().
150 */
151 function aggregator2_settings() {
152 $form = array();
153
154 $form['agg2_create_feed_blocks'] = array(
155 '#type' => 'checkbox',
156 '#title' => t('Create drupal blocks for each feed'),
157 '#default_value' => variable_get('agg2_create_feed_blocks', 0),
158 '#description' => t('If enabled, aggragator2 will create block for each feed. Such block still needs to be enabled on %link page.', array('%link' => l('admin/block', 'admin/block')))
159 );
160 $form['agg2_show_feed_link'] = array(
161 '#type' => 'checkbox',
162 '#title' => t('Show link to feed with each item'),
163 '#default_value' => variable_get('agg2_show_feed_link', 0),
164 '#description' => t('If enabled, aggragator2 will show "source" link with each item. It will point to item\'s feed node.')
165 );
166 $form['agg2_show_item_link'] = array(
167 '#type' => 'checkbox',
168 '#title' => t('Show link to items with each feed'),
169 '#default_value' => variable_get('agg2_show_item_link', 0),
170 '#description' => t('If enabled, aggragator2 will show "items" link with each feed. It will point to feed node list of all items.')
171 );
172 $form['agg2_original_links'] = array(
173 '#type' => 'checkbox',
174 '#title' => t('Use link to origin source whenever possible'),
175 '#default_value' => variable_get('agg2_original_links', 0),
176 '#description' => t('If enabled, aggragator2 will use data from "source" tags instead of "link" tags. That will make "full article" link point to site which first published article, instead to site from which article was aggregated. Unfortunetly many sites do not use "source" tags, so often links will still point to site from which feeed was aggregated.')
177 );
178
179 // how many feeds to update at one cron run
180 $feed_count = drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 100));
181 $feed_count['9999999'] = t('All');
182 $form['agg2_cron_feed_count'] = array (
183 '#type' => 'select',
184 '#title' => t('Number of feeds to update at a time'),
185 '#default_value' => variable_get('agg2_cron_feed_count', 10),
186 '#options' => $feed_count,
187 '#description' => t('Select how many feeds can be updated at one cron run.')
188 );
189
190 // how long intervals to use between node_save()/node_delete() calls
191 $sleep_interval = drupal_map_assoc(array(0, 1, 2, 3, 4, 5));
192 $form['agg2_sleep_interval'] = array (
193 '#type' => 'select',
194 '#title' => t('Interval between node updates'),
195 '#default_value' => variable_get('agg2_sleep_interval', 3),
196 '#options' => $sleep_interval,
197 '#description' => t('Select how many seconds aggregator2 should wait before trying to save/delete next node.')
198 );
199
200 $form['agg2_blacklist_url'] = array (
201 '#type' => 'textarea',
202 '#title' => t('Blacklist URLs'),
203 '#default_value' => variable_get('agg2_blacklist_url', ''),
204 '#rows' => 5,
205 '#description' => t('One entry per line. You can enter full URLs or domain names only. You can also enter regular expression (find out more about what it is at %link. more examples can be found also at %link2). For example "http://some.url.com/some/page.html" will blacklist that specific URL. ".url.com" will blacklist all URLs from url.com domain, and all it\'s subdomains. "some.url.com" will blacklist all URLs from "some" subdomain. "/^ftp\:\/\//" will blacklist any ftp:// URL. Feed which has URL which matches any of the rules on blacklist will be blocked. Items which have link pointing to URL which matches any of the rules from blacklist will not be created.', array('%link' => l('http://www.php.net/manual/en/reference.pcre.pattern.syntax.php', 'http://www.php.net/manual/en/reference.pcre.pattern.syntax.php'), '%link2' => l('http://www.php.net/manual/en/function.preg-match.php', 'http://www.php.net/manual/en/function.preg-match.php')))
206 );
207
208 // Globally change settings for all feeds - useful if one wants to change setting without need to edit each feed
209 $form['once_click_change'] = array(
210 '#type' => 'fieldset',
211 '#title' => t('Change all news feeds with one click'),
212 '#collapsible' => TRUE,
213 '#collapsed' => TRUE
214 );
215 if ($clear_items = variable_get('agg2_clear_items', 0)) {
216 db_query("UPDATE {aggregator2_feed} SET clear_items = %d", $clear_items);
217 variable_set('agg2_clear_items', 0);
218 }
219 $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval');
220 $period['0'] = t('Do not change');
221 $period['1000000000'] = t('Never');
222 $form['once_click_change']['agg2_clear_items'] = array (
223 '#type' => 'select',
224 '#title' => t('Discard feed items older than'),
225 '#default_value' => 0,
226 '#options' => $period,
227 '#description' => t('The time feed items should be kept. Older items will be automatically discarded. Requires crontab.')
228 );
229
230 return $form;
231 }
232
233 /**
234 * Implementation of hook_form_alter().
235 */
236 function aggregator2_form_alter($form_id, &$form) {
237 if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id && $form['type']['#value'] == 'aggregator2_feed') {
238 $form['workflow']['agg2_feed_defs'] = array(
239 '#type' => 'fieldset',
240 '#title' => t('Default feed options'),
241 '#collapsible' => TRUE,
242 '#collapsed' => FALSE
243 );
244 $form['workflow']['agg2_feed_defs']['agg2_feed_freezed'] = array(
245 '#type' => 'checkbox',
246 '#title' => t('Freeze'),
247 '#default_value' => variable_get('agg2_feed_freezed', 0)
248 );
249 $form['workflow']['agg2_feed_defs']['agg2_feed_apply_old'] = array(
250 '#type' => 'checkbox',
251 '#title' => t('Apply changes to already existing items after feed is re-edited'),
252 '#default_value' => variable_get('agg2_feed_apply_old', 0)
253 );
254 $form['workflow']['agg2_feed_defs']['agg2_update_items'] = array(
255 '#type' => 'checkbox',
256 '#title' => t('Update existing items'),
257 '#default_value' => variable_get('agg2_update_items', 1)
258 );
259 $form['workflow']['agg2_feed_defs']['agg2_item_status'] = array(
260 '#type' => 'checkbox',
261 '#title' => t('Publish new items'),
262 '#default_value' => variable_get('agg2_item_status', 1)
263 );
264 $form['workflow']['agg2_feed_defs']['agg2_item_delete_mode'] = array(
265 '#type' => 'checkbox',
266 '#title' => t('Discard only items not published currently'),
267 '#default_value' => variable_get('agg2_item_delete_mode', AGGREGATOR2_ITEM_DELETE_UNPUBLISHED)
268 );
269 $form['workflow']['agg2_feed_defs']['agg2_guid_items'] = array(
270 '#type' => 'checkbox',
271 '#title' => t('Create GUID for items'),
272 '#default_value' => variable_get('agg2_guid_items', 1)
273 );
274 $promoted = drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30));
275 $promoted['0'] = t('None');
276 $promoted['1000000000'] = t('All');
277 $form['workflow']['agg2_feed_defs']['agg2_promoted_items'] = array (
278 '#type' => 'select',
279 '#title' => t('By default promote items'),
280 '#default_value' => variable_get('agg2_promoted_items', 0),
281 '#options' => $promoted,
282 '#description' => t('Select how many of aggregated items should be promoted to front page. When new items are created old ones will be taken out from front page.')
283 );
284 $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval');
285 $period['1000000000'] = t('Never');
286 $form['workflow']['agg2_feed_defs']['agg2_clear_items'] = array (
287 '#type' => 'select',
288 '#title' => t('By default discard feed items older than'),
289 '#default_value' => variable_get('agg2_clear_items', 86400),
290 '#options' => $period,
291 '#description' => t('The time feed items should be kept. Older items will be automatically discarded. Requires crontab.')
292 );
293 $form['workflow']['agg2_feed_defs']['agg2_item_date_source'] = array (
294 '#type' => 'select',
295 '#title' => t('Item date source'),
296 '#default_value' => variable_get('agg2_item_date_source', AGGREGATOR2_ITEM_DATE_SNIFFED),
297 '#options' => array(
298 AGGREGATOR2_ITEM_DATE_SNIFFED => t('Feed'),
299 AGGREGATOR2_ITEM_DATE_CURRENT => t('Current')
300 ),
301 '#description' => t('Select which date will be used for aggregated items. If "Feed" is selected, Aggregator2 module will try to find date in feed, and if not found, current date will be used. If "Current" is selected, date of creation of items will always be set to the one at which items are aggregated.')
302 );
303 $form['workflow']['agg2_feed_defs']['agg2_item_show_link'] = array (
304 '#type' => 'select',
305 '#title' => t('Show "full article"/"visit site" link'),
306 '#default_value' => variable_get('agg2_item_show_link', AGGREGATOR2_SHOW_LINK_PAGE_ONLY),
307 '#options' => array(
308 AGGREGATOR2_SHOW_LINK_ALWAYS => t('Always'),
309 AGGREGATOR2_SHOW_LINK_NEVER => t('Do not display'),
310 AGGREGATOR2_SHOW_LINK_TEASER_ONLY => t('Only with teaser'),
311 AGGREGATOR2_SHOW_LINK_PAGE_ONLY => t('Only on full page')
312 ),
313 '#description' => t('Select place(s) where link to full article (for news items) or visit site (for news feeds) will be shown.')
314 );
315 return $form;
316 }
317 else if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id && $form['type']['#value'] == 'aggregator2_item') {
318 // nothing needed here for now ;]
319 }
320 }
321
322 /**
323 * Implementation of hook_block().
324 *
325 * Generates news feeds blocks for display.
326 */
327 function aggregator2_block($op = 'list', $delta = 0) {
328 if (variable_get('agg2_create_feed_blocks', 0) == 1) {
329 if ($op == 'list') {
330 $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n WHERE n.type = \'aggregator2_feed\' AND n.status = 1'));
331 while ($block = db_fetch_object($result)) {
332 $blocks[$block->nid]['info'] = $block->title;
333 }
334 $blocks['sources']['info'] = t('Latest sources');
335 return $blocks;
336 }
337 else if ($op == 'view') {
338 if ($block = cache_get('aggregator2:block:'.$delta)) {
339 return unserialize($block->data);
340 }
341 else if ($delta != 'sources') {
342 $feed = db_fetch_object(db_query('SELECT n.nid, n.title FROM {node} n WHERE n.nid = %d', $delta));
343 if ($feed->nid) {
344 $block = array();
345 $block['subject'] = $feed->title;
346 $items = db_query('SELECT n.nid, n.title FROM {node} n, {aggregator2_item} a WHERE n.nid = a.nid AND a.fid = %d ORDER BY n.created DESC, n.title LIMIT 10', $feed->nid);
347 $block['content'] = node_title_list($items);
348 cache_set('aggregator2:block:'.$delta, serialize($block));
349 return $block;
350 }
351 }
352 else {
353 $block = array();
354 $items = array();
355 $block['subject'] = t('Latest sources');
356 $result = db_query('SELECT n.nid, n.title FROM {node} n WHERE n.type = \'aggregator2_feed\' AND n.status = 1 ORDER BY n.changed DESC LIMIT 10');
357 while ($temp = db_fetch_object($result)) {
358 $items[] = l($temp->title, 'aggregator2/sources/'.$temp->nid);
359 }
360 $block['content'] = theme('node_list', $items, NULL);
361 $block['content'] .= l(t('all sources'), 'aggregator2/sources');
362 cache_set('aggregator2:block:'.$delta, serialize($block));
363 return $block;
364 }
365 }
366 }
367 }
368
369 /**
370 * Implementation of hook_nodeapi().
371 */
372 function aggregator2_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
373 switch ($op) {
374 case 'rss item':
375 if ($node->type == 'aggregator2_item') {
376 return array(array('key' => 'source',
377 'attributes' => array('url' => ($node->source_xml ? $node->source_xml : $node->feed_url)),
378 'value' => check_plain(($node->source_title ? $node->source_title : $node->feed_title))),
379 array('key' => 'dc:source',
380 'value' => ($node->source_link ? $node->source_link : $node->link)));
381 }
382 if ($node->type == 'aggregator2_feed') {
383 return array(array('key' => 'source',
384 'attributes' => array('url' => $node->url),
385 'value' => check_plain($node->title)),
386 array('key' => 'dc:source',
387 'value' => $node->link));
388 }
389 break;
390 }
391 }
392
393 /**
394 * Implementation of hook_link().
395 */
396 function aggregator2_link($type, $node = NULL, $teaser = FALSE) {
397 $links = array();
398
399 if ($type == 'node' && $node != NULL) {
400 if (($node->item_show_link == AGGREGATOR2_SHOW_LINK_ALWAYS) ||
401 ($teaser && $node->item_show_link == AGGREGATOR2_SHOW_LINK_TEASER_ONLY) ||
402 (!$teaser && $node->item_show_link == AGGREGATOR2_SHOW_LINK_PAGE_ONLY)) {
403 if ($node->type == 'aggregator2_item') {
404 $links[] = theme('aggregator2_link_full_article', $node);
405 }
406 else if ($node->type == 'aggregator2_feed') {
407 $links[] = theme('aggregator2_link_visit_site', $node);
408 }
409 }
410 global $user;
411 if ($node->type == 'aggregator2_feed') {
412 if ((user_access(AGGREGATOR2_PERM_REFRESH_OWN_FEED) && ($user->uid == $node->uid)) || user_access('administer nodes')) {
413 $links[] = l(t('refresh items'), "admin/aggregator2/refresh/{$node->nid}");
414 }
415 if ((user_access(AGGREGATOR2_PERM_EDIT_OWN_ITEM) && ($user->uid == $node->uid)) || user_access('administer nodes')) {
416 $links[] = l(t('remove items'), "admin/aggregator2/remove/{$node->nid}");
417 }
418 if (variable_get('agg2_show_item_link', 0)) {
419 $links[] = l(t('view items'), "aggregator2/sources/{$node->nid}");
420 }
421 }
422 else if ($node->type == 'aggregator2_item' && variable_get('agg2_show_feed_link', 0)) {
423 $links[] = l(t('source'), "aggregator2/sources/{$node->fid}");
424 }
425 }
426
427 return $links;
428 }
429
430 /**
431 * Implementation of hook_cron().
432 *
433 * Checks news feeds for updates once their refresh interval has elapsed.
434 */
435 function aggregator2_cron() {
436 global $user;
437 $old_user = $user;
438
439 // check how many feed nodew we can update at a time
440 $limit = variable_get('aggregator2_cron_feed_count', 10);
441 if (is_numeric($limit) && $limit > -1) {
442 $limit = 'LIMIT '. $limit;
443 }
444 else {
445 $limit = '';
446 }
447
448 $updated_feeds = array();
449 $result = db_query('SELECT nid FROM {aggregator2_feed} WHERE freezed = 0 AND checked + refresh < %d ORDER BY checked ASC '. $limit, time());
450 while ($temp = db_fetch_array($result)) {
451 $feed = node_load($temp['nid']);
452 // Fake login
453 if ($feed->uid != $user->uid) {
454 $user = user_load(array('uid' => $feed->uid, 'status' => 1));
455 }
456 // Check again if it's correct uid and only then refresh feed
457 if ($feed->uid == $user->uid) {
458 aggregator2_refresh($feed);
459 $updated_feeds[$feed->nid] = array($feed->clear_items, $feed->item_delete_mode);
460 }
461 }
462
463 // Now delete old items as admin (to make it faster - we don't really need to delete node as owner)
464 if ($user->uid != 1) {
465 $user = user_load(array('uid' => 1));
466 }
467 foreach ($updated_feeds as $nid => $args) {
468 aggregator2_remove_old_items($nid, $args[0], $args[1]);
469 }
470
471 // Now "logout"
472 if ($user->uid != $old_user->uid) {
473 $user = $old_user;
474 }
475 }
476
477 /**
478 * Implementation of hook_prepare().
479 */
480 function aggregator2_feed_prepare(&$node, $teaser = FALSE) {
481 if (!$node->nid) {
482 $node->refresh = 3600;
483 $node->clear_items = variable_get('agg2_clear_items', 86400);
484 }
485
486 // Remove "empty" terms
487 $temp = array();
488 if (is_array($node->feed_item_taxonomy)) {
489 foreach ($node->feed_item_taxonomy as $tid) {
490 if ($tid) {
491 $temp[] = $tid;
492 }
493 }
494 }
495 if (count($temp) > 0) {
496 $node->feed_item_taxonomy = $temp;
497 }
498
499 // Overwrite only if it's not saved from cron run, so it not gets freezed after each update :)
500 // TODO: if there will be some other module saving nodes, it will trigger overwriting values. Find way to workaround that?
501 global $AGGREGATOR2_REFRESH_FEED_RUNNING;
502 if (!$AGGREGATOR2_REFRESH_FEED_RUNNING && (!user_access('administer nodes') || !$node->nid)) {
503 $node->update_items = variable_get('agg2_update_items', 1);
504 $node->item_status = variable_get('agg2_item_status', 1);
505 $node->item_delete_mode = variable_get('agg2_item_delete_mode', AGGREGATOR2_ITEM_DELETE_UNPUBLISHED);
506 $node->clear_items = variable_get('agg2_clear_items', 86400);
507 $node->promoted_items = variable_get('agg2_promoted_items', 0);
508 $node->freezed = variable_get('agg2_feed_freezed', 0);
509 $node->change_existing_items = variable_get('agg2_feed_apply_old', 0);
510 $node->guid_items = variable_get('agg2_guid_items', 1);
511 $node->item_date_source = variable_get('agg2_item_date_source', AGGREGATOR2_ITEM_DATE_SNIFFED);
512 $node->item_show_link = variable_get('agg2_item_show_link', AGGREGATOR2_SHOW_LINK_PAGE_ONLY);
513 }
514
515 return $node;
516 }
517
518 /**
519 * Implementation of hook_form().
520 */
521 function aggregator2_feed_form(&$node) {
522 $form = array();
523
524 if (user_access('administer nodes')) {
525 $form['admin'] = array(
526 '#type' => 'fieldset',
527 '#title' => t('Feed options'),
528 '#collapsible' => TRUE,
529 '#collapsed' => FALSE,
530 '#weight' => 0
531 );
532 $form['admin']['freezed'] = array(
533 '#type' => 'checkbox',
534 '#title' => t('Freeze'),
535 '#default_value' => $node->freezed,
536 '#description' => t('If set, aggragator2 will not create new items, or update old ones, for this feed.')
537 );
538 $form['admin']['change_existing_items'] = array(
539 '#type' => 'checkbox',
540 '#title' => t('Apply changes to already existing items after feed is re-edited'),
541 '#default_value' => $node->change_existing_items,
542 '#description' => t('If set, changes to input format and item categories will be applied also to already existing items.')
543 );
544 $form['admin']['update_items'] = array(
545 '#type' => 'checkbox',
546 '#title' => t('Update existing items'),
547 '#default_value' => $node->update_items,
548 '#description' => t('If enabled, aggragator2 will update already existing items, overwriting any changes done between cron runs.')
549 );
550 $form['admin']['item_status'] = array(
551 '#type' => 'checkbox',
552 '#title' => t('Publish new items'),
553 '#default_value' => $node->item_status,
554 '#description' => t('If enabled, aggragator2 will mark each new item as published.')
555 );
556 $form['admin']['item_delete_mode'] = array(
557 '#type' => 'checkbox',
558 '#title' => t('Discard only items not published currently'),
559 '#default_value' => $node->item_delete_mode,
560 '#description' => t('The time feed items should be kept. Older items will be automatically discarded. Requires crontab.')
561 );
562 $form['admin']['guid_items'] = array(
563 '#type' => 'checkbox',
564 '#title' => t('Create GUID for items'),
565 '#default_value' => $node->guid_items,
566 '#description' => t('If enabled, aggragator2 will try to generate GUID for each item. Use this ONLY if aggregated items do not contain GUID tag and their LINK is not unique (ie. more than one item has the same link).')
567 );
568 $promoted = drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30));
569 $promoted['0'] = t('None');
570 $promoted['1000000000'] = t('All');
571 $form['admin']['promoted_items'] = array (
572 '#type' => 'select',
573 '#title' => t('By default promote items'),
574 '#default_value' => $node->promoted_items,
575 '#options' => $promoted,
576 '#description' => t('Select how many of aggregated items should be promoted to front page. When new items are created old ones will be taken out from front page.')
577 );
578 $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval');
579 $period['1000000000'] = t('Never');
580 $form['admin']['clear_items'] = array (
581 '#type' => 'select',
582 '#title' => t('By default discard feed items older than'),
583 '#default_value' => $node->clear_items,
584 '#options' => $period,
585 '#description' => t('The time feed items should be kept. Older items will be automatically discarded. Requires crontab.')
586 );
587 $form['admin']['item_date_source'] = array (
588 '#type' => 'select',
589 '#title' => t('Item date source'),
590 '#default_value' => $node->item_date_source,
591 '#options' => array(
592 AGGREGATOR2_ITEM_DATE_SNIFFED => t('Feed'),
593 AGGREGATOR2_ITEM_DATE_CURRENT => t('Current')
594 ),
595 '#description' => t('Select which date will be used for aggregated items. If "Feed" is selected, Aggregator2 module will try to find date in feed, and if not found, current date will be used. If "Current" is selected, date of creation of items will always be set to the one at which items are aggregated.')
596 );
597 $form['admin']['item_show_link'] = array (
598 '#type' => 'select',
599 '#title' => t('Show "full article"/"visit site" link'),
600 '#default_value' => $node->item_show_link,
601 '#options' => array(
602 AGGREGATOR2_SHOW_LINK_ALWAYS => t('Always'),
603 AGGREGATOR2_SHOW_LINK_NEVER => t('Do not display'),
604 AGGREGATOR2_SHOW_LINK_TEASER_ONLY => t('Only with teaser'),
605 AGGREGATOR2_SHOW_LINK_PAGE_ONLY => t('Only on full page')
606 ),
607 '#description' => t('Select place(s) where link to full article (for news items) or visit site (for news feeds) will be shown.')
608 );
609
610 // don't allow regular user's to "steal fame" :)
611 $form['original_author'] = array(
612 '#type' => 'textfield',
613 '#title' => t('Original author'),
614 '#default_value' => $node->original_author,
615 '#size' => 60,
616 '#maxlength' => 60,
617 '#weight' => -101
618 );
619 // don't allow user to setup logo
620 $form['image'] = array(
621 '#type' => 'textfield',
622 '#title' => t('Logo-link HTML'),
623 '#default_value' => $node->image,
624 '#size' => 60,
625 '#maxlength' => 1024,
626 '#description' => t('Leave it blank to allow aggregator2 to auto-generate it. Use only full URL (including "http://" part too) to image so it does not break RSS/ATOM feed compatibility.'),
627 '#weight' => -96
628 );
629 }
630 else {
631 // don't allow regular user's to "steal fame" :)
632 $form['original_author'] = array(
633 '#type' => 'hidden',
634 '#value' => $node->original_author
635 );
636 // don't allow user to setup logo
637 $form['image'] = array(
638 '#type' => 'hidden',
639 '#value' => $node->image
640 );
641 }
642
643 $form['url'] = array(
644 '#type' => 'textfield',
645 '#title' => t('Feed URL'),
646 '#default_value' => $node->url,
647 '#size' => 60,
648 '#maxlength' => 250,
649 '#required' => TRUE,
650 '#weight' => -99
651 );
652 $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
653 $form['refresh'] = array (
654 '#type' => 'select',
655 '#title' => t('Update interval'),
656 '#default_value' => $node->refresh,
657 '#options' => $period,
658 '#description' => t('The refresh interval indicating how often you want to update this feed. Requires crontab.'),
659 '#weight' => -97
660 );
661
662 $form['title'] = array(
663 '#type' => 'textfield',
664 '#title' => t('Title'),
665 '#default_value' => $node->title,
666 '#required' => TRUE,
667 '#weight' => -100
668 );
669 $form['body'] = array(
670 '#type' => 'textarea',
671 '#title' => t('Description'),
672 '#default_value' => $node->body,
673 '#description' => t('Leave it blank to allow aggregator2 to use aggregated content for description.'),
674 '#weight' => -98
675 );
676 $form['format'] = filter_form($node->format);
677 $form['format']['#weight'] = 0;
678
679
680 // Now fake form to get taxonomy for items
681 $form['item_setup'] = array(
682 '#type' => 'fieldset',
683 '#title' => t('Item categories'),
684 '#collapsible' => TRUE,
685 '#collapsed' => FALSE,
686 '#weight' => 0
687 );
688 $fakeform = array('type' => array('#value' => 'aggregator2_item'), '#node' => new StdClass());
689 $fakeform['#node']->type = 'aggregator2_item';
690 $fakeform['#node']->taxonomy = $node->feed_item_taxonomy;
691 if (is_array($fakeform['#node']->taxonomy)) {
692 foreach ($fakeform['#node']->taxonomy as $id => $term) {
693 if (is_array($term)) {
694 foreach ($term as $vid => $value) {
695 $fakeform['#node']->taxonomy['tags'][$vid] = $value;
696 }
697 unset($fakeform['#node']->taxonomy[$id]);
698 }
699 }
700 }
701 taxonomy_form_alter('aggregator2_item_node_form', $fakeform);
702 $form['item_setup']['feed_item_taxonomy'] = $fakeform['taxonomy'];
703 unset($fakeform);
704
705 return $form;
706 }
707
708 /**
709 * Implementation of hook_form().
710 */
711 function aggregator2_item_form(&$node) {
712 $form = array();
713
714 if (user_access('administer nodes')) {
715 // don't allow regular user's to "steal fame" :)
716 $form['original_author'] = array(
717 '#type' => 'textfield',
718 '#title' => t('Original author'),
719 '#default_value' => $node->original_author,
720 '#size' => 60,
721 '#maxlength' => 60,
722 '#weight' => -101
723 );
724 $form['link'] = array(
725 '#type' => 'textfield',
726 '#title' => t('Link'),
727 '#default_value' => $node->link,
728 '#size' => 60,
729 '#maxlength' => 250,
730 '#weight' => -99
731 );
732 $form['source_link'] = array(
733 '#type' => 'textfield',
734 '#title' => t('Source Link'),
735 '#default_value' => $node->source_link,
736 '#size' => 60,
737 '#maxlength' => 250,
738 '#weight' => -95
739 );
740 $form['source_xml'] = array(
741 '#type' => 'textfield',
742 '#title' => t('Source XML'),
743 '#default_value' => $node->source_xml,
744 '#size' => 60,
745 '#maxlength' => 250,
746 '#weight' => -95
747 );
748 $form['source_title'] = array(
749 '#type' => 'textfield',
750 '#title' => t('Source Title'),
751 '#default_value' => $node->source_title,
752 '#size' => 60,
753 '#maxlength' => 250,
754 '#weight' => -94
755 );
756 }
757 else {
758 // don't allow regular user's to "steal fame" :)
759 $form['original_author'] = array(
760 '#type' => 'hidden',
761 '#value' => $node->original_author
762 );
763 $form['link'] = array(
764 '#type' => 'hidden',
765 '#value' => $node->link
766 );
767 $form['source_link'] = array(
768 '#type' => 'hidden',
769 '#value' => $node->source_link
770 );
771 $form['source_xml'] = array(
772 '#type' => 'hidden',
773 '#value' => $node->source_xml
774 );
775 $form['source_title'] = array(
776 '#type' => 'hidden',
777 '#value' => $node->source_title
778 );
779 }
780
781 $form['title'] = array(
782 '#type' => 'textfield',
783 '#title' => t('Title'),
784 '#default_value' => $node->title,
785 '#required' => TRUE,
786 '#weight' => -100
787 );
788
789 $feeds = array();
790 $result = db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n, {aggregator2_feed} af WHERE af.nid = n.nid'));
791 while ($temp = db_fetch_array($result)) {
792 $feeds[$temp['nid']] = $temp['title'];
793 }
794 $form['fid'] = array (
795 '#type' => 'select',
796 '#title' => t('Feed Name'),
797 '#default_value' => $node->fid,
798 '#options' => $feeds,
799 '#required' => TRUE,
800 '#description' => t('The RSS/ATOM feed which this item belongs to.'),
801 '#weight' => -98
802 );
803
804 $form['body'] = array(
805 '#type' => 'textarea',
806 '#title' => t('Description'),
807 '#default_value' => $node->body,
808 '#weight' => -97
809 );
810 $form['format'] = filter_form($node->format);
811 $form['format']['#weight'] = 0;
812
813 return $form;
814 }
815
816 /**
817 * Implementation of hook_access().
818 */
819 function aggregator2_feed_access($op, $node) {
820 global $AGGREGATOR2_REFRESH_FEED_RUNNING;
821 global $user;
822
823 switch ($op) {
824 case 'create':
825 return user_access(AGGREGATOR2_PERM_CREATE_FEED);
826 break;
827 case 'update':
828 case 'delete':
829 if ($AGGREGATOR2_REFRESH_FEED_RUNNING || (user_access(AGGREGATOR2_PERM_EDIT_OWN_FEED) && ($user->uid == $node->uid))) {
830 return TRUE;
831 }
832 break;
833 case 'view':
834 return user_access(AGGREGATOR2_PERM_ACCESS_FEED);
835 break;
836 }
837 }
838
839 /**
840 * Implementation of hook_access().
841 */
842 function aggregator2_item_access($op, $node) {
843 global $AGGREGATOR2_REFRESH_FEED_RUNNING;
844 global $user;
845
846 switch ($op) {
847 case 'create':
848 if ($AGGREGATOR2_REFRESH_FEED_RUNNING) {
849 return TRUE;
850 }
851 break;
852 case 'update':
853 case 'delete':
854 if ($AGGREGATOR2_REFRESH_FEED_RUNNING || (user_access(AGGREGATOR2_PERM_EDIT_OWN_ITEM) && ($user->uid == $node->uid))) {
855 return TRUE;
856 }
857 break;
858 case 'view':
859 return user_access(AGGREGATOR2_PERM_ACCESS_ITEM);
860 break;
861 }
862 }
863
864 /**
865 * Implementation of hook_validate().
866 */
867 function aggregator2_feed_validate($node) {
868 if (isset($node->url)) {
869 if (trim($node->url) == '') {
870 form_set_error('url', t('URL field may not be empty, without it aggregator2 will not know from where to aggregate items.'));
871 }
872 $result = db_query("SELECT af.nid, n.title FROM {node} n, {aggregator2_feed} af WHERE af.url = '%s' AND af.nid = n.nid", $node->url);
873 while ($feed = db_fetch_object($result)) {
874 if ($feed->nid != $node->nid) {
875 $link = l($feed->title, "node/{$feed->nid}");
876 form_set_error('url', t('Duplicated URL: %link already uses that URL.', array('%link' => $link)));
877 break;
878 }
879 }
880 if (!aggregator2_is_valid_url($node->url)) {
881 form_set_error('url', t('That URL is not allowed.'));
882 }
883 }
884 }
885
886 /**
887 * Implementation of hook_validate().
888 */
889 function aggregator2_item_validate($node) {
890 if (!isset($node->fid) || $node->fid < 1) {
891 form_set_error('fid', t('Invalid feed selected'));
892 }
893 if (!aggregator2_is_valid_url($node->link)) {
894 form_set_error('url', t('That URL is not allowed.'));
895 }
896 }
897
898 /**
899 * Implementation of hook_insert().
900 */
901 function aggregator2_feed_insert($node) {
902 db_query("INSERT INTO {aggregator2_feed} (nid, author, url, freezed, refresh, clear_items, update_items, guid_items, promoted_items, item_status, item_taxonomy, item_date_source, item_show_link, item_delete_mode) VALUES (%d, '%s', '%s', %d, %d, %d, %d, %d, %d, %d, '%s', %d, %d, %d)", $node->nid, $node->author, $node->url, $node->freezed, $node->refresh, $node->clear_items, $node->update_items, $node->guid_items, $node->promoted_items, $node->item_status, serialize($node->feed_item_taxonomy), $node->item_date_source, $node->item_show_link, $node->item_delete_mode);
903 cache_clear_all('aggregator2:block:sources');
904 }
905
906 /**
907 * Implementation of hook_insert().
908 */
909 function aggregator2_item_insert($node) {
910 db_query("INSERT INTO {aggregator2_item} (nid, fid, author, link, guid, source_link, source_xml, source_title) VALUES (%d, %d, '%s', '%s', '%s', '%s', '%s', '%s')", $node->nid, $node->fid, $node->author, $node->link, $node->guid, $node->source_link, $node->source_xml, $node->source_title);
911 }
912
913 /**
914 * Implementation of hook_update().
915 */
916 function aggregator2_feed_update($node) {
917 db_query("UPDATE {aggregator2_feed} SET author = '%s', url = '%s', freezed = %d, refresh = %d, clear_items = %d, update_items = %d, guid_items = %d, promoted_items = %d, checked = %d, link = '%s', image = '%s', etag = '%s', modified = %d, item_status = %d, item_taxonomy = '%s', item_date_source = %d, item_show_link = %d, item_delete_mode = %d WHERE nid = %d", $node->author, $node->url, $node->freezed, $node->refresh, $node->clear_items, $node->update_items, $node->guid_items, $node->promoted_items, $node->checked, $node->link, $node->image, $node->etag, $node->modified, $node->item_status, serialize($node->feed_item_taxonomy), $node->item_date_source, $node->item_show_link, $node->item_delete_mode, $node->nid);
918 // update taxonomy for already existing nodes, it may take a while...
919 // TODO: find a way to split work and run it at cron run?
920 // maybe store serialized array as drupal variable (for example: aggregator2_update_items_[FEED->NID])
921 // and at cron run, at feed update time, load it and update X items form it, then save what's left for next cron run?
922 if (function_exists('taxonomy_node_save') && $node->change_existing_items == 1) {
923 $result = db_query("SELECT ai.nid FROM {aggregator2_item} ai WHERE ai.fid = '%d'", $node->nid);
924 $items = array();
925 while ($temp = db_fetch_object($result)) {
926 $items[] = $temp->nid;
927 // TODO: this removes previous categories, including those from autotaxonomy :( Find a way to remove only those we don't want?
928 // maybe setting by which vocabularies should change and which not? then load taxonomy, and remove only those terms which are from vocabularies allowed to change?
929 taxonomy_node_save($temp->nid, $node->feed_item_taxonomy);
930 }
931 if (count($items) > 0) {
932 // Update filter format of items
933 db_query('UPDATE {node_revisions} SET format = %d WHERE nid IN(%s)', $node->format, implode(',', $items));
934 drupal_set_message(t('Updated existing items'));
935 }
936 }
937 // Clear cache
938 cache_clear_all('aggregator2:block:sources');
939 cache_clear_all('aggregator2:block:'.$node->nid);
940 }
941
942
943 /**
944 * Implementation of hook_update().
945 */
946 function aggregator2_item_update($node) {
947 db_query("UPDATE {aggregator2_item} SET link = '%s', author = '%s', fid = %d WHERE nid = %d", $node->link, $node->author, $node->fid, $node->nid);
948 }
949
950 /**
951 * Implementation of hook_delete().
952 */
953 function aggregator2_feed_delete(&$node) {
954 db_query('DELETE FROM {aggregator2_feed} WHERE nid = %d', $node->nid);
955 // Clear cache
956 cache_clear_all('aggregator2:block:'.$node->nid);
957 cache_clear_all('aggregator2:block:sources');
958 }
959
960 /**
961 * Implementation of hook_delete().
962 */
963 function aggregator2_item_delete(&$node) {
964 db_query('DELETE FROM {aggregator2_item} WHERE nid = %d', $node->nid);
965 cache_clear_all('aggregator2:block:'.$node->fid);
966 }
967
968 /**
969 * Implementation of hook_load().
970 */
971 function aggregator2_feed_load($node) {
972 $temp = db_fetch_object(db_query('SELECT * FROM {aggregator2_feed} WHERE nid = %d', $node->nid));
973 $temp->feed_item_taxonomy = unserialize($temp->item_taxonomy);
974 unset($temp->item_taxonomy);
975 return $temp;
976 }
977
978 /**
979 * Implementation of hook_load().
980 */
981 function aggregator2_item_load($node) {
982 return db_fetch_object(db_query('SELECT ai.fid, ai.link, ai.source_link, ai.source_xml, ai.source_title, ai.author AS author, n.title AS feed_title, af.url AS feed_url, af.item_show_link AS item_show_link FROM {aggregator2_item} ai LEFT JOIN {aggregator2_feed} af ON af.nid = ai.fid LEFT JOIN {node} n ON n.nid = ai.fid WHERE ai.nid = %d', $node->nid));
983 }
984
985 /**
986 * Implementation of hook_view().
987 */
988 function aggregator2_feed_view(&$node, $teaser = FALSE, $page = FALSE) {
989 // Provide some statistics for feed nodes
990 $rows = array();
991 $items_count = db_result(db_query("SELECT COUNT(ai.nid) FROM {aggregator2_item} ai WHERE ai.fid = '%d'", $node->nid));
992 $rows[] = array(t('Feed hosted at'), $node->url);
993 $rows[] = array(t('Feed currently contains'), format_plural($items_count, '1 item', '%count items'));
994 $rows[] = array(t('Last checked feed host'), ($node->checked ? t('%time ago', array('%time' => format_interval(time() - $node->checked))) : t('never')) );
995 $rows[] = array(t('Time until next refresh'), ($node->checked ? t('%time left', array('%time' => format_interval($node->checked + $node->refresh - time()))) : t('never')) );
996
997 $output .= theme('table', array(), $rows);
998 $node->body .= $output;
999
1000 $node = node_prepare($node, $teaser);
1001 }
1002
1003
1004
1005
1006
1007 /**
1008 * Menu callback; displays the aggregator administration page.
1009 */
1010 function aggregator2_admin_overview() {
1011 global $user;
1012 if (!user_access('administer nodes')) {
1013 $uid = ' WHERE n.uid = '. $user->uid .' ';
1014 $can_edit = user_access(AGGREGATOR2_PERM_EDIT_OWN_FEED);
1015 $can_remove = user_access(AGGREGATOR2_PERM_EDIT_OWN_ITEM);
1016 $can_refresh = user_access(AGGREGATOR2_PERM_REFRESH_OWN_FEED);
1017 }
1018 else {
1019 $uid = '';
1020 $can_edit = TRUE;
1021 $can_remove = TRUE;
1022 $can_refresh = TRUE;
1023 }
1024
1025 $result = db_query("SELECT n.nid, n.title, af.checked, af.refresh, af.freezed FROM {node} n INNER JOIN {aggregator2_feed} af ON n.nid = af.nid $uid ORDER BY n.title ASC");
1026
1027 $output = '<h3>'. t('Feed overview') .'</h3>';
1028
1029 $header = array(t('Title'), t('Items'), t('Last update'