You are here

feedapi.module in FeedAPI 5

Same filename and directory in other branches
  1. 6 feedapi.module

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

File

feedapi.module
View source
<?php

/**
 * @file
 * Handle the submodules (for feed and item processing)
 * Provide a basic management of feeds
 */
define('FEEDAPI_NEVER_DELETE_OLD', 0);
define('FEEDAPI_TIMEOUT', 1);

// Number of feeds to process for each step in cron.
define('FEEDAPI_CRON_FEEDS', 100);

// Minimum time that must elapse before a feed can be refreshed again on cron.
define('FEEDAPI_CRON_MIN_REFRESH_TIME', 1800);

// Prune FeedAPI stats 4 weeks
define('FEEDAPI_CRON_STAT_LIFETIME', 28 * 24 * 3600);

/**
 * Implementation of hook_help().
 */
function feedapi_help($section) {
  switch ($section) {
    case 'admin/help#feedapi':
      $output = '<p>' . t('Provides feed management interface and handles underlying processors and parsers for any type of feeds.') . '</p>';
      $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>';
      return $output;
    case 'admin/content/feed':
      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>';
    case 'admin/content/feed/import_opml':
      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>';
    case 'admin/settings/feedapi':
      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>';
  }
}

/**
 * Implementation of hook_menu().
 */
function feedapi_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/content/feed',
      'title' => t('Feeds'),
      'description' => t("Overview which content your site aggregates from other sites and see detailed statistics about the feeds."),
      'callback' => 'feedapi_admin_overview',
      'access' => user_access('administer feedapi'),
    );
    $items[] = array(
      'path' => 'admin/content/feed/list',
      'title' => t('List'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'access' => user_access('administer feedapi'),
      'weight' => -15,
    );
    $items[] = array(
      'path' => 'admin/content/feed/import_opml',
      'title' => t('Import OPML'),
      'access' => user_access('administer feedapi'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'feedapi_import_feeds_form',
      ),
    );
    $items[] = array(
      'path' => 'admin/content/feed/export_opml',
      'title' => t('Export all feeds as OPML'),
      'access' => user_access('administer feedapi'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'feedapi_export_opml',
      ),
    );
    $items[] = array(
      'path' => 'admin/settings/feedapi',
      'title' => t('FeedAPI settings'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'feedapi_admin_settings',
      ),
      'access' => user_access('administer feedapi'),
    );
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      if (isset($node->feed)) {
        global $user;
        $own_feed = $node->uid == $user->uid && user_access('edit own ' . $node->type . ' content') ? TRUE : FALSE;
        $items[] = array(
          'path' => 'node/' . $node->nid . '/refresh',
          'title' => t('Refresh'),
          'callback' => 'feedapi_refresh',
          'callback arguments' => array(
            $node,
            'node/' . $node->nid,
          ),
          'type' => MENU_LOCAL_TASK,
          'access' => user_access('administer feedapi') || $own_feed,
        );
        $items[] = array(
          'path' => 'node/' . $node->nid . '/purge',
          'title' => t('Remove items'),
          'callback' => 'feedapi_invoke',
          'callback arguments' => array(
            "purge",
            $node->feed,
            'items',
          ),
          'type' => MENU_LOCAL_TASK,
          'access' => user_access('administer feedapi') || $own_feed,
        );
      }
    }
  }
  return $items;
}

/**
 * Implementation of hook_nodeapi().
 */
function feedapi_nodeapi(&$node, $op, $teaser, $page) {
  if (isset($node->feed) || feedapi_enabled_type($node->type)) {
    switch ($op) {
      case 'insert':
        _feedapi_insert($node, $teaser, $page);
        break;
      case 'update':
        _feedapi_update($node, $teaser, $page);
        break;
      case 'load':
        if ($feed = db_fetch_object(db_query('SELECT * FROM {feedapi} WHERE nid = %d', $node->nid))) {
          $node->feed = $feed;
          $node->feed->settings = feedapi_get_settings($node->type, $node->nid);

          // Load parsers and processors from content type
          $node_type_settings = feedapi_get_settings($node->type);
          $node->feed->parsers = _feedapi_format_settings($node_type_settings, 'parsers');
          $node->feed->processors = _feedapi_format_settings($node_type_settings, 'processors');
        }
        break;
      case 'delete':

        // Could be a performance problem - think of thousands of node feed items.
        //  This is a temporary status. See: http://drupal.org/node/195723
        // feedapi_invoke_feedapi('purge', $node->feed);
        db_query("DELETE FROM {feedapi_stat} WHERE id = %d", $node->nid);
        db_query("DELETE FROM {feedapi} WHERE nid = %d", $node->nid);
        break;
      case 'validate':
        if (!$node->title) {
          form_set_error('title', t('Title could not be retrieved from feed.'));
        }
        break;
      case 'submit':

        // Todo: the first case essentially copies the entire feed array from feedapi to feed.
        // Figure out a way how to save this step.
        $node->feed = isset($node->feedapi_feed_object) ? $node->feedapi_feed_object : _feedapi_build_feed_object($node->type, $node->feedapi['feedapi_url']);
        break;
    }
  }
}

/**
 * Implementation of hook_node_type().
 */
function feedapi_node_type($op, $info) {
  switch ($op) {
    case 'delete':
      variable_del('feedapi_settings_' . $info->type);
      variable_del('feedapi_' . $info->type);
      break;
    case 'update':
      if (!empty($info->old_type) && $info->old_type != $info->type) {
        $setting = variable_get('feedapi_settings_' . $info->old_type, array());
        variable_del('feedapi_settings_' . $info->old_type);
        variable_set('feedapi_settings_' . $info->type, $setting);
      }
      break;
  }
}

/**
 * Implementation of hook_block().
 */
function feedapi_block($op = 'list', $delta = 0) {
  $blocks = array();
  $names = feedapi_get_types();
  switch ($op) {
    case 'list':
      foreach ($names as $type => $name) {
        $blocks[$type]['info'] = t('FeedAPI: Quick create !preset', array(
          '!preset' => $name,
        ));
      }
      break;
    case 'view':
      if (node_access('create', $delta)) {
        $blocks['subject'] = t('Create !preset', array(
          '!preset' => $names[$delta],
        ));
        $blocks['content'] = drupal_get_form('feedapi_simplified_form', $delta);
      }
      break;
  }
  return $blocks;
}

/**
 * Implementation of hook_perm().
 */
function feedapi_perm() {
  return array(
    'administer feedapi',
    'advanced feedapi options',
  );
}

/**
 * Implementation of hook_link().
 */
function feedapi_link($type, $node = NULL) {
  if ($type == 'node' && isset($node->feed)) {
    if (strlen($node->feed->link) > 0) {
      $links['feedapi_original'] = array(
        'title' => t('Link to site'),
        'href' => $node->feed->link,
      );
      return $links;
    }
  }
}

/**
 * Invoke feedapi API callback functions.
 *
 * @param $op
 *  "load"    Load the feed items basic data into the $feed->items[]
 *  "refresh"  Re-download the feed and process newly arrived item
 *  "purge"    Delete all the feed items
 * 
 * @param $feed
 *  A feed object. If only the ID is known, you should pass something like this: $feed->nid = X 
 * @param $param
 *  Depends on the $op value.
 */
function feedapi_invoke($op, &$feed, $param = NULL) {
  if (!is_object($feed)) {
    return FALSE;
  }
  if (!isset($feed->processors)) {
    $node = node_load($feed->nid);
    if (!isset($node->feed)) {
      return FALSE;
    }
    $feed = $node->feed;
  }
  _feedapi_sanitize_processors($feed);
  switch ($op) {
    case 'load':
      return _feedapi_invoke_load($feed, $param);
    case 'refresh':
      return _feedapi_invoke_refresh($feed, $param);
    case 'purge':
      return _feedapi_invoke_purge($feed, $param);
    default:

      // Other operations
      return _feedapi_invoke($op, $feed, $param);
  }
}

