first import

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-08 11:40:19 +02:00
commit 1bc61b12ad
8435 changed files with 1582817 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
<?php
/**
* @file
* FeedsConfigurable and helper functions.
*/
/**
* Used when an object does not exist in the DB or code but should.
*/
class FeedsNotExistingException extends Exception {
}
/**
* Base class for configurable classes. Captures configuration handling, form
* handling and distinguishes between in-memory configuration and persistent
* configuration.
*/
abstract class FeedsConfigurable {
// Holds the actual configuration information.
protected $config;
// A unique identifier for the configuration.
protected $id;
/*
CTools export type of this object.
@todo Should live in FeedsImporter. Not all child classes
of FeedsConfigurable are exportable. Same goes for $disabled.
Export type can be one of
FEEDS_EXPORT_NONE - the configurable only exists in memory
EXPORT_IN_DATABASE - the configurable is defined in the database.
EXPORT_IN_CODE - the configurable is defined in code.
EXPORT_IN_CODE | EXPORT_IN_DATABASE - the configurable is defined in code, but
overridden in the database.*/
protected $export_type;
/**
* CTools export enabled status of this object.
*/
protected $disabled;
/**
* Instantiate a FeedsConfigurable object.
*
* Don't use directly, use feeds_importer() or feeds_plugin()
* instead.
*/
public static function instance($class, $id) {
// This is useful at least as long as we're developing.
if (empty($id)) {
throw new Exception(t('Empty configuration identifier.'));
}
static $instances = array();
if (!isset($instances[$class][$id])) {
$instances[$class][$id] = new $class($id);
}
return $instances[$class][$id];
}
/**
* Constructor, set id and load default configuration.
*/
protected function __construct($id) {
// Set this object's id.
$this->id = $id;
// Per default we assume that a Feeds object is not saved to
// database nor is it exported to code.
$this->export_type = FEEDS_EXPORT_NONE;
// Make sure configuration is populated.
$this->config = $this->configDefaults();
$this->disabled = FALSE;
}
/**
* Override magic method __isset(). This is needed due to overriding __get().
*/
public function __isset($name) {
return isset($this->$name) ? TRUE : FALSE;
}
/**
* Determine whether this object is persistent and enabled. I. e. it is
* defined either in code or in the database and it is enabled.
*/
public function existing() {
if ($this->export_type == FEEDS_EXPORT_NONE) {
throw new FeedsNotExistingException(t('Object is not persistent.'));
}
if ($this->disabled) {
throw new FeedsNotExistingException(t('Object is disabled.'));
}
return $this;
}
/**
* Save a configuration. Concrete extending classes must implement a save
* operation.
*/
public abstract function save();
/**
* Copy a configuration.
*/
public function copy(FeedsConfigurable $configurable) {
$this->setConfig($configurable->config);
}
/**
* Set configuration.
*
* @param $config
* Array containing configuration information. Config array will be filtered
* by the keys returned by configDefaults() and populated with default
* values that are not included in $config.
*/
public function setConfig($config) {
$defaults = $this->configDefaults();
$this->config = array_intersect_key($config, $defaults) + $defaults;
}
/**
* Similar to setConfig but adds to existing configuration.
*
* @param $config
* Array containing configuration information. Will be filtered by the keys
* returned by configDefaults().
*/
public function addConfig($config) {
$this->config = is_array($this->config) ? array_merge($this->config, $config) : $config;
$default_keys = $this->configDefaults();
$this->config = array_intersect_key($this->config, $default_keys);
}
/**
* Override magic method __get(). Make sure that $this->config goes through
* getConfig().
*/
public function __get($name) {
if ($name == 'config') {
return $this->getConfig();
}
return isset($this->$name) ? $this->$name : NULL;
}
/**
* Implements getConfig().
*
* Return configuration array, ensure that all default values are present.
*/
public function getConfig() {
$defaults = $this->configDefaults();
return $this->config + $defaults;
}
/**
* Return default configuration.
*
* @todo rename to getConfigDefaults().
*
* @return
* Array where keys are the variable names of the configuration elements and
* values are their default values.
*/
public function configDefaults() {
return array();
}
/**
* Return configuration form for this object. The keys of the configuration
* form must match the keys of the array returned by configDefaults().
*
* @return
* FormAPI style form definition.
*/
public function configForm(&$form_state) {
return array();
}
/**
* Validation handler for configForm().
*
* Set errors with form_set_error().
*
* @param $values
* An array that contains the values entered by the user through configForm.
*/
public function configFormValidate(&$values) {
}
/**
* Submission handler for configForm().
*
* @param $values
*/
public function configFormSubmit(&$values) {
$this->addConfig($values);
$this->save();
drupal_set_message(t('Your changes have been saved.'));
feeds_cache_clear(FALSE);
}
}
/**
* Config form wrapper. Use to render the configuration form of
* a FeedsConfigurable object.
*
* @param $configurable
* FeedsConfigurable object.
* @param $form_method
* The form method that should be rendered.
*
* @return
* Config form array if available. NULL otherwise.
*/
function feeds_get_form($configurable, $form_method) {
if (method_exists($configurable, $form_method)) {
return drupal_get_form(get_class($configurable) . '_feeds_form', $configurable, $form_method);
}
}
/**
* Config form callback. Don't call directly, but use
* feeds_get_form($configurable, 'method') instead.
*
* @param
* FormAPI $form_state.
* @param
* FeedsConfigurable object.
* @param
* The object to perform the save() operation on.
* @param $form_method
* The $form_method that should be rendered.
*/
function feeds_form($form, &$form_state, $configurable, $form_method) {
$form = $configurable->$form_method($form_state);
$form['#configurable'] = $configurable;
$form['#feeds_form_method'] = $form_method;
$form['#validate'] = array('feeds_form_validate');
$form['#submit'] = array('feeds_form_submit');
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 100,
);
return $form;
}
/**
* Validation handler for feeds_form().
*/
function feeds_form_validate($form, &$form_state) {
_feeds_form_helper($form, $form_state, 'Validate');
}
/**
* Submit handler for feeds_form().
*/
function feeds_form_submit($form, &$form_state) {
_feeds_form_helper($form, $form_state, 'Submit');
}
/**
* Helper for Feeds validate and submit callbacks.
*/
function _feeds_form_helper($form, &$form_state, $action) {
$method = $form['#feeds_form_method'] . $action;
$class = get_class($form['#configurable']);
$id = $form['#configurable']->id;
// Re-initialize the configurable object. Using feeds_importer() and
// feeds_plugin() will ensure that we're using the same instance. We can't
// reuse the previous form instance because feeds_importer() is used to save.
// This will re-initialize all of the plugins anyway, causing some tricky
// saving issues in certain cases.
// See http://drupal.org/node/1672880.
if ($class == variable_get('feeds_importer_class', 'FeedsImporter')) {
$form['#configurable'] = feeds_importer($id);
}
else {
$form['#configurable'] = feeds_plugin($class, $id);
}
if (method_exists($form['#configurable'], $method)) {
$form['#configurable']->$method($form_state['values']);
}
}

