Commit cf882741 authored by Dries's avatar Dries

Issue #293318 by alex_b, Berdir, rootatwc, Aron Novak, mustafau, akahn:...

Issue #293318 by alex_b, Berdir, rootatwc, Aron Novak, mustafau, akahn: Convert Aggregator feeds into entities.
parent 8a4e0eb9
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\Core\Entity; namespace Drupal\Core\Entity;
use Drupal\Core\Language\Language;
use Drupal\Core\TypedData\ContextAwareInterface; use Drupal\Core\TypedData\ContextAwareInterface;
use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Component\Uuid\Uuid; use Drupal\Component\Uuid\Uuid;
...@@ -264,8 +265,11 @@ public function isEmpty() { ...@@ -264,8 +265,11 @@ public function isEmpty() {
* Implements \Drupal\Core\TypedData\TranslatableInterface::language(). * Implements \Drupal\Core\TypedData\TranslatableInterface::language().
*/ */
public function language() { public function language() {
// Get the language code if the property exists.
if ($this->getPropertyDefinition('langcode')) {
$language = $this->get('langcode')->language; $language = $this->get('langcode')->language;
if (!$language) { }
if (!isset($language)) {
// Make sure we return a proper language object. // Make sure we return a proper language object.
$language = new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED)); $language = new Language(array('langcode' => LANGUAGE_NOT_SPECIFIED));
} }
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Field\Type\UriItem.
*/
namespace Drupal\Core\Entity\Field\Type;
use Drupal\Core\Entity\Field\FieldItemBase;
/**
* Defines the 'uri_field' entity field item.
*/
class UriItem extends FieldItemBase {
/**
* Field definitions of the contained properties.
*
* @see self::getPropertyDefinitions()
*
* @var array
*/
static $propertyDefinitions;
/**
* Implements ComplexDataInterface::getPropertyDefinitions().
*/
public function getPropertyDefinitions() {
if (!isset(self::$propertyDefinitions)) {
self::$propertyDefinitions['value'] = array(
'type' => 'string',
'label' => t('Text value'),
);
}
return self::$propertyDefinitions;
}
}
This diff is collapsed.
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
* Parser functions for the aggregator module. * Parser functions for the aggregator module.
*/ */
use Drupal\aggregator\Plugin\Core\Entity\Feed;
/** /**
* Implements hook_aggregator_parse_info(). * Implements hook_aggregator_parse_info().
*/ */
...@@ -18,7 +20,7 @@ function aggregator_aggregator_parse_info() { ...@@ -18,7 +20,7 @@ function aggregator_aggregator_parse_info() {
/** /**
* Implements hook_aggregator_parse(). * Implements hook_aggregator_parse().
*/ */
function aggregator_aggregator_parse($feed) { function aggregator_aggregator_parse(Feed $feed) {
global $channel, $image; global $channel, $image;
// Filter the input data. // Filter the input data.
...@@ -35,9 +37,9 @@ function aggregator_aggregator_parse($feed) { ...@@ -35,9 +37,9 @@ function aggregator_aggregator_parse($feed) {
} }
// Add parsed data to the feed object. // Add parsed data to the feed object.
$feed->link = !empty($channel['link']) ? $channel['link'] : ''; $feed->link->value = !empty($channel['link']) ? $channel['link'] : '';
$feed->description = !empty($channel['description']) ? $channel['description'] : ''; $feed->description->value = !empty($channel['description']) ? $channel['description'] : '';
$feed->image = !empty($image['url']) ? $image['url'] : ''; $feed->image->value = !empty($image['url']) ? $image['url'] : '';
// Clear the page and block caches. // Clear the page and block caches.
cache_invalidate_tags(array('content' => TRUE)); cache_invalidate_tags(array('content' => TRUE));
...@@ -51,15 +53,15 @@ function aggregator_aggregator_parse($feed) { ...@@ -51,15 +53,15 @@ function aggregator_aggregator_parse($feed) {
/** /**
* Parses a feed and stores its items. * Parses a feed and stores its items.
* *
* @param $data * @param string $data
* The feed data. * The feed data.
* @param $feed * @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
* An object describing the feed to be parsed. * An object describing the feed to be parsed.
* *
* @return * @return
* FALSE on error, TRUE otherwise. * FALSE on error, TRUE otherwise.
*/ */
function aggregator_parse_feed(&$data, $feed) { function aggregator_parse_feed(&$data, Feed $feed) {
global $items, $image, $channel; global $items, $image, $channel;
// Unset the global variables before we use them. // Unset the global variables before we use them.
...@@ -74,8 +76,8 @@ function aggregator_parse_feed(&$data, $feed) { ...@@ -74,8 +76,8 @@ function aggregator_parse_feed(&$data, $feed) {
xml_set_character_data_handler($xml_parser, 'aggregator_element_data'); xml_set_character_data_handler($xml_parser, 'aggregator_element_data');
if (!xml_parse($xml_parser, $data, 1)) { if (!xml_parse($xml_parser, $data, 1)) {
watchdog('aggregator', 'The feed from %site seems to be broken due to an error "%error" on line %line.', array('%site' => $feed->title, '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser)), WATCHDOG_WARNING); watchdog('aggregator', 'The feed from %site seems to be broken due to an error "%error" on line %line.', array('%site' => $feed->label(), '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser)), WATCHDOG_WARNING);
drupal_set_message(t('The feed from %site seems to be broken because of error "%error" on line %line.', array('%site' => $feed->title, '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser))), 'error'); drupal_set_message(t('The feed from %site seems to be broken because of error "%error" on line %line.', array('%site' => $feed->label(), '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser))), 'error');
return FALSE; return FALSE;
} }
xml_parser_free($xml_parser); xml_parser_free($xml_parser);
...@@ -111,7 +113,7 @@ function aggregator_parse_feed(&$data, $feed) { ...@@ -111,7 +113,7 @@ function aggregator_parse_feed(&$data, $feed) {
$item['link'] = $item['link']; $item['link'] = $item['link'];
} }
else { else {
$item['link'] = $feed->link; $item['link'] = $feed->link->value;
} }
// Atom feeds have an ID tag instead of a GUID tag. // Atom feeds have an ID tag instead of a GUID tag.
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
* Processor functions for the aggregator module. * Processor functions for the aggregator module.
*/ */
use Drupal\aggregator\Plugin\Core\Entity\Feed;
/** /**
* Implements hook_aggregator_process_info(). * Implements hook_aggregator_process_info().
*/ */
...@@ -22,26 +24,46 @@ function aggregator_aggregator_process($feed) { ...@@ -22,26 +24,46 @@ function aggregator_aggregator_process($feed) {
if (is_object($feed)) { if (is_object($feed)) {
if (is_array($feed->items)) { if (is_array($feed->items)) {
foreach ($feed->items as $item) { foreach ($feed->items as $item) {
// @todo: The default entity render controller always returns an empty
// array, which is ignored in aggregator_save_item() currently. Should
// probably be fixed.
if (empty($item['title'])) {
continue;
}
// Save this item. Try to avoid duplicate entries as much as possible. If // Save this item. Try to avoid duplicate entries as much as possible. If
// we find a duplicate entry, we resolve it and pass along its ID is such // we find a duplicate entry, we resolve it and pass along its ID is such
// that we can update it if needed. // that we can update it if needed.
if (!empty($item['guid'])) { if (!empty($item['guid'])) {
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND guid = :guid", array(':fid' => $feed->fid, ':guid' => $item['guid']))->fetchObject(); $values = array('fid' => $feed->id(), 'guid' => $item['guid']);
} }
elseif ($item['link'] && $item['link'] != $feed->link && $item['link'] != $feed->url) { elseif ($item['link'] && $item['link'] != $feed->link && $item['link'] != $feed->url) {
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND link = :link", array(':fid' => $feed->fid, ':link' => $item['link']))->fetchObject(); $values = array('fid' => $feed->id(), 'link' => $item['link']);
} }
else { else {
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND title = :title", array(':fid' => $feed->fid, ':title' => $item['title']))->fetchObject(); $values = array('fid' => $feed->id(), 'title' => $item['title']);
} }
if (!$item['timestamp']) {
$item['timestamp'] = isset($entry->timestamp) ? $entry->timestamp : REQUEST_TIME; // Try to load an existing entry.
if ($entry = entity_load_multiple_by_properties('aggregator_item', $values)) {
$entry = reset($entry);
}
else {
$entry = entity_create('aggregator_item', array());
}
if ($item['timestamp']) {
$entry->timestamp->value = $item['timestamp'];
} }
// Make sure the item title and author fit in the 255 varchar column. // Make sure the item title and author fit in the 255 varchar column.
$item['title'] = truncate_utf8($item['title'], 255, TRUE, TRUE); $entry->title->value = truncate_utf8($item['title'], 255, TRUE, TRUE);
$item['author'] = truncate_utf8($item['author'], 255, TRUE, TRUE); $entry->author->value = truncate_utf8($item['author'], 255, TRUE, TRUE);
aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid : ''), 'fid' => $feed->fid, 'timestamp' => $item['timestamp'], 'title' => $item['title'], 'link' => $item['link'], 'author' => $item['author'], 'description' => $item['description'], 'guid' => $item['guid']));
$entry->fid->value = $feed->id();
$entry->link->value = $item['link'];
$entry->description->value = $item['description'];
$entry->guid->value = $item['guid'];
$entry->save();
} }
} }
} }
...@@ -51,17 +73,10 @@ function aggregator_aggregator_process($feed) { ...@@ -51,17 +73,10 @@ function aggregator_aggregator_process($feed) {
* Implements hook_aggregator_remove(). * Implements hook_aggregator_remove().
*/ */
function aggregator_aggregator_remove($feed) { function aggregator_aggregator_remove($feed) {
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchCol(); $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->id()))->fetchCol();
if ($iids) { entity_delete_multiple('aggregator_item', $iids);
db_delete('aggregator_category_item')
->condition('iid', $iids, 'IN')
->execute();
}
db_delete('aggregator_item')
->condition('fid', $feed->fid)
->execute();
drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed->title))); drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed->label())));
} }
/** /**
...@@ -144,72 +159,25 @@ function _aggregator_characters($length) { ...@@ -144,72 +159,25 @@ function _aggregator_characters($length) {
return ($length == 0) ? t('Unlimited') : format_plural($length, '1 character', '@count characters'); return ($length == 0) ? t('Unlimited') : format_plural($length, '1 character', '@count characters');
} }
/**
* Adds/edits/deletes an aggregator item.
*
* @param $edit
* An associative array describing the item to be added/edited/deleted.
*/
function aggregator_save_item($edit) {
if ($edit['title'] && empty($edit['iid'])) {
$edit['iid'] = db_insert('aggregator_item')
->fields(array(
'title' => $edit['title'],
'link' => $edit['link'],
'author' => $edit['author'],
'description' => $edit['description'],
'guid' => $edit['guid'],
'timestamp' => $edit['timestamp'],
'fid' => $edit['fid'],
))
->execute();
}
if ($edit['iid'] && !$edit['title']) {
db_delete('aggregator_item')
->condition('iid', $edit['iid'])
->execute();
db_delete('aggregator_category_item')
->condition('iid', $edit['iid'])
->execute();
}
elseif ($edit['title'] && $edit['link']) {
// file the items in the categories indicated by the feed
$result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $edit['fid']));
foreach ($result as $category) {
db_merge('aggregator_category_item')
->key(array(
'iid' => $edit['iid'],
'cid' => $category->cid,
))
->execute();
}
}
}
/** /**
* Expires items from a feed depending on expiration settings. * Expires items from a feed depending on expiration settings.
* *
* @param $feed * @param \Drupal\aggregator\Plugin\Core\Entity\Feed $feed
* Object describing feed. * Object describing feed.
*/ */
function aggregator_expire($feed) { function aggregator_expire(Feed $feed) {
$aggregator_clear = config('aggregator.settings')->get('items.expire'); $aggregator_clear = config('aggregator.settings')->get('items.expire');
if ($aggregator_clear != AGGREGATOR_CLEAR_NEVER) { if ($aggregator_clear != AGGREGATOR_CLEAR_NEVER) {
// Remove all items that are older than flush item timer. // Remove all items that are older than flush item timer.
$age = REQUEST_TIME - $aggregator_clear; $age = REQUEST_TIME - $aggregator_clear;
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid AND timestamp < :timestamp', array( $iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid AND timestamp < :timestamp', array(
':fid' => $feed->fid, ':fid' => $feed->id(),
':timestamp' => $age, ':timestamp' => $age,
)) ))
->fetchCol(); ->fetchCol();
if ($iids) { if ($iids) {
db_delete('aggregator_category_item') entity_delete_multiple('aggregator_item', $iids);
->condition('iid', $iids, 'IN')
->execute();
db_delete('aggregator_item')
->condition('iid', $iids, 'IN')
->execute();
} }
} }
} }
<?php
/**
* @file
* Contains \Drupal\aggregator\FeedFormController.
*/
namespace Drupal\aggregator;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityFormControllerNG;
/**
* Form controller for the aggregator feed edit forms.
*/
class FeedFormController extends EntityFormControllerNG {
/**
* Overrides Drupal\Core\Entity\EntityFormController::form().
*/
public function form(array $form, array &$form_state, EntityInterface $feed) {
$period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
$period[AGGREGATOR_CLEAR_NEVER] = t('Never');
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $feed->label(),
'#maxlength' => 255,
'#description' => t('The name of the feed (or the name of the website providing the feed).'),
'#required' => TRUE,
);
$form['url'] = array(
'#type' => 'url',
'#title' => t('URL'),
'#default_value' => $feed->url->value,
'#maxlength' => NULL,
'#description' => t('The fully-qualified URL of the feed.'),
'#required' => TRUE,
);
$form['refresh'] = array('#type' => 'select',
'#title' => t('Update interval'),
'#default_value' => $feed->refresh->value,
'#options' => $period,
'#description' => t('The length of time between feed updates. Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))),
);
$form['block'] = array('#type' => 'select',
'#title' => t('News items in block'),
'#default_value' => $feed->block->value,
'#options' => drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)),
'#description' => t("Drupal can make a block with the most recent news items of this feed. You can <a href=\"@block-admin\">configure blocks</a> to be displayed in the sidebar of your page. This setting lets you configure the number of news items to show in this feed's block. If you choose '0' this feed's block will be disabled.", array('@block-admin' => url('admin/structure/block'))),
);
// Handling of categories.
$options = array();
$values = array();
$categories = db_query('SELECT c.cid, c.title FROM {aggregator_category} c ORDER BY title');
foreach ($categories as $category) {
$options[$category->cid] = check_plain($category->title);
if (!empty($feed->categories) && in_array($category->cid, array_keys($feed->categories))) {
$values[] = $category->cid;
}
}
if ($options) {
$form['category'] = array(
'#type' => 'checkboxes',
'#title' => t('Categorize news items'),
'#default_value' => $values,
'#options' => $options,
'#description' => t('New feed items are automatically filed in the checked categories.'),
);
}
return parent::form($form, $form_state, $feed);
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::validate().
*/
public function validate(array $form, array &$form_state) {
$feed = $this->buildEntity($form, $form_state);
// Check for duplicate titles.
if ($feed->id()) {
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = :title OR url = :url) AND fid <> :fid", array(':title' => $feed->label(), ':url' => $feed->url->value, ':fid' => $feed->id()));
}
else {
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $feed->label(), ':url' => $feed->url->value));
}
foreach ($result as $item) {
if (strcasecmp($item->title, $feed->label()) == 0) {
form_set_error('title', t('A feed named %feed already exists. Enter a unique title.', array('%feed' => $feed->label())));
}
if (strcasecmp($item->url, $feed->url->value) == 0) {
form_set_error('url', t('A feed with this URL %url already exists. Enter a unique URL.', array('%url' => $feed->url->value)));
}
}
parent::validate($form, $form_state);
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::save().
*/
public function save(array $form, array &$form_state) {
$feed = $this->getEntity($form_state);
$insert = (bool) $feed->id();
if (!empty($form_state['values']['category'])) {
// Store category values for post save operations.
// @see Drupal\Core\Entity\FeedStorageController::postSave()
$feed->categories = $form_state['values']['category'];
}
$feed->save();
if ($insert) {
drupal_set_message(t('The feed %feed has been updated.', array('%feed' => $feed->label())));
if (arg(0) == 'admin') {
$form_state['redirect'] = 'admin/config/services/aggregator';
}
else {
$form_state['redirect'] = 'aggregator/sources/' . $feed->id();
}
}
else {
watchdog('aggregator', 'Feed %feed added.', array('%feed' => $feed->label()), WATCHDOG_NOTICE, l(t('view'), 'admin/config/services/aggregator'));
drupal_set_message(t('The feed %feed has been added.', array('%feed' => $feed->label())));
}
}
/**
* Overrides Drupal\Core\Entity\EntityFormController::delete().
*/
public function delete(array $form, array &$form_state) {
$feed = $this->getEntity($form_state);
$feed->delete();
watchdog('aggregator', 'Feed %feed deleted.', array('%feed' => $feed->label()));
drupal_set_message(t('The feed %feed has been deleted.', array('%feed' => $feed->label())));
if (arg(0) == 'admin') {
$form_state['redirect'] = 'admin/config/services/aggregator';
}
else {
$form_state['redirect'] = 'aggregator/sources';
}
}
}
<?php
/**
* @file
* Contains \Drupal\aggregator\FeedRenderController.
*/
namespace Drupal\aggregator;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRenderController;
/**
* Render controller for aggregator feed items.
*/
class FeedRenderController extends EntityRenderController {
/**
* Overrides Drupal\Core\Entity\EntityRenderController::getBuildDefaults().
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
$defaults = parent::getBuildDefaults($entity, $view_mode, $langcode);
$defaults['#theme'] = 'aggregator_feed_source';
return $defaults;
}
}
<?php
/**
* @file
* Contains \Drupal\aggregator\FeedStorageController.
*/
namespace Drupal\aggregator;
use Drupal\Core\Entity\DatabaseStorageControllerNG;
use Drupal\Core\Entity\EntityInterface;
/**
* Controller class for aggregators feeds.
*
* This extends the Drupal\Core\Entity\DatabaseStorageController class, adding
* required special handling for feed entities.
*/
class FeedStorageController extends DatabaseStorageControllerNG {
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::create().
*/
public function create(array $values) {
$values += array(
'link' => '',
'description' => '',
'image' => '',
);
return parent::create($values);
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::attachLoad().
*/
protected function attachLoad(&$queried_entities, $load_revision = FALSE) {
parent::attachLoad($queried_entities, $load_revision);
foreach ($queried_entities as $item) {
$item->categories = db_query('SELECT c.cid, c.title FROM {aggregator_category} c JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => $item->id()))->fetchAllKeyed();
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::preDelete().
*/
protected function preDelete($entities) {
parent::preDelete($entities);
// Invalidate the block cache to update aggregator feed-based derivatives.
if (module_exists('block')) {
drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
}
foreach ($entities as $entity) {
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $entity->id()))->fetchCol();
if ($iids) {
entity_delete_multiple('aggregator_item', $iids);
}
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::postDelete().
*/
protected function postDelete($entities) {
parent::postDelete($entities);
foreach ($entities as $entity) {
// Make sure there is no active block for this feed.
$block_configs = config_get_storage_names_with_prefix('plugin.core.block');
foreach ($block_configs as $config_id) {
$config = config($config_id);
if ($config->get('id') == 'aggregator_feed_block:' . $entity->id()) {
$config->delete();
}
}
}
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::preSave().
*/
protected function preSave(EntityInterface $entity) {
parent::preSave($entity);
// Invalidate the block cache to update aggregator feed-based derivatives.
if (module_exists('block')) {
drupal_container()->get('plugin.manager.block')->clearCachedDefinitions();
}
// An existing feed is being modified, delete the category listings.
db_delete('aggregator_category_feed')
->condition('fid', $entity->id())
->execute();
}
/**
* Overrides Drupal\Core\Entity\DataBaseStorageController::postSave().
*/
protected function postSave(EntityInterface $entity, $update) {
parent::postSave($entity, $update);
if (!empty($entity->categories)) {
foreach ($entity->categories as $cid => $value) {
if ($value) {
db_insert('aggregator_category_feed')
->fields(array(
'fid' => $entity->id(),
'cid' => $cid,
))
->execute();
}
}
}
}
/**
* Implements Drupal\Core\Entity\DataBaseStorageControllerNG::baseFieldDefinitions().
*/
public function baseFieldDefinitions() {
$fields['fid'] = array(
'label' => t('ID'),
'description' => t('The ID of the aggregor feed.'),
'type' => 'integer_field',
'read-only' => TRUE,
);
$fields['title'] = array(
'label' => t('Title'),
'description' => t('The title of the feed.'),
'type' => 'string_field',
);
$fields['url'] = array(
'label' => t('URL'),
'description' => t('The URL to the feed.'),
'type' => 'uri_field',
);
$fields['refresh'] = array(
'label' => t('Refresh'),
'description' => t('How often to check for new feed items, in seconds.'),
'type' => 'integer_field',
);
$fields['checked'] = array(
'label' => t('Checked'),