diff -u b/core/modules/media/media.module b/core/modules/media/media.module --- b/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -44,6 +44,11 @@ 'media' => [ 'render element' => 'elements', ], + 'media_oembed' => [ + 'variables' => [ + 'post' => NULL, + ], + ], ]; } @@ -61,20 +66,4 @@ /** - * 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') { - $providers[$provider_name]['endpoints'][0]['schemes'] = [ - 'http*://*youtube.com/*', - 'http*://*youtu.be/*', - ]; - } - } -} - -/** * Implements hook_theme_suggestions_HOOK(). */ diff -u b/core/modules/media/src/Exception/OEmbedProviderException.php b/core/modules/media/src/Exception/OEmbedProviderException.php --- b/core/modules/media/src/Exception/OEmbedProviderException.php +++ b/core/modules/media/src/Exception/OEmbedProviderException.php @@ -8,33 +8 @@ -class OEmbedProviderException extends \Exception { - - /** - * List of oEmbed providers. - * - * @var array - */ - protected $providers; - - /** - * OEmbedResourceException constructor. - * - * @param string $message - * Exception message. - * @param array $providers - * List of providers. - */ - public function __construct($message = "", array $providers = []) { - parent::__construct($message); - $this->providers = $providers; - } - - /** - * Get the list of providers. - * - * @return array - * Returns oEmbed provider list. - */ - public function getProviders() { - return $this->providers; - } - -} +class OEmbedProviderException extends \Exception {} 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 @@ -4,6 +4,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\UseCacheBackendTrait; use Drupal\Core\Http\ClientFactory; @@ -91,38 +92,7 @@ /** * {@inheritdoc} */ - public function isProvidedUrl($url, array $providers_limit = []) { - $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 TRUE; - } - } - } - } - } - } - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function urlDiscovery($url) { - if (!$this->isProvidedUrl($url)) { - return FALSE; - } - + public function getResourceUrl($url) { $cacheKey = 'media:oembed:url:' . sha1($url); if (!empty($this->discovered[$url])) { return $this->discovered[$url]; @@ -132,49 +102,43 @@ 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 = (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; - $this->discovered[$url] = $result->item(0)->getAttribute('href'); - + if ($this->discovered[$url] = $this->urlDiscovery($url)) { $this->cacheSet($cacheKey, $this->discovered[$url]); return $this->discovered[$url]; } + if ($provider_info = $this->getProvider($url)) { + // todo: Take formats into account + // todo: We could specify width and height + if (!empty($provider_info['endpoints'])) { + foreach ($provider_info['endpoints'] as $endpoint) { + if (!empty($endpoint['url'])) { + $options = [ + 'url' => $url, + ]; + $query_parameter = UrlHelper::buildQuery($options); + $this->discovered[$url] = $endpoint['url'] . '?' . $query_parameter; + $this->cacheSet($cacheKey, $this->discovered[$url]); + return $this->discovered[$url]; + } + } + } + } } - - return FALSE; + throw new OEmbedResourceException(); } /** * {@inheritdoc} */ - public function isOEmbedResource($url) { - try { - if ($this->urlDiscovery($url)) { + 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; } } - catch (OEmbedResourceException $exception) { - return FALSE; - } - // TODO: handle providers that do not support discovery. + return FALSE; } @@ -209,11 +173,12 @@ $format = $response->getHeader('Content-Type'); $content = (string) $response->getBody(); + $data = ''; - if ($format[0] == 'application/json') { + if (strpos($format[0], 'application/json') !== FALSE) { $data = Json::decode($content); } - elseif ($format[0] == 'text/xml') { + elseif (strpos($format[0], 'text/xml') !== FALSE) { $data = Json::decode(Json::encode($content)); } @@ -233,2 +198,75 @@ + /** + * 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; + } + } 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 @@ -12,39 +12,28 @@ * * @param string $url * The URL to be tested. - * @param array $allowed_providers - * (optional) A list of providers to restrict to. If present, this needs to - * be an array where the values are the provider names - * (codified with bin2hex()). If omitted, all providers will be considered. * * @return bool * If a match was found, will return TRUE, otherwise FALSE. * * @see \Drupal\media\OEmbedInterface::getProviders() */ - public function isProvidedUrl($url, array $allowed_providers = []); - - /** - * 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. - */ - public function urlDiscovery($url); + public function getResourceUrl($url); /** * Determines whether a given URL is an oEmbed resource. * * @param string $url * URL of the resource. + * @param array $allowed_providers + * (optional) A list of providers to restrict to. If present, this needs to + * be an array where the values are the provider names + * (codified with bin2hex()). If omitted, all providers will be considered. * * @return bool * TRUE if the URL represents a valid oEmbed resource, or FALSE otherwise. */ - public function isOEmbedResource($url); + public function isAllowedProvider($url, array $allowed_providers = []); /** * Fetches information about the oEmbed resource. diff -u b/core/modules/media/src/OEmbedProviderDiscovery.php b/core/modules/media/src/OEmbedProviderDiscovery.php --- b/core/modules/media/src/OEmbedProviderDiscovery.php +++ b/core/modules/media/src/OEmbedProviderDiscovery.php @@ -100,11 +100,9 @@ $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.', []); + throw new OEmbedProviderException('Remote oEmbed providers database returned empty list.'); } - \Drupal::moduleHandler()->alter('oembed_providers', $providers); - // 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 @@ -112,17 +110,6 @@ // necessary. $keyed_providers = []; foreach ($providers as $provider) { - // TODO: Filter out providers without discovery and schemes until we - // know how to handle them. - $filter = TRUE; - foreach ($provider['endpoints'] as $endpoint) { - if (!empty($endpoint['discovery']) && !empty($endpoint['schemes'])) { - $filter = FALSE; - } - } - if ($filter) { - continue; - } $keyed_providers[bin2hex($provider['provider_name'])] = $provider; } 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 @@ -83,10 +83,10 @@ $main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); if (!empty($item->{$main_property})) { try { - $resource = $this->oEmbed->fetchResource($this->oEmbed->urlDiscovery($item->{$main_property})); + $resource = $this->oEmbed->fetchResource($this->oEmbed->getResourceUrl($item->{$main_property})); $element[$delta] = [ - '#markup' => $resource['html'], - '#allowed_tags' => ['iframe'], + '#theme' => 'media_oembed', + '#post' => (string) $resource['html'], ]; } catch (OEmbedResourceException $exception) { 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 @@ -65,7 +65,7 @@ if (!empty($source_config['allowed_providers'])) { $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); try { - if (!$this->oEmbed->isProvidedUrl($value->first()->get($main_property)->getString(), array_keys($source_config['allowed_providers']))) { + if (!$this->oEmbed->isAllowedProvider($value->first()->get($main_property)->getString(), array_keys($source_config['allowed_providers']))) { $this->context->addViolation($constraint->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 @@ -42,7 +42,7 @@ */ public function validate($value, Constraint $constraint) { $main_property = $value->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName(); - if (!$this->oEmbed->isOEmbedResource($value->first()->get($main_property)->getString())) { + if (!$this->oEmbed->getResourceUrl($value->first()->get($main_property)->getString())) { $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 @@ -17,8 +17,6 @@ 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; /** @@ -135,7 +133,7 @@ $main_property = $media->get($this->configuration['source_field'])->first()->mainPropertyName(); $resource_url = $media->get($this->configuration['source_field'])->first()->get($main_property)->getString(); try { - $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->urlDiscovery($resource_url)); + $oembed_data = $this->oEmbed->fetchResource($this->oEmbed->getResourceUrl($resource_url)); } catch (OEmbedResourceException $exception) { drupal_set_message($this->t('Could not retrieve the remote URL.'), 'error'); @@ -176,7 +174,7 @@ return parent::getMetadata($media, 'default_name'); case 'thumbnail_uri': - if ($uri = $this->getMetadata($media, 'thumbnail_local_uri')) { + if ($uri = $this->getMetadata($media, 'thumbnail_local')) { return $uri; } return parent::getMetadata($media, 'thumbnail_uri'); 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 @@ -70,13 +70,11 @@ $oEmbedProviderDiscovery->method('getProviders')->willReturn($providers); $oEmbed = new OEmbed($oEmbedProviderDiscovery, $client, $cacheBackend, $logger); - $this->assertTrue($oEmbed->isProvidedUrl('https://vimeo.com/7073899')); - $this->assertTrue($oEmbed->isProvidedUrl('https://www.youtube.com/watch?v=lZ-s3DRZJKY')); + $this->assertTrue($oEmbed->isAllowedProvider('https://vimeo.com/7073899')); + $this->assertTrue($oEmbed->isAllowedProvider('https://www.youtube.com/watch?v=lZ-s3DRZJKY')); - $this->assertFalse($oEmbed->isProvidedUrl('https://vimeo.com/7073899', ['596f7554756265'])); - $this->assertTrue($oEmbed->isProvidedUrl('https://www.youtube.com/watch?v=lZ-s3DRZJKY', ['596f7554756265'])); - - $this->assertFalse($oEmbed->isProvidedUrl('https://www.instagram.com/p/Bb-Qt2nAmu3/')); + $this->assertFalse($oEmbed->isAllowedProvider('https://vimeo.com/7073899', ['596f7554756265'])); + $this->assertTrue($oEmbed->isAllowedProvider('https://www.youtube.com/watch?v=lZ-s3DRZJKY', ['596f7554756265'])); } /** @@ -91,8 +89,10 @@ $oEmbed = new OEmbed($oEmbedProviderDiscovery, $client, $cacheBackend, $logger); - $this->assertSame('https://vimeo.com/api/oembed.json?url=https%3A%2F%2Fvimeo.com%2F7073899', $oEmbed->urlDiscovery('https://vimeo.com/7073899')); - $this->assertFalse($oEmbed->urlDiscovery('https://twitter.com/drupaldevdays/status/935643039741202432')); + // Received by discovery. + $this->assertSame('https://vimeo.com/api/oembed.json?url=https%3A%2F%2Fvimeo.com%2F7073899', $oEmbed->getResourceUrl('https://vimeo.com/7073899')); + // Received by URL building. + $this->assertSame('https://publish.twitter.com/oembed?url=https://twitter.com/drupaldevdays/status/935643039741202432', $oEmbed->getResourceUrl('https://twitter.com/drupaldevdays/status/935643039741202432')); } /** diff -u b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedTest.php --- b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaSourceOEmbedTest.php @@ -83,7 +83,7 @@ // Try to create a youtube item, which should not be allowed. $this->drupalGet("media/add/{$media_type_id}"); - $page->fillField("OEmbed", 'https://www.youtube.com/watch?v=lZ-s3DRZJKY'); + $page->fillField("OEmbed", 'https://clips.twitch.tv/SleepyBoringLapwingRuleFive'); $page->pressButton('Save'); $assert_session->pageTextContains('The provider used is not allowed.'); diff -u b/core/profiles/standard/config/optional/media.type.video.yml b/core/profiles/standard/config/optional/media.type.video.yml --- b/core/profiles/standard/config/optional/media.type.video.yml +++ b/core/profiles/standard/config/optional/media.type.video.yml @@ -3,7 +3,7 @@ dependencies: { } id: video label: Video -description: "Use thie media type to store remote content such as YouTube videos." +description: "Use this media type to store remote content such as YouTube videos." source: oembed queue_thumbnail_downloads: false new_revision: true only in patch2: unchanged: --- /dev/null +++ b/core/modules/media/templates/media-oembed.html.twig @@ -0,0 +1,9 @@ +{# +/** + * @file + * Default theme implementation to display a post. + * + * @ingroup themeable + */ +#} +{{ post|raw }}