| 1 |
<?php
|
| 2 |
// $Id: feedapi.module,v 1.22 2007/07/24 14:03:47 aronnovak Exp $
|
| 3 |
|
| 4 |
/**
|
| 5 |
* @file
|
| 6 |
* Handle the submodules (for feed and item processing)
|
| 7 |
* Provide a basic management of feeds
|
| 8 |
*/
|
| 9 |
|
| 10 |
/**
|
| 11 |
* Implementation of hook_help().
|
| 12 |
*/
|
| 13 |
function feedapi_help($section) {
|
| 14 |
switch ($section) {
|
| 15 |
case 'admin/help#feedapi':
|
| 16 |
return t('Provides feed management interface and handle underlying processors and parsers for any type of feeds.');
|
| 17 |
}
|
| 18 |
}
|
| 19 |
|
| 20 |
/**
|
| 21 |
* Implementation of hook_menu().
|
| 22 |
*/
|
| 23 |
function feedapi_menu($may_cache) {
|
| 24 |
if ($may_cache) {
|
| 25 |
$items[] = array(
|
| 26 |
'path' => 'admin/content/feed',
|
| 27 |
'title' => t('Feed'),
|
| 28 |
'callback' => 'feedapi_management_page',
|
| 29 |
'access' => user_access('administer feedapi'),
|
| 30 |
);
|
| 31 |
$items[] = array(
|
| 32 |
'path' => 'admin/content/feed/list',
|
| 33 |
'title' => t('List'),
|
| 34 |
'type' => MENU_DEFAULT_LOCAL_TASK,
|
| 35 |
'weight' => -15,
|
| 36 |
);
|
| 37 |
$items[] = array(
|
| 38 |
'path' => 'feed/add',
|
| 39 |
'title' => t('Create feed'),
|
| 40 |
'callback' => 'drupal_get_form',
|
| 41 |
'callback arguments' => array('feedapi_add_page'),
|
| 42 |
'access' => user_access('handle own feeds') || user_access('administer feedapi'),
|
| 43 |
);
|
| 44 |
$items[] = array('path' => 'admin/settings/feedapi',
|
| 45 |
'title' => t('FeedAPI settings'),
|
| 46 |
'callback' => 'drupal_get_form',
|
| 47 |
'callback arguments' => array('feedapi_admin_settings'),
|
| 48 |
'type' => MENU_NORMAL_ITEM,
|
| 49 |
'access' => user_access('administer feedapi'),
|
| 50 |
);
|
| 51 |
}
|
| 52 |
else if (arg(0) == 'feed' && is_numeric(arg(1))) {
|
| 53 |
global $user;
|
| 54 |
$feed = new stdClass();
|
| 55 |
$feed->fid = arg(1);
|
| 56 |
feedapi_invoke_feedapi("load", $feed);
|
| 57 |
$own_feed = $feed->uid == $user->uid && user_access('handle own feeds') ? TRUE : FALSE;
|
| 58 |
$items[] = array('path' => 'feed/'. arg(1) .'/delete', 'title' => t('Delete'),
|
| 59 |
'callback' => 'drupal_get_form',
|
| 60 |
'callback arguments' => array('feedapi_delete_confirm', $feed),
|
| 61 |
'access' => user_access('administer feedapi') || $own_feed,
|
| 62 |
'type' => MENU_CALLBACK
|
| 63 |
);
|
| 64 |
$items[] = array('path' => 'feed/'. arg(1) .'/refresh', 'title' => t('Refresh'),
|
| 65 |
'callback' => 'feedapi_invoke_feedapi',
|
| 66 |
'callback arguments' => array("refresh", $feed),
|
| 67 |
'access' => user_access('administer feedapi') || $own_feed,
|
| 68 |
'type' => MENU_CALLBACK
|
| 69 |
);
|
| 70 |
$items[] = array('path' => 'feed/'. arg(1) .'/edit', 'title' => t('Edit'),
|
| 71 |
'callback' => 'drupal_get_form',
|
| 72 |
'callback arguments' => array('feedapi_edit_page', $feed),
|
| 73 |
'access' => user_access('administer feedapi') || $own_feed,
|
| 74 |
'type' => MENU_CALLBACK
|
| 75 |
);
|
| 76 |
}
|
| 77 |
return $items;
|
| 78 |
}
|
| 79 |
|
| 80 |
/**
|
| 81 |
* Implementation of hook_perm().
|
| 82 |
*/
|
| 83 |
function feedapi_perm() {
|
| 84 |
return array('administer feedapi', 'handle own feeds');
|
| 85 |
}
|
| 86 |
|
| 87 |
/**
|
| 88 |
* Do various things with feed. Handle the core data and call the
|
| 89 |
* underyling modules (parsers/processors) too
|
| 90 |
*
|
| 91 |
* @param $op
|
| 92 |
* "load" Load the feed. $param can be:
|
| 93 |
* "no" - the feed object won't have items member
|
| 94 |
* "info" - only the nid, fid, fiid data members will put ito each item
|
| 95 |
* "full" - the whole stored structure will be fetched into items
|
| 96 |
* "refresh" Re-download the feed and process newly arrived item
|
| 97 |
* "purge" Delete the feed and all connected things (eg. items)
|
| 98 |
* "save" Save the feed
|
| 99 |
* "update" Update the feed
|
| 100 |
*
|
| 101 |
* FIXME: maybe split into private functions
|
| 102 |
*
|
| 103 |
* @param $feed
|
| 104 |
* A feed object. If only the ID is known, you should pass something like this: $feed->fid = X
|
| 105 |
* @param $param
|
| 106 |
* Depends on the $op value.
|
| 107 |
*/
|
| 108 |
function feedapi_invoke_feedapi($op, &$feed, $param = NULL) {
|
| 109 |
if (!is_object($feed)) {
|
| 110 |
return FALSE;
|
| 111 |
}
|
| 112 |
$user = isset($feed->user) || isset($feed->uid) ? user_load(array('name' => $feed->user, 'uid' => $feed->uid)) : NULL;
|
| 113 |
if (!isset($user->uid) || $user->uid == 0) {
|
| 114 |
global $user;
|
| 115 |
}
|
| 116 |
_feedapi_sanitize_processors($feed);
|
| 117 |
switch ($op) {
|
| 118 |
case 'load':
|
| 119 |
$load_items = $param;
|
| 120 |
$feed = db_fetch_object(db_query("SELECT * FROM {feedapi} WHERE fid = %d", $feed->fid));
|
| 121 |
$feed->parsers = unserialize($feed->parsers);
|
| 122 |
$feed->processors_item = unserialize($feed->processors_item);
|
| 123 |
$feed->processors_feed = unserialize($feed->processors_feed);
|
| 124 |
_feedapi_sanitize_processors($feed);
|
| 125 |
$feed->options = new stdClass();
|
| 126 |
// Load additional elements provided by the processors
|
| 127 |
foreach ($feed->processors_feed as $processor) {
|
| 128 |
$feed = module_invoke($processor, "feedapi_load", $feed);
|
| 129 |
}
|
| 130 |
if ($load_items == "info" || $load_items == "full") {
|
| 131 |
$feed->items = array();
|
| 132 |
foreach ($feed->processors_item as $processor) {
|
| 133 |
$items = module_invoke($processor, "feedapi_item_fetch_items", $feed);
|
| 134 |
$feed->items += is_array($items) ? $items : array();
|
| 135 |
}
|
| 136 |
}
|
| 137 |
if ($load_items == "full") {
|
| 138 |
foreach ($feed->items as $key => $item) {
|
| 139 |
foreach ($feed->processors_item as $processor) {
|
| 140 |
$item = module_invoke($processor, "feedapi_item_load", $item);
|
| 141 |
$feed->items[$key] = $item;
|
| 142 |
}
|
| 143 |
}
|
| 144 |
}
|
| 145 |
module_invoke_all('feedapi_after_load', $feed);
|
| 146 |
break;
|
| 147 |
|
| 148 |
case 'refresh':
|
| 149 |
$cron = $param;
|
| 150 |
$goto = user_access('administer feedapi') ? 'admin/content/feed' : drupal_init_path();
|
| 151 |
feedapi_invoke_feedapi("load", $feed, "info");
|
| 152 |
if (!is_array($feed->processors_item) || count($feed->processors_item) == 0) {
|
| 153 |
if (!$cron) {
|
| 154 |
drupal_set_message(t("This feed (%url) doesn't have any item processor. It's not possible to refresh the feed.", array('%url' => $feed->url)), "error");
|
| 155 |
drupal_goto($goto);
|
| 156 |
}
|
| 157 |
return;
|
| 158 |
}
|
| 159 |
if (!is_array($feed->processors_feed) || count($feed->processors_feed) == 0) {
|
| 160 |
if (!$cron) {
|
| 161 |
drupal_set_message(t("This feed (%url) doesn't have any feed processor. It's not possible to refresh the feed.", array('%url' => $feed->url)), "error");
|
| 162 |
drupal_goto($goto);
|
| 163 |
}
|
| 164 |
return;
|
| 165 |
}
|
| 166 |
$processors_item = $feed->processors_item;
|
| 167 |
$processors_feed = $feed->processors_feed;
|
| 168 |
// Force the processors to delete old items and determine the max. create elements
|
| 169 |
$max_new_items = variable_get('feedapi_refresh_once', 10);
|
| 170 |
foreach ($feed->processors_feed as $processor) {
|
| 171 |
module_invoke($processor, 'feedapi_expire', $feed, $cron === TRUE ? FALSE : TRUE);
|
| 172 |
}
|
| 173 |
$feed = _feedapi_call_parsers($feed, $feed->parsers);
|
| 174 |
$feed->processors_item = $processors_item;
|
| 175 |
$feed->processors_feed = $processors_feed;
|
| 176 |
$items = array_reverse($feed->items);
|
| 177 |
$updated = 0;
|
| 178 |
$new = 0;
|
| 179 |
// Walk through the items
|
| 180 |
foreach ($items as $item) {
|
| 181 |
// Call each item parser
|
| 182 |
$is_updated = FALSE;
|
| 183 |
$is_new = FALSE;
|
| 184 |
foreach ($feed->processors_item as $processor) {
|
| 185 |
if (!module_invoke($processor, 'feedapi_item_unique', $item, $feed->fid)) {
|
| 186 |
module_invoke($processor, 'feedapi_item_update', $item, $feed->fid);
|
| 187 |
$is_updated = TRUE;
|
| 188 |
}
|
| 189 |
else {
|
| 190 |
module_invoke($processor, 'feedapi_item_save', $item, $feed->fid);
|
| 191 |
$is_new = TRUE;
|
| 192 |
}
|
| 193 |
}
|
| 194 |
$new = $is_new ? $new + 1 : $new;
|
| 195 |
$updated = ($is_updated && !$is_new) ? $updated + 1 : $updated;
|
| 196 |
// If we consumed the max new item limit -> stop
|
| 197 |
if ($max_new_items == $new) {
|
| 198 |
break;
|
| 199 |
}
|
| 200 |
}
|
| 201 |
db_query("UPDATE {feedapi} SET checked = %d WHERE fid = %d", time(), $feed->fid);
|
| 202 |
module_invoke_all('feedapi_after_refresh', $feed);
|
| 203 |
if (!$cron) {
|
| 204 |
drupal_set_message(t("%new feed item(s) are created and %updated feed items are updated.", array("%new" => $new, "%updated" => $updated)));
|
| 205 |
drupal_goto($goto);
|
| 206 |
}
|
| 207 |
break;
|
| 208 |
|
| 209 |
case 'purge':
|
| 210 |
feedapi_invoke_feedapi("load", $feed, "info");
|
| 211 |
// Delete items from the processors
|
| 212 |
foreach ($feed->items as $item) {
|
| 213 |
foreach($feed->processors_item as $processor) {
|
| 214 |
module_invoke($processor, 'feedapi_item_delete', $item);
|
| 215 |
}
|
| 216 |
}
|
| 217 |
// Delete feed from the processors
|
| 218 |
foreach($feed->processors_feed as $processor) {
|
| 219 |
module_invoke($processor, 'feedapi_delete', $feed);
|
| 220 |
}
|
| 221 |
// Delete core data
|
| 222 |
db_query("DELETE FROM {feedapi} WHERE fid = %d", $feed->fid);
|
| 223 |
module_invoke_all('feedapi_after_purge', $feed);
|
| 224 |
drupal_goto("admin/content/feed");
|
| 225 |
break;
|
| 226 |
|
| 227 |
case 'save':
|
| 228 |
db_query("INSERT INTO {feedapi} (
|
| 229 |
url, feed_type, processors_item, processors_feed,
|
| 230 |
parsers, refresh, checked, uid) VALUES
|
| 231 |
('%s', '%s', '%s', '%s', '%s', %d, %d, %d)",
|
| 232 |
$feed->url, $feed->feed_type, serialize($feed->processors_item),
|
| 233 |
serialize($feed->processors_feed), serialize($feed->parsers),
|
| 234 |
$feed->refresh, 0, // Means that the feed have never been refreshed yet
|
| 235 |
$user->uid
|
| 236 |
);
|
| 237 |
$feed->fid = db_result(db_query("SELECT fid FROM {feedapi} WHERE url = '%s'", $feed->url));
|
| 238 |
// Parse the feed with the parsers
|
| 239 |
$title = $feed->title;
|
| 240 |
$description = $feed->description;
|
| 241 |
$feed = _feedapi_call_parsers($feed, $feed->parsers);
|
| 242 |
// If the user provided a title and a description, drop the parsers' extracted
|
| 243 |
$feed->title = !empty($title) ? $title : $feed->title;
|
| 244 |
$feed->description = !empty($description) ? $description : $feed->description;
|
| 245 |
foreach ($feed->processors_feed as $processor) {
|
| 246 |
$feed = module_invoke($processor, 'feedapi_save', $feed);
|
| 247 |
}
|
| 248 |
module_invoke_all('feedapi_after_save', $feed);
|
| 249 |
drupal_set_message(t('The %feed feed (%url) was saved successfully.', array('%feed' => $feed->title, '%url' => $feed->url)));
|
| 250 |
// Tell the user if the feed is currently not usable
|
| 251 |
if (strlen($feed->feed_type) < 1
|
| 252 |
|| !module_exists($feed->parsers['primary'])
|
| 253 |
|| !module_exists($feed->processors_feed[0])
|
| 254 |
|| !module_exists($feed->processors_item[0])) {
|
| 255 |
drupal_set_message(t('The feed is not usable now, because there is no suitable processors and parser in the system'));
|
| 256 |
}
|
| 257 |
break;
|
| 258 |
|
| 259 |
case 'update':
|
| 260 |
// Store the common things
|
| 261 |
db_query("UPDATE {feedapi} SET
|
| 262 |
feed_type = '%s', url = '%s', processors_item = '%s',
|
| 263 |
processors_feed = '%s', parsers = '%s', refresh = %d,
|
| 264 |
checked = %d, uid = %d WHERE fid = %d",
|
| 265 |
$feed->feed_type, $feed->url, serialize($feed->processors_item),
|
| 266 |
serialize($feed->processors_feed), serialize($feed->parsers), $feed->refresh,
|
| 267 |
$feed->checked, $user->uid, $feed->fid
|
| 268 |
);
|
| 269 |
// Parse the feed with the parsers
|
| 270 |
$title = $feed->title;
|
| 271 |
$description = $feed->description;
|
| 272 |
$feed = _feedapi_call_parsers($feed, $feed->parsers);
|
| 273 |
// If the user provided a title and a description, drop the parsers' extracted
|
| 274 |
$feed->title = !empty($title) ? $title : $feed->title;
|
| 275 |
$feed->description = !empty($description) ? $description : $feed->description;
|
| 276 |
// Call the enabled feed processors
|
| 277 |
foreach ($feed->processors_feed as $processor) {
|
| 278 |
// Check if the feed processor has this feed or not
|
| 279 |
$feed_try_load = new stdClass();
|
| 280 |
$feed_try_load->options = new stdClass();
|
| 281 |
$feed_try_load->fid = $feed->fid;
|
| 282 |
if (module_invoke($processor, 'feedapi_load', $feed_try_load) === FALSE) {
|
| 283 |
$feed = module_invoke($processor, 'feedapi_save', $feed);
|
| 284 |
}
|
| 285 |
else {
|
| 286 |
$feed = module_invoke($processor, 'feedapi_update', $feed);
|
| 287 |
}
|
| 288 |
}
|
| 289 |
module_invoke_all('feedapi_after_update', $feed);
|
| 290 |
drupal_set_message(t('The %feed feed (%url) was updated successfully.', array('%feed' => $feed->title, '%url' => $feed->url)));
|
| 291 |
break;
|
| 292 |
}
|
| 293 |
}
|
| 294 |
|
| 295 |
/**
|
| 296 |
* Implementation of hook_cron().
|
| 297 |
*/
|
| 298 |
function feedapi_cron() {
|
| 299 |
$result = db_query_range("SELECT fid FROM {feedapi} ORDER BY checked ASC", 0, variable_get('feedapi_cron_max', 5));
|
| 300 |
while ($fid = db_result($result, $row++)) {
|
| 301 |
$feed = new stdClass();
|
| 302 |
$feed->fid = $fid;
|
| 303 |
feedapi_invoke_feedapi('load', $feed);
|
| 304 |
feedapi_invoke_feedapi('refresh', $feed, TRUE);
|
| 305 |
}
|
| 306 |
}
|
| 307 |
|
| 308 |
/**
|
| 309 |
* Menu callback -- ask for confirmation of feed deletion
|
| 310 |
*/
|
| 311 |
function feedapi_delete_confirm($feed) {
|
| 312 |
$form['fid'] = array('#type' => 'value', '#value' => $feed->fid);
|
| 313 |
return confirm_form($form,
|
| 314 |
t('Are you sure you want to delete %title?', array('%title' => $feed->title)),
|
| 315 |
'admin/content/feed',
|
| 316 |
t('This action cannot be undone.'),
|
| 317 |
t('Delete'), t('Cancel'));
|
| 318 |
}
|
| 319 |
|
| 320 |
/**
|
| 321 |
* Execute feed deletion
|
| 322 |
*/
|
| 323 |
function feedapi_delete_confirm_submit($form_id, $form_values) {
|
| 324 |
if ($form_values['confirm']) {
|
| 325 |
$feed = new stdClass();
|
| 326 |
$feed->fid = $form_values['fid'];
|
| 327 |
feedapi_invoke_feedapi("purge", $feed);
|
| 328 |
}
|
| 329 |
}
|
| 330 |
|
| 331 |
/**
|
| 332 |
* Provide a UI for overviewing the existing feeds
|
| 333 |
*/
|
| 334 |
function feedapi_management_page() {
|
| 335 |
$header = array(t('URL'),
|
| 336 |
t('Refresh interval'),
|
| 337 |
t('Last refresh'),
|
| 338 |
t('Title'),
|
| 339 |
t('Commands')
|
| 340 |
);
|
| 341 |
$rows = array();
|
| 342 |
$result = db_query("SELECT fid from {feedapi}");
|
| 343 |
while ($fid = db_result($result, $row++)) {
|
| 344 |
$commands = array(l(t('Delete'), 'feed/'. $fid . '/delete'),
|
| 345 |
l(t('Refresh'), 'feed/'. $fid . '/refresh'),
|
| 346 |
l(t('Edit'), 'feed/'. $fid . '/edit'),
|
| 347 |
);
|
| 348 |
$ext_commands = module_invoke_all('feedapi_edit_option', $fid);
|
| 349 |
if (count($ext_commands) > 0) {
|
| 350 |
foreach ($ext_commands as $command) {
|
| 351 |
$commands[] = l($command['name'], $command['link']);
|
| 352 |
}
|
| 353 |
}
|
| 354 |
$feed = new stdClass();
|
| 355 |
$feed->fid = $fid;
|
| 356 |
feedapi_invoke_feedapi("load", $feed);
|
| 357 |
$rows[] = array($feed->url,
|
| 358 |
format_interval($feed->refresh),
|
| 359 |
$feed->checked == 0 ? t('Never') : format_interval(time() - $feed->checked) ." ". t("ago"),
|
| 360 |
$feed->title,
|
| 361 |
theme_item_list($commands)
|
| 362 |
);
|
| 363 |
}
|
| 364 |
return theme_table($header, $rows);
|
| 365 |
}
|
| 366 |
|
| 367 |
/**
|
| 368 |
* Provide UI for edit a feed
|
| 369 |
*/
|
| 370 |
function feedapi_edit_page($feed) {
|
| 371 |
$form = _feedapi_common_form($feed);
|
| 372 |
foreach (array('fid', 'feed_type', 'checked') as $value) {
|
| 373 |
$form[$value] = array('#type' => 'hidden', '#value' => $feed->{$value});
|
| 374 |
}
|
| 375 |
// Select from the parsers and the processors
|
| 376 |
$all = strlen($feed->feed_type) < 1 ? TRUE : FALSE;
|
| 377 |
$form['processors_feed'] = array('#type' => 'checkboxes',
|
| 378 |
'#options' => _feedapi_suitable_feed_processors($feed->feed_type, $all),
|
| 379 |
'#default_value' => $feed->processors_feed,
|
| 380 |
'#title' => t('Feed processor'),
|
| 381 |
);
|
| 382 |
$form['processors_item'] = array('#type' => 'checkboxes',
|
| 383 |
'#options' => _feedapi_suitable_item_processors($feed->feed_type, $all),
|
| 384 |
'#default_value' => $feed->processors_item,
|
| 385 |
'#title' => t('Feed item processor'),
|
| 386 |
);
|
| 387 |
$parsers = _feedapi_suitable_parsers($feed->feed_type, $all);
|
| 388 |
$form['primary'] = array('#type' => 'radios',
|
| 389 |
'#title' => t('Primary parser'),
|
| 390 |
'#options' => $parsers,
|
| 391 |
'#default_value' => $feed->parsers['primary'],
|
| 392 |
'#required' => TRUE,
|
| 393 |
);
|
| 394 |
$form['secondary'] = array('#type' => 'checkboxes',
|
| 395 |
'#title' => t('Secondary parsers'),
|
| 396 |
'#options' => $parsers,
|
| 397 |
'#default_value' => $feed->parsers['secondary'],
|
| 398 |
);
|
| 399 |
$form['processors_settings'] = array('#type' => 'fieldset',
|
| 400 |
'#title' => t('The currently enabled processors\' settings'),
|
| 401 |
'#collapsible' => TRUE,
|
| 402 |
'#collapsed' => FALSE,
|
| 403 |
);
|
| 404 |
$form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
|
| 405 |
return $form;
|
| 406 |
}
|
| 407 |
|
| 408 |
/**
|
| 409 |
* Validate as the submission
|
| 410 |
*/
|
| 411 |
function feedapi_edit_page_validate($form_id, $form_values) {
|
| 412 |
$feed = new stdClass();
|
| 413 |
$feed->url = $form_values['url'];
|
| 414 |
foreach ($form_values['processors_feed'] as $processor) {
|
| 415 |
$feed = module_invoke($processor, 'feedapi_validate', $form_id, $form_values, $feed);
|
| 416 |
}
|
| 417 |
// Check if the selected parser-processor configuration has a common type
|
| 418 |
$type = module_invoke($form_values['primary'], 'feedapi_type');
|
| 419 |
$type = is_array($type) ? $type : array();
|
| 420 |
foreach ($form_values['processors_item'] as $processor => $turned) {
|
| 421 |
if ($turned && !empty($processor)) {
|
| 422 |
$type = array_intersect($type, module_invoke($processor, 'feedapi_type'));
|
| 423 |
}
|
| 424 |
}
|
| 425 |
foreach ($form_values['processors_feed'] as $processor => $turned) {
|
| 426 |
if ($turned && !empty($processor)) {
|
| 427 |
$type = array_intersect($type, module_invoke($processor, 'feedapi_type'));
|
| 428 |
}
|
| 429 |
}
|
| 430 |
if (count($type) == 0) {
|
| 431 |
form_set_error('feedapi', t('You have selected an invalid parser-processor configuration.'), 'error');
|
| 432 |
}
|
| 433 |
}
|
| 434 |
|
| 435 |
/**
|
| 436 |
* Construct parsers and processors data structures and call update
|
| 437 |
*/
|
| 438 |
function feedapi_edit_page_submit($form_id, $form_values) {
|
| 439 |
$feed = new stdClass();
|
| 440 |
$feed->parsers = array();
|
| 441 |
$feed->parsers['primary'] = $form_values['primary'];
|
| 442 |
$feed->parsers['secondary'] = array();
|
| 443 |
foreach ($form_values['secondary'] as $parser => $turned) {
|
| 444 |
if ($turned) {
|
| 445 |
$feed->parsers['secondary'][] = $parser;
|
| 446 |
}
|
| 447 |
}
|
| 448 |
if (strlen($form_values['feed_type']) < 1) {
|
| 449 |
// Check the feed against the main parser to get the type
|
| 450 |
$form_values['feed_type'] = module_invoke($feed->parsers['primary'], 'feed_compatible', $form_values['url']);
|
| 451 |
}
|
| 452 |
$feed->processors_item = array_values($form_values['processors_item']);
|
| 453 |
$feed->processors_feed = array_values($form_values['processors_feed']);
|
| 454 |
$feed = (object) array_merge($form_values, (array) $feed);
|
| 455 |
feedapi_invoke_feedapi("update", $feed);
|
| 456 |
drupal_goto('admin/content/feed');
|
| 457 |
}
|
| 458 |
|
| 459 |
/**
|
| 460 |
* Provide a UI for create a feed
|
| 461 |
*/
|
| 462 |
function feedapi_add_page() {
|
| 463 |
$form = _feedapi_common_form();
|
| 464 |
$form['advanced'] = array('#type' => 'checkbox',
|
| 465 |
'#title' => t('Show me the advanced parser/processor configuration'),
|
| 466 |
'#default_value' => FALSE,
|
| 467 |
'#description' => t('After the feed will be created, you will be able to edit the assigned parsers and processors.'),
|
| 468 |
);
|
| 469 |
$form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
|
| 470 |
return $form;
|
| 471 |
}
|
| 472 |
|
| 473 |
/**
|
| 474 |
* Let the feed processor allow or reject the given URL
|
| 475 |
*/
|
| 476 |
function feedapi_add_page_validate($form_id, $form_values) {
|
| 477 |
$feed = new stdClass();
|
| 478 |
$feed->url = $form_values['url'];
|
| 479 |
$assigned = _feedapi_assign_parser_processor($feed->url);
|
| 480 |
$feed = module_invoke($assigned['processor_feed'], 'feedapi_validate', $form_id, $form_values, $feed);
|
| 481 |
if (db_result(db_query("SELECT fid FROM {feedapi} WHERE url = '%s'", $feed->url))) {
|
| 482 |
form_set_error('feedapi', t('The feed URL provided already exists. Please use a different URL.'));
|
| 483 |
}
|
| 484 |
}
|
| 485 |
|
| 486 |
/**
|
| 487 |
* Create the feed and invoke the the assigned processor
|
| 488 |
*/
|
| 489 |
function feedapi_add_page_submit($form_id, $form_values) {
|
| 490 |
// Assign one parser and one processor to the URL
|
| 491 |
$assigned = _feedapi_assign_parser_processor($form_values['url']);
|
| 492 |
$feed = new stdClass();
|
| 493 |
$feed = (object) array_merge($form_values, (array) $feed);
|
| 494 |
$feed->parsers = array();
|
| 495 |
// We store it in such structures because the user will be able to assign more parsers and processors to one feed later
|
| 496 |
$feed->parsers['primary'] = $assigned['parser'];
|
| 497 |
$feed->parsers['secondary'] = array();
|
| 498 |
$feed->feed_type = $assigned['type'];
|
| 499 |
$feed->processors_item = array($assigned['processor_item']);
|
| 500 |
$feed->processors_feed = array($assigned['processor_feed']);
|
| 501 |
// Store the common things
|
| 502 |
feedapi_invoke_feedapi('save', $feed);
|
| 503 |
drupal_goto(($form_values['advanced'] == TRUE) ? 'feed/'. $feed->fid. '/edit' : drupal_init_path());
|
| 504 |
}
|
| 505 |
|
| 506 |
/**
|
| 507 |
* Settings:
|
| 508 |
* Allowed HTML tags, number of feeds refreshed in one round
|
| 509 |
*/
|
| 510 |
function feedapi_admin_settings() {
|
| 511 |
$form['feedapi_allowed_html_tags'] = array(
|
| 512 |
'#type' => 'textfield', '#title' => t('Allowed HTML tags'), '#size' => 80, '#maxlength' => 255,
|
| 513 |
'#default_value' => variable_get('feedapi_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'),
|
| 514 |
'#description' => t('The list of tags which are allowed in feeds, i.e., which will not be removed by Drupal.')
|
| 515 |
);
|
| 516 |
$form['feedapi_cron_max'] = array(
|
| 517 |
'#type' => 'textfield', '#title' => t('Per-cron feeds refresh'), '#size' => 3, '#maxlength' => 10,
|
| 518 |
'#default_value' => variable_get('feedapi_cron_max', 5),
|
| 519 |
'#description' => t('How much feed should be refreshed in one cron run maximally')
|
| 520 |
);
|
| 521 |
|
| 522 |
$form['feedapi_refresh_once'] = array(
|
| 523 |
'#type' => 'textfield', '#title' => t('Maximal number of items updated/created in one refresh round'), '#size' => 5, '#maxlength' => 7,
|
| 524 |
'#default_value' => variable_get('feedapi_refresh_once', 10),
|
| 525 |
);
|
| 526 |
$form['advanced'] = array('#type' => 'fieldset',
|
| 527 |
'#title' => t('Default parser and processor configuration'),
|
| 528 |
'#description' => t('These will be used first when the detection mechanism try to assign a suitable parser and processor to the feed.
|
| 529 |
If these values are set to the most common situation, the feed-creation can be much faster.'),
|
| 530 |
'#collapsible' => TRUE,
|
| 531 |
'#collapsed' => TRUE,
|
| 532 |
);
|
| 533 |
|
| 534 |
$form['advanced']['parser'] = array('#type' => 'fieldset',
|
| 535 |
'#title' => t('Parsers order'),
|
| 536 |
'#description' => t('The lighter parser will be tried out earlier.'),
|
| 537 |
'#collapsible' => TRUE,
|
| 538 |
'#collapsed' => TRUE,
|
| 539 |
);
|
| 540 |
// Walk through all the possible processors - not just for one feed type's
|
| 541 |
$parsers = _feedapi_suitable_parsers('', TRUE);
|
| 542 |
foreach ($parsers as $parser) {
|
| 543 |
$form['advanced']['parser']['feedapi_weight_parser_'. $parser] = array(
|
| 544 |
'#type' => 'weight',
|
| 545 |
'#title' => $parser,
|
| 546 |
'#delta' => 10,
|
| 547 |
'#default_value' => variable_get('feedapi_weight_parser_'. $parser, 0),
|
| 548 |
);
|
| 549 |
}
|
| 550 |
$form['advanced']['feed_processor'] = array('#type' => 'fieldset',
|
| 551 |
'#title' => t('Feed processors order'),
|
| 552 |
'#description' => t('The lighter feed processor will be tried out earlier.'),
|
| 553 |
'#collapsible' => TRUE,
|
| 554 |
'#collapsed' => TRUE,
|
| 555 |
);
|
| 556 |
// Walk through all the possible processors - not just for one feed type's
|
| 557 |
$processors = _feedapi_suitable_feed_processors('', TRUE);
|
| 558 |
foreach ($processors as $feed_processor) {
|
| 559 |
$form['advanced']['feed_processor']['feedapi_weight_feed_processor_'. $feed_processor] = array(
|
| 560 |
'#type' => 'weight',
|
| 561 |
'#title' => $feed_processor,
|
| 562 |
'#delta' => 10,
|
| 563 |
'#default_value' => variable_get('feedapi_weight_feed_processor_'. $feed_processor, 0),
|
| 564 |
);
|
| 565 |
}
|
| 566 |
|
| 567 |
$form['advanced']['item_processor'] = array('#type' => 'fieldset',
|
| 568 |
'#title' => t('Feed item processors order'),
|
| 569 |
'#description' => t('The lighter feed item processor will be tried out earlier.'),
|
| 570 |
'#collapsible' => TRUE,
|
| 571 |
'#collapsed' => TRUE,
|
| 572 |
);
|
| 573 |
// Walk through all the possible item processors - not just for one feed type's
|
| 574 |
$processors = _feedapi_suitable_item_processors('', TRUE);
|
| 575 |
foreach ($processors as $item_processor) {
|
| 576 |
$form['advanced']['item_processor']['feedapi_weight_item_processor_'. $item_processor] = array(
|
| 577 |
'#type' => 'weight',
|
| 578 |
'#title' => $item_processor,
|
| 579 |
'#delta' => 10,
|
| 580 |
'#default_value' => variable_get('feedapi_weight_item_processor_'. $item_processor, 0),
|
| 581 |
);
|
| 582 |
}
|
| 583 |
return system_settings_form($form);
|
| 584 |
}
|
| 585 |
|
| 586 |
/**
|
| 587 |
* Get the list of the parsers for the given feed type
|
| 588 |
*
|
| 589 |
* @param $type
|
| 590 |
* The descriptor of the feed type. For eg. "XML feed"
|
| 591 |
* @param $all
|
| 592 |
* If you set this to TRUE, all the parsers will be returned (not check the type)
|
| 593 |
* @return
|
| 594 |
* An associative array with the names of the suitable parsers
|
| 595 |
*/
|
| 596 |
function _feedapi_suitable_parsers($type, $all = FALSE) {
|
| 597 |
$req = array('feedapi_type', 'feedapi_parse');
|
| 598 |
return _feedapi_check_requirement($type, $req, $all);
|
| 599 |
}
|
| 600 |
|
| 601 |
/**
|
| 602 |
* Execute the enabled parsers and create an unified output
|
| 603 |
*
|
| 604 |
* @param $feed
|
| 605 |
* Feed object
|
| 606 |
* @param $parsers
|
| 607 |
* Structure: array(
|
| 608 |
* "primary" => "parser_primary",
|
| 609 |
* "secondary" => array("parser1", "parser2", "parserN")
|
| 610 |
* );
|
| 611 |
* @return
|
| 612 |
* The object of the parser data
|
| 613 |
*/
|
| 614 |
function _feedapi_call_parsers($feed, $parsers) {
|
| 615 |
$fid = $feed->fid;
|
| 616 |
if (module_exists($parsers["primary"])) {
|
| 617 |
$parser_output = module_invoke($parsers["primary"], "feedapi_parse", $feed);
|
| 618 |
$feed = (object) array_merge((array) $feed, (array) $parser_output);
|
| 619 |
}
|
| 620 |
// Call the turned on parsers, create an union of returned options
|
| 621 |
foreach ($parsers["secondary"] as $parser) {
|
| 622 |
$feed_ext = module_invoke($parser, "feedapi_parse", $feed);
|
| 623 |
$feed->options = (object) ((array) $feed->options + (array) $feed_ext->options);
|
| 624 |
}
|
| 625 |
$feed->fid = $fid;
|
| 626 |
// Filter bad or not allowed tags
|
| 627 |
$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);
|
| 628 |
foreach (array('title', 'description') as $property) {
|
| 629 |
if (isset($feed->{$property})) {
|
| 630 |
if (is_string($feed->{$property})) {
|
| 631 |
$feed->{$property} = filter_xss($feed->{$property}, $allowed);
|
| 632 |
}
|
| 633 |
}
|
| 634 |
}
|
| 635 |
for ($i = 0; $i < count($feed->items); $i++) {
|
| 636 |
$feed->items[$i]->title = filter_xss($feed->items[$i]->title, $allowed);
|
| 637 |
$feed->items[$i]->description = filter_xss($feed->items[$i]->description, $allowed);
|
| 638 |
}
|
| 639 |
return $feed;
|
| 640 |
}
|
| 641 |
|
| 642 |
/**
|
| 643 |
* Auto-detect one possible parser and one feed and item processor to the given URL
|
| 644 |
* It follows the order of the modules from the module's settings page
|
| 645 |
*
|
| 646 |
* @param $url
|
| 647 |
* The fully-qualified URL of the feed
|
| 648 |
*/
|
| 649 |
function _feedapi_assign_parser_processor($url) {
|
| 650 |
$parsers = _feedapi_suitable_parsers('', TRUE);
|
| 651 |
// Get the parsers' weight
|
| 652 |
$sorted_parsers = array();
|
| 653 |
foreach ($parsers as $parser) {
|
| 654 |
$sorted_parsers[variable_get('feedapi_weight_parser_'. $parser, 0)][] = $parser;
|
| 655 |
}
|
| 656 |
$success = FALSE;
|
| 657 |
$keys = array_keys($sorted_parsers);
|
| 658 |
if (count($keys) > 0) {
|
| 659 |
$low = min($keys);
|
| 660 |
$max = max($keys);
|
| 661 |
// Call the ordered parsers to find one compatible
|
| 662 |
for ($i = $low; $i <= $max; $i++) {
|
| 663 |
if (isset($sorted_parsers[$i])) {
|
| 664 |
foreach ($sorted_parsers[$i] as $parser) {
|
| 665 |
$type = module_invoke($parser, 'feedapi_compatible', $url);
|
| 666 |
$i = is_string($type) ? $max + 2: $i;
|
| 667 |
$success = is_string($type) ? TRUE : $success;
|
| 668 |
}
|
| 669 |
}
|
| 670 |
}
|
| 671 |
}
|
| 672 |
if (!$success) {
|
| 673 |
// We can't found a suitable parser at all
|
| 674 |
return array('type' => '', 'parser' => '', 'processor_feed' => '', 'processor_item' => '');
|
| 675 |
}
|
| 676 |
// Get the feed processor's weight
|
| 677 |
$processors_feed = _feedapi_suitable_feed_processors($type);
|
| 678 |
foreach ($processors_feed as $processor) {
|
| 679 |
$sorted_feed_processors[variable_get('feedapi_weight_feed_processor_'. $processor, 0)][] = $processor;
|
| 680 |
}
|
| 681 |
// Get the feed item processor's weight
|
| 682 |
$processors_item = _feedapi_suitable_item_processors($type);
|
| 683 |
foreach ($processors_item as $processor) {
|
| 684 |
$sorted_item_processors[variable_get('feedapi_weight_item_processor_'. $processor, 0)][] = $processor;
|
| 685 |
}
|
| 686 |
return array(
|
| 687 |
'type' => $type,
|
| 688 |
'parser' => $parser,
|
| 689 |
'processor_feed' => array_shift($sorted_feed_processors[min(array_keys($sorted_feed_processors))]), // lightest item processor
|
| 690 |
'processor_item' => array_shift($sorted_item_processors[min(array_keys($sorted_item_processors))]) // lightest feed processor
|
| 691 |
);
|
| 692 |
}
|
| 693 |
|
| 694 |
/**
|
| 695 |
* Collect a list of an arrays that fulfill the given requirements (hooks and type)
|
| 696 |
*
|
| 697 |
* @param $type
|
| 698 |
* The descriptor of the feed type. For eg. "XML feed"
|
| 699 |
* @param $all
|
| 700 |
* If you set this to TRUE, all the feed processors will be returned (not check the type)
|
| 701 |
* @return
|
| 702 |
* An associative array with the names of the suitable feed processors
|
| 703 |
*/
|
| 704 |
function _feedapi_check_requirement($type, $hooks, $all) {
|
| 705 |
$possible = array();
|
| 706 |
$suitable = array();
|
| 707 |
for ($i = count($hooks) - 1; $i >= 0; $i--) {
|
| 708 |
$hooks[$i] = module_implements($hooks[$i]);
|
| 709 |
}
|
| 710 |
$possible = call_user_func_array('array_intersect', $hooks);
|
| 711 |
foreach ($possible as $parser) {
|
| 712 |
$types = module_invoke($parser, "feedapi_type");
|
| 713 |
if (in_array($type, $types) || $all) {
|
| 714 |
$suitable[] = $parser;
|
| 715 |
}
|
| 716 |
}
|
| 717 |
return drupal_map_assoc($suitable);
|
| 718 |
}
|
| 719 |
|
| 720 |
/**
|
| 721 |
* Get the list of the feed processors for the given feed type
|
| 722 |
*
|
| 723 |
* @param $type
|
| 724 |
* The descriptor of the feed type. For eg. "XML feed"
|
| 725 |
* @param $all
|
| 726 |
* If you set this to TRUE, all the feed processors will be returned (not check the type)
|
| 727 |
* @return
|
| 728 |
* An associative array with the names of the suitable feed processors
|
| 729 |
*/
|
| 730 |
function _feedapi_suitable_feed_processors($type, $all = FALSE) {
|
| 731 |
$req = array('feedapi_type', 'feedapi_save', 'feedapi_update',
|
| 732 |
'feedapi_delete', 'feedapi_expire', 'feedapi_load',
|
| 733 |
'feedapi_validate', 'feedapi_get',
|
| 734 |
);
|
| 735 |
return _feedapi_check_requirement($type, $req, $all);
|
| 736 |
}
|
| 737 |
|
| 738 |
/**
|
| 739 |
* Get the list of the item processors for the given feed type
|
| 740 |
*
|
| 741 |
* @param $type
|
| 742 |
* The descriptor of the feed type. For eg. "XML feed"
|
| 743 |
* @param $all
|
| 744 |
* If you set this to TRUE, all the item processors will be returned (not check the type)
|
| 745 |
* @return
|
| 746 |
* An associative array with the names of the suitable feed processors
|
| 747 |
*/
|
| 748 |
function _feedapi_suitable_item_processors($type, $all = FALSE) {
|
| 749 |
$req = array('feedapi_type', 'feedapi_item_save', 'feedapi_item_update',
|
| 750 |
'feedapi_item_delete', 'feedapi_item_load',
|
| 751 |
'feedapi_item_unique', 'feedapi_item_fetch_items',
|
| 752 |
);
|
| 753 |
return _feedapi_check_requirement($type, $req, $all);
|
| 754 |
}
|
| 755 |
|
| 756 |
/**
|
| 757 |
* Common form parts of editing/creating a feed
|
| 758 |
*/
|
| 759 |
function _feedapi_common_form($feed = FALSE) {
|
| 760 |
$period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
|
| 761 |
$form['url'] = array('#type' => 'textfield',
|
| 762 |
'#title' => t('URL'),
|
| 763 |
'#description' => t('The fully-qualified URL of the feed.'),
|
| 764 |
'#maxlength' => 255,
|
| 765 |
'#required' => TRUE,
|
| 766 |
);
|
| 767 |
$form['title'] = array('#type' => 'textfield',
|
| 768 |
'#title' => t('Title'),
|
| 769 |
'#maxlength' => 64,
|
| 770 |
'#description' => 'If you fill this field, the title from the feed will be overwritten by this',
|
| 771 |
'#required' => FALSE,
|
| 772 |
);
|
| 773 |
$form['description'] = array('#type' => 'textarea',
|
| 774 |
'#title' => t('Description'),
|
| 775 |
'#description' => 'If you fill this field, the description from the feed will be overwritten by this',
|
| 776 |
);
|
| 777 |
$form['refresh'] = array('#type' => 'select',
|
| 778 |
'#title' => t('Update interval'),
|
| 779 |
'#options' => $period,
|
| 780 |
'#description' => t('The refresh interval indicating how often you want to update this feed. Requires crontab.'),
|
| 781 |
);
|
| 782 |
if (user_access('administer feedapi')) {
|
| 783 |
$form['user'] = array(
|
| 784 |
'#type' => 'textfield',
|
| 785 |
'#title' => t('The owner of the feed'),
|
| 786 |
'#autocomplete_path' => 'user/autocomplete',
|
| 787 |
);
|
| 788 |
}
|
| 789 |
if (is_object($feed)) {
|
| 790 |
$form['url']['#default_value'] = isset($feed->url) ? $feed->url : '';
|
| 791 |
$form['title']['#default_value'] = isset($feed->title) ? $feed->title : '';
|
| 792 |
$form['description']['#default_value'] = isset($feed->description) ? $feed->description : '';
|
| 793 |
$form['refresh']['#default_value'] = isset($feed->refresh) ? $feed->refresh : 900;
|
| 794 |
if (user_access('administer feedapi')) {
|
| 795 |
$user = user_load(array('uid' => $feed->uid));
|
| 796 |
$form['user']['#default_value'] = $user->name;
|
| 797 |
}
|
| 798 |
}
|
| 799 |
return $form;
|
| 800 |
}
|
| 801 |
|
| 802 |
/**
|
| 803 |
* Remove non-existing processors from the processors arrays
|
| 804 |
*/
|
| 805 |
function _feedapi_sanitize_processors(&$feed) {
|
| 806 |
foreach (array('processors_feed', 'processors_item') as $proc) {
|
| 807 |
if (is_array($feed->{$proc})) {
|
| 808 |
foreach ($feed->{$proc} as $key => $processor) {
|
| 809 |
if (!module_exists($processor)) {
|
| 810 |
unset($feed->{$proc}[$key]);
|
| 811 |
}
|
| 812 |
}
|
| 813 |
}
|
| 814 |
}
|
| 815 |
}
|