/**
 * Ask for confirmation before deleting all the items
 */
function feedapi_purge_confirm($node) {
  $output = confirm_form(array(
    'nid' => array(
      '#type' => hidden,
      '#value' => $node->nid,
    ),
  ), t('Delete all the feed items from !name', array(
    '!name' => $node->title,
  )), 'node/' . $node->nid, t("Are you sure you want to delete all the feed items from !name?", array(
    '!name' => $node->title,
  )), t('Yes'), t('No'), 'feedapi_purge_confirm');
  return $output;
}

/**
 * Submitted items purging form. Drop all the items.
 */
function feedapi_purge_confirm_submit($form_id, $form_values) {
  $feed->nid = $form_values['nid'];
  feedapi_invoke('purge', $feed, 'items_confirmed');
  return 'node/' . $form_values['nid'];
}

/**
 * OPML Feed import form, also allows setting defaults to be applied to each feed
 */
function feedapi_import_feeds_form() {
  $form['opml'] = array(
    '#type' => 'file',
    '#title' => t('OPML File'),
    '#size' => 50,
    '#description' => t('Upload an OPML file containing a list of newsfeeds to be imported.'),
  );
  $form['feed_type'] = array(
    '#type' => 'select',
    '#title' => t('Feed Type'),
    '#description' => t("The type of feed you would like to associate this import with."),
    '#options' => feedapi_get_types(),
    '#required' => TRUE,
  );
  $form['override_title'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use TITLE attribute of OPML entries as feed title'),
    '#description' => t('If checked feed title will be overriden with the information from OPML file'),
  );
  $form['override_body'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use TEXT attribute of OPML entries as feed description'),
    '#description' => t('If checked feed description will be overriden with the information from OPML file'),
  );
  $form['#attributes']['enctype'] = 'multipart/form-data';
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Submit',
  );
  if (module_exists('og')) {
    og_form_add_og_audience('feedapi_import_feeds_form', $form);
  }
  return $form;
}

/**
 * Handle the submission of the OPML import form
 */
function feedapi_import_feeds_form_submit($form_id, $form_values) {
  $file = file_check_upload('opml');
  if ($file = file($file->filepath)) {
    $file = implode('', $file);
    if ($count = _feedapi_import_opml_process($file, $form_values)) {
      drupal_set_message(t('Successfuly imported %count feeds from OPML', array(
        '%count' => $count,
      )));
    }
    else {
      drupal_set_message(t('Feed list could not be imported. Please check that this is a valid OPML file.'), 'error');
    }
  }
  else {
    drupal_set_message(t('Data could not be retrieved, invalid or empty file.'), 'error');
  }
  return 'admin/content/feed';
}

/**
 * Delete expired items and return informations about the feed refreshing
 * 
 * @param $feed
 *  The feed object
 * @param $force
 *  Not in use anymore. Will be removed in future releases.
 * @param $settings
 *  Optional feed settings 
 * @return
 *  FALSE if the feed don't have to be refreshed. (forbidden if the $force is TRUE)
 */
function feedapi_expire($feed, $settings = NULL) {

  // Backwards compatibility, get settings if not passed
  $settings = is_null($settings) ? feedapi_get_settings(NULL, $feed->nid) : $settings;

  // Each processor can have its own expiration criteria ?
  $expired = _feedapi_invoke('expire', $feed, $settings);

  // Return the number of expired items
  return $expired ? array_sum($expired) : 0;
}

/**
 * Callback for expired items. Does the actual deleting
 */
function feedapi_expire_item($feed, $item) {
  foreach ($feed->processors as $processor) {
    module_invoke($processor, 'feedapi_item', 'delete', $item, $feed->nid);
  }
}

/**
 * Implementation of hook_form_alter().
 */
