diff -u b/core/modules/media/media.services.yml b/core/modules/media/media.services.yml --- b/core/modules/media/media.services.yml +++ b/core/modules/media/media.services.yml @@ -10,3 +10,6 @@ + media.oembed_provider_discovery: + class: Drupal\media\OEmbedProviderDiscovery + arguments: ['@http_client_factory', '@cache.default', '@logger.factory'] media.oembed: class: Drupal\media\OEmbed - arguments: ['@http_client_factory', '@cache.default', '@logger.factory'] + arguments: ['@media.oembed_provider_discovery', '@http_client_factory', '@cache.default', '@logger.factory'] diff -u b/core/modules/media/src/OEmbed.php b/core/modules/media/src/OEmbed.php --- b/core/modules/media/src/OEmbed.php +++ b/core/modules/media/src/OEmbed.php @@ -2,10 +2,16 @@ namespace Drupal\media; +use Drupal\Component\Serialization\Json; +use Drupal\Component\Utility\Html; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\UseCacheBackendTrait; use Drupal\Core\Http\ClientFactory; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\media\Exception\OEmbedResourceException; +use GuzzleHttp\Exception\BadResponseException; +use GuzzleHttp\Exception\RequestException; /** * OEmbed service. @@ -13,25 +19,14 @@ class OEmbed implements OEmbedInterface { use StringTranslationTrait; + use UseCacheBackendTrait; /** - * Cache key to be used to store providers info into cache. + * The OEmbed provider discovery service. * - * @var string + * @var \Drupal\media\OEmbedProviderDiscoveryInterface */ - protected static $providersCacheKey = 'media:oembed:providers'; - - /** - * URL of the JSON with providers info. - * - * @var string - */ - protected static $providersUrl = 'http://oembed.com/providers.json'; - - /** - * Cache lifetime for the providers info. - */ - protected static $providersCacheLifetime = 60 * 60 * 24 * 7; + protected $oEmbedProviderDiscovery; /** * The HTTP client factory service. @@ -41,13 +36,6 @@ protected $clientFactory; /** - * OEmbed providers information. - * - * @var array - */ - protected $providers; - - /** * Cache backend. * * @var \Drupal\Core\Cache\CacheBackendInterface @@ -57,6 +45,9 @@ /** * Static cache of fetched oEmbed resources. * + * A fetched oEmbed resource it the actual retrieved data for a specific + * media object. + * * @var array */ protected $resources; @@ -64,6 +55,9 @@ /** * Static cache of discovered oEmbed resources. * + * A discovered oEmbed resource is the oEmbed URL for a specific media object. + * This oEmbed URL is fetched from the cononical URL of the media object. + * * @var array */ protected $discovered; @@ -78,142 +72,89 @@ /** * Constructs OEmbed class. * + * @param \Drupal\media\OEmbedProviderDiscoveryInterface $oembed_provider_discovery + * The OEmbed provider discovery service. * @param \Drupal\Core\Http\ClientFactory $client_factory * The HTTP client factory service. - * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend * The cache backend. * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory * The logger channel for media. */ - public function __construct(ClientFactory $client_factory, CacheBackendInterface $cache, LoggerChannelFactoryInterface $logger_factory) { + public function __construct(OEmbedProviderDiscoveryInterface $oembed_provider_discovery, ClientFactory $client_factory, CacheBackendInterface $cache_backend, LoggerChannelFactoryInterface $logger_factory) { + $this->oEmbedProviderDiscovery = $oembed_provider_discovery; $this->clientFactory = $client_factory; - $this->cache = $cache; + $this->cacheBackend = $cache_backend; $this->logger = $logger_factory->get('media'); } /** * {@inheritdoc} */ - public function getProviders() { - if (!empty($this->providers)) { - return $this->providers; - } - elseif ($data = $this->cache->get(static::$providersCacheKey)) { - $this->providers = $data->data; - return $this->providers; - } - else { - $response = $this->clientFactory->fromOptions()->get(static::$providersUrl); - if ($response->getStatusCode() !== 200) { - drupal_set_message($this->t('Could not retrieve the providers list from the remote oEmbed database.'), 'error'); - $this->logger->error('Remote oEmbed providers database returned status code @code.', [ - '@code' => $response->getStatusCode(), - ]); - return FALSE; - } - - $providers = \GuzzleHttp\json_decode($response->getBody()->getContents(), TRUE); - - if (!is_array($providers)) { - drupal_set_message($this->t('An error occurred while trying to retrieve the providers list from the remote oEmbed database.'), 'error'); - $this->logger->error('Remote oEmbed providers database returned incorrect response content. Providers: @response', [ - '@response' => json_encode($providers), - ]); - return FALSE; - } - - // Some provider names may contain dot chars ("."), which are not allowed - // in config keys. We store them in an array where keys are hex-converted - // names, in order to allow an easy conversion back to their original - // names when necessary. - $keyed_providers = []; - foreach ($providers as $provider) { - $keyed_providers[bin2hex($provider['provider_name'])] = $provider; - } - - $this->cache->set(self::$providersCacheKey, $keyed_providers, static::$providersCacheLifetime); - $this->providers = $keyed_providers; - return $this->providers; - } - } - - /** - * {@inheritdoc} - */ - public function matchUrl($url, array $providers_limit = []) { - $providers = $this->getProviders(); + public function isProvidedUrl($url, array $providers_limit = []) { + $providers = $this->oEmbedProviderDiscovery->getProviders(); if (!empty($providers_limit)) { $providers = array_intersect_key($providers, $providers_limit); } - foreach ($providers as $provider_name => $provider_info) { - // @TODO Figure out a way of making this validation more robust. Youtube, - // for instance, does not come with schemes... - if ($provider_info['provider_name'] === 'YouTube') { - $provider_info['endpoints'][0]['schemes'] = [ - 'http*://*youtube.com/*', - 'http*://*youtu.be/*', - ]; - } if (!empty($provider_info['endpoints'])) { foreach ($provider_info['endpoints'] as $endpoint) { if (!empty($endpoint['schemes'])) { foreach ($endpoint['schemes'] as $scheme) { $regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme); if (preg_match("|$regexp|", $url)) { - return [ - 'provider' => $provider_name, - 'endpoint' => $endpoint['url'], - ]; + return TRUE; } } } } } } - return FALSE; } /** * {@inheritdoc} */ - public function doUrlDiscovery($url) { + public function urlDiscovery($url) { + if (!$this->isProvidedUrl($url)) { + return FALSE; + } + + $cacheKey = 'media:oembed:url:' . sha1($url); if (!empty($this->discovered[$url])) { return $this->discovered[$url]; } - - $response = $this->clientFactory->fromOptions()->get($url); - if ($response->getStatusCode() !== 200) { - drupal_set_message($this->t('Could not retrieve the remote URL correctly.'), 'error'); - $this->logger->error('Resource for URL @url responded with status code @code.', [ - '@url' => $url, - '@code' => $response->getStatusCode(), - ]); - return FALSE; + elseif ($data = $this->cacheGet($cacheKey)) { + $this->discovered[$url] = $data->data; + return $this->discovered[$url]; } + else { + try { + $response = $this->clientFactory->fromOptions()->get($url); + } + catch (BadResponseException $exception) { + $statusCode = $exception->getResponse()->getStatusCode(); + $this->logger->error('Resource for URL @url responded with status code @code.', [ + '@url' => $url, + '@code' => $statusCode, + ]); + throw new OEmbedResourceException("Remote oEmbed resource returned status code $statusCode."); + } - $content = $response->getBody()->getContents(); - $dom = new \DOMDocument(); + $content = $response->getBody()->getContents(); - // TODO this causes annoying warning with YT videos. Fix it: - // Warning: DOMDocument::loadHTML(): htmlParseEntityRef: no name in Entity - if (!@$dom->loadHTML($content)) { - drupal_set_message($this->t('The remote URL returned an incorrect document structure.'), 'error'); - return FALSE; - } + $xpath = new \DOMXpath(Html::load($content)); + $resultJson = $xpath->query("//link[@type='application/json+oembed']"); + $resultXml = $xpath->query("//link[@type='text/xml+oembed']"); - $xpath = new \DOMXpath($dom); - $result = $xpath->query("//link[@type='application/json+oembed']"); - if ($result->length > 0) { - $this->discovered[$url] = $result->item(0)->getAttribute('href'); - return $this->discovered[$url]; - } + if ($resultJson->length > 0 || $resultXml->length > 0) { + $result = ($resultJson->length > 0) ? $resultJson : $resultXml; + $this->discovered[$url] = $result->item(0)->getAttribute('href'); - $result = $xpath->query("//link[@type='text/xml+oembed']"); - if ($result->length > 0) { - $this->discovered[$url] = $result->item(0)->getAttribute('href'); - return $this->discovered[$url]; + $this->cacheSet($cacheKey, $this->discovered[$url]); + return $this->discovered[$url]; + } } return FALSE; @@ -223,12 +164,17 @@ * {@inheritdoc} */ public function isOEmbedResource($url) { - if ($this->doUrlDiscovery($url)) { - return TRUE; + try { + if ($this->urlDiscovery($url)) { + return TRUE; + } + } + catch (OEmbedResourceException $exception) { + return FALSE; } - // TODO handle providers that do not support discovery. + // TODO: handle providers that do not support discovery. return FALSE; } @@ -236,51 +182,53 @@ * {@inheritdoc} */ public function fetchResource($endpoint_url) { - if (!empty($this->resources[$endpoint_url])) { + $cacheKey = 'media:oembed:resource:' . sha1($endpoint_url); + if (isset($this->resources[$endpoint_url])) { return $this->resources[$endpoint_url]; } - - $response = $this->clientFactory->fromOptions()->get($endpoint_url); - if ($response->getStatusCode() !== 200) { - drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); - $this->logger->error('Remote resource returned status code @code.', [ - '@code' => $response->getStatusCode(), - ]); - return FALSE; + elseif ($data = $this->cacheGet($cacheKey)) { + $this->resources[$endpoint_url] = $data->data; + return $this->resources[$endpoint_url]; } + else { - $format = $response->getHeader('Content-Type'); - $content = $response->getBody()->getContents(); - if ($format[0] == 'application/json') { - $data = \GuzzleHttp\json_decode($content, TRUE); - - if (!is_array($data)) { - drupal_set_message($this->t('An error occurred while trying to retrieve the remote URL content.'), 'error'); - $this->logger->error('Remote resource returned incorrect response content. Data: @response', [ - '@response' => json_encode($data), + try { + $response = $this->clientFactory->fromOptions()->get($endpoint_url); + } + catch (BadResponseException $exception) { + $statusCode = $exception->getResponse()->getStatusCode(); + $this->logger->error('Remote oEmbed resource returned status code @code.', [ + '@code' => $statusCode, ]); - return FALSE; + throw new OEmbedResourceException("Remote oEmbed resource returned status code $statusCode."); + } + catch (RequestException $exception) { + $this->logger->error($exception->getMessage()); + throw new OEmbedResourceException('Could not retrieve the oEmbed resource.'); } - $this->resources[$endpoint_url] = $data; - return $data; - } - elseif ($format[0] == 'text/xml') { - $data = \GuzzleHttp\json_decode(\GuzzleHttp\json_encode($content), TRUE); + $format = $response->getHeader('Content-Type'); + $content = $response->getBody()->getContents(); + $data = ''; + if ($format[0] == 'application/json') { + $data = Json::decode($content); + } + elseif ($format[0] == 'text/xml') { + $data = Json::decode(Json::encode($content)); + } - if (!is_array($data)) { - drupal_set_message($this->t('An error occurred while trying to retrieve the remote URL content.'), 'error'); + if ($data && is_array($data)) { + $this->cacheSet($cacheKey, $data); + $this->resources[$endpoint_url] = $data; + return $data; + } + else { $this->logger->error('Remote resource returned incorrect response content. Data: @response', [ - '@response' => json_encode($data), + '@response' => Json::encode($data), ]); - return FALSE; + throw new OEmbedResourceException('An error occurred while trying to retrieve the remote URL content.'); } - - $this->resources[$endpoint_url] = $data; - return $data; } - - return FALSE; } } diff -u b/core/modules/media/src/OEmbedInterface.php b/core/modules/media/src/OEmbedInterface.php --- b/core/modules/media/src/OEmbedInterface.php +++ b/core/modules/media/src/OEmbedInterface.php @@ -8,26 +8,6 @@ interface OEmbedInterface { /** - * Gets oEmbed providers information. - * - * Returns a multi-dimensional array where each provider is represented as a - * single top-level array element. Each individual element is keyed by the - * provider's name codified with bin2hex(), and has the following values: - * - provider_name: Human readable name of the provider. - * - provider_url: Main URL of the provider. - * - endpoints: List of endpoints this provider exposes. Each endpoint is an - * array with the following values: - * - url: The endpoint's URL. - * - schemes: List of URL schemes supported by the provider. - * - formats: List of supported formats. Can be "json", "xml" or both. - * - discovery: Whether the provider supports oEmbed discovery. - * - * @return array[] - * Information about oEmbed providers. - */ - public function getProviders(); - - /** * Tries to match the URL against the database of oEmbed providers. * * @param string $url @@ -38,15 +18,12 @@ * and the values are irrelevant. If ommitted, all providers will be * considered. * - * @return array|bool - * If a match was found, will return an associative array with two values: - * - provider: Matching provider's name (coded with bin2hex()). - * - endpoint: URL of the matching endpoint. - * Will return FALSE if no match was found. + * @return bool + * If a match was found, will return TRUE, otherwise FALSE. * * @see \Drupal\media\OEmbedInterface::getProviders() */ - public function matchUrl($url, array $allowed_providers = []); + public function isProvidedUrl($url, array $allowed_providers = []); /** * Runs oEmbed discovery and returns the endpoint URL if successful. @@ -57,7 +34,7 @@ * @return string|bool * URL of the oEmbed endpoint, or FALSE if the discovery was not successful. */ - public function doUrlDiscovery($url); + public function urlDiscovery($url); /** * Determines whether a given URL is an oEmbed resource. diff -u b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php --- b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php +++ b/core/modules/media/src/Plugin/Field/FieldFormatter/OEmbedFormatter.php @@ -6,6 +6,7 @@ use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FormatterBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\media\Exception\OEmbedResourceException; use Drupal\media\OEmbedInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -81,11 +82,16 @@ foreach ($items as $delta => $item) { $main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); if (!empty($item->{$main_property})) { - $resource = $this->oEmbed->fetchResource($this->oEmbed->doUrlDiscovery($item->{$main_property})); - $element[$delta] = [ - '#markup' => $resource['html'], - '#allowed_tags' => ['iframe'], - ]; + try { + $resource = $this->oEmbed->fetchResource($this->oEmbed->urlDiscovery($item->{$main_property})); + $element[$delta] = [ + '#markup' => $resource['html'], + '#allowed_tags' => ['iframe'], + ]; + } + catch (OEmbedResourceException $exception) { + drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); + } } } diff -u b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php --- b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php +++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraintValidator.php @@ -3,8 +3,9 @@ namespace Drupal\media\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Field\FieldItemListInterface; -use Drupal\media\MediaInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\media\Exception\OEmbedProviderException; use Drupal\media\OEmbedInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; @@ -15,6 +16,8 @@ */ class OEmbedProviderConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { + use StringTranslationTrait; + /** * The oEmbed service. * @@ -23,20 +26,33 @@ protected $oEmbed; /** - * Constructs a new TweetVisibleConstraintValidator. + * The logger channel for media. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** + * Constructs a new OEmbedProviderConstraintValidator. * * @param \Drupal\media\OEmbedInterface $oembed * The oEmbed service. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger channel for media. */ - public function __construct(OEmbedInterface $oembed) { + public function __construct(OEmbedInterface $oembed, LoggerChannelFactoryInterface $logger_factory) { $this->oEmbed = $oembed; + $this->logger = $logger_factory->get('media'); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('media.oembed')); + return new static( + $container->get('media.oembed'), + $container->get('logger.factory') + ); } /** @@ -44,19 +60,19 @@ */ public function validate($value, Constraint $constraint) { // TODO check if resource belongs to an allowed provider. - /** @var FieldItemListInterface $value */ - $foo = 'ar'; - /** @var MediaInterface $media */ + /** @var \Drupal\media\MediaInterface $media */ $media = $value->getEntity(); $source_config = $media->getSource()->getConfiguration(); if (!empty($source_config['allowed_providers'])) { - $allowed_providers = array_map(function ($name) { - return hex2bin($name); - }, $source_config['allowed_providers']); + $allowed_providers = array_map('hex2bin', $source_config['allowed_providers']); $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); - $url_matches = $this->oEmbed->matchUrl($value->first()->get($main_property)->getString(), $allowed_providers); - if (!$url_matches) { - $this->context->addViolation($constraint->message); + try { + if (!$this->oEmbed->isProvidedUrl($value->first()->get($main_property)->getString(), $allowed_providers)) { + $this->context->addViolation($constraint->message); + } + } + catch (OEmbedProviderException $exception) { + $this->context->addViolation($this->t('An error occurred while trying to retrieve the providers list from the remote oEmbed database.')); } } } diff -u b/core/modules/media/src/Plugin/media/Source/OEmbed.php b/core/modules/media/src/Plugin/media/Source/OEmbed.php --- b/core/modules/media/src/Plugin/media/Source/OEmbed.php +++ b/core/modules/media/src/Plugin/media/Source/OEmbed.php @@ -9,11 +9,16 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\link\LinkItemInterface; +use Drupal\media\Exception\OEmbedProviderException; +use Drupal\media\Exception\OEmbedResourceException; use Drupal\media\MediaSourceBase; use Drupal\media\MediaInterface; use Drupal\media\MediaSourceFieldConstraintsInterface; use Drupal\media\MediaTypeInterface; use Drupal\media\OEmbedInterface; +use Drupal\media\OEmbedProviderDiscoveryInterface; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\RequestException; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -44,6 +49,13 @@ protected $oEmbed; /** + * The OEmbed provider discovery service. + * + * @var \Drupal\media\OEmbedProviderDiscoveryInterface + */ + protected $oEmbedProviderDiscovery; + + /** * Constructs a new OEmbed instance. * * @param array $configuration @@ -65,10 +77,11 @@ * @param \Drupal\media\OEmbedInterface $oembed * The oEmbed service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, FieldTypePluginManagerInterface $field_type_manager, LoggerChannelInterface $logger, OEmbedInterface $oembed) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, FieldTypePluginManagerInterface $field_type_manager, LoggerChannelInterface $logger, OEmbedInterface $oembed, OEmbedProviderDiscoveryInterface $oembed_provider_discovery) { parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory); - $this->oEmbed = $oembed; $this->logger = $logger; + $this->oEmbed = $oembed; + $this->oEmbedProviderDiscovery = $oembed_provider_discovery; } /** @@ -84,7 +97,8 @@ $container->get('config.factory'), $container->get('plugin.manager.field.field_type'), $container->get('logger.factory')->get('media'), - $container->get('media.oembed') + $container->get('media.oembed'), + $container->get('media.oembed_provider_discovery') ); } @@ -118,7 +132,12 @@ public function getMetadata(MediaInterface $media, $name) { $main_property = $media->get($this->configuration['source_field'])->first()->mainPropertyName(); $resource_url = $media->get($this->configuration['source_field'])->first()->get($main_property)->getString(); - $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->doUrlDiscovery($resource_url)); + try { + $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->urlDiscovery($resource_url)); + } + catch (OEmbedResourceException $exception) { + drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); + } switch ($name) { case 'thumbnail_local': @@ -181,9 +200,31 @@ '#description' => $this->t('Thumbnails will be fetched from the provider for local usage. This is the location where they will be placed.'), ]; - $provider_options = array_map(function ($provider) { - return $provider['provider_name']; - }, $this->oEmbed->getProviders()); + $provider_options = []; + try { + $provider_options = array_map(function ($provider) { + return $provider['provider_name']; + }, $this->oEmbedProviderDiscovery->getProviders()); + } + catch (OEmbedProviderException $exception) { + drupal_set_message($this->t('An error occurred while trying to retrieve the providers list from the remote oEmbed database.')); + $this->logger->error('Remote oEmbed providers database returned incorrect response content. Providers: @response', [ + '@response' => json_encode($exception->getProviders()), + ]); + } + catch (ClientException $exception) { + drupal_set_message($this->t('Could not retrieve the providers list from the remote oEmbed database.'), 'error'); + $this->logger->error('Remote oEmbed providers database returned status code @code.', [ + '@code' => $exception->getResponse()->getStatusCode(), + ]); + return FALSE; + } + catch (RequestException $exception) { + drupal_set_message($this->t('Could not retrieve the providers list from the remote oEmbed database.'), 'error'); + $this->logger->error($exception->getMessage()); + return FALSE; + } + $form['allowed_providers'] = [ '#type' => 'select', '#title' => $this->t('Allowed providers'), @@ -271,2 +312,3 @@ } + } only in patch2: unchanged: --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -59,6 +59,24 @@ function media_entity_access(EntityInterface $entity, $operation, AccountInterfa return AccessResult::neutral(); } +/** + * Implements hook_oembed_providers_alter(). + */ +function media_oembed_providers_alter(array $providers) { + foreach ($providers as $provider_name => $provider_info) { + // @TODO Figure out a way of making this validation more robust. Youtube, + // for instance, does not come with schemes... + if ($provider_info['provider_name'] === 'YouTube') { + $provider_info['endpoints'][0]['schemes'] = [ + 'http*://*youtube.com/*', + 'http*://*youtu.be/*', + ]; + } + } + + return $providers; +} + /** * Implements hook_theme_suggestions_HOOK(). */ only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/Exception/OEmbedProviderException.php @@ -0,0 +1,25 @@ +providers = $providers; + } + + /** + * @return array + */ + public function getProviders() { + return $this->providers; + } + + + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/Exception/OEmbedResourceException.php @@ -0,0 +1,25 @@ +providers = $providers; + } + + /** + * @return array + */ + public function getProviders() { + return $this->providers; + } + + + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbedProviderDiscovery.php @@ -0,0 +1,124 @@ +clientFactory = $client_factory; + $this->cacheBackend = $cacheBackend; + $this->logger = $logger_factory->get('media'); + $this->providersUrl = $providers_url; + } + + /** + * {@inheritdoc} + */ + public function getProviders() { + $cacheKey = 'media:oembed:providers'; + if (isset($this->providers)) { + return $this->providers; + } + elseif ($data = $this->cacheGet($cacheKey)) { + $this->providers = $data->data; + return $this->providers; + } + else { + try { + $response = $this->clientFactory->fromOptions() + ->request('get', $this->providersUrl); + } + catch (BadResponseException $exception) { + $statusCode = $exception->getResponse()->getStatusCode(); + $this->logger->error('Remote oEmbed providers database returned status code @code.', [ + '@code' => $statusCode, + ]); + throw new OEmbedProviderException("Remote oEmbed providers database returned status code $statusCode."); + } + catch (RequestException $exception) { + $this->logger->error($exception->getMessage()); + throw new OEmbedProviderException('Could not retrieve the providers list from the remote oEmbed database.'); + } + + $providers = Json::decode($response->getBody()->getContents()); + + if (!is_array($providers) || empty($providers)) { + $this->logger->error('Remote oEmbed providers database returned incorrect response content. Providers: @response', [ + '@response' => json_encode($providers), + ]); + throw new OEmbedProviderException('Remote oEmbed providers database returned empty list.', $providers); + } + + $providers = \Drupal::moduleHandler() + ->invokeAll('oembed_providers_alter', [$providers]); + + // Some provider names may contain dot chars ("."), which are not + // allowed in config keys. We store them in an array where keys are + // hex-converted names, in order to allow an easy conversion back to + // their original names when necessary. + $keyed_providers = []; + foreach ($providers as $provider) { + $keyed_providers[bin2hex($provider['provider_name'])] = $provider; + } + + $this->cacheSet($cacheKey, $keyed_providers, time() + (60 * 60 * 24 * 7)); + $this->providers = $keyed_providers; + return $this->providers; + } + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbedProviderDiscoveryInterface.php @@ -0,0 +1,32 @@ +container->get('cache.default'); + $logger = $this->container->get('logger.factory'); + + $body = $this->getMockBuilder('\GuzzleHttp\Psr7\Stream') + ->disableOriginalConstructor() + ->getMock(); + $body->method('getContents')->willReturn($content); + + $response = $this->getMockBuilder('\GuzzleHttp\Psr7\Response') + ->disableOriginalConstructor() + ->getMock(); + $response->method('getBody')->willReturn($body); + + $client = $this->getMockBuilder('\GuzzleHttp\Client') + ->getMock(); + $client->method('request')->withAnyParameters()->willReturn($response); + + $clientFactory = $this->getMockBuilder('\Drupal\Core\Http\ClientFactory') + ->disableOriginalConstructor() + ->getMock(); + $clientFactory->method('fromOptions')->willReturn($client); + + $this->setExpectedException($exceptionClass, $exceptionMessage); + + /** @var \Drupal\media\OEmbedProviderDiscoveryInterface $providerDiscovery */ + $providerDiscovery = new OEmbedProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); + $providerDiscovery->getProviders(); + } + + /** + * Data provider for testEmptyProviderList. + * + * @return array + * Data. + */ + public function dataProviderEmptyProviderList() { + return [ + [ + json_encode([]), + 'http://oembed.com/providers.json', + 'Drupal\media\Exception\OEmbedProviderException', + 'Remote oEmbed providers database returned empty list.', + ], + [ + '', + 'http://oembed.com/providers.json', + 'TypeError', + NULL, + ], + ]; + } + + /** + * Test provider discovery. + * + * @dataProvider dataProviderNonExistingProviderDatabase + */ + public function testNonExistingProviderDatabase($providerUrl, $exceptionClass, $exceptionMessage) { + + $cacheBackend = $this->container->get('cache.default'); + $logger = $this->container->get('logger.factory'); + $clientFactory = $this->container->get('http_client_factory'); + + $this->setExpectedException($exceptionClass, $exceptionMessage); + + /** @var \Drupal\media\OEmbedProviderDiscoveryInterface $providerDiscovery */ + $providerDiscovery = new OEmbedProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); + $providerDiscovery->getProviders(); + } + + /** + * Data provider for testEmptyProviderList. + * + * @return array + * Data. + */ + public function dataProviderNonExistingProviderDatabase() { + return [ + [ + 'http://oembed1.com/providers.json', + 'Drupal\media\Exception\OEmbedProviderException', + 'Could not retrieve the providers list from the remote oEmbed database.', + ], + [ + 'http://oembed.com/providers1.json', + 'Drupal\media\Exception\OEmbedProviderException', + 'Remote oEmbed providers database returned status code 404.', + ], + ]; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/tests/src/Kernel/OEmbedProviderDiscoveryTest.php @@ -0,0 +1,68 @@ +container->get('cache.default'); + $logger = $this->container->get('logger.factory'); + + $body = $this->getMockBuilder('\GuzzleHttp\Psr7\Stream') + ->disableOriginalConstructor() + ->getMock(); + $body->method('getContents')->willReturn($content); + + $response = $this->getMockBuilder('\GuzzleHttp\Psr7\Response') + ->disableOriginalConstructor() + ->getMock(); + $response->method('getBody')->willReturn($body); + + $client = $this->getMockBuilder('\GuzzleHttp\Client') + ->getMock(); + $client->method('request')->withAnyParameters()->willReturn($response); + + $clientFactory = $this->getMockBuilder('\Drupal\Core\Http\ClientFactory') + ->disableOriginalConstructor() + ->getMock(); + $clientFactory->method('fromOptions')->willReturn($client); + + $this->setExpectedException($exceptionClass, $exceptionMessage); + + /** @var \Drupal\media\OEmbedProviderDiscoveryInterface $providerDiscovery */ + $providerDiscovery = new OEmbedProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); + $providerDiscovery->getProviders(); + } + + /** + * Data provider for testEmptyProviderList. + * + * @return array + * Data. + */ + public function dataProvider() { + return [ + [ + json_encode([]), + 'http://oembed.com/providers.json', + 'Drupal\media\Exception\OEmbedProviderException', + 'Remote oEmbed providers database returned empty list.', + ], + ['', 'http://oembed.com/providers.json', 'TypeError', NULL], + ]; + } + +}