Unverified Commit cc59fcb0 authored by alexpott's avatar alexpott

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
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;