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