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,216 @@
<?php
/**
* @file
* Contains the FeedsCSVParser class.
*/
/**
* Parses a given file as a CSV file.
*/
class FeedsCSVParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
$source_config = $source->getConfigFor($this);
$state = $source->state(FEEDS_PARSE);
// Load and configure parser.
feeds_include_library('ParserCSV.inc', 'ParserCSV');
$parser = new ParserCSV();
$delimiter = $source_config['delimiter'] == 'TAB' ? "\t" : $source_config['delimiter'];
$parser->setDelimiter($delimiter);
$iterator = new ParserCSVIterator($fetcher_result->getFilePath());
if (empty($source_config['no_headers'])) {
// Get first line and use it for column names, convert them to lower case.
$header = $this->parseHeader($parser, $iterator);
if (!$header) {
return;
}
$parser->setColumnNames($header);
}
// Determine section to parse, parse.
$start = $state->pointer ? $state->pointer : $parser->lastLinePos();
$limit = $source->importer->getLimit();
$rows = $this->parseItems($parser, $iterator, $start, $limit);
// Report progress.
$state->total = filesize($fetcher_result->getFilePath());
$state->pointer = $parser->lastLinePos();
$progress = $parser->lastLinePos() ? $parser->lastLinePos() : $state->total;
$state->progress($state->total, $progress);
// Create a result object and return it.
return new FeedsParserResult($rows, $source->feed_nid);
}
/**
* Get first line and use it for column names, convert them to lower case.
* Be aware that the $parser and iterator objects can be modified in this
* function since they are passed in by reference
*
* @param ParserCSV $parser
* @param ParserCSVIterator $iterator
* @return
* An array of lower-cased column names to use as keys for the parsed items.
*/
protected function parseHeader(ParserCSV $parser, ParserCSVIterator $iterator) {
$parser->setLineLimit(1);
$rows = $parser->parse($iterator);
if (!count($rows)) {
return FALSE;
}
$header = array_shift($rows);
foreach ($header as $i => $title) {
$header[$i] = trim(drupal_strtolower($title));
}
return $header;
}
/**
* Parse all of the items from the CSV.
*
* @param ParserCSV $parser
* @param ParserCSVIterator $iterator
* @return
* An array of rows of the CSV keyed by the column names previously set
*/
protected function parseItems(ParserCSV $parser, ParserCSVIterator $iterator, $start = 0, $limit = 0) {
$parser->setLineLimit($limit);
$parser->setStartByte($start);
$rows = $parser->parse($iterator);
return $rows;
}
/**
* Override parent::getMappingSources().
*/
public function getMappingSources() {
return FALSE;
}
/**
* Override parent::getSourceElement() to use only lower keys.
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
return parent::getSourceElement($source, $result, drupal_strtolower($element_key));
}
/**
* Define defaults.
*/
public function sourceDefaults() {
return array(
'delimiter' => $this->config['delimiter'],
'no_headers' => $this->config['no_headers'],
);
}
/**
* Source form.
*
* Show mapping configuration as a guidance for import form users.
*/
public function sourceForm($source_config) {
$form = array();
$form['#weight'] = -10;
$mappings = feeds_importer($this->id)->processor->config['mappings'];
$sources = $uniques = array();
foreach ($mappings as $mapping) {
$sources[] = check_plain($mapping['source']);
if ($mapping['unique']) {
$uniques[] = check_plain($mapping['source']);
}
}
$output = t('Import !csv_files with one or more of these columns: !columns.', array('!csv_files' => l(t('CSV files'), 'http://en.wikipedia.org/wiki/Comma-separated_values'), '!columns' => implode(', ', $sources)));
$items = array();
$items[] = format_plural(count($uniques), t('Column <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.', array('!column' => implode(', ', $uniques))), t('Columns <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these column will be created.', array('!columns' => implode(', ', $uniques))));
$items[] = l(t('Download a template'), 'import/' . $this->id . '/template');
$form['help']['#markup'] = '<div class="help"><p>' . $output . '</p>' . theme('item_list', array('items' => $items)) . '</div>';
$form['delimiter'] = array(
'#type' => 'select',
'#title' => t('Delimiter'),
'#description' => t('The character that delimits fields in the CSV file.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#default_value' => isset($source_config['delimiter']) ? $source_config['delimiter'] : ',',
);
$form['no_headers'] = array(
'#type' => 'checkbox',
'#title' => t('No Headers'),
'#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
'#default_value' => isset($source_config['no_headers']) ? $source_config['no_headers'] : 0,
);
return $form;
}
/**
* Define default configuration.
*/
public function configDefaults() {
return array(
'delimiter' => ',',
'no_headers' => 0,
);
}
/**
* Build configuration form.
*/
public function configForm(&$form_state) {
$form = array();
$form['delimiter'] = array(
'#type' => 'select',
'#title' => t('Default delimiter'),
'#description' => t('Default field delimiter.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#default_value' => $this->config['delimiter'],
);
$form['no_headers'] = array(
'#type' => 'checkbox',
'#title' => t('No headers'),
'#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
'#default_value' => $this->config['no_headers'],
);
return $form;
}
public function getTemplate() {
$mappings = feeds_importer($this->id)->processor->config['mappings'];
$sources = $uniques = array();
foreach ($mappings as $mapping) {
if ($mapping['unique']) {
$uniques[] = check_plain($mapping['source']);
}
else {
$sources[] = check_plain($mapping['source']);
}
}
$sep = ',';
$columns = array();
foreach (array_merge($uniques, $sources) as $col) {
if (strpos($col, $sep) !== FALSE) {
$col = '"' . str_replace('"', '""', $col) . '"';
}
$columns[] = $col;
}
drupal_add_http_header('Cache-Control', 'max-age=60, must-revalidate');
drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $this->id . '_template.csv"');
drupal_add_http_header('Content-type', 'text/csv; charset=utf-8');
print implode($sep, $columns);
return;
}
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* @file
* Contains the FeedsFetcher and related classes.
*/
/**
* Base class for all fetcher results.
*/
class FeedsFetcherResult extends FeedsResult {
protected $raw;
protected $file_path;
/**
* Constructor.
*/
public function __construct($raw) {
$this->raw = $raw;
}
/**
* @return
* The raw content from the source as a string.
*
* @throws Exception
* Extending classes MAY throw an exception if a problem occurred.
*/
public function getRaw() {
return $this->sanitizeRaw($this->raw);
}
/**
* Get a path to a temporary file containing the resource provided by the
* fetcher.
*
* File will be deleted after DRUPAL_MAXIMUM_TEMP_FILE_AGE.
*
* @return
* A path to a file containing the raw content as a source.
*
* @throws Exception
* If an unexpected problem occurred.
*/
public function getFilePath() {
if (!isset($this->file_path)) {
$destination = 'public://feeds';
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new Exception(t('Feeds directory either cannot be created or is not writable.'));
}
$this->file_path = FALSE;
if ($file = file_save_data($this->getRaw(), $destination . '/' . get_class($this) . REQUEST_TIME)) {
$file->status = 0;
file_save($file);
$this->file_path = $file->uri;
}
else {
throw new Exception(t('Cannot write content to %dest', array('%dest' => $destination)));
}
}
return $this->sanitizeFile($this->file_path);
}
/**
* Sanitize the raw content string. Currently supported sanitizations:
*
* - Remove BOM header from UTF-8 files.
*
* @param string $raw
* The raw content string to be sanitized.
* @return
* The sanitized content as a string.
*/
public function sanitizeRaw($raw) {
if (substr($raw, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
$raw = substr($raw, 3);
}
return $raw;
}
/**
* Sanitize the file in place. Currently supported sanitizations:
*
* - Remove BOM header from UTF-8 files.
*
* @param string $filepath
* The file path of the file to be sanitized.
* @return
* The file path of the sanitized file.
*/
public function sanitizeFile($filepath) {
$handle = fopen($filepath, 'r');
$line = fgets($handle);
fclose($handle);
// If BOM header is present, read entire contents of file and overwrite
// the file with corrected contents.
if (substr($line, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
$contents = file_get_contents($filepath);
$contents = substr($contents, 3);
$status = file_put_contents($filepath, $contents);
if ($status === FALSE) {
throw new Exception(t('File @filepath is not writeable.', array('@filepath' => $filepath)));
}
}
return $filepath;
}
}
/**
* Abstract class, defines shared functionality between fetchers.
*
* Implements FeedsSourceInfoInterface to expose source forms to Feeds.
*/
abstract class FeedsFetcher extends FeedsPlugin {
/**
* Fetch content from a source and return it.
*
* Every class that extends FeedsFetcher must implement this method.
*
* @param $source
* Source value as entered by user through sourceForm().
*
* @return
* A FeedsFetcherResult object.
*/
public abstract function fetch(FeedsSource $source);
/**
* Clear all caches for results for given source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
*/
public function clear(FeedsSource $source) {}
/**
* Request handler invoked if callback URL is requested. Locked down by
* default. For a example usage see FeedsHTTPFetcher.
*
* Note: this method may exit the script.
*
* @return
* A string to be returned to the client.
*/
public function request($feed_nid = 0) {
drupal_access_denied();
}
/**
* Construct a path for a concrete fetcher/source combination. The result of
* this method matches up with the general path definition in
* FeedsFetcher::menuItem(). For example usage look at FeedsHTTPFetcher.
*
* @return
* Path for this fetcher/source combination.
*/
public function path($feed_nid = 0) {
$id = urlencode($this->id);
if ($feed_nid && is_numeric($feed_nid)) {
return "feeds/importer/$id/$feed_nid";
}
return "feeds/importer/$id";
}
/**
* Menu item definition for fetchers of this class. Note how the path
* component in the item definition matches the return value of
* FeedsFetcher::path();
*
* Requests to this menu item will be routed to FeedsFetcher::request().
*
* @return
* An array where the key is the Drupal menu item path and the value is
* a valid Drupal menu item definition.
*/
public function menuItem() {
return array(
'feeds/importer/%feeds_importer' => array(
'page callback' => 'feeds_fetcher_callback',
'page arguments' => array(2, 3),
'access callback' => TRUE,
'file' => 'feeds.pages.inc',
'type' => MENU_CALLBACK,
),
);
}
/**
* Subscribe to a source. Only implement if fetcher requires subscription.
*
* @param FeedsSource $source
* Source information for this subscription.
*/
public function subscribe(FeedsSource $source) {}
/**
* Unsubscribe from a source. Only implement if fetcher requires subscription.
*
* @param FeedsSource $source
* Source information for unsubscribing.
*/
public function unsubscribe(FeedsSource $source) {}
/**
* Override import period settings. This can be used to force a certain import
* interval.
*
* @param $source
* A FeedsSource object.
*
* @return
* A time span in seconds if periodic import should be overridden for given
* $source, NULL otherwise.
*/
public function importPeriod(FeedsSource $source) {}
}

View File

@@ -0,0 +1,229 @@
<?php
/**
* @file
* Home of the FeedsFileFetcher and related classes.
*/
/**
* Definition of the import batch object created on the fetching stage by
* FeedsFileFetcher.
*/
class FeedsFileFetcherResult extends FeedsFetcherResult {
/**
* Constructor.
*/
public function __construct($file_path) {
parent::__construct('');
$this->file_path = $file_path;
}
/**
* Overrides parent::getRaw();
*/
public function getRaw() {
return $this->sanitizeRaw(file_get_contents($this->file_path));
}
/**
* Overrides parent::getFilePath().
*/
public function getFilePath() {
if (!file_exists($this->file_path)) {
throw new Exception(t('File @filepath is not accessible.', array('@filepath' => $this->file_path)));
}
return $this->sanitizeFile($this->file_path);
}
}
/**
* Fetches data via HTTP.
*/
class FeedsFileFetcher extends FeedsFetcher {
/**
* Implements FeedsFetcher::fetch().
*/
public function fetch(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
// Just return a file fetcher result if this is a file.
if (is_file($source_config['source'])) {
return new FeedsFileFetcherResult($source_config['source']);
}
// Batch if this is a directory.
$state = $source->state(FEEDS_FETCH);
$files = array();
if (!isset($state->files)) {
$state->files = $this->listFiles($source_config['source']);
$state->total = count($state->files);
}
if (count($state->files)) {
$file = array_shift($state->files);
$state->progress($state->total, $state->total - count($state->files));
return new FeedsFileFetcherResult($file);
}
throw new Exception(t('Resource is not a file or it is an empty directory: %source', array('%source' => $source_config['source'])));
}
/**
* Return an array of files in a directory.
*
* @param $dir
* A stream wreapper URI that is a directory.
*
* @return
* An array of stream wrapper URIs pointing to files. The array is empty
* if no files could be found. Never contains directories.
*/
protected function listFiles($dir) {
$dir = file_stream_wrapper_uri_normalize($dir);
$files = array();
if ($items = @scandir($dir)) {
foreach ($items as $item) {
if (is_file("$dir/$item") && strpos($item, '.') !== 0) {
$files[] = "$dir/$item";
}
}
}
return $files;
}
/**
* Source form.
*/
public function sourceForm($source_config) {
$form = array();
$form['fid'] = array(
'#type' => 'value',
'#value' => empty($source_config['fid']) ? 0 : $source_config['fid'],
);
if (empty($this->config['direct'])) {
$form['source'] = array(
'#type' => 'value',
'#value' => empty($source_config['source']) ? '' : $source_config['source'],
);
$form['upload'] = array(
'#type' => 'file',
'#title' => empty($this->config['direct']) ? t('File') : NULL,
'#description' => empty($source_config['source']) ? t('Select a file from your local system.') : t('Select a different file from your local system.'),
'#theme' => 'feeds_upload',
'#file_info' => empty($source_config['fid']) ? NULL : file_load($source_config['fid']),
'#size' => 10,
);
}
else {
$form['source'] = array(
'#type' => 'textfield',
'#title' => t('File'),
'#description' => t('Specify a path to a file or a directory. Path must start with @scheme://', array('@scheme' => file_default_scheme())),
'#default_value' => empty($source_config['source']) ? '' : $source_config['source'],
);
}
return $form;
}
/**
* Override parent::sourceFormValidate().
*/
public function sourceFormValidate(&$values) {
$values['source'] = trim($values['source']);
$feed_dir = 'public://feeds';
file_prepare_directory($feed_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
// If there is a file uploaded, save it, otherwise validate input on
// file.
// @todo: Track usage of file, remove file when removing source.
if ($file = file_save_upload('feeds', array('file_validate_extensions' => array(0 => $this->config['allowed_extensions'])), $feed_dir)) {
$values['source'] = $file->uri;
$values['file'] = $file;
}
elseif (empty($values['source'])) {
form_set_error('feeds][source', t('Upload a file first.'));
}
// If a file has not been uploaded and $values['source'] is not empty, make
// sure that this file is within Drupal's files directory as otherwise
// potentially any file that the web server has access to could be exposed.
elseif (strpos($values['source'], file_default_scheme()) !== 0) {
form_set_error('feeds][source', t('File needs to reside within the site\'s file directory, its path needs to start with @scheme://.', array('@scheme' => file_default_scheme())));
}
}
/**
* Override parent::sourceSave().
*/
public function sourceSave(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
// If a new file is present, delete the old one and replace it with the new
// one.
if (isset($source_config['file'])) {
$file = $source_config['file'];
if (isset($source_config['fid'])) {
$this->deleteFile($source_config['fid'], $source->feed_nid);
}
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
file_usage_add($file, 'feeds', get_class($this), $source->feed_nid);
$source_config['fid'] = $file->fid;
unset($source_config['file']);
$source->setConfigFor($this, $source_config);
}
}
/**
* Override parent::sourceDelete().
*/
public function sourceDelete(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
if (isset($source_config['fid'])) {
$this->deleteFile($source_config['fid'], $source->feed_nid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'allowed_extensions' => 'txt csv tsv xml opml',
'direct' => FALSE,
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = array();
$form['allowed_extensions'] = array(
'#type' => 'textfield',
'#title' => t('Allowed file extensions'),
'#description' => t('Allowed file extensions for upload.'),
'#default_value' => $this->config['allowed_extensions'],
);
$form['direct'] = array(
'#type' => 'checkbox',
'#title' => t('Supply path to file or directory directly'),
'#description' => t('For experts. Lets users specify a path to a file <em>or a directory of files</em> directly,
instead of a file upload through the browser. This is useful when the files that need to be imported
are already on the server.'),
'#default_value' => $this->config['direct'],
);
return $form;
}
/**
* Helper. Deletes a file.
*/
protected function deleteFile($fid, $feed_nid) {
if ($file = file_load($fid)) {
file_usage_delete($file, 'feeds', get_class($this), $feed_nid);
file_delete($file);
}
}
}

View File

@@ -0,0 +1,332 @@
<?php
/**
* @file
* Home of the FeedsHTTPFetcher and related classes.
*/
feeds_include_library('PuSHSubscriber.inc', 'PuSHSubscriber');
/**
* Result of FeedsHTTPFetcher::fetch().
*/
class FeedsHTTPFetcherResult extends FeedsFetcherResult {
protected $url;
protected $file_path;
/**
* Constructor.
*/
public function __construct($url = NULL) {
$this->url = $url;
parent::__construct('');
}
/**
* Overrides FeedsFetcherResult::getRaw();
*/
public function getRaw() {
feeds_include_library('http_request.inc', 'http_request');
$result = http_request_get($this->url);
if (!in_array($result->code, array(200, 201, 202, 203, 204, 205, 206))) {
throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->url, '!code' => $result->code)));
}
return $this->sanitizeRaw($result->data);
}
}
/**
* Fetches data via HTTP.
*/
class FeedsHTTPFetcher extends FeedsFetcher {
/**
* Implements FeedsFetcher::fetch().
*/
public function fetch(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
if ($this->config['use_pubsubhubbub'] && ($raw = $this->subscriber($source->feed_nid)->receive())) {
return new FeedsFetcherResult($raw);
}
return new FeedsHTTPFetcherResult($source_config['source']);
}
/**
* Clear caches.
*/
public function clear(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$url = $source_config['source'];
feeds_include_library('http_request.inc', 'http_request');
http_request_clear_cache($url);
}
/**
* Implements FeedsFetcher::request().
*/
public function request($feed_nid = 0) {
feeds_dbg($_GET);
@feeds_dbg(file_get_contents('php://input'));
// A subscription verification has been sent, verify.
if (isset($_GET['hub_challenge'])) {
$this->subscriber($feed_nid)->verifyRequest();
}
// No subscription notification has ben sent, we are being notified.
else {
try {
feeds_source($this->id, $feed_nid)->existing()->import();
}
catch (Exception $e) {
// In case of an error, respond with a 503 Service (temporary) unavailable.
header('HTTP/1.1 503 "Not Found"', NULL, 503);
drupal_exit();
}
}
// Will generate the default 200 response.
header('HTTP/1.1 200 "OK"', NULL, 200);
drupal_exit();
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'auto_detect_feeds' => FALSE,
'use_pubsubhubbub' => FALSE,
'designated_hub' => '',
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = array();
$form['auto_detect_feeds'] = array(
'#type' => 'checkbox',
'#title' => t('Auto detect feeds'),
'#description' => t('If the supplied URL does not point to a feed but an HTML document, attempt to extract a feed URL from the document.'),
'#default_value' => $this->config['auto_detect_feeds'],
);
$form['use_pubsubhubbub'] = array(
'#type' => 'checkbox',
'#title' => t('Use PubSubHubbub'),
'#description' => t('Attempt to use a <a href="http://en.wikipedia.org/wiki/PubSubHubbub">PubSubHubbub</a> subscription if available.'),
'#default_value' => $this->config['use_pubsubhubbub'],
);
$form['designated_hub'] = array(
'#type' => 'textfield',
'#title' => t('Designated hub'),
'#description' => t('Enter the URL of a designated PubSubHubbub hub (e. g. superfeedr.com). If given, this hub will be used instead of the hub specified in the actual feed.'),
'#default_value' => $this->config['designated_hub'],
'#dependency' => array(
'edit-use-pubsubhubbub' => array(1),
),
);
return $form;
}
/**
* Expose source form.
*/
public function sourceForm($source_config) {
$form = array();
$form['source'] = array(
'#type' => 'textfield',
'#title' => t('URL'),
'#description' => t('Enter a feed URL.'),
'#default_value' => isset($source_config['source']) ? $source_config['source'] : '',
'#maxlength' => NULL,
'#required' => TRUE,
);
return $form;
}
/**
* Override parent::sourceFormValidate().
*/
public function sourceFormValidate(&$values) {
$values['source'] = trim($values['source']);
if (!feeds_valid_url($values['source'], TRUE)) {
$form_key = 'feeds][' . get_class($this) . '][source';
form_set_error($form_key, t('The URL %source is invalid.', array('%source' => $values['source'])));
}
elseif ($this->config['auto_detect_feeds']) {
feeds_include_library('http_request.inc', 'http_request');
if ($url = http_request_get_common_syndication($values['source'])) {
$values['source'] = $url;
}
}
}
/**
* Override sourceSave() - subscribe to hub.
*/
public function sourceSave(FeedsSource $source) {
if ($this->config['use_pubsubhubbub']) {
// If this is a feeds node we want to delay the subscription to
// feeds_exit() to avoid transaction race conditions.
if ($source->feed_nid) {
$job = array('fetcher' => $this, 'source' => $source);
feeds_set_subscription_job($job);
}
else {
$this->subscribe($source);
}
}
}
/**
* Override sourceDelete() - unsubscribe from hub.
*/
public function sourceDelete(FeedsSource $source) {
if ($this->config['use_pubsubhubbub']) {
// If we're in a feed node, queue the unsubscribe,
// else process immediately.
if ($source->feed_nid) {
$job = array(
'type' => $source->id,
'id' => $source->feed_nid,
'period' => 0,
'periodic' => FALSE,
);
JobScheduler::get('feeds_push_unsubscribe')->set($job);
}
else {
$this->unsubscribe($source);
}
}
}
/**
* Implement FeedsFetcher::subscribe() - subscribe to hub.
*/
public function subscribe(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$this->subscriber($source->feed_nid)->subscribe($source_config['source'], url($this->path($source->feed_nid), array('absolute' => TRUE)), valid_url($this->config['designated_hub']) ? $this->config['designated_hub'] : '');
}
/**
* Implement FeedsFetcher::unsubscribe() - unsubscribe from hub.
*/
public function unsubscribe(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$this->subscriber($source->feed_nid)->unsubscribe($source_config['source'], url($this->path($source->feed_nid), array('absolute' => TRUE)));
}
/**
* Implement FeedsFetcher::importPeriod().
*/
public function importPeriod(FeedsSource $source) {
if ($this->subscriber($source->feed_nid)->subscribed()) {
return 259200; // Delay for three days if there is a successful subscription.
}
}
/**
* Convenience method for instantiating a subscriber object.
*/
protected function subscriber($subscriber_id) {
return PushSubscriber::instance($this->id, $subscriber_id, 'PuSHSubscription', PuSHEnvironment::instance());
}
}
/**
* Implement a PuSHSubscriptionInterface.
*/
class PuSHSubscription implements PuSHSubscriptionInterface {
public $domain;
public $subscriber_id;
public $hub;
public $topic;
public $status;
public $secret;
public $post_fields;
public $timestamp;
/**
* Load a subscription.
*/
public static function load($domain, $subscriber_id) {
if ($v = db_query("SELECT * FROM {feeds_push_subscriptions} WHERE domain = :domain AND subscriber_id = :sid", array(':domain' => $domain, ':sid' => $subscriber_id))->fetchAssoc()) {
$v['post_fields'] = unserialize($v['post_fields']);
return new PuSHSubscription($v['domain'], $v['subscriber_id'], $v['hub'], $v['topic'], $v['secret'], $v['status'], $v['post_fields'], $v['timestamp']);
}
}
/**
* Create a subscription.
*/
public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '') {
$this->domain = $domain;
$this->subscriber_id = $subscriber_id;
$this->hub = $hub;
$this->topic = $topic;
$this->status = $status;
$this->secret = $secret;
$this->post_fields = $post_fields;
}
/**
* Save a subscription.
*/
public function save() {
$this->timestamp = time();
$this->delete($this->domain, $this->subscriber_id);
drupal_write_record('feeds_push_subscriptions', $this);
}
/**
* Delete a subscription.
*/
public function delete() {
db_delete('feeds_push_subscriptions')
->condition('domain', $this->domain)
->condition('subscriber_id', $this->subscriber_id)
->execute();
}
}
/**
* Provide environmental functions to the PuSHSubscriber library.
*/
class PuSHEnvironment implements PuSHSubscriberEnvironmentInterface {
/**
* Singleton.
*/
public static function instance() {
static $env;
if (empty($env)) {
$env = new PuSHEnvironment();
}
return $env;
}
/**
* Implements PuSHSubscriberEnvironmentInterface::msg().
*/
public function msg($msg, $level = 'status') {
drupal_set_message(check_plain($msg), $level);
}
/**
* Implements PuSHSubscriberEnvironmentInterface::log().
*/
public function log($msg, $level = 'status') {
switch ($level) {
case 'error':
$severity = WATCHDOG_ERROR;
break;
case 'warning':
$severity = WATCHDOG_WARNING;
break;
default:
$severity = WATCHDOG_NOTICE;
break;
}
feeds_dbg($msg);
watchdog('FeedsHTTPFetcher', $msg, array(), $severity);
}
}

View File

@@ -0,0 +1,391 @@
<?php
/**
* @file
* Class definition of FeedsNodeProcessor.
*/
/**
* Creates nodes from feed items.
*/
class FeedsNodeProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'node';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Nodes');
return $info;
}
/**
* Creates a new node in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$node = new stdClass();
$node->type = $this->config['content_type'];
$node->changed = REQUEST_TIME;
$node->created = REQUEST_TIME;
$node->language = LANGUAGE_NONE;
node_object_prepare($node);
// Populate properties that are set by node_object_prepare().
$node->log = 'Created by FeedsNodeProcessor';
$node->uid = $this->config['author'];
return $node;
}
/**
* Loads an existing node.
*
* If the update existing method is not FEEDS_UPDATE_EXISTING, only the node
* table will be loaded, foregoing the node_load API for better performance.
*
* @todo Reevaluate the use of node_object_prepare().
*/
protected function entityLoad(FeedsSource $source, $nid) {
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$node = node_load($nid, NULL, TRUE);
}
else {
// We're replacing the existing node. Only save the absolutely necessary.
$node = db_query("SELECT created, nid, vid, type, status FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchObject();
$node->uid = $this->config['author'];
}
node_object_prepare($node);
// Workaround for issue #1247506. See #1245094 for backstory.
if (!empty($node->menu)) {
// If the node has a menu item(with a valid mlid) it must be flagged
// 'enabled'.
$node->menu['enabled'] = (int) (bool) $node->menu['mlid'];
}
// Populate properties that are set by node_object_prepare().
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$node->log = 'Updated by FeedsNodeProcessor';
}
else {
$node->log = 'Replaced by FeedsNodeProcessor';
}
return $node;
}
/**
* Check that the user has permission to save a node.
*/
protected function entitySaveAccess($entity) {
// The check will be skipped for anonymous nodes.
if ($this->config['authorize'] && !empty($entity->uid)) {
$author = user_load($entity->uid);
// If the uid was mapped directly, rather than by email or username, it
// could be invalid.
if (!$author) {
$message = 'User %uid is not a valid user.';
throw new FeedsAccessException(t($message, array('%uid' => $entity->uid)));
}
if (empty($entity->nid) || !empty($entity->is_new)) {
$op = 'create';
$access = node_access($op, $entity->type, $author);
}
else {
$op = 'update';
$access = node_access($op, $entity, $author);
}
if (!$access) {
$message = 'User %name is not authorized to %op content type %content_type.';
throw new FeedsAccessException(t($message, array('%name' => $author->name, '%op' => $op, '%content_type' => $entity->type)));
}
}
}
/**
* Save a node.
*/
public function entitySave($entity) {
// If nid is set and a node with that id doesn't exist, flag as new.
if (!empty($entity->nid) && !node_load($entity->nid)) {
$entity->is_new = TRUE;
}
node_save($entity);
}
/**
* Delete a series of nodes.
*/
protected function entityDeleteMultiple($nids) {
node_delete_multiple($nids);
}
/**
* Implement expire().
*
* @todo: move to processor stage?
*/
public function expire($time = NULL) {
if ($time === NULL) {
$time = $this->expiryTime();
}
if ($time == FEEDS_EXPIRE_NEVER) {
return;
}
$count = $this->getLimit();
$nodes = db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_item} fi ON fi.entity_type = 'node' AND n.nid = fi.entity_id WHERE fi.id = :id AND n.created < :created", 0, $count, array(':id' => $this->id, ':created' => REQUEST_TIME - $time));
$nids = array();
foreach ($nodes as $node) {
$nids[$node->nid] = $node->nid;
}
$this->entityDeleteMultiple($nids);
if (db_query_range("SELECT 1 FROM {node} n JOIN {feeds_item} fi ON fi.entity_type = 'node' AND n.nid = fi.entity_id WHERE fi.id = :id AND n.created < :created", 0, 1, array(':id' => $this->id, ':created' => REQUEST_TIME - $time))->fetchField()) {
return FEEDS_BATCH_ACTIVE;
}
return FEEDS_BATCH_COMPLETE;
}
/**
* Return expiry time.
*/
public function expiryTime() {
return $this->config['expire'];
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
$types = node_type_get_names();
$type = isset($types['article']) ? 'article' : key($types);
return array(
'content_type' => $type,
'expire' => FEEDS_EXPIRE_NEVER,
'author' => 0,
'authorize' => TRUE,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$types = node_type_get_names();
array_walk($types, 'check_plain');
$form = parent::configForm($form_state);
$form['content_type'] = array(
'#type' => 'select',
'#title' => t('Content type'),
'#description' => t('Select the content type for the nodes to be created. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> nodes of the content type selected here regardless of the node level permissions. Further, users with "clear !feed_id permissions" will be able to <strong>delete</strong> imported nodes regardless of their node level permissions.', array('!feed_id' => $this->id)),
'#options' => $types,
'#default_value' => $this->config['content_type'],
);
$author = user_load($this->config['author']);
$form['author'] = array(
'#type' => 'textfield',
'#title' => t('Author'),
'#description' => t('Select the author of the nodes to be created - leave empty to assign "anonymous".'),
'#autocomplete_path' => 'user/autocomplete',
'#default_value' => empty($author->name) ? 'anonymous' : check_plain($author->name),
);
$form['authorize'] = array(
'#type' => 'checkbox',
'#title' => t('Authorize'),
'#description' => t('Check that the author has permission to create the node.'),
'#default_value' => $this->config['authorize'],
);
$period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2592000, 2592000 * 3, 2592000 * 6, 31536000), 'feeds_format_expire');
$form['expire'] = array(
'#type' => 'select',
'#title' => t('Expire nodes'),
'#options' => $period,
'#description' => t('Select after how much time nodes should be deleted. The node\'s published date will be used for determining the node\'s age, see Mapping settings.'),
'#default_value' => $this->config['expire'],
);
$form['update_existing']['#options'] = array(
FEEDS_SKIP_EXISTING => 'Do not update existing nodes',
FEEDS_REPLACE_EXISTING => 'Replace existing nodes',
FEEDS_UPDATE_EXISTING => 'Update existing nodes (slower than replacing them)',
);
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
if ($author = user_load_by_name($values['author'])) {
$values['author'] = $author->uid;
}
else {
$values['author'] = 0;
}
}
/**
* Reschedule if expiry time changes.
*/
public function configFormSubmit(&$values) {
if ($this->config['expire'] != $values['expire']) {
feeds_reschedule($this->id);
}
parent::configFormSubmit($values);
}
/**
* Override setTargetElement to operate on a target item that is a node.
*/
public function setTargetElement(FeedsSource $source, $target_node, $target_element, $value) {
switch ($target_element) {
case 'created':
$target_node->created = feeds_to_unixtime($value, REQUEST_TIME);
break;
case 'feeds_source':
// Get the class of the feed node importer's fetcher and set the source
// property. See feeds_node_update() how $node->feeds gets stored.
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$class = get_class(feeds_importer($id)->fetcher);
$target_node->feeds[$class]['source'] = $value;
// This effectively suppresses 'import on submission' feature.
// See feeds_node_insert().
$target_node->feeds['suppress_import'] = TRUE;
}
break;
case 'user_name':
if ($user = user_load_by_name($value)) {
$target_node->uid = $user->uid;
}
break;
case 'user_mail':
if ($user = user_load_by_mail($value)) {
$target_node->uid = $user->uid;
}
break;
default:
parent::setTargetElement($source, $target_node, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$type = node_type_get_type($this->config['content_type']);
$targets = parent::getMappingTargets();
if ($type->has_title) {
$targets['title'] = array(
'name' => t('Title'),
'description' => t('The title of the node.'),
'optional_unique' => TRUE,
);
}
$targets['nid'] = array(
'name' => t('Node ID'),
'description' => t('The nid of the node. NOTE: use this feature with care, node ids are usually assigned by Drupal.'),
'optional_unique' => TRUE,
);
$targets['uid'] = array(
'name' => t('User ID'),
'description' => t('The Drupal user ID of the node author.'),
);
$targets['user_name'] = array(
'name' => t('Username'),
'description' => t('The Drupal username of the node author.'),
);
$targets['user_mail'] = array(
'name' => t('User email'),
'description' => t('The email address of the node author.'),
);
$targets['status'] = array(
'name' => t('Published status'),
'description' => t('Whether a node is published or not. 1 stands for published, 0 for not published.'),
);
$targets['created'] = array(
'name' => t('Published date'),
'description' => t('The UNIX time when a node has been published.'),
);
$targets['promote'] = array(
'name' => t('Promoted to front page'),
'description' => t('Boolean value, whether or not node is promoted to front page. (1 = promoted, 0 = not promoted)'),
);
$targets['sticky'] = array(
'name' => t('Sticky'),
'description' => t('Boolean value, whether or not node is sticky at top of lists. (1 = sticky, 0 = not sticky)'),
);
// Include language field if Locale module is enabled.
if (module_exists('locale')) {
$targets['language'] = array(
'name' => t('Language'),
'description' => t('The two-character language code of the node.'),
);
}
// Include comment field if Comment module is enabled.
if (module_exists('comment')) {
$targets['comment'] = array(
'name' => t('Comments'),
'description' => t('Whether comments are allowed on this node: 0 = no, 1 = read only, 2 = read/write.'),
);
}
// If the target content type is a Feed node, expose its source field.
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$name = feeds_importer($id)->config['name'];
$targets['feeds_source'] = array(
'name' => t('Feed source'),
'description' => t('The content type created by this processor is a Feed Node, it represents a source itself. Depending on the fetcher selected on the importer "@importer", this field is expected to be for example a URL or a path to a file.', array('@importer' => $name)),
'optional_unique' => TRUE,
);
}
// Let other modules expose mapping targets.
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->config['content_type'];
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
return $targets;
}
/**
* Get nid of an existing feed item node if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($nid = parent::existingEntityId($source, $result)) {
return $nid;
}
// Iterate through all unique targets and test whether they do already
// exist in the database.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'nid':
$nid = db_query("SELECT nid FROM {node} WHERE nid = :nid", array(':nid' => $value))->fetchField();
break;
case 'title':
$nid = db_query("SELECT nid FROM {node} WHERE title = :title AND type = :type", array(':title' => $value, ':type' => $this->config['content_type']))->fetchField();
break;
case 'feeds_source':
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$nid = db_query("SELECT fs.feed_nid FROM {node} n JOIN {feeds_source} fs ON n.nid = fs.feed_nid WHERE fs.id = :id AND fs.source = :source", array(':id' => $id, ':source' => $value))->fetchField();
}
break;
}
if ($nid) {
// Return with the first nid found.
return $nid;
}
}
return 0;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @file
* OPML Parser plugin.
*/
/**
* Feeds parser plugin that parses OPML feeds.
*/
class FeedsOPMLParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_library('opml_parser.inc', 'opml_parser');
$opml = opml_parser_parse($fetcher_result->getRaw());
$result = new FeedsParserResult($opml['items']);
$result->title = $opml['title'];
return $result;
}
/**
* Return mapping sources.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Feed title'),
'description' => t('Title of the feed.'),
),
'xmlurl' => array(
'name' => t('Feed URL'),
'description' => t('URL of the feed.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,757 @@
<?php
/**
* @file
* Contains FeedsParser and related classes.
*/
/**
* A result of a parsing stage.
*/
class FeedsParserResult extends FeedsResult {
public $title;
public $description;
public $link;
public $items;
public $current_item;
/**
* Constructor.
*/
public function __construct($items = array()) {
$this->title = '';
$this->description = '';
$this->link = '';
$this->items = $items;
}
/**
* @todo Move to a nextItem() based approach, not consuming the item array.
* Can only be done once we don't cache the entire batch object between page
* loads for batching anymore.
*
* @return
* Next available item or NULL if there is none. Every returned item is
* removed from the internal array.
*/
public function shiftItem() {
$this->current_item = array_shift($this->items);
return $this->current_item;
}
/**
* @return
* Current result item.
*/
public function currentItem() {
return empty($this->current_item) ? NULL : $this->current_item;
}
}
/**
* Abstract class, defines interface for parsers.
*/
abstract class FeedsParser extends FeedsPlugin {
/**
* Parse content fetched by fetcher.
*
* Extending classes must implement this method.
*
* @param FeedsSource $source
* Source information.
* @param $fetcher_result
* FeedsFetcherResult returned by fetcher.
*/
public abstract function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result);
/**
* Clear all caches for results for given source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
*/
public function clear(FeedsSource $source) {}
/**
* Declare the possible mapping sources that this parser produces.
*
* @ingroup mappingapi
*
* @return
* An array of mapping sources, or FALSE if the sources can be defined by
* typing a value in a text field.
*
* Example:
* @code
* array(
* 'title' => t('Title'),
* 'created' => t('Published date'),
* 'url' => t('Feed item URL'),
* 'guid' => t('Feed item GUID'),
* )
* @endcode
*/
public function getMappingSources() {
self::loadMappers();
$sources = array();
$content_type = feeds_importer($this->id)->config['content_type'];
drupal_alter('feeds_parser_sources', $sources, $content_type);
if (!feeds_importer($this->id)->config['content_type']) {
return $sources;
}
$sources['parent:uid'] = array(
'name' => t('Feed node: User ID'),
'description' => t('The feed node author uid.'),
);
$sources['parent:nid'] = array(
'name' => t('Feed node: Node ID'),
'description' => t('The feed node nid.'),
);
return $sources;
}
/**
* Get an element identified by $element_key of the given item.
* The element key corresponds to the values in the array returned by
* FeedsParser::getMappingSources().
*
* This method is invoked from FeedsProcessor::map() when a concrete item is
* processed.
*
* @ingroup mappingapi
*
* @param $batch
* FeedsImportBatch object containing the sources to be mapped from.
* @param $element_key
* The key identifying the element that should be retrieved from $source
*
* @return
* The source element from $item identified by $element_key.
*
* @see FeedsProcessor::map()
* @see FeedsCSVParser::getSourceElement()
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
switch ($element_key) {
case 'parent:uid':
if ($source->feed_nid && $node = node_load($source->feed_nid)) {
return $node->uid;
}
break;
case 'parent:nid':
return $source->feed_nid;
}
$item = $result->currentItem();
return isset($item[$element_key]) ? $item[$element_key] : '';
}
}
/**
* Defines an element of a parsed result. Such an element can be a simple type,
* a complex type (derived from FeedsElement) or an array of either.
*
* @see FeedsEnclosure
*/
class FeedsElement {
// The standard value of this element. This value can contain be a simple type,
// a FeedsElement or an array of either.
protected $value;
/**
* Constructor.
*/
public function __construct($value) {
$this->value = $value;
}
/**
* @todo Make value public and deprecate use of getValue().
*
* @return
* Value of this FeedsElement represented as a scalar.
*/
public function getValue() {
return $this->value;
}
/**
* Magic method __toString() for printing and string conversion of this
* object.
*
* @return
* A string representation of this element.
*/
public function __toString() {
if (is_array($this->value)) {
return 'Array';
}
if (is_object($this->value)) {
return 'Object';
}
return (string) $this->getValue();
}
}
/**
* Encapsulates a taxonomy style term object.
*
* Objects of this class can be turned into a taxonomy term style arrays by
* casting them.
*
* @code
* $term_object = new FeedsTermElement($term_array);
* $term_array = (array)$term_object;
* @endcode
*/
class FeedsTermElement extends FeedsElement {
public $tid, $vid, $name;
/**
* @param $term
* An array or a stdClass object that is a Drupal taxonomy term.
*/
public function __construct($term) {
if (is_array($term)) {
parent::__construct($term['name']);
foreach ($this as $key => $value) {
$this->$key = isset($term[$key]) ? $term[$key] : NULL;
}
}
elseif (is_object($term)) {
parent::__construct($term->name);
foreach ($this as $key => $value) {
$this->$key = isset($term->$key) ? $term->$key : NULL;
}
}
}
/**
* Use $name as $value.
*/
public function getValue() {
return $this->name;
}
}
/**
* A geo term element.
*/
class FeedsGeoTermElement extends FeedsTermElement {
public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
/**
* @param $term
* An array or a stdClass object that is a Drupal taxonomy term. Can include
* geo extensions.
*/
public function __construct($term) {
parent::__construct($term);
}
}
/**
* Enclosure element, can be part of the result array.
*/
class FeedsEnclosure extends FeedsElement {
protected $mime_type;
/**
* Constructor, requires MIME type.
*
* @param $value
* A path to a local file or a URL to a remote document.
* @param $mimetype
* The mime type of the resource.
*/
public function __construct($value, $mime_type) {
parent::__construct($value);
$this->mime_type = $mime_type;
}
/**
* @return
* MIME type of return value of getValue().
*/
public function getMIMEType() {
return $this->mime_type;
}
/**
* Use this method instead of FeedsElement::getValue() when fetching the file
* from the URL.
*
* @return
* Value with encoded space characters to safely fetch the file from the URL.
*
* @see FeedsElement::getValue()
*/
public function getUrlEncodedValue() {
return str_replace(' ', '%20', $this->getValue());
}
/**
* Use this method instead of FeedsElement::getValue() to get the file name
* transformed for better local saving (underscores instead of spaces)
*
* @return
* Value with space characters changed to underscores.
*
* @see FeedsElement::getValue()
*/
public function getLocalValue() {
return str_replace(' ', '_', $this->getValue());
}
/**
* @return
* The content of the referenced resource.
*/
public function getContent() {
feeds_include_library('http_request.inc', 'http_request');
$result = http_request_get($this->getUrlEncodedValue());
if ($result->code != 200) {
throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->getUrlEncodedValue(), '!code' => $result->code)));
}
return $result->data;
}
/**
* Get a Drupal file object of the enclosed resource, download if necessary.
*
* @param $destination
* The path or uri specifying the target directory in which the file is
* expected. Don't use trailing slashes unless it's a streamwrapper scheme.
*
* @return
* A Drupal temporary file object of the enclosed resource.
*
* @throws Exception
* If file object could not be created.
*/
public function getFile($destination) {
if ($this->getValue()) {
// Prepare destination directory.
file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
// Copy or save file depending on whether it is remote or local.
if (drupal_realpath($this->getValue())) {
$file = new stdClass();
$file->uid = 0;
$file->uri = $this->getValue();
$file->filemime = $this->mime_type;
$file->filename = basename($file->uri);
if (dirname($file->uri) != $destination) {
$file = file_copy($file, $destination);
}
else {
// If file is not to be copied, check whether file already exists,
// as file_save() won't do that for us (compare file_copy() and
// file_save())
$existing_files = file_load_multiple(array(), array('uri' => $file->uri));
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->fid;
$file->filename = $existing->filename;
}
file_save($file);
}
}
else {
$filename = basename($this->getLocalValue());
if (module_exists('transliteration')) {
require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
$filename = transliteration_clean_filename($filename);
}
if (file_uri_target($destination)) {
$destination = trim($destination, '/') . '/';
}
try {
$file = file_save_data($this->getContent(), $destination . $filename);
}
catch (Exception $e) {
watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
}
}
// We couldn't make sense of this enclosure, throw an exception.
if (!$file) {
throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
}
}
return $file;
}
}
/**
* Defines a date element of a parsed result (including ranges, repeat).
*/
class FeedsDateTimeElement extends FeedsElement {
// Start date and end date.
public $start;
public $end;
/**
* Constructor.
*
* @param $start
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $end
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $tz
* A PHP DateTimeZone object.
*/
public function __construct($start = NULL, $end = NULL, $tz = NULL) {
$this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
$this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
}
/**
* Override FeedsElement::getValue().
*
* @return
* The UNIX timestamp of this object's start date. Return value is
* technically a string but will only contain numeric values.
*/
public function getValue() {
if ($this->start) {
return $this->start->format('U');
}
return '0';
}
/**
* Merge this field with another. Most stuff goes down when merging the two
* sub-dates.
*
* @see FeedsDateTime
*/
public function merge(FeedsDateTimeElement $other) {
$this2 = clone $this;
if ($this->start && $other->start) {
$this2->start = $this->start->merge($other->start);
}
elseif ($other->start) {
$this2->start = clone $other->start;
}
elseif ($this->start) {
$this2->start = clone $this->start;
}
if ($this->end && $other->end) {
$this2->end = $this->end->merge($other->end);
}
elseif ($other->end) {
$this2->end = clone $other->end;
}
elseif ($this->end) {
$this2->end = clone $this->end;
}
return $this2;
}
/**
* Helper method for buildDateField(). Build a FeedsDateTimeElement object
* from a standard formatted node.
*/
protected static function readDateField($entity, $field_name) {
$ret = new FeedsDateTimeElement();
if (isset($entity->{$field_name}['und'][0]['date']) && $entity->{$field_name}['und'][0]['date'] instanceof FeedsDateTime) {
$ret->start = $entity->{$field_name}['und'][0]['date'];
}
if (isset($entity->{$field_name}['und'][0]['date2']) && $entity->{$field_name}['und'][0]['date2'] instanceof FeedsDateTime) {
$ret->end = $entity->{$field_name}['und'][0]['date2'];
}
return $ret;
}
/**
* Build a entity's date field from our object.
*
* @param $entity
* The entity to build the date field on.
* @param $field_name
* The name of the field to build.
*/
public function buildDateField($entity, $field_name) {
$info = field_info_field($field_name);
$oldfield = FeedsDateTimeElement::readDateField($entity, $field_name);
// Merge with any preexisting objects on the field; we take precedence.
$oldfield = $this->merge($oldfield);
$use_start = $oldfield->start;
$use_end = $oldfield->end;
// Set timezone if not already in the FeedsDateTime object
$to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
$temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
$db_tz = '';
if ($use_start) {
$use_start = $use_start->merge($temp);
if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
$use_start->setTimezone(new DateTimeZone("UTC"));
}
$db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
}
if ($use_end) {
$use_end = $use_end->merge($temp);
if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
$use_end->setTimezone(new DateTimeZone("UTC"));
}
if (!$db_tz) {
$db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
}
}
if (!$db_tz) {
return;
}
$db_tz = new DateTimeZone($db_tz);
if (!isset($entity->{$field_name})) {
$entity->{$field_name} = array('und' => array());
}
if ($use_start) {
$entity->{$field_name}['und'][0]['timezone'] = $use_start->getTimezone()->getName();
$entity->{$field_name}['und'][0]['offset'] = $use_start->getOffset();
$use_start->setTimezone($db_tz);
$entity->{$field_name}['und'][0]['date'] = $use_start;
/**
* @todo the date_type_format line could be simplified based upon a patch
* DO issue #259308 could affect this, follow up on at some point.
* Without this, all granularity info is lost.
* $use_start->format(date_type_format($field['type'], $use_start->granularity));
*/
$entity->{$field_name}['und'][0]['value'] = $use_start->format(date_type_format($info['type']));
}
if ($use_end) {
// Don't ever use end to set timezone (for now)
$entity->{$field_name}['und'][0]['offset2'] = $use_end->getOffset();
$use_end->setTimezone($db_tz);
$entity->{$field_name}['und'][0]['date2'] = $use_end;
$entity->{$field_name}['und'][0]['value2'] = $use_end->format(date_type_format($info['type']));
}
}
}
/**
* Extend PHP DateTime class with granularity handling, merge functionality and
* slightly more flexible initialization parameters.
*
* This class is a Drupal independent extension of the >= PHP 5.2 DateTime
* class.
*
* @see FeedsDateTimeElement
*/
class FeedsDateTime extends DateTime {
public $granularity = array();
protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
private $_serialized_time;
private $_serialized_timezone;
/**
* Helper function to prepare the object during serialization.
*
* We are extending a core class and core classes cannot be serialized.
*
* Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
*/
public function __sleep() {
$this->_serialized_time = $this->format('c');
$this->_serialized_timezone = $this->getTimezone()->getName();
return array('_serialized_time', '_serialized_timezone');
}
/**
* Upon unserializing, we must re-build ourselves using local variables.
*/
public function __wakeup() {
$this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
}
/**
* Overridden constructor.
*
* @param $time
* time string, flexible format including timestamp. Invalid formats will
* fall back to 'now'.
* @param $tz
* PHP DateTimeZone object, NULL allowed
*/
public function __construct($time = '', $tz = NULL) {
// Assume UNIX timestamp if numeric.
if (is_numeric($time)) {
// Make sure it's not a simple year
if ((is_string($time) && strlen($time) > 4) || is_int($time)) {
$time = "@" . $time;
}
}
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
$time = str_replace("GMT-", "-", $time);
$time = str_replace("GMT+", "+", $time);
// Some PHP 5.2 version's DateTime class chokes on invalid dates.
if (!strtotime($time)) {
$time = 'now';
}
// Create and set time zone separately, PHP 5.2.6 does not respect time zone
// argument in __construct().
parent::__construct($time);
$tz = $tz ? $tz : new DateTimeZone("UTC");
$this->setTimeZone($tz);
// Verify that timezone has not been specified as an offset.
if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
$this->setTimezone(new DateTimeZone("UTC"));
}
// Finally set granularity.
$this->setGranularityFromTime($time, $tz);
}
/**
* This function will keep this object's values by default.
*/
public function merge(FeedsDateTime $other) {
$other_tz = $other->getTimezone();
$this_tz = $this->getTimezone();
// Figure out which timezone to use for combination.
$use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
$this2 = clone $this;
$this2->setTimezone($use_tz);
$other->setTimezone($use_tz);
$val = $this2->toArray();
$otherval = $other->toArray();
foreach (self::$allgranularity as $g) {
if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
// The other class has a property we don't; steal it.
$this2->addGranularity($g);
$val[$g] = $otherval[$g];
}
}
$other->setTimezone($other_tz);
$this2->setDate($val['year'], $val['month'], $val['day']);
$this2->setTime($val['hour'], $val['minute'], $val['second']);
return $this2;
}
/**
* Overrides default DateTime function. Only changes output values if
* actually had time granularity. This should be used as a "converter" for
* output, to switch tzs.
*
* In order to set a timezone for a datetime that doesn't have such
* granularity, merge() it with one that does.
*/
public function setTimezone($tz, $force = FALSE) {
// PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
// http://bugs.php.net/bug.php?id=45038
if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
$tz = new DateTimeZone($tz->getName());
}
if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
// this has no time or timezone granularity, so timezone doesn't mean much
// We set the timezone using the method, which will change the day/hour, but then we switch back
$arr = $this->toArray();
parent::setTimezone($tz);
$this->setDate($arr['year'], $arr['month'], $arr['day']);
$this->setTime($arr['hour'], $arr['minute'], $arr['second']);
return;
}
parent::setTimezone($tz);
}
/**
* Safely adds a granularity entry to the array.
*/
public function addGranularity($g) {
$this->granularity[] = $g;
$this->granularity = array_unique($this->granularity);
}
/**
* Removes a granularity entry from the array.
*/
public function removeGranularity($g) {
if ($key = array_search($g, $this->granularity)) {
unset($this->granularity[$key]);
}
}
/**
* Checks granularity array for a given entry.
*/
public function hasGranularity($g) {
return in_array($g, $this->granularity);
}
/**
* Returns whether this object has time set. Used primarily for timezone
* conversion and fomratting.
*
* @todo currently very simplistic, but effective, see usage
*/
public function hasTime() {
return $this->hasGranularity('hour');
}
/**
* Protected function to find the granularity given by the arguments to the
* constructor.
*/
protected function setGranularityFromTime($time, $tz) {
$this->granularity = array();
$temp = date_parse($time);
// This PHP method currently doesn't have resolution down to seconds, so if
// there is some time, all will be set.
foreach (self::$allgranularity AS $g) {
if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
$this->granularity[] = $g;
}
}
if ($tz) {
$this->addGranularity('zone');
}
}
/**
* Helper to return all standard date parts in an array.
*/
protected function toArray() {
return array('year' => $this->format('Y'), 'month' => $this->format('m'), 'day' => $this->format('d'), 'hour' => $this->format('H'), 'minute' => $this->format('i'), 'second' => $this->format('s'), 'zone' => $this->format('e'));
}
}
/**
* Converts to UNIX time.
*
* @param $date
* A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
* @param $default_value
* A default UNIX timestamp to return if $date could not be parsed.
*
* @return
* $date as UNIX time if conversion was successful, $dfeault_value otherwise.
*/
function feeds_to_unixtime($date, $default_value) {
if (is_numeric($date)) {
return $date;
}
elseif (is_string($date) && !empty($date)) {
$date = new FeedsDateTimeElement($date);
return $date->getValue();
}
elseif ($date instanceof FeedsDateTimeElement) {
return $date->getValue();
}
return $default_value;
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* @file
* Definition of FeedsPlugin class.
*/
/**
* Base class for a fetcher, parser or processor result.
*/
class FeedsResult {}
/**
* Implement source interface for all plugins.
*
* Note how this class does not attempt to store source information locally.
* Doing this would break the model where source information is represented by
* an object that is being passed into a Feed object and its plugins.
*/
abstract class FeedsPlugin extends FeedsConfigurable implements FeedsSourceInterface {
/**
* Constructor.
*
* Initialize class variables.
*/
protected function __construct($id) {
parent::__construct($id);
$this->source_config = $this->sourceDefaults();
}
/**
* Save changes to the configuration of this object.
* Delegate saving to parent (= Feed) which will collect
* information from this object by way of getConfig() and store it.
*/
public function save() {
feeds_importer($this->id)->save();
}
/**
* Returns TRUE if $this->sourceForm() returns a form.
*/
public function hasSourceConfig() {
$form = $this->sourceForm(array());
return !empty($form);
}
/**
* Implements FeedsSourceInterface::sourceDefaults().
*/
public function sourceDefaults() {
$values = array_flip(array_keys($this->sourceForm(array())));
foreach ($values as $k => $v) {
$values[$k] = '';
}
return $values;
}
/**
* Callback methods, exposes source form.
*/
public function sourceForm($source_config) {
return array();
}
/**
* Validation handler for 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) {}
/**
* Loads on-behalf implementations from mappers/ directory.
*
* FeedsProcessor::map() does not load from mappers/ as only node and user
* processor ship with on-behalf implementations.
*
* @see FeedsNodeProcessor::map()
* @see FeedsUserProcessor::map()
*
* @todo: Use CTools Plugin API.
*/
protected static function loadMappers() {
static $loaded = FALSE;
if (!$loaded) {
$path = drupal_get_path('module', 'feeds') . '/mappers';
$files = drupal_system_listing('/.*\.inc$/', $path, 'name', 0);
foreach ($files as $file) {
if (strstr($file->uri, '/mappers/')) {
require_once(DRUPAL_ROOT . '/' . $file->uri);
}
}
}
$loaded = TRUE;
}
/**
* Get all available plugins.
*/
public static function all() {
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
$result = array();
foreach ($plugins as $key => $info) {
if (!empty($info['hidden'])) {
continue;
}
$result[$key] = $info;
}
// Sort plugins by name and return.
uasort($result, 'feeds_plugin_compare');
return $result;
}
/**
* Determines whether given plugin is derived from given base plugin.
*
* @param $plugin_key
* String that identifies a Feeds plugin key.
* @param $parent_plugin
* String that identifies a Feeds plugin key to be tested against.
*
* @return
* TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
* FALSE otherwise.
*/
public static function child($plugin_key, $parent_plugin) {
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
$info = $plugins[$plugin_key];
if (empty($info['handler']['parent'])) {
return FALSE;
}
elseif ($info['handler']['parent'] == $parent_plugin) {
return TRUE;
}
else {
return self::child($info['handler']['parent'], $parent_plugin);
}
}
/**
* Determines the type of a plugin.
*
* @todo PHP5.3: Implement self::type() and query with $plugin_key::type().
*
* @param $plugin_key
* String that identifies a Feeds plugin key.
*
* @return
* One of the following values:
* 'fetcher' if the plugin is a fetcher
* 'parser' if the plugin is a parser
* 'processor' if the plugin is a processor
* FALSE otherwise.
*/
public static function typeOf($plugin_key) {
if (self::child($plugin_key, 'FeedsFetcher')) {
return 'fetcher';
}
elseif (self::child($plugin_key, 'FeedsParser')) {
return 'parser';
}
elseif (self::child($plugin_key, 'FeedsProcessor')) {
return 'processor';
}
return FALSE;
}
/**
* Gets all available plugins of a particular type.
*
* @param $type
* 'fetcher', 'parser' or 'processor'
*/
public static function byType($type) {
$plugins = self::all();
$result = array();
foreach ($plugins as $key => $info) {
if ($type == self::typeOf($key)) {
$result[$key] = $info;
}
}
return $result;
}
}
/**
* Used when a plugin is missing.
*/
class FeedsMissingPlugin extends FeedsPlugin {
public function menuItem() {
return array();
}
}
/**
* Sort callback for FeedsPlugin::all().
*/
function feeds_plugin_compare($a, $b) {
return strcasecmp($a['name'], $b['name']);
}

View File

@@ -0,0 +1,705 @@
<?php
/**
* @file
* Contains FeedsProcessor and related classes.
*/
// Update mode for existing items.
define('FEEDS_SKIP_EXISTING', 0);
define('FEEDS_REPLACE_EXISTING', 1);
define('FEEDS_UPDATE_EXISTING', 2);
// Default limit for creating items on a page load, not respected by all
// processors.
define('FEEDS_PROCESS_LIMIT', 50);
/**
* Thrown if a validation fails.
*/
class FeedsValidationException extends Exception {}
/**
* Thrown if a an access check fails.
*/
class FeedsAccessException extends Exception {}
/**
* Abstract class, defines interface for processors.
*/
abstract class FeedsProcessor extends FeedsPlugin {
/**
* @defgroup entity_api_wrapper Entity API wrapper.
*/
/**
* Entity type this processor operates on.
*/
public abstract function entityType();
/**
* Create a new entity.
*
* @param $source
* The feeds source that spawns this entity.
*
* @return
* A new entity object.
*/
protected abstract function newEntity(FeedsSource $source);
/**
* Load an existing entity.
*
* @param $source
* The feeds source that spawns this entity.
* @param $entity_id
* The unique id of the entity that should be loaded.
*
* @return
* A new entity object.
*/
protected abstract function entityLoad(FeedsSource $source, $entity_id);
/**
* Validate an entity.
*
* @throws FeedsValidationException $e
* If validation fails.
*/
protected function entityValidate($entity) {}
/**
* Access check for saving an enity.
*
* @param $entity
* Entity to be saved.
*
* @throws FeedsAccessException $e
* If the access check fails.
*/
protected function entitySaveAccess($entity) {}
/**
* Save an entity.
*
* @param $entity
* Entity to be saved.
*/
protected abstract function entitySave($entity);
/**
* Delete a series of entities.
*
* @param $entity_ids
* Array of unique identity ids to be deleted.
*/
protected abstract function entityDeleteMultiple($entity_ids);
/**
* Wrap entity_get_info() into a method so that extending classes can override
* it and more entity information. Allowed additional keys:
*
* 'label plural' ... the plural label of an entity type.
*/
protected function entityInfo() {
return entity_get_info($this->entityType());
}
/**
* @}
*/
/**
* Process the result of the parsing stage.
*
* @param FeedsSource $source
* Source information about this import.
* @param FeedsParserResult $parser_result
* The result of the parsing stage.
*/
public function process(FeedsSource $source, FeedsParserResult $parser_result) {
$state = $source->state(FEEDS_PROCESS);
while ($item = $parser_result->shiftItem()) {
// Check if this item already exists.
$entity_id = $this->existingEntityId($source, $parser_result);
$skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
// If it exists, and we are not updating, pass onto the next item.
if ($entity_id && $skip_existing) {
continue;
}
$hash = $this->hash($item);
$changed = ($hash !== $this->getHash($entity_id));
$force_update = $this->config['skip_hash_check'];
// Do not proceed if the item exists, has not changed, and we're not
// forcing the update.
if ($entity_id && !$changed && !$force_update) {
continue;
}
try {
// Build a new entity.
if (empty($entity_id)) {
$entity = $this->newEntity($source);
$this->newItemInfo($entity, $source->feed_nid, $hash);
}
// Load an existing entity.
else {
$entity = $this->entityLoad($source, $entity_id);
// The feeds_item table is always updated with the info for the most recently processed entity.
// The only carryover is the entity_id.
$this->newItemInfo($entity, $source->feed_nid, $hash);
$entity->feeds_item->entity_id = $entity_id;
}
// Set property and field values.
$this->map($source, $parser_result, $entity);
$this->entityValidate($entity);
// Allow modules to alter the entity before saving.
module_invoke_all('feeds_presave', $source, $entity, $item);
if (module_exists('rules')) {
rules_invoke_event('feeds_import_'. $source->importer()->id, $entity);
}
// Enable modules to skip saving at all.
if (!empty($entity->feeds_item->skip)) {
continue;
}
// This will throw an exception on failure.
$this->entitySaveAccess($entity);
$this->entitySave($entity);
// Track progress.
if (empty($entity_id)) {
$state->created++;
}
else {
$state->updated++;
}
}
// Something bad happened, log it.
catch (Exception $e) {
$state->failed++;
drupal_set_message($e->getMessage(), 'warning');
$message = $e->getMessage();
$message .= '<h3>Original item</h3>';
$message .= '<pre>' . var_export($item, TRUE) . '</pre>';
$message .= '<h3>Entity</h3>';
$message .= '<pre>' . var_export($entity, TRUE) . '</pre>';
$source->log('import', $message, array(), WATCHDOG_ERROR);
}
}
// Set messages if we're done.
if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
return;
}
$info = $this->entityInfo();
$tokens = array(
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
);
$messages = array();
if ($state->created) {
$messages[] = array(
'message' => format_plural(
$state->created,
'Created @number @entity.',
'Created @number @entities.',
array('@number' => $state->created) + $tokens
),
);
}
if ($state->updated) {
$messages[] = array(
'message' => format_plural(
$state->updated,
'Updated @number @entity.',
'Updated @number @entities.',
array('@number' => $state->updated) + $tokens
),
);
}
if ($state->failed) {
$messages[] = array(
'message' => format_plural(
$state->failed,
'Failed importing @number @entity.',
'Failed importing @number @entities.',
array('@number' => $state->failed) + $tokens
),
'level' => WATCHDOG_ERROR,
);
}
if (empty($messages)) {
$messages[] = array(
'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
);
}
foreach ($messages as $message) {
drupal_set_message($message['message']);
$source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
}
}
/**
* Remove all stored results or stored results up to a certain time for a
* source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers should only delete items
* pertaining to this source. The preferred way of determining whether an
* item pertains to a certain souce is by using $source->feed_nid. It is the
* processor's responsibility to store the feed_nid of an imported item in
* the processing stage.
*/
public function clear(FeedsSource $source) {
$state = $source->state(FEEDS_PROCESS_CLEAR);
// Build base select statement.
$info = $this->entityInfo();
$select = db_select($info['base table'], 'e');
$select->addField('e', $info['entity keys']['id'], 'entity_id');
$select->join(
'feeds_item',
'fi',
"e.{$info['entity keys']['id']} = fi.entity_id AND fi.entity_type = '{$this->entityType()}'");
$select->condition('fi.id', $this->id);
$select->condition('fi.feed_nid', $source->feed_nid);
// If there is no total, query it.
if (!$state->total) {
$state->total = $select->countQuery()
->execute()
->fetchField();
}
// Delete a batch of entities.
$entities = $select->range(0, $this->getLimit())->execute();
$entity_ids = array();
foreach ($entities as $entity) {
$entity_ids[$entity->entity_id] = $entity->entity_id;
}
$this->entityDeleteMultiple($entity_ids);
// Report progress, take into account that we may not have deleted as
// many items as we have counted at first.
if (count($entity_ids)) {
$state->deleted += count($entity_ids);
$state->progress($state->total, $state->deleted);
}
else {
$state->progress($state->total, $state->total);
}
// Report results when done.
if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
if ($state->deleted) {
$message = format_plural(
$state->deleted,
'Deleted @number @entity',
'Deleted @number @entities',
array(
'@number' => $state->deleted,
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
)
);
$source->log('clear', $message, array(), WATCHDOG_INFO);
drupal_set_message($message);
}
else {
drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
}
}
}
/*
* Report number of items that can be processed per call.
*
* 0 means 'unlimited'.
*
* If a number other than 0 is given, Feeds parsers that support batching
* will only deliver this limit to the processor.
*
* @see FeedsSource::getLimit()
* @see FeedsCSVParser::parse()
*/
public function getLimit() {
return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
}
/**
* Delete feed items younger than now - $time. Do not invoke expire on a
* processor directly, but use FeedsImporter::expire() instead.
*
* @see FeedsImporter::expire().
* @see FeedsDataProcessor::expire().
*
* @param $time
* If implemented, all items produced by this configuration that are older
* than REQUEST_TIME - $time should be deleted.
* If $time === NULL processor should use internal configuration.
*
* @return
* FEEDS_BATCH_COMPLETE if all items have been processed, a float between 0
* and 0.99* indicating progress otherwise.
*/
public function expire($time = NULL) {
return FEEDS_BATCH_COMPLETE;
}
/**
* Counts the number of items imported by this processor.
*/
public function itemCount(FeedsSource $source) {
return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(':id' => $this->id, ':entity_type' => $this->entityType(), ':feed_nid' => $source->feed_nid))->fetchField();
}
/**
* Execute mapping on an item.
*
* This method encapsulates the central mapping functionality. When an item is
* processed, it is passed through map() where the properties of $source_item
* are mapped onto $target_item following the processor's mapping
* configuration.
*
* For each mapping FeedsParser::getSourceElement() is executed to retrieve
* the source element, then FeedsProcessor::setTargetElement() is invoked
* to populate the target item properly. Alternatively a
* hook_x_targets_alter() may have specified a callback for a mapping target
* in which case the callback is asked to populate the target item instead of
* FeedsProcessor::setTargetElement().
*
* @ingroup mappingapi
*
* @see hook_feeds_parser_sources_alter()
* @see hook_feeds_data_processor_targets_alter()
* @see hook_feeds_node_processor_targets_alter()
* @see hook_feeds_term_processor_targets_alter()
* @see hook_feeds_user_processor_targets_alter()
*/
protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
// Static cache $targets as getMappingTargets() may be an expensive method.
static $sources;
if (!isset($sources[$this->id])) {
$sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
}
static $targets;
if (!isset($targets[$this->id])) {
$targets[$this->id] = $this->getMappingTargets();
}
$parser = feeds_importer($this->id)->parser;
if (empty($target_item)) {
$target_item = array();
}
// Many mappers add to existing fields rather than replacing them. Hence we
// need to clear target elements of each item before mapping in case we are
// mapping on a prepopulated item such as an existing node.
foreach ($this->config['mappings'] as $mapping) {
if (isset($targets[$this->id][$mapping['target']]['real_target'])) {
unset($target_item->{$targets[$this->id][$mapping['target']]['real_target']});
}
elseif (isset($target_item->{$mapping['target']})) {
unset($target_item->{$mapping['target']});
}
}
/*
This is where the actual mapping happens: For every mapping we envoke
the parser's getSourceElement() method to retrieve the value of the source
element and pass it to the processor's setTargetElement() to stick it
on the right place of the target item.
If the mapping specifies a callback method, use the callback instead of
setTargetElement().
*/
self::loadMappers();
foreach ($this->config['mappings'] as $mapping) {
// Retrieve source element's value from parser.
if (isset($sources[$this->id][$mapping['source']]) &&
is_array($sources[$this->id][$mapping['source']]) &&
isset($sources[$this->id][$mapping['source']]['callback']) &&
function_exists($sources[$this->id][$mapping['source']]['callback'])) {
$callback = $sources[$this->id][$mapping['source']]['callback'];
$value = $callback($source, $result, $mapping['source']);
}
else {
$value = $parser->getSourceElement($source, $result, $mapping['source']);
}
// Map the source element's value to the target.
if (isset($targets[$this->id][$mapping['target']]) &&
is_array($targets[$this->id][$mapping['target']]) &&
isset($targets[$this->id][$mapping['target']]['callback']) &&
function_exists($targets[$this->id][$mapping['target']]['callback'])) {
$callback = $targets[$this->id][$mapping['target']]['callback'];
$callback($source, $target_item, $mapping['target'], $value, $mapping);
}
else {
$this->setTargetElement($source, $target_item, $mapping['target'], $value, $mapping);
}
}
return $target_item;
}
/**
* Per default, don't support expiry. If processor supports expiry of imported
* items, return the time after which items should be removed.
*/
public function expiryTime() {
return FEEDS_EXPIRE_NEVER;
}
/**
* Declare default configuration.
*/
public function configDefaults() {
return array(
'mappings' => array(),
'update_existing' => FEEDS_SKIP_EXISTING,
'input_format' => NULL,
'skip_hash_check' => FALSE,
);
}
/**
* Overrides parent::configForm().
*/
public function configForm(&$form_state) {
$info = $this->entityInfo();
$form = array();
$tokens = array('@entities' => strtolower($info['label plural']));
$form['update_existing'] = array(
'#type' => 'radios',
'#title' => t('Update existing @entities', $tokens),
'#description' =>
t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
'#options' => array(
FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
),
'#default_value' => $this->config['update_existing'],
);
global $user;
$formats = filter_formats($user);
foreach ($formats as $format) {
$format_options[$format->format] = $format->name;
}
$form['skip_hash_check'] = array(
'#type' => 'checkbox',
'#title' => t('Skip hash check'),
'#description' => t('Force update of items even if item source data did not change.'),
'#default_value' => $this->config['skip_hash_check'],
);
$form['input_format'] = array(
'#type' => 'select',
'#title' => t('Text format'),
'#description' => t('Select the input format for the body field of the nodes to be created.'),
'#options' => $format_options,
'#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
'#required' => TRUE,
);
return $form;
}
/**
* Get mappings.
*/
public function getMappings() {
return isset($this->config['mappings']) ? $this->config['mappings'] : array();
}
/**
* Declare possible mapping targets that this processor exposes.
*
* @ingroup mappingapi
*
* @return
* An array of mapping targets. Keys are paths to targets
* separated by ->, values are TRUE if target can be unique,
* FALSE otherwise.
*/
public function getMappingTargets() {
return array(
'url' => array(
'name' => t('URL'),
'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
'guid' => array(
'name' => t('GUID'),
'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
);
}
/**
* Set a concrete target element. Invoked from FeedsProcessor::map().
*
* @ingroup mappingapi
*/
public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
switch ($target_element) {
case 'url':
case 'guid':
$target_item->feeds_item->$target_element = $value;
break;
default:
$target_item->$target_element = $value;
break;
}
}
/**
* Retrieve the target entity's existing id if available. Otherwise return 0.
*
* @ingroup mappingapi
*
* @param FeedsSource $source
* The source information about this import.
* @param $result
* A FeedsParserResult object.
*
* @return
* The serial id of an entity if found, 0 otherwise.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
$query = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('feed_nid', $source->feed_nid)
->condition('entity_type', $this->entityType())
->condition('id', $source->id);
// Iterate through all unique targets and test whether they do already
// exist in the database.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'url':
$entity_id = $query->condition('url', $value)->execute()->fetchField();
break;
case 'guid':
$entity_id = $query->condition('guid', $value)->execute()->fetchField();
break;
}
if (isset($entity_id)) {
// Return with the content id found.
return $entity_id;
}
}
return 0;
}
/**
* Utility function that iterates over a target array and retrieves all
* sources that are unique.
*
* @param $batch
* A FeedsImportBatch.
*
* @return
* An array where the keys are target field names and the values are the
* elements from the source item mapped to these targets.
*/
protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
$parser = feeds_importer($this->id)->parser;
$targets = array();
foreach ($this->config['mappings'] as $mapping) {
if ($mapping['unique']) {
// Invoke the parser's getSourceElement to retrieve the value for this
// mapping's source.
$targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
}
}
return $targets;
}
/**
* Adds Feeds specific information on $entity->feeds_item.
*
* @param $entity
* The entity object to be populated with new item info.
* @param $feed_nid
* The feed nid of the source that produces this entity.
* @param $hash
* The fingerprint of the source item.
*/
protected function newItemInfo($entity, $feed_nid, $hash = '') {
$entity->feeds_item = new stdClass();
$entity->feeds_item->entity_id = 0;
$entity->feeds_item->entity_type = $this->entityType();
$entity->feeds_item->id = $this->id;
$entity->feeds_item->feed_nid = $feed_nid;
$entity->feeds_item->imported = REQUEST_TIME;
$entity->feeds_item->hash = $hash;
$entity->feeds_item->url = '';
$entity->feeds_item->guid = '';
}
/**
* Loads existing entity information and places it on $entity->feeds_item.
*
* @param $entity
* The entity object to load item info for. Id key must be present.
*
* @return
* TRUE if item info could be loaded, false if not.
*/
protected function loadItemInfo($entity) {
$entity_info = entity_get_info($this->entityType());
$key = $entity_info['entity keys']['id'];
if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
$entity->feeds_item = $item_info;
return TRUE;
}
return FALSE;
}
/**
* Create MD5 hash of item and mappings array.
*
* Include mappings as a change in mappings may have an affect on the item
* produced.
*
* @return Always returns a hash, even with empty, NULL, FALSE:
* Empty arrays return 40cd750bba9870f18aada2478b24840a
* Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
*/
protected function hash($item) {
static $serialized_mappings;
if (!$serialized_mappings) {
$serialized_mappings = serialize($this->config['mappings']);
}
return hash('md5', serialize($item) . $serialized_mappings);
}
/**
* Retrieves the MD5 hash of $entity_id from the database.
*
* @return string
* Empty string if no item is found, hash otherwise.
*/
protected function getHash($entity_id) {
if ($hash = db_query("SELECT hash FROM {feeds_item} WHERE entity_type = :type AND entity_id = :id", array(':type' => $this->entityType(), ':id' => $entity_id))->fetchField()) {
// Return with the hash.
return $hash;
}
return '';
}
}

