Newer
Older
Alex Barth
committed
Alex Barth
committed
* Feeds - basic API functions and hook implementations.
*/
// Common request time, use as point of reference and to avoid calls to time().
Alex Barth
committed
define('FEEDS_REQUEST_TIME', time());
// Do not schedule a feed for refresh.
define('FEEDS_SCHEDULE_NEVER', -1);
// Never expire feed items.
define('FEEDS_EXPIRE_NEVER', -1);
// An object that is not persistent. Compare EXPORT_IN_DATABASE, EXPORT_IN_CODE.
Alex Barth
committed
define('FEEDS_EXPORT_NONE', 0x0);
// The Drupal Queue FeedsScheduler may use for scheduling importing or expiry.
define('FEEDS_SCHEDULER_QUEUE', 'feeds_queue');
// Status of batched operations.
define('FEEDS_BATCH_COMPLETE', 1);
define('FEEDS_BATCH_ACTIVE', 0);
Alex Barth
committed
/**
Alex Barth
committed
* @{
*/
/**
* Implementation of hook_cron().
*/
function feeds_cron() {
Alex Barth
committed
feeds_scheduler()->cron();
}
/**
* Implementation of hook_cron_queue_info().
Alex Barth
committed
* Invoked by drupal_queue module if present.
*/
function feeds_cron_queue_info() {
$queues = array();
Alex Barth
committed
'worker callback' => 'feeds_scheduler_work',
'time' => variable_get('feeds_worker_time', 15),
Alex Barth
committed
);
return $queues;
}
/**
* Implementation of hook_perm().
*/
function feeds_perm() {
Alex Barth
committed
$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().
*
* Declare form callbacks for all known classes derived from FeedsConfigurable.
Alex Barth
committed
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
*/
function feeds_forms() {
$forms = array();
$forms['FeedsImporter_feeds_config_form']['callback'] = 'feeds_config_form';
$plugins = feeds_get_plugins();
foreach ($plugins as $plugin) {
$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,
);
}
Alex Barth
committed
}
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_upload' => array(
'file' => 'feeds.pages.inc',
Alex Barth
committed
),
);
}
/**
* 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) {
$importer_id = feeds_get_importer_id($param->type);
Alex Barth
committed
}
// Check for permissions if feed id is present, otherwise return FALSE.
if ($importer_id) {
Alex Barth
committed
if (user_access('administer feeds') || user_access($action .' '. $importer_id .' feeds')) {
return TRUE;
}
}
return FALSE;
}
/**
* Menu access callback.
*/
function feeds_page_access() {
if (user_access('administer feeds')) {
return TRUE;
}
foreach (feeds_enabled_importers() as $id) {
Alex Barth
committed
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);
}
}
Alex Barth
committed
/**
* Implementation of hook_ctools_plugin_plugins().
*
* Psuedo hook defintion plugin system options and defaults.
*/
function feeds_ctools_plugin_plugins() {
return array(
'cache' => TRUE,
'use hooks' => TRUE,
);
}
Alex Barth
committed
/**
* Implementation of hook_feeds_plugins().
*/
function feeds_feeds_plugins() {
module_load_include('inc', 'feeds', 'feeds.plugins');
return _feeds_feeds_plugins();
}
/**
* Implementation of hook_nodeapi().
*
* @todo For Drupal 7, revisit static cache based shuttling of values between
* 'validate' and 'update'/'insert'.
Alex Barth
committed
*/
function feeds_nodeapi(&$node, $op, $form) {
// $node looses any changes after 'validate' stage (see node_form_validate()).
// Keep a copy of title and feeds array between 'validate' and subsequent
// stages. This allows for automatically populating the title of the node form
// and modifying the $form['feeds'] array on node validation just like on the
// standalone form.
Alex Barth
committed
static $last_title;
static $node_feeds;
Alex Barth
committed
// Break out node processor related nodeapi functionality.
_feeds_nodeapi_node_processor($node, $op);
if ($importer_id = feeds_get_importer_id($node->type)) {
Alex Barth
committed
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 no
// $node->nid at this stage.
$source = feeds_source($importer_id);
Alex Barth
committed
// Node module magically moved $form['feeds'] to $node->feeds :P
$node_feeds = $node->feeds;
$source->configFormValidate($node_feeds);
Alex Barth
committed
// If node title is empty, try to retrieve title from feed.
if (trim($node->title) == '') {
try {
$source->addConfig($node_feeds);
if (!$last_title = $source->preview()->getTitle()) {
Alex Barth
committed
throw new Exception();
Alex Barth
committed
}
}
catch (Exception $e) {
drupal_set_message($e->getMessage(), 'error');
Alex Barth
committed
form_set_error('title', t('Could not retrieve title from feed.'), 'error');
Alex Barth
committed
}
}
break;
case 'presave':
if (!empty($last_title)) {
$node->title = $last_title;
}
$last_title = NULL;
break;
case 'insert':
case 'update':
// A node may not have been validated, make sure $node_feeds is present.
if (empty($node_feeds)) {
$node_feeds = $node->feeds;
}
Alex Barth
committed
// Add configuration to feed source and save.
$source = feeds_source($importer_id, $node->nid);
$source->addConfig($node_feeds);
Alex Barth
committed
$source->save();
// Refresh feed if import on create is selected and suppress_import is
// not set.
if ($op == 'insert' && feeds_importer($importer_id)->config['import_on_create'] && !isset($node_feeds['suppress_import'])) {
feeds_batch_set(t('Importing'), 'import', $importer_id, $node->nid);
Alex Barth
committed
}
// Add import to scheduler.
feeds_scheduler()->add($importer_id, 'import', $node->nid);
Alex Barth
committed
// Add expiry to schedule, in case this is the first feed of this
// configuration.
feeds_scheduler()->add($importer_id, 'expire');
Alex Barth
committed
break;
case 'delete':
// Remove feed from scheduler and delete source.
feeds_scheduler()->remove($importer_id, 'import', $node->nid);
feeds_source($importer_id, $node->nid)->delete();
Alex Barth
committed
break;
}
}
}
/**
* Handles FeedsNodeProcessor specific nodeapi operations.
Alex Barth
committed
*/
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))) {
Alex Barth
committed
$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);
Alex Barth
committed
}
break;
}
/**
* Implementation of hook_form_alter().
*/
function feeds_form_alter(&$form, $form_state, $form_id) {
if ($form['#id'] == 'node-form') {
if ($importer_id = feeds_get_importer_id($form['type']['#value'])) {
Alex Barth
committed
// Set title to not required, try to retrieve it from feed.
$form['title']['#required'] = FALSE;
// Enable uploads.
$form['#attributes']['enctype'] = 'multipart/form-data';
Alex Barth
committed
// Build form.
$source = feeds_source($importer_id, empty($form['nid']['#value']) ? 0 : $form['nid']['#value']);
Alex Barth
committed
$form['feeds'] = array(
'#type' => 'fieldset',
'#title' => t('Feed'),
'#tree' => TRUE,
);
$form['feeds'] += $source->configForm($form_state);
$form['#feed_id'] = $importer_id;
Alex Barth
committed
}
}
}
/**
Alex Barth
committed
*
* Used as a worker callback for drupal_queue.
*
Alex Barth
committed
* 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($job) {
feeds_scheduler()->work($job);
Alex Barth
committed
}
/**
Alex Barth
committed
*/
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
*/
/**
* Batch helper.
*
* @param $title
* Title to show to user when executing batch.
* @param $method
* Method to execute on importer; one of 'import', 'clear' or 'expire'.
* @param $importer_id
* Identifier of a FeedsImporter object.
* @param $feed_nid
* If importer is attached to content type, feed node id identifying the
* source to be imported.
*/
function feeds_batch_set($title, $method, $importer_id, $feed_nid = 0) {
$batch = array(
'title' => $title,
'operations' => array(
array('feeds_batch', array($method, $importer_id, $feed_nid)),
),
'progress_message' => '',
);
batch_set($batch);
}
/**
* Batch callback.
*
* @param $method
* Method to execute on importer; one of 'import' or 'clear'.
* @param $importer_id
* Identifier of a FeedsImporter object.
* @param $feed_nid
* If importer is attached to content type, feed node id identifying the
* source to be imported.
* @param $context
* Batch context.
*/
function feeds_batch($method, $importer_id, $feed_nid = 0, &$context) {
$context['finished'] = 1;
try {
$context['finished'] = feeds_source($importer_id, $feed_nid)->$method();
}
catch (Exception $e) {
drupal_set_message($e->getMessage(), 'error');
}
}
/**
Alex Barth
committed
/**
* @defgroup utility Utility functions
* @{
*/
/**
* Loads all importers.
Alex Barth
committed
*
* @param $load_disabled
* Pass TRUE to load all importers, enabled or disabled, pass FALSE to only
* retrieve enabled importers.
Alex Barth
committed
* @return
* An array of all feed configurations available.
*/
function feeds_importer_load_all($load_disabled = FALSE) {
Alex Barth
committed
$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 || empty($config->disabled))) {
Alex Barth
committed
$feeds[$config->id] = feeds_importer($config->id);
Alex Barth
committed
return $feeds;
Alex Barth
committed
/**
* Gets an array of enabled importer ids.
Alex Barth
committed
*
* @return
* An array where the values contain ids of enabled importers.
Alex Barth
committed
*/
function feeds_enabled_importers() {
return array_keys(_feeds_importer_digest());
Alex Barth
committed
}
* Gets an enabled importer configuration by content type.
Alex Barth
committed
*
* @param $content_type
* A node type string.
*
* @return
* A FeedsImporter id if there is an importer for the given content type,
* FALSE otherwise.
function feeds_get_importer_id($content_type) {
$importers = array_flip(_feeds_importer_digest());
return isset($importers[$content_type]) ? $importers[$content_type] : FALSE;
}
/**
* Helper function for feeds_get_importer_id() and feeds_enabled_importers().
*/
function _feeds_importer_digest() {
$importers = &ctools_static(__FUNCTION__);
if ($importers === NULL) {
if ($cache = cache_get(__FUNCTION__)) {
$importers = $cache->data;
}
else {
$importers = array();
foreach (feeds_importer_load_all() as $importer) {
$importers[$importer->id] = isset($importer->config['content_type']) ? $importer->config['content_type'] : '';
Alex Barth
committed
}
cache_set(__FUNCTION__, $importers);
Alex Barth
committed
}
return $importers;
}
/**
* Resets importer caches. Call when enabling/disabling importers.
*/
function feeds_cache_clear($rebuild_menu = TRUE) {
cache_clear_all('_feeds_importer_digest', 'cache');
ctools_static_reset('_feeds_importer_digest');
ctools_include('export');
ctools_export_load_object_reset('feeds_importer');
node_get_types('types', NULL, TRUE);
if ($rebuild_menu) {
menu_rebuild();
}
}
* Exports a FeedsImporter configuration to code.
Alex Barth
committed
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);
}
}
* Logs to a file like /mytmp/feeds_my_domain_org.log in temporary directory.
*/
function feeds_dbg($msg) {
if (variable_get('feeds_debug', false)) {
if (!is_string($msg)) {
$msg = var_export($msg, true);
}
$filename = trim(str_replace('/', '_', $_SERVER['HTTP_HOST'] . base_path()), '_');
$handle = fopen(file_directory_temp() ."/feeds_$filename.log", 'a');
fwrite($handle, date('c') ."\t$msg\n");
fclose($handle);
}
}
Alex Barth
committed
/**
Alex Barth
committed
*/
/**
* @defgroup instantiators Instantiators
* @{
*/
/**
* Gets an importer instance.
Alex Barth
committed
*
* @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);
}
/**
* Gets an instance of a source object.
Alex Barth
committed
*
* @param $importer_id
* A FeedsImporter id.
Alex Barth
committed
* @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_id, $feed_nid = 0) {
Alex Barth
committed
feeds_include('FeedsImporter');
return FeedsSource::instance($importer_id, $feed_nid);
Alex Barth
committed
}
/**
* Gets a scheduler instance.
Alex Barth
committed
*
* @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();
Alex Barth
committed
*/
/**
* @defgroup plugins Plugin functions
* @{
*
* @todo Encapsulate this in a FeedsPluginHandler class, move it to includes/
Alex Barth
committed
* and only load it if we're manipulating plugins.
*/
/**
* Gets all available plugins. Does not list hidden plugins.
Alex Barth
committed
*
* @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() {
Alex Barth
committed
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
Alex Barth
committed
foreach ($plugins as $key => $info) {
if (!empty($info['hidden'])) {
continue;
Alex Barth
committed
$result[$key] = $info;
Alex Barth
committed
// Sort plugins by name and return.
uasort($result, 'feeds_plugin_compare');
return $result;
}
/**
Alex Barth
committed
* Sort callback for feeds_get_plugins().
Alex Barth
committed
function feeds_plugin_compare($a, $b) {
return strcasecmp($a['name'], $b['name']);
* Gets all available plugins of a particular type.
Alex Barth
committed
*
* @param $type
* 'fetcher', 'parser' or 'processor'
Alex Barth
committed
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;
* Gets an instance of a class for a given plugin and id.
Alex Barth
committed
*
* @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.
Alex Barth
committed
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);
$args = array( '%plugin' => $plugin);
if (user_access('administer feeds')) {
$args['!link'] = l($id, 'admin/build/feeds/edit/' . $id);
drupal_set_message(t('Missing Feeds plugin %plugin. See !link. Check whether all required libraries and modules are installed properly.', $args), 'warning');
}
else {
drupal_set_message(t('Missing Feeds plugin %plugin. Please contact your site administrator.', $args), 'warning');
}
$class = ctools_plugin_load_class('feeds', 'plugins', 'FeedsMissingPlugin', 'handler');
return FeedsConfigurable::instance($class, $id);
Alex Barth
committed
* Determines whether given plugin is derived from given base plugin.
*
Alex Barth
committed
* 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_key, $parent_plugin) {
Alex Barth
committed
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
$info = $plugins[$plugin_key];
Alex Barth
committed
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);
* Determines the type of a plugin.
Alex Barth
committed
*
* @param $plugin_key
* String that identifies a Feeds plugin key.
Alex Barth
committed
*
* @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.
Alex Barth
committed
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;
Alex Barth
committed
/**
Alex Barth
committed
*/
/**
* @defgroup include Funtions for loading libraries
* @{
*/
/**
* Includes a feeds module include file.
Alex Barth
committed
*
* @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
Alex Barth
committed
function feeds_include($file, $directory = 'includes') {
static $included = array();
if (!isset($included[$file])) {
Alex Barth
committed
require './'. drupal_get_path('module', 'feeds') ."/$directory/$file.inc";
Alex Barth
committed
$included[$file] = TRUE;
}
Alex Barth
committed
/**
* Includes a library file.
Alex Barth
committed
*
* @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;
}
Alex Barth
committed
/**
* 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;
}
/**