diff -u b/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml --- b/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -61,9 +61,10 @@ label: 'Thumbnails location' allowed_providers: type: sequence - label: 'Allowed providers' + label: 'Allowed oEmbed providers' sequence: type: string + label: 'Provider name' media.source.field_aware: type: mapping 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,6 +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: ['@media.oembed_provider_discovery', '@http_client_factory', '@cache.default', '@logger.factory'] + media.oembed.provider_discovery: + class: Drupal\media\OEmbed\ProviderDiscovery + arguments: ['@http_client', 'https://oembed.com/providers.json', '@cache.default'] + media.oembed.resource_fetcher: + class: Drupal\media\OEmbed\ResourceFetcher + arguments: ['@media.oembed.provider_discovery', '@http_client', '@module_handler', '@cache.default'] reverted: --- b/core/modules/media/src/Exception/OEmbedProviderException.php +++ /dev/null @@ -1,8 +0,0 @@ -oEmbedProviderDiscovery = $oembed_provider_discovery; - $this->clientFactory = $client_factory; - $this->cacheBackend = $cache_backend; - $this->logger = $logger_factory->get('media'); - } - - /** - * {@inheritdoc} - */ - public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) { - $cacheKey = 'media:oembed:url:' . sha1($url . $max_width . $max_height); - if (!empty($this->discovered[$url])) { - return $this->discovered[$url]; - } - elseif ($data = $this->cacheGet($cacheKey)) { - $this->discovered[$url] = $data->data; - return $this->discovered[$url]; - } - else { - if ($provider_info = $this->getProvider($url)) { - if (!empty($provider_info['endpoints'])) { - foreach ($provider_info['endpoints'] as $endpoint) { - if (!empty($endpoint['url'])) { - $options = [ - 'url' => $url, - ]; - $endpoint_url = $endpoint['url']; - if (strpos($endpoint_url, '{format}') !== FALSE) { - $format = (!empty($endpoint['formats']) && is_array($endpoint['formats'])) ? reset($endpoint['formats']) : $format = 'json'; - $endpoint_url = str_replace('{format}', $format, $endpoint_url); - } - $this->discovered[$url] = $endpoint_url . '?' . UrlHelper::buildQuery($options); - } - } - } - } - else { - $this->discovered[$url] = $this->urlDiscovery($url); - } - - if (!empty($this->discovered[$url])) { - $parsed_url = UrlHelper::parse($this->discovered[$url]); - if (!empty($max_width)) { - $parsed_url['query']['maxwidth'] = $max_width; - } - if (!empty($max_height)) { - $parsed_url['query']['maxheight'] = $max_height; - } - // Let other modules alter the query string, because some oEmbed - // providers are providing extra parameters. For example Instagram also - // supports the 'omitscript' parameter. - \Drupal::moduleHandler()->alter('oembed_resource_query', $parsed_url['query']); - $this->discovered[$url] = $parsed_url['path'] . '?' . UrlHelper::buildQuery($parsed_url['query']); - - $this->cacheSet($cacheKey, $this->discovered[$url]); - return $this->discovered[$url]; - } - - } - throw new OEmbedResourceException(); - } - - /** - * {@inheritdoc} - */ - public function isAllowedProvider($url, array $allowed_providers = []) { - if ($resource_url = $this->getResourceUrl($url)) { - $data = $this->fetchResource($resource_url); - if (empty($allowed_providers) || (!empty($data['provider_name']) && in_array(bin2hex($data['provider_name']), $allowed_providers))) { - return TRUE; - } - } - - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function fetchResource($endpoint_url) { - $cacheKey = 'media:oembed:resource:' . sha1($endpoint_url); - if (isset($this->resources[$endpoint_url])) { - return $this->resources[$endpoint_url]; - } - elseif ($data = $this->cacheGet($cacheKey)) { - $this->resources[$endpoint_url] = $data->data; - return $this->resources[$endpoint_url]; - } - else { - - 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, - ]); - 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.'); - } - - $format = $response->getHeader('Content-Type'); - $content = (string) $response->getBody(); - - if (strpos($format[0], 'application/json') === FALSE && strpos($format[0], 'text/xml') === FALSE) { - $this->logger->error('Remote resource returned incorrect response content. Data: @response', [ - '@response' => $content, - ]); - throw new OEmbedResourceException('An error occurred while trying to retrieve the remote URL content.'); - } - - if (strpos($format[0], 'text/xml') !== FALSE) { - // Load XML into SimpleXMLElement. - $content = simplexml_load_string($content, "SimpleXMLElement", LIBXML_NOCDATA); - // Converting SimpleXMLElement to JSON. - $content = Json::encode($content); - } - - $this->resources[$endpoint_url] = Json::decode($content); - $this->cacheSet($cacheKey, $data); - return $this->resources[$endpoint_url]; - } - } - - /** - * Tries to get provider info form the provider list. - * - * @param string $url - * The URL of a media object to match against the oEmbed providers. - * - * @return array|bool - * Returns a single provider if found, otherwise FALSE. - * - * @see \Drupal\media\OEmbedProviderDiscoveryInterface::getProviders() - */ - protected function getProvider($url) { - $providers = $this->oEmbedProviderDiscovery->getProviders(); - if (!empty($providers_limit)) { - $providers = array_intersect_key($providers, array_fill_keys($providers_limit, '')); - } - // Check url against every scheme of every endpoint of every provider. - foreach ($providers as $provider_name => $provider_info) { - if (!empty($provider_info['endpoints'])) { - foreach ($provider_info['endpoints'] as $endpoint) { - if (!empty($endpoint['schemes'])) { - foreach ($endpoint['schemes'] as $scheme) { - // Convert scheme into a valid RegEx pattern. - $regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme); - if (preg_match("|$regexp|", $url)) { - return $provider_info; - } - } - } - } - } - } - return FALSE; - } - - /** - * Runs oEmbed discovery and returns the endpoint URL if successful. - * - * @param string $url - * The resource's URL. - * - * @return string|bool - * URL of the oEmbed endpoint, or FALSE if the discovery was not successful. - * - * @throws \Drupal\media\Exception\OEmbedResourceException - */ - protected function urlDiscovery($url) { - 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 = (string) $response->getBody(); - - $xpath = new \DOMXpath(Html::load($content)); - $resultJson = $xpath->query("//link[@type='application/json+oembed']"); - $resultXml = $xpath->query("//link[@type='text/xml+oembed']"); - - if ($resultJson->length > 0 || $resultXml->length > 0) { - $result = ($resultJson->length > 0) ? $resultJson : $resultXml; - return $result->item(0)->getAttribute('href'); - } - - return FALSE; - } - -} reverted: --- b/core/modules/media/src/OEmbedInterface.php +++ /dev/null @@ -1,54 +0,0 @@ -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((string) $response->getBody()); - - 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.'); - } - - // Some provider names, like "Wordpress.com" or "Getty Images", contain - // dot chars (".") or spaces (" "), 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; - } - } - -} reverted: --- b/core/modules/media/src/OEmbedProviderDiscoveryInterface.php +++ /dev/null @@ -1,32 +0,0 @@ -oEmbed = $oembed; } @@ -100,7 +100,7 @@ '#post' => (string) $resource['html'], ]; } - catch (OEmbedResourceException $exception) { + catch (ResourceException $exception) { drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); } } diff -u b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php --- b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php +++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedProviderConstraint.php @@ -21,5 +21,5 @@ * @var string */ - public $message = 'The provider used is not allowed.'; + public $message = 'The @name provider is not allowed.'; } 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 @@ -2,11 +2,11 @@ namespace Drupal\media\Plugin\Validation\Constraint; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\media\Exception\OEmbedProviderException; -use Drupal\media\OEmbedInterface; +use Drupal\media\OEmbed\ProviderDiscoveryInterface; +use Drupal\media\OEmbed\ProviderException; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -19,30 +19,20 @@ use StringTranslationTrait; /** - * The oEmbed service. + * The oEmbed provider discovery service. * - * @var \Drupal\media\OEmbed + * @var \Drupal\media\OEmbed\ProviderDiscoveryInterface */ - protected $oEmbed; - - /** - * The logger channel for media. - * - * @var \Drupal\Core\Logger\LoggerChannelInterface - */ - protected $logger; + protected $providerDiscovery; /** * Constructs a new OEmbedProviderConstraintValidator. * - * @param \Drupal\media\OEmbedInterface $oembed - * The oEmbed service. - * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory - * The logger channel for media. + * @param \Drupal\media\OEmbed\ProviderDiscoveryInterface $provider_discovery + * The oEmbed provider discovery service. */ - public function __construct(OEmbedInterface $oembed, LoggerChannelFactoryInterface $logger_factory) { - $this->oEmbed = $oembed; - $this->logger = $logger_factory->get('media'); + public function __construct(ProviderDiscoveryInterface $provider_discovery) { + $this->providerDiscovery = $provider_discovery; } /** @@ -50,8 +40,7 @@ */ public static function create(ContainerInterface $container) { return new static( - $container->get('media.oembed'), - $container->get('logger.factory') + $container->get('media.oembed.provider_discovery') ); } @@ -61,16 +50,29 @@ public function validate($value, Constraint $constraint) { /** @var \Drupal\media\MediaInterface $media */ $media = $value->getEntity(); + $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); + $url = $value->first()->get($main_property)->getString(); + + try { + $provider = $this->providerDiscovery->getProviderByUrl($url); + } + catch (ProviderException $e) { + $this->context->addViolation($this->t('An error occurred while trying to retrieve the oEmbed provider database.')); + return; + } + + if (empty($provider)) { + $this->context->addViolation($this->t('The given URL does not match any known oEmbed providers.')); + return; + } + $source_config = $media->getSource()->getConfiguration(); if (!empty($source_config['allowed_providers'])) { - $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); - try { - if (!$this->oEmbed->isAllowedProvider($value->first()->get($main_property)->getString(), array_keys($source_config['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.')); + if (!in_array($provider['provider_name'], $source_config['allowed_providers'], TRUE)) { + $message = new FormattableMarkup($constraint->message, [ + '@name' => $provider['provider_name'], + ]); + $this->context->addViolation((string) $message); } } } diff -u b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php --- b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php +++ b/core/modules/media/src/Plugin/Validation/Constraint/OEmbedResourceConstraintValidator.php @@ -3,7 +3,8 @@ namespace Drupal\media\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\media\OEmbedInterface; +use Drupal\media\OEmbed\ResourceException; +use Drupal\media\OEmbed\ResourceFetcherInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -14,27 +15,27 @@ class OEmbedResourceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { /** - * The oEmbed service. + * The oEmbed resource fetcher service. * - * @var \Drupal\media\OEmbed + * @var \Drupal\media\OEmbed\ResourceFetcherInterface */ - protected $oEmbed; + protected $resourceFetcher; /** - * Constructs a new TweetVisibleConstraintValidator. + * Constructs a new OEmbedResourceConstraintValidator instance. * - * @param \Drupal\media\OEmbedInterface $oembed - * The oEmbed service. + * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher + * The oEmbed resource fetcher service. */ - public function __construct(OEmbedInterface $oembed) { - $this->oEmbed = $oembed; + public function __construct(ResourceFetcherInterface $resource_fetcher) { + $this->resourceFetcher = $resource_fetcher; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('media.oembed')); + return new static($container->get('media.oembed.resource_fetcher')); } /** @@ -42,7 +43,12 @@ */ public function validate($value, Constraint $constraint) { $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); - if (!$this->oEmbed->getResourceUrl($value->first()->get($main_property)->getString())) { + $url = $value->first()->get($main_property)->getString(); + + try { + $this->resourceFetcher->getResourceUrl($url); + } + catch (ResourceException $e) { $this->context->addViolation($constraint->message); } } 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,14 +9,14 @@ 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\OEmbed\ProviderException; +use Drupal\media\OEmbed\ResourceException; 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 Drupal\media\OEmbed\ResourceFetcherInterface; +use Drupal\media\OEmbed\ProviderDiscoveryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -40,18 +40,18 @@ protected $logger; /** - * The oEmbed service. + * The oEmbed resource fetcher service. * - * @var \Drupal\media\OEmbedInterface + * @var \Drupal\media\OEmbed\ResourceFetcherInterface */ - protected $oEmbed; + protected $resourceFetcher; /** * The OEmbed provider discovery service. * - * @var \Drupal\media\OEmbedProviderDiscoveryInterface + * @var \Drupal\media\OEmbed\ProviderDiscoveryInterface */ - protected $oEmbedProviderDiscovery; + protected $providerDiscovery; /** * Constructs a new OEmbed instance. @@ -72,16 +72,16 @@ * The field type plugin manager service. * @param \Drupal\Core\Logger\LoggerChannelInterface $logger * The logger channel for media. - * @param \Drupal\media\OEmbedInterface $oembed - * The oEmbed service. - * @param \Drupal\media\OEmbedProviderDiscoveryInterface $oembed_provider_discovery - * The oEmbed service. + * @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher + * The oEmbed resource fetcher service. + * @param \Drupal\media\OEmbed\ProviderDiscoveryInterface $provider_discovery + * The oEmbed provider discovery 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, OEmbedProviderDiscoveryInterface $oembed_provider_discovery) { + 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, ResourceFetcherInterface $resource_fetcher, ProviderDiscoveryInterface $provider_discovery) { parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory); $this->logger = $logger; - $this->oEmbed = $oembed; - $this->oEmbedProviderDiscovery = $oembed_provider_discovery; + $this->resourceFetcher = $resource_fetcher; + $this->providerDiscovery = $provider_discovery; } /** @@ -97,8 +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_provider_discovery') + $container->get('media.oembed.resource_fetcher'), + $container->get('media.oembed.provider_discovery') ); } @@ -130,13 +130,20 @@ * {@inheritdoc} */ 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(); + $source_field = $this->getSourceFieldDefinition($media->bundle->entity)->getName(); + /** @var \Drupal\Core\Field\FieldItemInterface $source_field_item */ + $source_field_item = $media->get($source_field)->first(); + + $main_property = $source_field_item->mainPropertyName(); + $resource_url = $source_field_item->get($main_property)->getString(); + try { - $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->getResourceUrl($resource_url)); + $resource_url = $this->resourceFetcher->getResourceUrl($resource_url); + $resource = $this->resourceFetcher->fetchResource($resource_url); } - catch (OEmbedResourceException $exception) { - drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); + catch (ResourceException $e) { + drupal_set_message($e->getMessage(), 'error'); + return NULL; } switch ($name) { @@ -180,13 +187,11 @@ return parent::getMetadata($media, 'thumbnail_uri'); default: - if (!empty($oembed_data[$name])) { - return $oembed_data[$name]; + if (!empty($resource[$name])) { + return $resource[$name]; } break; - } - return NULL; } @@ -205,33 +210,35 @@ $provider_options = []; try { - $provider_options = array_map(function ($provider) { + $provider_options = array_map(function (array $provider) { return $provider['provider_name']; - }, $this->oEmbedProviderDiscovery->getProviders()); + }, $this->providerDiscovery->getAll()); } - 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 (ProviderException $e) { + drupal_set_message($e->getMessage(), 'error'); } $form['allowed_providers'] = [ - '#type' => 'select', + '#type' => 'checkboxes', '#title' => $this->t('Allowed providers'), - '#multiple' => TRUE, '#default_value' => $this->configuration['allowed_providers'], '#options' => $provider_options, '#description' => $this->t('Optionally select the allowed oEmbed providers for this media type. If left blank, all providers will be allowed.'), - '#attributes' => ['size' => 20], ]; - return $form; } /** * {@inheritdoc} */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['allowed_providers'] = array_values($this->configuration['allowed_providers']); + } + + /** + * {@inheritdoc} + */ public function defaultConfiguration() { return [ 'thumbnails_location' => 'public://oembed_thumbnails', diff -u b/core/modules/media/tests/src/Functional/OEmbedProviderDiscoveryTest.php b/core/modules/media/tests/src/Functional/OEmbedProviderDiscoveryTest.php --- b/core/modules/media/tests/src/Functional/OEmbedProviderDiscoveryTest.php +++ b/core/modules/media/tests/src/Functional/OEmbedProviderDiscoveryTest.php @@ -2,12 +2,12 @@ namespace Drupal\Tests\media\Functional; -use Drupal\media\OEmbedProviderDiscovery; +use Drupal\media\ProviderDiscovery; /** * Tests the oEmbed provider discovery. * - * @covers \Drupal\media\OEmbedProviderDiscovery + * @covers \Drupal\media\ProviderDiscovery * * @group media */ @@ -44,8 +44,8 @@ $this->setExpectedException($exceptionClass, $exceptionMessage); - /** @var \Drupal\media\OEmbedProviderDiscoveryInterface $providerDiscovery */ - $providerDiscovery = new OEmbedProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); + /** @var \Drupal\media\ProviderDiscoveryInterface $providerDiscovery */ + $providerDiscovery = new ProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); $providerDiscovery->getProviders(); } @@ -85,8 +85,8 @@ $this->setExpectedException($exceptionClass, $exceptionMessage); - /** @var \Drupal\media\OEmbedProviderDiscoveryInterface $providerDiscovery */ - $providerDiscovery = new OEmbedProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); + /** @var \Drupal\media\ProviderDiscoveryInterface $providerDiscovery */ + $providerDiscovery = new ProviderDiscovery($clientFactory, $cacheBackend, $logger, $providerUrl); $providerDiscovery->getProviders(); } diff -u b/core/modules/media/tests/src/Functional/OEmbedTest.php b/core/modules/media/tests/src/Functional/OEmbedTest.php --- b/core/modules/media/tests/src/Functional/OEmbedTest.php +++ b/core/modules/media/tests/src/Functional/OEmbedTest.php @@ -63,7 +63,7 @@ $logger = $this->container->get('logger.factory'); $client = $this->container->get('http_client_factory'); - $oEmbedProviderDiscovery = $this->getMockBuilder('Drupal\media\OEmbedProviderDiscovery') + $oEmbedProviderDiscovery = $this->getMockBuilder('Drupal\media\ProviderDiscovery') ->disableOriginalConstructor() ->getMock(); $oEmbedProviderDiscovery->method('getProviders')->willReturn($providers); only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbed/ProviderDiscovery.php @@ -0,0 +1,133 @@ +httpClient = $http_client; + $this->providersUrl = $providers_url; + $this->cacheBackend = $cache_backend; + $this->useCaches = isset($cache_backend); + } + + /** + * {@inheritdoc} + */ + public function getAll() { + $cache_id = 'media:oembed_providers'; + + $cached = $this->cacheGet($cache_id); + if ($cached) { + return $cached->data; + } + + try { + $response = $this->httpClient->get($this->providersUrl); + } + catch (RequestException $e) { + throw new ProviderException("Could not retrieve the oEmbed provider database from $this->providersUrl", [], $e); + } + + $providers = Json::decode((string) $response->getBody()); + + if (!is_array($providers) || empty($providers)) { + throw new ProviderException('Remote oEmbed providers database returned invalid or empty list.'); + } + + // Some provider names, like "Wordpress.com" or "Getty Images", contain + // dot chars (".") or spaces (" "), 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) { + $name = $provider['provider_name']; + $keyed_providers[$name] = $provider; + } + + $this->cacheSet($cache_id, $keyed_providers, time() + (60 * 60 * 24 * 7)); + return $keyed_providers; + } + + /** + * {@inheritdoc} + */ + public function get($provider_name) { + $providers = $this->getAll(); + + if (isset($providers[$provider_name])) { + return $providers[$provider_name]; + } + else { + throw new \InvalidArgumentException("Unknown provider '$provider_name'"); + } + } + + /** + * {@inheritdoc} + */ + public function getProviderByUrl($url) { + if ($url instanceof Url) { + $url = $url->toString(); + } + + // Check the URL against every scheme of every endpoint of every provider + // until we find a match. + foreach ($this->getAll() as $provider_name => $provider_info) { + if (empty($provider_info['endpoints'])) { + continue; + } + foreach ($provider_info['endpoints'] as $endpoint) { + if (empty($endpoint['schemes'])) { + continue; + } + foreach ($endpoint['schemes'] as $scheme) { + // Convert scheme into a valid regular expression. + $regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme); + if (preg_match("|$regexp|", $url)) { + return $provider_info; + } + } + } + } + return FALSE; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbed/ProviderDiscoveryInterface.php @@ -0,0 +1,59 @@ +' if not. + * @param array $provider + * (optional) The provider information. + * @param \Exception $previous + * (optional) The previous exception, if any. + */ + public function __construct($message, array $provider = [], \Exception $previous = NULL) { + $variables = [ + '@name' => isset($provider['provider_name']) ? $provider['provider_name'] : '', + ]; + $message = (string) new FormattableMarkup($message, $variables); + parent::__construct($message, 0, $previous); + } + + /** + * Gets the provider information, if available. + * + * @return array + * The information about the oEmbed provider which caused the exception. + */ + public function getProvider() { + return $this->provider; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbed/ResourceException.php @@ -0,0 +1,61 @@ +url = $url; + $this->resource = $resource; + parent::__construct($message, 0, $previous); + } + + /** + * Gets the URL of the oEmbed resource which caused the exception. + * + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * Gets the oEmbed resource which caused the exception, if available. + * + * @return array + */ + public function getResource() { + return $this->resource; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbed/ResourceFetcher.php @@ -0,0 +1,223 @@ +providerDiscovery = $provider_discovery; + $this->httpClient = $http_client; + $this->moduleHandler = $module_handler; + $this->cacheBackend = $cache_backend; + $this->useCaches = isset($cache_backend); + } + + /** + * {@inheritdoc} + */ + public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) { + if ($this->urlCache === NULL) { + $this->urlCache = []; + + $cached = $this->cacheGet('media:oembed_resource_url'); + if ($cached) { + $this->urlCache = $cached->data; + } + } + + if ($url instanceof Url) { + $url = $url->toString(); + } + + if (isset($this->urlCache[$url])) { + return $this->urlCache[$url]; + } + + $provider = $this->providerDiscovery->getProviderByUrl($url); + if ($provider) { + $resource_url = $this->buildResourceUrl($provider, $url); + } + else { + $resource_url = $this->discoverResourceUrl($url); + } + + if (empty($resource_url)) { + throw new ResourceException('Could not determine the oEmbed resource URL.', $url); + } + + $parsed_url = UrlHelper::parse($resource_url); + if ($max_width) { + $parsed_url['query']['maxwidth'] = $max_width; + } + if ($max_height) { + $parsed_url['query']['maxheight'] = $max_height; + } + // Let other modules alter the query string, because some oEmbed providers + // are providing extra parameters. For example, Instagram also supports the + // 'omitscript' parameter. + $this->moduleHandler->alter('oembed_resource_url', $parsed_url); + $resource_url = $parsed_url['path'] . '?' . UrlHelper::buildQuery($parsed_url['query']); + + $this->urlCache[$url] = $resource_url; + $this->cacheSet('media:oembed_resource_url', $this->urlCache); + return $resource_url; + } + + protected function buildResourceUrl(array $provider_info, $url) { + if (empty($provider_info['endpoints'])) { + throw new ProviderException('Provider @name does not define any endpoints.', $provider_info); + } + + $endpoints = array_filter($provider_info['endpoints'], function (array $endpoint) { + return isset($endpoint['url']); + }); + if (empty($endpoints)) { + throw new ProviderException('Provider @name does not provide any endpoint URLs.', $provider_info); + } + + $query = [ + 'url' => $url, + ]; + foreach ($endpoints as $endpoint) { + $format = 'json'; + if (!empty($endpoint['formats']) && is_array($endpoint['formats'])) { + $format = reset($endpoint['formats']); + } + $endpoint_url = str_replace('{format}', $format, $endpoint['url']); + + return $endpoint_url . '?' . UrlHelper::buildQuery($query); + } + return FALSE; + } + + /** + * Runs oEmbed discovery and returns the endpoint URL if successful. + * + * @param string $url + * The resource's URL. + * + * @return string|bool + * URL of the oEmbed endpoint, or FALSE if the discovery was not successful. + * + * @throws \Drupal\media\OEmbed\ResourceException + */ + protected function discoverResourceUrl($url) { + try { + $response = $this->httpClient->get($url); + } + catch (RequestException $e) { + throw new ResourceException('Could not fetch oEmbed resource.', $url, [], $e); + } + + $document = Html::load((string) $response->getBody()); + $xpath = new \DOMXpath($document); + + return $this->findUrl($xpath, 'json') ?: $this->findUrl($xpath, 'xml'); + } + + protected function findUrl(\DOMXPath $xpath, $format) { + $result = $xpath->query("//link[@type='application/$format+oembed']"); + return $result->length ? $result->item(0)->getAttribute('href') : FALSE; + } + + /** + * {@inheritdoc} + */ + public function fetchResource($url) { + if ($url instanceof Url) { + $url = $url->toString(); + } + + $cache_id = 'media:oembed:' . hash('sha256', $url); + + $cached = $this->cacheGet($cache_id); + if ($cached) { + return $cached->data; + } + + try { + $response = $this->httpClient->get($url); + } + catch (RequestException $e) { + throw new ResourceException('Could not retrieve the oEmbed resource.', $url, [], $e); + } + + $format = $response->getHeader('Content-Type'); + $content = (string) $response->getBody(); + + // If the response is XML, magically convert it to JSON. + if (strpos($format[0], 'text/xml') !== FALSE) { + // Load XML into SimpleXMLElement. + $content = simplexml_load_string($content, "SimpleXMLElement", LIBXML_NOCDATA); + // Convert the SimpleXMLElement to JSON. + $content = Json::encode($content); + } + // If the response wasn't XML or JSON, we are in bat country. + elseif (strpos($format[0], 'application/json') === FALSE) { + throw new ResourceException('The fetched resource did not have a valid Content-Type header.', $url); + } + + $resource = Json::decode($content); + $this->cacheSet($cache_id, $resource); + return $resource; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/src/OEmbed/ResourceFetcherInterface.php @@ -0,0 +1,39 @@ +