Removing translation directories
[project/feedapi.git] / feedapi.module
1 <?php
2
3 /**
4 * @file
5 * Handle the submodules (for feed and item processing)
6 * Provide a basic management of feeds
7 */
8
9 define('FEEDAPI_NEVER_DELETE_OLD', 0);
10 define('FEEDAPI_TIMEOUT', 1);
11 // Number of feeds to process for each step in cron.
12 define('FEEDAPI_CRON_FEEDS', 100);
13 // Default time that should elapse before a feed can be refreshed again on cron.
14 define('FEEDAPI_CRON_DEFAULT_REFRESH_TIME', 1800);
15 // Denotes that a feed should never be refreshed.
16 define('FEEDAPI_CRON_NEVER_REFRESH', -1);
17 // Denotes that a feed should be refreshed as often as possible.
18 define('FEEDAPI_CRON_ALWAYS_REFRESH', 0);
19 // Prune FeedAPI stats 4 weeks
20 define('FEEDAPI_CRON_STAT_LIFETIME', 28*24*3600);
21
22 /**
23 * Implementation of hook_help().
24 */
25 function feedapi_help($path, $arg) {
26 switch ($path) {
27 case 'admin/help#feedapi':
28 $output = '<p>'. t('Provides feed management interface and handles underlying processors and parsers for any type of feeds.') .'</p>';
29 $output .= '<p>'. t('Feeds are based on content types. Default content types are created on install. You can create new content types on the <a href="@content-types">add content types</a> page. To do that, enable the "Is a feed content type" checkbox under the Feed API group on the content type edit form. Then choose the processors and parsers that you would like to use. At least one parser and one processor must be enabled.', array('@content-types' => url('admin/content/types/add'))) .'</p>';
30 return $output;
31 case 'admin/content/feed':
32 return '<p>'. t('Current feeds are listed below. For each FeedAPI-enabled content type, the <em>Quick create</em> block may be enabled at the <a href="@block">blocks administration page</a>.', array('@block' => url('admin/build/block'))) .'</p>';
33 case 'admin/content/feed/import_opml':
34 return '<p>'. t('Feeds can be imported from a valid OPML file. You can check your OPML file at <a href="@validator">OPML Validator</a>.', array('@validator' => url('http://validator.opml.org/'))) .'</p>';
35 case 'admin/settings/feedapi':
36 return '<p>'. t('You can find more configuration options on the content type edit form of FeedAPI-enabled <a href="@content-types">content types</a>.', array('@content-types' => url('admin/content/types'))) .'</p>';
37 }
38 }
39
40 /**
41 * Implementation of hook_theme().
42 */
43 function feedapi_theme() {
44 return array(
45 'feedapi_export_opml' => array(
46 'arguments' => array('feeds' => NULL),
47 ),
48 );
49 }
50
51 /**
52 * Implementation of hook_menu().
53 */
54 function feedapi_menu() {
55 $items = array();
56 $items['admin/content/feed'] = array(
57 'title' => 'Feeds',
58 'description' => 'Overview which content your site aggregates from other sites and see detailed statistics about the feeds.',
59 'page callback' => 'feedapi_admin_overview',
60 'access arguments' => array('administer feedapi'),
61 'file' => 'feedapi.admin.inc',
62 );
63 $items['admin/content/feed/list'] = array(
64 'title' => 'List',
65 'type' => MENU_DEFAULT_LOCAL_TASK,
66 'access arguments' => array('administer feedapi'),
67 'weight' => -15,
68 );
69 $items['admin/content/feed/import_opml'] = array(
70 'title' => 'Import OPML',
71 'access arguments' => array('administer feedapi'),
72 'page callback' => 'drupal_get_form',
73 'page arguments' => array('feedapi_import_opml'),
74 'file' => 'feedapi.opml.inc',
75 );
76 $items['admin/content/feed/export_opml'] = array(
77 'title' => 'Export all feeds as OPML',
78 'access arguments' => array('administer feedapi'),
79 'page callback' => 'feedapi_export_opml',
80 'file' => 'feedapi.opml.inc',
81 );
82 $items['admin/settings/feedapi'] = array(
83 'title' => 'FeedAPI',
84 'description' => 'Configure advanced options for FeedAPI module.',
85 'page callback' => 'drupal_get_form',
86 'page arguments' => array('feedapi_admin_settings'),
87 'access arguments' => array('administer feedapi'),
88 'file' => 'feedapi.admin.inc',
89 );
90
91 $items['node/%node/refresh'] = array(
92 'title' => 'Refresh',
93 'page callback' => 'feedapi_refresh',
94 'page arguments' => array(1),
95 'type' => MENU_LOCAL_TASK,
96 'access callback' => '_feedapi_op_access',
97 'access arguments' => array(1),
98 );
99 $items['node/%node/purge'] = array(
100 'title' => 'Remove items',
101 'page callback' => 'feedapi_invoke',
102 'page arguments' => array("purge", 1, 'items'),
103 'type' => MENU_LOCAL_TASK,
104 'access callback' => '_feedapi_op_access',
105 'access arguments' => array(1),
106 );
107 return $items;
108 }
109
110 function _feedapi_op_access($node) {
111 if (!feedapi_enabled_type($node->type)) {
112 return FALSE;
113 }
114 global $user;
115 $own_feed = $node->uid == $user->uid && user_access('edit own '. $node->type .' content') ? TRUE : FALSE;
116 return user_access('administer feedapi') || $own_feed;
117 }
118
119 /**
120 * Implementation of hook_nodeapi().
121 */
122 function feedapi_nodeapi(&$node, $op, $teaser, $page) {
123 if (isset($node->feed) || feedapi_enabled_type($node->type)) {
124 switch ($op) {
125 case 'validate':
126 $node->feed->settings = feedapi_get_settings($node->type);
127 $node->feed->parsers = _feedapi_format_settings($node->feed->settings, 'parsers');
128 $node->feed->processors = _feedapi_format_settings($node->feed->settings, 'processors');
129 if (count($node->feed->parsers) < 1) {
130 if (user_access('administer content types')) {
131 form_set_error('', t('There are no enabled parsers for this content type. In order to import feed items, you need to select a feed parser from the <a href="@url">content type settings</a>.', array('@url' => url("admin/content/node-type/$node->type"))));
132 }
133 else {
134 form_set_error('', t('There is no parser enabled for this content-type. Contact your site administrator for help.'));
135 }
136 }
137 if (count($node->feed->processors) < 1) {
138 if (user_access('administer content types')) {
139 form_set_error('', t('There are no enabled processors for this content type. In order to import feed items, you need to select a processor from the <a href="@url">content type settings</a>.', array('@url' => url("admin/content/node-type/$node->type"))));
140 }
141 else {
142 form_set_error('', t('There is no processor enabled for this content-type. Contact your site administrator for help.'));
143 }
144 }
145 break;
146 case 'insert':
147 _feedapi_insert($node);
148 break;
149 case 'update':
150 _feedapi_update($node);
151 break;
152 case 'load':
153 if ($feed = db_fetch_object(db_query('SELECT * FROM {feedapi} WHERE vid = %d', $node->vid))) {
154 $node->feed = $feed;
155 $node->feed->vid = $node->vid;
156 $node->feed->nid = $node->nid;
157 $node->feed->settings = feedapi_get_settings($node->type, $node->vid);
158 // Load parsers and processors from content type
159 $node_type_settings = feedapi_get_settings($node->type);
160 $node->feed->parsers = _feedapi_format_settings($node_type_settings, 'parsers');
161 $node->feed->processors = _feedapi_format_settings($node_type_settings, 'processors');
162 }
163 break;
164 case 'delete':
165 // Could be a performance problem - think of thousands of node feed items.
166 // This is a temporary status. See: http://drupal.org/node/195723
167 // feedapi_invoke('purge', $node->feed);
168 db_query("DELETE FROM {feedapi_stat} WHERE id = %d", $node->nid);
169 db_query("DELETE FROM {feedapi} WHERE nid = %d", $node->nid);
170 break;
171 case 'presave':
172 if (is_array($node->feedapi) || isset($node->feedapi_object)) {
173 $node->feed = isset($node->feedapi_object) ? $node->feedapi_object : _feedapi_build_feed_object($node->type, $node->feedapi['feedapi_url']);
174 }
175 break;
176 case 'delete revision':
177 db_query("DELETE FROM {feedapi} WHERE nid = %d AND vid = %d", $node->nid, $node->vid);
178 break;
179 }
180 }
181 }
182
183 /**
184 * Implementation of hook_node_type().
185 */
186 function feedapi_node_type($op, $info) {
187 switch ($op) {
188 case 'delete':
189 variable_del('feedapi_settings_'. $info->type);
190 variable_del('feedapi_'. $info->type);
191 break;
192 case 'update':
193 if (!empty($info->old_type) && $info->old_type != $info->type) {
194 $setting = variable_get('feedapi_settings_'. $info->old_type, array());
195 variable_del('feedapi_settings_'. $info->old_type);
196 variable_set('feedapi_settings_'. $info->type, $setting);
197 }
198 break;
199 }
200 }
201
202 /**
203 * Implementation of hook_block().
204 */
205 function feedapi_block($op = 'list', $delta = 0) {
206 $blocks = array();
207 $names = feedapi_get_types();
208 switch ($op) {
209 case 'list':
210 foreach ($names as $type => $name) {
211 $blocks[$type]['info'] = t('FeedAPI: Quick create !preset', array('!preset' => $name));
212 $blocks[$type]['cache'] = BLOCK_CACHE_GLOBAL;
213 }
214 break;
215 case 'view':
216 if (node_access('create', $delta)) {
217 $blocks['subject'] = t('Create !preset', array('!preset' => $names[$delta]));
218 $blocks['content'] = drupal_get_form('feedapi_simplified_form', $delta);
219 }
220 break;
221 }
222 return $blocks;
223 }
224
225 /**
226 * Implementation of hook_perm().
227 */
228 function feedapi_perm() {
229 return array('administer feedapi', 'advanced feedapi options', 'use local files as feeds');
230 }
231
232 /**
233 * Implementation of hook_link().
234 */
235 function feedapi_link($type, $node = NULL) {
236 if ($type == 'node' && isset($node->feed)) {
237 if (strlen($node->feed->link) > 0) {
238 $links['feedapi_original'] = array(
239 'title' => t('Link to site'),
240 'href' => $node->feed->link,
241 );
242 return $links;
243 }
244 }
245 }
246
247 /**
248 * Implementation of hook_node_views().
249 */
250 function feedapi_views_api() {
251 return array(
252 'api' => 2,
253 'path' => drupal_get_path('module', 'feedapi') .'/views',
254 );
255 }
256
257 /**
258 * Invoke feedapi API callback functions.
259 *
260 * @param $op
261 * "load" Load the feed items basic data into the $feed->items[]
262 * "refresh" Re-download the feed and process newly arrived item
263 * "purge" Delete all the feed items
264 *
265 * @param $feed
266 * A feed object. If only the ID is known, you should pass something like this: $feed->nid = X
267 * @param $param
268 * Depends on the $op value.
269 */
270 function feedapi_invoke($op, &$feed, $param = NULL) {
271 if (!is_object($feed)) {
272 return FALSE;
273 }
274 // The node is passed.
275 if (isset($feed->feed) && is_object($feed->feed)) {
276 $feed = $feed->feed;
277 }
278 if (!isset($feed->processors)) {
279 $node = node_load($feed->nid);
280 if (!isset($node->feed)) {
281 return FALSE;
282 }
283 $feed = $node->feed;
284 }
285 _feedapi_sanitize_processors($feed);
286
287 switch ($op) {
288 case 'refresh':
289 return _feedapi_invoke_refresh($feed, $param);
290 case 'purge':
291 return _feedapi_invoke_purge($feed, $param);
292 default: // Other operations
293 return _feedapi_invoke($op, $feed, $param);
294 }
295 }
296
297 /**
298 * Ask for confirmation before deleting all the items
299 */
300 function feedapi_purge_confirm($form_state, $node) {
301 $output = confirm_form(
302 array('nid' => array('#type' => 'hidden', '#value' => $node->nid)),
303 t('Delete all the feed items from !name', array('!name' => $node->title)),
304 isset($_GET['destination']) ? $_GET['destination'] : 'node/'. $node->nid,
305 t("Are you sure you want to delete all the feed items from !name?", array('!name' => $node->title)),
306 t('Yes'), t('No'),
307 'feedapi_purge_confirm'
308 );
309 return $output;
310 }
311
312 /**
313 * Submitted items purging form. Drop all the items.
314 */
315 function feedapi_purge_confirm_submit($form, &$form_state) {
316 $feed->nid = $form_state['values']['nid'];
317 feedapi_invoke('purge', $feed);
318 $form_state['redirect'] = 'node/'. $form_state['values']['nid'];
319 }
320
321 /**
322 * Delete expired items and return informations about the feed refreshing
323 *
324 * @param $feed
325 * The feed object
326 * @param $settings
327 * Optional feed settings
328 * @return
329 * FALSE if the feed don't have to be refreshed. (forbidden if the $force is TRUE)
330 */
331 function feedapi_expire($feed, $settings = NULL) {
332 // Backwards compatibility, get settings if not passed
333 $settings = is_null($settings) ? feedapi_get_settings(NULL, $feed->vid) : $settings;
334 // Each processor can have its own expiration criteria ?
335 $expired = _feedapi_invoke('expire', $feed, $settings);
336 // Return the number of expired items
337 return $expired ? array_sum($expired) : 0;
338 }
339
340 /**
341 * Callback for expired items. Does the actual deleting
342 */
343 function feedapi_expire_item($feed, $item) {
344 foreach ($feed->processors as $processor) {
345 module_invoke($processor, 'feedapi_item', 'delete', $item, $feed->nid);
346 }
347 }
348
349 /**
350 * Implementation of hook_form_alter().
351 */
352 function feedapi_form_alter(&$form, $form_state, $form_id) {
353 // Content type form.
354 if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
355 $node_type_settings = feedapi_get_settings($form['#node_type']->type);
356
357 $form['#validate'][] = 'feedapi_content_type_validate';
358
359 // Don't blow away existing form elements.
360 if (!isset($form['feedapi'])) {
361 $form['feedapi'] = array();
362 }
363 $form['feedapi'] += array(
364 '#type' => 'fieldset',
365 '#title' => t('Feed API'),
366 '#collapsible' => TRUE,
367 '#collapsed' => isset($node_type_settings['enabled']) ? !($node_type_settings['enabled']) : TRUE,
368 '#tree' => TRUE,
369 );
370 $form['feedapi']['enabled'] = array(
371 '#type' => 'checkbox',
372 '#title' => t('Is a feed content type'),
373 '#description' => t('Check if you want to use this content type for downloading feeds to your site.'),
374 '#default_value' => isset($node_type_settings['enabled']) ? $node_type_settings['enabled'] : FALSE,
375 '#weight' => -15,
376 );
377 $form['feedapi']['upload_method'] = array(
378 '#type' => 'radios',
379 '#title' => t('Supply feed as'),
380 '#description' => t('Select how a user will supply a feed. Choose URL if the user will paste a URL to a textfield, choose File upload if the user will upload a feed from the local disk.'),
381 '#options' => array('url' => t('URL'), 'upload' => t('File upload')),
382 '#default_value' => isset($node_type_settings['upload_method']) ? $node_type_settings['upload_method'] : 'url',
383 '#weight' => -14,
384 );
385 $modules = module_implements('feedapi_settings_form');
386 foreach ($modules as $module) {
387 $form['feedapi']['defaults'] = array('#type' => 'markup', '#value' => '<strong>'. t('Default settings') .'</strong><hr/>');
388 if ($feedapi_form = module_invoke($module, 'feedapi_settings_form', 'general')) {
389 $form['feedapi'] = array_merge_recursive($form['feedapi'], $feedapi_form);
390 }
391 }
392 $form['feedapi']['parsers'] = array(
393 '#type' => 'fieldset',
394 '#title' => t('Parser settings'),
395 '#description' => t('Parsers turn a feed into an object ready for processing. Choose at least one.'),
396 '#collapsible' => FALSE,
397 '#tree' => TRUE,
398 );
399 $parsers = module_implements('feedapi_feed', TRUE);
400 rsort($parsers);
401 foreach ($parsers as $parser) {
402 $form['feedapi']['parsers'][$parser] = array(
403 '#type' => 'fieldset',
404 '#title' => feedapi_get_natural_name($parser),
405 '#collapsible' => TRUE,
406 '#collapsed' => isset($node_type_settings['parsers'][$parser]['enabled']) ? !($node_type_settings['parsers'][$parser]['enabled']) : TRUE,
407 '#tree' => TRUE,
408 '#weight' => isset($node_type_settings['parsers'][$parser]['weight']) ? $node_type_settings['parsers'][$parser]['weight']: 0,
409 );
410 $form['feedapi']['parsers'][$parser]['enabled'] = array(
411 '#type' => 'checkbox',
412 '#title' => t('Enable'),
413 '#description' => t('Check this box if you want to enable the @name parser on this feed.', array('@name' => $parser)),
414 '#default_value' => isset($node_type_settings['parsers'][$parser]['enabled']) ? $node_type_settings['parsers'][$parser]['enabled'] : FALSE,
415 '#weight' => -15,
416 );
417 $form['feedapi']['parsers'][$parser]['weight'] = array(
418 '#type' => 'weight',
419 '#delta' => 15,
420 '#title' => t('Weight'),
421 '#description' => t('Control the execution order. Parsers with lower weights are called before parsers with higher weights.'),
422 '#default_value' => isset($node_type_settings['parsers'][$parser]['weight']) ? $node_type_settings['parsers'][$parser]['weight'] : 0,
423 '#weight' => -14,
424 );
425 if ($parser_form = module_invoke($parser, 'feedapi_settings_form', 'parsers')) {
426 $form['feedapi']['parsers'][$parser]['defaults'] = array('#type' => 'markup', '#value' => '<strong>'. t('Default settings') .'</strong><hr/>');
427 $form['feedapi']['parsers'][$parser] = array_merge_recursive($form['feedapi']['parsers'][$parser], $parser_form);
428 }
429 }
430 $form['feedapi']['processors'] = array(
431 '#type' => 'fieldset',
432 '#title' => t('Processor settings'),
433 '#description' => t('Processors are any kind of add on modules that hook into the feed handling process on download time - you can decide here what should happen to feed items once they are downloaded and parsed.'),
434 '#collapsible' => FALSE,
435 '#tree' => TRUE,
436 );
437 $processors = module_implements('feedapi_item', TRUE);
438 rsort($processors);
439 foreach ($processors as $processor) {
440 $form['feedapi']['processors'][$processor] = array(
441 '#type' => 'fieldset',
442 '#title' => feedapi_get_natural_name($processor),
443 '#collapsible' => TRUE,
444 '#collapsed' => isset($node_type_settings['processors'][$processor]['enabled']) ? !($node_type_settings['processors'][$processor]['enabled']): TRUE,
445 '#tree' => TRUE,
446 '#weight' => isset($node_type_settings['processors'][$processor]['weight']) ? $node_type_settings['processors'][$processor]['weight'] : 0,
447 );
448 $form['feedapi']['processors'][$processor]['enabled'] = array(
449 '#type' => 'checkbox',
450 '#title' => t('Enable'),
451 '#description' => t('Check this box if you want to enable the @name processor on this feed.', array('@name' => $processor)),
452 '#default_value' => isset($node_type_settings['processors'][$processor]['enabled']) ? $node_type_settings['processors'][$processor]['enabled'] : FALSE,
453 '#weight' => -15,
454 );
455 $form['feedapi']['processors'][$processor]['weight'] = array(
456 '#type' => 'weight',
457 '#delta' => 15,
458 '#title' => t('Weight'),
459 '#description' => t('Control the execution order. Processors with lower weights are called before processors with higher weights.'),
460 '#default_value' => isset($node_type_settings['processors'][$processor]['weight']) ? $node_type_settings['processors'][$processor]['weight'] : 0,
461 '#weight' => -14,
462 );
463 if ($processor_form = module_invoke($processor, 'feedapi_settings_form', 'processors')) {
464 $form['feedapi']['processors'][$processor]['defaults'] = array('#type' => 'markup', '#value' => '<strong>'. t('Default settings') .'</strong><hr/>');
465 $form['feedapi']['processors'][$processor] = array_merge_recursive($form['feedapi']['processors'][$processor], $processor_form);
466 }
467 }
468 // Populate form with node type settings if available.
469 if ($node_type_settings) {
470 $form['feedapi'] = _feedapi_populate($form['feedapi'], $node_type_settings);
471 }
472 $form['#submit'][] = 'feedapi_content_type_submit';
473 }
474 elseif (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id && feedapi_enabled_type($form['type']['#value'])) {
475 // Get settings for corresponding content type
476 // Which parsers / processors are enabled is a per content-type setting.
477 $node_type_settings = feedapi_get_settings($form['type']['#value']);
478
479 // FeedAPI-enabled node form.
480 $form['title']['#required'] = FALSE;
481 $form['title']['#description'] = t('This field will be populated with the feed title. You can override by filling in this field.');
482 $form['body_field']['body']['#description'] = t('This field will be populated with the feed description. You can override by filling in this field.');
483 $form['body_field']['body']['#rows'] = 2;
484
485 // Don't blow away existing form elements.
486 if (!isset($form['feedapi'])) {
487 $form['feedapi'] = array();
488 }
489 $form['feedapi'] += array(
490 '#type' => 'fieldset',
491 '#title' => t('Feed'),
492 '#collapsible' => TRUE,
493 '#collapsed' => FALSE,
494 '#tree' => TRUE,
495 );
496 $feedapi_url_default = '';
497 if (isset($form['#node']->feed->url)) {
498 $feedapi_url_default = $form['#node']->feed->url;
499 }
500 elseif (isset($form_state['values']['feedapi']['feedapi_url'])) {
501 $feedapi_url_default = $form_state['values']['feedapi']['feedapi_url'];
502 }
503 if (isset($node_type_settings['upload_method']) && $node_type_settings['upload_method'] == 'upload') {
504 // Makes possible to upload file via this form.
505 $form['#attributes']['enctype'] = 'multipart/form-data';
506 $form['feedapi']['feedapi_file'] = array(
507 '#type' => 'file',
508 '#title' => t('Upload a feed'),
509 '#description' => $feedapi_url_default ? '<div class="feed-url">'. $feedapi_url_default .'</div>' : '',
510 '#size' => 40,
511 );
512 $form['feedapi']['feedapi_url'] = array(
513 '#type' => 'value',
514 '#value' => $feedapi_url_default,
515 );
516 }
517 else {
518 $form['feedapi']['feedapi_url'] = array(
519 '#type' => 'textfield',
520 '#title' => t('Feed URL'),
521 '#description' => t('Enter feed URL. The set of supported schemas (e.g. ftp://, http://) depends on the parser that you use.'),
522 '#default_value' => $feedapi_url_default,
523 '#maxlength' => 2048,
524 );
525 }
526 // Show per-node-type feedapi, parser options only for users with permissions.
527 if (user_access('advanced feedapi options')) {
528 // retrieve forms.
529 $modules = module_implements('feedapi_settings_form');
530 foreach ($modules as $module) {
531 if ($feedapi_form = module_invoke($module, 'feedapi_settings_form', 'general')) {
532 $form['feedapi'] = array_merge_recursive($form['feedapi'], $feedapi_form);
533 }
534 }
535
536 $submodules_names = array(
537 'parsers' => t('Parsers'),
538 'processors' => t('Processors'),
539 );
540 foreach (array("parsers" => "feedapi_feed", "processors" => "feedapi_item") as $type => $requirement) {
541 $suitable_handlers = module_implements($requirement, TRUE);
542 foreach ($suitable_handlers as $module) {
543 if (isset($node_type_settings[$type][$module]) && $node_type_settings[$type][$module]['enabled']) {
544 $result = array();
545 $result = module_invoke($module, 'feedapi_settings_form', $type);
546 if (is_array($result)) {
547 $result['#weight'] = $node_type_settings[$type][$module]['weight'];
548 $form['feedapi'][$type][$module] = $result;
549 $form['feedapi'][$type][$module]['#type'] = 'fieldset';
550 $form['feedapi'][$type][$module]['#title'] = feedapi_get_natural_name($module);
551 $form['feedapi'][$type][$module]['#collapsible'] = TRUE;
552 $form['feedapi'][$type][$module]['#collapsed'] = FALSE;
553 $form['feedapi'][$type][$module]['#tree'] = TRUE;
554 }
555 }
556 }
557 if (isset($form['feedapi'][$type])) {
558 $form['feedapi'][$type]['#type'] = 'fieldset';
559 $form['feedapi'][$type]['#title'] = $submodules_names[$type];
560 $form['feedapi'][$type]['#collapsible'] = TRUE;
561 $form['feedapi'][$type]['#collapsed'] = TRUE;
562 $form['feedapi'][$type]['#tree'] = TRUE;
563 }
564 }
565 }
566 // If we are on a node form, get per node settings and populate form.
567 if (isset($form['#node']->nid)) {
568 $settings = feedapi_get_settings($form['type']['#value'], $form['#node']->vid);
569 }
570 elseif (isset($node_type_settings)) {
571 $settings = $node_type_settings;
572 }
573 if (isset($settings)) {
574 $form['feedapi'] = _feedapi_populate($form['feedapi'], $settings);
575 }
576 $form['#validate'][] = 'feedapi_node_validate';
577 }
578 }
579
580 /**
581 * Build feed object on validate and submit.
582 * See feedapi_form_alter on finding out how it is called (via FormAPI)
583 */
584 function feedapi_node_validate($form, &$form_state) {
585 // Don't validate when deleting.
586 if ($form_state['values']['op'] == t('Delete')) {
587 return TRUE;
588 }
589 // Upload file.
590 $feed_dir = file_directory_path() .'/feeds';
591 file_check_directory($feed_dir, TRUE);
592 $file = file_save_upload('feedapi', array(), $feed_dir);
593 $has_upload = is_object($file);
594
595 // Validate and transform settings for submission.
596 if (empty($form_state['values']['feedapi']['feedapi_url']) && !$has_upload) {
597 form_set_error('source', t('The Feed URL or uploading a file is required.'));
598 }
599 else if (!empty($form_state['values']['feedapi']['feedapi_url']) && !$has_upload && (strpos($form_state['values']['feedapi']['feedapi_url'], 'file://') === 0) && !user_access('use local files as feeds')) {
600 form_set_error('source', t('You do not have sufficient permissions to use local files as feeds.'));
601 }
602 else if (strpos($form_state['values']['feedapi']['feedapi_url'], 'file://') === 0 && !file_check_location(substr($form_state['values']['feedapi']['feedapi_url'], 7), file_directory_path())) {
603 drupal_set_message(file_check_location(substr($form_state['values']['feedapi']['feedapi_url'], 7), file_directory_path()));
604 form_set_error('source', t('file:// is only allowed for files under the files directory.'));
605 }
606 else {
607 if ($has_upload) {
608 $form_state['values']['feedapi']['feedapi_url'] = file_create_url($file->filepath);
609 }
610 $feed = _feedapi_build_feed_object($form_state['values']['type'], $form_state['values']['feedapi']['feedapi_url']);
611 if (!isset($feed->title) && $has_upload) {
612 $form_state['values']['feedapi']['feedapi_url'] = NULL;
613 }
614 // Stick feed object into feedapi form snippet - store it in submit.
615 $form_state['values']['feedapi_object'] = $feed;
616 if ($has_upload) {
617 $form_state['values']['feedapi_object']->url = str_replace($GLOBALS['base_url'], '', $form_state['values']['feedapi_object']->url);
618 }
619 if (empty($form_state['values']['title']) && isset($feed->title)) {
620 form_set_value($form['title'], $feed->title, $form_state);
621 }
622 if (isset($form['body_field']) && empty($form_state['values']['body']) && isset($feed->description)) {
623 form_set_value($form['body_field']['body'], $feed->description, $form_state);
624 }
625 if (empty($form_state['values']['title'])) {
626 if (!$has_upload) {
627 form_set_error('title', t('Title could not be retrieved from feed.'));
628 }
629 else {
630 form_set_error('title', t('Title could not be detected. Make sure that the uploaded file is a valid feed.'));
631 }
632 }
633 elseif ($has_upload) {
634 file_set_status($file, FILE_STATUS_PERMANENT);
635 }
636 }
637 }
638
639 /**
640 * Store per-content-type settings
641 */
642 function feedapi_content_type_submit($form, &$form_state) {
643 // TODO: Drupal automatically stores mutilated 'feedapi_'. $form['#node_type']->type - remove.
644 $type = !empty($form['#node_type']->type) ? $form['#node_type']->type : $form['#post']['type'];
645 _feedapi_store_settings(array('node_type' => $type), $form_state['values']['feedapi']);
646 }
647
648 /**
649 * Implementation of hook_feedapi_settings_form().
650 */
651 function feedapi_feedapi_settings_form($type) {
652 if ($type == 'general') {
653 $form['refresh_on_create'] = array(
654 '#type' => 'checkbox',
655 '#title' => t('Refresh feed on creation'),
656 '#description' => t('If checked, feed items will be processed immediately after a feed is created.'),
657 '#default_value' => 0,
658 );
659 $form['update_existing'] = array(
660 '#type' => 'checkbox',
661 '#title' => t('Update existing feed items'),
662 '#description' => t('If checked, existing feed items will be updated when feed is refreshed.'),
663 '#default_value' => 1,
664 );
665 $period = array();
666 $period[FEEDAPI_CRON_ALWAYS_REFRESH] = t('As often as possible');
667 $period += drupal_map_assoc(array(900, 1800, 3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval');
668 $period[FEEDAPI_CRON_NEVER_REFRESH] = t('Never refresh');
669 $form['refresh_time'] = array(
670 '#type' => 'select',
671 '#title' => t('Minimum refresh period'),
672 '#description' => t('Select the minimum time that should elapse between two refreshes of the same feed. For news feeds, don\'t go under 30 minutes. Note that FeedAPI cannot guarantee that a feed will be refreshed at the rate of the selected time. The actual refresh rate depends on many factors such as number of feeds in system and your hardware.'),
673 '#options' => $period,
674 '#default_value' => FEEDAPI_CRON_DEFAULT_REFRESH_TIME,
675 );
676 $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 3628800, 4838400, 7257600, 15724800, 31536000), 'format_interval');
677 $period[FEEDAPI_NEVER_DELETE_OLD] = t('Never delete');
678 $form['items_delete'] = array(
679 '#type' => 'select',
680 '#title' => t('Delete news items older than'),
681 '#options' => $period,
682 '#default_value' => FEEDAPI_NEVER_DELETE_OLD,
683 );
684 }
685 return $form;
686 }
687
688 /**
689 * Implementation of hook_cron().
690 */
691 function feedapi_cron() {
692 global $user;
693
694 // Saves the currently logged in user and start safe impersonating
695 $original_user = $user;
696 session_save_session(FALSE);
697
698 db_query('DELETE FROM {feedapi_stat} WHERE timestamp < %d', variable_get('cron_semaphore', FALSE) - FEEDAPI_CRON_STAT_LIFETIME);
699
700 // Initialize counters
701 $count = array(
702 '%feeds' => 0,
703 '%expired' => 0,
704 '%new' => 0,
705 '%updated' => 0,
706 );
707
708 // We get feeds in small lots, this will save memory and have the process adjusting to the
709 // time limit even when we have many thousands of them.
710 $now = time();
711 $process = 0;
712 // The counter process will be > 0 if we've selected less feeds
713 while (!$process && feedapi_cron_time()) {
714 $process = FEEDAPI_CRON_FEEDS;
715 $result = db_query_range("SELECT f.nid, n.uid FROM {feedapi} f JOIN {node} n ON n.vid = f.vid WHERE next_refresh_time <= %d AND next_refresh_time <> %d ORDER BY next_refresh_time ASC", $now, FEEDAPI_CRON_NEVER_REFRESH, 0, FEEDAPI_CRON_FEEDS);
716
717 while (feedapi_cron_time() && $feed = db_fetch_object($result)) {
718 $user = user_load(array('uid' => $feed->uid));
719 // Call the refresh process for each feed and store counters
720 $counter = feedapi_invoke('refresh', $feed, TRUE);
721 if ($counter) {
722 foreach ($counter as $name => $value) {
723 $count['%'. $name] += $value;
724 }
725 }
726 $count['%feeds']++;
727 $process--;
728 }
729 }
730
731 // Loads back the logged in user
732 $user = $original_user;
733 session_save_session(TRUE);
734 }
735
736 /**
737 * Check for time limits in cron processing.
738 *
739 * @return
740 * Number of seconds left, zero if none.
741 */
742 function feedapi_cron_time() {
743 static $time_limit;
744
745 if (!$time_limit) {
746 $max_exec_time = ini_get('max_execution_time') == 0 ? 120 : ini_get('max_execution_time');
747 $time_limit = time() + (variable_get('feedapi_cron_percentage', 15) / 100) * $max_exec_time;
748 // However, check for left time, maybe some other cron processing already occured
749 $cron_semaphore = variable_get('cron_semaphore', 0);
750 if ($cron_semaphore) {
751 $time_limit = min($time_limit, $cron_semaphore + $max_exec_time);
752 }
753 timer_start('feedapi_cron');
754 }
755 return max($time_limit - time(), 0);
756 }
757
758 /**
759 * This is shown instead of normal node form when the simplified form is chosen at the settings
760 */
761 function feedapi_simplified_form($form_state, $type) {
762 $form['node']['#tree'] = TRUE;
763 $form['node']['type'] = array(
764 '#type' => 'hidden',
765 '#value' => $type
766 );
767 $form['url'] = array(
768 '#title' => t('Feed URL'),
769 '#type' => 'textfield',
770 '#size' => 25,
771 '#required' => TRUE,
772 '#maxlength' => 2048,
773 );
774 $form['add'] = array(
775 '#type' => 'submit',
776 '#value' => t('Add'),
777 );
778 return $form;
779 }
780
781 /**
782 * Validates simplified form.
783 */
784 function feedapi_simplified_form_validate($form, &$form_state) {
785 if (!empty($form_state['values']['url']) && (strpos($form_state['values']['url'], 'file://') === 0) && !user_access('use local files as feeds')) {
786 form_set_error('url', t('You do not have sufficient permissions to use local files as feeds.'));
787 }
788 else if (strpos($form_state['values']['url'], 'file://') === 0 && !file_check_location(substr($form_state['values']['feedapi']['feedapi_url'], 7), file_directory_path())) {
789 form_set_error('url', t('file:// is only allowed for files under the files directory.'));
790 }
791 }
792
793 /**
794 * Create the node object and save
795 */
796 function feedapi_simplified_form_submit($form, &$form_state) {
797 $node_template = (object)$form_state['values']['node'];
798 $feed_type = (string)$_POST['node']['type'];
799 $valid_types = array_keys(feedapi_get_types());
800 foreach ($valid_types as $type) {
801 if ($type === $feed_type) {
802 $node_template->type = $type;
803 }
804 }
805 if ($node = feedapi_create_node($node_template, $form_state['values']['url'])) {
806 drupal_set_message(t('Feed successfully created.'));
807 $form_state['redirect'] = 'node/'. $node->nid;
808 }
809 else {
810 drupal_set_message(t('Could not retrieve title from feed.'), 'error');
811 $form_state['redirect'] = array('node/add/'. $node_template->type, 'feedapi_url='. urlencode($form_state['values']['url']));
812 }
813 }
814
815 /**
816 * Get the module-defined natural name of FeedAPI parser or processor
817 * Define this name in hook_help():
818 *
819 * function hook_help($section) {
820 * switch ($section) {
821 * case 'feedapi/full_name':
822 * return t('Natural name');
823 * break;
824 * }
825 * }
826 */
827 function feedapi_get_natural_name($module) {
828 $help = $module .'_help';
829 $module_natural = function_exists($help) ? $help('feedapi/full_name', '') : $module;
830 return empty($module_natural) ? $module : $module_natural;
831 }
832
833 /**
834 * Create a feedapi node programatically.
835 *
836 * @param $param
837 * Either a feedapi - enabled node type or a $node object with at least valid $node->type.
838 * @param $url
839 * URI of feed.
840 */
841 function feedapi_create_node($param, $url) {
842 if (is_object($param)) {
843 $node = $param;
844 }
845 else {
846 $node = new stdClass();
847 $node->type = $param;
848 }
849 if (!feedapi_enabled_type($node->type)) {
850 return FALSE;
851 }
852 $feed = _feedapi_build_feed_object($node->type, $url);
853 if (!$feed->title && !$node->title) {
854 return FALSE;
855 }
856 module_load_include('inc', 'node', 'node.pages');
857 $node->title = $node->title ? $node->title : $feed->title;
858 $node->body = $node->body ? $node->body : $feed->description;
859 $node->feedapi_object = $feed;
860 // Get the content-type settings as default
861 $node->feedapi = feedapi_get_settings($node->type);
862 node_object_prepare($node);
863 global $user;
864 $node->uid = $user->uid;
865 node_save($node);
866 return $node;
867 }
868
869 /**
870 * Load node by URL.
871 * @param $args
872 * Currently only supported $args['url] - URL string.
873 * @return
874 * Node object if successful, FALSE if not.
875 */
876 function feedapi_load_node($args) {
877 if ($nid = db_result(db_query("SELECT nid FROM {feedapi} WHERE url = '%s'", $args['url']))) {
878 return node_load($nid);
879 }
880 return FALSE;
881 }
882
883 /**
884 * Refresh a feed node (= run enabled processors on it).
885 * @param $node
886 * A node object with a $node->feed object.
887 * @param $destination_path
888 * If a destination path is given, function redirects to this destination.
889 */
890 function feedapi_refresh($node, $destination_path = NULL) {
891 feedapi_invoke('refresh', $node->feed, FALSE);
892 if ($destination_path) {
893 drupal_goto($destination_path);
894 }
895 else {
896 drupal_goto('node/'. $node->nid);
897 }
898 }
899
900 /**
901 * Insert feedapi data to the DB when it's a new for for FeedAPI
902 */
903 function _feedapi_insert(&$node) {
904
905 if (isset($node->feed->url) && isset($node->feed->feed_type)) {
906 if (isset($node->feedapi)) {
907 $values = $node->feedapi;
908 }
909 else {
910 // On revert revision, settings are on $node->feed->settings
911 // @todo: verify, settings shouldn't be NOT an array here anyway.
912 $values = (array) $node->feed->settings;
913 }
914 db_query("INSERT INTO {feedapi} (
915 nid, vid, url, link, feed_type, processors,
916 parsers, next_refresh_time, settings) VALUES
917 (%d, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s')",
918 $node->nid,
919 $node->vid,
920 $node->feed->url,
921 isset($node->feed->options->link) ? $node->feed->options->link : '',
922 $node->feed->feed_type,
923 serialize($node->feed->processors),
924 serialize($node->feed->parsers),
925 $values['refresh_time'] == FEEDAPI_CRON_NEVER_REFRESH ? $values['refresh_time'] : time() + $values['refresh_time'],
926 serialize(array())
927 );
928 // Store add on module's settings if user has permission to do so.
929 if (user_access('advanced feedapi options')) {
930 _feedapi_store_settings(array('vid' => $node->vid), $values);
931 }
932 // Refresh feed if the user would like to do that
933 $settings = feedapi_get_settings($node->type, $node->vid);
934 if (isset($settings['refresh_on_create'])) {
935 if ($settings['refresh_on_create'] == TRUE) {
936 $node->feed->nid = $node->nid;
937 $node->feed->vid = $node->vid;
938 $node->feed->settings = $settings;
939 feedapi_invoke('refresh', $node->feed);
940 }
941 }
942 }
943 }
944
945 /**
946 * Update feed data of an existing feed
947 */
948 function _feedapi_update(&$node) {
949 if (isset($node->feed)) {
950 $old_config = node_load($node->nid);
951
952 // In that case this feed has never have feed data. Should be created then, this is not really an update
953 if (!is_numeric($old_config->feed->nid)) {
954 $url = isset($node->feed->url) ? $node->feed->url : $node->feedapi['feedapi_url'];
955 $node->feed = _feedapi_build_feed_object($node->type, $url);
956 _feedapi_insert($node);
957 return;
958 }
959 $old_vid = db_result(db_query_range("SELECT vid FROM {feedapi} WHERE nid = %d ORDER BY vid DESC", $node->nid, 0, 1));
960 if ($old_vid !== $node->vid) {
961 _feedapi_insert($node);
962 return;
963 }
964 // Only change next_refresh_time if refresh_time changed
965 // or if $next_refresh_time is FEEDAPI_CRON_NEVER_REFRESH.
966 $next_refresh_time = $old_config->feed->next_refresh_time;
967 if (isset($node->feedapi['refresh_time'])) {
968 if ($node->feedapi['refresh_time'] == FEEDAPI_CRON_NEVER_REFRESH) {
969 $next_refresh_time = FEEDAPI_CRON_NEVER_REFRESH;
970 }
971 elseif ($old_config->feed->settings['refresh_time'] != $node->feedapi['refresh_time'] || $next_refresh_time == FEEDAPI_CRON_NEVER_REFRESH) {
972 $next_refresh_time = time() + $node->feedapi['refresh_time'];
973 }
974 }
975
976 db_query("UPDATE {feedapi} SET
977 url = '%s',
978 feed_type = '%s',
979 processors = '%s',
980 parsers = '%s',
981 link = '%s',
982 next_refresh_time = %d
983 WHERE vid = %d",
984 isset($node->feed->url) ? $node->feed->url : $node->feedapi['feedapi_url'],
985 isset($node->feed->feed_type) ? $node->feed->feed_type : '',
986 isset($node->feed->processors) ? serialize($node->feed->processors) : serialize($old_config->feed->processors),
987 isset($node->feed->parsers) ? serialize($node->feed->parsers) : serialize($old_config->feed->parsers),
988 isset($node->feed->link) ? $node->feed->link : '',
989 $next_refresh_time,
990 $node->vid
991 );
992 // Store add on module's settings if user has permission to do so.
993 if (user_access('advanced feedapi options')) {
994 _feedapi_store_settings(array('vid' => $node->vid), $node->feedapi);
995 }
996 }
997 }
998
999 /**
1000 * Execute the enabled parsers and create an unified output
1001 *
1002 * @param $feed
1003 * Feed object
1004 * @param $parsers
1005 * Structure: array(
1006 * "primary" => "parser_primary",
1007 * "secondary" => array("parser1", "parser2", "parserN")
1008 * );
1009 * @return
1010 * The object of the parser data
1011 */
1012 function _feedapi_call_parsers($feed, $parsers, $settings) {
1013 $nid = isset($feed->nid) ? $feed->nid : '';
1014 $parser_primary = array_shift($parsers);
1015 $parsers_secondary = $parsers;
1016 // Normalize relative URLs according to the base URL (mostly for uploaded feeds), but allow other schemes too
1017 if (!valid_url($feed->url, TRUE) && valid_url($feed->url) && !strpos($feed->url, '://')) {
1018 $feed->url = $GLOBALS['base_url'] . $feed->url;
1019 }
1020 if (module_exists($parser_primary)) {
1021 $settings_primary = isset($settings[$parser_primary]) ? $settings[$parser_primary] : array();
1022 $feed->feed_type = module_invoke($parser_primary, 'feedapi_feed', 'compatible', $feed, $settings_primary);
1023 $parser_output = module_invoke($parser_primary, 'feedapi_feed', 'parse', $feed, $settings_primary);
1024 if ($parser_output === FALSE) {
1025 return $feed;
1026 }
1027 $feed = (object) array_merge((array) $feed, (array) $parser_output);
1028 }
1029 // Call the turned on parsers, create a union of returned options
1030 $parsers_secondary = is_array($parsers_secondary) ? $parsers_secondary : array();
1031 foreach ($parsers_secondary as $parser) {
1032 $settings_secondary = isset($settings[$parser]) ? $settings[$parser] : array();
1033 $feed_ext = module_invoke($parser, 'feedapi_feed', 'parse', $feed, $settings_secondary);
1034 $feed->options = (object) ((array) $feed->options + (array) $feed_ext->options);
1035 // Merge items' options
1036 if (is_array($feed_ext->items)) {
1037 foreach ($feed_ext->items as $key => $item) {
1038 $src = isset($feed->items[$key]) ? $feed->items[$key]->options : array();
1039 $feed->items[$key]->options = (object) ((array) $src + (array) $item->options);
1040 }
1041 }
1042 }
1043 $feed->nid = $nid;
1044 foreach (module_implements('feedapi_after_parse') as $module) {
1045 $func = $module .'_feedapi_after_parse';
1046 $func($feed);
1047 }
1048 // Filter bad or not allowed tags, sanitize data (currently timestamp checking)
1049 if (!variable_get('feedapi_allow_html_all', FALSE)) {
1050 $allowed = preg_split('/\s+|<|>/', variable_get('feedapi_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'), -1, PREG_SPLIT_NO_EMPTY);
1051 }
1052 else {
1053 $allowed = TRUE;
1054 }
1055 foreach (array('title', 'description') as $property) {
1056 if (isset($feed->{$property})) {
1057 if (is_string($feed->{$property})) {
1058 $feed->{$property} = _feedapi_process_text($feed->{$property}, $allowed);
1059 }
1060 }
1061 }
1062 if (isset($feed->options)) {
1063 $props = array_keys(get_object_vars($feed->options));
1064 foreach ($props as $property) {
1065 if (isset($feed->options->{$property})) {
1066 if (is_string($feed->options->{$property})) {
1067 $feed->options->{$property} = _feedapi_process_text($feed->options->{$property}, $allowed);
1068 }
1069 }
1070 }
1071 }
1072
1073 if (isset($feed->items)) {
1074 foreach (array_keys($feed->items) as $i) {
1075 $feed->items[$i]->title = _feedapi_process_text($feed->items[$i]->title, array());
1076 $feed->items[$i]->description = _feedapi_process_text($feed->items[$i]->description, $allowed);
1077 if ($feed->items[$i]->options->timestamp == 0) {
1078 $feed->items[$i]->options->timestamp = time();
1079 }
1080 }
1081 }
1082
1083 return $feed;
1084 }
1085
1086 /**
1087 * Filter texts from parsers
1088 *
1089 * @param $text
1090 * The text to be processed
1091 * @param $allowed
1092 * Allowed tags in that text
1093 * @return
1094 * The safe string
1095 */
1096 function _feedapi_process_text($text, $allowed) {
1097 if (is_array($allowed)) {
1098 $text = filter_xss($text, $allowed);
1099 }
1100 if (version_compare(PHP_VERSION, '5.0.0', '<')) {
1101 return trim(html_entity_decode($text, ENT_QUOTES));
1102 }
1103 else {
1104 return trim(html_entity_decode($text, ENT_QUOTES, 'UTF-8'));
1105 }
1106 }
1107
1108 /**
1109 * Stores settings per content type or per node.
1110 *
1111 * @param $args
1112 * Associative array which is $args['vid'] = N or $args['node_type'] = "content_type". Depends on what to store for
1113 * @param $settings
1114 * The settings data itself
1115 */
1116 function _feedapi_store_settings($args, $settings) {
1117 if (isset($args['vid'])) {
1118 db_query("UPDATE {feedapi} SET settings = '%s' WHERE vid = %d", serialize($settings), $args['vid']);
1119 module_invoke_all('feedapi_after_settings', $args['vid'], $settings);
1120 // This ensures that next time, not the cached, but the updated value will be used.
1121 feedapi_get_settings(NULL, $args['vid'], TRUE);
1122 }
1123 elseif (isset($args['node_type'])) {
1124 variable_set('feedapi_settings_'. $args['node_type'], $settings);
1125 }
1126 }
1127
1128 /**
1129 * Determines wether feedapi is enabled for given node type.
1130 * If parser or processor is passed in, this function determines wether given
1131 * parser or processor is enabled for given node type.
1132 * @param $node_type
1133 * A Drupal node type.
1134 * @param $parser_or_processor
1135 * A parser or processor - pass in by module name.
1136 * @return TRUE if enabled, FALSE if not.
1137 */
1138 function feedapi_enabled_type($node_type, $parser_or_processor = '') {
1139 $settings = feedapi_get_settings($node_type);
1140 if (empty($parser_or_processor)) {
1141 if (isset($settings['enabled'])) {
1142 return $settings['enabled'] ? TRUE : FALSE;
1143 }
1144 else {
1145 return FALSE;
1146 }
1147 }
1148 foreach (array('parsers', 'processors') as $stage) {
1149 if (isset($settings[$stage][$parser_or_processor]['enabled'])) {
1150 if ($settings[$stage][$parser_or_processor]['enabled'] == TRUE) {
1151 return TRUE;
1152 }
1153 }
1154 }
1155 return FALSE;
1156 }
1157
1158 /**
1159 * Helper function for feedapi_invoke().
1160 *
1161 * Generic operations, collects results and returns array
1162 */
1163 function _feedapi_invoke($op, &$feed, $param) {
1164 $output = array();
1165 foreach ($feed->processors as $processor) {
1166 $result = module_invoke($processor, 'feedapi_item', $op, $feed, $param);
1167 // Result may be a list of items or single values (count)
1168 if ($result) {
1169 if (is_array($result)) {
1170 $output = array_merge($output, $result);
1171 }
1172 else {
1173 $output[] = $result;
1174 }
1175 }
1176 }
1177 return $output;
1178 }
1179
1180 /**
1181 * Helper function for feedapi_invoke().
1182 * Refresh the feed, call the proper parsers and processors' hooks.
1183 * Don't call this function directly, use feedapi_refresh() instead.
1184 *
1185 * @ TODO Fix: This may loop forever when a feed has no processors
1186 */
1187 function _feedapi_invoke_refresh(&$feed, $param) {
1188 $timestamp = variable_get('cron_semaphore', FALSE) !== FALSE ? variable_get('cron_semaphore', FALSE) : time();
1189
1190 $counter = array();
1191 timer_start('feedapi_'. $feed->nid);
1192 $memory_usage_before = function_exists('memory_get_usage') ? memory_get_usage() : 0;
1193 $cron = $param;
1194
1195 // Step 0: Check processors and grab settings
1196 if (!is_array($feed->processors) || count($feed->processors) == 0) {
1197 if (!$cron) {
1198 drupal_set_message(t("No processors specified for URL %url. Could not refresh.", array('%url' => $feed->url)), "error");
1199 drupal_goto('node/'. $feed->nid);
1200 }
1201 return 0;
1202 }
1203 $settings = feedapi_get_settings(NULL, $feed->vid);
1204
1205 // Step 1: Force processors to delete old items and determine the max. create elements.
1206 $counter['expired'] = feedapi_expire($feed, $settings);
1207
1208 // Step 2: Get feed.
1209 $nid = $feed->nid;
1210 $hash_old = isset($feed->hash) ? $feed->hash : '';
1211 $feed = _feedapi_call_parsers($feed, $feed->parsers, $settings['parsers']);
1212 if (is_object($feed)) {
1213 $feed->hash = md5(serialize($feed->items));
1214 }
1215
1216 // Step 3: See, whether feed has been modified.
1217 if (!isset($feed->items) || $hash_old == $feed->hash) {
1218 // Updated the next_refresh_time field in any case.
1219 db_query("UPDATE {feedapi} SET next_refresh_time = %d, half_done = %d WHERE nid = %d", time() + $settings['refresh_time'], FALSE, $nid);
1220 if (!$cron) {
1221 if (is_object($feed) && $hash_old == $feed->hash) {
1222 drupal_set_message(t('There are no new items in the feed.'), 'status');
1223 }
1224 else {
1225 drupal_set_message(t('Could not refresh feed.'), 'error');
1226 }
1227 }
1228 return $counter;
1229 }
1230
1231 // Step 4: Walk through the items and check duplicates, then save or update
1232 $items = $feed->items;
1233 $updated = 0;
1234 $new = 0;
1235 $half_done = FALSE;
1236
1237 // We check for time-out after each item
1238 foreach ($items as $index => $item) {
1239 // Call each item parser.
1240 $item->is_updated = FALSE;
1241 $item->is_new = FALSE;
1242 foreach ($feed->processors as $processor) {
1243 $unique = module_invoke($processor, 'feedapi_item', 'unique', $item, $feed->nid, $settings['processors'][$processor]);
1244 if ($unique === FALSE || is_numeric($unique)) {
1245 if ($settings['update_existing'] == TRUE) {
1246 module_invoke($processor, 'feedapi_item', 'update', $item, $feed->nid, $settings['processors'][$processor], $unique);
1247 $item->is_updated = TRUE;
1248 }
1249 }
1250 else {
1251 // We have checked before for expired items, so just save it.
1252 // if the item is already expired then do nothing
1253 $items_delete = $settings['items_delete'];
1254 $diff = abs(time() - (isset($item->options->timestamp) ? $item->options->timestamp : time()));
1255 if ($diff > $items_delete && ($items_delete > FEEDAPI_NEVER_DELETE_OLD)) {
1256 break;
1257 }
1258 $result = module_invoke($processor, 'feedapi_item', 'save', $item, $feed->nid, $settings['processors'][$processor]);
1259 if ($result !== FALSE) {
1260 $item->is_new = TRUE;
1261 }
1262 }
1263 }
1264 $new = $item->is_new ? $new + 1 : $new;
1265 $updated = ($item->is_updated && !$item->is_new) ? $updated + 1 : $updated;
1266
1267 // Decision on time. If the exec time is greather than the user-set percentage of php max execution time
1268 if ($cron && !feedapi_cron_time()) {
1269 $half_done = ($new + $updated) == count($items) ? FALSE : TRUE;
1270 break;
1271 }
1272 // Save the item status for further processing
1273 $feed->items[$index] = $item;
1274 }
1275
1276 // Closing step: Call after refresh and update feed statistics
1277 foreach (module_implements('feedapi_after_refresh') as $module) {
1278 $func = $module .'_feedapi_after_refresh';
1279 $func($feed);
1280 }
1281
1282 // Set next_refresh_time to FEEDAPI_CRON_NEVER_REFRESH if refresh_time is FEEDAPI_CRON_NEVER_REFRESH.
1283 $next_refresh_time = $settings['refresh_time'] == FEEDAPI_CRON_NEVER_REFRESH ? $settings['refresh_time'] : (time() + $settings['refresh_time']);
1284 db_query("UPDATE {feedapi} SET next_refresh_time = %d, half_done = %d, hash = '%s' WHERE nid = %d", $next_refresh_time, $half_done, $feed->hash, $feed->nid);
1285
1286 // Log statistics.
1287 $memory_usage_after = function_exists('memory_get_usage') ? memory_get_usage() : 0;
1288 _feedapi_store_stat($nid, 'update_times', time(), $timestamp);
1289 _feedapi_store_stat($nid, 'new', $new, $timestamp);
1290 _feedapi_store_stat($nid, 'download_num', count($items), $timestamp);
1291 _feedapi_store_stat($nid, 'process_time', timer_read('feedapi_'. $feed->nid), $timestamp);
1292 _feedapi_store_stat($nid, 'memory_increase', $memory_usage_after - $memory_usage_before, $timestamp);
1293 _feedapi_store_stat($nid, 'next_refresh_time', $next_refresh_time, $timestamp);
1294
1295 if (!$cron) {
1296 if ($new == 0 && $updated == 0) {
1297 drupal_set_message(t('There are no new items in the feed.'), 'status');
1298 }
1299 else {
1300 drupal_set_message(t("%new new item(s) were saved. %updated existing item(s) were updated.", array("%new" => $new, "%updated" => $updated)));
1301 }
1302 // @ TODO what value to return here?
1303 }
1304 else {
1305 // Update and return counter
1306 $counter['new'] = $new;
1307 $counter['updated'] = $updated;
1308 return $counter;
1309 }
1310 }
1311
1312 /**
1313 * Helper function for feedapi_invoke().
1314 * Delete all feed items of a feed.
1315 */
1316 function _feedapi_invoke_purge(&$feed, $param) {
1317 $node = node_load($feed->nid);
1318
1319 if ($param == 'items') {
1320 return drupal_get_form('feedapi_purge_confirm', $node);
1321 }
1322
1323 // Delete items from the processors
1324 foreach ($feed->processors as $processor) {
1325 // FIXME: it's possible now to accidentally delete an item from another processor
1326 module_invoke($processor, 'feedapi_item', 'purge', $feed);
1327 }
1328
1329 // Closing step: Call after purge hook
1330 foreach (module_implements('feedapi_after_purge') as $module) {
1331 $func = $module .'_feedapi_after_purge';
1332 $func($feed);
1333 }
1334
1335 // Reset hash.
1336 db_query("UPDATE {feedapi} SET hash = 0 WHERE nid = %d", $feed->nid);
1337 }
1338
1339 /**
1340 * Builds feed object ready to be sticked onto node.
1341 */
1342 function _feedapi_build_feed_object($node_type, $url) {
1343 $feed = new stdClass();
1344 $feed->url = $url;
1345 $node_type_settings = feedapi_get_settings($node_type);
1346 $feed->processors = _feedapi_format_settings($node_type_settings, 'processors');
1347 $feed->parsers = _feedapi_format_settings($node_type_settings, 'parsers');
1348 if (isset($feed->url)) {
1349 $feed = _feedapi_call_parsers($feed, $feed->parsers, $node_type_settings['parsers']);
1350 }
1351 $feed->link = isset($feed->options->link) ? $feed->options->link : '';
1352 return $feed;
1353 }
1354
1355 /**
1356 * Returns per content type settings ordered by weight
1357 * and only those that are turned on.
1358 * @param $node_type_settings
1359 * Content type settings retrieved with feedapi_get_settings().
1360 * @param $stage_type
1361 * 'parsers' or 'processors'
1362 */
1363 function _feedapi_format_settings($node_type_settings, $stage_type) {
1364 $result = array();
1365 $settings = $node_type_settings[$stage_type];
1366 if (!is_array($settings)) {
1367 return $result;
1368 }
1369 foreach ($settings as $name => $properties) {
1370 if (isset($properties['enabled'])) {
1371 if ($properties['enabled'] == TRUE) {
1372 $result[$properties['weight']] = $name;
1373 }
1374 }
1375 }
1376 ksort($result);
1377 return $result;
1378 }
1379
1380 /**
1381 * Retrieve settings per content type or per node.
1382 *
1383 * @param $node_type
1384 * Content type name or NULL if per node
1385 * @param $vid
1386 * Node vid or NULL if per content type
1387 * @param $reset
1388 * If TRUE, the data is returned from the database.
1389 * @return
1390 * The associative array of feedapi settings
1391 *
1392 * @todo: Use node type settings for pulling on/off and weight of
1393 * parsers/processors, use per node settings to override their
1394 * configuration, this allows us a more predictable
1395 * presets/settings behaviour. See d. o. #191692
1396 * Watch out: cache permutations of node_type or node_type+nid or nid.
1397 * Watch out: changes within page load likely.
1398 */
1399 function feedapi_get_settings($node_type, $vid = FALSE, $reset = FALSE) {
1400 static $node_settings;
1401
1402 if (is_numeric($vid)) {
1403 if (!isset($node_settings[$vid]) || $reset) {
1404 if ($settings = db_fetch_object(db_query('SELECT settings FROM {feedapi} WHERE vid = %d', $vid))) {
1405 $settings = unserialize($settings->settings);
1406 // If parsers don't have any settings, create an empty array
1407 if (!isset($settings['parsers'])) {
1408 $settings['parsers'] = array();
1409 }
1410 // If processors don't have any settings, create an empty array
1411 if (!isset($settings['processors'])) {
1412 $settings['processors'] = array();
1413 }
1414 }
1415 if (is_array($settings) && count($settings['processors']) == 0 && count($settings['parsers']) == 0) {
1416 $settings = NULL;
1417 }
1418 $node_settings[$vid] = !empty($settings) && is_array($settings) ? $settings : FALSE;
1419 }
1420 if (!is_array($node_settings[$vid])) {
1421 if (empty($node_type)) {
1422 // In normal case, this shouldn't happen. This is an emergency branch
1423 $node_type = db_result(db_query("SELECT type FROM {node} WHERE vid = %d", $vid));
1424 }
1425 }
1426 else {
1427 return $node_settings[$vid];
1428 }
1429 }
1430
1431 // Fallback: node_type.
1432 if (isset($node_type) && is_string($node_type)) {
1433 if (($settings = variable_get('feedapi_settings_'. $node_type, FALSE)) && ($settings['enabled'] == 1)) {
1434 // Sanitize data right now, tricky users may turned off the module
1435 foreach (array('parsers', 'processors') as $type) {
1436 if (isset($settings[$type]) && is_array($settings[$type])) {
1437 $modules = array_keys($settings[$type]);
1438 foreach ($modules as $module) {
1439 if (!module_exists($module)) {
1440 unset($settings['parsers'][$module]);
1441 }
1442 }
1443 }
1444 else {
1445 // Missing parser or processor, set error message.
1446 if (user_access('administer content types')) {
1447 drupal_set_message(t('There are no !type defined for this content type. Go to !edit_page and enable at least one.', array('!type' => $type, '!edit_page' => l('admin/content/node-type/'. $node_type, 'admin/content/node-type/'. $node_type))), 'warning', FALSE);
1448 }
1449 else {
1450 drupal_set_message(t('There are no !type defined for this content type. Contact your site administrator.', array('!type' => $type)), 'warning', FALSE);
1451 }
1452 }
1453 }
1454 return $settings;
1455 }
1456 }
1457 return FALSE;
1458 }
1459
1460 /**
1461 * Set default value of $form elements if present in $settings.
1462 */
1463 function _feedapi_populate($form, $settings) {
1464 foreach ($form as $k => $v) {
1465 if (is_array($v)) {
1466 if (array_key_exists('#default_value', $v)) {
1467 // Don't prepopulate feedapi_url slot, not stored in settings
1468 // Might be overwritten otherwise by users without advanced feedapi options permissions.
1469 // Todo: stick all settings form elements that are not in 'parsers' or 'processors' in 'general' -
1470 // This is kind of tricky though without breaking sites out there.
1471 if ($k != 'feedapi_url') {
1472 if (isset($form[$k]['#parents']) && is_array($form[$k]['#parents'])) {
1473 // respect #parents if set
1474 $form[$k]['#default_value'] = _feedapi_populate_get_setting($form[$k]['#parents'], $settings);
1475 }
1476 elseif (isset($settings[$k])) {
1477 $form[$k]['#default_value'] = $settings[$k];
1478 }
1479 }
1480 }
1481 elseif (isset($settings[$k])) {
1482 $form[$k] = _feedapi_populate($form[$k], $settings[$k]);
1483 }
1484 }
1485 }
1486 return $form;
1487 }
1488
1489 /**
1490 * Gets the setting for '#parent'
1491 * (there must be a more efficent way)
1492 */
1493 function _feedapi_populate_get_setting($parents, $settings) {
1494 if (is_array($parents) && count($parents)) {
1495 $this_parent = array_shift($parents);
1496 return _feedapi_populate_get_setting($parents, $settings[$this_parent]);
1497 }
1498 else {
1499 return $settings[$parents];
1500 }
1501 }
1502
1503 /**
1504 * Calculate the average between-update time
1505 */
1506 function _feedapi_update_rate($update_times) {
1507 $between = array();
1508 for ($i = 0; $i < count($update_times) - 1; $i++) {
1509 $between[] = abs($update_times[$i] - $update_times[$i + 1]);
1510 }
1511 return (count($between) > 0) ? round(array_sum($between) / count($between), 2) : t('No data yet');
1512 }
1513
1514 /**
1515 * Remove non-existing processors from the processors arrays
1516 */
1517 function _feedapi_sanitize_processors(&$feed) {
1518 if (is_array($feed->processors)) {
1519 foreach ($feed->processors as $key => $processor) {
1520 if (!module_exists($processor)) {
1521 unset($feed->processors[$key]);
1522 }
1523 }
1524 }
1525 }
1526
1527 /**
1528 * Store statistics information
1529 *
1530 * @param $id
1531 * A numerical id
1532 * @param $type
1533 * A string which describes what we want to store. This is an identifier, think of as a variable name
1534 * @param $val
1535 * This is the variable value
1536 * @param $timestamp
1537 * Timestamp for the value
1538 * @param $time
1539 * Optional, a string equivalent to the $timestamp
1540 * @param $update
1541 * Boolean, TRUE if you'd like to modify an existing entry in the stat table
1542 */
1543 function _feedapi_store_stat($id, $type, $val, $timestamp, $time = NULL, $update = FALSE) {
1544 if (!$time) {
1545 $time = date("Y-m-d H:i", $timestamp);
1546 }
1547 if ($update) {
1548 db_query("UPDATE {feedapi_stat} SET value = %d, timestamp = %d WHERE time = '%s' AND type = '%s' AND id = %d", $val, $timestamp, $time, $type, $id);
1549 }
1550 if (!$update || !db_affected_rows()) {
1551 db_query("INSERT INTO {feedapi_stat} (id, value, time, timestamp, type) VALUES (%d, %d, '%s', %d, '%s')", $id, $val, $time, $timestamp, $type);
1552 }
1553 }
1554
1555 /**
1556 * Return the type-specific statistics data
1557 *
1558 * @param $id
1559 * A numerical id
1560 * @param $type
1561 * Name of the type (variable)
1562 * @name $only_val
1563 * If TRUE, only the values are returned, no more.
1564 * @return
1565 * $only_val = FALSE -> array("timestamp" => array(), "time" => array(), "value" => array());
1566 */
1567 function _feedapi_get_stat($id, $type, $only_val = FALSE) {
1568 $stat = array();
1569 $result = db_query("SELECT timestamp, time, value FROM {feedapi_stat} WHERE type = '%s' AND id = %d", $type, $id);
1570 while ($row = db_fetch_array($result)) {
1571 if ($only_val) {
1572 $stat[] = $row['value'];
1573 }
1574 else {
1575 foreach (array('timestamp', 'time', 'value') as $member) {
1576 $stat[$member][] = $row[$member];
1577 }
1578 }
1579 }
1580 return $stat;
1581 }
1582
1583 /**
1584 * Return a list of FeedAPI-enabled content-types list, ready-to-use for #options at FormsAPI
1585 */
1586 function feedapi_get_types() {
1587 $names = node_get_types('names');
1588 foreach ($names as $type => $name) {
1589 if (!feedapi_enabled_type($type)) {
1590 unset($names[$type]);
1591 }
1592 }
1593 return $names;
1594 }
1595
1596 /**
1597 * Prevent users to use the same weight for two or more parsers and processors
1598 * because FeedAPI cannot handle this. And this is not neccessary too.
1599 */
1600 function feedapi_content_type_validate($form, &$form_state) {
1601 if ($form_state['values']['feedapi']['enabled'] == FALSE) {
1602 return;
1603 }
1604 $parsers = module_implements('feedapi_feed', TRUE);
1605 rsort($parsers);
1606 $processors = module_implements('feedapi_item', TRUE);
1607 rsort($processors);
1608 $count_enabled_per_type = array();
1609 $count_enabled_per_type['parsers'] = 0;
1610 $count_enabled_per_type['processors'] = 0;
1611 foreach (array('processors', 'parsers') as $type) {
1612 $proc_weight = array();
1613 foreach (${$type} as $stuff) {
1614 if (isset($form_state['values']['feedapi'][$type][$stuff]) && $form_state['values']['feedapi'][$type][$stuff]['enabled'] == TRUE) {
1615 $count_enabled_per_type[$type]++;
1616 $weight = $form_state['values']['feedapi'][$type][$stuff]['weight'];
1617 if (!isset($proc_weight[$weight])) {
1618 $proc_weight[$weight] = 0;
1619 }
1620 if (++$proc_weight[$weight] > 1) {
1621 form_error($form, t('Two enabled processors or parsers cannot have the same weight.'), 'error');
1622 }
1623 }
1624 }
1625 }
1626 if ($count_enabled_per_type['parsers'] == 0) {
1627 form_error($form, t('Using FeedAPI for this content-type requires at least one enabled parser.'));
1628 }
1629 if ($count_enabled_per_type['processors'] == 0) {
1630 form_error($form, t('Using FeedAPI for this content-type requires at least one enabled processor.'));
1631 }
1632 }