function feedapi_form_alter($form_id, &$form) {

  // Content type form.
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    if (isset($form['#post']['feedapi'])) {

      // TODO: Drupal automatically stores mutilated 'feedapi_'. $form['#node_type']->type - remove.
      $type = !empty($form['#node_type']->type) ? $form['#node_type']->type : $form['#post']['type'];
      _feedapi_store_settings(array(
        'node_type' => $type,
      ), $form['#post']['feedapi']);
    }
    $node_type_settings = feedapi_get_settings($form['#node_type']->type);
    $form['feedapi'] = array(
      '#type' => 'fieldset',
      '#title' => t('Feed API'),
      '#collapsible' => TRUE,
      '#collapsed' => isset($node_type_settings['enabled']) ? FALSE : TRUE,
      '#tree' => TRUE,
      '#validate' => array(
        'feedapi_content_type_validate' => array(),
      ),
    );
    $form['feedapi']['enabled'] = array(
      '#type' => 'checkbox',
      '#title' => t('Is a feed content type'),
      '#description' => t('Check if you want to use this content type for downloading feeds to your site.'),
      '#default_value' => isset($node_type_settings['enabled']) ? $node_type_settings['enabled'] : FALSE,
      '#weight' => -15,
    );
    $modules = module_implements('feedapi_settings_form');
    foreach ($modules as $module) {
      $form['feedapi']['defaults'] = array(
        '#type' => 'markup',
        '#value' => '<strong>' . t('Default settings') . '</strong><hr/>',
      );
      if ($feedapi_form = module_invoke($module, 'feedapi_settings_form', 'general')) {
        $form['feedapi'] = array_merge_recursive($form['feedapi'], $feedapi_form);
      }
    }
    $form['feedapi']['parsers'] = array(
      '#type' => 'fieldset',
      '#title' => t('Parser settings'),
      '#description' => t('Parsers turn a feed into an object ready for processing. Choose at least one.'),
      '#collapsible' => FALSE,
      '#tree' => TRUE,
    );
    $parsers = module_implements('feedapi_feed', TRUE);
    rsort($parsers);
    foreach ($parsers as $parser) {
      $form['feedapi']['parsers'][$parser] = array(
        '#type' => 'fieldset',
        '#title' => feedapi_get_natural_name($parser),
        '#collapsible' => TRUE,
        '#collapsed' => !($node_type_settings['parsers'][$parser]['enabled'] || count($parsers) == 1),
        '#tree' => TRUE,
        '#weight' => $node_type_settings['parsers'][$parser]['weight'],
      );
      $form['feedapi']['parsers'][$parser]['enabled'] = array(
        '#type' => 'checkbox',
        '#title' => t('Enable'),
        '#description' => t('Check this box if you want to enable the @name parser on this feed.', array(
          '@name' => t($parser),
        )),
        '#default_value' => $node_type_settings['parsers'][$parser]['enabled'] || count($parsers) == 1,
        '#weight' => -15,
      );
      $form['feedapi']['parsers'][$parser]['weight'] = array(
        '#type' => 'weight',
        '#delta' => 15,
        '#title' => t('Weight'),
        '#description' => t('Control the execution order. Parsers with lower weights are called before parsers with higher weights.'),
        '#default_value' => $node_type_settings['parsers'][$parser]['weight'],
        '#weight' => -14,
      );
      if ($parser_form = module_invoke($parser, 'feedapi_settings_form', 'parsers')) {
        $form['feedapi']['parsers'][$parser]['defaults'] = array(
          '#type' => 'markup',
          '#value' => '<strong>' . t('Default settings') . '</strong><hr/>',
        );
        $form['feedapi']['parsers'][$parser] = array_merge_recursive($form['feedapi']['parsers'][$parser], $parser_form);
      }
    }
    $form['feedapi']['processors'] = array(
      '#type' => 'fieldset',
      '#title' => t('Processor settings'),
      '#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.'),
      '#collapsible' => FALSE,
      '#tree' => TRUE,
    );
    $processors = module_implements('feedapi_item', TRUE);
    rsort($processors);
    foreach ($processors as $processor) {
      $form['feedapi']['processors'][$processor] = array(
        '#type' => 'fieldset',
        '#title' => feedapi_get_natural_name($processor),
        '#collapsible' => TRUE,
        '#collapsed' => !($node_type_settings['processors'][$processor]['enabled'] || count($processors) == 1),
        '#tree' => TRUE,
        '#weight' => $node_type_settings['processors'][$processor]['weight'],
      );
      $form['feedapi']['processors'][$processor]['enabled'] = array(
        '#type' => 'checkbox',
        '#title' => t('Enable'),
        '#description' => t('Check this box if you want to enable the @name processor on this feed.', array(
          '@name' => t($processor),
        )),
        '#default_value' => $node_type_settings['processors'][$processor]['enabled'],
        '#weight' => -15,
      );
      $form['feedapi']['processors'][$processor]['weight'] = array(
        '#type' => 'weight',
        '#delta' => 15,
        '#title' => t('Weight'),
        '#description' => t('Control the execution order. Processors with lower weights are called before processors with higher weights.'),
        '#default_value' => $node_type_settings['processors'][$processor]['weight'],
        '#weight' => -14,
      );
      if ($processor_form = module_invoke($processor, 'feedapi_settings_form', 'processors')) {
        $form['feedapi']['processors'][$processor]['defaults'] = array(
          '#type' => 'markup',
          '#value' => '<strong>' . t('Default settings') . '</strong><hr/>',
        );
        $form['feedapi']['processors'][$processor] = array_merge_recursive($form['feedapi']['processors'][$processor], $processor_form);
      }
    }

    // Populate form with node type settings if available.
    if ($node_type_settings) {
      $form['feedapi'] = _feedapi_populate($form['feedapi'], $node_type_settings);
    }

    // We add again to ensure the correct order. Without it, the submit and delete buttons are in a wrong place (not last)
    $submit = $form['submit'];
    $delete = $form['delete'];
    unset($form['submit'], $form['delete']);
    $form['submit'] = $submit;
    $form['delete'] = $delete;
  }

  // FeedAPI-enabled node form.
  if (isset($form['type']) && $form['type']['#value'] . '_node_form' == $form_id && feedapi_enabled_type($form['type']['#value'])) {
    $form['title']['#required'] = FALSE;
    $form['title']['#description'] = t('This field will be populated with the feed title. You can override by filling in this field.');
    $form['body_filter']['body']['#description'] = t('This field will be populated with the feed description. You can override by filling in this field.');
    $form['feedapi'] = array(
      '#type' => 'fieldset',
      '#title' => t('Feed'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#tree' => TRUE,
    );
    if ($_GET['feedapi_url']) {
      $feedapi_url_default = $_GET['feedapi_url'];
    }
    else {
      if ($form['#node']->feed->url) {
        $feedapi_url_default = $form['#node']->feed->url;
      }
      else {
        if ($form['#post']['feedapi']['feedapi_url']) {
          $feedapi_url_default = $form['#post']['feedapi']['feedapi_url'];
        }
      }
    }
    $form['feedapi']['feedapi_url'] = array(
      '#type' => 'textfield',
      '#title' => t('Feed URL'),
      '#description' => t('Enter feed URL.'),
      '#default_value' => $feedapi_url_default,
      '#maxlength' => 2048,
    );

    // Build feed object on validate and submit.
    if ($form['#post']['op']) {
      $feed = _feedapi_build_feed_object($form['type']['#value'], $form['#post']['feedapi']['feedapi_url']);

      // Stick feed object into feedapi form snippet - store it in submit.
      $form['feedapi_feed_object'] = array(
        '#type' => 'value',
        '#value' => $feed,
      );
      if (!$form['#post']['title']) {
        $form['title']['#value'] = $feed->title;
      }
      if (!$form['#post']['body']) {
        $form['body_filter']['body']['#value'] = $feed->description;
      }
    }

    // Show per-node-type feedapi, parser options only for users with permissions.
    if (user_access('advanced feedapi options')) {
      $modules = module_implements('feedapi_settings_form');
      foreach ($modules as $module) {
        if ($feedapi_form = module_invoke($module, 'feedapi_settings_form', 'general')) {
          $form['feedapi'] = array_merge_recursive($form['feedapi'], $feedapi_form);
        }
      }

      // Get settings for corresponding content type
      // Which parsers / processors are enabled is a per content-type setting.
      $node_type_settings = feedapi_get_settings($form['type']['#value']);

      // retrieve forms.
      foreach (array(
        "parsers" => "feedapi_feed",
        "processors" => "feedapi_item",
      ) as $type => $requirement) {
        $suitable_handlers = module_implements($requirement, TRUE);
        foreach ($suitable_handlers as $module) {
          if ($node_type_settings[$type][$module]['enabled']) {
            $result = array();
            $result = module_invoke($module, 'feedapi_settings_form', $type);
            if (is_array($result)) {
              $result['#weight'] = $node_type_settings[$type][$module]['weight'];
              $form['feedapi'][$type][$module] = $result;
              $form['feedapi'][$type][$module]['#type'] = 'fieldset';
              $form['feedapi'][$type][$module]['#title'] = feedapi_get_natural_name($module);
              $form['feedapi'][$type][$module]['#collapsible'] = TRUE;
              $form['feedapi'][$type][$module]['#collapsed'] = FALSE;
              $form['feedapi'][$type][$module]['#tree'] = TRUE;
            }
          }
        }
        if (isset($form['feedapi'][$type])) {
          $form['feedapi'][$type]['#type'] = 'fieldset';
          $form['feedapi'][$type]['#title'] = t(ucfirst($type));
          $form['feedapi'][$type]['#collapsible'] = TRUE;
          $form['feedapi'][$type]['#collapsed'] = TRUE;
          $form['feedapi'][$type]['#tree'] = TRUE;
        }
      }
    }

    // If we are on a node form, get per node settings and populate form.
    if (!($settings = feedapi_get_settings($form['type']['#value'], $form['#node']->nid))) {
      $settings = $node_type_settings;
    }
    $form['feedapi'] = _feedapi_populate($form['feedapi'], $settings);
  }
}

/**
 * Implementation of hook_feedapi_settings_form().
 */
function feedapi_feedapi_settings_form($type) {
  if ($type == 'general') {
    $form['refresh_on_create'] = array(
      '#type' => 'checkbox',
      '#title' => t('Refresh feed on creation'),
      '#description' => t('If checked, feed items will be processed immediately after a feed is created.'),
      '#default_value' => 0,
    );
    $form['update_existing'] = array(
      '#type' => 'checkbox',
      '#title' => t('Update existing feed items'),
      '#description' => t('If checked, existing feed items will be updated when feed is refreshed.'),
      '#default_value' => 1,
    );
    $form['skip'] = array(
      '#type' => 'checkbox',
      '#title' => t('Pause automatic feed update'),
      '#description' => t('If checked, feed will not be updated automatically on cron.'),
      '#default_value' => 0,
    );
    $period = drupal_map_assoc(array(
      3600,
      10800,
      21600,
      32400,
      43200,
      86400,
      172800,
      259200,
      604800,
      1209600,
      2419200,
      3628800,
      4838400,
      7257600,
      15724800,
      31536000,
    ), 'format_interval');
    $period[FEEDAPI_NEVER_DELETE_OLD] = t('Never');
    $form['items_delete'] = array(
      '#type' => 'select',
      '#title' => t('Delete news items older than'),
      '#options' => $period,
      '#default_value' => FEEDAPI_NEVER_DELETE_OLD,
    );
  }
  return $form;
}

/**
 * Implementation of hook_cron().
 */
function feedapi_cron() {
  db_query('DELETE FROM {feedapi_stat} WHERE timestamp < %d', variable_get('cron_semaphore', FALSE) - FEEDAPI_CRON_STAT_LIFETIME);

  // Initialize counters
  $count = array(
    '%feeds' => 0,
    '%expired' => 0,
    '%new' => 0,
    '%updated' => 0,
  );

  // We get feeds in small lots, this will save memory and have the process adjusting to the
  // time limit even when we have many thousands of them.
  $start = time() - FEEDAPI_CRON_MIN_REFRESH_TIME;
  $process = 0;

  // The counter process will be > 0 if we've selected less feeds
  while (!$process && feedapi_cron_time()) {
    $process = FEEDAPI_CRON_FEEDS;
    $result = db_query_range("SELECT nid FROM {feedapi} WHERE checked <= %d AND skip = 0 ORDER BY checked", $start, 0, FEEDAPI_CRON_FEEDS);
    while (feedapi_cron_time() && ($feed = db_fetch_object($result))) {

      // Call the refresh process for each feed and store counters
      $counter = feedapi_invoke('refresh', $feed, TRUE);
      if ($counter) {
        foreach ($counter as $name => $value) {
          $count['%' . $name] += $value;
        }
      }
      $count['%feeds']++;
      $process--;
    }
  }
}

/**
 * Check for time limits in cron processing.
 * 
 * @return
 *   Number of seconds left, zero if none.
 */
function feedapi_cron_time() {
  static $time_limit;
  if (!$time_limit) {
    $time_limit = time() + variable_get('feedapi_cron_percentage', 15) / 100 * ini_get('max_execution_time');

    // However, check for left time, maybe some other cron processing already occured
    $time_limit = min($time_limit, variable_get('cron_semaphore', 0) + ini_get('max_execution_time'));
    timer_start('feedapi_cron');
  }
  return max($time_limit - time(), 0);
}

/**
 * Provide a UI for overviewing the existing feeds
 */
function feedapi_admin_overview() {
  $header = array(
    t('Title'),
    t('Last refresh'),
    t('New items added per update'),
    t('Update rate'),
    t('Number of items'),
    t('Processing time'),
    t('Commands'),
  );
  $rows = array();
  $result = pager_query("SELECT nid from {feedapi} ORDER BY checked DESC", 50, 0, "SELECT count(*) FROM {feedapi}");
  while ($nid = db_fetch_array($result)) {
    $node = node_load($nid['nid']);
    $commands = array(
      l(t('Delete'), 'node/' . $node->nid . '/delete', NULL, 'destination=admin/content/feed'),
      l(t('Remove items'), 'node/' . $node->nid . '/purge', NULL, 'destination=admin/content/feed'),
      l(t('Refresh'), 'node/' . $node->nid . '/refresh', NULL, 'destination=admin/content/feed'),
      l(t('Edit'), 'node/' . $node->nid . '/edit', NULL, 'destination=admin/content/feed'),
    );

    // Fetch statistics for this feed
    foreach (array(
      'download_num',
      'new',
      'process_time',
      'update_times',
    ) as $type) {
      $node->feed->statistics[$type] = _feedapi_get_stat($node->nid, $type, TRUE);
    }
    if (count($node->feed->statistics['download_num']) != 0 && count($node->feed->statistics['new']) != 0 && count($node->feed->statistics['process_time']) != 0) {
      $update_rate = _feedapi_update_rate($node->feed->statistics['update_times']);
      $rows[] = array(
        l($node->title, "node/{$node->nid}"),
        $node->feed->checked == 0 ? t('Never') : t('%time ago', array(
          '%time' => format_interval(time() - $node->feed->checked),
        )),
        round(array_sum($node->feed->statistics['new']) / count($node->feed->statistics['new']), 2),
        is_numeric($update_rate) ? format_interval($update_rate) : $update_rate,
        round(array_sum($node->feed->statistics['download_num']) / count($node->feed->statistics['download_num']), 2),
        round(array_sum($node->feed->statistics['process_time']) / count($node->feed->statistics['process_time']), 2) . ' ' . t('ms'),
        theme('item_list', $commands),
      );
    }
    else {
      $rows[] = array(
        l($node->title, "node/{$node->nid}"),
        $node->feed->checked == 0 ? t('Never') : t('%time ago', array(
          '%time' => format_interval(time() - $node->feed->checked),
        )),
        '',
        '',
        t('No enough data for statistics'),
        '',
        theme('item_list', $commands),
      );
    }
  }
  $output = format_plural(round(FEEDAPI_CRON_STAT_LIFETIME / (24 * 3600)), "Average over the last day.", "Averages over the last @count days.");
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', 0, 50);
  return $output;
}

/**
 * This is shown instead of normal node form when the simplified form is chosen at the settings
 */
function feedapi_simplified_form($type) {
  $form['node']['#tree'] = TRUE;
  $form['node']['type'] = array(
    '#type' => 'hidden',
    '#value' => $type,
  );
  $form['url'] = array(
    '#title' => t('Feed URL'),
    '#type' => 'textfield',
    '#size' => 25,
    '#required' => TRUE,
    '#maxlength' => 2048,
  );
  $form['add'] = array(
    '#type' => 'submit',
    '#value' => t('Add'),
  );
  return $form;
}

/**
 * Create the node object and save
 */
function feedapi_simplified_form_submit($form_id, $form_values) {
  $node_template = (object) $form_values['node'];
  $feed_type = (string) $_POST['node']['type'];
  $valid_types = array_keys(feedapi_get_types());
  foreach ($valid_types as $type) {
    if ($type === $feed_type) {
      $node_template->type = $type;
    }
  }
  if ($node = feedapi_create_node($node_template, $form_values['url'])) {
    drupal_set_message(t('Feed successfully created.'));
    return 'node/' . $node->nid;
  }
  else {
    drupal_set_message(t('Could not retrieve title from feed.'), 'error');
    return array(
      'node/add/' . $node_template->type,
      'feedapi_url=' . urlencode($form_values['url']),
    );
  }
}

/**
 * Get the module-defined natural name of FeedAPI parser or processor
 * Define this name in hook_help():
 * 
 *  function hook_help($section) {
 *    switch ($section) {
 *      case 'feedapi/full_name':
 *        return t('Natural name');
 *        break;
 *      }
 *    }
 */
function feedapi_get_natural_name($module) {
  $help = $module . '_help';
  $module_natural = function_exists($help) ? $help('feedapi/full_name') : t($module);
  return empty($module_natural) ? t($module) : $module_natural;
}

/**
 * Settings: allowed HTML tags, number of feeds refreshed in one round
 */
function feedapi_admin_settings() {
  $form['feedapi_allowed_html_tags'] = array(
    '#type' => 'textfield',
    '#title' => t('Allowed HTML tags'),
    '#size' => 80,
    '#maxlength' => 255,
    '#default_value' => variable_get('feedapi_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'),
    '#description' => t('The list of tags which are allowed in feeds, i.e., which will not be removed by Drupal.'),
  );
  $form['feedapi_allow_html_all'] = array(
    '#type' => 'checkbox',
    '#title' => t('Allow all HTML tags'),
    '#default_value' => variable_get('feedapi_allow_html_all', FALSE),
    '#description' => t('In this case the module does\'t filter any HTML elements from the incoming fields. This checkbox overrides the above list of allowed tags.'),
  );
  if (variable_get('feedapi_allow_html_all', FALSE)) {
    $form['feedapi_allowed_html_tags']['#disabled'] = TRUE;
  }

  // Drupal will try to overwrite this value at cron time
  $max_exec = !ini_get('safe_mode') ? 240 : ini_get('max_execution_time');
  $form['feedapi_cron_percentage'] = array(
    '#type' => 'select',
    '#title' => t('Cron time for FeedAPI [%]'),
    '#options' => drupal_map_assoc(array(
      15,
      25,
      50,
      75,
    )),
    '#default_value' => variable_get('feedapi_cron_percentage', 15),
    '#description' => t('Percentage of maximal PHP execution time (currently !exec seconds).
                         At current settings, the FeedAPI cron process can run for up to !now seconds.', array(
      "!exec" => $max_exec,
      "!now" => variable_get('feedapi_cron_percentage', 15) / 100 * $max_exec,
    )),
  );
  return system_settings_form($form);
}

/**
 * Create a feedapi node programatically.
 * 
 * @param $param
 *  Either a feedapi - enabled node type or a $node object with at least valid $node->type.
 * @param $url
 *  URI of feed.
 */
function feedapi_create_node($param, $url) {
  if (is_object($param)) {
    $node = $param;
  }
  else {
    $node = new stdClass();
    $node->type = $param;
  }
  if (!feedapi_enabled_type($node->type)) {
    return FALSE;
  }
  $feed = _feedapi_build_feed_object($node->type, $url);
  if (!$feed->title && !$node->title) {
    return FALSE;
  }
  $node->title = $node->title ? $node->title : $feed->title;
  $node->body = $node->body ? $node->body : $feed->description;
  $node->feed = $feed;
  $node_options = variable_get('node_options_' . $node->type, array(
    'status',
    'promote',
  ));

  // If this is a new node, fill in the default values.
  foreach (array(
    'status',
    'promote',
    'sticky',
  ) as $key) {
    $node->{$key} = in_array($key, $node_options);
  }

  // Get the content-type settings as default
  $node->feedapi = feedapi_get_settings($node->type);
  node_object_prepare($node);
  global $user;
  $node->uid = $user->uid;
  node_save($node);
  return $node;
}

/**
 * Load node by URL.
 * @param $args
 *  Currently only supported $args['url] - URL string.
 * @return 
 *  Node object if successful, FALSE if not. 
 */
function feedapi_load_node($args) {
  if ($nid = db_result(db_query("SELECT nid FROM {feedapi} WHERE url = '%s'", $args['url']))) {
    return node_load($nid);
  }
  return FALSE;
}

/**
 * Refresh a feed node (= run enabled processors on it).
 * @param $node
 *  A node object with a $node->feed object.
 * @param $destination_path
 *  If a destination path is given, function redirects to this destination.
 */
function feedapi_refresh($node, $destination_path = NULL) {
  feedapi_invoke('refresh', $node->feed, FALSE);
  if ($destination_path) {
    drupal_goto($destination_path);
  }
}

/**
 * Insert feedapi data to the DB when it's a new for for FeedAPI
 */
function _feedapi_insert(&$node, $teaser, $page) {
  if (isset($node->feed->url) && isset($node->feed->feed_type)) {
    db_query("INSERT INTO {feedapi} (\n            nid, url, link, feed_type, processors,\n            parsers, checked, settings) VALUES\n            (%d, '%s', '%s', '%s', '%s', '%s', %d, '%s')", $node->nid, $node->feed->url, $node->feed->options->link, $node->feed->feed_type, serialize($node->feed->processors), serialize($node->feed->parsers), 0, serialize(array()));

    // Store add on module's settings if user has permission to do so.
    if (user_access('advanced feedapi options')) {
      _feedapi_store_settings(array(
        'nid' => $node->nid,
      ), $node->feedapi);
    }

    // Refresh feed if the user would like to do that
    if ($node->feedapi['refresh_on_create'] == TRUE) {
      $node->feed->nid = $node->nid;
      $node->feed->settings = feedapi_get_settings(NULL, $node->nid);
      feedapi_invoke('refresh', $node->feed);
    }
  }
}

/**
 * Update feed data of an existing feed
 */
function _feedapi_update(&$node, $teaser, $page) {
  if (isset($node->feed)) {
    $old_config = node_load($node->nid);

    // In that case this feed has never have feed data. Should be created then, this is not really an update
    if (!is_numeric($old_config->feed->nid)) {
      $node->feed = _feedapi_build_feed_object($node->type, $node->feed->url);
      _feedapi_insert($node, 'insert', $teaser, $page);
      return;
    }
    db_query("UPDATE {feedapi} SET url = '%s', feed_type = '%s', processors = '%s', parsers = '%s', link = '%s' WHERE nid = %d", isset($node->feed->url) ? $node->feed->url : $node->feedapi['feedapi_url'], isset($node->feed->feed_type) ? $node->feed->feed_type : '', isset($node->feed->processors) ? serialize($node->feed->processors) : serialize($old_config->feed->processors), isset($node->feed->parsers) ? serialize($node->feed->parsers) : serialize($old_config->feed->parsers), $node->feed->options->link, $node->nid);

    // Store add on module's settings if user has permission to do so.
    if (user_access('advanced feedapi options')) {
      _feedapi_store_settings(array(
        'nid' => $node->nid,
      ), $node->feedapi);
    }
  }
}

/**
 * Execute the enabled parsers and create an unified output
 *
 * @param $feed
 *  Feed object
 * @param $parsers
 *  Structure: array(
 *    "primary" => "parser_primary",
 *    "secondary" => array("parser1", "parser2", "parserN")
 *  );
 * @return
 *  The object of the parser data
 */
function _feedapi_call_parsers($feed, $parsers) {
  $nid = $feed->nid;
  $parser_primary = array_shift($parsers);
  $parsers_secondary = $parsers;
  if (module_exists($parser_primary)) {
    $feed->feed_type = module_invoke($parser_primary, 'feedapi_feed', 'compatible', $feed);
    $parser_output = module_invoke($parser_primary, 'feedapi_feed', 'parse', $feed);
    if ($parser_output === FALSE) {
      return $parser_output;
    }
    $feed = (object) array_merge((array) $feed, (array) $parser_output);
  }

  // Call the turned on parsers, create a union of returned options
  $parsers_secondary = is_array($parsers_secondary) ? $parsers_secondary : array();
  foreach ($parsers_secondary as $parser) {
    $feed_ext = module_invoke($parser, 'feedapi_feed', 'parse', $feed, FALSE);
    $feed->options = (object) ((array) $feed->options + (array) $feed_ext->options);

    // Merge items' options
    if (is_array($feed_ext->items)) {
      foreach ($feed_ext->items as $key => $item) {
        $feed->items[$key]->options = (object) ((array) $feed->items[$key]->options + (array) $item->options);
      }
    }
  }
  $feed->nid = $nid;
  foreach (module_implements('feedapi_after_parse') as $module) {
    $func = $module . '_feedapi_after_parse';
    $func($feed);
  }

  // Filter bad or not allowed tags, sanitize data (currently timestamp checking)
  if (!variable_get('feedapi_allow_html_all', FALSE)) {
    $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);
    foreach (array(
      'title',
      'description',
    ) as $property) {
      if (isset($feed->{$property})) {
        if (is_string($feed->{$property})) {
          $feed->{$property} = filter_xss($feed->{$property}, $allowed);
        }
      }
    }
    for ($i = 0; $i < count($feed->items); $i++) {
      $feed->items[$i]->title = filter_xss($feed->items[$i]->title, $allowed);
      $feed->items[$i]->description = filter_xss($feed->items[$i]->description, $allowed);
      if ($feed->items[$i]->options->timestamp == 0) {
        $feed->items[$i]->options->timestamp = time();
      }
    }
  }
  return $feed;
}

/**
 * Stores settings per content type or per node.
 *
 * @param $args
 *  Associative array which is $args['nid'] = N or $args['node_type'] = "content_type". Depends on what to store for
 * @param $settings
 *  The settings data itself
 */