View File

@@ -0,0 +1,333 @@
<?php
/**
* @file
* FeedsImporter class and related.
*/
/**
* A FeedsImporter object describes how an external source should be fetched,
* parsed and processed. Feeds can manage an arbitrary amount of importers.
*
* A FeedsImporter holds a pointer to a FeedsFetcher, a FeedsParser and a
* FeedsProcessor plugin. It further contains the configuration for itself and
* each of the three plugins.
*
* Its most important responsibilities are configuration management, interfacing
* with the job scheduler and expiring of all items produced by this
* importer.
*
* 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.
*/
class FeedsImporter extends FeedsConfigurable {
// Every feed has a fetcher, a parser and a processor.
// These variable names match the possible return values of
// FeedsPlugin::typeOf().
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($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.
*
* @param $time
* All items older than REQUEST_TIME - $time will be deleted. If not
* given, internal processor settings will be used.
*
* @return
* FEEDS_BATCH_COMPLETE if the expiry process finished. A decimal between
* 0.0 and 0.9 periodic if expiry is still in progress.
*
* @throws
* Throws Exception if an error occurs when expiring items.
*/
public function expire($time = NULL) {
return $this->processor->expire($time);
}
/**
* Schedule all periodic tasks for this importer.
*/
public function schedule() {
$this->scheduleExpire();
}
/**
* Schedule expiry of items.
*/
public function scheduleExpire() {
$job = array(
'type' => $this->id,
'period' => 0,
'periodic' => TRUE,
);
if (FEEDS_EXPIRE_NEVER != $this->processor->expiryTime()) {
$job['period'] = 3600;
JobScheduler::get('feeds_importer_expire')->set($job);
}
else {
JobScheduler::get('feeds_importer_expire')->remove($job);
}
}
/**
* Report how many items *should* be created on one page load by this
* importer.
*
* Note:
*
* It depends on whether parser implements batching if this limit is actually
* respected. Further, if no limit is reported it doesn't mean that the
* number of items that can be created on one page load is actually without
* limit.
*
* @return
* A positive number defining the number of items that can be created on
* one page load. 0 if this number is unlimited.
*/
public function getLimit() {
return $this->processor->getLimit();
}
/**
* Save configuration.
*/
public function save() {
$save = new stdClass();
$save->id = $this->id;
$save->config = $this->getConfig();
if ($config = db_query("SELECT config FROM {feeds_importer} WHERE id = :id", array(':id' => $this->id))->fetchField()) {
drupal_write_record('feeds_importer', $save, 'id');
// Only rebuild menu if content_type has changed. Don't worry about
// rebuilding menus when creating a new importer since it will default
// to the standalone page.
$config = unserialize($config);
if ($config['content_type'] != $save->config['content_type']) {
variable_set('menu_rebuild_needed', TRUE);
}
}
else {
drupal_write_record('feeds_importer', $save);
}
}
/**
* 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 = isset($config->disabled) ? $config->disabled : FALSE;
$this->config = $config->config;
return TRUE;
}
return FALSE;
}
/**
* Delete configuration. Removes configuration information
* from database, does not delete configuration itself.
*/
public function delete() {
db_delete('feeds_importer')
->condition('id', $this->id)
->execute();
$job = array(
'type' => $this->id,
'id' => 0,
);
if ($this->export_type & EXPORT_IN_CODE) {
feeds_reschedule($this->id);
}
else {
JobScheduler::get('feeds_importer_expire')->remove($job);
}
}
/**
* 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 = FeedsPlugin::typeOf($plugin_key)) {
if ($plugin = feeds_plugin($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(FeedsConfigurable $configurable) {
parent::copy($configurable);
if ($configurable instanceof FeedsImporter) {
// Instantiate new fetcher, parser and processor and initialize their
// configurations.
foreach ($this->plugin_types as $plugin_type) {
$this->setPlugin($configurable->config[$plugin_type]['plugin_key']);
$this->$plugin_type->setConfig($configurable->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 parent::getConfig();
}
/**
* 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' => '',
'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 submission.
'process_in_background' => FALSE,
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$config = $this->getConfig();
$form = array();
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#description' => t('A human readable name of this importer.'),
'#default_value' => $config['name'],
'#required' => TRUE,
);
$form['description'] = array(
'#type' => 'textfield',
'#title' => t('Description'),
'#description' => t('A description of this importer.'),
'#default_value' => $config['description'],
);
$node_types = node_type_get_names();
array_walk($node_types, 'check_plain');
$form['content_type'] = array(
'#type' => 'select',
'#title' => t('Attach to content type'),
'#description' => t('If "Use standalone form" is selected a source is imported by using a form under !import_form.
If a content type is selected a source is imported by creating a node of that content type.',
array('!import_form' => l(url('import', array('absolute' => TRUE)), 'import', array('attributes' => array('target' => '_new'))))),
'#options' => array('' => t('Use standalone form')) + $node_types,
'#default_value' => $config['content_type'],
);
$cron_required = ' ' . l(t('Requires cron to be configured.'), 'http://drupal.org/cron', array('attributes' => array('target' => '_new')));
$period = drupal_map_assoc(array(900, 1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2419200), 'format_interval');
foreach ($period as &$p) {
$p = t('Every !p', array('!p' => $p));
}
$period = array(
FEEDS_SCHEDULE_NEVER => t('Off'),
0 => t('As often as possible'),
) + $period;
$form['import_period'] = array(
'#type' => 'select',
'#title' => t('Periodic import'),
'#options' => $period,
'#description' => t('Choose how often a source should be imported periodically.') . $cron_required,
'#default_value' => $config['import_period'],
);
$form['import_on_create'] = array(
'#type' => 'checkbox',
'#title' => t('Import on submission'),
'#description' => t('Check if import should be started at the moment a standalone form or node form is submitted.'),
'#default_value' => $config['import_on_create'],
);
$form['process_in_background'] = array(
'#type' => 'checkbox',
'#title' => t('Process in background'),
'#description' => t('For very large imports. If checked, import and delete tasks started from the web UI will be handled by a cron task in the background rather than by the browser. This does not affect periodic imports, they are handled by a cron task in any case.') . $cron_required,
'#default_value' => $config['process_in_background'],
);
return $form;
}
/**
* Reschedule if import period changes.
*/
public function configFormSubmit(&$values) {
if ($this->config['import_period'] != $values['import_period']) {
feeds_reschedule($this->id);
}
parent::configFormSubmit($values);
}
}
/**
* 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)));
}

View File

@@ -0,0 +1,725 @@
<?php
/**
* @file
* Definition of FeedsSourceInterface and FeedsSource class.
*/
/**
* Distinguish exceptions occuring when handling locks.
*/
class FeedsLockException extends Exception {}
/**
* Denote a import or clearing stage. Used for multi page processing.
*/
define('FEEDS_START', 'start_time');
define('FEEDS_FETCH', 'fetch');
define('FEEDS_PARSE', 'parse');
define('FEEDS_PROCESS', 'process');
define('FEEDS_PROCESS_CLEAR', 'process_clear');
/**
* Declares an interface for a class that defines default values and form
* descriptions for a FeedSource.
*/
interface FeedsSourceInterface {
/**
* Crutch: for ease of use, we implement FeedsSourceInterface for every
* plugin, but then we need to have a handle which plugin actually implements
* a source.
*
* @see FeedsPlugin class.
*
* @return
* TRUE if a plugin handles source specific configuration, FALSE otherwise.
*/
public function hasSourceConfig();
/**
* Return an associative array of default values.
*/
public function sourceDefaults();
/**
* Return a Form API form array that defines a form configuring values. Keys
* correspond to the keys of the return value of sourceDefaults().
*/
public function sourceForm($source_config);
/**
* Validate user entered values submitted by sourceForm().
*/
public function sourceFormValidate(&$source_config);
/**
* A source is being saved.
*/
public function sourceSave(FeedsSource $source);
/**
* A source is being deleted.
*/
public function sourceDelete(FeedsSource $source);
}
/**
* Status of an import or clearing operation on a source.
*/
class FeedsState {
/**
* Floating point number denoting the progress made. 0.0 meaning no progress
* 1.0 = FEEDS_BATCH_COMPLETE meaning finished.
*/
public $progress;
/**
* Used as a pointer to store where left off. Must be serializable.
*/
public $pointer;
/**
* Natural numbers denoting more details about the progress being made.
*/
public $total;
public $created;
public $updated;
public $deleted;
public $skipped;
public $failed;
/**
* Constructor, initialize variables.
*/
public function __construct() {
$this->progress = FEEDS_BATCH_COMPLETE;
$this->total =
$this->created =
$this->updated =
$this->deleted =
$this->skipped =
$this->failed = 0;
}
/**
* Safely report progress.
*
* When $total == $progress, the state of the task tracked by this state is
* regarded to be complete.
*
* Handles the following cases gracefully:
*
* - $total is 0
* - $progress is larger than $total
* - $progress approximates $total so that $finished rounds to 1.0
*
* @param $total
* A natural number that is the total to be worked off.
* @param $progress
* A natural number that is the progress made on $total.
*/
public function progress($total, $progress) {
if ($progress > $total) {
$this->progress = FEEDS_BATCH_COMPLETE;
}
elseif ($total) {
$this->progress = $progress / $total;
if ($this->progress == FEEDS_BATCH_COMPLETE && $total != $progress) {
$this->progress = 0.99;
}
}
else {
$this->progress = FEEDS_BATCH_COMPLETE;
}
}
}
/**
* This class encapsulates a source of a feed. It stores where the feed can be
* found and how to import it.
*
* Information on how to import a feed is encapsulated in a FeedsImporter object
* which is identified by the common id of the FeedsSource and the
* FeedsImporter. More than one FeedsSource can use the same FeedsImporter
* therefore a FeedsImporter never holds a pointer to a FeedsSource object, nor
* does it hold any other information for a particular FeedsSource object.
*
* Classes extending FeedsPlugin can implement a sourceForm to expose
* configuration for a FeedsSource object. This is for instance how FeedsFetcher
* exposes a text field for a feed URL or how FeedsCSVParser exposes a select
* field for choosing between colon or semicolon delimiters.
*
* It is important that a FeedsPlugin does not directly hold information about
* a source but leave all storage up to FeedsSource. An instance of a
* FeedsPlugin class only exists once per FeedsImporter configuration, while an
* instance of a FeedsSource class exists once per feed_nid to be imported.
*
* As with FeedsImporter, the idea with FeedsSource is that it can be used
* without actually saving the object to the database.
*/
class FeedsSource extends FeedsConfigurable {
// Contains the node id of the feed this source info object is attached to.
// Equals 0 if not attached to any node - i. e. if used on a
// standalone import form within Feeds or by other API users.
protected $feed_nid;
// The FeedsImporter object that this source is expected to be used with.
protected $importer;
// A FeedsSourceState object holding the current import/clearing state of this
// source.
protected $state;
// Fetcher result, used to cache fetcher result when batching.
protected $fetcher_result;
// Timestamp when this source was imported the last time.
protected $imported;
/**
* Instantiate a unique object per class/id/feed_nid. Don't use
* directly, use feeds_source() instead.
*/
public static function instance($importer_id, $feed_nid) {
$class = variable_get('feeds_source_class', 'FeedsSource');
static $instances = array();
if (!isset($instances[$class][$importer_id][$feed_nid])) {
$instances[$class][$importer_id][$feed_nid] = new $class($importer_id, $feed_nid);
}
return $instances[$class][$importer_id][$feed_nid];
}
/**
* Constructor.
*/
protected function __construct($importer_id, $feed_nid) {
$this->feed_nid = $feed_nid;
$this->importer = feeds_importer($importer_id);
parent::__construct($importer_id);
$this->load();
}
/**
* Returns the FeedsImporter object that this source is expected to be used with.
*/
public function importer() {
return $this->importer;
}
/**
* Preview = fetch and parse a feed.
*
* @return
* FeedsParserResult object.
*
* @throws
* Throws Exception if an error occurs when fetching or parsing.
*/
public function preview() {
$result = $this->importer->fetcher->fetch($this);
$result = $this->importer->parser->parse($this, $result);
module_invoke_all('feeds_after_parse', $this, $result);
return $result;
}
/**
* Start importing a source.
*
* This method starts an import job. Depending on the configuration of the
* importer of this source, a Batch API job or a background job with Job
* Scheduler will be created.
*
* @throws Exception
* If processing in background is enabled, the first batch chunk of the
* import will be executed on the current page request. This means that this
* method may throw the same exceptions as FeedsSource::import().
*/
public function startImport() {
$config = $this->importer->getConfig();
if ($config['process_in_background']) {
$this->startBackgroundJob('import');
}
else {
$this->startBatchAPIJob(t('Importing'), 'import');
}
}
/**
* Start deleting all imported items of a source.
*
* This method starts a clear job. Depending on the configuration of the
* importer of this source, a Batch API job or a background job with Job
* Scheduler will be created.
*
* @throws Exception
* If processing in background is enabled, the first batch chunk of the
* clear task will be executed on the current page request. This means that
* this method may throw the same exceptions as FeedsSource::clear().
*/
public function startClear() {
$config = $this->importer->getConfig();
if ($config['process_in_background']) {
$this->startBackgroundJob('clear');
}
else {
$this->startBatchAPIJob(t('Deleting'), 'clear');
}
}
/**
* Schedule all periodic tasks for this source.
*/
public function schedule() {
$this->scheduleImport();
}
/**
* Schedule periodic or background import tasks.
*/
public function scheduleImport() {
// Check whether any fetcher is overriding the import period.
$period = $this->importer->config['import_period'];
$fetcher_period = $this->importer->fetcher->importPeriod($this);
if (is_numeric($fetcher_period)) {
$period = $fetcher_period;
}
$period = $this->progressImporting() === FEEDS_BATCH_COMPLETE ? $period : 0;
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
// Schedule as soon as possible if a batch is active.
'period' => $period,
'periodic' => TRUE,
);
if ($period != FEEDS_SCHEDULE_NEVER) {
JobScheduler::get('feeds_source_import')->set($job);
}
else {
JobScheduler::get('feeds_source_import')->remove($job);
}
}
/**
* Schedule background clearing tasks.
*/
public function scheduleClear() {
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
'period' => 0,
'periodic' => TRUE,
);
// Remove job if batch is complete.
if ($this->progressClearing() === FEEDS_BATCH_COMPLETE) {
JobScheduler::get('feeds_source_clear')->remove($job);
}
// Schedule as soon as possible if batch is not complete.
else {
JobScheduler::get('feeds_source_clear')->set($job);
}
}
/**
* Import a source: execute fetching, parsing and processing stage.
*
* This method only executes the current batch chunk, then returns. If you are
* looking to import an entire source, use FeedsSource::startImport() instead.
*
* @return
* FEEDS_BATCH_COMPLETE if the import process finished. A decimal between
* 0.0 and 0.9 periodic if import is still in progress.
*
* @throws
* Throws Exception if an error occurs when importing.
*/
public function import() {
$this->acquireLock();
try {
// If fetcher result is empty, we are starting a new import, log.
if (empty($this->fetcher_result)) {
$this->state[FEEDS_START] = time();
}
// Fetch.
if (empty($this->fetcher_result) || FEEDS_BATCH_COMPLETE == $this->progressParsing()) {
$this->fetcher_result = $this->importer->fetcher->fetch($this);
// Clean the parser's state, we are parsing an entirely new file.
unset($this->state[FEEDS_PARSE]);
}
// Parse.
$parser_result = $this->importer->parser->parse($this, $this->fetcher_result);
module_invoke_all('feeds_after_parse', $this, $parser_result);
// Process.
$this->importer->processor->process($this, $parser_result);
}
catch (Exception $e) {
// Do nothing.
}
$this->releaseLock();
// Clean up.
$result = $this->progressImporting();
if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
$this->imported = time();
$this->log('import', 'Imported in !s s', array('!s' => $this->imported - $this->state[FEEDS_START]), WATCHDOG_INFO);
module_invoke_all('feeds_after_import', $this);
unset($this->fetcher_result, $this->state);
}
$this->save();
if (isset($e)) {
throw $e;
}
return $result;
}
/**
* Remove all items from a feed.
*
* This method only executes the current batch chunk, then returns. If you are
* looking to delete all items of a source, use FeedsSource::startClear()
* instead.
*
* @return
* FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between
* 0.0 and 0.9 periodic if clearing is still in progress.
*
* @throws
* Throws Exception if an error occurs when clearing.
*/
public function clear() {
$this->acquireLock();
try {
$this->importer->fetcher->clear($this);
$this->importer->parser->clear($this);
$this->importer->processor->clear($this);
}
catch (Exception $e) {
// Do nothing.
}
$this->releaseLock();
// Clean up.
$result = $this->progressClearing();
if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
module_invoke_all('feeds_after_clear', $this);
unset($this->state);
}
$this->save();
if (isset($e)) {
throw $e;
}
return $result;
}
/**
* Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
*/
public function progressParsing() {
return $this->state(FEEDS_PARSE)->progress;
}
/**
* Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
*/
public function progressImporting() {
$fetcher = $this->state(FEEDS_FETCH);
$parser = $this->state(FEEDS_PARSE);
if ($fetcher->progress == FEEDS_BATCH_COMPLETE && $parser->progress == FEEDS_BATCH_COMPLETE) {
return FEEDS_BATCH_COMPLETE;
}
// Fetching envelops parsing.
// @todo: this assumes all fetchers neatly use total. May not be the case.
$fetcher_fraction = $fetcher->total ? 1.0 / $fetcher->total : 1.0;
$parser_progress = $parser->progress * $fetcher_fraction;
$result = $fetcher->progress - $fetcher_fraction + $parser_progress;
if ($result == FEEDS_BATCH_COMPLETE) {
return 0.99;
}
return $result;
}
/**
* Report progress on clearing.
*/
public function progressClearing() {
return $this->state(FEEDS_PROCESS_CLEAR)->progress;
}
/**
* Return a state object for a given stage. Lazy instantiates new states.
*
* @todo Rename getConfigFor() accordingly to config().
*
* @param $stage
* One of FEEDS_FETCH, FEEDS_PARSE, FEEDS_PROCESS or FEEDS_PROCESS_CLEAR.
*
* @return
* The FeedsState object for the given stage.
*/
public function state($stage) {
if (!is_array($this->state)) {
$this->state = array();
}
if (!isset($this->state[$stage])) {
$this->state[$stage] = new FeedsState();
}
return $this->state[$stage];
}
/**
* Count items imported by this source.
*/
public function itemCount() {
return $this->importer->processor->itemCount($this);
}
/**
* Save configuration.
*/
public function save() {
// Alert implementers of FeedsSourceInterface to the fact that we're saving.
foreach ($this->importer->plugin_types as $type) {
$this->importer->$type->sourceSave($this);
}
$config = $this->getConfig();
// Store the source property of the fetcher in a separate column so that we
// can do fast lookups on it.
$source = '';
if (isset($config[get_class($this->importer->fetcher)]['source'])) {
$source = $config[get_class($this->importer->fetcher)]['source'];
}
$object = array(
'id' => $this->id,
'feed_nid' => $this->feed_nid,
'imported' => $this->imported,
'config' => $config,
'source' => $source,
'state' => isset($this->state) ? $this->state : FALSE,
'fetcher_result' => isset($this->fetcher_result) ? $this->fetcher_result : FALSE,
);
if (db_query_range("SELECT 1 FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", 0, 1, array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchField()) {
drupal_write_record('feeds_source', $object, array('id', 'feed_nid'));
}
else {
drupal_write_record('feeds_source', $object);
}
}
/**
* Load configuration and unpack.
*
* @todo Patch CTools to move constants from export.inc to ctools.module.
*/
public function load() {
if ($record = db_query("SELECT imported, config, state, fetcher_result FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchObject()) {
// While FeedsSource cannot be exported, we still use CTool's export.inc
// export definitions.
ctools_include('export');
$this->export_type = EXPORT_IN_DATABASE;
$this->imported = $record->imported;
$this->config = unserialize($record->config);
if (!empty($record->state)) {
$this->state = unserialize($record->state);
}
if (!empty($record->fetcher_result)) {
$this->fetcher_result = unserialize($record->fetcher_result);
}
}
}
/**
* Delete configuration. Removes configuration information
* from database, does not delete configuration itself.
*/
public function delete() {
// Alert implementers of FeedsSourceInterface to the fact that we're
// deleting.
foreach ($this->importer->plugin_types as $type) {
$this->importer->$type->sourceDelete($this);
}
db_delete('feeds_source')
->condition('id', $this->id)
->condition('feed_nid', $this->feed_nid)
->execute();
// Remove from schedule.
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
);
JobScheduler::get('feeds_source_import')->remove($job);
}
/**
* Only return source if configuration is persistent and valid.
*
* @see FeedsConfigurable::existing().
*/
public function existing() {
// If there is no feed nid given, there must be no content type specified.
// If there is a feed nid given, there must be a content type specified.
// Ensure that importer is persistent (= defined in code or DB).
// Ensure that source is persistent (= defined in DB).
if ((empty($this->feed_nid) && empty($this->importer->config['content_type'])) ||
(!empty($this->feed_nid) && !empty($this->importer->config['content_type']))) {
$this->importer->existing();
return parent::existing();
}
throw new FeedsNotExistingException(t('Source configuration not valid.'));
}
/**
* Returns the configuration for a specific client class.
*
* @param FeedsSourceInterface $client
* An object that is an implementer of FeedsSourceInterface.
*
* @return
* An array stored for $client.
*/
public function getConfigFor(FeedsSourceInterface $client) {
$class = get_class($client);
return isset($this->config[$class]) ? $this->config[$class] : $client->sourceDefaults();
}
/**
* Sets the configuration for a specific client class.
*
* @param FeedsSourceInterface $client
* An object that is an implementer of FeedsSourceInterface.
* @param $config
* The configuration for $client.
*
* @return
* An array stored for $client.
*/
public function setConfigFor(FeedsSourceInterface $client, $config) {
$this->config[get_class($client)] = $config;
}
/**
* Return defaults for feed configuration.
*/
public function configDefaults() {
// Collect information from plugins.
$defaults = array();
foreach ($this->importer->plugin_types as $type) {
if ($this->importer->$type->hasSourceConfig()) {
$defaults[get_class($this->importer->$type)] = $this->importer->$type->sourceDefaults();
}
}
return $defaults;
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
// Collect information from plugins.
$form = array();
foreach ($this->importer->plugin_types as $type) {
if ($this->importer->$type->hasSourceConfig()) {
$class = get_class($this->importer->$type);
$config = isset($this->config[$class]) ? $this->config[$class] : array();
$form[$class] = $this->importer->$type->sourceForm($config);
$form[$class]['#tree'] = TRUE;
}
}
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
foreach ($this->importer->plugin_types as $type) {
$class = get_class($this->importer->$type);
if (isset($values[$class]) && $this->importer->$type->hasSourceConfig()) {
$this->importer->$type->sourceFormValidate($values[$class]);
}
}
}
/**
* Writes to feeds log.
*/
public function log($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
feeds_log($this->id, $this->feed_nid, $type, $message, $variables, $severity);
}
/**
* Background job helper. Starts a background job using Job Scheduler.
*
* Execute the first batch chunk of a background job on the current page load,
* moves the rest of the job processing to a cron powered background job.
*
* Executing the first batch chunk is important, otherwise, when a user
* submits a source for import or clearing, we will leave her without any
* visual indicators of an ongoing job.
*
* @see FeedsSource::startImport().
* @see FeedsSource::startClear().
*
* @param $method
* Method to execute on importer; one of 'import' or 'clear'.
*
* @throws Exception $e
*/
protected function startBackgroundJob($method) {
if (FEEDS_BATCH_COMPLETE != $this->$method()) {
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
'period' => 0,
'periodic' => FALSE,
);
JobScheduler::get("feeds_source_{$method}")->set($job);
}
}
/**
* Batch API helper. Starts a Batch API job.
*
* @see FeedsSource::startImport().
* @see FeedsSource::startClear().
* @see feeds_batch()
*
* @param $title
* Title to show to user when executing batch.
* @param $method
* Method to execute on importer; one of 'import' or 'clear'.
*/
protected function startBatchAPIJob($title, $method) {
$batch = array(
'title' => $title,
'operations' => array(
array('feeds_batch', array($method, $this->id, $this->feed_nid)),
),
'progress_message' => '',
);
batch_set($batch);
}
/**
* Acquires a lock for this source.
*
* @throws FeedsLockException
* If a lock for the requested job could not be acquired.
*/
protected function acquireLock() {
if (!lock_acquire("feeds_source_{$this->id}_{$this->feed_nid}", 60.0)) {
throw new FeedsLockException(t('Cannot acquire lock for source @id / @feed_nid.', array('@id' => $this->id, '@feed_nid' => $this->feed_nid)));
}
}
/**
* Releases a lock for this source.
*/
protected function releaseLock() {
lock_release("feeds_source_{$this->id}_{$this->feed_nid}");
}
}