5 * Feeds - basic API functions and hook implementations.
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);
21 * @defgroup hooks Hook and callback implementations
26 * Implements hook_cron().
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();
37 feeds_reschedule(FALSE
);
43 * Implementation of hook_cron_queue_info().
45 * Invoked by drupal_queue module if present.
47 function feeds_cron_queue_info() {
49 $queues['feeds_source_import'] = array(
50 'worker callback' => 'feeds_source_import',
51 'time' => variable_get('feeds_worker_time', 15),
53 $queues['feeds_importer_expire'] = array(
54 'worker callback' => 'feeds_importer_expire',
55 'time' => variable_get('feeds_worker_time', 15),
61 * Scheduler callback for importing from a source.
63 function feeds_source_import($job) {
64 $source = feeds_source($job['type'], $job['id']);
66 $source->existing()->import();
68 catch (FeedsNotExistingException
$e) {
71 catch (Exception
$e) {
72 watchdog('feeds_source_import()', $e->getMessage(), array(), WATCHDOG_ERROR
);
78 * Scheduler callback for expiring content.
80 function feeds_importer_expire($job) {
81 $importer = feeds_importer($job['type']);
83 $importer->existing()->expire();
85 catch (FeedsNotExistingException
$e) {
88 catch (Exception
$e) {
89 watchdog('feeds_importer_expire()', $e->getMessage(), array(), WATCHDOG_ERROR
);
91 $importer->schedule();
95 * Reschedule one or all importers.
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.
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
107 * TRUE if all importers need rescheduling. FALSE if no rescheduling is
108 * required. An array of importers that need rescheduling.
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;
115 elseif (is_string($importer_id) && $reschedule !== TRUE
) {
116 $reschedule = is_array($reschedule) ?
$reschedule : array();
117 $reschedule[$importer_id] = $importer_id;
119 variable_set('feeds_reschedule', $reschedule);
120 if ($reschedule === TRUE
) {
121 return feeds_enabled_importers();
127 * Implementation of hook_perm().
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';
139 * Implementation of hook_forms().
141 * Declare form callbacks for all known classes derived from FeedsConfigurable.
143 function feeds_forms() {
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';
154 * Implementation of hook_menu().
156 function feeds_menu() {
157 // Register a callback for all feed configurations that are not attached to a content type.
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',
169 $items['import/'.
$importer->id .
'/import'] = array(
171 'type' => MENU_DEFAULT_LOCAL_TASK
,
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
,
185 $items['node/%node/import'] = array(
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
,
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
,
206 $items += $importer->fetcher
->menuItem();
209 $items['import'] = array(
211 'page callback' => 'feeds_page',
212 'access callback' => 'feeds_page_access',
213 'file' => 'feeds.pages.inc',
220 * Menu loader callback.
222 function feeds_importer_load($id) {
223 return feeds_importer($id);
227 * Implementation of hook_theme().
229 function feeds_theme() {
231 'feeds_upload' => array(
232 'file' => 'feeds.pages.inc',
238 * Menu access callback.
241 * The action to be performed. Possible values are:
245 * Node object or FeedsImporter id.
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.
253 if (is_string($param)) {
254 $importer_id = $param;
256 elseif ($param->type
) {
257 $importer_id = feeds_get_importer_id($param->type
);
260 // Check for permissions if feed id is present, otherwise return FALSE.
262 if (user_access('administer feeds') || user_access($action .
' '.
$importer_id .
' feeds')) {
270 * Menu access callback.
272 function feeds_page_access() {
273 if (user_access('administer feeds')) {
276 foreach (feeds_enabled_importers() as
$id) {
277 if (user_access("import $id feeds")) {
285 * Implementation of hook_views_api().
287 function feeds_views_api() {
290 'path' => drupal_get_path('module', 'feeds') .
'/views',
295 * Implementation of hook_ctools_plugin_api().
297 function feeds_ctools_plugin_api($owner, $api) {
298 if ($owner == 'feeds' && $api == 'plugins') {
299 return array('version' => 1);
304 * Implementation of hook_ctools_plugin_plugins().
306 * Psuedo hook defintion plugin system options and defaults.
308 function feeds_ctools_plugin_plugins() {
316 * Implementation of hook_feeds_plugins().
318 function feeds_feeds_plugins() {
319 module_load_include('inc', 'feeds', 'feeds.plugins');
320 return _feeds_feeds_plugins();
324 * Implementation of hook_nodeapi().
326 * @todo For Drupal 7, revisit static cache based shuttling of values between
327 * 'validate' and 'update'/'insert'.
329 function feeds_nodeapi(&$node, $op, $form) {
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
339 // Break out node processor related nodeapi functionality.
340 _feeds_nodeapi_node_processor($node, $op);
342 if ($importer_id = feeds_get_importer_id($node->type
)) {
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);
350 // Node module magically moved $form['feeds'] to $node->feeds :P
351 $node_feeds = $node->feeds
;
352 $source->configFormValidate($node_feeds);
354 // If node title is empty, try to retrieve title from feed.
355 if (trim($node->title
) == '') {
357 $source->addConfig($node_feeds);
358 if (!$last_title = $source->preview()->getTitle()) {
359 throw new
Exception();
362 catch (Exception
$e) {
363 drupal_set_message($e->getMessage(), 'error');
364 form_set_error('title', t('Could not retrieve title from feed.'));
369 if (!empty($last_title)) {
370 $node->title
= $last_title;
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
)) {
383 $node_feeds = $node->feeds
;
385 // Add configuration to feed source and save.
386 $source = feeds_source($importer_id, $node->nid
);
387 $source->addConfig($node_feeds);
390 // Refresh feed if import on create is selected and suppress_import is
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
);
395 // Add source to schedule, make sure importer is scheduled, too.
396 if ($op == 'insert') {
398 $source->importer
->schedule();
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
);
407 // Remove attached source.
415 * Handles FeedsNodeProcessor specific nodeapi operations.
417 function _feeds_nodeapi_node_processor($node, $op) {
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;
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
);
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');
437 db_query("DELETE FROM {feeds_node_item} WHERE nid = %d", $node->nid
);
443 * Implementation of hook_taxonomy().
445 function feeds_taxonomy($op = NULL
, $type = NULL
, $term = NULL
) {
446 if ($type == 'term' && !empty($term['tid'])) {
449 db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
452 if (isset($term['feeds_term_item'])) {
453 db_query("DELETE FROM {feeds_term_item} WHERE tid = %d", $term['tid']);
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']);
466 * Implements hook_form_alter().
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
;
477 $form['#attributes']['enctype'] = 'multipart/form-data';
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'),
487 $form['feeds'] += $source->configForm($form_state);
488 $form['#feed_id'] = $importer_id;
494 * Implements hook_content_extra_fields().
496 function feeds_content_extra_fields($type) {
498 if (feeds_get_importer_id($type)) {
499 $extras['feeds'] = array(
500 'label' => t('Feed'),
501 'description' => t('Feeds module form elements'),
513 * @defgroup batch Batch functions
520 * Title to show to user when executing batch.
522 * Method to execute on importer; one of 'import', 'clear' or 'expire'.
523 * @param $importer_id
524 * Identifier of a FeedsImporter object.
526 * If importer is attached to content type, feed node id identifying the
527 * source to be imported.
529 function feeds_batch_set($title, $method, $importer_id, $feed_nid = 0) {
532 'operations' => array(
533 array('feeds_batch', array($method, $importer_id, $feed_nid)),
535 'progress_message' => '',
544 * Method to execute on importer; one of 'import' or 'clear'.
545 * @param $importer_id
546 * Identifier of a FeedsImporter object.
548 * If importer is attached to content type, feed node id identifying the
549 * source to be imported.
553 function feeds_batch($method, $importer_id, $feed_nid = 0, &$context) {
554 $context['finished'] = 1;
556 $context['finished'] = feeds_source($importer_id, $feed_nid)->$method();
558 catch (Exception
$e) {
559 drupal_set_message($e->getMessage(), 'error');
568 * @defgroup utility Utility functions
573 * Loads all importers.
575 * @param $load_disabled
576 * Pass TRUE to load all importers, enabled or disabled, pass FALSE to only
577 * retrieve enabled importers.
580 * An array of all feed configurations available.
582 function feeds_importer_load_all($load_disabled = FALSE
) {
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
);
599 * Gets an array of enabled importer ids.
602 * An array where the values contain ids of enabled importers.
604 function feeds_enabled_importers() {
605 return array_keys(_feeds_importer_digest());
609 * Gets an enabled importer configuration by content type.
611 * @param $content_type
612 * A node type string.
615 * A FeedsImporter id if there is an importer for the given content type,
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
;
624 * Helper function for feeds_get_importer_id() and feeds_enabled_importers().
626 function _feeds_importer_digest() {
627 $importers = &ctools_static(__FUNCTION__
);
628 if ($importers === NULL
) {
629 if ($cache = cache_get(__FUNCTION__
)) {
630 $importers = $cache->data
;
633 $importers = array();
634 foreach (feeds_importer_load_all() as
$importer) {
635 $importers[$importer->id
] = isset($importer->config
['content_type']) ?
$importer->config
['content_type'] : '';
637 cache_set(__FUNCTION__
, $importers);
644 * Resets importer caches. Call when enabling/disabling importers.
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
);
658 * Exports a FeedsImporter configuration to code.
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);
669 * Logs to a file like /mytmp/feeds_my_domain_org.log in temporary directory.
671 function feeds_dbg($msg) {
672 if (variable_get('feeds_debug', FALSE
)) {
673 if (!is_string($msg)) {
674 $msg = var_export($msg, TRUE
);
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");
688 * @defgroup instantiators Instantiators
693 * Gets an importer instance.
696 * The unique id of the importer object.
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.
703 function feeds_importer($id) {
704 feeds_include('FeedsImporter');
705 return FeedsConfigurable
::instance(variable_get('feeds_importer_class', 'FeedsImporter'), $id);
709 * Gets an instance of a source object.
711 * @param $importer_id
712 * A FeedsImporter id.
714 * The node id of a feed node if the source is attached to a feed node.
717 * A FeedsSource object or an object of a class defiend by the Drupal
718 * variable 'source_class'.
720 function feeds_source($importer_id, $feed_nid = 0) {
721 feeds_include('FeedsImporter');
722 return FeedsSource
::instance($importer_id, $feed_nid);
730 * @defgroup plugins Plugin functions
733 * @todo Encapsulate this in a FeedsPluginHandler class, move it to includes/
734 * and only load it if we're manipulating plugins.
738 * Gets all available plugins. Does not list hidden plugins.
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().
744 function feeds_get_plugins() {
745 ctools_include('plugins');
746 $plugins = ctools_get_plugins('feeds', 'plugins');
749 foreach ($plugins as
$key => $info) {
750 if (!empty($info['hidden'])) {
753 $result[$key] = $info;
756 // Sort plugins by name and return.
757 uasort($result, 'feeds_plugin_compare');
762 * Sort callback for feeds_get_plugins().
764 function feeds_plugin_compare($a, $b) {
765 return strcasecmp($a['name'], $b['name']);
769 * Gets all available plugins of a particular type.
772 * 'fetcher', 'parser' or 'processor'
774 function feeds_get_plugins_by_type($type) {
775 $plugins = feeds_get_plugins();
778 foreach ($plugins as
$key => $info) {
779 if ($type == feeds_plugin_type($key)) {
780 $result[$key] = $info;
787 * Gets an instance of a class for a given plugin and id.
790 * A string that is the key of the plugin to load.
792 * A string that is the id of the object.
795 * A FeedsPlugin object.
798 * If plugin can't be instantiated.
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);
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
);
812 drupal_set_message(t('Missing Feeds plugin %plugin. Please contact your site administrator.', $args), 'warning', FALSE
);
814 $class = ctools_plugin_load_class('feeds', 'plugins', 'FeedsMissingPlugin', 'handler');
815 return FeedsConfigurable
::instance($class, $id);
819 * Determines whether given plugin is derived from given base plugin.
822 * String that identifies a Feeds plugin key.
823 * @param $parent_plugin
824 * String that identifies a Feeds plugin key to be tested against.
827 * TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
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];
835 if (empty($info['handler']['parent'])) {
838 elseif ($info['handler']['parent'] == $parent_plugin) {
842 return feeds_plugin_child($info['handler']['parent'], $parent_plugin);
847 * Determines the type of a plugin.
850 * String that identifies a Feeds plugin key.
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
859 function feeds_plugin_type($plugin_key) {
860 if (feeds_plugin_child($plugin_key, 'FeedsFetcher')) {
863 elseif (feeds_plugin_child($plugin_key, 'FeedsParser')) {
866 elseif (feeds_plugin_child($plugin_key, 'FeedsProcessor')) {
877 * @defgroup include Funtions for loading libraries
882 * Includes a feeds module include file.
885 * The filename without the .inc extension.
887 * The directory to include the file from. Do not include files from libraries
888 * directory. Use feeds_include_library() instead
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";
895 $included[$file] = TRUE
;
899 * Includes a library file.
902 * The filename to load from.
904 * The name of the library. If libraries module is installed,
905 * feeds_include_library() will look for libraries with this name managed by
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";
917 require
'./' .
drupal_get_path('module', 'feeds') .
"/libraries/$file";
920 $included[$file] = TRUE
;
924 * Checks whether a library is present.
927 * The filename to load from.
929 * The name of the library. If libraries module is installed,
930 * feeds_library_exists() will look for libraries with this name managed by
933 function feeds_library_exists($file, $library) {
934 if (module_exists('libraries') && file_exists(libraries_get_path($library) .
"/$file")) {
937 elseif (file_exists('./' .
drupal_get_path('module', 'feeds') .
"/libraries/$file")) {
948 * Copy of valid_url() that supports the webcal scheme.
952 * @todo Replace with valid_url() when http://drupal.org/node/295021 is fixed.
954 function feeds_valid_url($url, $absolute = FALSE
) {
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
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
967 (?::[0-9]+)? # Server port number (optional)
969 (?:[|\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
974 return (bool
) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);