first import
This commit is contained in:
216
sites/all/modules/feeds/plugins/FeedsCSVParser.inc
Normal file
216
sites/all/modules/feeds/plugins/FeedsCSVParser.inc
Normal 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;
|
||||
}
|
||||
}
|
218
sites/all/modules/feeds/plugins/FeedsFetcher.inc
Normal file
218
sites/all/modules/feeds/plugins/FeedsFetcher.inc
Normal 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) {}
|
||||
}
|
229
sites/all/modules/feeds/plugins/FeedsFileFetcher.inc
Normal file
229
sites/all/modules/feeds/plugins/FeedsFileFetcher.inc
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
332
sites/all/modules/feeds/plugins/FeedsHTTPFetcher.inc
Normal file
332
sites/all/modules/feeds/plugins/FeedsHTTPFetcher.inc
Normal 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);
|
||||
}
|
||||
}
|
391
sites/all/modules/feeds/plugins/FeedsNodeProcessor.inc
Normal file
391
sites/all/modules/feeds/plugins/FeedsNodeProcessor.inc
Normal 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;
|
||||
}
|
||||
}
|
39
sites/all/modules/feeds/plugins/FeedsOPMLParser.inc
Normal file
39
sites/all/modules/feeds/plugins/FeedsOPMLParser.inc
Normal 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();
|
||||
}
|
||||
}
|
757
sites/all/modules/feeds/plugins/FeedsParser.inc
Normal file
757
sites/all/modules/feeds/plugins/FeedsParser.inc
Normal 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;
|
||||
}
|
216
sites/all/modules/feeds/plugins/FeedsPlugin.inc
Normal file
216
sites/all/modules/feeds/plugins/FeedsPlugin.inc
Normal 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']);
|
||||
}
|
705
sites/all/modules/feeds/plugins/FeedsProcessor.inc
Normal file
705
sites/all/modules/feeds/plugins/FeedsProcessor.inc
Normal 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 '';
|
||||
}
|
||||
}
|
240
sites/all/modules/feeds/plugins/FeedsSimplePieParser.inc
Normal file
240
sites/all/modules/feeds/plugins/FeedsSimplePieParser.inc
Normal 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);
|
||||
}
|
||||
}
|
62
sites/all/modules/feeds/plugins/FeedsSitemapParser.inc
Normal file
62
sites/all/modules/feeds/plugins/FeedsSitemapParser.inc
Normal 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();
|
||||
}
|
||||
}
|
81
sites/all/modules/feeds/plugins/FeedsSyndicationParser.inc
Normal file
81
sites/all/modules/feeds/plugins/FeedsSyndicationParser.inc
Normal 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();
|
||||
}
|
||||
}
|
245
sites/all/modules/feeds/plugins/FeedsTermProcessor.inc
Normal file
245
sites/all/modules/feeds/plugins/FeedsTermProcessor.inc
Normal 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.'));
|
||||
}
|
||||
}
|
242
sites/all/modules/feeds/plugins/FeedsUserProcessor.inc
Normal file
242
sites/all/modules/feeds/plugins/FeedsUserProcessor.inc
Normal 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;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user