Fix warning if mapping key doesn't exist.
[project/feeds.git] / feeds.module
1 <?php
2
3 /**
4 * @file
5 * Feeds - basic API functions and hook implementations.
6 */
7
8 // Common request time, use as point of reference and to avoid calls to time().
9 define('FEEDS_REQUEST_TIME', time());
10 // Do not schedule a feed for refresh.
11 define('FEEDS_SCHEDULE_NEVER', -1);
12 // Never expire feed items.
13 define('FEEDS_EXPIRE_NEVER', -1);
14 // An object that is not persistent. Compare EXPORT_IN_DATABASE, EXPORT_IN_CODE.
15 define('FEEDS_EXPORT_NONE', 0x0);
16 // Status of batched operations.
17 define('FEEDS_BATCH_COMPLETE', 1);
18 define('FEEDS_BATCH_ACTIVE', 0);
19
20 /**
21 * @defgroup hooks Hook and callback implementations
22 * @{
23 */
24
25 /**
26 * Implements hook_cron().
27 */
28 function feeds_cron() {
29 if ($importers = feeds_reschedule()) {
30 foreach ($importers as $id) {
31 feeds_importer($id)->schedule();
32 $result = db_query("SELECT feed_nid FROM {feeds_source} WHERE id = '%s'", $id);
33 while ($row = db_fetch_object($result)) {
34 feeds_source($id, $row->feed_nid)->schedule();
35 }
36 }
37 feeds_reschedule(FALSE);
38 return;
39 }
40 }
41
42 /**
43 * Implementation of hook_cron_queue_info().
44 *
45 * Invoked by drupal_queue module if present.
46 */
47 function feeds_cron_queue_info() {
48 $queues = array();
49 $queues['feeds_source_import'] = array(
50 'worker callback' => 'feeds_source_import',
51 'time' => variable_get('feeds_worker_time', 15),
52 );
53 $queues['feeds_importer_expire'] = array(
54 'worker callback' => 'feeds_importer_expire',
55 'time' => variable_get('feeds_worker_time', 15),
56 );
57 return $queues;
58 }
59
60 /**
61 * Scheduler callback for importing from a source.
62 */
63 function feeds_source_import($job) {
64 $source = feeds_source($job['type'], $job['id']);
65 try {
66 $source->existing()->import();
67 }
68 catch (FeedsNotExistingException $e) {
69 // Do nothing.
70 }
71 catch (Exception $e) {
72 watchdog('feeds_source_import()', $e->getMessage(), array(), WATCHDOG_ERROR);
73 }
74 $source->schedule();
75 }
76
77 /**
78 * Scheduler callback for expiring content.
79 */
80 function feeds_importer_expire($job) {
81 $importer = feeds_importer($job['type']);
82 try {
83 $importer->existing()->expire();
84 }
85 catch (FeedsNotExistingException $e) {
86 // Do nothing.
87 }
88 catch (Exception $e) {
89 watchdog('feeds_importer_expire()', $e->getMessage(), array(), WATCHDOG_ERROR);
90 }
91 $importer->schedule();
92 }
93
94 /**
95 * Reschedule one or all importers.
96 *
97 * Note: variable_set('feeds_reschedule', TRUE) is used in update hook
98 * feeds_update_6013() and as such must be maintained as part of the upgrade
99 * path from pre 6.x 1.0 beta 6 versions of Feeds.
100 *
101 * @param $importer_id
102 * If TRUE, all importers will be rescheduled, if FALSE, no importers will
103 * be rescheduled, if an importer id, only importer of that id will be
104 * rescheduled.
105 *
106 * @return
107 * TRUE if all importers need rescheduling. FALSE if no rescheduling is
108 * required. An array of importers that need rescheduling.
109 */
110 function feeds_reschedule($importer_id = NULL) {
111 $reschedule = variable_get('feeds_reschedule', FALSE);
112 if ($importer_id === TRUE || $importer_id === FALSE) {
113 $reschedule = $importer_id;
114 }
115 elseif (is_string($importer_id) && $reschedule !== TRUE) {
116 $reschedule = is_array($reschedule) ? $reschedule : array();
117 $reschedule[$importer_id] = $importer_id;
118 }
119 variable_set('feeds_reschedule', $reschedule);
120 if ($reschedule === TRUE) {
121 return feeds_enabled_importers();
122 }
123 return $reschedule;
124 }
125
126 /**
127 * Implementation of hook_perm().
128 */
129 function feeds_perm() {
130 $perms = array('administer feeds');
131 foreach (feeds_importer_load_all() as $importer) {
132 $perms[] = 'import '. $importer->id .' feeds';
133 $perms[] = 'clear '. $importer->id .' feeds';
134 }
135 return $perms;
136 }
137
138 /**
139 * Implementation of hook_forms().
140 *
141 * Declare form callbacks for all known classes derived from FeedsConfigurable.
142 */
143 function feeds_forms() {
144 $forms = array();
145 $forms['FeedsImporter_feeds_form']['callback'] = 'feeds_form';
146 $plugins = feeds_get_plugins();
147 foreach ($plugins as $plugin) {
148 $forms[$plugin['handler']['class'] .'_feeds_form']['callback'] = 'feeds_form';
149 }
150 return $forms;
151 }
152
153 /**
154 * Implementation of hook_menu().
155 */
156 function feeds_menu() {
157 // Register a callback for all feed configurations that are not attached to a content type.
158 $items = array();
159 foreach (feeds_importer_load_all() as $importer) {
160 if (empty($importer->config['content_type'])) {
161 $items['import/'. $importer->id] = array(
162 'title' => $importer->config['name'],
163 'page callback' => 'drupal_get_form',
164 'page arguments' => array('feeds_import_form', 1),
165 'access callback' => 'feeds_access',
166 'access arguments' => array('import', $importer->id),
167 'file' => 'feeds.pages.inc',
168 );
169 $items['import/'. $importer->id .'/import'] = array(
170 'title' => 'Import',
171 'type' => MENU_DEFAULT_LOCAL_TASK,
172 'weight' => -10,
173 );
174 $items['import/'. $importer->id .'/delete-items'] = array(
175 'title' => 'Delete items',
176 'page callback' => 'drupal_get_form',
177 'page arguments' => array('feeds_delete_tab_form', 1),
178 'access callback' => 'feeds_access',
179 'access arguments' => array('clear', $importer->id),
180 'file' => 'feeds.pages.inc',
181 'type' => MENU_LOCAL_TASK,
182 );
183 }
184 else {
185 $items['node/%node/import'] = array(
186 'title' => 'Import',
187 'page callback' => 'drupal_get_form',
188 'page arguments' => array('feeds_import_tab_form', 1),
189 'access callback' => 'feeds_access',
190 'access arguments' => array('import', 1),
191 'file' => 'feeds.pages.inc',
192 'type' => MENU_LOCAL_TASK,
193 'weight' => 10,
194 );
195 $items['node/%node/delete-items'] = array(
196 'title' => 'Delete items',
197 'page callback' => 'drupal_get_form',
198 'page arguments' => array('feeds_delete_tab_form', NULL, 1),
199 'access callback' => 'feeds_access',
200 'access arguments' => array('clear', 1),
201 'file' => 'feeds.pages.inc',
202 'type' => MENU_LOCAL_TASK,
203 'weight' => 11,
204 );
205 }
206 $items += $importer->fetcher->menuItem();
207 }
208 if (count($items)) {
209 $items['import'] = array(
210 'title' => 'Import',
211 'page callback' => 'feeds_page',
212 'access callback' => 'feeds_page_access',
213 'file' => 'feeds.pages.inc',
214 );
215 }
216 return $items;
217 }
218
219 /**
220 * Menu loader callback.
221 */
222 function feeds_importer_load($id) {
223 return feeds_importer($id);
224 }
225
226 /**
227 * Implementation of hook_theme().
228 */
229 function feeds_theme() {
230 return array(
231 'feeds_upload' => array(
232 'file' => 'feeds.pages.inc',
233 ),
234 );
235 }
236
237 /**
238 * Menu access callback.
239 *
240 * @param $action
241 * The action to be performed. Possible values are:
242 * - import
243 * - clear
244 * @param $param
245 * Node object or FeedsImporter id.
246 */
247 function feeds_access($action, $param) {
248 if (!in_array($action, array('import', 'clear'))) {
249 // If $action is not one of the supported actions, we return access denied.
250 return FALSE;
251 }
252
253 if (is_string($param)) {
254 $importer_id = $param;
255 }
256 elseif ($param->type) {
257 $importer_id = feeds_get_importer_id($param->type);
258 }
259
260 // Check for permissions if feed id is present, otherwise return FALSE.
261 if ($importer_id) {
262 if (user_access('administer feeds') || user_access($action .' '. $importer_id .' feeds')) {
263 return TRUE;
264 }
265 }
266 return FALSE;
267 }
268
269 /**
270 * Menu access callback.
271 */
272 function feeds_page_access() {
273 if (user_access('administer feeds')) {
274 return TRUE;
275 }
276 foreach (feeds_enabled_importers() as $id) {
277 if (user_access("import $id feeds")) {
278 return TRUE;
279 }
280 }
281 return FALSE;
282 }
283
284 /**
285 * Implementation of hook_views_api().
286 */
287 function feeds_views_api() {
288 return array(
289 'api' => '2.0',
290 'path' => drupal_get_path('module', 'feeds') .'/views',
291 );
292 }
293
294 /**
295 * Implementation of hook_ctools_plugin_api().
296 */
297 function feeds_ctools_plugin_api($owner, $api) {
298 if ($owner == 'feeds' && $api == 'plugins') {
299 return array('version' => 1);
300 }
301 }
302
303 /**
304 * Implementation of hook_ctools_plugin_plugins().
305 *
306 * Psuedo hook defintion plugin system options and defaults.
307 */
308 function feeds_ctools_plugin_plugins() {
309 return array(
310 'cache' => TRUE,
311 'use hooks' => TRUE,
312 );
313 }
314
315 /**
316 * Implementation of hook_feeds_plugins().
317 */
318 function feeds_feeds_plugins() {
319 module_load_include('inc', 'feeds', 'feeds.plugins');
320 return _feeds_feeds_plugins();
321 }
322
323 /**
324 * Implementation of hook_nodeapi().
325 *
326 * @todo For Drupal 7, revisit static cache based shuttling of values between
327 * 'validate' and 'update'/'insert'.
328 */
329 function feeds_nodeapi(&$node, $op, $form) {
330
331 // $node looses any changes after 'validate' stage (see node_form_validate()).
332 // Keep a copy of title and feeds array between 'validate' and subsequent
333 // stages. This allows for automatically populating the title of the node form
334 // and modifying the $form['feeds'] array on node validation just like on the
335 // standalone form.
336 static $last_title;
337 static $node_feeds;
338
339 // Break out node processor related nodeapi functionality.
340 _feeds_nodeapi_node_processor($node, $op);
341
342 if ($importer_id = feeds_get_importer_id($node->type)) {
343 switch ($op) {
344 case 'validate':
345 // On validation stage we are working with a FeedsSource object that is
346 // not tied to a nid - when creating a new node there is no
347 // $node->nid at this stage.
348 $source = feeds_source($importer_id);
349
350 // Node module magically moved $form['feeds'] to $node->feeds :P
351 $node_feeds = $node->feeds;
352 $source->configFormValidate($node_feeds);
353
354 // If node title is empty, try to retrieve title from feed.
355 if (trim($node->title) == '') {
356 try {
357 $source->addConfig($node_feeds);
358 if (!$last_title = $source->preview()->getTitle()) {
359 throw new Exception();
360 }
361 }
362 catch (Exception $e) {
363 drupal_set_message($e->getMessage(), 'error');
364 form_set_error('title', t('Could not retrieve title from feed.'));
365 }
366 }
367 break;
368 case 'presave':
369 if (!empty($last_title)) {
370 $node->title = $last_title;
371 }
372 $last_title = NULL;
373 break;
374 case 'insert':
375 case 'update':
376 // A node may not have been validated, make sure $node_feeds is present.
377 if (empty($node_feeds)) {
378 // If $node->feeds is empty here, nodes are being programatically
379 // created in some fashion without Feeds stuff being added.
380 if (empty($node->feeds)) {
381 return;
382 }
383 $node_feeds = $node->feeds;
384 }
385 // Add configuration to feed source and save.
386 $source = feeds_source($importer_id, $node->nid);
387 $source->addConfig($node_feeds);
388 $source->save();
389
390 // Refresh feed if import on create is selected and suppress_import is
391 // not set.
392 if ($op == 'insert' && feeds_importer($importer_id)->config['import_on_create'] && !isset($node_feeds['suppress_import'])) {
393 feeds_batch_set(t('Importing'), 'import', $importer_id, $node->nid);
394 }
395 // Add source to schedule, make sure importer is scheduled, too.
396 if ($op == 'insert') {
397 $source->schedule();
398 $source->importer->schedule();
399 }
400 $node_feeds = NULL;
401 break;
402 case 'delete':
403 $source = feeds_source($importer_id, $node->nid);
404 if (!empty($source->importer->processor->config['delete_with_source'])) {
405 feeds_batch_set(t('Deleting'), 'clear', $importer_id, $node->nid);
406 }
407 // Remove attached source.
408 $source->delete();
409 break;
410 }
411 }
412 }
413
414 /**
415 * Handles FeedsNodeProcessor specific nodeapi operations.
416 */
417 function _feeds_nodeapi_node_processor($node, $op) {
418 switch ($op) {
419 case 'load':
420 if ($result = db_fetch_object(db_query("SELECT imported, guid, url, feed_nid FROM {feeds_node_item} WHERE nid = %d", $node->nid))) {
421 $node->feeds_node_item = $result;
422 }
423 break;
424 case 'insert':
425 if (isset($node->feeds_node_item)) {
426 $node->feeds_node_item->nid = $node->nid;
427 drupal_write_record('feeds_node_item', $node->feeds_node_item);
428 }
429 break;
430 case 'update':
431 if (isset($node->feeds_node_item)) {
432 $node->feeds_node_item->nid = $node->nid;
433 drupal_write_record('feeds_node_item', $node->feeds_node_item, 'nid');
434 }
435 break;
436 case 'delete':
437 db_query("DELETE FROM {feeds_node_item} WHERE nid = %d", $node->nid);
438 break;
439 }
440 }
441
442 /**
443 * Implementation of hook_taxonomy().
444 */
445 function feeds_taxonomy($op = NULL, $type = NULL, $term = NULL) {
446 if ($type == 'term' && !empty($term['tid'])) {
447 switch ($op) {
448 case 'delete':
449 db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
450 break;
451 case 'update':
452 if (isset($term['feeds_term_item'])) {
453 db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
454 }
455 case 'insert':
456 if (isset($term['feeds_term_item'])) {
457 $term['feeds_term_item']['tid'] = $term['tid'];
458 drupal_write_record('feeds_term_item', $term['feeds_term_item']);
459 }
460 break;
461 }
462 }
463 }
464
465 /**
466 * Implements hook_form_alter().
467 */
468 function feeds_form_alter(&$form, $form_state, $form_id) {
469 if (isset($form['#node']->type) && $form['#node']->type . '_node_form' == $form_id) {
470 if ($importer_id = feeds_get_importer_id($form['#node']->type)) {
471 // Set title to not required, try to retrieve it from feed.
472 if (isset($form['title'])) {
473 $form['title']['#required'] = FALSE;
474 }
475
476 // Enable uploads.
477 $form['#attributes']['enctype'] = 'multipart/form-data';
478
479 // Build form.
480 $source = feeds_source($importer_id, empty($form['#node']->nid) ? 0 : $form['#node']->nid);
481 $form['feeds'] = array(
482 '#type' => 'fieldset',
483 '#title' => t('Feed'),
484 '#tree' => TRUE,
485 '#weight' => 0,
486 );
487 $form['feeds'] += $source->configForm($form_state);
488 $form['#feed_id'] = $importer_id;
489 }
490 }
491 }
492
493 /**
494 * Implements hook_content_extra_fields().
495 */
496 function feeds_content_extra_fields($type) {
497 $extras = array();
498 if (feeds_get_importer_id($type)) {
499 $extras['feeds'] = array(
500 'label' => t('Feed'),
501 'description' => t('Feeds module form elements'),
502 'weight' => 0,
503 );
504 }
505 return $extras;
506 }
507
508 /**
509 * @}
510 */
511
512 /**
513 * @defgroup batch Batch functions
514 */
515
516 /**
517 * Batch helper.
518 *
519 * @param $title
520 * Title to show to user when executing batch.
521 * @param $method
522 * Method to execute on importer; one of 'import', 'clear' or 'expire'.
523 * @param $importer_id
524 * Identifier of a FeedsImporter object.
525 * @param $feed_nid
526 * If importer is attached to content type, feed node id identifying the
527 * source to be imported.
528 */
529 function feeds_batch_set($title, $method, $importer_id, $feed_nid = 0) {
530 $batch = array(
531 'title' => $title,
532 'operations' => array(
533 array('feeds_batch', array($method, $importer_id, $feed_nid)),
534 ),
535 'progress_message' => '',
536 );
537 batch_set($batch);
538 }
539
540 /**
541 * Batch callback.
542 *
543 * @param $method
544 * Method to execute on importer; one of 'import' or 'clear'.
545 * @param $importer_id
546 * Identifier of a FeedsImporter object.
547 * @param $feed_nid
548 * If importer is attached to content type, feed node id identifying the
549 * source to be imported.
550 * @param $context
551 * Batch context.
552 */
553 function feeds_batch($method, $importer_id, $feed_nid = 0, &$context) {
554 $context['finished'] = 1;
555 try {
556 $context['finished'] = feeds_source($importer_id, $feed_nid)->$method();
557 }
558 catch (Exception $e) {
559 drupal_set_message($e->getMessage(), 'error');
560 }
561 }
562
563 /**
564 * @}
565 */
566
567 /**
568 * @defgroup utility Utility functions
569 * @{
570 */
571
572 /**
573 * Loads all importers.
574 *
575 * @param $load_disabled
576 * Pass TRUE to load all importers, enabled or disabled, pass FALSE to only
577 * retrieve enabled importers.
578 *
579 * @return
580 * An array of all feed configurations available.
581 */
582 function feeds_importer_load_all($load_disabled = FALSE) {
583 $feeds = array();
584 // This function can get called very early in install process through
585 // menu_router_rebuild(). Do not try to include CTools if not available.
586 if (function_exists('ctools_include')) {
587 ctools_include('export');
588 $configs = ctools_export_load_object('feeds_importer', 'all');
589 foreach ($configs as $config) {
590 if (!empty($config->id) && ($load_disabled || empty($config->disabled))) {
591 $feeds[$config->id] = feeds_importer($config->id);
592 }
593 }
594 }
595 return $feeds;
596 }
597
598 /**
599 * Gets an array of enabled importer ids.
600 *
601 * @return
602 * An array where the values contain ids of enabled importers.
603 */
604 function feeds_enabled_importers() {
605 return array_keys(_feeds_importer_digest());
606 }
607
608 /**
609 * Gets an enabled importer configuration by content type.
610 *
611 * @param $content_type
612 * A node type string.
613 *
614 * @return
615 * A FeedsImporter id if there is an importer for the given content type,
616 * FALSE otherwise.
617 */
618 function feeds_get_importer_id($content_type) {
619 $importers = array_flip(_feeds_importer_digest());
620 return isset($importers[$content_type]) ? $importers[$content_type] : FALSE;
621 }
622
623 /**
624 * Helper function for feeds_get_importer_id() and feeds_enabled_importers().
625 */
626 function _feeds_importer_digest() {
627 $importers = &ctools_static(__FUNCTION__);
628 if ($importers === NULL) {
629 if ($cache = cache_get(__FUNCTION__)) {
630 $importers = $cache->data;
631 }
632 else {
633 $importers = array();
634 foreach (feeds_importer_load_all() as $importer) {
635 $importers[$importer->id] = isset($importer->config['content_type']) ? $importer->config['content_type'] : '';
636 }
637 cache_set(__FUNCTION__, $importers);
638 }
639 }
640 return $importers;
641 }
642
643 /**
644 * Resets importer caches. Call when enabling/disabling importers.
645 */
646 function feeds_cache_clear($rebuild_menu = TRUE) {
647 cache_clear_all('_feeds_importer_digest', 'cache');
648 ctools_static_reset('_feeds_importer_digest');
649 ctools_include('export');
650 ctools_export_load_object_reset('feeds_importer');
651 node_get_types('types', NULL, TRUE);
652 if ($rebuild_menu) {
653 menu_rebuild();
654 }
655 }
656
657 /**
658 * Exports a FeedsImporter configuration to code.
659 */
660 function feeds_export($importer_id, $indent = '') {
661 ctools_include('export');
662 $result = ctools_export_load_object('feeds_importer', 'names', array('id' => $importer_id));
663 if (isset($result[$importer_id])) {
664 return ctools_export_object('feeds_importer', $result[$importer_id], $indent);
665 }
666 }
667
668 /**
669 * Logs to a file like /mytmp/feeds_my_domain_org.log in temporary directory.
670 */
671 function feeds_dbg($msg) {
672 if (variable_get('feeds_debug', FALSE)) {
673 if (!is_string($msg)) {
674 $msg = var_export($msg, TRUE);
675 }
676 $filename = trim(str_replace('/', '_', $_SERVER['HTTP_HOST'] . base_path()), '_');
677 $handle = fopen(file_directory_temp() ."/feeds_$filename.log", 'a');
678 fwrite($handle, date('c') ."\t$msg\n");
679 fclose($handle);
680 }
681 }
682
683 /**
684 * @}
685 */
686
687 /**
688 * @defgroup instantiators Instantiators
689 * @{
690 */
691
692 /**
693 * Gets an importer instance.
694 *
695 * @param $id
696 * The unique id of the importer object.
697 *
698 * @return
699 * A FeedsImporter object or an object of a class defined by the Drupal
700 * variable 'feeds_importer_class'. There is only one importer object
701 * per $id system-wide.
702 */
703 function feeds_importer($id) {
704 feeds_include('FeedsImporter');
705 return FeedsConfigurable::instance(variable_get('feeds_importer_class', 'FeedsImporter'), $id);
706 }
707
708 /**
709 * Gets an instance of a source object.
710 *
711 * @param $importer_id
712 * A FeedsImporter id.
713 * @param $feed_nid
714 * The node id of a feed node if the source is attached to a feed node.
715 *
716 * @return
717 * A FeedsSource object or an object of a class defiend by the Drupal
718 * variable 'source_class'.
719 */
720 function feeds_source($importer_id, $feed_nid = 0) {
721 feeds_include('FeedsImporter');
722 return FeedsSource::instance($importer_id, $feed_nid);
723 }
724
725 /**
726 * @}
727 */
728
729 /**
730 * @defgroup plugins Plugin functions
731 * @{
732 *
733 * @todo Encapsulate this in a FeedsPluginHandler class, move it to includes/
734 * and only load it if we're manipulating plugins.
735 */
736
737 /**
738 * Gets all available plugins. Does not list hidden plugins.
739 *
740 * @return
741 * An array where the keys are the plugin keys and the values
742 * are the plugin info arrays as defined in hook_feeds_plugins().
743 */
744 function feeds_get_plugins() {
745 ctools_include('plugins');
746 $plugins = ctools_get_plugins('feeds', 'plugins');
747
748 $result = array();
749 foreach ($plugins as $key => $info) {
750 if (!empty($info['hidden'])) {
751 continue;
752 }
753 $result[$key] = $info;
754 }
755
756 // Sort plugins by name and return.
757 uasort($result, 'feeds_plugin_compare');
758 return $result;
759 }
760
761 /**
762 * Sort callback for feeds_get_plugins().
763 */
764 function feeds_plugin_compare($a, $b) {
765 return strcasecmp($a['name'], $b['name']);
766 }
767
768 /**
769 * Gets all available plugins of a particular type.
770 *
771 * @param $type
772 * 'fetcher', 'parser' or 'processor'
773 */
774 function feeds_get_plugins_by_type($type) {
775 $plugins = feeds_get_plugins();
776
777 $result = array();
778 foreach ($plugins as $key => $info) {
779 if ($type == feeds_plugin_type($key)) {
780 $result[$key] = $info;
781 }
782 }
783 return $result;
784 }
785
786 /**
787 * Gets an instance of a class for a given plugin and id.
788 *
789 * @param $plugin
790 * A string that is the key of the plugin to load.
791 * @param $id
792 * A string that is the id of the object.
793 *
794 * @return
795 * A FeedsPlugin object.
796 *
797 * @throws Exception
798 * If plugin can't be instantiated.
799 */
800 function feeds_plugin_instance($plugin, $id) {
801 feeds_include('FeedsImporter');
802 ctools_include('plugins');
803 if ($class = ctools_plugin_load_class('feeds', 'plugins', $plugin, 'handler')) {
804 return FeedsConfigurable::instance($class, $id);
805 }
806 $args = array( '%plugin' => $plugin, '@id' => $id);
807 if (user_access('administer feeds')) {
808 $args['@link'] = url('admin/build/feeds/edit/' . $id);
809 drupal_set_message(t('Missing Feeds plugin %plugin. See <a href="@link">@id</a>. Check whether all required libraries and modules are installed properly.', $args), 'warning', FALSE);
810 }
811 else {
812 drupal_set_message(t('Missing Feeds plugin %plugin. Please contact your site administrator.', $args), 'warning', FALSE);
813 }
814 $class = ctools_plugin_load_class('feeds', 'plugins', 'FeedsMissingPlugin', 'handler');
815 return FeedsConfigurable::instance($class, $id);
816 }
817
818 /**
819 * Determines whether given plugin is derived from given base plugin.
820 *
821 * @param $plugin_key
822 * String that identifies a Feeds plugin key.
823 * @param $parent_plugin
824 * String that identifies a Feeds plugin key to be tested against.
825 *
826 * @return
827 * TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
828 * FALSE otherwise.
829 */
830 function feeds_plugin_child($plugin_key, $parent_plugin) {
831 ctools_include('plugins');
832 $plugins = ctools_get_plugins('feeds', 'plugins');
833 $info = $plugins[$plugin_key];
834
835 if (empty($info['handler']['parent'])) {
836 return FALSE;
837 }
838 elseif ($info['handler']['parent'] == $parent_plugin) {
839 return TRUE;
840 }
841 else {
842 return feeds_plugin_child($info['handler']['parent'], $parent_plugin);
843 }
844 }
845
846 /**
847 * Determines the type of a plugin.
848 *
849 * @param $plugin_key
850 * String that identifies a Feeds plugin key.
851 *
852 * @return
853 * One of the following values:
854 * 'fetcher' if the plugin is a fetcher
855 * 'parser' if the plugin is a parser
856 * 'processor' if the plugin is a processor
857 * FALSE otherwise.
858 */
859 function feeds_plugin_type($plugin_key) {
860 if (feeds_plugin_child($plugin_key, 'FeedsFetcher')) {
861 return 'fetcher';
862 }
863 elseif (feeds_plugin_child($plugin_key, 'FeedsParser')) {
864 return 'parser';
865 }
866 elseif (feeds_plugin_child($plugin_key, 'FeedsProcessor')) {
867 return 'processor';
868 }
869 return FALSE;
870 }
871
872 /**
873 * @}
874 */
875
876 /**
877 * @defgroup include Funtions for loading libraries
878 * @{
879 */
880
881 /**
882 * Includes a feeds module include file.
883 *
884 * @param $file
885 * The filename without the .inc extension.
886 * @param $directory
887 * The directory to include the file from. Do not include files from libraries
888 * directory. Use feeds_include_library() instead
889 */
890 function feeds_include($file, $directory = 'includes') {
891 static $included = array();
892 if (!isset($included[$file])) {
893 require './'. drupal_get_path('module', 'feeds') ."/$directory/$file.inc";
894 }
895 $included[$file] = TRUE;
896 }
897
898 /**
899 * Includes a library file.
900 *
901 * @param $file
902 * The filename to load from.
903 * @param $library
904 * The name of the library. If libraries module is installed,
905 * feeds_include_library() will look for libraries with this name managed by
906 * libraries module.
907 */
908 function feeds_include_library($file, $library) {
909 static $included = array();
910 if (!isset($included[$file])) {
911 // Try first whether libraries module is present and load the file from
912 // there. If this fails, require the library from the local path.
913 if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) {
914 require libraries_get_path($library) ."/$file";
915 }
916 else {
917 require './' . drupal_get_path('module', 'feeds') . "/libraries/$file";
918 }
919 }
920 $included[$file] = TRUE;
921 }
922
923 /**
924 * Checks whether a library is present.
925 *
926 * @param $file
927 * The filename to load from.
928 * @param $library
929 * The name of the library. If libraries module is installed,
930 * feeds_library_exists() will look for libraries with this name managed by
931 * libraries module.
932 */
933 function feeds_library_exists($file, $library) {
934 if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) {
935 return TRUE;
936 }
937 elseif (file_exists('./' . drupal_get_path('module', 'feeds') . "/libraries/$file")) {
938 return TRUE;
939 }
940 return FALSE;
941 }
942
943 /**
944 * @}
945 */
946
947 /**
948 * Copy of valid_url() that supports the webcal scheme.
949 *
950 * @see valid_url().
951 *
952 * @todo Replace with valid_url() when http://drupal.org/node/295021 is fixed.
953 */
954 function feeds_valid_url($url, $absolute = FALSE) {
955 if ($absolute) {
956 return (bool) preg_match("
957 /^ # Start at the beginning of the text
958 (?:ftp|https?|feed|webcal):\/\/ # Look for ftp, http, https, feed or webcal schemes
959 (?: # Userinfo (optional) which is typically
960 (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password
961 (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination
962 )?
963 (?:
964 (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address
965 |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address
966 )
967 (?::[0-9]+)? # Server port number (optional)
968 (?:[\/|\?]
969 (?:[|\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
970 *)?
971 $/xi", $url);
972 }
973 else {
974 return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
975 }
976 }