Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / web / modules / contrib / media_entity_twitter / src / Plugin / media / Source / Twitter.php
1 <?php
2
3 namespace Drupal\media_entity_twitter\Plugin\media\Source;
4
5 use Drupal\Core\Config\ConfigFactoryInterface;
6 use Drupal\Core\Entity\EntityFieldManagerInterface;
7 use Drupal\Core\Entity\EntityTypeManagerInterface;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Logger\LoggerChannelInterface;
10 use Drupal\Core\Render\RendererInterface;
11 use Drupal\media\MediaInterface;
12 use Drupal\media\MediaSourceBase;
13 use Drupal\media\MediaTypeInterface;
14 use Drupal\media_entity_twitter\TweetFetcherInterface;
15 use Symfony\Component\DependencyInjection\ContainerInterface;
16 use Drupal\Core\Field\FieldTypePluginManagerInterface;
17 use Drupal\media\MediaSourceFieldConstraintsInterface;
18
19 /**
20  * Twitter entity media source.
21  *
22  * @MediaSource(
23  *   id = "twitter",
24  *   label = @Translation("Twitter"),
25  *   allowed_field_types = {"string", "string_long", "link"},
26  *   default_thumbnail_filename = "twitter.png",
27  *   description = @Translation("Provides business logic and metadata for Twitter.")
28  * )
29  */
30 class Twitter extends MediaSourceBase implements MediaSourceFieldConstraintsInterface {
31
32   /**
33    * The renderer.
34    *
35    * @var \Drupal\Core\Render\RendererInterface
36    */
37   protected $renderer;
38
39   /**
40    * The tweet fetcher.
41    *
42    * @var \Drupal\media_entity_twitter\TweetFetcherInterface
43    */
44   protected $tweetFetcher;
45
46   /**
47    * The logger channel.
48    *
49    * @var \Drupal\Core\Logger\LoggerChannelInterface
50    */
51   protected $logger;
52
53   /**
54    * {@inheritdoc}
55    */
56   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
57     return new static(
58       $configuration,
59       $plugin_id,
60       $plugin_definition,
61       $container->get('entity_type.manager'),
62       $container->get('entity_field.manager'),
63       $container->get('plugin.manager.field.field_type'),
64       $container->get('config.factory'),
65       $container->get('renderer'),
66       $container->get('media_entity_twitter.tweet_fetcher'),
67       $container->get('logger.factory')->get('media_entity_twitter')
68     );
69   }
70
71   /**
72    * List of validation regular expressions.
73    *
74    * @var array
75    */
76   public static $validationRegexp = [
77     '@((http|https):){0,1}//(www\.){0,1}twitter\.com/(?<user>[a-z0-9_-]+)/(status(es){0,1})/(?<id>[\d]+)@i' => 'id',
78   ];
79
80   /**
81    * Constructs a new class instance.
82    *
83    * @param array $configuration
84    *   A configuration array containing information about the plugin instance.
85    * @param string $plugin_id
86    *   The plugin_id for the plugin instance.
87    * @param mixed $plugin_definition
88    *   The plugin implementation definition.
89    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
90    *   Entity type manager service.
91    * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
92    *   Entity field manager service.
93    * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
94    *   Config field type manager service.
95    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
96    *   Config factory service.
97    * @param \Drupal\Core\Render\RendererInterface $renderer
98    *   The renderer.
99    * @param \Drupal\media_entity_twitter\TweetFetcherInterface $tweet_fetcher
100    *   The tweet fetcher.
101    * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
102    *   The logger channel.
103    */
104   public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, RendererInterface $renderer, TweetFetcherInterface $tweet_fetcher, LoggerChannelInterface $logger) {
105     parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
106     $this->renderer = $renderer;
107     $this->tweetFetcher = $tweet_fetcher;
108     $this->logger = $logger;
109   }
110
111   /**
112    * {@inheritdoc}
113    */
114   public function defaultConfiguration() {
115     return [
116       'source_field' => '',
117       'use_twitter_api' => FALSE,
118       'generate_thumbnails' => FALSE,
119       'consumer_key' => '',
120       'consumer_secret' => '',
121       'oauth_access_token' => '',
122       'oauth_access_token_secret' => '',
123     ];
124   }
125
126   /**
127    * {@inheritdoc}
128    */
129   public function getMetadataAttributes() {
130     $attributes = [
131       'id' => $this->t('Tweet ID'),
132       'user' => $this->t('Twitter user information'),
133     ];
134
135     if ($this->configuration['use_twitter_api']) {
136       $attributes += [
137         'image' => $this->t('Link to the twitter image'),
138         'image_local' => $this->t('Copies tweet image to the local filesystem and returns the URI.'),
139         'image_local_uri' => $this->t('Gets URI of the locally saved image.'),
140         'content' => $this->t('This tweet content'),
141         'retweet_count' => $this->t('Retweet count for this tweet'),
142         'profile_image_url_https' => $this->t('Link to profile image'),
143       ];
144     }
145
146     return $attributes;
147   }
148
149   /**
150    * {@inheritdoc}
151    */
152   public function getMetadata(MediaInterface $media, $attribute_name) {
153     $matches = $this->matchRegexp($media);
154
155     if (!$matches['id']) {
156       return NULL;
157     }
158
159     // First we return the fields that are available from regex.
160     switch ($attribute_name) {
161       case 'id':
162         return $matches['id'];
163
164       case 'user':
165         return $matches['user'] ?: NULL;
166
167       case 'thumbnail_uri':
168         // If there's already a local image, use it.
169         if ($local_image = $this->getMetadata($media, 'image_local')) {
170           return $local_image;
171         }
172
173         // If thumbnail generation is disabled, use the default thumbnail.
174         if (empty($this->configuration['generate_thumbnails'])) {
175           return parent::getMetadata($media, $attribute_name);
176         }
177
178         // We might need to generate a thumbnail...
179         $id = $this->getMetadata($media, 'id');
180         $thumbnail_uri = $this->getLocalImageUri($id, $media);
181
182         // ...unless we already have, in which case, use it.
183         if (file_exists($thumbnail_uri)) {
184           return $thumbnail_uri;
185         }
186
187         // Render the thumbnail SVG using the theme system.
188         $thumbnail = [
189           '#theme' => 'media_entity_twitter_tweet_thumbnail',
190           '#content' => $this->getMetadata($media, 'content'),
191           '#author' => $this->getMetadata($media, 'user'),
192           '#avatar' => $this->getMetadata($media, 'profile_image_url_https'),
193         ];
194         $svg = $this->renderer->renderRoot($thumbnail);
195
196         return file_unmanaged_save_data($svg, $thumbnail_uri, FILE_EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name);
197     }
198
199     // If we have auth settings return the other fields.
200     if ($this->configuration['use_twitter_api'] && $tweet = $this->fetchTweet($matches['id'])) {
201       switch ($attribute_name) {
202         case 'image':
203           if (isset($tweet['extended_entities']['media'][0]['media_url'])) {
204             return $tweet['extended_entities']['media'][0]['media_url'];
205           }
206           return NULL;
207
208         case 'image_local':
209           $local_uri = $this->getMetadata($media, 'image_local_uri');
210
211           if ($local_uri) {
212             if (file_exists($local_uri)) {
213               return $local_uri;
214             }
215             else {
216               $image_url = $this->getMetadata($media, 'image');
217               // @TODO: Use Guzzle, possibly in a service, for this.
218               $image_data = file_get_contents($image_url);
219               if ($image_data) {
220                 return file_unmanaged_save_data($image_data, $local_uri, FILE_EXISTS_REPLACE);
221               }
222             }
223           }
224           return NULL;
225
226         case 'image_local_uri':
227           $image_url = $this->getMetadata($media, 'image');
228           if ($image_url) {
229             return $this->getLocalImageUri($matches['id'], $media, $image_url);
230           }
231           return NULL;
232
233         case 'content':
234           if (isset($tweet['text'])) {
235             return $tweet['text'];
236           }
237           return NULL;
238
239         case 'retweet_count':
240           if (isset($tweet['retweet_count'])) {
241             return $tweet['retweet_count'];
242           }
243           return NULL;
244
245         case 'profile_image_url_https':
246           if (isset($tweet['user']['profile_image_url_https'])) {
247             return $tweet['user']['profile_image_url_https'];
248           }
249           return NULL;
250
251         case 'default_name':
252           $user = $this->getMetadata($media, 'user');
253           $id = $this->getMetadata($media, 'id');
254           if (!empty($user) && !empty($id)) {
255             return $user . ' - ' . $id;
256           }
257           return NULL;
258       }
259     }
260
261     return NULL;
262   }
263
264   /**
265    * {@inheritdoc}
266    */
267   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
268     $form = parent::buildConfigurationForm($form, $form_state);
269
270     $form['use_twitter_api'] = [
271       '#type' => 'select',
272       '#title' => $this->t('Whether to use Twitter api to fetch tweets or not.'),
273       '#description' => $this->t("In order to use Twitter's api you have to create a developer account and an application. For more information consult the readme file."),
274       '#default_value' => empty($this->configuration['use_twitter_api']) ? 0 : $this->configuration['use_twitter_api'],
275       '#options' => [
276         0 => $this->t('No'),
277         1 => $this->t('Yes'),
278       ],
279     ];
280
281     // @todo: Evaluate if this should be a site-wide configuration.
282     $form['consumer_key'] = [
283       '#type' => 'textfield',
284       '#title' => $this->t('Consumer key'),
285       '#default_value' => empty($this->configuration['consumer_key']) ? NULL : $this->configuration['consumer_key'],
286       '#states' => [
287         'visible' => [
288           ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
289         ],
290       ],
291     ];
292
293     $form['consumer_secret'] = [
294       '#type' => 'textfield',
295       '#title' => $this->t('Consumer secret'),
296       '#default_value' => empty($this->configuration['consumer_secret']) ? NULL : $this->configuration['consumer_secret'],
297       '#states' => [
298         'visible' => [
299           ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
300         ],
301       ],
302     ];
303
304     $form['oauth_access_token'] = [
305       '#type' => 'textfield',
306       '#title' => $this->t('Oauth access token'),
307       '#default_value' => empty($this->configuration['oauth_access_token']) ? NULL : $this->configuration['oauth_access_token'],
308       '#states' => [
309         'visible' => [
310           ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
311         ],
312       ],
313     ];
314
315     $form['oauth_access_token_secret'] = [
316       '#type' => 'textfield',
317       '#title' => $this->t('Oauth access token secret'),
318       '#default_value' => empty($this->configuration['oauth_access_token_secret']) ? NULL : $this->configuration['oauth_access_token_secret'],
319       '#states' => [
320         'visible' => [
321           ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
322         ],
323       ],
324     ];
325
326     $form['generate_thumbnails'] = [
327       '#type' => 'checkbox',
328       '#title' => $this->t('Generate thumbnails'),
329       '#default_value' => $this->configuration['generate_thumbnails'],
330       '#states' => [
331         'visible' => [
332           ':input[name="source_configuration[use_twitter_api]"]' => [
333             'checked' => TRUE,
334           ],
335         ],
336       ],
337       '#description' => $this->t('If checked, Drupal will automatically generate thumbnails for tweets that do not reference any external media. In certain circumstances, <strong>this may violate <a href="@policy">Twitter\'s fair use policy</a></strong>. Please <strong>read it and be careful</strong> if you choose to enable this.', [
338         '@policy' => 'https://dev.twitter.com/overview/terms/agreement-and-policy',
339       ]),
340     ];
341
342     return $form;
343   }
344
345   /**
346    * {@inheritdoc}
347    */
348   public function getSourceFieldConstraints() {
349     return [
350       'TweetEmbedCode' => [],
351       'TweetVisible' => [],
352     ];
353   }
354
355   /**
356    * {@inheritdoc}
357    */
358   public function createSourceField(MediaTypeInterface $type) {
359     return parent::createSourceField($type)->set('label', 'Tweet URL');
360   }
361
362   /**
363    * Computes the destination URI for a tweet image.
364    *
365    * @param mixed $id
366    *   The tweet ID.
367    * @param \Drupal\media\MediaInterface $media
368    *   The media entity.
369    * @param string|null $media_url
370    *   The URL of the media (i.e., photo, video, etc.) associated with the
371    *   tweet.
372    *
373    * @return string
374    *   The desired local URI.
375    */
376   protected function getLocalImageUri($id, MediaInterface $media, $media_url = NULL) {
377     $directory = $this->configFactory
378       ->get('media_entity_twitter.settings')
379       ->get('local_images');
380
381     // Ensure that the destination directory is writable. If not, log a warning
382     // and return the default thumbnail.
383     $ready = file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
384     if (!$ready) {
385       $this->logger->warning('Could not prepare thumbnail destination directory @dir', [
386         '@dir' => $directory,
387       ]);
388       return parent::getMetadata($media, 'thumbnail_uri');
389     }
390
391     $local_uri = $directory . '/' . $id . '.';
392     if ($media_url) {
393       $local_uri .= pathinfo($media_url, PATHINFO_EXTENSION);
394     }
395     else {
396       // If there is no media associated with the tweet, we will generate an
397       // SVG thumbnail.
398       $local_uri .= 'svg';
399     }
400
401     return $local_uri;
402   }
403
404   /**
405    * Runs preg_match on embed code/URL.
406    *
407    * @param \Drupal\media\MediaInterface $media
408    *   Media object.
409    *
410    * @return array|bool
411    *   Array of preg matches or FALSE if no match.
412    *
413    * @see preg_match()
414    */
415   protected function matchRegexp(MediaInterface $media) {
416     $matches = [];
417
418     $source_field = $this->getSourceFieldDefinition($media->bundle->entity)->getName();
419     if ($media->hasField($source_field)) {
420       $property_name = $media->get($source_field)->first()->mainPropertyName();
421       foreach (static::$validationRegexp as $pattern => $key) {
422         if (preg_match($pattern, $media->get($source_field)->{$property_name}, $matches)) {
423           return $matches;
424         }
425       }
426     }
427
428     return FALSE;
429   }
430
431   /**
432    * Get a single tweet.
433    *
434    * @param int $id
435    *   The tweet ID.
436    *
437    * @return array
438    *   The tweet information.
439    */
440   protected function fetchTweet($id) {
441     $this->tweetFetcher->setCredentials(
442       $this->configuration['consumer_key'],
443       $this->configuration['consumer_secret'],
444       $this->configuration['oauth_access_token'],
445       $this->configuration['oauth_access_token_secret']
446     );
447
448     return $this->tweetFetcher->fetchTweet($id);
449   }
450
451 }