3 namespace Drupal\media_entity_twitter\Plugin\media\Source;
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;
20 * Twitter entity media source.
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.")
30 class Twitter extends MediaSourceBase implements MediaSourceFieldConstraintsInterface {
35 * @var \Drupal\Core\Render\RendererInterface
42 * @var \Drupal\media_entity_twitter\TweetFetcherInterface
44 protected $tweetFetcher;
49 * @var \Drupal\Core\Logger\LoggerChannelInterface
56 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $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')
72 * List of validation regular expressions.
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',
81 * Constructs a new class instance.
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
99 * @param \Drupal\media_entity_twitter\TweetFetcherInterface $tweet_fetcher
101 * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
102 * The logger channel.
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;
114 public function defaultConfiguration() {
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' => '',
129 public function getMetadataAttributes() {
131 'id' => $this->t('Tweet ID'),
132 'user' => $this->t('Twitter user information'),
135 if ($this->configuration['use_twitter_api']) {
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'),
152 public function getMetadata(MediaInterface $media, $attribute_name) {
153 $matches = $this->matchRegexp($media);
155 if (!$matches['id']) {
159 // First we return the fields that are available from regex.
160 switch ($attribute_name) {
162 return $matches['id'];
165 return $matches['user'] ?: NULL;
167 case 'thumbnail_uri':
168 // If there's already a local image, use it.
169 if ($local_image = $this->getMetadata($media, 'image_local')) {
173 // If thumbnail generation is disabled, use the default thumbnail.
174 if (empty($this->configuration['generate_thumbnails'])) {
175 return parent::getMetadata($media, $attribute_name);
178 // We might need to generate a thumbnail...
179 $id = $this->getMetadata($media, 'id');
180 $thumbnail_uri = $this->getLocalImageUri($id, $media);
182 // ...unless we already have, in which case, use it.
183 if (file_exists($thumbnail_uri)) {
184 return $thumbnail_uri;
187 // Render the thumbnail SVG using the theme system.
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'),
194 $svg = $this->renderer->renderRoot($thumbnail);
196 return file_unmanaged_save_data($svg, $thumbnail_uri, FILE_EXISTS_ERROR) ?: parent::getMetadata($media, $attribute_name);
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) {
203 if (isset($tweet['extended_entities']['media'][0]['media_url'])) {
204 return $tweet['extended_entities']['media'][0]['media_url'];
209 $local_uri = $this->getMetadata($media, 'image_local_uri');
212 if (file_exists($local_uri)) {
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);
220 return file_unmanaged_save_data($image_data, $local_uri, FILE_EXISTS_REPLACE);
226 case 'image_local_uri':
227 $image_url = $this->getMetadata($media, 'image');
229 return $this->getLocalImageUri($matches['id'], $media, $image_url);
234 if (isset($tweet['text'])) {
235 return $tweet['text'];
239 case 'retweet_count':
240 if (isset($tweet['retweet_count'])) {
241 return $tweet['retweet_count'];
245 case 'profile_image_url_https':
246 if (isset($tweet['user']['profile_image_url_https'])) {
247 return $tweet['user']['profile_image_url_https'];
252 $user = $this->getMetadata($media, 'user');
253 $id = $this->getMetadata($media, 'id');
254 if (!empty($user) && !empty($id)) {
255 return $user . ' - ' . $id;
267 public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
268 $form = parent::buildConfigurationForm($form, $form_state);
270 $form['use_twitter_api'] = [
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'],
277 1 => $this->t('Yes'),
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'],
288 ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
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'],
299 ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
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'],
310 ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
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'],
321 ':input[name="source_configuration[use_twitter_api]"]' => ['value' => '1'],
326 $form['generate_thumbnails'] = [
327 '#type' => 'checkbox',
328 '#title' => $this->t('Generate thumbnails'),
329 '#default_value' => $this->configuration['generate_thumbnails'],
332 ':input[name="source_configuration[use_twitter_api]"]' => [
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',
348 public function getSourceFieldConstraints() {
350 'TweetEmbedCode' => [],
351 'TweetVisible' => [],
358 public function createSourceField(MediaTypeInterface $type) {
359 return parent::createSourceField($type)->set('label', 'Tweet URL');
363 * Computes the destination URI for a tweet image.
367 * @param \Drupal\media\MediaInterface $media
369 * @param string|null $media_url
370 * The URL of the media (i.e., photo, video, etc.) associated with the
374 * The desired local URI.
376 protected function getLocalImageUri($id, MediaInterface $media, $media_url = NULL) {
377 $directory = $this->configFactory
378 ->get('media_entity_twitter.settings')
379 ->get('local_images');
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);
385 $this->logger->warning('Could not prepare thumbnail destination directory @dir', [
386 '@dir' => $directory,
388 return parent::getMetadata($media, 'thumbnail_uri');
391 $local_uri = $directory . '/' . $id . '.';
393 $local_uri .= pathinfo($media_url, PATHINFO_EXTENSION);
396 // If there is no media associated with the tweet, we will generate an
405 * Runs preg_match on embed code/URL.
407 * @param \Drupal\media\MediaInterface $media
411 * Array of preg matches or FALSE if no match.
415 protected function matchRegexp(MediaInterface $media) {
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)) {
432 * Get a single tweet.
438 * The tweet information.
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']
448 return $this->tweetFetcher->fetchTweet($id);