cron(); } /** * Implementation of hook_cron_queue_info(). * Invoked by drupal_queue module if present. */ function feeds_cron_queue_info() { $queues = array(); $queues[FEEDS_SCHEDULER_QUEUE] = array( 'worker callback' => 'feeds_scheduler_work', 'time' => variable_get('feeds_worker_time', 15), ); return $queues; } /** * Implementation of hook_perm(). */ function feeds_perm() { $perms = array('administer feeds'); foreach (feeds_importer_load_all() as $importer) { $perms[] = 'import '. $importer->id .' feeds'; $perms[] = 'clear '. $importer->id .' feeds'; } return $perms; } /** * Implementation of hook_forms(). */ function feeds_forms() { // Declare form callbacks for all known classes derived from FeedsConfigurable. $forms = array(); $forms['FeedsImporter_feeds_config_form']['callback'] = 'feeds_config_form'; $plugins = feeds_get_plugins(); foreach ($plugins as $plugin) { // See feeds_get_config_form(). $forms[$plugin['handler']['class'] .'_feeds_config_form']['callback'] = 'feeds_config_form'; } return $forms; } /** * Implementation of hook_menu(). */ function feeds_menu() { // Register a callback for all feed configurations that are not attached to a content type. $items = array(); foreach (feeds_importer_load_all() as $importer) { if (empty($importer->config['content_type'])) { $items['import/'. $importer->id] = array( 'title' => $importer->config['name'], 'page callback' => 'drupal_get_form', 'page arguments' => array('feeds_import_form', 1), 'access callback' => 'feeds_access', 'access arguments' => array('import', $importer->id), 'file' => 'feeds.pages.inc', ); $items['import/'. $importer->id .'/import'] = array( 'title' => 'Import', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['import/'. $importer->id .'/delete-items'] = array( 'title' => 'Delete items', 'page callback' => 'drupal_get_form', 'page arguments' => array('feeds_delete_tab_form', 1), 'access callback' => 'feeds_access', 'access arguments' => array('clear', $importer->id), 'file' => 'feeds.pages.inc', 'type' => MENU_LOCAL_TASK, ); } else { $items['node/%node/import'] = array( 'title' => 'Import', 'page callback' => 'drupal_get_form', 'page arguments' => array('feeds_import_tab_form', 1), 'access callback' => 'feeds_access', 'access arguments' => array('import', 1), 'file' => 'feeds.pages.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 10, ); $items['node/%node/delete-items'] = array( 'title' => 'Delete items', 'page callback' => 'drupal_get_form', 'page arguments' => array('feeds_delete_tab_form', NULL, 1), 'access callback' => 'feeds_access', 'access arguments' => array('clear', 1), 'file' => 'feeds.pages.inc', 'type' => MENU_LOCAL_TASK, 'weight' => 11, ); } } if (count($items)) { $items['import'] = array( 'title' => 'Import', 'page callback' => 'feeds_page', 'access callback' => 'feeds_page_access', 'file' => 'feeds.pages.inc', ); } return $items; } /** * Menu loader callback. */ function feeds_importer_load($id) { return feeds_importer($id); } /** * Implementation of hook_theme(). */ function feeds_theme() { return array( 'feeds_info' => array( 'file' => 'feeds.pages.inc', ), ); } /** * Menu access callback. * * @param $action * One of 'import' or 'clear'. * @param $param * Node object or FeedsImporter id. */ function feeds_access($action, $param) { if (is_string($param)) { $importer_id = $param; } elseif ($param->type) { if ($importer = feeds_importer_by_content_type($param->type)) { $importer_id = $importer->id; } } // Check for permissions if feed id is present, otherwise return FALSE. if (isset($importer_id)) { if (user_access('administer feeds') || user_access($action .' '. $importer_id .' feeds')) { return TRUE; } } return FALSE; } /** * Menu access callback. * * @todo: Create cached function that returns available configurations * and use it for enumerating feed configuration ids. */ function feeds_page_access() { if (user_access('administer feeds')) { return TRUE; } foreach (feeds_importer_load_all() as $importer) { if (user_access('import '. $importer->id .' feed')) { return TRUE; } } return FALSE; } /** * Implementation of hook_views_api(). */ function feeds_views_api() { return array( 'api' => '2.0', 'path' => drupal_get_path('module', 'feeds') .'/views', ); } /** * Implementation of hook_ctools_plugin_api(). */ function feeds_ctools_plugin_api($owner, $api) { if ($owner == 'feeds' && $api == 'plugins') { return array('version' => 1); } } /** * Implementation of hook_feeds_plugins(). * * @todo: Document API. Uses CTools plugin handling, extra keys: * 'description', 'hidden' * * @todo: Also declare and load include/ files with CTools plugins? */ function feeds_feeds_plugins() { module_load_include('inc', 'feeds', 'feeds.plugins'); return _feeds_feeds_plugins(); } /** * Implementation of hook_nodeapi(). */ function feeds_nodeapi(&$node, $op, $form) { static $last_title; // Break out node processor related nodeapi functionality. _feeds_nodeapi_node_processor($node, $op); if ($importer = feeds_importer_by_content_type($node->type)) { switch ($op) { case 'validate': // On validation stage we are working with a FeedsSource object that is // not tied to a nid - when creating a new node there is $node->nid at // this stage. $source = feeds_source($importer); // If node title is empty, try to retrieve title from feed. if (trim($node->title) == '') { try { $source->addConfig($node->feeds); $result = $importer->fetcher->fetch($source); $result = $importer->parser->parse($result, $source); if (!isset($result->value['title']) || trim($result->value['title']) == '') { form_set_error('title', t('Could not retrieve title from feed.'), 'error'); } else { // Keep the title in a static cache and populate $node->title on // 'presave' as node module looses any changes to $node after // 'validate'. $last_title = $result->value['title']; } } catch (Exception $e) { drupal_set_message($e->getMessage(), 'error'); } } // Invoke source // Node module magically moved $form['feeds'] to $node->feeds :P $source->configFormValidate($node->feeds); break; case 'presave': if (!empty($last_title)) { $node->title = $last_title; } $last_title = NULL; break; case 'insert': case 'update': // Add configuration to feed source and save. $source = feeds_source($importer, $node->nid); $source->addConfig($node->feeds); $source->save(); // Refresh feed if import on create is selected and suppress_import is // not set. if ($op == 'insert' && $importer->config['import_on_create'] && !isset($node->feeds['suppress_import'])) { $importer->import($source); } // Add import to scheduler. feeds_scheduler()->add($importer->id, 'import', $node->nid); // Add expiry to schedule, in case this is the first feed of this // configuration. feeds_scheduler()->add($importer->id, 'expire'); break; case 'delete': // Remove feed from scheduler and delete source. feeds_scheduler()->remove($importer->id, 'import', $node->nid); feeds_source($importer, $node->nid)->delete(); break; } } } /** * Break out FeedsNodeProcessor specific nodeapi operations. */ function _feeds_nodeapi_node_processor($node, $op) { switch ($op) { case 'load': if ($result = db_fetch_object(db_query('SELECT imported, guid, url, feed_nid FROM {feeds_node_item} WHERE nid = %d', $node->nid))) { $node->feeds_node_item = $result; } break; case 'insert': if (isset($node->feeds_node_item)) { $node->feeds_node_item->nid = $node->nid; drupal_write_record('feeds_node_item', $node->feeds_node_item); } break; case 'update': if (isset($node->feeds_node_item)) { $node->feeds_node_item->nid = $node->nid; drupal_write_record('feeds_node_item', $node->feeds_node_item, 'nid'); } break; case 'delete': if (isset($node->feeds_node_item)) { db_query('DELETE FROM {feeds_node_item} WHERE nid = %d', $node->nid); } break; } } /** * Implementation of hook_form_alter(). */ function feeds_form_alter(&$form, $form_state, $form_id) { if ($form['#id'] == 'node-form') { if ($importer = feeds_importer_by_content_type($form['type']['#value'])) { // Set title to not required, try to retrieve it from feed. $form['title']['#required'] = FALSE; // Build form. $source = feeds_source($importer, empty($form['nid']['#value']) ? 0 : $form['nid']['#value']); $form['feeds'] = array( '#type' => 'fieldset', '#title' => t('Feed'), '#tree' => TRUE, ); $form['feeds'] += $source->configForm($form_state); // Cannot pass on the FeedsImporter object, pass on id. $form['#feed_id'] = $importer->id; } } } /** * Refreshes a feed identified by $feed_info. * * Used as a worker callback for drupal_queue. * * @param $feed_info * Array where the key 'id' is the id of a FeedsImporter object and the key * 'feed_nid' is the node id of feed node. */ function feeds_scheduler_work($feed_info) { feeds_scheduler()->work($feed_info); } /** * @} End of "defgroup hooks". */ /** * @defgroup utility Utility functions * @{ */ /** * Load all importers. * * @ * * @return * An array of all feed configurations available. */ function feeds_importer_load_all($load_disabled = FALSE) { $feeds = array(); // This function can get called very early in install process through // menu_router_rebuild(). Do not try to include CTools if not available. if (function_exists('ctools_include')) { ctools_include('export'); $configs = ctools_export_load_object('feeds_importer', 'all'); foreach ($configs as $config) { if (!empty($config->id) && ($load_disabled || !$config->disabled)) { $feeds[$config->id] = feeds_importer($config->id); } } } return $feeds; } /** * Return whether a importer is enabled or not. * * Use only for importer configurations that ship with feeds. This function * assumes that importers are initially off and then enabled manually on * admin/build/feeds. * * @return * TRUE if the importer is enabled, FALSE if not. */ function feeds_importer_enabled($id) { // 'default_feeds_importer' variable is used internally by CTools to determine // the enabled/disabled state of a configuration. This variable may not be // completely populated. $disabled = variable_get('default_feeds_importer', FALSE); if ($disabled === FALSE) { $disabled = array(); foreach (feeds_importer_load_all() as $importer) { $disabled[$importer->id] = $disabled->disabled; } variable_set('default_feeds_importer', $disabled); } // If disabled is not present, assume that importer configuration is disabled. if (!isset($disabled[$id])) { return FALSE; } return !$disabled[$id]; } /** * Get a an enabled importer configuration by content type. * * @todo: speed this up by DB caching the result. * * @param $content_type * A node type string. * * @return * A FeedsImporter object if one is available, FALSE otherwise. */ function feeds_importer_by_content_type($content_type) { static $feeds = array(); if (!isset($feeds[$content_type])) { $feeds[$content_type] = FALSE; foreach (feeds_importer_load_all() as $importer) { if ((!$importer->disabled) && $importer->config['content_type'] == $content_type) { $feeds[$content_type] = $importer; break; } } } return $feeds[$content_type]; } /** * Export a FeedsImporter configuration to code. */ function feeds_export($importer_id, $indent = '') { ctools_include('export'); $result = ctools_export_load_object('feeds_importer', 'names', array('id' => $importer_id)); if (isset($result[$importer_id])) { return ctools_export_object('feeds_importer', $result[$importer_id], $indent); } } /** * @} End of "defgroup utility". */ /** * @defgroup instantiators Instantiators * @{ */ /** * Get an importer instance. * * @param $id * The unique id of the importer object. * * @return * A FeedsImporter object or an object of a class defined by the Drupal * variable 'feeds_importer_class'. There is only one importer object * per $id system-wide. */ function feeds_importer($id) { feeds_include('FeedsImporter'); return FeedsConfigurable::instance(variable_get('feeds_importer_class', 'FeedsImporter'), $id); } /** * Get an instance of a source object. * * @param $importer * A FeedsImporter object. * @param $feed_nid * The node id of a feed node if the source is attached to a feed node. * * @return * A FeedsSource object or an object of a class defiend by the Drupal * variable 'source_class'. */ function feeds_source($importer, $feed_nid = 0) { feeds_include('FeedsImporter'); return FeedsSource::instance($importer, $feed_nid); } /** * Get a scheduler instance. * * @return * A FeedsScheduler object or an object of a class defined by the Drupal * variable 'feeds_scheduler_class'. */ function feeds_scheduler() { feeds_include('FeedsImporter'); feeds_include('FeedsScheduler'); return FeedsScheduler::instance(); } /** * @} End of "defgroup instantiators". */ /** * @defgroup plugins Plugin functions * @{ * * @todo: Encapsulate this in a FeedsPluginHandler class, move it to includes/ * and only load it if we're manipulating plugins. */ /** * Get all available plugins. Does not list hidden plugins. * * @return * An array where the keys are the plugin keys and the values * are the plugin info arrays as defined in hook_feeds_plugins(). */ function feeds_get_plugins() { ctools_include('plugins'); $plugins = ctools_get_plugins('feeds', 'plugins'); $result = array(); foreach ($plugins as $key => $info) { if (!empty($info['hidden'])) { continue; } $result[$key] = $info; } // Sort plugins by name and return. uasort($result, 'feeds_plugin_compare'); return $result; } /** * Sort callback for feeds_get_plugins(). */ function feeds_plugin_compare($a, $b) { return strcasecmp($a['name'], $b['name']); } /** * Get all available plugins of a particular type. * * @param $type * 'fetcher', 'parser' or 'processor' */ function feeds_get_plugins_by_type($type) { $plugins = feeds_get_plugins(); $result = array(); foreach ($plugins as $key => $info) { if ($type == feeds_plugin_type($key)) { $result[$key] = $info; } } return $result; } /** * Get an instance of a class for a given plugin and id. * * @param $plugin * A string that is the key of the plugin to load. * @param $id * A string that is the id of the object. * * @return * A FeedsPlugin object. * * @throws Exception * If plugin can't be instantiated. */ function feeds_plugin_instance($plugin, $id) { feeds_include('FeedsImporter'); ctools_include('plugins'); if ($class = ctools_plugin_load_class('feeds', 'plugins', $plugin, 'handler')) { return FeedsConfigurable::instance($class, $id); } // Return FeedsMissingPlugin if no plugin was found. // @todo: better error handling. drupal_set_message(t('Missing Feeds plugin. Check whether all required libraries and modules are installed properly.'), 'error'); $class = ctools_plugin_load_class('feeds', 'plugins', 'FeedsMissingPlugin', 'handler'); return FeedsConfigurable::instance($class, $id); } /** * Determines whether given plugin is derived from given base plugin. * * @todo: update variable names. Should be $plugin_key if it is a key string. * * @param $plugin * String that identifies a Feeds plugin key. * @param $parent_plugin * String that identifies a Feeds plugin key to be tested against. * * @return * TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin, * FALSE otherwise. */ function feeds_plugin_child($plugin, $parent_plugin) { ctools_include('plugins'); $plugins = ctools_get_plugins('feeds', 'plugins'); $info = $plugins[$plugin]; if (empty($info['handler']['parent'])) { return FALSE; } elseif ($info['handler']['parent'] == $parent_plugin) { return TRUE; } else { return feeds_plugin_child($info['handler']['parent'], $parent_plugin); } } /** * Determine the type of a plugin. * * @param $plugin_key * String that is a Feeds plugin key. * * @return * One of the following values: * 'fetcher' if the plugin is a fetcher * 'parser' if the plugin is a parser * 'processor' if the plugin is a processor * FALSE otherwise. */ function feeds_plugin_type($plugin_key) { if (feeds_plugin_child($plugin_key, 'FeedsFetcher')) { return 'fetcher'; } elseif (feeds_plugin_child($plugin_key, 'FeedsParser')) { return 'parser'; } elseif (feeds_plugin_child($plugin_key, 'FeedsProcessor')) { return 'processor'; } return FALSE; } /** * @} End of "defgroup plugins". */ /** * @defgroup include Funtions for loading libraries * @{ */ /** * Includes a feeds module include file. * * @param $file * The filename without the .inc extension. * @param $directory * The directory to include the file from. Do not include files from libraries * directory. Use feeds_include_library() instead */ function feeds_include($file, $directory = 'includes') { static $included = array(); if (!isset($included[$file])) { require './'. drupal_get_path('module', 'feeds') ."/$directory/$file.inc"; } $included[$file] = TRUE; } /** * Include a library file. * * @param $file * The filename to load from. * @param $library * The name of the library. If libraries module is installed, * feeds_include_library() will look for libraries with this name managed by * libraries module. */ function feeds_include_library($file, $library) { static $included = array(); if (!isset($included[$file])) { // Try first whether libraries module is present and load the file from // there. If this fails, require the library from the local path. if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) { require libraries_get_path($library) ."/$file"; } else { require './'. drupal_get_path('module', 'feeds') ."/libraries/$file"; } } $included[$file] = TRUE; } /** * Checks whether a library is present. * * @param $file * The filename to load from. * @param $library * The name of the library. If libraries module is installed, * feeds_library_exists() will look for libraries with this name managed by * libraries module. */ function feeds_library_exists($file, $library) { if (module_exists('libraries') && file_exists(libraries_get_path($library) ."/$file")) { return TRUE; } elseif (file_exists(drupal_get_path('module', 'feeds') ."/libraries/$file")) { return TRUE; } return FALSE; } /** * @} End of "defgroup include". */