function _feedapi_store_settings($args, $settings) {
  if ($args['nid']) {
    db_query("UPDATE {feedapi} SET settings = '%s', skip = %d WHERE nid = %d", serialize($settings), $settings['skip'], $args['nid']);
    module_invoke_all('feedapi_after_settings', $args['nid'], $settings);

    // This ensures that next time, not the cached, but the updated value will be used.
    feedapi_get_settings(NULL, $args['nid'], TRUE);
  }
  elseif ($args['node_type']) {
    variable_set('feedapi_settings_' . $args['node_type'], $settings);
  }
}

/**
 * Determines wether feedapi is enabled for given node type.
 * If parser or processor is passed in, this function determines wether given
 * parser or processor is enabled for given node type.
 * @param $node_type
 *  A Drupal node type.
 * @param $parser_or_processor
 *  A parser or processor - pass in by module name.
 * @return TRUE if enabled, FALSE if not.
 */
function feedapi_enabled_type($node_type, $parser_or_processor = '') {
  $settings = feedapi_get_settings($node_type);
  if (empty($parser_or_processor)) {
    if (isset($settings['enabled'])) {
      return $settings['enabled'] ? TRUE : FALSE;
    }
    else {
      return FALSE;
    }
  }
  foreach (array(
    'parsers',
    'processors',
  ) as $stage) {
    if (isset($settings[$stage][$parser_or_processor]['enabled'])) {
      if ($settings[$stage][$parser_or_processor]['enabled'] == TRUE) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/**
 * Helper function for feedapi_invoke().
 * 
 * Generic operations, collects results and returns array
 */
function _feedapi_invoke($op, &$feed, $param) {
  $output = array();
  foreach ($feed->processors as $processor) {
    $result = module_invoke($processor, 'feedapi_item', $op, $feed, $param);

    // Result may be a list of items or single values (count)
    if ($result) {
      if (is_array($result)) {
        $output = array_merge($output, $result);
      }
      else {
        $output[] = $result;
      }
    }
  }
  return $output;
}

/**
 * Helper function for feedapi_invoke().
 * Load a list of feed items into the feed object from the processors.
 */
function _feedapi_invoke_load(&$feed, $param) {
  $feed->items = array();
  foreach ($feed->processors as $processor) {
    $items = module_invoke($processor, 'feedapi_item', 'fetch', $feed);
    if (is_array($items)) {
      foreach ($items as $item) {
        $feed->items[] = $item;
      }
    }
  }
}

/**
 * Helper function for feedapi_invoke().
 * Refresh the feed, call the proper parsers and processors' hooks.
 * Don't call this function directly, use feedapi_refresh() instead.
 * 
 * @ TODO Fix: This may loop forever when a feed has no processors
 */
function _feedapi_invoke_refresh(&$feed, $param) {
  $timestamp = variable_get('cron_semaphore', FALSE) !== FALSE ? variable_get('cron_semaphore', FALSE) : time();
  $counter = array();
  timer_start('feedapi_' . $feed->nid);
  $cron = $param;

  // Step 0: Check processors and parsers and grab settings
  if (!is_array($feed->processors) || count($feed->processors) == 0) {
    if (!$cron) {
      drupal_set_message(t("No processors specified for URL %url. Could not refresh.", array(
        '%url' => $feed->url,
      )), "error");
      drupal_goto('node/' . $feed->nid);
    }
    return 0;
  }
  else {
    if (!is_array($feed->parsers) || count($feed->parsers) == 0) {
      drupal_set_message(t("No parsers specified for URL %url. Could not refresh.", array(
        '%url' => $feed->url,
      )), "error");
      drupal_goto('node/' . $feed->nid);
    }
  }
  $settings = feedapi_get_settings(NULL, $feed->nid);

  // Step 1: Force processors to delete old items and determine the max. create elements.
  $counter['expired'] = feedapi_expire($feed);

  // Step 2: Get feed.
  $nid = $feed->nid;
  $hash_old = $feed->hash;
  $feed = _feedapi_call_parsers($feed, $feed->parsers, $feed->half_done);
  if (is_object($feed)) {
    $feed->hash = md5(serialize($feed->items));
  }

  // Step 3: See, whether feed has been modified.
  if ($feed === FALSE || $hash_old == $feed->hash) {

    // Updated the checked field in any case.
    db_query("UPDATE {feedapi} SET checked = %d, half_done = %d WHERE nid = %d", time(), FALSE, $nid);
    if (!$cron) {
      if ($hash_old == $feed->hash && is_object($feed)) {
        drupal_set_message(t('There are no new items in the feed.'), 'status');
      }
      else {
        drupal_set_message(t('Could not refresh feed.'), 'error');
      }
    }
    return $counter;
  }

  // Step 4: Walk through the items and check duplicates, then save or update
  $items = $feed->items;
  $updated = 0;
  $new = 0;
  $half_done = FALSE;

  // We check for time-out after each item
  foreach ($items as $index => $item) {

    // Call each item parser
    $item->is_updated = FALSE;
    $item->is_new = FALSE;
    foreach ($feed->processors as $processor) {
      if (!module_invoke($processor, 'feedapi_item', 'unique', $item, $feed->nid, $settings['processors'][$processor])) {
        if ($settings['update_existing'] == TRUE) {
          module_invoke($processor, 'feedapi_item', 'update', $item, $feed->nid, $settings['processors'][$processor]);
          $item->is_updated = TRUE;
        }
      }
      else {

        // We have checked before for expired items, so just save it.
        // if the item is already expired then do nothing
        $items_delete = $settings['items_delete'];
        $diff = abs(time() - (isset($item->options->timestamp) ? $item->options->timestamp : time()));
        if ($diff > $items_delete && $items_delete > FEEDAPI_NEVER_DELETE_OLD) {
          break;
        }
        $result = module_invoke($processor, 'feedapi_item', 'save', $item, $feed->nid, $settings['processors'][$processor]);
        if ($result !== FALSE) {
          $item->is_new = TRUE;
        }
      }
    }
    $new = $item->is_new ? $new + 1 : $new;
    $updated = $item->is_updated && !$item->is_new ? $updated + 1 : $updated;

    // Decision on time. If the exec time is greather than the user-set percentage of php max execution time
    if ($cron && !feedapi_cron_time()) {
      $half_done = $new + $updated == count($items) ? FALSE : TRUE;
      break;
    }

    // Save the item status for further processing
    $feed->items[$index] = $item;
  }

  // Closing step: Call after refresh and update feed statistics
  foreach (module_implements('feedapi_after_refresh') as $module) {
    $func = $module . '_feedapi_after_refresh';
    $func($feed);
  }
  if ($new > 0) {
    _feedapi_store_stat($nid, 'update_times', time(), $timestamp);
    _feedapi_store_stat($nid, 'new', $new, $timestamp);
    _feedapi_store_stat($nid, 'download_num', count($items), $timestamp);
    _feedapi_store_stat($nid, 'process_time', timer_read('feedapi_' . $feed->nid), $timestamp);
  }
  db_query("UPDATE {feedapi} SET checked = %d, half_done = %d, hash = '%s' WHERE nid = %d", time(), $half_done, $feed->hash, $feed->nid);
  if (!$cron) {
    if ($new == 0 && $updated == 0) {
      drupal_set_message(t('There are no new items in the feed.'), 'status');
    }
    else {
      drupal_set_message(t("%new new item(s) were saved. %updated existing item(s) were updated.", array(
        "%new" => $new,
        "%updated" => $updated,
      )));
    }

    // @ TODO what value to return here?
  }
  else {

    // Update and return counter
    $counter['new'] = $new;
    $counter['updated'] = $updated;
    return $counter;
  }
}

/**
 * Helper function for feedapi_invoke().
 * Delete all feed items of a feed.
 */
function _feedapi_invoke_purge(&$feed, $param) {
  $node = node_load($feed->nid);

  // Reset hash.
  db_query("UPDATE {feedapi} SET hash = 0 WHERE nid = %d", $feed->nid);
  if ($param == 'items') {
    return drupal_get_form('feedapi_purge_confirm', $node);
  }
  feedapi_invoke('load', $feed);

  // Delete items from the processors
  foreach ($feed->items as $item) {
    foreach ($feed->processors as $processor) {

      // FIXME: it's possible now to accidentally delete an item from another processor
      module_invoke($processor, 'feedapi_item', 'delete', $item, $feed->settings['processors'][$processor]);
    }
  }
  if ($param == 'items_confirmed') {
    drupal_set_message(t('!count feed items have been deleted successfully from the feed.', array(
      '!count' => count($feed->items),
    )));
  }
}

/**
 * Builds feed object ready to be sticked onto node.
 */
function _feedapi_build_feed_object($node_type, $url) {
  $feed = new stdClass();
  $feed->url = $url;
  $node_type_settings = feedapi_get_settings($node_type);
  $feed->processors = _feedapi_format_settings($node_type_settings, 'processors');
  $feed->parsers = _feedapi_format_settings($node_type_settings, 'parsers');
  if ($feed->url) {
    $feed = _feedapi_call_parsers($feed, $feed->parsers);
  }
  $feed->link = $feed->options->link;
  return $feed;
}

/**
 * Returns per content type settings ordered by weight
 * and only those that are turned on.
 * @param $node_type_settings
 *  Content type settings retrieved with feedapi_get_settings().
 * @param $stage_type
 *  'parsers' or 'processors'
 */
function _feedapi_format_settings($node_type_settings, $stage_type) {
  $result = array();
  $settings = $node_type_settings[$stage_type];
  if (is_array($settings)) {
    foreach ($settings as $name => $properties) {
      if (isset($properties['enabled'])) {
        if ($properties['enabled'] == TRUE) {
          $result[$settings[$name]['weight']] = $name;
        }
      }
    }
    ksort($result);
  }
  return $result;
}

/**
 * Retrieve settings per content type or per node.
 * 
 * @param $node_type
 *  Content type name or NULL if per node
 * @param $nid
 *  Node nid or NULL if per content type
 * @param $reset
 *  If TRUE, the data is returned from the database.
 * @return
 *  Associative array with feedapi settings
 * @todo: Use node type settings for pulling on/off and weight of 
 *       parsers/processors, use per node settings to override their 
 *       configuration, this allows us a more predictable 
 *       presets/settings behaviour. See d. o. #191692
 * Watch out: cache permutations of node_type or node_type+nid or nid.
 * Watch out: changes within page load likely.
 */
function feedapi_get_settings($node_type, $nid = FALSE, $reset = FALSE) {
  static $node_settings;
  if (is_numeric($nid)) {
    if (!isset($node_settings[$nid]) || $reset) {
      if ($settings = db_fetch_object(db_query('SELECT settings FROM {feedapi} WHERE nid = %d', $nid))) {
        $settings = unserialize($settings->settings);
      }
      $node_settings[$nid] = is_array($settings) && !empty($settings) ? $settings : FALSE;
    }
    if (!is_array($node_settings[$nid])) {
      if (empty($node_type)) {

        // In normal case, this shouldn't happen. This is an emergency branch
        $node_type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $nid));
      }
    }
    else {
      return $node_settings[$nid];
    }
  }

  // Fallback: node_type.
  if (isset($node_type) && is_string($node_type)) {
    if ($settings = variable_get('feedapi_settings_' . $node_type, FALSE)) {
      return $settings;
    }
  }
  return FALSE;
}

/**
 * Set default value of $form elements if present in $settings.
 */
function _feedapi_populate($form, $settings) {
  foreach ($form as $k => $v) {
    if (is_array($v)) {
      if (array_key_exists('#default_value', $v)) {

        // Don't prepopulate feedapi_url slot, not stored in settings
        // Might be overwritten otherwise by users without advanced feedapi options permissions.
        // Todo: stick all settings form elements that are not in 'parsers' or 'processors' in 'general' -
        // This is kind of tricky though without breaking sites out there.
        if ($k != 'feedapi_url') {
          if (isset($form[$k]['#parents']) && is_array($form[$k]['#parents'])) {

            // respect #parents if set
            $form[$k]['#default_value'] = _feedapi_populate_get_setting($form[$k]['#parents'], $settings);
          }
          elseif (isset($settings[$k])) {
            $form[$k]['#default_value'] = $settings[$k];
          }
        }
      }
      elseif (isset($settings[$k])) {
        $form[$k] = _feedapi_populate($form[$k], $settings[$k]);
      }
    }
  }
  return $form;
}

/**
 * Gets the setting for '#parent'
 * (there must be a more efficent way)
 */
function _feedapi_populate_get_setting($parents, $settings) {
  if (is_array($parents) && count($parents)) {
    $this_parent = array_shift($parents);
    return _feedapi_populate_get_setting($parents, $settings[$this_parent]);
  }
  else {
    return $settings[$parents];
  }
}

/**
 * Calculate the average between-update time
 */
function _feedapi_update_rate($update_times) {
  for ($i = 0; $i < count($update_times) - 1; $i++) {
    $between[] = abs($update_times[$i] - $update_times[$i + 1]);
  }
  return count($between) > 0 ? round(array_sum($between) / count($between), 2) : t('No data yet');
}

/**
 * Remove non-existing processors from the processors arrays
 */
function _feedapi_sanitize_processors(&$feed) {
  if (is_array($feed->processors)) {
    foreach ($feed->processors as $key => $processor) {
      if (!module_exists($processor)) {
        unset($feed->processors[$key]);
      }
    }
  }
}

/**
 * Generates an OPML representation of all feeds.
 */
function feedapi_export_opml() {
  $result = db_query(db_rewrite_sql('SELECT n.title, f.url FROM {feedapi} f INNER JOIN {node} n ON f.nid = n.nid ORDER BY n.title ASC'));
  $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  $output .= "<opml version=\"1.1\">\n";
  $output .= "<head>\n";
  $output .= '<title>' . variable_get('site_name', 'drupal') . ' - ' . variable_get('site_slogan', '') . "</title>\n";
  $output .= '<dateModified>' . gmdate('r') . "</dateModified>\n";
  $output .= "</head>\n";
  $output .= "<body>\n";
  while ($feed = db_fetch_object($result)) {
    $output .= '<outline text="' . check_plain($feed->title) . '" xmlUrl="' . check_plain($feed->url) . '" />' . "\n";
  }
  $output .= "</body>\n";
  $output .= "</opml>\n";
  drupal_set_header('Content-Type: text/xml; charset=utf-8');
  drupal_set_header('Content-Disposition: attachment; filename="' . variable_get('site_name', 'drupal') . '.opml"');
  print $output;
  exit(0);
}

/**
* Imports from OPML XML file
*/
function _feedapi_import_opml_process($opml, $args = array()) {
  $feeds = array();
  $count = 0;
  $parser = drupal_xml_parser_create($opml);

  //Some OPML Files don't have the xml tag, which causes parsing to fail. Hence, using the appended version as a fallback parse
  if (xml_parse_into_struct($parser, $opml, $vals, $index) || xml_parse_into_struct($parser, '<?xml version="1.0"?>' . $opml, $vals, $index)) {
    foreach ($vals as $entry) {
      if ($entry['tag'] == 'OUTLINE') {
        $feeds[] = $entry['attributes'];
      }
    }
    foreach ($feeds as $feed) {
      if (strlen($feed['XMLURL']) > 1) {

        // check if feed url is already in the list
        $dupe = db_result(db_query("SELECT nid FROM {feedapi} WHERE url = '%s'", $feed['XMLURL']));

        // If the feed is not already in the list, add it
        if ($dupe === FALSE) {

          // Generate a feed structure
          $node = new stdClass();
          $node->type = $args['feed_type'];
          if ($args['override_title']) {
            $node->title = $feed['TITLE'];
          }
          if ($args['override_body']) {
            $node->body = $feed['TEXT'];
          }
          $node->og_groups = $args['og_groups'];
          $node->og_public = (int) $args['og_public'];
          $node = feedapi_create_node($node, $feed['XMLURL']);
          $count++;
        }
      }
    }
  }
  return $count;
}

/**
 * Store statistics information
 *
 * @param $id
 *  A numerical id
 * @param $type
 *  A string which describes what we want to store. This is an identifier, think of as a variable name
 * @param $val
 *  This is the variable value
 * @param $timestamp
 *  Timestamp for the value
 * @param $time
 *  Optional, a string equivalent to the $timestamp
 * @param $update
 *  Boolean, TRUE if you'd like to modify an existing entry in the stat table
 */
function _feedapi_store_stat($id, $type, $val, $timestamp, $time = NULL, $update = FALSE) {
  if (!$time) {
    $time = date("Y-m-d H:i", $timestamp);
  }
  if ($update) {
    db_query("UPDATE {feedapi_stat} SET value = %d, timestamp = %d WHERE time = '%s' AND type = '%s' AND id = %d", $val, $timestamp, $time, $type, $id);
  }
  if (!$update || !db_affected_rows()) {
    db_query("INSERT INTO {feedapi_stat} (id, value, time, timestamp, type) VALUES (%d, %d, '%s', %d, '%s')", $id, $val, $time, $timestamp, $type);
  }
}

/**
 * Return the type-specific statistics data
 *
 * @param $id
 *  A numerical id
 * @param $type
 *  Name of the type (variable)
 * @name $only_val
 *  If TRUE, only the values are returned, no more 
 * @return
 *  $only_val = FALSE -> array("timestamp" => array(), "time" => array(), "value" => array());
 */
function _feedapi_get_stat($id, $type, $only_val = FALSE) {
  $result = db_query("SELECT timestamp, time, value FROM {feedapi_stat} WHERE type = '%s' AND id = %d", $type, $id);
  while ($row = db_fetch_array($result)) {
    if ($only_val) {
      $stat[] = $row['value'];
    }
    else {
      foreach (array(
        'timestamp',
        'time',
        'value',
      ) as $member) {
        $stat[$member][] = $row[$member];
      }
    }
  }
  return $stat;
}

/**
 * Return a list of FeedAPI-enabled content-types list, ready-to-use for #options at FormsAPI
 */
function feedapi_get_types() {
  $names = node_get_types('names');
  foreach ($names as $type => $name) {
    if (!feedapi_enabled_type($type)) {
      unset($names[$type]);
    }
  }
  return $names;
}

/**
 * Prevent users to use the same weight for two or more parsers and processors
 * because FeedAPI cannot handle this. And this is not neccessary too.
 */
function feedapi_content_type_validate($form) {
  $parsers = module_implements('feedapi_feed', TRUE);
  rsort($parsers);
  $processors = module_implements('feedapi_item', TRUE);
  rsort($processors);
  foreach (array(
    'processors',
    'parsers',
  ) as $type) {
    $proc_weight = array();
    foreach (${$type} as $stuff) {
      if (isset($form[$type][$stuff]) && $form[$type][$stuff]['enabled']['#value'] == TRUE) {
        if (++$proc_weight[$form[$type][$stuff]['weight']['#value']] > 1) {
          form_error($form, t('Two enabled processors or parsers cannot have the same weight.'), 'error');
        }
      }
    }
  }
}

Functions

Namesort descending Description
feedapi_admin_overview Provide a UI for overviewing the existing feeds
feedapi_admin_settings Settings: allowed HTML tags, number of feeds refreshed in one round
feedapi_block Implementation of hook_block().
feedapi_content_type_validate Prevent users to use the same weight for two or more parsers and processors because FeedAPI cannot handle this. And this is not neccessary too.
feedapi_create_node Create a feedapi node programatically.
feedapi_cron Implementation of hook_cron().
feedapi_cron_time Check for time limits in cron processing.
feedapi_enabled_type Determines wether feedapi is enabled for given node type. If parser or processor is passed in, this function determines wether given parser or processor is enabled for given node type.
feedapi_expire Delete expired items and return informations about the feed refreshing
feedapi_expire_item Callback for expired items. Does the actual deleting
feedapi_export_opml Generates an OPML representation of all feeds.
feedapi_feedapi_settings_form Implementation of hook_feedapi_settings_form().
feedapi_form_alter Implementation of hook_form_alter().
feedapi_get_natural_name Get the module-defined natural name of FeedAPI parser or processor Define this name in hook_help():
feedapi_get_settings Retrieve settings per content type or per node.
feedapi_get_types Return a list of FeedAPI-enabled content-types list, ready-to-use for #options at FormsAPI
feedapi_help Implementation of hook_help().
feedapi_import_feeds_form OPML Feed import form, also allows setting defaults to be applied to each feed
feedapi_import_feeds_form_submit Handle the submission of the OPML import form
feedapi_invoke Invoke feedapi API callback functions.
feedapi_link Implementation of hook_link().
feedapi_load_node Load node by URL.
feedapi_menu Implementation of hook_menu().
feedapi_nodeapi Implementation of hook_nodeapi().
feedapi_node_type Implementation of hook_node_type().
feedapi_perm Implementation of hook_perm().
feedapi_purge_confirm Ask for confirmation before deleting all the items
feedapi_purge_confirm_submit Submitted items purging form. Drop all the items.
feedapi_refresh Refresh a feed node (= run enabled processors on it).
feedapi_simplified_form This is shown instead of normal node form when the simplified form is chosen at the settings
feedapi_simplified_form_submit Create the node object and save
_feedapi_build_feed_object Builds feed object ready to be sticked onto node.
_feedapi_call_parsers Execute the enabled parsers and create an unified output
_feedapi_format_settings Returns per content type settings ordered by weight and only those that are turned on.
_feedapi_get_stat Return the type-specific statistics data
_feedapi_import_opml_process Imports from OPML XML file
_feedapi_insert Insert feedapi data to the DB when it's a new for for FeedAPI
_feedapi_invoke Helper function for feedapi_invoke().
_feedapi_invoke_load Helper function for feedapi_invoke(). Load a list of feed items into the feed object from the processors.
_feedapi_invoke_purge Helper function for feedapi_invoke(). Delete all feed items of a feed.
_feedapi_invoke_refresh Helper function for feedapi_invoke(). Refresh the feed, call the proper parsers and processors' hooks. Don't call this function directly, use feedapi_refresh() instead.
_feedapi_populate Set default value of $form elements if present in $settings.
_feedapi_populate_get_setting Gets the setting for '#parent' (there must be a more efficent way)
_feedapi_sanitize_processors Remove non-existing processors from the processors arrays
_feedapi_store_settings Stores settings per content type or per node.
_feedapi_store_stat Store statistics information
_feedapi_update Update feed data of an existing feed
_feedapi_update_rate Calculate the average between-update time

Constants

Namesort descending Description
FEEDAPI_CRON_FEEDS
FEEDAPI_CRON_MIN_REFRESH_TIME
FEEDAPI_CRON_STAT_LIFETIME
FEEDAPI_NEVER_DELETE_OLD @file Handle the submodules (for feed and item processing) Provide a basic management of feeds
FEEDAPI_TIMEOUT