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