Skip to content
Snippets Groups Projects
Unverified Commit cc59fcb0 authored by Alex Pott's avatar Alex Pott
Browse files

Revert "Issue #2831944 by chr.fritsch, phenaproxima, marcoscano, seanB,...

Revert "Issue #2831944 by chr.fritsch, phenaproxima, marcoscano, seanB, slashrsm, robpowell, alexpott, samuel.mortenson, dawehner, tstoeckler, Wim Leers, Gábor Hojtsy, martin107, aheimlich, idebr, tedbow, larowlan, ckrina, mtodor, bkosborne: Implement media source plugin for remote video via oEmbed"

This reverts commit 8f6fcbdd.
parent 93894b24
No related branches found
No related tags found
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
Showing
with 2 additions and 1542 deletions
icon_base_uri: 'public://media-icons/generic'
oembed_providers: 'https://oembed.com/providers.json'
......@@ -5,12 +5,6 @@ media.settings:
icon_base_uri:
type: string
label: 'Full URI to a folder where the media icons will be installed'
iframe_domain:
type: string
label: 'Domain from which to serve oEmbed content in an iframe'
oembed_providers:
type: string
label: 'The URL of the oEmbed providers database in JSON format'
media.type.*:
type: config_entity
......@@ -46,21 +40,6 @@ field.formatter.settings.media_thumbnail:
type: field.formatter.settings.image
label: 'Media thumbnail field display format settings'
field.formatter.settings.oembed:
type: mapping
label: 'oEmbed display format settings'
mapping:
max_width:
type: integer
label: 'Maximum width'
max_height:
type: integer
label: 'Maximum height'
field.widget.settings.oembed_textfield:
type: field.widget.settings.string_textfield
label: 'oEmbed widget format settings'
media.source.*:
type: mapping
label: 'Media source settings'
......@@ -81,20 +60,6 @@ media.source.video_file:
type: media.source.field_aware
label: '"Video" media source configuration'
media.source.oembed:*:
type: media.source.field_aware
label: 'oEmbed media source configuration'
mapping:
thumbnails_uri:
type: uri
label: 'Thumbnail storage URI'
allowed_providers:
type: sequence
label: 'Allowed oEmbed providers'
sequence:
type: string
label: 'Provider name'
media.source.field_aware:
type: mapping
mapping:
......
......@@ -20,23 +20,6 @@ function hook_media_source_info_alter(array &$sources) {
$sources['youtube']['label'] = t('Youtube rocks!');
}
/**
* Alters an oEmbed resource URL before it is fetched.
*
* @param array $parsed_url
* A parsed URL, as returned by \Drupal\Component\Utility\UrlHelper::parse().
* @param \Drupal\media\OEmbed\Provider $provider
* The oEmbed provider for the resource.
*
* @see \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()
*/
function hook_oembed_resource_url_alter(array &$parsed_url, \Drupal\media\OEmbed\Provider $provider) {
// Always serve YouTube videos from youtube-nocookie.com.
if ($provider->getName() === 'YouTube') {
$parsed_url['path'] = str_replace('://youtube.com/', '://youtube-nocookie.com/', $parsed_url['path']);
}
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -8,4 +8,3 @@ dependencies:
- drupal:file
- drupal:image
- drupal:user
configure: media.settings
......@@ -5,9 +5,6 @@
* Install, uninstall and update hooks for Media module.
*/
use Drupal\Core\Url;
use Drupal\media\MediaTypeInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Drupal\user\RoleInterface;
use Drupal\user\Entity\Role;
......@@ -78,36 +75,6 @@ function media_requirements($phase) {
}
}
}
elseif ($phase === 'runtime') {
// Check that oEmbed content is served in an iframe on a different domain,
// and complain if it isn't.
$domain = \Drupal::config('media.settings')->get('iframe_domain');
if (!\Drupal::service('media.oembed.url_resolver')->isSecure($domain)) {
// Find all media types which use a source plugin that implements
// OEmbedInterface.
$media_types = \Drupal::entityTypeManager()
->getStorage('media_type')
->loadMultiple();
$oembed_types = array_filter($media_types, function (MediaTypeInterface $media_type) {
return $media_type->getSource() instanceof OEmbedInterface;
});
if ($oembed_types) {
// @todo Potentially allow site administrators to suppress this warning
// permanently. See https://www.drupal.org/project/drupal/issues/2962753
// for more information.
$requirements['media_insecure_iframe'] = [
'title' => t('Media'),
'description' => t('It is potentially insecure to display oEmbed content in a frame that is served from the same domain as your main Drupal site, as this may allow execution of third-party code. <a href=":url">You can specify a different domain for serving oEmbed content here</a>.', [
':url' => Url::fromRoute('media.settings')->setAbsolute()->toString(),
]),
'severity' => REQUIREMENT_WARNING,
];
}
}
}
return $requirements;
}
......@@ -153,12 +120,3 @@ function media_update_8500() {
$role->save();
}
}
/**
* Creates the oembed_providers config setting.
*/
function media_update_8600() {
\Drupal::configFactory()->getEditable('media.settings')
->set('oembed_providers', 'https://oembed.com/providers.json')
->save(TRUE);
}
......@@ -3,9 +3,3 @@ entity.media_type.collection:
parent: system.admin_structure
description: 'Manage media types.'
route_name: entity.media_type.collection
media.settings:
title: 'Media settings'
parent: system.admin_config_media
description: 'Manage media settings.'
route_name: media.settings
......@@ -15,7 +15,6 @@
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
/**
* Implements hook_help().
......@@ -73,11 +72,6 @@ function media_theme() {
'render element' => 'element',
'base hook' => 'field_multiple_value_form',
],
'media_oembed' => [
'variables' => [
'post' => NULL,
],
],
];
}
......@@ -98,7 +92,6 @@ function media_entity_access(EntityInterface $entity, $operation, AccountInterfa
*/
function media_theme_suggestions_media(array $variables) {
$suggestions = [];
/** @var \Drupal\media\MediaInterface $media */
$media = $variables['elements']['#media'];
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
......@@ -106,18 +99,6 @@ function media_theme_suggestions_media(array $variables) {
$suggestions[] = 'media__' . $media->bundle();
$suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode;
$source = $media->getSource();
if ($source instanceof OEmbedInterface) {
$suggestions[] = 'media__oembed';
$provider_id = $source->getMetadata($media, 'provider_name');
if ($provider_id) {
$provider_id = \Drupal::transliteration()->transliterate($provider_id);
$provider_id = preg_replace('/[^a-z0-9_]+/', '_', mb_strtolower($provider_id));
$suggestions[] = "media__oembed__$provider_id";
}
}
return $suggestions;
}
......
......@@ -24,19 +24,3 @@ entity.media.revision:
requirements:
_access_media_revision: 'view'
media: \d+
media.oembed_iframe:
path: '/media/oembed'
defaults:
_controller: '\Drupal\media\Controller\OEmbedIframeController::render'
requirements:
_permission: 'access content'
_csrf_token: 'TRUE'
media.settings:
path: '/admin/config/media/media-settings'
defaults:
_form: '\Drupal\media\Form\MediaSettingsForm'
_title: 'Media settings'
requirements:
_permission: 'administer media'
......@@ -2,17 +2,9 @@ services:
plugin.manager.media.source:
class: Drupal\media\MediaSourceManager
parent: default_plugin_manager
access_check.media.revision:
class: Drupal\media\Access\MediaRevisionAccessCheck
arguments: ['@entity_type.manager']
tags:
- { name: access_check, applies_to: _access_media_revision }
media.oembed.url_resolver:
class: Drupal\media\OEmbed\UrlResolver
arguments: ['@media.oembed.provider_repository', '@media.oembed.resource_fetcher', '@http_client', '@module_handler', '@router.request_context', '@cache.default']
media.oembed.provider_repository:
class: Drupal\media\OEmbed\ProviderRepository
arguments: ['@http_client', '@config.factory', '@datetime.time', '@cache.default']
media.oembed.resource_fetcher:
class: Drupal\media\OEmbed\ResourceFetcher
arguments: ['@http_client', '@media.oembed.provider_repository', '@cache.default']
<?php
namespace Drupal\media\Controller;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Controller which renders an oEmbed resource in a bare page (without blocks).
*
* This controller is meant to render untrusted third-party HTML returned by
* an oEmbed provider in an iframe, so as to mitigate the potential dangers of
* of displaying third-party markup (i.e., XSS). The HTML returned by this
* controller should not be trusted, and should *never* be displayed outside
* of an iframe.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class OEmbedIframeController implements ContainerInjectionInterface {
/**
* The oEmbed resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs an OEmbedIframeController instance.
*
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
* The oEmbed resource fetcher service.
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
* The oEmbed URL resolver service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Logger\LoggerChannelInterface $logger
* The logger channel.
*/
public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerChannelInterface $logger) {
$this->resourceFetcher = $resource_fetcher;
$this->urlResolver = $url_resolver;
$this->renderer = $renderer;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('media.oembed.resource_fetcher'),
$container->get('media.oembed.url_resolver'),
$container->get('renderer'),
$container->get('logger.factory')->get('media')
);
}
/**
* Renders an oEmbed resource.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Will be thrown when the 'url' parameter is not specified, invalid, or not
* external.
*/
public function render(Request $request) {
$url = $request->query->get('url');
if (!$url) {
throw new BadRequestHttpException('url parameter not provided');
}
if (!UrlHelper::isValid($url, TRUE)) {
throw new BadRequestHttpException('url parameter is invalid');
}
if (!UrlHelper::isExternal($url)) {
throw new BadRequestHttpException('url parameter is not external');
}
// Return a response instead of a render array so that the frame content
// will not have all the blocks and page elements normally rendered by
// Drupal.
$response = new CacheableResponse();
$response->addCacheableDependency(Url::createFromRequest($request));
try {
$resource_url = $this->urlResolver->getResourceUrl($url, $request->query->getInt('max_width', NULL), $request->query->getInt('max_height', NULL));
$resource = $this->resourceFetcher->fetchResource($resource_url);
// Render the content in a new render context so that the cacheability
// metadata of the rendered HTML will be captured correctly.
$content = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($resource) {
$element = [
'#theme' => 'media_oembed',
// Even though the resource HTML is untrusted, Markup::create() will
// create a trusted string. The only reason this is okay is because
// we are serving it in an iframe, which will mitigate the potential
// dangers of displaying third-party markup.
'#post' => Markup::create($resource->getHtml()),
];
return $this->renderer->render($element);
});
$response->setContent($content)->addCacheableDependency($resource);
}
catch (ResourceException $e) {
// Prevent the response from being cached.
$response->setMaxAge(0);
// @todo Log additional information from ResourceException, to help with
// debugging, in https://www.drupal.org/project/drupal/issues/2972846.
$this->logger->error($e->getMessage());
}
return $response;
}
}
<?php
namespace Drupal\media\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\media\OEmbed\UrlResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form to configure Media settings.
*
* @internal
*/
class MediaSettingsForm extends ConfigFormBase {
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* MediaSettingsForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
* The oEmbed URL resolver service.
*/
public function __construct(ConfigFactoryInterface $config_factory, UrlResolverInterface $url_resolver) {
parent::__construct($config_factory);
$this->urlResolver = $url_resolver;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('media.oembed.url_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'media_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['media.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$domain = $this->config('media.settings')->get('iframe_domain');
if (!$this->urlResolver->isSecure($domain)) {
$message = $this->t('It is potentially insecure to display oEmbed content in a frame that is served from the same domain as your main Drupal site, as this may allow execution of third-party code. <a href="https://oembed.com/#section3" target="_blank">Take a look here for more information</a>.');
$this->messenger()->addWarning($message);
}
$description = '<p>' . $this->t('Displaying media assets from third-party services, such as YouTube or Twitter, can be risky. This is because many of these services return arbitrary HTML to represent those assets, and that HTML may contain executable JavaScript code. If handled improperly, this can increase the risk of your site being compromised.') . '</p>';
$description .= '<p>' . $this->t('In order to mitigate the risks, third-party assets are displayed in an iFrame, which effectively sandboxes any executable code running inside it. For even more security, the iFrame can be served from an alternate domain (that also points to your Drupal site), which you can configure on this page. This helps safeguard cookies and other sensitive information.') . '</p>';
$form['security'] = [
'#type' => 'details',
'#title' => $this->t('Security'),
'#description' => $description,
'#open' => TRUE,
];
// @todo Figure out how and if we should validate that this domain actually
// points back to Drupal.
// See https://www.drupal.org/project/drupal/issues/2965979 for more info.
$form['security']['iframe_domain'] = [
'#type' => 'url',
'#title' => $this->t('iFrame domain'),
'#size' => 40,
'#maxlength' => 255,
'#default_value' => $domain,
'#description' => $this->t('Enter a different domain from which to serve oEmbed content, including the <em>http://</em> or <em>https://</em> prefix. This domain needs to point back to this site, or existing oEmbed content may not display correctly, or at all.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('media.settings')
->set('iframe_domain', $form_state->getValue('iframe_domain'))
->save();
parent::submitForm($form, $form_state);
}
}
......@@ -301,9 +301,7 @@ public function createSourceField(MediaTypeInterface $type) {
* returned. Otherwise, a new, unused one is generated.
*/
protected function getSourceFieldName() {
// Some media sources are using a deriver, so their plugin IDs may contain
// a separator (usually ':') which is not allowed in field names.
$base_id = 'field_media_' . str_replace(static::DERIVATIVE_SEPARATOR, '_', $this->getPluginId());
$base_id = 'field_media_' . $this->getPluginId();
$tries = 0;
$storage = $this->entityTypeManager->getStorage('field_storage_config');
......
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Utility\UrlHelper;
/**
* Value object for oEmbed provider endpoints.
*
* @internal
* This class is an internal part of the oEmbed system and should only be
* instantiated by instances of Drupal\media\OEmbed\Provider.
*/
class Endpoint {
/**
* The endpoint's URL.
*
* @var string
*/
protected $url;
/**
* The provider this endpoint belongs to.
*
* @var \Drupal\media\OEmbed\Provider
*/
protected $provider;
/**
* List of URL schemes supported by the provider.
*
* @var string[]
*/
protected $schemes;
/**
* List of supported formats. Only 'json' and 'xml' are allowed.
*
* @var string[]
*
* @see https://oembed.com/#section2
*/
protected $formats;
/**
* Whether the provider supports oEmbed discovery.
*
* @var bool
*/
protected $supportsDiscovery;
/**
* Endpoint constructor.
*
* @param string $url
* The endpoint URL. May contain a @code '{format}' @endcode placeholder.
* @param \Drupal\media\OEmbed\Provider $provider
* The provider this endpoint belongs to.
* @param string[] $schemes
* List of URL schemes supported by the provider.
* @param string[] $formats
* List of supported formats. Can be "json", "xml" or both.
* @param bool $supports_discovery
* Whether the provider supports oEmbed discovery.
*
* @throws \InvalidArgumentException
* If the endpoint URL is empty.
*/
public function __construct($url, Provider $provider, array $schemes = [], array $formats = [], $supports_discovery = FALSE) {
$this->provider = $provider;
$this->schemes = array_map('mb_strtolower', $schemes);
$this->formats = $formats = array_map('mb_strtolower', $formats);
// Assert that only the supported formats are present.
assert(array_diff($formats, ['json', 'xml']) == []);
// Use the first provided format to build the endpoint URL. If no formats
// are provided, default to JSON.
$this->url = str_replace('{format}', reset($this->formats) ?: 'json', $url);
if (!UrlHelper::isValid($this->url, TRUE) || !UrlHelper::isExternal($this->url)) {
throw new \InvalidArgumentException('oEmbed endpoint must have a valid external URL');
}
$this->supportsDiscovery = (bool) $supports_discovery;
}
/**
* Returns the endpoint URL.
*
* The URL will be built with the first available format. If the endpoint
* does not provide any formats, JSON will be used.
*
* @return string
* The endpoint URL.
*/
public function getUrl() {
return $this->url;
}
/**
* Returns the provider this endpoint belongs to.
*
* @return \Drupal\media\OEmbed\Provider
* The provider object.
*/
public function getProvider() {
return $this->provider;
}
/**
* Returns list of URL schemes supported by the provider.
*
* @return string[]
* List of schemes.
*/
public function getSchemes() {
return $this->schemes;
}
/**
* Returns list of supported formats.
*
* @return string[]
* List of formats.
*/
public function getFormats() {
return $this->formats;
}
/**
* Returns whether the provider supports oEmbed discovery.
*
* @return bool
* Returns TRUE if the provides discovery, otherwise FALSE.
*/
public function supportsDiscovery() {
return $this->supportsDiscovery;
}
/**
* Tries to match a URL against the endpoint schemes.
*
* @param string $url
* Media item URL.
*
* @return bool
* TRUE if the URL matches against the endpoint schemes, otherwise FALSE.
*/
public function matchUrl($url) {
foreach ($this->getSchemes() as $scheme) {
// Convert scheme into a valid regular expression.
$regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme);
if (preg_match("|^$regexp$|", $url)) {
return TRUE;
}
}
return FALSE;
}
/**
* Builds and returns the endpoint URL.
*
* @param string $url
* The canonical media URL.
*
* @return string
* URL of the oEmbed endpoint.
*/
public function buildResourceUrl($url) {
$query = ['url' => $url];
return $this->getUrl() . '?' . UrlHelper::buildQuery($query);
}
}
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Utility\UrlHelper;
/**
* Value object for oEmbed providers.
*/
class Provider {
/**
* The provider name.
*
* @var string
*/
protected $name;
/**
* The provider URL.
*
* @var string
*/
protected $url;
/**
* The provider endpoints.
*
* @var \Drupal\media\OEmbed\Endpoint[]
*/
protected $endpoints = [];
/**
* Provider constructor.
*
* @param string $name
* The provider name.
* @param string $url
* The provider URL.
* @param array[] $endpoints
* List of endpoints this provider exposes.
*
* @throws \Drupal\media\OEmbed\ProviderException
*/
public function __construct($name, $url, array $endpoints) {
if (!UrlHelper::isValid($url, TRUE) || !UrlHelper::isExternal($url)) {
throw new ProviderException('Provider @name does not define a valid external URL.', $this);
}
$this->name = $name;
$this->url = $url;
try {
foreach ($endpoints as $endpoint) {
$endpoint += ['formats' => [], 'schemes' => [], 'discovery' => FALSE];
$this->endpoints[] = new Endpoint($endpoint['url'], $this, $endpoint['schemes'], $endpoint['formats'], $endpoint['discovery']);
}
}
catch (\InvalidArgumentException $e) {
// Just skip all the invalid endpoints.
// @todo Log the exception message to help with debugging in
// https://www.drupal.org/project/drupal/issues/2972846.
}
if (empty($this->endpoints)) {
throw new ProviderException('Provider @name does not define any valid endpoints.', $this);
}
}
/**
* Returns the provider name.
*
* @return string
* Name of the provider.
*/
public function getName() {
return $this->name;
}
/**
* Returns the provider URL.
*
* @return string
* URL of the provider.
*/
public function getUrl() {
return $this->url;
}
/**
* Returns the provider endpoints.
*
* @return \Drupal\media\OEmbed\Endpoint[]
* List of endpoints this provider exposes.
*/
public function getEndpoints() {
return $this->endpoints;
}
}
<?php
namespace Drupal\media\OEmbed;
/**
* Exception thrown if an oEmbed provider causes an error.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class ProviderException extends \Exception {
/**
* Information about the oEmbed provider which caused the exception.
*
* @var \Drupal\media\OEmbed\Provider
*
* @see \Drupal\media\OEmbed\ProviderRepositoryInterface::get()
*/
protected $provider;
/**
* ProviderException constructor.
*
* @param string $message
* The exception message. '@name' will be replaced with the provider name
* if available, or '<unknown>' if not.
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The provider information.
* @param \Exception $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, Provider $provider = NULL, \Exception $previous = NULL) {
$this->provider = $provider;
$message = str_replace('@name', $provider ? $provider->getName() : '<unknown>', $message);
parent::__construct($message, 0, $previous);
}
}
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use Drupal\Core\Config\ConfigFactoryInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Retrieves and caches information about oEmbed providers.
*/
class ProviderRepository implements ProviderRepositoryInterface {
use UseCacheBackendTrait;
/**
* How long the provider data should be cached, in seconds.
*
* @var int
*/
protected $maxAge;
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* URL of a JSON document which contains a database of oEmbed providers.
*
* @var string
*/
protected $providersUrl;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\TimeInterface
*/
protected $time;
/**
* Constructs a ProviderRepository instance.
*
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* (optional) The cache backend.
* @param int $max_age
* (optional) How long the cache data should be kept. Defaults to a week.
*/
public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, TimeInterface $time, CacheBackendInterface $cache_backend = NULL, $max_age = 604800) {
$this->httpClient = $http_client;
$this->providersUrl = $config_factory->get('media.settings')->get('oembed_providers');
$this->time = $time;
$this->cacheBackend = $cache_backend;
$this->maxAge = (int) $max_age;
}
/**
* {@inheritdoc}
*/
public function getAll() {
$cache_id = 'media:oembed_providers';
$cached = $this->cacheGet($cache_id);
if ($cached) {
return $cached->data;
}
try {
$response = $this->httpClient->request('GET', $this->providersUrl);
}
catch (RequestException $e) {
throw new ProviderException("Could not retrieve the oEmbed provider database from $this->providersUrl", NULL, $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.');
}
$keyed_providers = [];
foreach ($providers as $provider) {
try {
$name = (string) $provider['provider_name'];
$keyed_providers[$name] = new Provider($provider['provider_name'], $provider['provider_url'], $provider['endpoints']);
}
catch (ProviderException $e) {
// Just skip all the invalid providers.
// @todo Log the exception message to help with debugging.
}
}
$this->cacheSet($cache_id, $keyed_providers, $this->time->getCurrentTime() + $this->maxAge);
return $keyed_providers;
}
/**
* {@inheritdoc}
*/
public function get($provider_name) {
$providers = $this->getAll();
if (!isset($providers[$provider_name])) {
throw new \InvalidArgumentException("Unknown provider '$provider_name'");
}
return $providers[$provider_name];
}
}
<?php
namespace Drupal\media\OEmbed;
/**
* Defines an interface for a collection of oEmbed provider information.
*
* The provider repository is responsible for fetching information about all
* available oEmbed providers, most likely pulled from the online database at
* https://oembed.com/providers.json, and creating \Drupal\media\OEmbed\Provider
* value objects for each provider.
*/
interface ProviderRepositoryInterface {
/**
* Returns information on all available oEmbed providers.
*
* @return \Drupal\media\OEmbed\Provider[]
* Returns an array of provider value objects, keyed by provider name.
*
* @throws \Drupal\media\OEmbed\ProviderException
* If the oEmbed provider information cannot be retrieved.
*/
public function getAll();
/**
* Returns information for a specific oEmbed provider.
*
* @param string $provider_name
* The name of the provider.
*
* @return \Drupal\media\OEmbed\Provider
* A value object containing information about the provider.
*
* @throws \InvalidArgumentException
* If there is no known oEmbed provider with the specified name.
*/
public function get($provider_name);
}
<?php
namespace Drupal\media\OEmbed;
/**
* Exception thrown if an oEmbed resource causes an error.
*
* This differs from \Drupal\media\OEmbed\ResourceException in that it is only
* thrown before a \Drupal\media\OEmbed\Resource value object has been created
* for the resource.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class RawResourceException extends ResourceException {
/**
* The URL of the resource.
*
* @var string
*/
protected $url;
/**
* The resource data.
*
* @var array
*/
protected $resource = [];
/**
* RawResourceException constructor.
*
* @param string $message
* The exception message.
* @param string $url
* The URL of the resource. Can be the actual endpoint URL or the canonical
* URL.
* @param array $resource
* (optional) The raw resource data.
* @param \Exception $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, $url, array $resource = [], \Exception $previous = NULL) {
$this->url = $url;
$this->resource = $resource;
parent::__construct($message, 0, $previous);
}
/**
* Gets the URL of the resource which caused the exception.
*
* @return string
* The URL of the resource.
*/
public function getUrl() {
return $this->url;
}
/**
* Gets the raw resource data, if available.
*
* @return array
* The resource data.
*/
public function getResource() {
return $this->resource;
}
}
<?php
namespace Drupal\media\OEmbed;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableDependencyTrait;
use Drupal\Core\Url;
/**
* Value object representing an oEmbed resource.
*
* Data received from an oEmbed provider could be insecure. For example,
* resources of the 'rich' type provide an HTML representation which is not
* sanitized by this object in any way. Any values you retrieve from this object
* should be treated as potentially dangerous user input and carefully validated
* and sanitized before being displayed or otherwise manipulated by your code.
*
* Valid resource types are defined in the oEmbed specification and represented
* by the TYPE_* constants in this class.
*
* @see https://oembed.com/#section2
*
* @internal
* This class is an internal part of the oEmbed system and should only be
* instantiated by
* \Drupal\media\OEmbed\ResourceFetcherInterface::fetchResource().
*/
class Resource implements CacheableDependencyInterface {
use CacheableDependencyTrait;
/**
* The resource type for link resources.
*/
const TYPE_LINK = 'link';
/**
* The resource type for photo resources.
*/
const TYPE_PHOTO = 'photo';
/**
* The resource type for rich resources.
*/
const TYPE_RICH = 'rich';
/**
* The resource type for video resources.
*/
const TYPE_VIDEO = 'video';
/**
* The resource type. Can be one of the static::TYPE_* constants.
*
* @var string
*/
protected $type;
/**
* The resource provider.
*
* @var \Drupal\media\OEmbed\Provider
*/
protected $provider;
/**
* A text title, describing the resource.
*
* @var string
*/
protected $title;
/**
* The name of the author/owner of the resource.
*
* @var string
*/
protected $authorName;
/**
* A URL for the author/owner of the resource.
*
* @var string
*/
protected $authorUrl;
/**
* A URL to a thumbnail image representing the resource.
*
* The thumbnail must respect any maxwidth and maxheight parameters passed
* to the oEmbed endpoint. If this parameter is present, thumbnail_width and
* thumbnail_height must also be present.
*
* @var string
*
* @see \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()
* @see https://oembed.com/#section2
*/
protected $thumbnailUrl;
/**
* The width of the thumbnail, in pixels.
*
* If this parameter is present, thumbnail_url and thumbnail_height must also
* be present.
*
* @var int
*/
protected $thumbnailWidth;
/**
* The height of the thumbnail, in pixels.
*
* If this parameter is present, thumbnail_url and thumbnail_width must also
* be present.
*
* @var int
*/
protected $thumbnailHeight;
/**
* The width of the resource, in pixels.
*
* @var int
*/
protected $width;
/**
* The height of the resource, in pixels.
*
* @var int
*/
protected $height;
/**
* The resource URL. Only applies to 'photo' and 'link' resources.
*
* @var string
*/
protected $url;
/**
* The HTML representation of the resource.
*
* Only applies to 'rich' and 'video' resources.
*
* @var string
*/
protected $html;
/**
* Resource constructor.
*
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The resource provider.
* @param string $title
* (optional) A text title, describing the resource.
* @param string $author_name
* (optional) The name of the author/owner of the resource.
* @param string $author_url
* (optional) A URL for the author/owner of the resource.
* @param int $cache_age
* (optional) The suggested cache lifetime for this resource, in seconds.
* @param string $thumbnail_url
* (optional) A URL to a thumbnail image representing the resource. If this
* parameter is present, $thumbnail_width and $thumbnail_height must also be
* present.
* @param int $thumbnail_width
* (optional) The width of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_height must also be present.
* @param int $thumbnail_height
* (optional) The height of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_width must also be present.
*/
protected function __construct(Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
$this->provider = $provider;
$this->title = $title;
$this->authorName = $author_name;
$this->authorUrl = $author_url;
if (isset($cache_age) && is_numeric($cache_age)) {
// If the cache age is too big, it can overflow the 'expire' column of
// database cache backends, causing SQL exceptions. To prevent that,
// arbitrarily limit the cache age to 5 years. That should be enough.
$this->cacheMaxAge = Cache::mergeMaxAges((int) $cache_age, 157680000);
}
if ($thumbnail_url) {
$this->thumbnailUrl = $thumbnail_url;
$this->setThumbnailDimensions($thumbnail_width, $thumbnail_height);
}
}
/**
* Creates a link resource.
*
* @param string $url
* (optional) The URL of the resource.
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The resource provider.
* @param string $title
* (optional) A text title, describing the resource.
* @param string $author_name
* (optional) The name of the author/owner of the resource.
* @param string $author_url
* (optional) A URL for the author/owner of the resource.
* @param int $cache_age
* (optional) The suggested cache lifetime for this resource, in seconds.
* @param string $thumbnail_url
* (optional) A URL to a thumbnail image representing the resource. If this
* parameter is present, $thumbnail_width and $thumbnail_height must also be
* present.
* @param int $thumbnail_width
* (optional) The width of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_height must also be present.
* @param int $thumbnail_height
* (optional) The height of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_width must also be present.
*
* @return static
*/
public static function link($url = NULL, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
$resource = new static($provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
$resource->type = self::TYPE_LINK;
$resource->url = $url;
return $resource;
}
/**
* Creates a photo resource.
*
* @param string $url
* The URL of the photo.
* @param int $width
* The width of the photo, in pixels.
* @param int $height
* The height of the photo, in pixels.
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The resource provider.
* @param string $title
* (optional) A text title, describing the resource.
* @param string $author_name
* (optional) The name of the author/owner of the resource.
* @param string $author_url
* (optional) A URL for the author/owner of the resource.
* @param int $cache_age
* (optional) The suggested cache lifetime for this resource, in seconds.
* @param string $thumbnail_url
* (optional) A URL to a thumbnail image representing the resource. If this
* parameter is present, $thumbnail_width and $thumbnail_height must also be
* present.
* @param int $thumbnail_width
* (optional) The width of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_height must also be present.
* @param int $thumbnail_height
* (optional) The height of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_width must also be present.
*
* @return static
*/
public static function photo($url, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
if (empty($url)) {
throw new \InvalidArgumentException('Photo resources must provide a URL.');
}
$resource = static::link($url, $provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
$resource->type = self::TYPE_PHOTO;
$resource->setDimensions($width, $height);
return $resource;
}
/**
* Creates a rich resource.
*
* @param string $html
* The HTML representation of the resource.
* @param int $width
* The width of the resource, in pixels.
* @param int $height
* The height of the resource, in pixels.
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The resource provider.
* @param string $title
* (optional) A text title, describing the resource.
* @param string $author_name
* (optional) The name of the author/owner of the resource.
* @param string $author_url
* (optional) A URL for the author/owner of the resource.
* @param int $cache_age
* (optional) The suggested cache lifetime for this resource, in seconds.
* @param string $thumbnail_url
* (optional) A URL to a thumbnail image representing the resource. If this
* parameter is present, $thumbnail_width and $thumbnail_height must also be
* present.
* @param int $thumbnail_width
* (optional) The width of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_height must also be present.
* @param int $thumbnail_height
* (optional) The height of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_width must also be present.
*
* @return static
*/
public static function rich($html, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
if (empty($html)) {
throw new \InvalidArgumentException('The resource must provide an HTML representation.');
}
$resource = new static($provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
$resource->type = self::TYPE_RICH;
$resource->html = $html;
$resource->setDimensions($width, $height);
return $resource;
}
/**
* Creates a video resource.
*
* @param string $html
* The HTML required to display the video.
* @param int $width
* The width of the video, in pixels.
* @param int $height
* The height of the video, in pixels.
* @param \Drupal\media\OEmbed\Provider $provider
* (optional) The resource provider.
* @param string $title
* (optional) A text title, describing the resource.
* @param string $author_name
* (optional) The name of the author/owner of the resource.
* @param string $author_url
* (optional) A URL for the author/owner of the resource.
* @param int $cache_age
* (optional) The suggested cache lifetime for this resource, in seconds.
* @param string $thumbnail_url
* (optional) A URL to a thumbnail image representing the resource. If this
* parameter is present, $thumbnail_width and $thumbnail_height must also be
* present.
* @param int $thumbnail_width
* (optional) The width of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_height must also be present.
* @param int $thumbnail_height
* (optional) The height of the thumbnail, in pixels. If this parameter is
* present, $thumbnail_url and $thumbnail_width must also be present.
*
* @return static
*/
public static function video($html, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
$resource = static::rich($html, $width, $height, $provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
$resource->type = self::TYPE_VIDEO;
return $resource;
}
/**
* Returns the resource type.
*
* @return string
* The resource type. Will be one of the self::TYPE_* constants.
*/
public function getType() {
return $this->type;
}
/**
* Returns the title of the resource.
*
* @return string|null
* The title of the resource, if known.
*/
public function getTitle() {
return $this->title;
}
/**
* Returns the name of the resource author.
*
* @return string|null
* The name of the resource author, if known.
*/
public function getAuthorName() {
return $this->authorName;
}
/**
* Returns the URL of the resource author.
*
* @return \Drupal\Core\Url|null
* The absolute URL of the resource author, or NULL if none is provided.
*/
public function getAuthorUrl() {
return $this->authorUrl ? Url::fromUri($this->authorUrl)->setAbsolute() : NULL;
}
/**
* Returns the resource provider, if known.
*
* @return \Drupal\media\OEmbed\Provider|null
* The resource provider, or NULL if the provider is not known.
*/
public function getProvider() {
return $this->provider;
}
/**
* Returns the URL of the resource's thumbnail image.
*
* @return \Drupal\Core\Url|null
* The absolute URL of the thumbnail image, or NULL if there isn't one.
*/
public function getThumbnailUrl() {
return $this->thumbnailUrl ? Url::fromUri($this->thumbnailUrl)->setAbsolute() : NULL;
}
/**
* Returns the width of the resource's thumbnail image.
*
* @return int|null
* The thumbnail width in pixels, or NULL if there is no thumbnail.
*/
public function getThumbnailWidth() {
return $this->thumbnailWidth;
}
/**
* Returns the height of the resource's thumbnail image.
*
* @return int|null
* The thumbnail height in pixels, or NULL if there is no thumbnail.
*/
public function getThumbnailHeight() {
return $this->thumbnailHeight;
}
/**
* Returns the width of the resource.
*
* @return int|null
* The width of the resource in pixels, or NULL if the resource has no
* dimensions
*/
public function getWidth() {
return $this->width;
}
/**
* Returns the height of the resource.
*
* @return int|null
* The height of the resource in pixels, or NULL if the resource has no
* dimensions.
*/
public function getHeight() {
return $this->height;
}
/**
* Returns the URL of the resource. Only applies to 'photo' resources.
*
* @return \Drupal\Core\Url|null
* The resource URL, if it has one.
*/
public function getUrl() {
if ($this->url) {
return Url::fromUri($this->url)->setAbsolute();
}
return NULL;
}
/**
* Returns the HTML representation of the resource.
*
* Only applies to 'rich' and 'video' resources.
*
* @return string|null
* The HTML representation of the resource, if it has one.
*/
public function getHtml() {
return isset($this->html) ? (string) $this->html : NULL;
}
/**
* Sets the thumbnail dimensions.
*
* @param int $width
* The width of the resource.
* @param int $height
* The height of the resource.
*
* @throws \InvalidArgumentException
* If either $width or $height are not numbers greater than zero.
*/
protected function setThumbnailDimensions($width, $height) {
$width = (int) $width;
$height = (int) $height;
if ($width > 0 && $height > 0) {
$this->thumbnailWidth = $width;
$this->thumbnailHeight = $height;
}
else {
throw new \InvalidArgumentException('The thumbnail dimensions must be numbers greater than zero.');
}
}
/**
* Sets the dimensions.
*
* @param int $width
* The width of the resource.
* @param int $height
* The height of the resource.
*
* @throws \InvalidArgumentException
* If either $width or $height are not numbers greater than zero.
*/
protected function setDimensions($width, $height) {
$width = (int) $width;
$height = (int) $height;
if ($width > 0 && $height > 0) {
$this->width = $width;
$this->height = $height;
}
else {
throw new \InvalidArgumentException('The dimensions must be numbers greater than zero.');
}
}
}
<?php
namespace Drupal\media\OEmbed;
/**
* Exception thrown if an oEmbed resource causes an error.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class ResourceException extends \Exception {
/**
* The resource which caused the exception.
*
* @var \Drupal\media\OEmbed\Resource
*/
protected $resource;
/**
* ResourceException constructor.
*
* @param string $message
* The exception message.
* @param \Drupal\media\OEmbed\Resource $resource
* (optional) The value object for the resource.
* @param \Exception $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, Resource $resource = NULL, \Exception $previous = NULL) {
$this->resource = $resource;
parent::__construct($message, 0, $previous);
}
/**
* Gets the resource which caused the exception, if available.
*
* @return \Drupal\media\OEmbed\Resource|null
* The oEmbed resource.
*/
public function getResource() {
return $this->resource;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment