Skip to content
Snippets Groups Projects
FeedsImporter.inc 9.75 KiB
Newer Older
<?php
// $Id$

/**
 * @file
 * FeedsImporter class and related.
 */

// Including FeedsImporter.inc automatically includes dependencies.
require_once(dirname(__FILE__) .'/FeedsConfigurable.inc');
require_once(dirname(__FILE__) .'/FeedsSource.inc');

/**
 * A Feeds result class.
 *
 * @see class FeedsFetcherResult
 * @see class FeedsParserResult
 */
abstract class FeedsResult {

  // An array of valid values for $type.
  protected $valid_types = array();
  // The type of this result.
  protected $type;
  // The value of this result.
  protected $value;

  /**
   * Constructor: create object, validate class variables.
   *
   * @param $value
   *   The value of this result.
   * @param $type
   *   The type of this result. Must be one of $valid_types.
   */
  public function __construct($value, $type) {
    $this->__set('type', $type);
    $this->__set('value', $value);
  }

  /**
   * Control access to class variables.
   */
  public function __set($name, $value) {
    if ($name == 'valid_types') {
      throw new Exception(t('Cannot write FeedsResult::valid_types.'));
    }
    if ($name == 'type') {
      if (!in_array($value, $this->valid_types)) {
        throw new Exception(t('Invalid type "!type"', array('!type' => $value)));
      }
    }
    $this->$name = $value;
  }

  /**
   * Control access to class variables.
   */
  public function __get($name) {
    return $this->$name;
  }
}

/**
 * Class defining an importer object. This is the main hub for Feeds module's
 * functionality.
 *
 * A FeedsImporter holds a pointer to a fetcher, a parser and a processor
 * plugin. It further contains the configuration for itself and each of the
 * three plugins.
 *
 * Its most important responsibilities are configuration management, importing
 * and purging.
 *
 * When a FeedsImporter is instantiated, it loads its configuration. Then it
 * instantiates one fetcher, one parser and one processor plugin depending on
 * the configuration information. After instantiating them, it sets them to
 * the configuration information it holds for them.
 *
 * @see __construct()
 *
 * When importing or purging, a FeedsSource object is passed into import() and
 * the fetcher, the parser and the processor are subsequently executed. It is
 * important to note that at no time a FeedsImporter object holds a pointer to a
 * FeedsSource object, while a FeedsSource object always holds a pointer to a
 * FeedsImporter object. The reason is that there is only one FeedsImporter
 * instance per configuration, while there is a FeedsSource object per source to
 * be imported. Sources can be tied to feed nodes, thus there can be potentially
 * many sources per feeds configuration.
 *
 * @see import()
 * @see clear()
 */
class FeedsImporter extends FeedsConfigurable {

  // Every feed has a fetcher, a parser and a processor.
  // These variable names match the possible return values of
  // feeds_plugin_type().
  protected $fetcher, $parser, $processor;

  // This array defines the variable names of the plugins above.
  protected $plugin_types = array('fetcher', 'parser', 'processor');

  /**
   * Instantiate class variables, initialize and configure
   * plugins.
   */
  protected function __construct($id) {
    parent::__construct($id);

    // Try to load information from database.
    $this->load();

    // Instantiate fetcher, parser and processor, set their configuration if
    // stored info is available.
    foreach ($this->plugin_types as $type) {
      $plugin = feeds_plugin_instance($this->config[$type]['plugin_key'], $this->id);

      if (isset($this->config[$type]['config'])) {
        $plugin->setConfig($this->config[$type]['config']);
      }
      $this->$type = $plugin;
    }
  }

  /**
   * Remove items older than $time. If $time is not given, processor settings
   * will be used.
   */
  public function expire($time = NULL) {
    try {
      $this->processor->expire($time);
    }
    catch (Exception $e) {
      drupal_set_message($e->getMessage(), 'error');
    }
  }

  /**
   * Get the refresh period for import() or expire().
   */
  public function getSchedulePeriod($callback) {
    if ($callback == 'import') {
      return $this->config['import_period'];
    }
    if ($callback == 'expire') {
      // If a processor has expiry time set, run expiry every hour.
      if (FEEDS_EXPIRE_NEVER != $this->processor->expiryTime()) {
        return 3600;
      }
      return FEEDS_SCHEDULE_NEVER;
    }
  }

  /**
   * Save configuration.
   */
  public function save() {
    $save = new stdClass();
    $save->id = $this->id;
    $save->config = $this->getConfig();
    // Make sure a source record is present at all time, try to update first,
    // then insert.
    drupal_write_record('feeds_importer', $save, 'id');
    if (!db_affected_rows()) {
      drupal_write_record('feeds_importer', $save);
    }
    // Clear menu cache, changes to importer can change menu items.
    menu_rebuild();
  }