View File

@@ -0,0 +1,240 @@
<?php
/**
* @file
* Contains FeedsSimplePieParser and related classes.
*/
/**
* Adapter to present SimplePie_Enclosure as FeedsEnclosure object.
*/
class FeedsSimplePieEnclosure extends FeedsEnclosure {
protected $simplepie_enclosure;
private $_serialized_simplepie_enclosure;
/**
* Constructor requires SimplePie enclosure object.
*/
function __construct(SimplePie_Enclosure $enclosure) {
$this->simplepie_enclosure = $enclosure;
}
/**
* Serialization helper.
*
* Handle the simplepie enclosure class seperately ourselves.
*/
public function __sleep() {
$this->_serialized_simplepie_enclosure = serialize($this->simplepie_enclosure);
return array('_serialized_simplepie_enclosure');
}
/**
* Unserialization helper.
*
* Ensure that the simplepie class definitions are loaded for the enclosure when unserializing.
*/
public function __wakeup() {
feeds_include_simplepie();
$this->simplepie_enclosure = unserialize($this->_serialized_simplepie_enclosure);
}
/**
* Override parent::getValue().
*/
public function getValue() {
return $this->simplepie_enclosure->get_link();
}
/**
* Override parent::getMIMEType().
*/
public function getMIMEType() {
return $this->simplepie_enclosure->get_real_type();
}
}
/**
* Class definition for Common Syndication Parser.
*
* Parses RSS and Atom feeds.
*/
class FeedsSimplePieParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_simplepie();
// Please be quiet SimplePie.
$level = error_reporting();
error_reporting($level ^ E_DEPRECATED ^ E_STRICT);
// Initialize SimplePie.
$parser = new SimplePie();
$parser->set_raw_data($fetcher_result->getRaw());
$parser->set_stupidly_fast(TRUE);
$parser->encode_instead_of_strip(FALSE);
// @todo Is caching effective when we pass in raw data?
$parser->enable_cache(TRUE);
$parser->set_cache_location($this->cacheDirectory());
$parser->init();
// Construct the standard form of the parsed feed
$result = new FeedsParserResult();
$result->title = html_entity_decode(($title = $parser->get_title()) ? $title : $this->createTitle($parser->get_description()));
$result->description = $parser->get_description();
$result->link = html_entity_decode($parser->get_link());
$items_num = $parser->get_item_quantity();
for ($i = 0; $i < $items_num; $i++) {
$item = array();
$simplepie_item = $parser->get_item($i);
$item['title'] = html_entity_decode(($title = $simplepie_item->get_title()) ? $title : $this->createTitle($simplepie_item->get_content()));
$item['description'] = $simplepie_item->get_content();
$item['url'] = html_entity_decode($simplepie_item->get_link());
// Use UNIX time. If no date is defined, fall back to REQUEST_TIME.
$item['timestamp'] = $simplepie_item->get_date("U");
if (empty($item['timestamp'])) {
$item['timestamp'] = REQUEST_TIME;
}
$item['guid'] = $simplepie_item->get_id();
// Use URL as GUID if there is no GUID.
if (empty($item['guid'])) {
$item['guid'] = $item['url'];
}
$author = $simplepie_item->get_author();
$item['author_name'] = isset($author->name) ? html_entity_decode($author->name) : '';
$item['author_link'] = isset($author->link) ? $author->link : '';
$item['author_email'] = isset($author->email) ? $author->email : '';
// Enclosures
$enclosures = $simplepie_item->get_enclosures();
if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
$item['enclosures'][] = new FeedsSimplePieEnclosure($enclosure);
}
}
// Location
$latitude = $simplepie_item->get_latitude();
$longitude = $simplepie_item->get_longitude();
if (!is_null($latitude) && !is_null($longitude)) {
$item['location_latitude'][] = $latitude;
$item['location_longitude'][] = $longitude;
}
// Extract tags related to the item
$simplepie_tags = $simplepie_item->get_categories();
$tags = array();
$domains = array();
if (count($simplepie_tags) > 0) {
foreach ($simplepie_tags as $tag) {
$tags[] = (string) $tag->term;
$domain = (string) $tag->get_scheme();
if (!empty($domain)) {
if (!isset($domains[$domain])) {
$domains[$domain] = array();
}
$domains[$domain][] = count($tags) - 1;
}
}
}
$item['domains'] = $domains;
$item['tags'] = $tags;
// Allow parsing to be extended.
$this->parseExtensions($item, $simplepie_item);
$item['raw'] = $simplepie_item->data;
$result->items[] = $item;
}
// Release parser.
unset($parser);
// Set error reporting back to its previous value.
error_reporting($level);
return $result;
}
/**
* Allow extension of FeedsSimplePie item parsing.
*/
protected function parseExtensions(&$item, $simplepie_item) {}
/**
* Return mapping sources.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Title'),
'description' => t('Title of the feed item.'),
),
'description' => array(
'name' => t('Description'),
'description' => t('Description of the feed item.'),
),
'author_name' => array(
'name' => t('Author name'),
'description' => t('Name of the feed item\'s author.'),
),
'author_link' => array(
'name' => t('Author link'),
'description' => t('Link to the feed item\'s author.'),
),
'author_email' => array(
'name' => t('Author email'),
'description' => t('Email address of the feed item\'s author.'),
),
'timestamp' => array(
'name' => t('Published date'),
'description' => t('Published date as UNIX time GMT of the feed item.'),
),
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'guid' => array(
'name' => t('Item GUID'),
'description' => t('Global Unique Identifier of the feed item.'),
),
'tags' => array(
'name' => t('Categories'),
'description' => t('An array of categories that have been assigned to the feed item.'),
),
'domains' => array(
'name' => t('Category domains'),
'description' => t('Domains of the categories.'),
),
'location_latitude' => array(
'name' => t('Latitudes'),
'description' => t('An array of latitudes assigned to the feed item.'),
),
'location_longitude' => array(
'name' => t('Longitudes'),
'description' => t('An array of longitudes assigned to the feed item.'),
),
'enclosures' => array(
'name' => t('Enclosures'),
'description' => t('An array of enclosures attached to the feed item.'),
),
) + parent::getMappingSources();
}
/**
* Returns cache directory. Creates it if it doesn't exist.
*/
protected function cacheDirectory() {
$directory = 'public://simplepie';
file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
return $directory;
}
/**
* Generate a title from a random text.
*/
protected function createTitle($text = FALSE) {
// Explode to words and use the first 3 words.
$words = preg_split("/[\s,]+/", $text);
$words = array_slice($words, 0, 3);
return implode(' ', $words);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains FeedsSitemapParser and related classes.
*/
/**
* A parser for the Sitemap specification http://www.sitemaps.org/protocol.php
*/
class FeedsSitemapParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
// Set time zone to GMT for parsing dates with strtotime().
$tz = date_default_timezone_get();
date_default_timezone_set('GMT');
// Yes, using a DOM parser is a bit inefficient, but will do for now
$xml = new SimpleXMLElement($fetcher_result->getRaw());
$result = new FeedsParserResult();
foreach ($xml->url as $url) {
$item = array('url' => (string) $url->loc);
if ($url->lastmod) {
$item['lastmod'] = strtotime($url->lastmod);
}
if ($url->changefreq) {
$item['changefreq'] = (string) $url->changefreq;
}
if ($url->priority) {
$item['priority'] = (string) $url->priority;
}
$result->items[] = $item;
}
date_default_timezone_set($tz);
return $result;
}
/**
* Implements FeedsParser::getMappingSources().
*/
public function getMappingSources() {
return array(
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'lastmod' => array(
'name' => t('Last modification date'),
'description' => t('Last modified date as UNIX time GMT of the feed item.'),
),
'changefreq' => array(
'name' => t('Change frequency'),
'description' => t('How frequently the page is likely to change.'),
),
'priority' => array(
'name' => t('Priority'),
'description' => t('The priority of this URL relative to other URLs on the site.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains FeedsSyndicationParser and related classes.
*/
/**
* Class definition for Common Syndication Parser.
*
* Parses RSS and Atom feeds.
*/
class FeedsSyndicationParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_library('common_syndication_parser.inc', 'common_syndication_parser');
$feed = common_syndication_parser_parse($fetcher_result->getRaw());
$result = new FeedsParserResult();
$result->title = $feed['title'];
$result->description = $feed['description'];
$result->link = $feed['link'];
if (is_array($feed['items'])) {
foreach ($feed['items'] as $item) {
if (isset($item['geolocations'])) {
foreach ($item['geolocations'] as $k => $v) {
$item['geolocations'][$k] = new FeedsGeoTermElement($v);
}
}
$result->items[] = $item;
}
}
return $result;
}
/**
* Return mapping sources.
*
* At a future point, we could expose data type information here,
* storage systems like Data module could use this information to store
* parsed data automatically in fields with a correct field type.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Title'),
'description' => t('Title of the feed item.'),
),
'description' => array(
'name' => t('Description'),
'description' => t('Description of the feed item.'),
),
'author_name' => array(
'name' => t('Author name'),
'description' => t('Name of the feed item\'s author.'),
),
'timestamp' => array(
'name' => t('Published date'),
'description' => t('Published date as UNIX time GMT of the feed item.'),
),
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'guid' => array(
'name' => t('Item GUID'),
'description' => t('Global Unique Identifier of the feed item.'),
),
'tags' => array(
'name' => t('Categories'),
'description' => t('An array of categories that have been assigned to the feed item.'),
),
'geolocations' => array(
'name' => t('Geo Locations'),
'description' => t('An array of geographic locations with a name and a position.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* @file
* FeedsTermProcessor class.
*/
/**
* Feeds processor plugin. Create taxonomy terms from feed items.
*/
class FeedsTermProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'taxonomy_term';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Terms');
return $info;
}
/**
* Creates a new term in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$vocabulary = $this->vocabulary();
$term = new stdClass();
$term->vid = $vocabulary->vid;
$term->vocabulary_machine_name = $vocabulary->machine_name;
$term->format = isset($this->config['input_format']) ? $this->config['input_format'] : filter_fallback_format();
return $term;
}
/**
* Loads an existing term.
*/
protected function entityLoad(FeedsSource $source, $tid) {
return taxonomy_term_load($tid);
}
/**
* Validates a term.
*/
protected function entityValidate($term) {
if (empty($term->name)) {
throw new FeedsValidationException(t('Term name missing.'));
}
}
/**
* Saves a term.
*
* We de-array parent fields with only one item.
* This stops leftandright module from freaking out.
*/
protected function entitySave($term) {
if (isset($term->parent)) {
if (is_array($term->parent) && count($term->parent) == 1) {
$term->parent = reset($term->parent);
}
if (isset($term->tid) && ($term->parent == $term->tid || (is_array($term->parent) && in_array($term->tid, $term->parent)))) {
throw new FeedsValidationException(t("A term can't be its own child. GUID:@guid", array('@guid' => $term->feeds_item->guid)));
}
}
taxonomy_term_save($term);
}
/**
* Deletes a series of terms.
*/
protected function entityDeleteMultiple($tids) {
foreach ($tids as $tid) {
taxonomy_term_delete($tid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'vocabulary' => 0,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$options = array(0 => t('Select a vocabulary'));
foreach (taxonomy_get_vocabularies() as $vocab) {
$options[$vocab->machine_name] = $vocab->name;
}
$form = parent::configForm($form_state);
$form['vocabulary'] = array(
'#type' => 'select',
'#title' => t('Import to vocabulary'),
'#description' => t('Choose the vocabulary to import into. <strong>CAUTION:</strong> when deleting terms through the "Delete items" tab, Feeds will delete <em>all</em> terms from this vocabulary.'),
'#options' => $options,
'#default_value' => $this->config['vocabulary'],
);
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
if (empty($values['vocabulary'])) {
form_set_error('vocabulary', t('Choose a vocabulary'));
}
}
/**
* Override setTargetElement to operate on a target item that is a taxonomy term.
*/
public function setTargetElement(FeedsSource $source, $target_term, $target_element, $value) {
switch ($target_element) {
case 'parent':
if (!empty($value)) {
$terms = taxonomy_get_term_by_name($value);
$parent_tid = '';
foreach ($terms as $term) {
if ($term->vid == $target_term->vid) {
$parent_tid = $term->tid;
}
}
if (!empty($parent_tid)) {
$target_term->parent[] = $parent_tid;
}
else {
$target_term->parent[] = 0;
}
}
else {
$target_term->parent[] = 0;
}
break;
case 'parentguid':
// value is parent_guid field value
$query = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('entity_type', $this->entityType());
$parent_tid = $query->condition('guid', $value)->execute()->fetchField();
$target_term->parent[] = ($parent_tid) ? $parent_tid : 0;
break;
case 'weight':
if (!empty($value)) {
$weight = intval($value);
}
else {
$weight = 0;
}
$target_term->weight = $weight;
break;
default:
parent::setTargetElement($source, $target_term, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$targets = parent::getMappingTargets();
$targets += array(
'name' => array(
'name' => t('Term name'),
'description' => t('Name of the taxonomy term.'),
'optional_unique' => TRUE,
),
'parent' => array(
'name' => t('Parent: Term name'),
'description' => t('The name of the parent taxonomy term.'),
'optional_unique' => TRUE,
),
'parentguid' => array(
'name' => t('Parent: GUID'),
'description' => t('The GUID of the parent taxonomy term.'),
'optional_unique' => TRUE,
),
'weight' => array(
'name' => t('Term weight'),
'description' => t('Weight of the taxonomy term.'),
'optional_unique' => TRUE,
),
'description' => array(
'name' => t('Term description'),
'description' => t('Description of the taxonomy term.'),
),
);
// Let implementers of hook_feeds_term_processor_targets() add their targets.
try {
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->vocabulary()->machine_name;
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
}
catch (Exception $e) {
// Do nothing.
}
return $targets;
}
/**
* Get id of an existing feed item term if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($tid = parent::existingEntityId($source, $result)) {
return $tid;
}
// The only possible unique target is name.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
if ($target == 'name') {
$vocabulary = $this->vocabulary();
if ($tid = db_query("SELECT tid FROM {taxonomy_term_data} WHERE name = :name AND vid = :vid", array(':name' => $value, ':vid' => $vocabulary->vid))->fetchField()) {
return $tid;
}
}
}
return 0;
}
/**
* Return vocabulary to map to.
*/
public function vocabulary() {
if (isset($this->config['vocabulary'])) {
if ($vocabulary = taxonomy_vocabulary_machine_name_load($this->config['vocabulary'])) {
return $vocabulary;
}
}
throw new Exception(t('No vocabulary defined for Taxonomy Term processor.'));
}
}

View File

@@ -0,0 +1,242 @@
<?php
/**
* @file
* FeedsUserProcessor class.
*/
/**
* Feeds processor plugin. Create users from feed items.
*/
class FeedsUserProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'user';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Users');
return $info;
}
/**
* Creates a new user account in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$account = new stdClass();
$account->uid = 0;
$account->roles = array_filter($this->config['roles']);
$account->status = $this->config['status'];
return $account;
}
/**
* Loads an existing user.
*/
protected function entityLoad(FeedsSource $source, $uid) {
// Copy the password so that we can compare it again at save.
$user = user_load($uid);
$user->feeds_original_pass = $user->pass;
return $user;
}
/**
* Validates a user account.
*/
protected function entityValidate($account) {
if (empty($account->name) || empty($account->mail) || !valid_email_address($account->mail)) {
throw new FeedsValidationException(t('User name missing or email not valid.'));
}
}
/**
* Save a user account.
*/
protected function entitySave($account) {
if ($this->config['defuse_mail']) {
$account->mail = $account->mail . '_test';
}
$edit = (array) $account;
// Remove pass from $edit if the password is unchanged.
if (isset($account->feeds_original_pass) && $account->pass == $account->feeds_original_pass) {
unset($edit['pass']);
}
user_save($account, $edit);
if ($account->uid && !empty($account->openid)) {
$authmap = array(
'uid' => $account->uid,
'module' => 'openid',
'authname' => $account->openid,
);
if (SAVED_UPDATED != drupal_write_record('authmap', $authmap, array('uid', 'module'))) {
drupal_write_record('authmap', $authmap);
}
}
}
/**
* Delete multiple user accounts.
*/
protected function entityDeleteMultiple($uids) {
foreach ($uids as $uid) {
user_delete($uid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'roles' => array(),
'status' => 1,
'defuse_mail' => FALSE,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = parent::configForm($form_state);
$form['status'] = array(
'#type' => 'radios',
'#title' => t('Status'),
'#description' => t('Select whether users should be imported active or blocked.'),
'#options' => array(0 => t('Blocked'), 1 => t('Active')),
'#default_value' => $this->config['status'],
);
$roles = user_roles(TRUE);
unset($roles[2]);
if (count($roles)) {
$form['roles'] = array(
'#type' => 'checkboxes',
'#title' => t('Additional roles'),
'#description' => t('Every user is assigned the "authenticated user" role. Select additional roles here.'),
'#default_value' => $this->config['roles'],
'#options' => $roles,
);
}
// @todo Implement true updating.
$form['update_existing'] = array(
'#type' => 'checkbox',
'#title' => t('Replace existing users'),
'#description' => t('If an existing user is found for an imported user, replace it. Existing users will be determined using mappings that are a "unique target".'),
'#default_value' => $this->config['update_existing'],
);
$form['defuse_mail'] = array(
'#type' => 'checkbox',
'#title' => t('Defuse e-mail addresses'),
'#description' => t('This appends _test to all imported e-mail addresses to ensure they cannot be used as recipients.'),
'#default_value' => $this->config['defuse_mail'],
);
return $form;
}
/**
* Override setTargetElement to operate on a target item that is a node.
*/
public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value) {
switch ($target_element) {
case 'created':
$target_user->created = feeds_to_unixtime($value, REQUEST_TIME);
break;
case 'language':
$target_user->language = strtolower($value);
break;
default:
parent::setTargetElement($source, $target_user, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$targets = parent::getMappingTargets();
$targets += array(
'name' => array(
'name' => t('User name'),
'description' => t('Name of the user.'),
'optional_unique' => TRUE,
),
'mail' => array(
'name' => t('Email address'),
'description' => t('Email address of the user.'),
'optional_unique' => TRUE,
),
'created' => array(
'name' => t('Created date'),
'description' => t('The created (e. g. joined) data of the user.'),
),
'pass' => array(
'name' => t('Unencrypted Password'),
'description' => t('The unencrypted user password.'),
),
'status' => array(
'name' => t('Account status'),
'description' => t('Whether a user is active or not. 1 stands for active, 0 for blocked.'),
),
'language' => array(
'name' => t('User language'),
'description' => t('Default language for the user.'),
),
);
if (module_exists('openid')) {
$targets['openid'] = array(
'name' => t('OpenID identifier'),
'description' => t('The OpenID identifier of the user. <strong>CAUTION:</strong> Use only for migration purposes, misconfiguration of the OpenID identifier can lead to severe security breaches like users gaining access to accounts other than their own.'),
'optional_unique' => TRUE,
);
}
// Let other modules expose mapping targets.
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->entityType();
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
return $targets;
}
/**
* Get id of an existing feed item term if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($uid = parent::existingEntityId($source, $result)) {
return $uid;
}
// Iterate through all unique targets and try to find a user for the
// target's value.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'name':
$uid = db_query("SELECT uid FROM {users} WHERE name = :name", array(':name' => $value))->fetchField();
break;
case 'mail':
$uid = db_query("SELECT uid FROM {users} WHERE mail = :mail", array(':mail' => $value))->fetchField();
break;
case 'openid':
$uid = db_query("SELECT uid FROM {authmap} WHERE authname = :authname AND module = 'openid'", array(':authname' => $value))->fetchField();
break;
}
if ($uid) {
// Return with the first nid found.
return $uid;
}
}
return 0;
}
}