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

Contents of /contributions/modules/aggregation/aggregation.module

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


Revision 1.4 - (show annotations) (download) (as text)
Sat Feb 24 00:28:57 2007 UTC (2 years, 9 months ago) by mistknight
Branch: MAIN
CVS Tags: DRUPAL-5--1-1, DRUPAL-5--1-0, HEAD
Branch point for: DRUPAL-5, DRUPAL-6--1
Changes since 1.3: +46 -34 lines
File MIME type: text/x-php
Updated to drupal 5.x
1 <?php
2
3 // $Id: aggregation.module,v 1.3 2007/02/23 07:08:09 mistknight Exp $
4
5 /**
6 * A number of defs to ease naming
7 */
8 define("FEEDS_ARENT_ASSIGNED_TERM_UNDER_ACTIVE_VOCABULARY", 1);
9 define("FTP_REQUEST_FAILED", 2);
10 define("HTTP_REQUEST_FAILED", 3);
11 define("MALFORMED_XML", 4);
12 define("AGGREGATION_FAILED", 5);
13 define("EMPTY_IMAGE", 6);
14 define("COULD_NOT_WRITE_TO_FILE", 7);
15
16 // These two variables are needed to get the etag and/or last modified
17
18 static $etag;
19 static $last_modified;
20
21 $GLOBALS['aggregation_item_count'] = 0;
22
23 /**
24 * Implementation of hook_help
25 */
26 function aggregation_help($section)
27 {
28 switch ($section)
29 {
30 case 'admin/help#aggregation':
31 $output = '<p>'. t('The aggregation module is a module that is responsible for aggregating any XML feed, custom feeds need a feed_handler. Check the accompanying readme file.') .'</p>';
32 $output .= '<p>' .t('Visit the <a href="!admin-settings">settings</a> page to tweak its behavior', array('!admin-settings'=> url('admin/settings/aggregation')));
33 return $output;
34 }
35 }
36
37
38 /**
39 * Implementation of hook_perm().
40 */
41 function aggregation_perm()
42 {
43 return array('manage aggregation feeds', 'manage aggregation items', 'view aggregation items',
44 'manage own feed items');
45 }
46
47 /**
48 * Implementation of hook_node_info().
49 */
50 function aggregation_node_info()
51 {
52 return array
53 (
54 'aggregation_feed' => array('name' => t('Feed'), 'module' => 'aggregation_feed', 'description' => t('Represents the feed the site aggregates from. Items are generated from feeds during cron runs.'), 'has_title' => TRUE, 'title_label' => t('Feed name'), 'has_body' => FALSE),
55 'aggregation_item' => array('name' => t('Feed Item'), 'module' => 'aggregation_item', 'description' => t('Items are usually auto-generated on cron runs, but this type exists in case you would like to manually add an item.'))
56 );
57 }
58
59 /**
60 * Implementation of hook_menu().
61 *
62 */
63 function aggregation_menu($may_cache)
64 {
65 $items = array();
66
67 if (!$may_cache)
68 {
69 $items[] = array(
70 'path' => 'admin/settings/aggregation',
71 'title' => t('Aggregation'),
72 'description' => t('These options control the general behavior of the aggregation module.'),
73 'callback' => 'drupal_get_form',
74 'callback arguments' => array('aggregation_admin_settings'),
75 'access' => user_access('administer site configuration'),
76 'type' => MENU_NORMAL_ITEM);
77 }
78
79 return $items;
80 }
81
82 /**
83 * This is the function that will be called instead of the old hook_settings
84 */
85 function aggregation_admin_settings()
86 {
87 $form = array();
88
89 $vocabs = taxonomy_get_vocabularies('aggregation_feed');
90
91 foreach ($vocabs AS $vid => $vocab_object)
92 $vocabs[$vid] = $vocab_object->name;
93
94 $form['aggregation_current_vid'] = array(
95 '#type' => 'select',
96 '#title' => t('Please specify the aggregation feed vid to use as meta for aggregation types'),
97 '#default_value' => variable_get('aggregation_current_vid', -1),
98 '#options' => $vocabs,
99 '#description' => t('The vocabularies determine the aggregation\'s parsing types. So change this value if you want to switch from the default vocabulary.'),
100 '#required' => TRUE
101 );
102
103 $form['aggregation_item_time_to_live'] = array(
104 '#type' => 'textfield',
105 '#default_value' => variable_get('aggregation_item_time_to_live', 30),
106 '#required' => TRUE,
107 '#title' => t('Please specify the number of DAYS an item will live'),
108 '#description' => t('If aggregation_feed is set to delete old items, this variable will determine the age after which an item will be deleted')
109 );
110
111 $form['aggregation_enable_logging'] = array(
112 '#type' => 'checkbox',
113 '#default_value' => variable_get('aggregation_enable_logging', 1),
114 '#title' => t('Log Operations'),
115 '#description' => t('After moving to production, you may wish to disable this if you feel everything is stable enough. But it\'s recommended you keep it on. Fatal errors will ALWAYS be logged.')
116 );
117
118 $form['aggregation_image_to_display'] = array(
119 '#type' => 'textfield',
120 '#default_value' => variable_get('aggregation_image_to_display', 'preview'),
121 '#required' => TRUE,
122 '#title' => t('Image to display'),
123 '#description' => t('This is the image size that will be displayed on aggregated items\' node page.')
124 );
125
126 $form['aggregation_enable_cron'] = array(
127 '#type' => 'checkbox',
128 '#default_value' => variable_get('aggregation_enable_cron', 1),
129 '#title' => t('Enable Cron'),
130 '#description' => t('Remove this check if you want to stop aggregation temporarily.')
131 );
132
133 $form['aggregation_feeds_to_refresh_per_cron'] = array(
134 '#type' => 'textfield',
135 '#default_value' => variable_get('aggregation_feeds_to_refresh_per_cron', 7),
136 '#required' => TRUE,
137 '#title' => t('The number of feeds to refresh every cron'),
138 '#description' => t('This is important if you have many feeds that may slow the site.').'<br />'.
139 t('All feeds will eventually be processed, so this option will simply slow the process down to alleviate congestion')
140 );
141
142 $form['aggregation_feed_refresh_cooldown'] = array(
143 '#type' => 'textfield',
144 '#default_value' => variable_get('aggregation_feed_refresh_cooldown', 0),
145 '#required' => TRUE,
146 '#title' => t('Time Between Feeds'),
147 '#description' => t('Does your server die when a refresh occurs? Try setting this IN SECONDS.')
148 );
149
150 return system_settings_form($form);
151 }
152
153 /**
154 * This function validates the settings form
155 */
156 function aggregation_validate_settings($form)
157 {
158 if (!is_numeric($form['aggregation_item_time_to_live']))
159 form_set_error('aggregation_item_time_to_live', t('This field must be numeric.'));
160
161 if ($form['aggregation_item_time_to_live'] <= 0)
162 form_set_error('aggregation_item_time_to_live', t('This field must be a positive number.'));
163
164 if (!is_numeric($form['aggregation_feeds_to_refresh_per_cron']))
165 form_set_error('aggregation_feeds_to_refresh_per_cron', t('This field must be numeric.'));
166
167 if ($form['aggregation_feeds_to_refresh_per_cron'] <= 0)
168 form_set_error('aggregation_feeds_to_refresh_per_cron', t('This field must be a positive number.'));
169
170 if (!is_numeric($form['aggregation_feed_refresh_cooldown']))
171 form_set_error('aggregation_feed_refresh_cooldown', t('This field must be numeric.'));
172
173 if ($form['aggregation_feed_refresh_cooldown'] < 0)
174 form_set_error('aggregation_feed_refresh_cooldown', t('This field must be positive or zero.'));
175 }
176
177 /**
178 * Implementation of hook_cron().
179 */
180 function aggregation_cron()
181 {
182 if (!variable_get('aggregation_enable_cron', TRUE))
183 {
184 watchdog('aggregation', 'You seem to have disabled aggregation from aggregation settings.');
185 return;
186 }
187 $vid = variable_get('aggregation_current_vid', -1);
188 if ($vid == -1)
189 {
190 watchdog('aggregation', 'For some reason, aggregation\'s vocabulary is not set.'.
191 'Try visiting aggregation\'s settings to set it!');
192 return;
193 }
194
195 $max_exec_time = ini_get('max_execution_time');
196 $max_exec_time = (int)$max_exec_time;
197
198 if (!ini_get('safe_mode'))
199 set_time_limit(20 * 60);
200
201 $feeds = db_query_range("SELECT n.nid FROM {aggregation_feed} af, {node} n ".
202 "WHERE af.nid = n.nid AND af.enabled = 'yes' AND (UNIX_TIMESTAMP() - af.last_refreshed) >= af.refresh_interval * 60 ORDER BY af.last_refreshed", 0, variable_get('aggregation_feeds_to_refresh_per_cron', 7));
203
204 while ($feed_nid = db_fetch_object($feeds)->nid)
205 {
206 db_query("UPDATE {aggregation_feed} SET last_refreshed = %d WHERE nid = %d", time(), $feed_nid);
207
208 $feed = node_load($feed_nid);
209 aggregation_feed_prepare($feed);
210
211 try {
212 $GLOBALS['aggregation_item_count'] = $feed->promote_to_frontpage - 1;
213
214 $success = _aggregation_parse($feed, $vid);
215
216 $GLOBALS['aggregation_item_count'] = 0;
217
218 if (is_null($success)) continue;
219
220 if (!$success)
221 throw new Exception('Something wrong occured during the aggregation process!', AGGREGATION_FAILED);
222
223 if (variable_get('aggregation_enable_logging', TRUE))
224 watchdog('aggregation', 'aggregation has finished aggregating items from '.$feed->title);
225
226 // using this instead of node_save for performance reasons
227 db_query("UPDATE {aggregation_feed} SET etag = '%s', ".
228 "last_modified = %d, last_refreshed = %d WHERE nid = %d", $feed->etag,
229 $feed->last_modified, time(), $feed->nid);
230
231 if ($feed->delete_old_items)
232 {
233 $items_to_be_deleted = db_query("SELECT n.nid FROM {node} n, ".
234 "{aggregation_item} ai WHERE n.nid = ai.nid AND ai.fid = %d AND ".
235 "(UNIX_TIMESTAMP() - n.created) >= %d", $feed->nid,
236 variable_get('aggregation_item_time_to_live', 30) * 24 * 60 * 60);
237
238 while ($item = db_fetch_object($items_to_be_deleted))
239 node_delete($item->nid);
240 }
241 }
242 catch (Exception $e) {
243 $GLOBALS['aggregation_item_count'] = 0;
244 watchdog('aggregation', $e->getMessage(), WATCHDOG_ERROR, l(t('view'), "node/$feed->nid"));
245 }
246 }
247
248 if (!ini_get('safe_mode') && is_numeric($max_exec_time) && $max_exec_time > 0)
249 set_time_limit($max_exec_time);
250 }
251
252 /* Following are the aggregation_feed hooks */
253
254 /**
255 * Implementation of hook_access().
256 */
257
258 function aggregation_feed_access($op, $node)
259 {
260 return user_access('manage aggregation feeds');
261 }
262
263 /**
264 * Implementation of hook_form().
265 */
266
267 function aggregation_feed_form(&$node, &$param)
268 {
269 $form = array();
270
271 $form['title'] = array (
272 '#type' => 'textfield',
273 '#default_value' => $node->title,
274 '#required' => TRUE,
275 '#title' => t(node_get_types('type', 'aggregation_feed')->title_label),
276 '#description' => t('Enter something descriptive to identify your feed.')
277 );
278
279 $form['original_author'] = array(
280 '#type' => 'textfield',
281 '#default_value' => $node->original_author,
282 '#required' => TRUE,
283 '#title' => t('Original Author'),
284 '#description' => t('Please specify the original author for the items of this feed.')
285 );
286
287 $form['url'] = array (
288 '#type' => 'textfield',
289 '#default_value' => $node->url,
290 '#required' => TRUE,
291 '#title' => t('Feed URL'),
292 '#description' => t('Please provide the feed URL.')
293 );
294
295 $form['authentication'] = array (
296 '#type' => 'fieldset',
297 '#title' => t('Authentication Settings'),
298 '#collapsible' => TRUE,
299 '#collapsed' => FALSE,
300 '#tree' => FALSE
301 );
302
303 $form['authentication']['username'] = array(
304 '#type' => 'textfield',
305 '#default_value' => $node->username,
306 '#required' => FALSE,
307 '#description' => t('If your site uses authentication, please specify the username here.'),
308 '#title' => t('Enter Username')
309 );
310
311 $description = t('If your site uses authentication, please specify the password here.');
312
313 // This indicates we're in edit mode, in which case I'd like to add an additional directive
314 if ($node->password)
315 $description .= '<br />'.
316 t('(leave empty to preserve value)');
317
318 $form['authentication']['password'] = array(
319 '#type' => 'password',
320 '#default_value' => $node->password,
321 '#required' => FALSE,
322 '#description' => $description,
323 '#title' => 'Enter Password'
324 );
325
326 $description = t('If your site uses authentication, please retype your password here.');
327
328 // This indicates we're in edit mode, in which case I'd like to add an additional directive
329 if ($node->password)
330 $description .= '<br />'.
331 t('(leave empty to preserve value)');
332
333 $form['authentication']['repeat_password'] = array(
334 '#type' => 'password',
335 '#default_value' => $node->password,
336 '#required' => FALSE,
337 '#description' => $description,
338 '#title' => t('Retype Your Password')
339 );
340
341 $form['refresh_interval'] = array(
342 '#type' => 'textfield',
343 '#default_value' => $node->refresh_interval,
344 '#required' => TRUE,
345 '#title' => t('Refresh Interval'),
346 '#description' => t('Please specify the refresh interval IN MINUTES.')
347 );
348
349 $form['title_as_guid_interval'] = array(
350 '#type' => 'textfield',
351 '#default_value' => $node->title_as_guid_interval ? $node->title_as_guid_interval : 0,
352 '#required' => TRUE,
353 '#title' => t('Title as GUID interval'),
354 '#description' => t('When a feed contains no GUID or image URL, then the title is it\'s guid. This field '.
355 'specifies the time period the title is considered as a GUID. If you provide a GUID or image URL then '.
356 'this field value is irrelevant. If not, then you can set this to 0 which would prevent any new article '.
357 'with the same title from ever entering (unless the old one is deleted) - this could also be achieved by sending '.
358 'the title as the GUID - , or you can set it to the time IN HOURS the title will act as a GUID, after which '.
359 'any other article still in the feed with the same title will enter the system.')
360 );
361
362 $form['enabled'] = array(
363 '#type' => 'checkbox',
364 '#default_value' => $node->enabled,
365 '#title' => t('Enabled'),
366 '#description' => t('Use this checkbox to enable/disable refreshing this feed.')
367 );
368
369 $form['publish_new_items'] = array(
370 '#type' => 'checkbox',
371 '#default_value' => $node->publish_new_items,
372 '#description' => t('Use this checkbox to publish all aggregated feed items.'),
373 '#title' => t('Publish items')
374 );
375
376 $form['aggregate_to_moderation_queue'] = array(
377 '#type' => 'checkbox',
378 '#default_value' => $node->aggregate_to_moderation_queue,
379 '#description' => t('Use this checkbox to send aggregated items to the moderation queue. Weather they appear on the site or not depends on the \'publish items\', not weather they are on the moderation queue.'),
380 '#title' => t('Add aggregated items to moderation queue')
381 );
382
383 $form['sticky_items'] = array(
384 '#type' => 'checkbox',
385 '#default_value' => $node->sticky_items,
386 '#description' => t('All items aggregated from this feed will be sticky.'),
387 '#title' => t('Sticky items')
388 );
389
390 $form['enable_comments_on_articles'] = array(
391 '#type' => 'checkbox',
392 '#default_value' => $node->enable_comments_on_articles,
393 '#description' => t('All aggregated articles will have commenting set to read/write.'),
394 '#title' => t('Aggregated article comments')
395 );
396
397 $form['enable_comments_on_images'] = array(
398 '#type' => 'checkbox',
399 '#default_value' => $node->enable_comments_on_images,
400 '#description' => t('All aggregated images will have commenting set to read/write.'),
401 '#title' => t('Aggregated images\' comments')
402 );
403
404 $form['delete_old_items'] = array(
405 '#type' => 'checkbox',
406 '#default_value' => $node->delete_old_items,
407 '#description' => t('Use this checkbox to delete items older than (n) days, change this value from the settings. Uncheck and items under this feed will never be deleted until this option is turned on.'),
408 '#title' => t('Delete Old Items')
409 );
410
411 $form['promote_to_frontpage'] = array(
412 '#type' => 'textfield',
413 '#default_value' => $node->promote_to_frontpage ? $node->promote_to_frontpage : 0,
414 '#required' => TRUE,
415 '#title' => t('Promote to frontpage'),
416 '#description' => t('Please specify the number of articles to promote to the frontpage.')
417 );
418
419 $form['item_taxonomies'] = array (
420 '#type' => 'fieldset',
421 '#title' => t('Item Taxonomies'),
422 '#collapsible' => TRUE,
423 '#collapsed' => FALSE,
424 '#tree' => FALSE,
425 '#description' => t('These taxonomies will be assigned to all items under this feed.')
426 );
427
428 $fakeform = array();
429 $fakeform['#node'] = new stdClass();
430
431 if ($node->item_categories)
432 $fakeform['#node']->taxonomy = $node->item_categories;
433
434 // This part will trick taxonomy into believing it's dealing with an aggregation_item
435 $fakeform['type']['#value'] = 'aggregation_item';
436 $fakeform['#node']->type = 'aggregation_item';
437
438 taxonomy_form_alter('aggregation_item_node_form', $fakeform);
439
440 $form['item_taxonomies']['item_categories'] = $fakeform['taxonomy'];
441
442 return $form;
443 }
444
445 /**
446 * Implementation of hook_validate().
447 */
448
449 function aggregation_feed_validate(&$node)
450 {
451 if (!is_numeric($node->refresh_interval) || $node->refresh_interval < 0)
452 form_set_error('refresh_interval', 'The refresh interval is a numeric value greater than or equal to 0.');
453 else if (!is_numeric($node->title_as_guid_interval) || $node->title_as_guid_interval < 0)
454 form_set_error('title_as_guid_interval', 'The title as GUID interval is a positive numeric or 0.');
455 else if (!is_numeric($node->promote_to_frontpage) || $node->promote_to_frontpage < 0)
456 form_set_error('promote_to_frontpage', 'The number of items to promote to the frontpage is a positive numeric or 0.');
457 else if ($node->password != $node->repeat_password)
458 form_set_error('password', 'Your passwords did not match.');
459 }
460
461 /**
462 * Implementation of hook_submit().
463 */
464
465 function aggregation_feed_submit(&$node)
466 {
467 $node->enabled = $node->enabled ? 'yes' : 'no';
468 $node->publish_new_items = $node->publish_new_items ? 'yes' : 'no';
469 $node->delete_old_items = $node->delete_old_items ? 'yes' : 'no';
470 $node->aggregate_to_moderation_queue = $node->aggregate_to_moderation_queue ? 'yes' : 'no';
471 $node->sticky_items = $node->sticky_items ? 'yes' : 'no';
472 $node->enable_comments_on_articles = $node->enable_comments_on_articles ? 'yes' : 'no';
473 $node->enable_comments_on_images = $node->enable_comments_on_images ? 'yes' : 'no';
474 $node->etag = '';
475 $node->last_modified = 0;
476
477 $node->title = trim($node->title);
478 $node->original_author = trim($node->original_author);
479 $node->url = trim($node->url);
480 $node->username = trim($node->username);
481 $node->password = trim($node->password);
482
483 $node->item_categories = serialize($node->item_categories);
484 }
485
486 /**
487 * Implementation of hook_prepare().
488 */
489
490 function aggregation_feed_prepare(&$node)
491 {
492 $node->refresh_interval = $node->refresh_interval ? $node->refresh_interval : 15;
493
494 $node->enabled = $node->enabled ? ($node->enabled == 'yes' ? TRUE : FALSE) : TRUE;
495
496 $node->publish_new_items = $node->publish_new_items ?
497 ($node->publish_new_items == 'yes' ? TRUE : FALSE) : TRUE;
498
499 $node->delete_old_items = $node->delete_old_items ?
500 ($node->delete_old_items == 'yes' ? TRUE : FALSE) : FALSE;
501
502 $node->aggregate_to_moderation_queue = $node->aggregate_to_moderation_queue ?
503 ($node->aggregate_to_moderation_queue == 'yes' ? TRUE : FALSE) : FALSE;
504
505 $node->sticky_items = $node->sticky_items ? ($node->sticky_items == 'yes' ? TRUE : FALSE) : FALSE;
506
507 $node->enable_comments_on_articles = $node->enable_comments_on_articles ?
508 ($node->enable_comments_on_articles == 'yes' ? TRUE : FALSE) : FALSE;
509
510 $node->enable_comments_on_images = $node->enable_comments_on_images ?
511 ($node->enable_comments_on_images == 'yes' ? TRUE : FALSE) : FALSE;
512
513 if ($node->item_categories && $node->item_categories != '')
514 $node->item_categories = unserialize($node->item_categories);
515
516 // There's a specific format needed to reconstruct a form, we're creating it now
517 if ($node->item_categories)
518 {
519 $formatted_item_categories = array();
520 if (is_array($node->item_categories))
521 {
522 foreach ($node->item_categories AS $vid => $tids)
523 {
524 if (is_array($tids) && count($tids) > 0)
525 {
526 foreach ($tids AS $tid => $tid)
527 if ($tid > 0)
528 $formatted_item_categories[$tid] = taxonomy_get_term($tid);
529 }
530 else if (is_string($tids))
531 {
532 if ($tids > 0)
533 $formatted_item_categories[$tids] = taxonomy_get_term($tids);
534 }
535 }
536 }
537
538 $node->item_categories = $formatted_item_categories;
539 }
540 }
541
542 /**
543 * Implementation of hook_load().
544 */
545
546 function aggregation_feed_load($node)
547 {
548 return db_fetch_object(db_query("SELECT original_author, url, username, password, ".
549 "refresh_interval, title_as_guid_interval, enabled, publish_new_items, aggregate_to_moderation_queue, ".
550 "sticky_items, enable_comments_on_articles, enable_comments_on_images, promote_to_frontpage, ".
551 "delete_old_items, item_categories, etag, last_modified FROM {aggregation_feed} WHERE nid = %d", $node->nid));
552 }
553
554 /**
555 * Implementation of hook_insert().
556 */
557
558 function aggregation_feed_insert($node)
559 {
560 db_query("INSERT INTO {aggregation_feed} (nid, original_author, url, username, password, ".
561 "refresh_interval, title_as_guid_interval, enabled, publish_new_items, aggregate_to_moderation_queue, sticky_items, enable_comments_on_articles, enable_comments_on_images, promote_to_frontpage, delete_old_items, item_categories, etag, last_modified, last_refreshed) VALUES ".
562 "(%d, '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', %d ,'%s', '%s', '%s', %d, %d)",
563 $node->nid, $node->original_author, $node->url, $node->username, $node->password,
564 $node->refresh_interval, $node->title_as_guid_interval, $node->enabled, $node->publish_new_items,
565 $node->aggregate_to_moderation_queue, $node->sticky_items, $node->enable_comments_on_articles,
566 $node->enable_comments_on_images, $node->promote_to_frontpage, $node->delete_old_items, $node->item_categories,
567 '', 0, 0);
568 }
569
570 /**
571 * Implementation of hook_update().
572 */
573
574 function aggregation_feed_update($node)
575 {
576 if (trim($node->password) != '')
577 {
578 db_query("UPDATE {aggregation_feed} SET original_author = '%s', url = '%s', ".
579 "username = '%s', password = '%s', refresh_interval = %d, title_as_guid_interval = %d, enabled = '%s', ".
580 "publish_new_items = '%s', aggregate_to_moderation_queue = '%s', sticky_items = '%s', enable_comments_on_articles = '%s', enable_comments_on_images = '%s', promote_to_frontpage = %d, delete_old_items = '%s', item_categories = '%s', ".
581 "last_modified = %d, last_refreshed = %d WHERE nid = %d",
582 $node->original_author, $node->url, $node->username, $node->password,
583 $node->refresh_interval, $node->title_as_guid_interval, $node->enabled, $node->publish_new_items,
584 $node->aggregate_to_moderation_queue, $node->sticky_items, $node->enable_comments_on_articles,
585 $node->enable_comments_on_images, $node->promote_to_frontpage,
586 $node->delete_old_items, $node->item_categories,
587 ($node->etag && $node->etag != '') ? $node->etag : '',
588 ($node->last_modified && $node->last_modified != 0) ? $node->last_modified : 0,
589 0, $node->nid);
590 }
591 else
592 {
593 db_query("UPDATE {aggregation_feed} SET original_author = '%s', url = '%s', ".
594 "username = '%s', refresh_interval = %d, title_as_guid_interval = %d, enabled = '%s', ".
595 "publish_new_items = '%s', aggregate_to_moderation_queue = '%s', sticky_items = '%s', enable_comments_on_articles = '%s', enable_comments_on_images = '%s', promote_to_frontpage = %d, delete_old_items = '%s', item_categories = '%s', ".
596 "etag = '%s', last_modified = %d, last_refreshed = %d WHERE nid = %d",
597 $node->original_author, $node->url, $node->username,
598 $node->refresh_interval, $node->title_as_guid_interval, $node->enabled, $node->publish_new_items,
599 $node->aggregate_to_moderation_queue, $node->sticky_items, $node->enable_comments_on_articles,
600 $node->enable_comments_on_images, $node->promote_to_frontpage,
601 $node->delete_old_items, $node->item_categories,
602 ($node->etag && $node->etag != '') ? $node->etag : '',
603 ($node->last_modified && $node->last_modified != 0) ? $node->last_modified : 0,
604 0, $node->nid);
605 }
606
607 }
608
609 /**
610 * Implementation of hook_delete().
611 */
612
613 function aggregation_feed_delete(&$node)
614 {
615 $items = _aggregation_get_feed_items($node->nid);
616
617 while ($item = db_fetch_object($items))
618 node_delete($item->nid);
619
620 db_query("DELETE FROM {aggregation_feed} WHERE nid = %d", $node->nid);
621 }
622
623 /* Following are the aggregation_item hooks */
624
625 /**
626 * Implementation of hook_access().
627 */
628
629 function aggregation_item_access($op, $node)
630 {
631 global $user;
632
633 if ($op == 'create') return user_access('manage aggregation items');
634 if ($op == 'update' || $op == 'delete')
635 return (user_access('manage own feed items') && ($user->uid == $node->uid));
636 if ($op == 'view') return user_access('view aggregation items');
637 }
638
639 /**
640 * Implementation of hook_form().
641 */
642
643 function aggregation_item_form(&$node, &$param)
644 {
645 $form = array();
646
647 $form['title'] = array (
648 '#type' => 'textfield',
649 '#default_value' => $node->title,
650 '#required' => TRUE,
651 '#title' => t(node_get_types('type', 'aggregation_item')->title_label),
652 '#description' => t('The title of your feed item.')
653 );
654
655 $form['teaser'] = array (
656 '#type' => 'textarea',
657 '#default_value' => $node->teaser,
658 '#required' => FALSE,
659 '#title' => t('Teaser'),
660 '#description' => t('This is the article\'s teaser.')
661 );
662
663 $form['body'] = array (
664 '#type' => 'textarea',
665 '#default_value' => $node->body,
666 '#required' => FALSE,
667 '#title' => t(node_get_types('type', 'aggregation_item')->body_label),
668 '#description' => t('This is the main body of your article.')
669 );
670
671 $form['original_author'] = array(
672 '#type' => 'textfield',
673 '#default_value' => $node->original_author,
674 '#required' => TRUE,
675 '#title' => t('Original Author'),
676 '#description' => t('Specify the author of this feed item.'),
677 );
678
679 $form['url'] = array (
680 '#type' => 'textfield',
681 '#default_value' => $node->url,
682 '#required' => FALSE,
683 '#title' => t('Original URL'),
684 '#description' => t('Provide the URL of the original article if needed.')
685 );
686
687 $form['image_id'] = array (
688 '#type' => 'textfield',
689 '#default_value' => $node->image_id,
690 '#required' => FALSE,
691 '#title' => t('Image ID'),
692 '#description' => t('Provide an image id to attach an image.')
693 );
694
695 $description = t('The GUID is a unique string that distingishes your article from '.
696 'all others and prevents a second aggregation. Please provide it if available.');
697
698 // This indicates we're in edit mode, in which case I'd like to add an additional directive
699 if ($node->story_guid)
700 $description .= '<br />'.
701 t('(leave empty to preserve value)');
702
703 $form['story_guid'] = array (
704 '#type' => 'textfield',
705 '#default_value' => '',
706 '#required' => FALSE,
707 '#title' => t('GUID'),
708 '#description' => $description
709 );
710
711 $feeds = db_query('SELECT n.nid, n.title FROM {node} n, {aggregation_feed} af '.
712 'WHERE n.nid = af.nid');
713
714 if (db_num_rows($feeds) != 0)
715 {
716 $feed_array = array();
717
718 $feed_array[0] = '<none>';
719
720 while ($feed = db_fetch_object($feeds))
721 $feed_array[$feed->nid] = $feed->title;
722
723 $form['fid'] = array (
724 '#type' => 'select',
725 '#default_value' => $node->fid,
726 '#required' => TRUE,
727 '#title' => t('Source Feed'),
728 '#description' => t('Provide the source feed if valid for this item.'),
729 '#options' => $feed_array
730 );
731 }
732
733 return $form;
734 }
735
736 /**
737 * Implementation of hook_validate().
738 */
739
740 function aggregation_item_validate(&$node)
741 {
742 if (trim($node->image_id) != '')
743 {
744 if (!is_numeric($node->image_id))
745 form_set_error('image_id', 'The image id must be numeric.');
746 // A node load is simply too costly, so I'm going to directly query instead.
747 else if ($node->image_id != 0 && db_fetch_object(db_query("SELECT COUNT(n.nid) AS image_count FROM {node} n ".
748 "WHERE n.nid = %d AND n.type = 'image'", $node->image_id))->image_count == 0)
749 form_set_error('image_id', 'This image ID seems to be incorrect!');
750 }
751 }
752
753 /**
754 * Implementation of hook_submit().
755 */
756
757 function aggregation_item_submit(&$node)
758 {
759 if (!$node->fid)
760 $node->fid = 0;
761
762 if ($node->image_id == '')
763 $node->image_id = 0;
764
765 if (trim($node->story_guid) != '')
766 $node->story_guid = crc32($node->story_guid);
767 else
768 $node->story_guid = '';
769 }
770
771 /**
772 * Implementation of hook_prepare().
773 */
774
775 function aggregation_item_prepare(&$node)
776 {
777
778 }
779
780 /**
781 * Implementation of hook_load().
782 */
783
784 function aggregation_item_load($node)
785 {
786 return db_fetch_object(db_query('SELECT url, original_author, story_guid, '.
787 'fid, image_id, image_guid FROM {aggregation_item} WHERE nid = %d', $node->nid));
788 }
789
790 /**
791 * Implementation of hook_insert().
792 */
793
794 function aggregation_item_insert($node)
795 {
796 db_query("INSERT INTO {aggregation_item} (nid, url, original_author, ".
797 "story_guid, fid, image_id, image_guid) VALUES (%d, '%s', '%s', %d, %d, ".
798 "%d, %d)", $node->nid, $node->url, $node->original_author, $node->story_guid, $node->fid, $node->image_id, $node->image_guid ? $node->image_guid : 0);
799 }
800
801 /**
802 * Implementation of hook_update().
803 */
804
805 function aggregation_item_update($node)
806 {
807 if ($node->story_guid === '')
808 db_query("UPDATE {aggregation_item} SET url = '%s', original_author = '%s', ".
809 "fid = %d, image_id = %d, image_guid = %d ".
810 "WHERE nid = %d", $node->url, $node->original_author, $node->fid, $node->image_id,
811 $node->image_guid ? $node->image_guid : 0, $node->nid);
812 else
813 db_query("UPDATE {aggregation_item} SET url = '%s', original_author = '%s', ".
814 "story_guid = %d, fid = %d, image_id = %d, image_guid = %d ".
815 "WHERE nid = %d", $node->url, $node->original_author, $node->story_guid, $node->fid,
816 $node->image_id, $node->image_guid ? $node->image_guid : 0, $node->nid);
817 }
818
819 /**
820 * Implementation of hook_delete().
821 */
822
823 function aggregation_item_delete(&$node)
824 {
825 if (db_fetch_object(db_query(
826 "SELECT count(nid) AS image_count FROM {aggregation_item} WHERE image_id = %d", $node->image_id))
827 ->image_count == 1) node_delete($node->image_id);
828
829 db_query("UPDATE {aggregation_feed} SET etag = '', last_modified = 0 WHERE nid = %d", $node->fid);
830
831 db_query('DELETE FROM {aggregation_item} WHERE nid = %d', $node->nid);
832 }
833
834 /**
835 * This function is the implementation of hook_view
836 */
837
838 function aggregation_item_view(&$node, $teaser = FALSE, $page = FALSE)
839 {
840 if ($node->image_id > 0)
841 {
842 $image = node_load($node->image_id);
843 $node->image = $image;
844
845 // theme image
846 $image_render = theme('aggregation_image_render', $image);
847 }
848 else
849 {
850 $node->image_nid = NULL;
851 $node->image = NULL;
852
853 $image_render = '';
854 }
855
856 // theme body
857 $body_render = theme('aggregation_body_render', $node->body);
858
859 // theme the final item
860 $node->content['body']['#value'] = theme('aggregation_item_render', $image_render, $body_render);
861
862 return $node;
863
864 }
865
866 function theme_aggregation_item_render($image_render, $body_render)
867 {
868 return "<div class=\"aggregation_item\">{$image_render}{$body_render}</div>";
869 }
870
871 function theme_aggregation_body_render($body)
872 {
873 return '<div class="aggregation_item_body">'.$body.'</div>';
874 }
875
876 function theme_aggregation_image_render($image)
877 {
878 return '<div class="aggregation_item_image">'.'<img src="'.base_path().file_directory_path().'/'.$image->images[variable_get('aggregation_image_to_display', 'preview')].'" />'.
879 '</div>';
880 }
881
882 /* Following are the module's private methods */
883
884 /**
885 * This function returns the feed items for a particular feed
886 */
887
888 function _aggregation_get_feed_items($fid)
889 {
890 return db_query("SELECT n.nid FROM {node} n, {aggregation_item} ai WHERE n.nid = ai.nid AND ai.fid = %d", $fid);
891 }
892
893 /**
894 * This method retrieved the etag and last_modified tags
895 */
896
897 function read_etag_and_modified($ch, $header)
898 {
899 global $etag;
900 global $last_modified;
901
902 $length = strlen($header);
903 if(strstr($header, "Last-Modified:"))
904 {
905 $last_modified = strtotime(substr($header, 15));
906 }
907 if (strstr($header, "ETag:"))
908 {
909 $etag = substr($header, 6);
910 }
911 return $length;
912 }
913
914 /**
915 * This function returns the data in a URL (XML, image, etc...)
916 */
917 function aggregation_get_URL($url, $username = NULL, $password = NULL, $feed = NULL,
918 $feed_etag = NULL, $feed_last_modified = NULL)
919 {
920 global $etag;
921 global $last_modified;
922
923 if (trim($username) == '') $username = NULL;
924 if (trim($password) == '') $password = NULL;
925
926 $headers = array();
927
928 if (!is_null($feed_etag) && $feed_etag != '')
929 $headers['If-None-Match'] = $feed_etag;
930
931 if (!is_null($feed_last_modified) && $feed_last_modified != 0)
932 $headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s', $feed_last_modified) .' GMT';
933
934 if(count($headers) > 0)
935 {
936 $temp = array();
937
938 foreach ($headers as $header => $value)
939 $temp[] = $header .': '. $value;
940
941 $headers = $temp;
942 }
943
944 $ch = curl_init();
945
946 $is_ftp = FALSE;
947
948 if (stripos($url, 'ftp://') !== FALSE)
949 $is_ftp = TRUE;
950
951 if (!$is_ftp)
952 curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)");
953
954 if (is_array($headers) && count($headers) > 0 && !$is_ftp)
955 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
956
957 if (!$is_ftp)
958 curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'read_etag_and_modified');
959
960 if (!$is_ftp)
961 {
962 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
963 curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
964 }
965
966 if (!is_null($username) && !is_null($password))
967 {
968 curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
969 if (!$is_ftp)
970 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
971 }
972 curl_setopt($ch, CURLOPT_URL, $url);
973 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
974 if ($is_ftp)
975 curl_setopt($ch, CURLOPT_FTP_USE_EPSV, FALSE);
976 curl_setopt($ch, CURLOPT_TIMEOUT, 15);
977 $data = curl_exec($ch);
978
979 $return_code = curl_getinfo($ch);
980 $return_code = $return_code['http_code'];
981
982 $error = curl_error($ch);
983
984 curl_close($ch);
985 unset($ch);
986
987 if ($return_code != 304)
988 {
989 // It was an FTP request but failed
990 if ($is_ftp && $return_code != 226)
991 {
992 $fragment = '';
993
994 if (!is_null($feed))
995 $fragment = " while processing feed \"$feed->title\"";
996
997 throw new Exception("FTP request returned error code \"$return_code\"".
998 "$fragment on \"$url\"", FTP_REQUEST_FAILED);
999 }
1000
1001 // It was an HTTP request but failed
1002 if (!$is_ftp && $return_code != 200)
1003 {
1004 $fragment = '';
1005
1006 if (!is_null($feed))
1007 $fragment = " while processing feed \"$feed->title\"";
1008
1009 throw new Exception("HTTP request returned error code \"$return_code\"".
1010 "$fragment on \"$url\"", HTTP_REQUEST_FAILED);
1011 }
1012 }
1013 else
1014 return FALSE;
1015
1016 if (!is_null($feed_etag) && !is_null($feed_last_modified))
1017 return array($data, $etag, $last_modified);
1018
1019 return $data;
1020 }
1021
1022 /**
1023 * This function returns the simpleXML object of a given string, or throws an exception
1024 * if the string is in an invalid XML format
1025 */
1026 function aggregation_get_XML($string, $feed = NULL)
1027 {
1028 @ $xml = simplexml_load_string($string, NULL, LIBXML_NOERROR | LIBXML_NOWARNING);
1029
1030 // We got a malformed XML
1031 if ($xml === FALSE)
1032 {
1033 $fragment = '';
1034
1035 if (!is_null($feed))
1036 $fragment = " while processing feed \"$feed->title\"";
1037
1038 throw new Exception("XML malformed$fragment", MALFORMED_XML);
1039 }
1040
1041 return $xml;
1042 }
1043
1044 /* This is where we'll implement the handler functions */
1045
1046 /**
1047 * This is the generic feed XML handler, it calls the user implemented handler, it is meant as
1048 * a wrapper to the actual call
1049 */
1050 function _aggregation_parse($feed, $vid)
1051 {
1052 $handler_name = '';
1053
1054 foreach ($feed->taxonomy AS $tid => $term_object)
1055 if ($term_object->vid == $vid)
1056 {
1057 $handler_name = $term_object->name;
1058 break;
1059 }
1060
1061 if ($handler_name == '')
1062 throw new Exception('Did you change the vocabulary name without re-assigning the feeds?',
1063 FEEDS_ARENT_ASSIGNED_TERM_UNDER_ACTIVE_VOCABULARY);
1064
1065 $data = aggregation_get_URL($feed->url, $feed->username, $feed->password, $feed,
1066 $feed->etag, $feed->last_modified);
1067
1068 if ($data === FALSE)
1069 {
1070 watchdog('aggregation', t("Feed \"%s\" was not modified since last refresh", array('%s' => $feed->title)));
1071 return NULL;
1072 }
1073
1074 $feed->etag = $data[1];
1075 $feed->last_modified = $data[2];
1076 $data = $data[0];
1077
1078 $feed_xml = aggregation_get_XML($data, $feed);
1079
1080 require_once(drupal_get_path('module', 'aggregation')."/feed_handlers/$handler_name.inc");
1081
1082 try {
1083 call_user_func_array('_aggregation_'.$handler_name.'_parse', array($feed_xml, $feed));
1084 } catch (Exception $e) {
1085 throw new Exception('Check your feed_handler! Also consider handling the exception in your code: <br />'.
1086 'All items after the exception have been skipped! Internal handling would prevent this.<br />'.
1087 'If you catch an exception, please save the object and throw it when you\'re done to preserve logging.<br >'.
1088 $e->getMessage(), $e->getCode());
1089 }
1090 db_query("UPDATE {aggregation_feed} SET last_refreshed = %d WHERE nid = %d", time(), $feed->nid);
1091 return TRUE;
1092 }
1093
1094 /**
1095 * This internal function is responsible for adding items
1096 */
1097 function _aggregation_add_item($title, $body, $teaser, $original_author, $feed,
1098 $additional_taxonomies, $timestamp = NULL, $original_item_url = NULL, $guid = NULL,
1099 $image_array = NULL)
1100 {
1101 if (!is_null($title)) $title = trim($title);
1102 if (!is_null($body)) $body = trim($body);
1103 if (!is_null($teaser)) $teaser = trim($teaser);
1104 if (!is_null($original_author)) $original_author = trim($original_author);
1105 if (!is_null($original_item_url)) $original_item_url = trim($original_item_url);
1106 if (!is_null($guid)) $guid = trim($guid);
1107
1108 if (is_array($image_array))
1109 {
1110 if (!is_null($image_array['url'])) $image_array['url'] = trim($image_array['url']);
1111 if (!$image_array['url'] || empty($image_array['url']))
1112 $image_array = NULL;
1113 else
1114 {
1115 if (!is_null($image_array['title'])) $image_array['title'] = trim($image_array['title']);
1116 if (!is_null($image_array['guid'])) $image_array['guid'] = trim($image_array['guid']);
1117 if (!is_null($image_array['teaser'])) $image_array['teaser'] = trim($image_array['teaser']);
1118 if (!is_null($image_array['body'])) $image_array['body'] = trim($image_array['body']);
1119 if (!is_null($image_array['url'])) $image_array['url'] = trim($image_array['url']);
1120
1121 if (is_null($image_array['title']) || empty($image_array['title']))
1122 $image_array['title'] = $title;
1123
1124 if (is_null($image_array['guid']) || empty($image_array['guid']))
1125 $image_array['guid'] = $image_array['url'];
1126
1127 if (is_null($image_array['body']) || empty($image_array['body']))
1128 $image_array['body'] = $image_array['title'];
1129
1130 if (is_null($image_array['teaser']) || empty($image_array['teaser']))
1131 $image_array['teaser'] = $image_array['title'];
1132
1133 if (is_null($image_array['timestamp']))
1134 $image_array['timestamp'] = time();
1135
1136 if (is_null($guid) || empty($guid))
1137 $guid = $image_array['url'];
1138 }
1139 }
1140
1141 if (!is_null($guid) && !empty($guid)) // check item exists by checking GUID
1142 {
1143 if (db_fetch_object(db_query("SELECT COUNT(nid) AS node_count FROM {aggregation_item} ".
11