contrib modules security updates

This commit is contained in:
Bachir Soussi Chiadmi
2016-10-13 12:10:40 +02:00
parent ffd758abc9
commit 747127f643
732 changed files with 67976 additions and 23207 deletions

View File

@@ -20,15 +20,20 @@ class FeedsCSVParser extends FeedsParser {
// Load and configure parser.
feeds_include_library('ParserCSV.inc', 'ParserCSV');
$parser = new ParserCSV();
$delimiter = $source_config['delimiter'] == 'TAB' ? "\t" : $source_config['delimiter'];
$delimiter = $this->getDelimiterChar($source_config);
$parser->setDelimiter($delimiter);
if (isset($source_config['encoding'])) {
// Encoding can only be set when the mbstring extension is loaded.
$parser->setEncoding($source_config['encoding']);
}
$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;
drupal_set_message(t('The CSV file is empty.'), 'warning', FALSE);
return new FeedsParserResult();
}
$parser->setColumnNames($header);
}
@@ -100,12 +105,20 @@ class FeedsCSVParser extends FeedsParser {
return parent::getSourceElement($source, $result, drupal_strtolower($element_key));
}
/**
* Override parent::getMappingSourceList() to use only lower keys.
*/
public function getMappingSourceList() {
return array_map('drupal_strtolower', parent::getMappingSourceList());
}
/**
* Define defaults.
*/
public function sourceDefaults() {
return array(
'delimiter' => $this->config['delimiter'],
'encoding' => $this->config['encoding'],
'no_headers' => $this->config['no_headers'],
);
}
@@ -122,26 +135,40 @@ class FeedsCSVParser extends FeedsParser {
$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']);
if (strpos($mapping['source'], ',') !== FALSE) {
$sources[] = '"' . $mapping['source'] . '"';
}
else {
$sources[] = $mapping['source'];
}
if (!empty($mapping['unique'])) {
$uniques[] = $mapping['source'];
}
}
$sources = array_unique($sources);
$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)));
$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[] = format_plural(count($uniques), 'Column <strong>@columns</strong> is mandatory and considered unique: only one item per @columns value will be created.', '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['help'] = array(
'#prefix' => '<div class="help">',
'#suffix' => '</div>',
'description' => array(
'#prefix' => '<p>',
'#markup' => $output,
'#suffix' => '</p>',
),
'list' => array(
'#theme' => 'item_list',
'#items' => $items,
),
);
$form['delimiter'] = array(
'#type' => 'select',
'#title' => t('Delimiter'),
'#description' => t('The character that delimits fields in the CSV file.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#options' => $this->getAllDelimiterTypes(),
'#default_value' => isset($source_config['delimiter']) ? $source_config['delimiter'] : ',',
);
$form['no_headers'] = array(
@@ -150,6 +177,10 @@ class FeedsCSVParser extends FeedsParser {
'#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,
);
$form['encoding'] = $this->configEncodingForm();
if (isset($source_config['encoding'])) {
$form['encoding']['#default_value'] = $source_config['encoding'];
}
return $form;
}
@@ -159,6 +190,7 @@ class FeedsCSVParser extends FeedsParser {
public function configDefaults() {
return array(
'delimiter' => ',',
'encoding' => 'UTF-8',
'no_headers' => 0,
);
}
@@ -172,11 +204,7 @@ class FeedsCSVParser extends FeedsParser {
'#type' => 'select',
'#title' => t('Default delimiter'),
'#description' => t('Default field delimiter.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#options' => $this->getAllDelimiterTypes(),
'#default_value' => $this->config['delimiter'],
);
$form['no_headers'] = array(
@@ -185,32 +213,162 @@ class FeedsCSVParser extends FeedsParser {
'#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'],
);
$form['encoding'] = $this->configEncodingForm();
return $form;
}
/**
* Builds configuration field for setting file encoding.
*
* If the mbstring extension is not available a markup render array
* will be returned instead.
*
* @return array
* A renderable array.
*/
public function configEncodingForm() {
if (extension_loaded('mbstring') && variable_get('feeds_use_mbstring', TRUE)) {
// Get the system's list of available encodings.
$options = mb_list_encodings();
// Make the key/values the same in the array.
$options = array_combine($options, $options);
// Sort alphabetically not-case sensitive.
natcasesort($options);
return array(
'#type' => 'select',
'#title' => t('File encoding'),
'#description' => t('Performs character encoding conversion from selected option to UTF-8.'),
'#options' => $options,
'#default_value' => $this->config['encoding'],
);
}
else {
return array(
'#markup' => '<em>' . t('PHP mbstring extension must be available for character encoding conversion.') . '</em>',
);
}
}
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']);
if (in_array($mapping['source'], $uniques) || in_array($mapping['source'], $sources)) {
// Skip columns we've already seen.
continue;
}
if (!empty($mapping['unique'])) {
$uniques[] = $mapping['source'];
}
else {
$sources[] = check_plain($mapping['source']);
$sources[] = $mapping['source'];
}
}
$sep = ',';
$sep = $this->getDelimiterChar($this->config);
$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');
$template_file_details = $this->getTemplateFileDetails($this->config);
$filename = "{$this->id}_template.{$template_file_details['extension']}";
$cache_control = 'max-age=60, must-revalidate';
$content_disposition = 'attachment; filename="' . $filename . '"';
$content_type = "{$template_file_details['mime_type']}; charset=utf-8";
drupal_add_http_header('Cache-Control', $cache_control);
drupal_add_http_header('Content-Disposition', $content_disposition);
drupal_add_http_header('Content-type', $content_type);
print implode($sep, $columns);
return;
}
/**
* Gets an associative array of the delimiters supported by this parser.
*
* The keys represent the value that is persisted into the database, and the
* value represents the text that is shown in the admins UI.
*
* @return array
* The associative array of delimiter types to display name.
*/
protected function getAllDelimiterTypes() {
$delimiters = array(
',',
';',
'TAB',
'|',
'+',
);
return array_combine($delimiters, $delimiters);
}
/**
* Gets the appropriate delimiter character for the delimiter in the config.
*
* @param array $config
* The configuration for the parser.
*
* @return string
* The delimiter character.
*/
protected function getDelimiterChar(array $config) {
$config_delimiter = $config['delimiter'];
switch ($config_delimiter) {
case 'TAB':
$delimiter = "\t";
break;
default:
$delimiter = $config_delimiter;
break;
}
return $delimiter;
}
/**
* Gets details about the template file, for the delimiter in the config.
*
* The resulting details indicate the file extension and mime type for the
* delimiter type.
*
* @param array $config
* The configuration for the parser.
*
* @return array
* An array with the following information:
* - 'extension': The file extension for the template ('tsv', 'csv', etc).
* - 'mime-type': The mime type for the template
* ('text/tab-separated-values', 'text/csv', etc).
*/
protected function getTemplateFileDetails(array $config) {
switch ($config['delimiter']) {
case 'TAB':
$extension = 'tsv';
$mime_type = 'text/tab-separated-values';
break;
default:
$extension = 'csv';
$mime_type = 'text/csv';
break;
}
return array(
'extension' => $extension,
'mime_type' => $mime_type,
);
}
}

