59e70d9727abadcf9a3929c9ee055c5ec386aeaf
[yaffs-website] / Form / OpmlFeedAdd.php
1 <?php
2
3 namespace Drupal\aggregator\Form;
4
5 use Drupal\aggregator\FeedStorageInterface;
6 use Drupal\Component\Utility\UrlHelper;
7 use Drupal\Core\Form\FormBase;
8 use Drupal\Core\Form\FormStateInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10 use GuzzleHttp\Exception\RequestException;
11 use GuzzleHttp\ClientInterface;
12
13 /**
14  * Imports feeds from OPML.
15  */
16 class OpmlFeedAdd extends FormBase {
17
18   /**
19    * The feed storage.
20    *
21    * @var \Drupal\aggregator\FeedStorageInterface
22    */
23   protected $feedStorage;
24
25   /**
26    * The HTTP client to fetch the feed data with.
27    *
28    * @var \GuzzleHttp\ClientInterface
29    */
30   protected $httpClient;
31
32   /**
33    * Constructs a database object.
34    *
35    * @param \Drupal\aggregator\FeedStorageInterface $feed_storage
36    *   The feed storage.
37    * @param \GuzzleHttp\ClientInterface $http_client
38    *   The Guzzle HTTP client.
39    */
40   public function __construct(FeedStorageInterface $feed_storage, ClientInterface $http_client) {
41     $this->feedStorage = $feed_storage;
42     $this->httpClient = $http_client;
43   }
44
45   /**
46    * {@inheritdoc}
47    */
48   public static function create(ContainerInterface $container) {
49     return new static(
50       $container->get('entity.manager')->getStorage('aggregator_feed'),
51       $container->get('http_client')
52     );
53   }
54
55   /**
56    * {@inheritdoc}
57    */
58   public function getFormId() {
59     return 'aggregator_opml_add';
60   }
61
62   /**
63    * {@inheritdoc}
64    */
65   public function buildForm(array $form, FormStateInterface $form_state) {
66     $intervals = [900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200];
67     $period = array_map([\Drupal::service('date.formatter'), 'formatInterval'], array_combine($intervals, $intervals));
68
69     $form['upload'] = [
70       '#type' => 'file',
71       '#title' => $this->t('OPML File'),
72       '#description' => $this->t('Upload an OPML file containing a list of feeds to be imported.'),
73     ];
74     $form['remote'] = [
75       '#type' => 'url',
76       '#title' => $this->t('OPML Remote URL'),
77       '#maxlength' => 1024,
78       '#description' => $this->t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'),
79     ];
80     $form['refresh'] = [
81       '#type' => 'select',
82       '#title' => $this->t('Update interval'),
83       '#default_value' => 3600,
84       '#options' => $period,
85       '#description' => $this->t('The length of time between feed updates. Requires a correctly configured <a href=":cron">cron maintenance task</a>.', [':cron' => $this->url('system.status')]),
86     ];
87
88     $form['actions'] = ['#type' => 'actions'];
89     $form['actions']['submit'] = [
90       '#type' => 'submit',
91       '#value' => $this->t('Import'),
92     ];
93
94     return $form;
95   }
96
97   /**
98    * {@inheritdoc}
99    */
100   public function validateForm(array &$form, FormStateInterface $form_state) {
101     // If both fields are empty or filled, cancel.
102     $all_files = $this->getRequest()->files->get('files', []);
103     if ($form_state->isValueEmpty('remote') == empty($all_files['upload'])) {
104       $form_state->setErrorByName('remote', $this->t('<em>Either</em> upload a file or enter a URL.'));
105     }
106   }
107
108   /**
109    * {@inheritdoc}
110    */
111   public function submitForm(array &$form, FormStateInterface $form_state) {
112     $validators = ['file_validate_extensions' => ['opml xml']];
113     if ($file = file_save_upload('upload', $validators, FALSE, 0)) {
114       $data = file_get_contents($file->getFileUri());
115     }
116     else {
117       // @todo Move this to a fetcher implementation.
118       try {
119         $response = $this->httpClient->get($form_state->getValue('remote'));
120         $data = (string) $response->getBody();
121       }
122       catch (RequestException $e) {
123         $this->logger('aggregator')->warning('Failed to download OPML file due to "%error".', ['%error' => $e->getMessage()]);
124         drupal_set_message($this->t('Failed to download OPML file due to "%error".', ['%error' => $e->getMessage()]));
125         return;
126       }
127     }
128
129     $feeds = $this->parseOpml($data);
130     if (empty($feeds)) {
131       drupal_set_message($this->t('No new feed has been added.'));
132       return;
133     }
134
135     // @todo Move this functionality to a processor.
136     foreach ($feeds as $feed) {
137       // Ensure URL is valid.
138       if (!UrlHelper::isValid($feed['url'], TRUE)) {
139         drupal_set_message($this->t('The URL %url is invalid.', ['%url' => $feed['url']]), 'warning');
140         continue;
141       }
142
143       // Check for duplicate titles or URLs.
144       $query = $this->feedStorage->getQuery();
145       $condition = $query->orConditionGroup()
146         ->condition('title', $feed['title'])
147         ->condition('url', $feed['url']);
148       $ids = $query
149         ->condition($condition)
150         ->execute();
151       $result = $this->feedStorage->loadMultiple($ids);
152       foreach ($result as $old) {
153         if (strcasecmp($old->label(), $feed['title']) == 0) {
154           drupal_set_message($this->t('A feed named %title already exists.', ['%title' => $old->label()]), 'warning');
155           continue 2;
156         }
157         if (strcasecmp($old->getUrl(), $feed['url']) == 0) {
158           drupal_set_message($this->t('A feed with the URL %url already exists.', ['%url' => $old->getUrl()]), 'warning');
159           continue 2;
160         }
161       }
162
163       $new_feed = $this->feedStorage->create([
164         'title' => $feed['title'],
165         'url' => $feed['url'],
166         'refresh' => $form_state->getValue('refresh'),
167       ]);
168       $new_feed->save();
169     }
170
171     $form_state->setRedirect('aggregator.admin_overview');
172   }
173
174   /**
175    * Parses an OPML file.
176    *
177    * Feeds are recognized as <outline> elements with the attributes "text" and
178    * "xmlurl" set.
179    *
180    * @param string $opml
181    *   The complete contents of an OPML document.
182    *
183    * @return array
184    *   An array of feeds, each an associative array with a "title" and a "url"
185    *   element, or NULL if the OPML document failed to be parsed. An empty array
186    *   will be returned if the document is valid but contains no feeds, as some
187    *   OPML documents do.
188    *
189    * @todo Move this to a parser in https://www.drupal.org/node/1963540.
190    */
191   protected function parseOpml($opml) {
192     $feeds = [];
193     $xml_parser = xml_parser_create();
194     xml_parser_set_option($xml_parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
195     if (xml_parse_into_struct($xml_parser, $opml, $values)) {
196       foreach ($values as $entry) {
197         if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
198           $item = $entry['attributes'];
199           if (!empty($item['XMLURL']) && !empty($item['TEXT'])) {
200             $feeds[] = ['title' => $item['TEXT'], 'url' => $item['XMLURL']];
201           }
202         }
203       }
204     }
205     xml_parser_free($xml_parser);
206
207     return $feeds;
208   }
209
210 }