feedapi.module

Tracking 6.x-1.x branch
  1. drupal
    1. 5 contributions/feedapi/feedapi.module
    2. 6 contributions/feedapi/feedapi.module

Handle the submodules (for feed and item processing) Provide a basic management of feeds

Functions & methods

NameDescription
feedapi_blockImplementation of hook_block().
feedapi_content_type_submitStore per-content-type settings
feedapi_content_type_validatePrevent users to use the same weight for two or more parsers and processors because FeedAPI cannot handle this. And this is not neccessary too.
feedapi_create_nodeCreate a feedapi node programatically.
feedapi_cronImplementation of hook_cron().
feedapi_cron_timeCheck for time limits in cron processing.
feedapi_enabled_typeDetermines wether feedapi is enabled for given node type. If parser or processor is passed in, this function determines wether given parser or processor is enabled for given node type.
feedapi_expireDelete expired items and return informations about the feed refreshing
feedapi_expire_itemCallback for expired items. Does the actual deleting
feedapi_feedapi_settings_formImplementation of hook_feedapi_settings_form().
feedapi_form_alterImplementation of hook_form_alter().
feedapi_get_natural_nameGet the module-defined natural name of FeedAPI parser or processor Define this name in hook_help():
feedapi_get_settingsRetrieve settings per content type or per node.
feedapi_get_typesReturn a list of FeedAPI-enabled content-types list, ready-to-use for #options at FormsAPI
feedapi_helpImplementation of hook_help().
feedapi_invokeInvoke feedapi API callback functions.
feedapi_linkImplementation of hook_link().
feedapi_load_nodeLoad node by URL.
feedapi_menuImplementation of hook_menu().
feedapi_nodeapiImplementation of hook_nodeapi().
feedapi_node_typeImplementation of hook_node_type().
feedapi_node_validateBuild feed object on validate and submit. See feedapi_form_alter on finding out how it is called (via FormAPI)
feedapi_permImplementation of hook_perm().
feedapi_purge_confirmAsk for confirmation before deleting all the items
feedapi_purge_confirm_submitSubmitted items purging form. Drop all the items.
feedapi_refreshRefresh a feed node (= run enabled processors on it).
feedapi_simplified_formThis is shown instead of normal node form when the simplified form is chosen at the settings
feedapi_simplified_form_submitCreate the node object and save
feedapi_simplified_form_validateValidates simplified form.
feedapi_themeImplementation of hook_theme().
feedapi_views_apiImplementation of hook_node_views().
_feedapi_build_feed_objectBuilds feed object ready to be sticked onto node.
_feedapi_call_parsersExecute the enabled parsers and create an unified output
_feedapi_format_settingsReturns per content type settings ordered by weight and only those that are turned on.
_feedapi_get_statReturn the type-specific statistics data
_feedapi_insertInsert feedapi data to the DB when it's a new for for FeedAPI
_feedapi_invokeHelper function for feedapi_invoke().
_feedapi_invoke_purgeHelper function for feedapi_invoke(). Delete all feed items of a feed.
_feedapi_invoke_refreshHelper function for feedapi_invoke(). Refresh the feed, call the proper parsers and processors' hooks. Don't call this function directly, use feedapi_refresh() instead.
_feedapi_op_access
_feedapi_populateSet default value of $form elements if present in $settings.
_feedapi_populate_get_settingGets the setting for '#parent' (there must be a more efficent way)
_feedapi_process_textFilter texts from parsers
_feedapi_sanitize_processorsRemove non-existing processors from the processors arrays
_feedapi_store_settingsStores settings per content type or per node.
_feedapi_store_statStore statistics information
_feedapi_updateUpdate feed data of an existing feed
_feedapi_update_rateCalculate the average between-update time

Constants

NameDescription
FEEDAPI_CRON_ALWAYS_REFRESH
FEEDAPI_CRON_DEFAULT_REFRESH_TIME
FEEDAPI_CRON_FEEDS
FEEDAPI_CRON_NEVER_REFRESH
FEEDAPI_CRON_STAT_LIFETIME
FEEDAPI_NEVER_DELETE_OLD
FEEDAPI_TIMEOUT

File

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