OpmlFeedAdd.php 8.55 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\aggregator\Form\OpmlFeedAdd.
 */

namespace Drupal\aggregator\Form;

10
use Drupal\aggregator\CategoryStorageControllerInterface;
11 12
use Drupal\aggregator\FeedStorageControllerInterface;
use Drupal\Component\Utility\Url;
13
use Drupal\Core\Entity\Query\QueryFactory;
14
use Drupal\Core\Form\FormBase;
15 16 17
use Symfony\Component\DependencyInjection\ContainerInterface;
use Guzzle\Http\Exception\RequestException;
use Guzzle\Http\Exception\BadResponseException;
18
use Guzzle\Http\ClientInterface;
19 20 21 22

/**
 * Imports feeds from OPML.
 */
23
class OpmlFeedAdd extends FormBase {
24 25 26 27 28 29 30 31 32

  /**
   * The entity query factory object.
   *
   * @var \Drupal\Core\Entity\Query\QueryFactory
   */
  protected $queryFactory;

  /**
33
   * The feed storage.
34
   *
35
   * @var \Drupal\aggregator\FeedStorageControllerInterface
36
   */
37
  protected $feedStorageController;
38 39 40 41

  /**
   * The HTTP client to fetch the feed data with.
   *
42
   * @var \Guzzle\Http\ClientInterface
43 44 45
   */
  protected $httpClient;

46 47 48 49 50 51 52
  /**
   * The category storage controller.
   *
   * @var \Drupal\aggregator\CategoryStorageControllerInterface
   */
  protected $categoryStorageController;

53 54 55 56 57
  /**
   * Constructs a database object.
   *
   * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
   *   The entity query object.
58 59
   * @param \Drupal\aggregator\FeedStorageControllerInterface $feed_storage
   *   The feed storage.
60
   * @param \Guzzle\Http\ClientInterface $http_client
61
   *   The Guzzle HTTP client.
62 63
   * @param \Drupal\aggregator\CategoryStorageControllerInterface $category_storage_controller
   *   The category storage controller.
64
   */
65
  public function __construct(QueryFactory $query_factory, FeedStorageControllerInterface $feed_storage, ClientInterface $http_client, CategoryStorageControllerInterface $category_storage_controller) {
66
    $this->queryFactory = $query_factory;
67
    $this->feedStorageController = $feed_storage;
68
    $this->httpClient = $http_client;
69
    $this->categoryStorageController = $category_storage_controller;
70 71 72 73 74 75 76 77
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity.query'),
78
      $container->get('entity.manager')->getStorageController('aggregator_feed'),
79 80
      $container->get('http_default_client'),
      $container->get('aggregator.category.storage')
81 82 83 84 85 86
    );
  }

  /**
   * {@inheritdoc}
   */
87
  public function getFormId() {
88 89 90 91 92 93 94 95 96 97 98 99
    return 'aggregator_opml_add';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, array &$form_state) {
    $period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200,
      64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');

    $form['upload'] = array(
      '#type' => 'file',
100 101
      '#title' => $this->t('OPML File'),
      '#description' => $this->t('Upload an OPML file containing a list of feeds to be imported.'),
102 103 104
    );
    $form['remote'] = array(
      '#type' => 'url',
105
      '#title' => $this->t('OPML Remote URL'),
106
      '#maxlength' => 1024,
107
      '#description' => $this->t('Enter the URL of an OPML file. This file will be downloaded and processed only once on submission of the form.'),
108 109 110
    );
    $form['refresh'] = array(
      '#type' => 'select',
111
      '#title' => $this->t('Update interval'),
112 113
      '#default_value' => 3600,
      '#options' => $period,
114
      '#description' => $this->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'))),
115 116 117
    );

    // Handling of categories.
118
    $options = array_map('check_plain', $this->categoryStorageController->loadAllKeyed());
119 120 121
    if ($options) {
      $form['category'] = array(
        '#type' => 'checkboxes',
122
        '#title' => $this->t('Categorize news items'),
123
        '#options' => $options,
124
        '#description' => $this->t('New feed items are automatically filed in the checked categories.'),
125 126 127 128 129
      );
    }
    $form['actions'] = array('#type' => 'actions');
    $form['actions']['submit'] = array(
      '#type' => 'submit',
130
      '#value' => $this->t('Import'),
131 132 133 134 135 136 137 138 139 140
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, array &$form_state) {
    // If both fields are empty or filled, cancel.
141 142
    $file_upload = $this->getRequest()->files->get('files[upload]', NULL, TRUE);
    if (empty($form_state['values']['remote']) == empty($file_upload)) {
143
      form_set_error('remote', $form_state, $this->t('You must <em>either</em> upload a file or enter a URL.'));
144 145 146 147 148 149 150 151
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, array &$form_state) {
    $validators = array('file_validate_extensions' => array('opml xml'));
152
    if ($file = file_save_upload('upload', $form_state, $validators, FALSE, 0)) {
153
      $data = file_get_contents($file->getFileUri());
154 155 156 157 158 159 160 161 162 163
    }
    else {
      // @todo Move this to a fetcher implementation.
      try {
        $response = $this->httpClient->get($form_state['values']['remote'])->send();
        $data = $response->getBody(TRUE);
      }
      catch (BadResponseException $e) {
        $response = $e->getResponse();
        watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase()), WATCHDOG_WARNING);
164
        drupal_set_message($this->t('Failed to download OPML file due to "%error".', array('%error' => $response->getStatusCode() . ' ' . $response->getReasonPhrase())));
165 166 167 168
        return;
      }
      catch (RequestException $e) {
        watchdog('aggregator', 'Failed to download OPML file due to "%error".', array('%error' => $e->getMessage()), WATCHDOG_WARNING);
169
        drupal_set_message($this->t('Failed to download OPML file due to "%error".', array('%error' => $e->getMessage())));
170 171 172 173 174 175
        return;
      }
    }

    $feeds = $this->parseOpml($data);
    if (empty($feeds)) {
176
      drupal_set_message($this->t('No new feed has been added.'));
177 178 179 180 181 182
      return;
    }

    // @todo Move this functionality to a processor.
    foreach ($feeds as $feed) {
      // Ensure URL is valid.
183 184
      if (!Url::isValid($feed['url'], TRUE)) {
        drupal_set_message($this->t('The URL %url is invalid.', array('%url' => $feed['url'])), 'warning');
185 186 187 188 189 190 191 192 193 194 195
        continue;
      }

      // Check for duplicate titles or URLs.
      $query = $this->queryFactory->get('aggregator_feed');
      $condition = $query->orConditionGroup()
        ->condition('title', $feed['title'])
        ->condition('url', $feed['url']);
      $ids = $query
        ->condition($condition)
        ->execute();
196
      $result = $this->feedStorageController->loadMultiple($ids);
197 198
      foreach ($result as $old) {
        if (strcasecmp($old->label(), $feed['title']) == 0) {
199
          drupal_set_message($this->t('A feed named %title already exists.', array('%title' => $old->label())), 'warning');
200 201 202
          continue 2;
        }
        if (strcasecmp($old->url->value, $feed['url']) == 0) {
203
          drupal_set_message($this->t('A feed with the URL %url already exists.', array('%url' => $old->url->value)), 'warning');
204 205 206 207
          continue 2;
        }
      }

208
      $new_feed = $this->feedStorageController->create(array(
209 210 211 212
        'title' => $feed['title'],
        'url' => $feed['url'],
        'refresh' => $form_state['values']['refresh'],
      ));
213 214 215 216
      $new_feed->categories = $form_state['values']['category'];
      $new_feed->save();
    }

217
    $form_state['redirect_route']['route_name'] = 'aggregator.admin_overview';
218 219 220 221 222 223 224 225 226 227 228 229 230
  }

  /**
   * Parses an OPML file.
   *
   * Feeds are recognized as <outline> elements with the attributes "text" and
   * "xmlurl" set.
   *
   * @todo Move this functionality to a parser.
   *
   * @param $opml
   *   The complete contents of an OPML document.
   *
231
   * @return array
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
   *   An array of feeds, each an associative array with a "title" and a "url"
   *   element, or NULL if the OPML document failed to be parsed. An empty array
   *   will be returned if the document is valid but contains no feeds, as some
   *   OPML documents do.
   */
  protected function parseOpml($opml) {
    $feeds = array();
    $xml_parser = drupal_xml_parser_create($opml);
    if (xml_parse_into_struct($xml_parser, $opml, $values)) {
      foreach ($values as $entry) {
        if ($entry['tag'] == 'OUTLINE' && isset($entry['attributes'])) {
          $item = $entry['attributes'];
          if (!empty($item['XMLURL']) && !empty($item['TEXT'])) {
            $feeds[] = array('title' => $item['TEXT'], 'url' => $item['XMLURL']);
          }
        }
      }
    }
    xml_parser_free($xml_parser);

    return $feeds;
  }

}