View File

@@ -113,6 +113,13 @@ class FeedsFetcherResult extends FeedsResult {
*/
abstract class FeedsFetcher extends FeedsPlugin {
/**
* Implements FeedsPlugin::pluginType().
*/
public function pluginType() {
return 'fetcher';
}
/**
* Fetch content from a source and return it.
*

View File

@@ -19,7 +19,7 @@ class FeedsFileFetcherResult extends FeedsFetcherResult {
}
/**
* Overrides parent::getRaw();
* Overrides parent::getRaw().
*/
public function getRaw() {
return $this->sanitizeRaw(file_get_contents($this->file_path));
@@ -29,7 +29,7 @@ class FeedsFileFetcherResult extends FeedsFetcherResult {
* Overrides parent::getFilePath().
*/
public function getFilePath() {
if (!file_exists($this->file_path)) {
if (!is_readable($this->file_path)) {
throw new Exception(t('File @filepath is not accessible.', array('@filepath' => $this->file_path)));
}
return $this->sanitizeFile($this->file_path);
@@ -69,25 +69,25 @@ class FeedsFileFetcher extends FeedsFetcher {
}
/**
* Return an array of files in a directory.
* Returns an array of files in a directory.
*
* @param $dir
* @param string $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.
* @return array
* 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);
// Seperate out string into array of extensions. Make sure its regex safe.
$config = $this->getConfig();
$extensions = array_filter(array_map('preg_quote', explode(' ', $config['allowed_extensions'])));
$regex = '/\.(' . implode('|', $extensions) . ')$/';
$files = array();
if ($items = @scandir($dir)) {
foreach ($items as $item) {
if (is_file("$dir/$item") && strpos($item, '.') !== 0) {
$files[] = "$dir/$item";
}
}
foreach (file_scan_directory($dir, $regex) as $file) {
$files[] = $file->uri;
}
return $files;
}
@@ -109,7 +109,7 @@ class FeedsFileFetcher extends FeedsFetcher {
'#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',
'#theme_wrappers' => array('feeds_upload'),
'#file_info' => empty($source_config['fid']) ? NULL : file_load($source_config['fid']),
'#size' => 10,
);
@@ -118,7 +118,7 @@ class FeedsFileFetcher extends FeedsFetcher {
$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())),
'#description' => t('Specify a path to a file or a directory. Prefix the path with a scheme. Available schemes: @schemes.', array('@schemes' => implode(', ', $this->config['allowed_schemes']))),
'#default_value' => empty($source_config['source']) ? '' : $source_config['source'],
);
}
@@ -126,34 +126,53 @@ class FeedsFileFetcher extends FeedsFetcher {
}
/**
* Override parent::sourceFormValidate().
* Overrides 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 (empty($this->config['direct'])) {
// 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;
$feed_dir = $this->config['directory'];
if (!file_prepare_directory($feed_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
if (user_access('administer feeds')) {
$plugin_key = feeds_importer($this->id)->config[$this->pluginType()]['plugin_key'];
$link = url('admin/structure/feeds/' . $this->id . '/settings/' . $plugin_key);
form_set_error('feeds][FeedsFileFetcher][source', t('Upload failed. Please check the upload <a href="@link">settings.</a>', array('@link' => $link)));
}
else {
form_set_error('feeds][FeedsFileFetcher][source', t('Upload failed. Please contact your site administrator.'));
}
watchdog('feeds', 'The upload directory %directory required by a feed could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $feed_dir));
}
// Validate and save uploaded file.
elseif ($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][FeedsFileFetcher][source', t('Please upload a file.'));
}
else {
// File present from previous upload. Nothing to validate.
}
}
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())));
else {
// Check if chosen url scheme is allowed.
$scheme = file_uri_scheme($values['source']);
if (!$scheme || !in_array($scheme, $this->config['allowed_schemes'])) {
form_set_error('feeds][FeedsFileFetcher][source', t("The file needs to reside within the site's files directory, its path needs to start with scheme://. Available schemes: @schemes.", array('@schemes' => implode(', ', $this->config['allowed_schemes']))));
}
// Check whether the given path is readable.
elseif (!is_readable($values['source'])) {
form_set_error('feeds][FeedsFileFetcher][source', t('The specified file or directory does not exist.'));
}
}
}
/**
* Override parent::sourceSave().
* Overrides parent::sourceSave().
*/
public function sourceSave(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
@@ -176,7 +195,7 @@ class FeedsFileFetcher extends FeedsFetcher {
}
/**
* Override parent::sourceDelete().
* Overrides parent::sourceDelete().
*/
public function sourceDelete(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
@@ -186,17 +205,22 @@ class FeedsFileFetcher extends FeedsFetcher {
}
/**
* Override parent::configDefaults().
* Overrides parent::configDefaults().
*/
public function configDefaults() {
$schemes = $this->getSchemes();
$scheme = in_array('private', $schemes) ? 'private' : 'public';
return array(
'allowed_extensions' => 'txt csv tsv xml opml',
'direct' => FALSE,
'directory' => $scheme . '://feeds',
'allowed_schemes' => $schemes,
);
}
/**
* Override parent::configForm().
* Overrides parent::configForm().
*/
public function configForm(&$form_state) {
$form = array();
@@ -214,16 +238,112 @@ class FeedsFileFetcher extends FeedsFetcher {
are already on the server.'),
'#default_value' => $this->config['direct'],
);
$form['directory'] = array(
'#type' => 'textfield',
'#title' => t('Upload directory'),
'#description' => t('Directory where uploaded files get stored. Prefix the path with a scheme. Available schemes: @schemes.', array('@schemes' => implode(', ', $this->getSchemes()))),
'#default_value' => $this->config['directory'],
'#states' => array(
'visible' => array(':input[name="direct"]' => array('checked' => FALSE)),
'required' => array(':input[name="direct"]' => array('checked' => FALSE)),
),
);
if ($options = $this->getSchemeOptions()) {
$form['allowed_schemes'] = array(
'#type' => 'checkboxes',
'#title' => t('Allowed schemes'),
'#default_value' => $this->config['allowed_schemes'],
'#options' => $options,
'#description' => t('Select the schemes you want to allow for direct upload.'),
'#states' => array(
'visible' => array(':input[name="direct"]' => array('checked' => TRUE)),
),
);
}
return $form;
}
/**
* Helper. Deletes a file.
* Overrides parent::configFormValidate().
*
* Ensure that the chosen directory is accessible.
*/
public function configFormValidate(&$values) {
$values['directory'] = trim($values['directory']);
$values['allowed_schemes'] = array_filter($values['allowed_schemes']);
if (!$values['direct']) {
// Ensure that the upload directory field is not empty when not in
// direct-mode.
if (!$values['directory']) {
form_set_error('directory', t('Please specify an upload directory.'));
// Do not continue validating the directory if none was specified.
return;
}
// Validate the URI scheme of the upload directory.
$scheme = file_uri_scheme($values['directory']);
if (!$scheme || !in_array($scheme, $this->getSchemes())) {
form_set_error('directory', t('Please enter a valid scheme into the directory location.'));
// Return here so that attempts to create the directory below don't
// throw warnings.
return;
}
// Ensure that the upload directory exists.
if (!file_prepare_directory($values['directory'], FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
form_set_error('directory', t('The chosen directory does not exist and attempts to create it failed.'));
}
}
}
/**
* Deletes a file.
*
* @param int $fid
* The file id.
* @param int $feed_nid
* The feed node's id, or 0 if a standalone feed.
*
* @return bool|array
* TRUE for success, FALSE in the event of an error, or an array if the file
* is being used by any modules.
*
* @see file_delete()
*/
protected function deleteFile($fid, $feed_nid) {
if ($file = file_load($fid)) {
file_usage_delete($file, 'feeds', get_class($this), $feed_nid);
file_delete($file);
return file_delete($file);
}
return FALSE;
}
/**
* Returns available schemes.
*
* @return array
* The available schemes.
*/
protected function getSchemes() {
return array_keys(file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE));
}
/**
* Returns available scheme options for use in checkboxes or select list.
*
* @return array
* The available scheme array keyed scheme => description
*/
protected function getSchemeOptions() {
$options = array();
foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $info) {
$options[$scheme] = check_plain($scheme . ': ' . $info['description']);
}
return $options;
}
}

View File

@@ -11,28 +11,70 @@ feeds_include_library('PuSHSubscriber.inc', 'PuSHSubscriber');
* Result of FeedsHTTPFetcher::fetch().
*/
class FeedsHTTPFetcherResult extends FeedsFetcherResult {
/**
* The URL of the feed being fetched.
*
* @var string
*/
protected $url;
protected $file_path;
/**
* The timeout in seconds to wait for a download.
*
* @var int
*/
protected $timeout;
/**
*
* Whether to ignore SSL validation errors.
*
* @var bool
*/
protected $acceptInvalidCert;
/**
* 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)));
if (!isset($this->raw)) {
feeds_include_library('http_request.inc', 'http_request');
$result = http_request_get($this->url, NULL, NULL, $this->acceptInvalidCert, $this->timeout);
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)));
}
$this->raw = $result->data;
}
return $this->sanitizeRaw($result->data);
return $this->sanitizeRaw($this->raw);
}
public function getTimeout() {
return $this->timeout;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
}
/**
* Sets the accept invalid certificates option.
*
* @param bool $accept_invalid_cert
* Whether to accept invalid certificates.
*/
public function setAcceptInvalidCert($accept_invalid_cert) {
$this->acceptInvalidCert = (bool) $accept_invalid_cert;
}
}
/**
@@ -48,7 +90,11 @@ class FeedsHTTPFetcher extends FeedsFetcher {
if ($this->config['use_pubsubhubbub'] && ($raw = $this->subscriber($source->feed_nid)->receive())) {
return new FeedsFetcherResult($raw);
}
return new FeedsHTTPFetcherResult($source_config['source']);
$fetcher_result = new FeedsHTTPFetcherResult($source_config['source']);
// When request_timeout is empty, the global value is used.
$fetcher_result->setTimeout($this->config['request_timeout']);
$fetcher_result->setAcceptInvalidCert($this->config['accept_invalid_cert']);
return $fetcher_result;
}
/**
@@ -95,6 +141,9 @@ class FeedsHTTPFetcher extends FeedsFetcher {
'auto_detect_feeds' => FALSE,
'use_pubsubhubbub' => FALSE,
'designated_hub' => '',
'request_timeout' => NULL,
'auto_scheme' => 'http',
'accept_invalid_cert' => FALSE,
);
}
@@ -115,15 +164,46 @@ class FeedsHTTPFetcher extends FeedsFetcher {
'#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(
$form['advanced'] = array(
'#title' => t('Advanced settings'),
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['advanced']['auto_scheme'] = array(
'#type' => 'textfield',
'#title' => t('Automatically add scheme'),
'#description' => t('If the supplied URL does not contain the scheme, use this one automatically. Keep empty to force the user to input the scheme.'),
'#default_value' => $this->config['auto_scheme'],
);
$form['advanced']['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),
'#states' => array(
'visible' => array(':input[name="use_pubsubhubbub"]' => array('checked' => TRUE)),
),
);
// Per importer override of global http request timeout setting.
$form['advanced']['request_timeout'] = array(
'#type' => 'textfield',
'#title' => t('Request timeout'),
'#description' => t('Timeout in seconds to wait for an HTTP get request to finish.</br>' .
'<b>Note:</b> this setting will override the global setting.</br>' .
'When left empty, the global value is used.'),
'#default_value' => $this->config['request_timeout'],
'#element_validate' => array('element_validate_integer_positive'),
'#maxlength' => 3,
'#size'=> 30,
);
$form['advanced']['accept_invalid_cert'] = array(
'#type' => 'checkbox',
'#title' => t('Accept invalid SSL certificates'),
'#description' => t('<strong>IMPORTANT:</strong> This setting will force cURL to completely ignore all SSL errors. This is a <strong>major security risk</strong> and should only be used during development.'),
'#default_value' => $this->config['accept_invalid_cert'],
);
return $form;
}
@@ -149,13 +229,24 @@ class FeedsHTTPFetcher extends FeedsFetcher {
public function sourceFormValidate(&$values) {
$values['source'] = trim($values['source']);
// Keep a copy for error messages.
$original_url = $values['source'];
$parts = parse_url($values['source']);
if (empty($parts['scheme']) && $this->config['auto_scheme']) {
$values['source'] = $this->config['auto_scheme'] . '://' . $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'])));
form_set_error($form_key, t('The URL %source is invalid.', array('%source' => $original_url)));
}
elseif ($this->config['auto_detect_feeds']) {
feeds_include_library('http_request.inc', 'http_request');
if ($url = http_request_get_common_syndication($values['source'])) {
$url = http_request_get_common_syndication($values['source'], array(
'accept_invalid_cert' => $this->config['accept_invalid_cert'],
));
if ($url) {
$values['source'] = $url;
}
}

View File

@@ -5,10 +5,17 @@
* Class definition of FeedsNodeProcessor.
*/
/**
* Option for handling content in Drupal but not in source data (unpublish
* instead of skip/delete).
*/
define('FEEDS_UNPUBLISH_NON_EXISTENT', 'unpublish');
/**
* Creates nodes from feed items.
*/
class FeedsNodeProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
@@ -29,11 +36,11 @@ class FeedsNodeProcessor extends FeedsProcessor {
* Creates a new node in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$node = new stdClass();
$node->type = $this->config['content_type'];
$node = parent::newEntity($source);
$node->type = $this->bundle();
$node->changed = REQUEST_TIME;
$node->created = REQUEST_TIME;
$node->language = LANGUAGE_NONE;
$node->is_new = TRUE;
node_object_prepare($node);
// Populate properties that are set by node_object_prepare().
$node->log = 'Created by FeedsNodeProcessor';
@@ -50,14 +57,12 @@ class FeedsNodeProcessor extends FeedsProcessor {
* @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 = parent::entityLoad($source, $nid);
if ($this->config['update_existing'] != FEEDS_UPDATE_EXISTING) {
$node->uid = $this->config['author'];
}
node_object_prepare($node);
// Workaround for issue #1247506. See #1245094 for backstory.
@@ -87,7 +92,7 @@ class FeedsNodeProcessor extends FeedsProcessor {
$author = user_load($entity->uid);
// If the uid was mapped directly, rather than by email or username, it
// 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.';
@@ -104,20 +109,34 @@ class FeedsNodeProcessor extends FeedsProcessor {
}
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)));
$message = t('The user %name is not authorized to %op content of type %content_type. To import this item, either the user "@name" (author of the item) must be given the permission to @op content of type @content_type, or the option "Authorize" on the Node processor settings must be turned off.', array(
'%name' => $author->name,
'%op' => $op,
'%content_type' => $entity->type,
'@name' => $author->name,
'@op' => $op,
'@content_type' => $entity->type,
));
throw new FeedsAccessException($message);
}
}
}
/**
* Validates a node.
*/
protected function entityValidate($entity) {
parent::entityValidate($entity);
if (!isset($entity->uid) || !is_numeric($entity->uid)) {
$entity->uid = $this->config['author'];
}
}
/**
* 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);
}
@@ -129,28 +148,12 @@ class FeedsNodeProcessor extends FeedsProcessor {
}
/**
* Implement expire().
*
* @todo: move to processor stage?
* Overrides parent::expiryQuery().
*/
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;
protected function expiryQuery(FeedsSource $source, $time) {
$select = parent::expiryQuery($source, $time);
$select->condition('e.created', REQUEST_TIME - $time, '<');
return $select;
}
/**
@@ -164,10 +167,7 @@ class FeedsNodeProcessor extends FeedsProcessor {
* 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,
@@ -178,16 +178,8 @@ class FeedsNodeProcessor extends FeedsProcessor {
* 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',
@@ -207,14 +199,13 @@ class FeedsNodeProcessor extends FeedsProcessor {
'#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.'),
'#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)',
);
// Add on the "Unpublish" option for nodes, update wording.
if (isset($form['update_non_existent'])) {
$form['update_non_existent']['#options'][FEEDS_UNPUBLISH_NON_EXISTENT] = t('Unpublish non-existent nodes');
}
return $form;
}
@@ -248,10 +239,16 @@ class FeedsNodeProcessor extends FeedsProcessor {
case 'created':
$target_node->created = feeds_to_unixtime($value, REQUEST_TIME);
break;
case 'changed':
// The 'changed' value will be set on the node in feeds_node_presave().
// This is because node_save() always overwrites this value (though
// before invoking hook_node_presave()).
$target_node->feeds_item->node_changed = 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'])) {
if ($id = feeds_get_importer_id($this->bundle())) {
$class = get_class(feeds_importer($id)->fetcher);
$target_node->feeds[$class]['source'] = $value;
// This effectively suppresses 'import on submission' feature.
@@ -279,9 +276,10 @@ class FeedsNodeProcessor extends FeedsProcessor {
* Return available mapping targets.
*/
public function getMappingTargets() {
$type = node_type_get_type($this->config['content_type']);
$type = node_type_get_type($this->bundle());
$targets = parent::getMappingTargets();
if ($type->has_title) {
if ($type && $type->has_title) {
$targets['title'] = array(
'name' => t('Title'),
'description' => t('The title of the node.'),
@@ -313,6 +311,10 @@ class FeedsNodeProcessor extends FeedsProcessor {
'name' => t('Published date'),
'description' => t('The UNIX time when a node has been published.'),
);
$targets['changed'] = array(
'name' => t('Updated date'),
'description' => t('The Unix timestamp when a node has been last updated.'),
);
$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)'),
@@ -339,7 +341,7 @@ class FeedsNodeProcessor extends FeedsProcessor {
}
// If the target content type is a Feed node, expose its source field.
if ($id = feeds_get_importer_id($this->config['content_type'])) {
if ($id = feeds_get_importer_id($this->bundle())) {
$name = feeds_importer($id)->config['name'];
$targets['feeds_source'] = array(
'name' => t('Feed source'),
@@ -348,11 +350,7 @@ class FeedsNodeProcessor extends FeedsProcessor {
);
}
// 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);
$this->getHookTargets($targets);
return $targets;
}
@@ -373,10 +371,10 @@ class FeedsNodeProcessor extends FeedsProcessor {
$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();
$nid = db_query("SELECT nid FROM {node} WHERE title = :title AND type = :type", array(':title' => $value, ':type' => $this->bundle()))->fetchField();
break;
case 'feeds_source':
if ($id = feeds_get_importer_id($this->config['content_type'])) {
if ($id = feeds_get_importer_id($this->bundle())) {
$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;
@@ -388,4 +386,34 @@ class FeedsNodeProcessor extends FeedsProcessor {
}
return 0;
}
/**
* Overrides FeedsProcessor::clean().
*
* Allow unpublish instead of delete.
*
* @param FeedsState $state
* The FeedsState object for the given stage.
*/
protected function clean(FeedsState $state) {
// Delegate to parent if not unpublishing or option not set.
if (!isset($this->config['update_non_existent']) || $this->config['update_non_existent'] != FEEDS_UNPUBLISH_NON_EXISTENT) {
return parent::clean($state);
}
$total = count($state->removeList);
if ($total) {
$nodes = node_load_multiple($state->removeList);
foreach ($nodes as &$node) {
$this->loadItemInfo($node);
// Update the hash value of the feed item to ensure that the item gets
// updated in case it reappears in the feed.
$node->feeds_item->hash = $this->config['update_non_existent'];
node_unpublish_action($node);
node_save($node);
$state->unpublished++;
}
}
}
}

View File

@@ -53,6 +53,13 @@ class FeedsParserResult extends FeedsResult {
*/
abstract class FeedsParser extends FeedsPlugin {
/**
* Implements FeedsPlugin::pluginType().
*/
public function pluginType() {
return 'parser';
}
/**
* Parse content fetched by fetcher.
*
@@ -112,6 +119,21 @@ abstract class FeedsParser extends FeedsPlugin {
return $sources;
}
/**
* Get list of mapped sources.
*
* @return array
* List of mapped source names in an array.
*/
public function getMappingSourceList() {
$mappings = feeds_importer($this->id)->processor->config['mappings'];
$sources = array();
foreach ($mappings as $mapping) {
$sources[] = $mapping['source'];
}
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
@@ -257,7 +279,27 @@ class FeedsGeoTermElement extends FeedsTermElement {
* Enclosure element, can be part of the result array.
*/
class FeedsEnclosure extends FeedsElement {
protected $mime_type;
/**
* The mime type of the enclosure.
*
* @param string
*/
protected $mime_type;
/**
* The default list of allowed extensions.
*
* @param string
*/
protected $allowedExtensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp';
/**
* The sanitized local file name.
*
* @var string
*/
protected $safeFilename;
/**
* Constructor, requires MIME type.
@@ -280,6 +322,17 @@ class FeedsEnclosure extends FeedsElement {
return $this->mime_type;
}
/**
* Sets the list of allowed extensions.
*
* @param string $extensions
* The list of allowed extensions separated by a space.
*/
public function setAllowedExtensions($extensions) {
// Normalize whitespace so that empty extensions are not allowed.
$this->allowedExtensions = drupal_strtolower(trim(preg_replace('/\s+/', ' ', $extensions)));
}
/**
* Use this method instead of FeedsElement::getValue() when fetching the file
* from the URL.
@@ -294,20 +347,74 @@ class FeedsEnclosure extends FeedsElement {
}
/**
* Use this method instead of FeedsElement::getValue() to get the file name
* transformed for better local saving (underscores instead of spaces)
* Returns the full path to the file URI with a safe file name.
*
* @return
* Value with space characters changed to underscores.
* @return string
* The safe file URI.
*
* @see FeedsElement::getValue()
* @throws RuntimeException
* Thrown if the file extension is invalid.
*/
public function getLocalValue() {
return str_replace(' ', '_', $this->getValue());
public function getSanitizedUri() {
return drupal_dirname($this->getValue()) . '/' . $this->getSafeFilename();
}
/**
* @return
* Returns the file name transformed for better local saving.
*
* @return string
* Value with space characters changed to underscores.
*
* @throws RuntimeException
* Thrown if the file extension is invalid.
*/
public function getLocalValue() {
return str_replace(' ', '_', $this->getSafeFilename());
}
/**
* Returns the safe file name.
*
* @return string
* A filename that is safe to save to the filesystem.
*
* @throws RuntimeException
* Thrown if the file extension is invalid.
*/
protected function getSafeFilename() {
if (isset($this->safeFilename)) {
return $this->safeFilename;
}
// Strip any query string or fragment from file name.
list($filename) = explode('?', $this->getValue());
list($filename) = explode('#', $filename);
$filename = rawurldecode(drupal_basename($filename));
// Remove leading and trailing whitespace and periods.
$filename = trim($filename, " \t\n\r\0\x0B.");
if (strpos($filename, '.') === FALSE) {
$extension = FALSE;
}
else {
$extension = drupal_strtolower(substr($filename, strrpos($filename, '.') + 1));
}
if (!$extension || !in_array($extension, explode(' ', $this->allowedExtensions), TRUE)) {
throw new RuntimeException(t('The file @file has an invalid extension.', array('@file' => $filename)));
}
$this->safeFilename = file_munge_filename($filename, $this->allowedExtensions, FALSE);
return $this->safeFilename;
}
/**
* Downloads the content from the file URL.
*
* @return string
* The content of the referenced resource.
*/
public function getContent() {
@@ -333,18 +440,19 @@ class FeedsEnclosure extends FeedsElement {
* If file object could not be created.
*/
public function getFile($destination) {
$file = NULL;
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())) {
if (drupal_realpath($this->getSanitizedUri())) {
$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->uri = $this->getSanitizedUri();
$file->filemime = $this->getMIMEType();
$file->filename = $this->getSafeFilename();
if (drupal_dirname($file->uri) !== $destination) {
$file = file_copy($file, $destination);
}
else {
@@ -361,15 +469,17 @@ class FeedsEnclosure extends FeedsElement {
}
}
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 {
$filename = $this->getLocalValue();
if (module_exists('transliteration')) {
require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
$filename = transliteration_clean_filename($filename);
}
$file = file_save_data($this->getContent(), $destination . $filename);
}
catch (Exception $e) {
@@ -381,8 +491,9 @@ class FeedsEnclosure extends FeedsElement {
if (!$file) {
throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
}
return $file;
}
return $file;
}
}
@@ -458,13 +569,13 @@ class FeedsDateTimeElement extends FeedsElement {
* Helper method for buildDateField(). Build a FeedsDateTimeElement object
* from a standard formatted node.
*/
protected static function readDateField($entity, $field_name) {
protected static function readDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
$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}[$language][$delta]['date']) && $entity->{$field_name}[$language][$delta]['date'] instanceof FeedsDateTime) {
$ret->start = $entity->{$field_name}[$language][$delta]['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'];
if (isset($entity->{$field_name}[$language][$delta]['date2']) && $entity->{$field_name}[$language][$delta]['date2'] instanceof FeedsDateTime) {
$ret->end = $entity->{$field_name}[$language][$delta]['date2'];
}
return $ret;
}
@@ -472,15 +583,17 @@ class FeedsDateTimeElement extends FeedsElement {
/**
* Build a entity's date field from our object.
*
* @param $entity
* @param object $entity
* The entity to build the date field on.
* @param $field_name
* @param str $field_name
* The name of the field to build.
* @param int $delta
* The delta in the field.
*/
public function buildDateField($entity, $field_name) {
public function buildDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
$info = field_info_field($field_name);
$oldfield = FeedsDateTimeElement::readDateField($entity, $field_name);
$oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta, $language);
// Merge with any preexisting objects on the field; we take precedence.
$oldfield = $this->merge($oldfield);
$use_start = $oldfield->start;
@@ -513,27 +626,27 @@ class FeedsDateTimeElement extends FeedsElement {
$db_tz = new DateTimeZone($db_tz);
if (!isset($entity->{$field_name})) {
$entity->{$field_name} = array('und' => array());
$entity->{$field_name} = array($language => array());
}
if ($use_start) {
$entity->{$field_name}['und'][0]['timezone'] = $use_start->getTimezone()->getName();
$entity->{$field_name}['und'][0]['offset'] = $use_start->getOffset();
$entity->{$field_name}[$language][$delta]['timezone'] = $use_start->getTimezone()->getName();
$entity->{$field_name}[$language][$delta]['offset'] = $use_start->getOffset();
$use_start->setTimezone($db_tz);
$entity->{$field_name}['und'][0]['date'] = $use_start;
$entity->{$field_name}[$language][$delta]['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']));
$entity->{$field_name}[$language][$delta]['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();
$entity->{$field_name}[$language][$delta]['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']));
$entity->{$field_name}[$language][$delta]['date2'] = $use_end;
$entity->{$field_name}[$language][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
}
}
}
@@ -583,12 +696,17 @@ class FeedsDateTime extends DateTime {
* 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)) {
// Assume UNIX timestamp if it doesn't look like a simple year.
if (strlen($time) > 4) {
$time = "@" . $time;
}
// If it's a year, add a default month too, because PHP's date functions
// won't parse standalone years after 2000 correctly (see explanation at
// http://aaronsaray.com/blog/2007/07/11/helpful-strtotime-reminders/#comment-47).
else {
$time = 'January ' . $time;
}
}
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
@@ -596,7 +714,7 @@ class FeedsDateTime extends DateTime {
$time = str_replace("GMT+", "+", $time);
// Some PHP 5.2 version's DateTime class chokes on invalid dates.
if (!strtotime($time)) {
if (!date_create($time)) {
$time = 'now';
}

View File

@@ -20,13 +20,84 @@ class FeedsResult {}
abstract class FeedsPlugin extends FeedsConfigurable implements FeedsSourceInterface {
/**
* Constructor.
* The plugin definition.
*
* Initialize class variables.
* @var array
*/
protected $pluginDefinition;
/**
* Constructs a FeedsPlugin object.
*
* A copy of FeedsConfigurable::__construct() that doesn't call
* configDefaults() so that we avoid circular dependencies.
*
* @param string $id
* The importer id.
*/
protected function __construct($id) {
parent::__construct($id);
$this->source_config = $this->sourceDefaults();
$this->id = $id;
$this->export_type = FEEDS_EXPORT_NONE;
$this->disabled = FALSE;
}
/**
* Instantiates a FeedsPlugin object.
*
* Don't use directly, use feeds_plugin() instead.
*
* @see feeds_plugin()
*/
public static function instance($class, $id, array $plugin_definition = array()) {
if (!strlen($id)) {
throw new InvalidArgumentException(t('Empty configuration identifier.'));
}
$instances = &drupal_static(__METHOD__, array());
if (!isset($instances[$class][$id])) {
$instance = new $class($id);
// The ordering here is important. The plugin definition should be usable
// in getConfig().
$instance->setPluginDefinition($plugin_definition);
$instance->setConfig($instance->configDefaults());
$instances[$class][$id] = $instance;
}
return $instances[$class][$id];
}
/**
* Returns the type of plugin.
*
* @return string
* One of either 'fetcher', 'parser', or 'processor'.
*/
abstract public function pluginType();
/**
* Returns the plugin definition.
*
* @return array
* The plugin definition array.
*
* @see ctools_get_plugins()
*/
public function pluginDefinition() {
return $this->pluginDefinition;
}
/**
* Sets the plugin definition.
*
* This is protected since we're only using it in FeedsPlugin::instance().
*
* @param array $plugin_definition
* The plugin definition.
*/
protected function setPluginDefinition(array $plugin_definition) {
$this->pluginDefinition = $plugin_definition;
}
/**
@@ -90,7 +161,7 @@ abstract class FeedsPlugin extends FeedsConfigurable implements FeedsSourceInter
*
* @todo: Use CTools Plugin API.
*/
protected static function loadMappers() {
public static function loadMappers() {
static $loaded = FALSE;
if (!$loaded) {
$path = drupal_get_path('module', 'feeds') . '/mappers';
@@ -197,15 +268,102 @@ abstract class FeedsPlugin extends FeedsConfigurable implements FeedsSourceInter
}
return $result;
}
/**
* Implements FeedsConfigurable::dependencies().
*/
public function dependencies() {
$dependencies = parent::dependencies();
// Find out which module provides this plugin.
$plugin_info = $this->pluginDefinition();
if (isset($plugin_info['module'])) {
$dependencies[$plugin_info['module']] = $plugin_info['module'];
}
return $dependencies;
}
}
/**
* Used when a plugin is missing.
*/
class FeedsMissingPlugin extends FeedsPlugin {
public function pluginType() {
return 'missing';
}
public function save() {}
/**
* Fetcher methods.
*/
public function fetch(FeedsSource $source) {
return new FeedsFetcherResult('');
}
public function clear(FeedsSource $source) {}
public function request($feed_nid = 0) {
drupal_access_denied();
}
public function menuItem() {
return array();
}
public function subscribe(FeedsSource $source) {}
public function unsubscribe(FeedsSource $source) {}
public function importPeriod(FeedsSource $source) {}
/**
* Parser methods.
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
return new FeedsParserResult();
}
public function getMappingSources() {
return array();
}
/**
* Processor methods.
*/
public function process(FeedsSource $source, FeedsParserResult $parser_result) {}
public function entityType() {}
public function bundle() {}
public function bundleOptions() {
return array();
}
public function getLimit() {
return 0;
}
public function getMappings() {
return array();
}
public function getMappingTargets() {
return array();
}
public function expire(FeedsSource $source, $time = NULL) {}
public function itemCount(FeedsSource $source) {
return 0;
}
public function expiryTime() {
return FEEDS_EXPIRE_NEVER;
}
}
/**

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ class FeedsSimplePieEnclosure extends FeedsEnclosure {
/**
* Serialization helper.
*
* Handle the simplepie enclosure class seperately ourselves.
* Handle the simplepie enclosure class separately ourselves.
*/
public function __sleep() {
$this->_serialized_simplepie_enclosure = serialize($this->simplepie_enclosure);
@@ -52,6 +52,7 @@ class FeedsSimplePieEnclosure extends FeedsEnclosure {
public function getMIMEType() {
return $this->simplepie_enclosure->get_real_type();
}
}
/**
@@ -67,10 +68,6 @@ class FeedsSimplePieParser extends FeedsParser {
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());
@@ -149,8 +146,7 @@ class FeedsSimplePieParser extends FeedsParser {
}
// Release parser.
unset($parser);
// Set error reporting back to its previous value.
error_reporting($level);
return $result;
}
@@ -237,4 +233,5 @@ class FeedsSimplePieParser extends FeedsParser {
$words = array_slice($words, 0, 3);
return implode(' ', $words);
}
}

View File

@@ -22,6 +22,7 @@ class FeedsTermProcessor extends FeedsProcessor {
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Terms');
$info['bundle name'] = t('Vocabulary');
return $info;
}
@@ -30,25 +31,35 @@ class FeedsTermProcessor extends FeedsProcessor {
*/
protected function newEntity(FeedsSource $source) {
$vocabulary = $this->vocabulary();
$term = new stdClass();
$term = parent::newEntity($source);
$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.
* Load an existing entity.
*/
protected function entityLoad(FeedsSource $source, $tid) {
return taxonomy_term_load($tid);
protected function entityLoad(FeedsSource $source, $entity_id) {
$entity = parent::entityLoad($source, $entity_id);
// Avoid missing bundle errors when term has been loaded directly from db.
if (empty($entity->vocabulary_machine_name) && !empty($entity->vid)) {
$vocabulary = taxonomy_vocabulary_load($entity->vid);
$entity->vocabulary_machine_name = ($vocabulary) ? $vocabulary->machine_name : NULL;
}
return $entity;
}
/**
* Validates a term.
*/
protected function entityValidate($term) {
if (empty($term->name)) {
parent::entityValidate($term);
if (drupal_strlen($term->name) == 0) {
throw new FeedsValidationException(t('Term name missing.'));
}
}
@@ -90,37 +101,11 @@ class FeedsTermProcessor extends FeedsProcessor {
}
/**
* Override parent::configForm().
* Overrides parent::setTargetElement().
*
* Operate on a target item that is a taxonomy term.
*/
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) {
public function setTargetElement(FeedsSource $source, $target_term, $target_element, $value, array $mapping = array()) {
switch ($target_element) {
case 'parent':
if (!empty($value)) {
@@ -142,15 +127,26 @@ class FeedsTermProcessor extends FeedsProcessor {
$target_term->parent[] = 0;
}
break;
case 'parentguid':
// value is parent_guid field value
$parent_tid = 0;
$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;
$term_ids = array_keys($query->condition('guid', $value)->execute()->fetchAllAssoc('entity_id'));
if (!empty($term_ids)) {
$terms = entity_load($this->entityType(), $term_ids);
foreach ($terms as $term) {
if ($term->vid == $target_term->vid) {
$parent_tid = $term->tid;
break;
}
}
}
$target_term->parent[] = $parent_tid;
break;
case 'weight':
if (!empty($value)) {
$weight = intval($value);
@@ -160,6 +156,20 @@ class FeedsTermProcessor extends FeedsProcessor {
}
$target_term->weight = $weight;
break;
case 'description':
if (!empty($mapping['format'])) {
$target_term->format = $mapping['format'];
}
elseif (!empty($this->config['input_format'])) {
$target_term->format = $this->config['input_format'];
}
else {
$target_term->format = filter_fallback_format();
}
$target_term->description = $value;
break;
default:
parent::setTargetElement($source, $target_term, $target_element, $value);
break;
@@ -195,19 +205,13 @@ class FeedsTermProcessor extends FeedsProcessor {
'description' => array(
'name' => t('Term description'),
'description' => t('Description of the taxonomy term.'),
'summary_callbacks' => array('text_feeds_summary_callback'),
'form_callbacks' => array('text_feeds_form_callback'),
),
);
// 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.
}
$this->getHookTargets($targets);
return $targets;
}
@@ -235,11 +239,19 @@ class FeedsTermProcessor extends FeedsProcessor {
* 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;
}
if ($vocabulary = taxonomy_vocabulary_machine_name_load($this->bundle())) {
return $vocabulary;
}
throw new Exception(t('No vocabulary defined for Taxonomy Term processor.'));
}
/**
* Overrides FeedsProcessor::dependencies().
*/
public function dependencies() {
$dependencies = parent::dependencies();
$dependencies['taxonomy'] = 'taxonomy';
return $dependencies;
}
}

View File

@@ -2,13 +2,21 @@
/**
* @file
* FeedsUserProcessor class.
* Contains FeedsUserProcessor.
*/
/**
* Option to block users not found in the feed.
*
* @var string
*/
define('FEEDS_BLOCK_NON_EXISTENT', 'block');
/**
* Feeds processor plugin. Create users from feed items.
*/
class FeedsUserProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
@@ -29,10 +37,11 @@ class FeedsUserProcessor extends FeedsProcessor {
* Creates a new user account in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$account = new stdClass();
$account = parent::newEntity($source);
$account->uid = 0;
$account->roles = array_filter($this->config['roles']);
$account->status = $this->config['status'];
return $account;
}
@@ -40,8 +49,9 @@ class FeedsUserProcessor extends FeedsProcessor {
* Loads an existing user.
*/
protected function entityLoad(FeedsSource $source, $uid) {
$user = parent::entityLoad($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;
}
@@ -50,6 +60,8 @@ class FeedsUserProcessor extends FeedsProcessor {
* Validates a user account.
*/
protected function entityValidate($account) {
parent::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.'));
}
@@ -87,9 +99,7 @@ class FeedsUserProcessor extends FeedsProcessor {
* Delete multiple user accounts.
*/
protected function entityDeleteMultiple($uids) {
foreach ($uids as $uid) {
user_delete($uid);
}
user_delete_multiple($uids);
}
/**
@@ -127,19 +137,13 @@ class FeedsUserProcessor extends FeedsProcessor {
'#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'],
);
$form['update_non_existent']['#options'][FEEDS_BLOCK_NON_EXISTENT] = t('Block non-existent users');
return $form;
}
@@ -201,11 +205,7 @@ class FeedsUserProcessor extends FeedsProcessor {
);
}
// Let other modules expose mapping targets.
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->entityType();
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
$this->getHookTargets($targets);
return $targets;
}
@@ -239,4 +239,35 @@ class FeedsUserProcessor extends FeedsProcessor {
}
return 0;
}
/**
* Overrides FeedsProcessor::clean().
*
* Block users instead of deleting them.
*
* @param FeedsState $state
* The FeedsState object for the given stage.
*/
protected function clean(FeedsState $state) {
// Delegate to parent if not blocking or option not set.
if (!isset($this->config['update_non_existent']) || $this->config['update_non_existent'] !== FEEDS_BLOCK_NON_EXISTENT) {
return parent::clean($state);
}
if (!empty($state->removeList)) {
// @see user_user_operations_block().
// The following foreach is copied from above function but with an added
// counter to count blocked users.
foreach (user_load_multiple($state->removeList) as $account) {
$this->loadItemInfo($account);
$account->feeds_item->hash = $this->config['update_non_existent'];
// For efficiency manually save the original account before applying any
// changes.
$account->original = clone $account;
user_save($account, array('status' => 0));
$state->blocked++;
}
}
}
}