  /**
   * Load configuration and unpack.
   */
  public function load() {
    ctools_include('export');
    if ($config = ctools_export_load_object('feeds_importer', 'conditions', array('id' => $this->id))) {
      $config = array_shift($config);
      $this->export_type = $config->export_type;
      $this->disabled = $config->disabled;
      $this->config = $config->config;
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Delete configuration. Removes configuration information
   * from database, does not delete configuration itself.
   */
  public function delete() {
    db_query('DELETE FROM {feeds_importer} WHERE id = "%s"', $this->id);
  }

  /**
   * Set plugin.
   *
   * @param $plugin_key
   *   A fetcher, parser or processor plugin.
   *
   * @todo: error handling, handle setting to the same plugin.
   */
  public function setPlugin($plugin_key) {
    // $plugin_type can be either 'fetcher', 'parser' or 'processor'
    if ($plugin_type = feeds_plugin_type($plugin_key)) {
      if ($plugin = feeds_plugin_instance($plugin_key, $this->id)) {
        // Unset existing plugin, switch to new plugin.
        unset($this->$plugin_type);
        $this->$plugin_type = $plugin;
        // Set configuration information, blow away any previous information on
        // this spot.
        $this->config[$plugin_type] = array('plugin_key' => $plugin_key);
      }
    }
  }

  /**
   * Copy a FeedsImporter configuration into this importer.
   *
   * @param FeedsImporter $importer
   *   The feeds importer object to copy from.
   */
  public function copy(FeedsImporter $importer) {
    $this->setConfig($importer->config);

    // Instantiate new fetcher, parser and processor and initialize their
    // configurations.
    foreach ($this->plugin_types as $plugin_type) {
      $this->setPlugin($importer->config[$plugin_type]['plugin_key']);
      $this->$plugin_type->setConfig($importer->config[$plugin_type]['config']);
    }
  }

  /**
   * Get configuration of this feed.
   */
  public function getConfig() {
    foreach (array('fetcher', 'parser', 'processor') as $type) {
      $this->config[$type]['config'] = $this->$type->getConfig();
    }
    return $this->config;// Collect information from plugins.
  }

  /**
   * Return defaults for feed configuration.
   */
  public function configDefaults() {
    return array(
      'name' => '',
      'description' => '',
      'fetcher' => array(
        'plugin_key' => 'FeedsHTTPFetcher',
      ),
      'parser' => array(
        'plugin_key' => 'FeedsSyndicationParser',
      ),
      'processor' => array(
        'plugin_key' => 'FeedsNodeProcessor',
      ),
      'content_type' => 'page', // @todo: provide default content type feed.
      'update' => 0,
      'import_period' => 1800, // Refresh every 30 minutes by default.
      'expire_period' => 3600, // Expire every hour by default, this is a hidden setting.
      'import_on_create' => TRUE, // Import on create.
    );
  }

  /**
   * Override parent::configForm().
   */
  public function configForm(&$form_state) {
    $form = array();
    $form['name'] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
      '#description' => t('The name of this configuration.'),
      '#default_value' => $this->config['name'],
      '#required' => TRUE,
    );
    $form['description'] = array(
      '#type' => 'textfield',
      '#title' => t('Description'),
      '#description' => t('A description of this configuration.'),
      '#default_value' => $this->config['description'],
    );
    $form['content_type'] = array(
      '#type' => 'select',
      '#title' => t('Attach to content type'),
      '#description' => t('If you attach a configuration to a node you can use nodes for creating feeds on your site.'),
      '#options' => array('' => t('Use standalone form')) + node_get_types('names'),
      '#default_value' => $this->config['content_type'],
    );
    $period = drupal_map_assoc(array(0, 900, 1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2419200), 'format_interval');
    $period[FEEDS_SCHEDULE_NEVER] = t('Never');
    $period[0] = t('As often as possible');
    $form['import_period'] = array(
      '#type' => 'select',
      '#title' => t('Minimum refresh period'),
      '#options' => $period,
      '#description' => t('This is the minimum time that must elapse before a feed may be refreshed automatically.'),
      '#default_value' => $this->config['import_period'],
    );
    $form['import_on_create'] = array(
      '#type' => 'checkbox',
      '#title' => t('Import on create'),
      '#description' => t('Check if content should be imported at the moment of feed submission.'),
      '#default_value' => $this->config['import_on_create'],
    );
    return $form;
  }
}

/**
 * Helper, see FeedsDataProcessor class.
 */
function feeds_format_expire($timestamp) {
  if ($timestamp == FEEDS_EXPIRE_NEVER) {
    return t('Never');
  }
  return t('after !time', array('!time' => format_interval($timestamp)));
}