1<?php
2
3namespace Drupal\aggregator\Plugin\aggregator\processor;
4
5use Drupal\aggregator\Entity\Item;
6use Drupal\aggregator\FeedInterface;
7use Drupal\aggregator\FeedStorageInterface;
8use Drupal\aggregator\ItemStorageInterface;
9use Drupal\aggregator\Plugin\AggregatorPluginSettingsBase;
10use Drupal\aggregator\Plugin\ProcessorInterface;
11use Drupal\Component\Utility\Unicode;
12use Drupal\Core\Config\ConfigFactoryInterface;
13use Drupal\Core\Datetime\DateFormatterInterface;
14use Drupal\Core\Form\ConfigFormBaseTrait;
15use Drupal\Core\Form\FormStateInterface;
16use Drupal\Core\Messenger\MessengerInterface;
17use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
18use Drupal\Core\Url;
19use Symfony\Component\DependencyInjection\ContainerInterface;
20
21/**
22 * Defines a default processor implementation.
23 *
24 * Creates lightweight records from feed items.
25 *
26 * @AggregatorProcessor(
27 *   id = "aggregator",
28 *   title = @Translation("Default processor"),
29 *   description = @Translation("Creates lightweight records from feed items.")
30 * )
31 */
32class DefaultProcessor extends AggregatorPluginSettingsBase implements ProcessorInterface, ContainerFactoryPluginInterface {
33
34  use ConfigFormBaseTrait;
35
36  /**
37   * Contains the configuration object factory.
38   *
39   * @var \Drupal\Core\Config\ConfigFactoryInterface
40   */
41  protected $configFactory;
42
43  /**
44   * The entity storage for items.
45   *
46   * @var \Drupal\aggregator\ItemStorageInterface
47   */
48  protected $itemStorage;
49
50  /**
51   * The date formatter service.
52   *
53   * @var \Drupal\Core\Datetime\DateFormatterInterface
54   */
55  protected $dateFormatter;
56
57  /**
58   * The messenger.
59   *
60   * @var \Drupal\Core\Messenger\MessengerInterface
61   */
62  protected $messenger;
63
64  /**
65   * Constructs a DefaultProcessor object.
66   *
67   * @param array $configuration
68   *   A configuration array containing information about the plugin instance.
69   * @param string $plugin_id
70   *   The plugin_id for the plugin instance.
71   * @param mixed $plugin_definition
72   *   The plugin implementation definition.
73   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
74   *   The configuration factory object.
75   * @param \Drupal\aggregator\ItemStorageInterface $item_storage
76   *   The entity storage for feed items.
77   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
78   *   The date formatter service.
79   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
80   *   The messenger.
81   */
82  public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config, ItemStorageInterface $item_storage, DateFormatterInterface $date_formatter, MessengerInterface $messenger) {
83    $this->configFactory = $config;
84    $this->itemStorage = $item_storage;
85    $this->dateFormatter = $date_formatter;
86    $this->messenger = $messenger;
87    // @todo Refactor aggregator plugins to ConfigEntity so merging
88    //   the configuration here is not needed.
89    parent::__construct($configuration + $this->getConfiguration(), $plugin_id, $plugin_definition);
90  }
91
92  /**
93   * {@inheritdoc}
94   */
95  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
96    return new static(
97      $configuration,
98      $plugin_id,
99      $plugin_definition,
100      $container->get('config.factory'),
101      $container->get('entity_type.manager')->getStorage('aggregator_item'),
102      $container->get('date.formatter'),
103      $container->get('messenger')
104    );
105  }
106
107  /**
108   * {@inheritdoc}
109   */
110  protected function getEditableConfigNames() {
111    return ['aggregator.settings'];
112  }
113
114  /**
115   * {@inheritdoc}
116   */
117  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
118    $config = $this->config('aggregator.settings');
119    $processors = $config->get('processors');
120    $info = $this->getPluginDefinition();
121    $counts = [3, 5, 10, 15, 20, 25];
122    $items = array_map(function ($count) {
123      return $this->formatPlural($count, '1 item', '@count items');
124    }, array_combine($counts, $counts));
125    $intervals = [3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800];
126    $period = array_map([$this->dateFormatter, 'formatInterval'], array_combine($intervals, $intervals));
127    $period[FeedStorageInterface::CLEAR_NEVER] = t('Never');
128
129    $form['processors'][$info['id']] = [];
130    // Only wrap into details if there is a basic configuration.
131    if (isset($form['basic_conf'])) {
132      $form['processors'][$info['id']] = [
133        '#type' => 'details',
134        '#title' => t('Default processor settings'),
135        '#description' => $info['description'],
136        '#open' => in_array($info['id'], $processors),
137      ];
138    }
139
140    $form['processors'][$info['id']]['aggregator_summary_items'] = [
141      '#type' => 'select',
142      '#title' => t('Number of items shown in listing pages'),
143      '#default_value' => $config->get('source.list_max'),
144      '#empty_value' => 0,
145      '#options' => $items,
146    ];
147
148    $form['processors'][$info['id']]['aggregator_clear'] = [
149      '#type' => 'select',
150      '#title' => t('Discard items older than'),
151      '#default_value' => $config->get('items.expire'),
152      '#options' => $period,
153      '#description' => t('Requires a correctly configured <a href=":cron">cron maintenance task</a>.', [':cron' => Url::fromRoute('system.status')->toString()]),
154    ];
155
156    $lengths = [0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000];
157    $options = array_map(function ($length) {
158      return ($length == 0) ? t('Unlimited') : $this->formatPlural($length, '1 character', '@count characters');
159    }, array_combine($lengths, $lengths));
160
161    $form['processors'][$info['id']]['aggregator_teaser_length'] = [
162      '#type' => 'select',
163      '#title' => t('Length of trimmed description'),
164      '#default_value' => $config->get('items.teaser_length'),
165      '#options' => $options,
166      '#description' => t('The maximum number of characters used in the trimmed version of content.'),
167    ];
168    return $form;
169  }
170
171  /**
172   * {@inheritdoc}
173   */
174  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
175    $this->configuration['items']['expire'] = $form_state->getValue('aggregator_clear');
176    $this->configuration['items']['teaser_length'] = $form_state->getValue('aggregator_teaser_length');
177    $this->configuration['source']['list_max'] = $form_state->getValue('aggregator_summary_items');
178    // @todo Refactor aggregator plugins to ConfigEntity so this is not needed.
179    $this->setConfiguration($this->configuration);
180  }
181
182  /**
183   * {@inheritdoc}
184   */
185  public function process(FeedInterface $feed) {
186    if (!is_array($feed->items)) {
187      return;
188    }
189    foreach ($feed->items as $item) {
190      // @todo The default entity view builder always returns an empty
191      //   array, which is ignored in aggregator_save_item() currently. Should
192      //   probably be fixed.
193      if (empty($item['title'])) {
194        continue;
195      }
196
197      // Save this item. Try to avoid duplicate entries as much as possible. If
198      // we find a duplicate entry, we resolve it and pass along its ID is such
199      // that we can update it if needed.
200      if (!empty($item['guid'])) {
201        $values = ['fid' => $feed->id(), 'guid' => $item['guid']];
202      }
203      elseif ($item['link'] && $item['link'] != $feed->link && $item['link'] != $feed->url) {
204        $values = ['fid' => $feed->id(), 'link' => $item['link']];
205      }
206      else {
207        $values = ['fid' => $feed->id(), 'title' => $item['title']];
208      }
209
210      // Try to load an existing entry.
211      if ($entry = $this->itemStorage->loadByProperties($values)) {
212        $entry = reset($entry);
213      }
214      else {
215        $entry = Item::create(['langcode' => $feed->language()->getId()]);
216      }
217      if ($item['timestamp']) {
218        $entry->setPostedTime($item['timestamp']);
219      }
220
221      // Make sure the item title and author fit in the 255 varchar column.
222      $entry->setTitle(Unicode::truncate($item['title'], 255, TRUE, TRUE));
223      $entry->setAuthor(Unicode::truncate($item['author'], 255, TRUE, TRUE));
224
225      $entry->setFeedId($feed->id());
226      $entry->setLink($item['link']);
227      $entry->setGuid($item['guid']);
228
229      $description = '';
230      if (!empty($item['description'])) {
231        $description = $item['description'];
232      }
233      $entry->setDescription($description);
234
235      $entry->save();
236    }
237  }
238
239  /**
240   * {@inheritdoc}
241   */
242  public function delete(FeedInterface $feed) {
243    if ($items = $this->itemStorage->loadByFeed($feed->id())) {
244      $this->itemStorage->delete($items);
245    }
246    // @todo This should be moved out to caller with a different message maybe.
247    $this->messenger->addStatus(t('The news items from %site have been deleted.', ['%site' => $feed->label()]));
248  }
249
250  /**
251   * Implements \Drupal\aggregator\Plugin\ProcessorInterface::postProcess().
252   *
253   * Expires items from a feed depending on expiration settings.
254   */
255  public function postProcess(FeedInterface $feed) {
256    $aggregator_clear = $this->configuration['items']['expire'];
257
258    if ($aggregator_clear != FeedStorageInterface::CLEAR_NEVER) {
259      // Delete all items that are older than flush item timer.
260      $age = REQUEST_TIME - $aggregator_clear;
261      $result = $this->itemStorage->getQuery()
262        ->accessCheck(FALSE)
263        ->condition('fid', $feed->id())
264        ->condition('timestamp', $age, '<')
265        ->execute();
266      if ($result) {
267        $entities = $this->itemStorage->loadMultiple($result);
268        $this->itemStorage->delete($entities);
269      }
270    }
271  }
272
273  /**
274   * {@inheritdoc}
275   */
276  public function getConfiguration() {
277    return $this->configFactory->get('aggregator.settings')->get();
278  }
279
280  /**
281   * {@inheritdoc}
282   */
283  public function setConfiguration(array $configuration) {
284    $config = $this->config('aggregator.settings');
285    foreach ($configuration as $key => $value) {
286      $config->set($key, $value);
287    }
288    $config->save();
289  }
290
291}
292