updated core to 8.6.1 via composer

This commit is contained in:
2018-09-12 13:58:26 +02:00
parent a9a219f2ed
commit ea56b9fba3
4443 changed files with 112098 additions and 40708 deletions
@@ -1 +1,3 @@
icon_base_uri: 'public://media-icons/generic'
iframe_domain: ''
oembed_providers_url: 'https://oembed.com/providers.json'
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- media
id: media_delete_action
label: 'Delete media'
type: media
plugin: entity:delete_action:media
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- media
id: media_publish_action
label: 'Publish media'
type: media
plugin: entity:publish_action:media
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- media
id: media_save_action
label: 'Save media'
type: media
plugin: entity:save_action:media
configuration: { }
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- media
id: media_unpublish_action
label: 'Unpublish media'
type: media
plugin: entity:unpublish_action:media
configuration: { }
@@ -132,6 +132,59 @@ display:
row:
type: fields
fields:
media_bulk_form:
id: media_bulk_form
table: media
field: media_bulk_form
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: Action
include_exclude: exclude
selected_actions: { }
entity_type: media
plugin_id: bulk_form
thumbnail__target_id:
id: thumbnail__target_id
table: media_field_data
@@ -758,7 +811,7 @@ display:
admin_label: ''
empty: true
tokenize: false
content: 'No content available.'
content: 'No media available.'
plugin_id: text_custom
relationships: { }
arguments: { }
@@ -5,6 +5,12 @@ media.settings:
icon_base_uri:
type: string
label: 'Full URI to a folder where the media icons will be installed'
iframe_domain:
type: uri
label: 'Domain from which to serve oEmbed content in an iframe'
oembed_providers_url:
type: uri
label: 'The URL of the oEmbed providers database in JSON format'
media.type.*:
type: config_entity
@@ -40,6 +46,21 @@ 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'
@@ -60,6 +81,20 @@ 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_directory:
type: uri
label: 'URI of thumbnail storage directory'
providers:
type: sequence
label: 'Allowed oEmbed providers'
sequence:
type: string
label: 'Provider name'
media.source.field_aware:
type: mapping
mapping:
+9 -6
View File
@@ -3,7 +3,7 @@
* Defines Javascript behaviors for the media form.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Behaviors for summaries for tabs in the media edit form.
*
@@ -16,21 +16,24 @@
attach(context) {
const $context = $(context);
$context.find('.media-form-author').drupalSetSummary((context) => {
$context.find('.media-form-author').drupalSetSummary(context => {
const $authorContext = $(context);
const name = $authorContext.find('.field--name-uid input').val();
const date = $authorContext.find('.field--name-created input').val();
if (name && date) {
return Drupal.t('By @name on @date', { '@name': name, '@date': date });
return Drupal.t('By @name on @date', {
'@name': name,
'@date': date,
});
}
else if (name) {
if (name) {
return Drupal.t('By @name', { '@name': name });
}
else if (date) {
if (date) {
return Drupal.t('Authored on @date', { '@date': date });
}
});
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+8 -3
View File
@@ -16,10 +16,15 @@
var date = $authorContext.find('.field--name-created input').val();
if (name && date) {
return Drupal.t('By @name on @date', { '@name': name, '@date': date });
} else if (name) {
return Drupal.t('By @name on @date', {
'@name': name,
'@date': date
});
}
if (name) {
return Drupal.t('By @name', { '@name': name });
} else if (date) {
}
if (date) {
return Drupal.t('Authored on @date', { '@date': date });
}
});
+39 -15
View File
@@ -3,7 +3,7 @@
* Defines JavaScript behaviors for the media type form.
*/
(function ($, Drupal) {
(function($, Drupal) {
/**
* Behaviors for setting summaries on media type form.
*
@@ -16,27 +16,51 @@
attach(context) {
const $context = $(context);
// Provide the vertical tab summaries.
$context.find('#edit-workflow').drupalSetSummary((context) => {
$context.find('#edit-workflow').drupalSetSummary(context => {
const vals = [];
$(context).find('input[name^="options"]:checked').parent().each(function () {
vals.push(Drupal.checkPlain($(this).find('label').text()));
});
if (!$(context).find('#edit-options-status').is(':checked')) {
$(context)
.find('input[name^="options"]:checked')
.parent()
.each(function() {
vals.push(
Drupal.checkPlain(
$(this)
.find('label')
.text(),
),
);
});
if (
!$(context)
.find('#edit-options-status')
.is(':checked')
) {
vals.unshift(Drupal.t('Not published'));
}
return vals.join(', ');
});
$(context).find('#edit-language').drupalSetSummary((context) => {
const vals = [];
$(context)
.find('#edit-language')
.drupalSetSummary(context => {
const vals = [];
vals.push($(context).find('.js-form-item-language-configuration-langcode select option:selected').text());
vals.push(
$(context)
.find(
'.js-form-item-language-configuration-langcode select option:selected',
)
.text(),
);
$(context).find('input:checked').next('label').each(function () {
vals.push(Drupal.checkPlain($(this).text()));
$(context)
.find('input:checked')
.next('label')
.each(function() {
vals.push(Drupal.checkPlain($(this).text()));
});
return vals.join(', ');
});
return vals.join(', ');
});
},
};
}(jQuery, Drupal));
})(jQuery, Drupal);
+17
View File
@@ -20,6 +20,23 @@ 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".
*/
+5 -4
View File
@@ -1,10 +1,11 @@
name: Media
description: 'Create reusable media.'
description: 'Manages the creation, configuration of display of media items.'
type: module
package: Core
version: VERSION
core: 8.x
dependencies:
- file
- image
- user
- drupal:file
- drupal:image
- drupal:user
configure: media.settings
+43
View File
@@ -5,6 +5,9 @@
* 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;
@@ -75,6 +78,36 @@ 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.iframe_url_helper')->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;
}
@@ -120,3 +153,13 @@ function media_update_8500() {
$role->save();
}
}
/**
* Updates media.settings to support OEmbed.
*/
function media_update_8600() {
\Drupal::configFactory()->getEditable('media.settings')
->set('iframe_domain', '')
->set('oembed_providers_url', 'https://oembed.com/providers.json')
->save(TRUE);
}
+5
View File
@@ -4,3 +4,8 @@ entity.media_type.collection:
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
+69 -25
View File
@@ -5,6 +5,7 @@
* Provides media items.
*/
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
@@ -15,6 +16,7 @@ use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\field\FieldConfigInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
/**
* Implements hook_help().
@@ -72,6 +74,11 @@ function media_theme() {
'render element' => 'element',
'base hook' => 'field_multiple_value_form',
],
'media_oembed_iframe' => [
'variables' => [
'media' => NULL,
],
],
];
}
@@ -92,6 +99,7 @@ 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'], '.', '_');
@@ -99,6 +107,31 @@ function media_theme_suggestions_media(array $variables) {
$suggestions[] = 'media__' . $media->bundle();
$suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode;
// Add suggestions based on the source plugin ID.
$source = $media->getSource();
if ($source instanceof DerivativeInspectionInterface) {
$source_id = $source->getBaseId();
$derivative_id = $source->getDerivativeId();
if ($derivative_id) {
$source_id .= '__derivative_' . $derivative_id;
}
}
else {
$source_id = $source->getPluginId();
}
$suggestions[] = "media__source_$source_id";
// If the source plugin uses oEmbed, add a suggestion based on the provider
// name, if available.
if ($source instanceof OEmbedInterface) {
$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[] = end($suggestions) . "__provider_$provider_id";
}
}
return $suggestions;
}
@@ -210,31 +243,9 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt
// Retrieve the media bundle list and add information for the user based on
// which bundles are available to be created or referenced.
$settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
$allowed_bundles = isset($settings['target_bundles']) ? $settings['target_bundles'] : [];
$access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
$all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
$bundle_labels = [];
$create_bundles = [];
foreach ($allowed_bundles as $bundle) {
$bundle_labels[] = $all_bundles[$bundle]['label'];
if ($access_handler->createAccess($bundle)) {
$create_bundles[] = $bundle;
if (count($create_bundles) > 1) {
// If the user has access to create more than 1 bundle then the
// individual media type form can not be used.
break;
}
}
}
// Add a section about how to create media if the user has access to do so.
if (!empty($create_bundles)) {
if (count($create_bundles) === 1) {
$add_url = Url::fromRoute('entity.media.add_form', ['media_type' => $create_bundles[0]])->toString();
}
elseif (count($create_bundles) > 1) {
$add_url = Url::fromRoute('entity.media.add_page')->toString();
}
$allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
$add_url = _media_get_add_url($allowed_bundles);
if ($add_url) {
$elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
}
@@ -275,6 +286,10 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt
if ($overview_url->access()) {
$elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
}
$all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
$bundle_labels = array_map(function ($bundle) use ($all_bundles) {
return $all_bundles[$bundle]['label'];
}, $allowed_bundles);
$elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
}
}
@@ -303,3 +318,32 @@ function media_preprocess_media_reference_help(&$variables) {
}
}
}
/**
* Returns the appropriate URL to add media for the current user.
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2938116
*
* @param string[] $allowed_bundles
* An array of bundles that should be checked for create access.
*
* @return bool|\Drupal\Core\Url
* The URL to add media, or FALSE if the user cannot create any media.
*
* @internal
* This function is internal and may be removed in a minor release.
*/
function _media_get_add_url($allowed_bundles) {
$access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
$create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']);
// Add a section about how to create media if the user has access to do so.
if (count($create_bundles) === 1) {
return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString();
}
elseif (count($create_bundles) > 1) {
return Url::fromRoute('entity.media.add_page')->toString();
}
return FALSE;
}
+7
View File
@@ -11,3 +11,10 @@
function media_post_update_collection_route() {
// Empty post-update hook.
}
/**
* Clear caches due to the addition of a Media-specific entity storage handler.
*/
function media_post_update_storage_handler() {
// Empty post-update hook.
}
+20
View File
@@ -1,3 +1,8 @@
# @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0.
# This route is not used in Drupal core. As an internal API, it may also be
# removed in a minor release. If you are using it, copy the class
# and the related "entity.media.multiple_delete_confirm" route to your
# module.
entity.media.multiple_delete_confirm:
path: '/admin/content/media/delete'
defaults:
@@ -19,3 +24,18 @@ 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: 'view media'
media.settings:
path: '/admin/config/media/media-settings'
defaults:
_form: '\Drupal\media\Form\MediaSettingsForm'
_title: 'Media settings'
requirements:
_permission: 'administer media'
+12 -1
View File
@@ -2,9 +2,20 @@ 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', '@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']
media.oembed.iframe_url_helper:
class: Drupal\media\IFrameUrlHelper
arguments: ['@router.request_context', '@private_key']
@@ -0,0 +1,182 @@
<?php
namespace Drupal\media\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\media\IFrameMarkup;
use Drupal\media\IFrameUrlHelper;
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\AccessDeniedHttpException;
/**
* 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;
/**
* The iFrame URL helper service.
*
* @var \Drupal\media\IFrameUrlHelper
*/
protected $iFrameUrlHelper;
/**
* 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.
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
* The iFrame URL helper service.
*/
public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerChannelInterface $logger, IFrameUrlHelper $iframe_url_helper) {
$this->resourceFetcher = $resource_fetcher;
$this->urlResolver = $url_resolver;
$this->renderer = $renderer;
$this->logger = $logger;
$this->iFrameUrlHelper = $iframe_url_helper;
}
/**
* {@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'),
$container->get('media.oembed.iframe_url_helper')
);
}
/**
* 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\AccessDeniedHttpException
* Will be thrown if the 'hash' parameter does not match the expected hash
* of the 'url' parameter.
*/
public function render(Request $request) {
$url = $request->query->get('url');
$max_width = $request->query->getInt('max_width', NULL);
$max_height = $request->query->getInt('max_height', NULL);
// Hash the URL and max dimensions, and ensure it is equal to the hash
// parameter passed in the query string.
$hash = $this->iFrameUrlHelper->getHash($url, $max_width, $max_height);
if (!Crypt::hashEquals($hash, $request->query->get('hash', ''))) {
throw new AccessDeniedHttpException('This resource is not available');
}
// 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, $max_width, $max_height);
$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.
$element = [
'#theme' => 'media_oembed_iframe',
// Even though the resource HTML is untrusted, IFrameMarkup::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.
'#media' => IFrameMarkup::create($resource->getHtml()),
'#cache' => [
// Add the 'rendered' cache tag as this response is not processed by
// \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse().
'tags' => ['rendered'],
],
];
$content = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($resource, $element) {
return $this->renderer->render($element);
});
$response
->setContent($content)
->addCacheableDependency($resource)
->addCacheableDependency(CacheableMetadata::createFromRenderArray($element));
}
catch (ResourceException $e) {
// Prevent the response from being cached.
$response->setMaxAge(0);
// The oEmbed system makes heavy use of exception wrapping, so log the
// entire exception chain to help with troubleshooting.
do {
// @todo Log additional information from ResourceException, to help with
// debugging, in https://www.drupal.org/project/drupal/issues/2972846.
$this->logger->error($e->getMessage());
$e = $e->getPrevious();
} while ($e);
}
return $response;
}
}
+112 -51
View File
@@ -29,7 +29,7 @@ use Drupal\user\UserInterface;
* ),
* bundle_label = @Translation("Media type"),
* handlers = {
* "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "storage" = "Drupal\media\MediaStorage",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\media\MediaListBuilder",
* "access" = "Drupal\media\MediaAccessControlHandler",
@@ -38,6 +38,7 @@ use Drupal\user\UserInterface;
* "add" = "Drupal\media\MediaForm",
* "edit" = "Drupal\media\MediaForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm",
* },
* "translation" = "Drupal\content_translation\ContentTranslationHandler",
* "views_data" = "Drupal\media\MediaViewsData",
@@ -76,6 +77,7 @@ use Drupal\user\UserInterface;
* "canonical" = "/media/{media}",
* "collection" = "/admin/content/media",
* "delete-form" = "/media/{media}/delete",
* "delete-multiple-form" = "/media/delete",
* "edit-form" = "/media/{media}/edit",
* "revision" = "/media/{media}/revisions/{media_revision}/view",
* }
@@ -179,25 +181,7 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
* https://www.drupal.org/node/2878119
*/
protected function updateThumbnail($from_queue = FALSE) {
$file_storage = \Drupal::service('entity_type.manager')->getStorage('file');
$thumbnail_uri = $this->getThumbnailUri($from_queue);
$existing = $file_storage->getQuery()
->condition('uri', $thumbnail_uri)
->execute();
if ($existing) {
$this->thumbnail->target_id = reset($existing);
}
else {
/** @var \Drupal\file\FileInterface $file */
$file = $file_storage->create(['uri' => $thumbnail_uri]);
if ($owner = $this->getOwner()) {
$file->setOwner($owner);
}
$file->setPermanent();
$file->save();
$this->thumbnail->target_id = $file->id();
}
$this->thumbnail->target_id = $this->loadThumbnail($this->getThumbnailUri($from_queue))->id();
// Set the thumbnail alt.
$media_source = $this->getSource();
@@ -220,6 +204,52 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
return $this;
}
/**
* Loads the file entity for the thumbnail.
*
* If the file entity does not exist, it will be created.
*
* @param string $thumbnail_uri
* (optional) The URI of the thumbnail, used to load or create the file
* entity. If omitted, the default thumbnail URI will be used.
*
* @return \Drupal\file\FileInterface
* The thumbnail file entity.
*/
protected function loadThumbnail($thumbnail_uri = NULL) {
$values = [
'uri' => $thumbnail_uri ?: $this->getDefaultThumbnailUri(),
];
$file_storage = $this->entityTypeManager()->getStorage('file');
$existing = $file_storage->loadByProperties($values);
if ($existing) {
$file = reset($existing);
}
else {
/** @var \Drupal\file\FileInterface $file */
$file = $file_storage->create($values);
if ($owner = $this->getOwner()) {
$file->setOwner($owner);
}
$file->setPermanent();
$file->save();
}
return $file;
}
/**
* Returns the URI of the default thumbnail.
*
* @return string
* The default thumbnail URI.
*/
protected function getDefaultThumbnailUri() {
$default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename'];
return \Drupal::config('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename;
}
/**
* Updates the queued thumbnail for the media item.
*
@@ -255,17 +285,14 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
protected function getThumbnailUri($from_queue) {
$thumbnails_queued = $this->bundle->entity->thumbnailDownloadsAreQueued();
if ($thumbnails_queued && $this->isNew()) {
$default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename'];
$thumbnail_uri = \Drupal::service('config.factory')->get('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename;
return $this->getDefaultThumbnailUri();
}
elseif ($thumbnails_queued && !$from_queue) {
$thumbnail_uri = $this->get('thumbnail')->entity->getFileUri();
}
else {
$thumbnail_uri = $this->getSource()->getMetadata($this, $this->getSource()->getPluginDefinition()['thumbnail_uri_metadata_attribute']);
return $this->get('thumbnail')->entity->getFileUri();
}
return $thumbnail_uri;
$source = $this->getSource();
return $source->getMetadata($this, $source->getPluginDefinition()['thumbnail_uri_metadata_attribute']);
}
/**
@@ -303,30 +330,9 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
$media_source = $this->getSource();
foreach ($this->translations as $langcode => $data) {
if ($this->hasTranslation($langcode)) {
$translation = $this->getTranslation($langcode);
// Try to set fields provided by the media source and mapped in
// media type config.
foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) {
// Only save value in entity field if empty. Do not overwrite existing
// data.
if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged())) {
$translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
}
}
// Try to set a default name for this media item if no name is provided.
if ($translation->get('name')->isEmpty()) {
$translation->setName($translation->getName());
}
// Set thumbnail.
if ($translation->shouldUpdateThumbnail()) {
$translation->updateThumbnail();
}
}
// If no thumbnail has been explicitly set, use the default thumbnail.
if ($this->get('thumbnail')->isEmpty()) {
$this->thumbnail->target_id = $this->loadThumbnail()->id();
}
}
@@ -367,6 +373,61 @@ class Media extends EditorialContentEntityBase implements MediaInterface {
}
}
/**
* Sets the media entity's field values from the source's metadata.
*
* Fetching the metadata could be slow (e.g., if requesting it from a remote
* API), so this is called by \Drupal\media\MediaStorage::save() prior to it
* beginning the database transaction, whereas static::preSave() executes
* after the transaction has already started.
*
* @internal
* Expose this as an API in
* https://www.drupal.org/project/drupal/issues/2992426.
*/
public function prepareSave() {
// @todo If the source plugin talks to a remote API (e.g. oEmbed), this code
// might be performing a fair number of HTTP requests. This is dangerously
// brittle and should probably be handled by a queue, to avoid doing HTTP
// operations during entity save. See
// https://www.drupal.org/project/drupal/issues/2976875 for more.
// In order for metadata to be mapped correctly, $this->original must be
// set. However, that is only set once parent::save() is called, so work
// around that by setting it here.
if (!isset($this->original) && $id = $this->id()) {
$this->original = $this->entityTypeManager()
->getStorage('media')
->loadUnchanged($id);
}
$media_source = $this->getSource();
foreach ($this->translations as $langcode => $data) {
if ($this->hasTranslation($langcode)) {
$translation = $this->getTranslation($langcode);
// Try to set fields provided by the media source and mapped in
// media type config.
foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) {
// Only save value in entity field if empty. Do not overwrite existing
// data.
if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged())) {
$translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
}
}
// Try to set a default name for this media item if no name is provided.
if ($translation->get('name')->isEmpty()) {
$translation->setName($translation->getName());
}
// Set thumbnail.
if ($translation->shouldUpdateThumbnail($this->isNew())) {
$translation->updateThumbnail();
}
}
}
}
/**
* {@inheritdoc}
*/
+13 -1
View File
@@ -100,6 +100,8 @@ class MediaType extends ConfigEntityBundleBase implements MediaTypeInterface, En
* Whether thumbnail downloads are queued.
*
* @var bool
*
* @see \Drupal\media\MediaTypeInterface::thumbnailDownloadsAreQueued()
*/
protected $queue_thumbnail_downloads = FALSE;
@@ -113,7 +115,15 @@ class MediaType extends ConfigEntityBundleBase implements MediaTypeInterface, En
/**
* The media source configuration.
*
* A media source can provide a configuration form with source plugin-specific
* configuration settings, which must at least include a source_field element
* containing a the name of the source field for the media type. The source
* configuration is defined by, and used to load, the source plugin. See
* \Drupal\media\MediaTypeInterface for an explanation of media sources.
*
* @var array
*
* @see \Drupal\media\MediaTypeInterface::getSource()
*/
protected $source_configuration = [];
@@ -125,9 +135,11 @@ class MediaType extends ConfigEntityBundleBase implements MediaTypeInterface, En
protected $sourcePluginCollection;
/**
* Field map. Fields provided by type plugin to be stored as entity fields.
* The metadata field map.
*
* @var array
*
* @see \Drupal\media\MediaTypeInterface::getFieldMap()
*/
protected $field_map = [];
@@ -13,6 +13,12 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Provides a confirmation form to delete multiple media items at once.
*
* @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0.
* This route is not used in Drupal core. As an internal API, it may also be
* removed in a minor release. If you are using it, copy the class
* and the related "entity.media.multiple_delete_confirm" route to your
* module.
*
* @internal
*/
class MediaDeleteMultipleConfirmForm extends ConfirmFormBase {
@@ -47,6 +53,7 @@ class MediaDeleteMultipleConfirmForm extends ConfirmFormBase {
* The entity type manager.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $manager) {
@trigger_error(__CLASS__ . ' is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. It is not used in Drupal core. As an internal API, it may also be removed in a minor release. If you are using it, copy the class and the related "entity.media.multiple_delete_confirm" route to your module.', E_USER_DEPRECATED);
$this->tempStoreFactory = $temp_store_factory;
$this->storage = $manager->getStorage('media');
}
@@ -195,7 +202,7 @@ class MediaDeleteMultipleConfirmForm extends ConfirmFormBase {
}
if ($total_count) {
drupal_set_message($this->formatPlural($total_count, 'Deleted 1 media item.', 'Deleted @count media items.'));
$this->messenger()->addStatus($this->formatPlural($total_count, 'Deleted 1 media item.', 'Deleted @count media items.'));
}
$this->tempStoreFactory->get('media_multiple_delete_confirm')->delete(\Drupal::currentUser()->id());
@@ -0,0 +1,108 @@
<?php
namespace Drupal\media\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\media\IFrameUrlHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form to configure Media settings.
*
* @internal
*/
class MediaSettingsForm extends ConfigFormBase {
/**
* The iFrame URL helper service.
*
* @var \Drupal\media\IFrameUrlHelper
*/
protected $iFrameUrlHelper;
/**
* MediaSettingsForm constructor.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
* The iFrame URL helper service.
*/
public function __construct(ConfigFactoryInterface $config_factory, IFrameUrlHelper $iframe_url_helper) {
parent::__construct($config_factory);
$this->iFrameUrlHelper = $iframe_url_helper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('media.oembed.iframe_url_helper')
);
}
/**
* {@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->iFrameUrlHelper->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);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace Drupal\media;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
/**
* Defines an object that wraps oEmbed markup for use in an iFrame.
*
* This object is not constructed with a known safe string as the strings come
* from an external site. It must not be used outside the Media module's oEmbed
* iframe rendering.
*
* @internal
* This object is an internal part of the oEmbed system and should only be
* used in \Drupal\media\Controller\OEmbedIframeController.
*
* @see \Drupal\media\Controller\OEmbedIframeController
*/
class IFrameMarkup implements MarkupInterface {
use MarkupTrait;
}
@@ -0,0 +1,84 @@
<?php
namespace Drupal\media;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\PrivateKey;
use Drupal\Core\Routing\RequestContext;
use Drupal\Core\Site\Settings;
/**
* Providers helper functions for displaying oEmbed resources in 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 IFrameUrlHelper {
/**
* The request context service.
*
* @var \Drupal\Core\Routing\RequestContext
*/
protected $requestContext;
/**
* The private key service.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* IFrameUrlHelper constructor.
*
* @param \Drupal\Core\Routing\RequestContext $request_context
* The request context service.
* @param \Drupal\Core\PrivateKey $private_key
* The private key service.
*/
public function __construct(RequestContext $request_context, PrivateKey $private_key) {
$this->requestContext = $request_context;
$this->privateKey = $private_key;
}
/**
* Hashes an oEmbed resource URL.
*
* @param string $url
* The resource URL.
* @param int $max_width
* (optional) The maximum width of the resource.
* @param int $max_height
* (optional) The maximum height of the resource.
*
* @return string
* The hashed URL.
*/
public function getHash($url, $max_width = NULL, $max_height = NULL) {
return Crypt::hmacBase64("$url:$max_width:$max_height", $this->privateKey->get() . Settings::getHashSalt());
}
/**
* Checks if an oEmbed URL can be securely displayed in an frame.
*
* @param string $url
* The URL to check.
*
* @return bool
* TRUE if the URL is considered secure, otherwise FALSE.
*/
public function isSecure($url) {
if (!$url) {
return FALSE;
}
$url_host = parse_url($url, PHP_URL_HOST);
$system_host = parse_url($this->requestContext->getCompleteBaseUrl(), PHP_URL_HOST);
// The URL is secure if its domain is not the same as the domain of the base
// URL of the current request.
return $url_host && $system_host && $url_host !== $system_host;
}
}
@@ -47,7 +47,7 @@ class MediaAccessControlHandler extends EntityAccessControlHandler {
if ($account->hasPermission('update media') && $is_owner) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
}
return AccessResult::neutral()->cachePerPermissions();
return AccessResult::neutral("The following permissions are required: 'update any media' OR 'update own media' OR '$type: edit any media' OR '$type: edit own media'.")->cachePerPermissions();
case 'delete':
if ($account->hasPermission('delete any ' . $type . ' media')) {
@@ -64,7 +64,7 @@ class MediaAccessControlHandler extends EntityAccessControlHandler {
if ($account->hasPermission('delete media') && $is_owner) {
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
}
return AccessResult::neutral()->cachePerPermissions();
return AccessResult::neutral("The following permissions are required: 'delete any media' OR 'delete own media' OR '$type: delete any media' OR '$type: delete own media'.")->cachePerPermissions();
default:
return AccessResult::neutral()->cachePerPermissions();
+14 -5
View File
@@ -59,20 +59,29 @@ class MediaForm extends ContentEntityForm {
*/
public function save(array $form, FormStateInterface $form_state) {
$saved = parent::save($form, $form_state);
$context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label()];
$context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label(), 'link' => $this->entity->toLink($this->t('View'))->toString()];
$logger = $this->logger('media');
$t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->label()];
$t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->toLink($this->entity->label())->toString()];
if ($saved === SAVED_NEW) {
$logger->notice('@type: added %label.', $context);
drupal_set_message($this->t('@type %label has been created.', $t_args));
$this->messenger()->addStatus($this->t('@type %label has been created.', $t_args));
}
else {
$logger->notice('@type: updated %label.', $context);
drupal_set_message($this->t('@type %label has been updated.', $t_args));
$this->messenger()->addStatus($this->t('@type %label has been updated.', $t_args));
}
// Redirect the user to the media overview if the user has the 'access media
// overview' permission. If not, redirect to the canonical URL of the media
// item.
if ($this->currentUser()->hasPermission('access media overview')) {
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
else {
$form_state->setRedirectUrl($this->entity->toUrl());
}
$form_state->setRedirectUrl($this->entity->toUrl('canonical'));
return $saved;
}
+2 -2
View File
@@ -115,11 +115,11 @@ class MediaListBuilder extends EntityListBuilder {
/** @var \Drupal\media\MediaInterface $entity */
if ($this->thumbnailStyleExists) {
$row['thumbnail'] = [];
if ($thumbnail_url = $entity->getSource()->getMetadata($entity, 'thumbnail_uri')) {
if ($thumbnail_uri = $entity->getSource()->getMetadata($entity, 'thumbnail_uri')) {
$row['thumbnail']['data'] = [
'#theme' => 'image_style',
'#style_name' => 'thumbnail',
'#uri' => $thumbnail_url,
'#uri' => $thumbnail_uri,
'#height' => 50,
];
}
+3 -1
View File
@@ -301,7 +301,9 @@ abstract class MediaSourceBase extends PluginBase implements MediaSourceInterfac
* returned. Otherwise, a new, unused one is generated.
*/
protected function getSourceFieldName() {
$base_id = 'field_media_' . $this->getPluginId();
// 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());
$tries = 0;
$storage = $this->entityTypeManager->getStorage('field_storage_config');
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Drupal\media;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Defines the storage handler class for media.
*
* The default storage is overridden to handle metadata fetching outside of the
* database transaction.
*/
class MediaStorage extends SqlContentEntityStorage {
/**
* {@inheritdoc}
*/
public function save(EntityInterface $media) {
// For backwards compatibility, modules that override the Media entity
// class, are not required to implement the prepareSave() method.
// @todo For Drupal 8.7, consider throwing a deprecation notice if the
// method doesn't exist. See
// https://www.drupal.org/project/drupal/issues/2992426 for further
// discussion.
if (method_exists($media, 'prepareSave')) {
$media->prepareSave();
}
return parent::save($media);
}
}
+8 -30
View File
@@ -2,6 +2,7 @@
namespace Drupal\media;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityFieldManagerInterface;
@@ -23,7 +24,7 @@ class MediaTypeForm extends EntityForm {
/**
* Media source plugin manager.
*
* @var \Drupal\media\MediaSourceManager
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $sourceManager;
@@ -37,12 +38,12 @@ class MediaTypeForm extends EntityForm {
/**
* Constructs a new class instance.
*
* @param \Drupal\media\MediaSourceManager $source_manager
* @param \Drupal\Component\Plugin\PluginManagerInterface $source_manager
* Media source plugin manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* Entity field manager service.
*/
public function __construct(MediaSourceManager $source_manager, EntityFieldManagerInterface $entity_field_manager) {
public function __construct(PluginManagerInterface $source_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->sourceManager = $source_manager;
$this->entityFieldManager = $entity_field_manager;
}
@@ -118,7 +119,7 @@ class MediaTypeForm extends EntityForm {
'#attributes' => ['id' => 'source-dependent'],
];
if ($source) {
if (!$this->entity->isNew()) {
$source_description = $this->t('<em>The media source cannot be changed after the media type is created.</em>');
}
else {
@@ -131,23 +132,12 @@ class MediaTypeForm extends EntityForm {
'#options' => $options,
'#description' => $source_description,
'#ajax' => ['callback' => '::ajaxHandlerData'],
// Rebuilding the form as part of the AJAX request is a workaround to
// enforce machine_name validation.
// @todo This was added as part of #2932226 and it should be removed once
// https://www.drupal.org/project/drupal/issues/2557299 solves it in a
// more generic way.
'#executes_submit_callback' => TRUE,
'#submit' => [[static::class, 'rebuildSubmit']],
'#required' => TRUE,
// Once the media type is created, its source plugin cannot be changed
// anymore.
'#disabled' => !empty($source),
'#disabled' => !$this->entity->isNew(),
];
if (!$source) {
$form['type']['#empty_option'] = $this->t('- Select media source -');
}
if ($source) {
// Media source plugin configuration.
$form['source_dependent']['source_configuration'] = [
@@ -240,18 +230,6 @@ class MediaTypeForm extends EntityForm {
return $form;
}
/**
* Form submission handler to rebuild the form on select submit.
*
* @param array $form
* Full form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Current form state.
*/
public static function rebuildSubmit(array &$form, FormStateInterface $form_state) {
$form_state->setRebuild();
}
/**
* Prepares workflow options to be used in the 'checkboxes' form element.
*
@@ -373,10 +351,10 @@ class MediaTypeForm extends EntityForm {
$t_args = ['%name' => $media_type->label()];
if ($status === SAVED_UPDATED) {
drupal_set_message($this->t('The media type %name has been updated.', $t_args));
$this->messenger()->addStatus($this->t('The media type %name has been updated.', $t_args));
}
elseif ($status === SAVED_NEW) {
drupal_set_message($this->t('The media type %name has been added.', $t_args));
$this->messenger()->addStatus($this->t('The media type %name has been added.', $t_args));
$this->logger('media')->notice('Added media type %name.', $t_args);
}
@@ -35,6 +35,10 @@ interface MediaTypeInterface extends ConfigEntityInterface, EntityDescriptionInt
/**
* Returns whether thumbnail downloads are queued.
*
* When using remote media sources, the thumbnail generation could be a slow
* process. Using a queue allows for this process to be handled in the
* background.
*
* @return bool
* TRUE if thumbnails are queued for download later, FALSE if they should be
* downloaded now.
+176
View File
@@ -0,0 +1,176 @@
<?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);
}
}
+100
View File
@@ -0,0 +1,100 @@
<?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;
}
}
@@ -0,0 +1,40 @@
<?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);
}
}
@@ -0,0 +1,122 @@
<?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_url');
$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];
}
}
@@ -0,0 +1,40 @@
<?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);
}
+534
View File
@@ -0,0 +1,534 @@
<?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.');
}
}
}
@@ -0,0 +1,67 @@
<?php
namespace Drupal\media\OEmbed;
/**
* Exception thrown if an oEmbed resource cannot be fetched or parsed.
*
* @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 URL of the resource.
*
* @var string
*/
protected $url;
/**
* The resource data.
*
* @var array
*/
protected $data = [];
/**
* ResourceException 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 $data
* (optional) The raw resource data, if available.
* @param \Exception $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, $url, array $data = [], \Exception $previous = NULL) {
$this->url = $url;
$this->data = $data;
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 getData() {
return $this->data;
}
}
@@ -0,0 +1,197 @@
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
/**
* Fetches and caches oEmbed resources.
*/
class ResourceFetcher implements ResourceFetcherInterface {
use UseCacheBackendTrait;
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The oEmbed provider repository service.
*
* @var \Drupal\media\OEmbed\ProviderRepositoryInterface
*/
protected $providers;
/**
* Constructs a ResourceFetcher object.
*
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers
* The oEmbed provider repository service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* (optional) The cache backend.
*/
public function __construct(ClientInterface $http_client, ProviderRepositoryInterface $providers, CacheBackendInterface $cache_backend = NULL) {
$this->httpClient = $http_client;
$this->providers = $providers;
$this->cacheBackend = $cache_backend;
$this->useCaches = isset($cache_backend);
}
/**
* {@inheritdoc}
*/
public function fetchResource($url) {
$cache_id = "media:oembed_resource:$url";
$cached = $this->cacheGet($cache_id);
if ($cached) {
return $this->createResource($cached->data, $url);
}
try {
$response = $this->httpClient->get($url);
}
catch (RequestException $e) {
throw new ResourceException('Could not retrieve the oEmbed resource.', $url, [], $e);
}
list($format) = $response->getHeader('Content-Type');
$content = (string) $response->getBody();
if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
$encoder = new XmlEncoder();
$data = $encoder->decode($content, 'xml');
}
elseif (strstr($format, 'text/javascript') || strstr($format, 'application/json')) {
$data = Json::decode($content);
}
// If the response is neither XML nor JSON, we are in bat country.
else {
throw new ResourceException('The fetched resource did not have a valid Content-Type header.', $url);
}
$this->cacheSet($cache_id, $data);
return $this->createResource($data, $url);
}
/**
* Creates a Resource object from raw resource data.
*
* @param array $data
* The resource data returned by the provider.
* @param string $url
* The URL of the resource.
*
* @return \Drupal\media\OEmbed\Resource
* A value object representing the resource.
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the resource cannot be created.
*/
protected function createResource(array $data, $url) {
$data += [
'title' => NULL,
'author_name' => NULL,
'author_url' => NULL,
'provider_name' => NULL,
'cache_age' => NULL,
'thumbnail_url' => NULL,
'thumbnail_width' => NULL,
'thumbnail_height' => NULL,
'width' => NULL,
'height' => NULL,
'url' => NULL,
'html' => NULL,
'version' => NULL,
];
if ($data['version'] !== '1.0') {
throw new ResourceException("Resource version must be '1.0'", $url, $data);
}
// Prepare the arguments to pass to the factory method.
$provider = $data['provider_name'] ? $this->providers->get($data['provider_name']) : NULL;
// The Resource object will validate the data we create it with and throw an
// exception if anything looks wrong. For better debugging, catch those
// exceptions and wrap them in a more specific and useful exception.
try {
switch ($data['type']) {
case Resource::TYPE_LINK:
return Resource::link(
$data['url'],
$provider,
$data['title'],
$data['author_name'],
$data['author_url'],
$data['cache_age'],
$data['thumbnail_url'],
$data['thumbnail_width'],
$data['thumbnail_height']
);
case Resource::TYPE_PHOTO:
return Resource::photo(
$data['url'],
$data['width'],
$data['height'],
$provider,
$data['title'],
$data['author_name'],
$data['author_url'],
$data['cache_age'],
$data['thumbnail_url'],
$data['thumbnail_width'],
$data['thumbnail_height']
);
case Resource::TYPE_RICH:
return Resource::rich(
$data['html'],
$data['width'],
$data['height'],
$provider,
$data['title'],
$data['author_name'],
$data['author_url'],
$data['cache_age'],
$data['thumbnail_url'],
$data['thumbnail_width'],
$data['thumbnail_height']
);
case Resource::TYPE_VIDEO:
return Resource::video(
$data['html'],
$data['width'],
$data['height'],
$provider,
$data['title'],
$data['author_name'],
$data['author_url'],
$data['cache_age'],
$data['thumbnail_url'],
$data['thumbnail_width'],
$data['thumbnail_height']
);
default:
throw new ResourceException('Unknown resource type: ' . $data['type'], $url, $data);
}
}
catch (\InvalidArgumentException $e) {
throw new ResourceException($e->getMessage(), $url, $data, $e);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\media\OEmbed;
/**
* Defines an interface for an oEmbed resource fetcher service.
*
* The resource fetcher's only responsibility is to retrieve oEmbed resource
* data from an endpoint URL (i.e., as returned by
* \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()) and return a
* \Drupal\media\OEmbed\Resource value object.
*/
interface ResourceFetcherInterface {
/**
* Fetches an oEmbed resource.
*
* @param string $url
* Endpoint-specific URL of the oEmbed resource.
*
* @return \Drupal\media\OEmbed\Resource
* A resource object built from the oEmbed resource data.
*
* @see https://oembed.com/#section2
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the oEmbed endpoint is not reachable or the response returns an
* unexpected Content-Type header.
*/
public function fetchResource($url);
}
@@ -0,0 +1,187 @@
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\UseCacheBackendTrait;
use Drupal\Core\Extension\ModuleHandlerInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
/**
* Converts oEmbed media URLs into endpoint-specific resource URLs.
*/
class UrlResolver implements UrlResolverInterface {
use UseCacheBackendTrait;
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The OEmbed provider repository service.
*
* @var \Drupal\media\OEmbed\ProviderRepositoryInterface
*/
protected $providers;
/**
* The OEmbed resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Static cache of discovered oEmbed resource URLs, keyed by canonical URL.
*
* A discovered resource URL is the actual endpoint URL for a specific media
* object, fetched from its canonical URL.
*
* @var string[]
*/
protected $urlCache = [];
/**
* Constructs a UrlResolver object.
*
* @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers
* The oEmbed provider repository service.
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
* The OEmbed resource fetcher service.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* (optional) The cache backend.
*/
public function __construct(ProviderRepositoryInterface $providers, ResourceFetcherInterface $resource_fetcher, ClientInterface $http_client, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend = NULL) {
$this->providers = $providers;
$this->resourceFetcher = $resource_fetcher;
$this->httpClient = $http_client;
$this->moduleHandler = $module_handler;
$this->cacheBackend = $cache_backend;
$this->useCaches = isset($cache_backend);
}
/**
* 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 unsuccessful.
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the resource cannot be retrieved.
*/
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');
}
/**
* Tries to find the oEmbed URL in a DOM.
*
* @param \DOMXPath $xpath
* Page HTML as DOMXPath.
* @param string $format
* Format of oEmbed resource. Possible values are 'json' and 'xml'.
*
* @return bool|string
* A URL to an oEmbed resource or FALSE if not found.
*/
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 getProviderByUrl($url) {
// Check the URL against every scheme of every endpoint of every provider
// until we find a match.
foreach ($this->providers->getAll() as $provider_name => $provider_info) {
foreach ($provider_info->getEndpoints() as $endpoint) {
if ($endpoint->matchUrl($url)) {
return $provider_info;
}
}
}
$resource_url = $this->discoverResourceUrl($url);
if ($resource_url) {
return $this->resourceFetcher->fetchResource($resource_url)->getProvider();
}
throw new ResourceException('No matching provider found.', $url);
}
/**
* {@inheritdoc}
*/
public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) {
// Try to get the resource URL from the static cache.
if (isset($this->urlCache[$url])) {
return $this->urlCache[$url];
}
// Try to get the resource URL from the persistent cache.
$cache_id = "media:oembed_resource_url:$url:$max_width:$max_height";
$cached = $this->cacheGet($cache_id);
if ($cached) {
$this->urlCache[$url] = $cached->data;
return $this->urlCache[$url];
}
$provider = $this->getProviderByUrl($url);
$endpoints = $provider->getEndpoints();
$endpoint = reset($endpoints);
$resource_url = $endpoint->buildResourceUrl($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 resource URL, because some oEmbed providers
// provide extra parameters in the query string. For example, Instagram also
// supports the 'omitscript' parameter.
$this->moduleHandler->alter('oembed_resource_url', $parsed_url, $provider);
$resource_url = $parsed_url['path'] . '?' . UrlHelper::buildQuery($parsed_url['query']);
$this->urlCache[$url] = $resource_url;
$this->cacheSet($cache_id, $resource_url);
return $resource_url;
}
}
@@ -0,0 +1,45 @@
<?php
namespace Drupal\media\OEmbed;
/**
* Defines the interface for the oEmbed URL resolver service.
*
* The URL resolver is responsible for converting oEmbed-compatible media asset
* URLs into canonical resource URLs, at which an oEmbed representation of the
* asset can be retrieved.
*/
interface UrlResolverInterface {
/**
* Tries to determine the oEmbed provider for a media asset URL.
*
* @param string $url
* The media asset URL.
*
* @return \Drupal\media\OEmbed\Provider
* The oEmbed provider for the asset.
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the provider cannot be determined.
* @throws \Drupal\media\OEmbed\ProviderException
* If tne oEmbed provider causes an error.
*/
public function getProviderByUrl($url);
/**
* Builds the resource URL for a media asset URL.
*
* @param string $url
* The media asset URL.
* @param int $max_width
* (optional) Maximum width of the oEmbed resource, in pixels.
* @param int $max_height
* (optional) Maximum height of the oEmbed resource, in pixels.
*
* @return string
* Returns the resource URL corresponding to the given media item URL.
*/
public function getResourceUrl($url, $max_width = NULL, $max_height = NULL);
}
@@ -0,0 +1,303 @@
<?php
namespace Drupal\media\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\media\Entity\MediaType;
use Drupal\media\IFrameUrlHelper;
use Drupal\media\OEmbed\Resource;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'oembed' formatter.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*
* @FieldFormatter(
* id = "oembed",
* label = @Translation("oEmbed content"),
* field_types = {
* "link",
* "string",
* "string_long",
* },
* )
*/
class OEmbedFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The oEmbed resource fetcher.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The logger service.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The media settings config.
*
* @var \Drupal\Core\Config\ImmutableConfig
*/
protected $config;
/**
* The iFrame URL helper service.
*
* @var \Drupal\media\IFrameUrlHelper
*/
protected $iFrameUrlHelper;
/**
* Constructs an OEmbedFormatter instance.
*
* @param string $plugin_id
* The plugin ID for the formatter.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the formatter is associated.
* @param array $settings
* The formatter settings.
* @param string $label
* The formatter label display setting.
* @param string $view_mode
* The view mode.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @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\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
* The iFrame URL helper service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, MessengerInterface $messenger, ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, LoggerChannelFactoryInterface $logger_factory, ConfigFactoryInterface $config_factory, IFrameUrlHelper $iframe_url_helper) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
$this->messenger = $messenger;
$this->resourceFetcher = $resource_fetcher;
$this->urlResolver = $url_resolver;
$this->logger = $logger_factory->get('media');
$this->config = $config_factory->get('media.settings');
$this->iFrameUrlHelper = $iframe_url_helper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings'],
$container->get('messenger'),
$container->get('media.oembed.resource_fetcher'),
$container->get('media.oembed.url_resolver'),
$container->get('logger.factory'),
$container->get('config.factory'),
$container->get('media.oembed.iframe_url_helper')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'max_width' => 0,
'max_height' => 0,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$element = [];
$max_width = $this->getSetting('max_width');
$max_height = $this->getSetting('max_height');
foreach ($items as $delta => $item) {
$main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
$value = $item->{$main_property};
if (empty($value)) {
continue;
}
try {
$resource_url = $this->urlResolver->getResourceUrl($value, $max_width, $max_height);
$resource = $this->resourceFetcher->fetchResource($resource_url);
}
catch (ResourceException $exception) {
$this->logger->error("Could not retrieve the remote URL (@url).", ['@url' => $value]);
continue;
}
if ($resource->getType() === Resource::TYPE_LINK) {
$element[$delta] = [
'#title' => $resource->getTitle(),
'#type' => 'link',
'#url' => Url::fromUri($value),
];
}
elseif ($resource->getType() === Resource::TYPE_PHOTO) {
$element[$delta] = [
'#theme' => 'image',
'#uri' => $resource->getUrl()->toString(),
'#width' => $max_width ?: $resource->getWidth(),
'#height' => $max_height ?: $resource->getHeight(),
];
}
else {
$url = Url::fromRoute('media.oembed_iframe', [], [
'query' => [
'url' => $value,
'max_width' => $max_width,
'max_height' => $max_height,
'hash' => $this->iFrameUrlHelper->getHash($value, $max_width, $max_height),
],
]);
$domain = $this->config->get('iframe_domain');
if ($domain) {
$url->setOption('base_url', $domain);
}
// Render videos and rich content in an iframe for security reasons.
// @see: https://oembed.com/#section3
$element[$delta] = [
'#type' => 'html_tag',
'#tag' => 'iframe',
'#attributes' => [
'src' => $url->toString(),
'frameborder' => 0,
'scrolling' => FALSE,
'allowtransparency' => TRUE,
'width' => $max_width ?: $resource->getWidth(),
'height' => $max_height ?: $resource->getHeight(),
],
];
CacheableMetadata::createFromObject($resource)
->addCacheTags($this->config->getCacheTags())
->applyTo($element[$delta]);
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return parent::settingsForm($form, $form_state) + [
'max_width' => [
'#type' => 'number',
'#title' => $this->t('Maximum width'),
'#default_value' => $this->getSetting('max_width'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
],
'max_height' => [
'#type' => 'number',
'#title' => $this->t('Maximum height'),
'#default_value' => $this->getSetting('max_height'),
'#size' => 5,
'#maxlength' => 5,
'#field_suffix' => $this->t('pixels'),
'#min' => 0,
],
];
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
if ($this->getSetting('max_width') && $this->getSetting('max_height')) {
$summary[] = $this->t('Maximum size: %max_width x %max_height pixels', [
'%max_width' => $this->getSetting('max_width'),
'%max_height' => $this->getSetting('max_height'),
]);
}
elseif ($this->getSetting('max_width')) {
$summary[] = $this->t('Maximum width: %max_width pixels', [
'%max_width' => $this->getSetting('max_width'),
]);
}
elseif ($this->getSetting('max_height')) {
$summary[] = $this->t('Maximum height: %max_height pixels', [
'%max_height' => $this->getSetting('max_height'),
]);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
if ($field_definition->getTargetEntityTypeId() !== 'media') {
return FALSE;
}
if (parent::isApplicable($field_definition)) {
$media_type = $field_definition->getTargetBundle();
if ($media_type) {
$media_type = MediaType::load($media_type);
return $media_type && $media_type->getSource() instanceof OEmbedInterface;
}
}
return FALSE;
}
}
@@ -0,0 +1,64 @@
<?php
namespace Drupal\media\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\media\Entity\MediaType;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
/**
* Plugin implementation of the 'oembed_textfield' widget.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*
* @FieldWidget(
* id = "oembed_textfield",
* label = @Translation("oEmbed URL"),
* field_types = {
* "string",
* },
* )
*/
class OEmbedWidget extends StringTextfieldWidget {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element = parent::formElement($items, $delta, $element, $form, $form_state);
/** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */
$source = $items->getEntity()->getSource();
$message = $this->t('You can link to media from the following services: @providers', ['@providers' => implode(', ', $source->getProviders())]);
if (!empty($element['#value']['#description'])) {
$element['value']['#description'] = [
'#theme' => 'item_list',
'#items' => [$element['value']['#description'], $message],
];
}
else {
$element['value']['#description'] = $message;
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
$target_bundle = $field_definition->getTargetBundle();
if (!parent::isApplicable($field_definition) || $field_definition->getTargetEntityTypeId() !== 'media' || !$target_bundle) {
return FALSE;
}
return MediaType::load($target_bundle)->getSource() instanceof OEmbedInterface;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Drupal\media\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Checks if a value represents a valid oEmbed resource URL.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*
* @Constraint(
* id = "oembed_resource",
* label = @Translation("oEmbed resource", context = "Validation"),
* type = {"link", "string", "string_long"}
* )
*/
class OEmbedResourceConstraint extends Constraint {
/**
* The error message if the URL does not match any known provider.
*
* @var string
*/
public $unknownProviderMessage = 'The given URL does not match any known oEmbed providers.';
/**
* The error message if the URL matches a disallowed provider.
*
* @var string
*/
public $disallowedProviderMessage = 'Sorry, the @name provider is not allowed.';
/**
* The error message if the URL is not a valid oEmbed resource.
*
* @var string
*/
public $invalidResourceMessage = 'The provided URL does not represent a valid oEmbed resource.';
/**
* The error message if an unexpected behavior occurs.
*
* @var string
*/
public $providerErrorMessage = 'An error occurred while trying to retrieve the oEmbed provider database.';
}
@@ -0,0 +1,144 @@
<?php
namespace Drupal\media\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\media\OEmbed\ProviderException;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates oEmbed resource URLs.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class OEmbedResourceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* The logger service.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs a new OEmbedResourceConstraintValidator.
*
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
* The oEmbed URL resolver service.
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
* The resource fetcher service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger service.
*/
public function __construct(UrlResolverInterface $url_resolver, ResourceFetcherInterface $resource_fetcher, LoggerChannelFactoryInterface $logger_factory) {
$this->urlResolver = $url_resolver;
$this->resourceFetcher = $resource_fetcher;
$this->logger = $logger_factory->get('media');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('media.oembed.url_resolver'),
$container->get('media.oembed.resource_fetcher'),
$container->get('logger.factory')
);
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
/** @var \Drupal\media\MediaInterface $media */
$media = $value->getEntity();
/** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */
$source = $media->getSource();
if (!($source instanceof OEmbedInterface)) {
throw new \LogicException('Media source must implement ' . OEmbedInterface::class);
}
$url = $source->getSourceFieldValue($media);
// Ensure that the URL matches a provider.
try {
$provider = $this->urlResolver->getProviderByUrl($url);
}
catch (ResourceException $e) {
$this->handleException($e, $constraint->unknownProviderMessage);
return;
}
catch (ProviderException $e) {
$this->handleException($e, $constraint->providerErrorMessage);
return;
}
// Ensure that the provider is allowed.
if (!in_array($provider->getName(), $source->getProviders(), TRUE)) {
$this->context->addViolation($constraint->disallowedProviderMessage, [
'@name' => $provider->getName(),
]);
return;
}
// Verify that resource fetching works, because some URLs might match
// the schemes but don't support oEmbed.
try {
$endpoints = $provider->getEndpoints();
$resource_url = reset($endpoints)->buildResourceUrl($url);
$this->resourceFetcher->fetchResource($resource_url);
}
catch (ResourceException $e) {
$this->handleException($e, $constraint->invalidResourceMessage);
}
}
/**
* Handles exceptions that occur during validation.
*
* @param \Exception $e
* The caught exception.
* @param string $error_message
* (optional) The error message to set as a constraint violation.
*/
protected function handleException(\Exception $e, $error_message = NULL) {
if ($error_message) {
$this->context->addViolation($error_message);
}
// The oEmbed system makes heavy use of exception wrapping, so log the
// entire exception chain to help with troubleshooting.
do {
// @todo If $e is a ProviderException or ResourceException, log additional
// debugging information contained in those exceptions in
// https://www.drupal.org/project/drupal/issues/2972846.
$this->logger->error($e->getMessage());
$e = $e->getPrevious();
} while ($e);
}
}
@@ -22,6 +22,13 @@ use Drupal\media\MediaSourceBase;
*/
class File extends MediaSourceBase {
/**
* Key for "Name" metadata attribute.
*
* @var string
*/
const METADATA_ATTRIBUTE_NAME = 'name';
/**
* Key for "MIME type" metadata attribute.
*
@@ -36,12 +43,12 @@ class File extends MediaSourceBase {
*/
const METADATA_ATTRIBUTE_SIZE = 'filesize';
/**
* {@inheritdoc}
*/
public function getMetadataAttributes() {
return [
static::METADATA_ATTRIBUTE_NAME => $this->t('Name'),
static::METADATA_ATTRIBUTE_MIME => $this->t('MIME type'),
static::METADATA_ATTRIBUTE_SIZE => $this->t('File size'),
];
@@ -58,15 +65,16 @@ class File extends MediaSourceBase {
return parent::getMetadata($media, $attribute_name);
}
switch ($attribute_name) {
case 'mimetype':
return $file->getMimeType();
case 'filesize':
return $file->getSize();
case static::METADATA_ATTRIBUTE_NAME:
case 'default_name':
return $file->getFilename();
case static::METADATA_ATTRIBUTE_MIME:
return $file->getMimeType();
case static::METADATA_ATTRIBUTE_SIZE:
return $file->getSize();
case 'thumbnail_uri':
return $this->getThumbnail($file) ?: parent::getMetadata($media, $attribute_name);
@@ -6,7 +6,7 @@ use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\File\FileSystem;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
@@ -51,7 +51,7 @@ class Image extends File {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystem
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
@@ -74,10 +74,10 @@ class Image extends File {
* The config factory service.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\File\FileSystem $file_system
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, ImageFactory $image_factory, FileSystem $file_system) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, ImageFactory $image_factory, FileSystemInterface $file_system) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
$this->imageFactory = $image_factory;
@@ -0,0 +1,466 @@
<?php
namespace Drupal\media\Plugin\media\Source;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Url;
use Drupal\media\IFrameUrlHelper;
use Drupal\media\OEmbed\Resource;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\MediaSourceBase;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a media source plugin for oEmbed resources.
*
* For security reasons, the oEmbed source (and, therefore, anything that
* extends it) obeys a hard-coded list of allowed third-party oEmbed providers
* set in its plugin definition's providers array. This array is a set of
* provider names, exactly as they appear in the canonical oEmbed provider
* database at https://oembed.com/providers.json.
*
* You can implement support for additional providers by defining a new plugin
* that uses this class. This can be done in hook_media_source_info_alter().
* For example:
* @code
* <?php
*
* function example_media_source_info_alter(array &$sources) {
* $sources['artwork'] = [
* 'id' => 'artwork',
* 'label' => t('Artwork'),
* 'description' => t('Use artwork from Flickr and DeviantArt.'),
* 'allowed_field_types' => ['string'],
* 'default_thumbnail_filename' => 'no-thumbnail.png',
* 'providers' => ['Deviantart.com', 'Flickr'],
* 'class' => 'Drupal\media\Plugin\media\Source\OEmbed',
* ];
* }
* @endcode
* The "Deviantart.com" and "Flickr" provider names are specified in
* https://oembed.com/providers.json. The
* \Drupal\media\Plugin\media\Source\OEmbed class already knows how to handle
* standard interactions with third-party oEmbed APIs, so there is no need to
* define a new class which extends it. With the code above, you will able to
* create media types which use the "Artwork" source plugin, and use those media
* types to link to assets on Deviantart and Flickr.
*
* @MediaSource(
* id = "oembed",
* label = @Translation("oEmbed source"),
* description = @Translation("Use oEmbed URL for reusable media."),
* allowed_field_types = {"string"},
* default_thumbnail_filename = "no-thumbnail.png",
* deriver = "Drupal\media\Plugin\media\Source\OEmbedDeriver",
* providers = {},
* )
*/
class OEmbed extends MediaSourceBase implements OEmbedInterface {
/**
* The logger channel for media.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The HTTP client.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* The oEmbed resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* The OEmbed manager service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The iFrame URL helper service.
*
* @var \Drupal\media\IFrameUrlHelper
*/
protected $iFrameUrlHelper;
/**
* Constructs a new OEmbed instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
* @param \Drupal\Core\Logger\LoggerChannelInterface $logger
* The logger channel for media.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @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\media\IFrameUrlHelper $iframe_url_helper
* The iFrame URL helper 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, MessengerInterface $messenger, ClientInterface $http_client, ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, IFrameUrlHelper $iframe_url_helper) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
$this->logger = $logger;
$this->messenger = $messenger;
$this->httpClient = $http_client;
$this->resourceFetcher = $resource_fetcher;
$this->urlResolver = $url_resolver;
$this->iFrameUrlHelper = $iframe_url_helper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_field.manager'),
$container->get('config.factory'),
$container->get('plugin.manager.field.field_type'),
$container->get('logger.factory')->get('media'),
$container->get('messenger'),
$container->get('http_client'),
$container->get('media.oembed.resource_fetcher'),
$container->get('media.oembed.url_resolver'),
$container->get('media.oembed.iframe_url_helper')
);
}
/**
* {@inheritdoc}
*/
public function getMetadataAttributes() {
return [
'type' => $this->t('Resource type'),
'title' => $this->t('Resource title'),
'author_name' => $this->t('The name of the author/owner'),
'author_url' => $this->t('The URL of the author/owner'),
'provider_name' => $this->t("The name of the provider"),
'provider_url' => $this->t('The URL of the provider'),
'cache_age' => $this->t('Suggested cache lifetime'),
'default_name' => $this->t('Default name of the media item'),
'thumbnail_uri' => $this->t('Local URI of the thumbnail'),
'thumbnail_width' => $this->t('Thumbnail width'),
'thumbnail_height' => $this->t('Thumbnail height'),
'url' => $this->t('The source URL of the resource'),
'width' => $this->t('The width of the resource'),
'height' => $this->t('The height of the resource'),
'html' => $this->t('The HTML representation of the resource'),
];
}
/**
* {@inheritdoc}
*/
public function getMetadata(MediaInterface $media, $name) {
$media_url = $this->getSourceFieldValue($media);
try {
$resource_url = $this->urlResolver->getResourceUrl($media_url);
$resource = $this->resourceFetcher->fetchResource($resource_url);
}
catch (ResourceException $e) {
$this->messenger->addError($e->getMessage());
return NULL;
}
switch ($name) {
case 'default_name':
if ($title = $this->getMetadata($media, 'title')) {
return $title;
}
elseif ($url = $this->getMetadata($media, 'url')) {
return $url;
}
return parent::getMetadata($media, 'default_name');
case 'thumbnail_uri':
return $this->getLocalThumbnailUri($resource) ?: parent::getMetadata($media, 'thumbnail_uri');
case 'type':
return $resource->getType();
case 'title':
return $resource->getTitle();
case 'author_name':
return $resource->getAuthorName();
case 'author_url':
return $resource->getAuthorUrl();
case 'provider_name':
$provider = $resource->getProvider();
return $provider ? $provider->getName() : '';
case 'provider_url':
$provider = $resource->getProvider();
return $provider ? $provider->getUrl() : NULL;
case 'cache_age':
return $resource->getCacheMaxAge();
case 'thumbnail_width':
return $resource->getThumbnailWidth();
case 'thumbnail_height':
return $resource->getThumbnailHeight();
case 'url':
$url = $resource->getUrl();
return $url ? $url->toString() : NULL;
case 'width':
return $resource->getWidth();
case 'height':
return $resource->getHeight();
case 'html':
return $resource->getHtml();
default:
break;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$domain = $this->configFactory->get('media.settings')->get('iframe_domain');
if (!$this->iFrameUrlHelper->isSecure($domain)) {
array_unshift($form, [
'#markup' => '<p>' . $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=":url" target="_blank">You can specify a different domain for serving oEmbed content here</a> (opens in a new window).', [
':url' => Url::fromRoute('media.settings')->setAbsolute()->toString(),
]) . '</p>',
]);
}
$form['thumbnails_directory'] = [
'#type' => 'textfield',
'#title' => $this->t('Thumbnails location'),
'#default_value' => $this->configuration['thumbnails_directory'],
'#description' => $this->t('Thumbnails will be fetched from the provider for local usage. This is the URI of the directory where they will be placed.'),
'#required' => TRUE,
];
$configuration = $this->getConfiguration();
$plugin_definition = $this->getPluginDefinition();
$form['providers'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Allowed providers'),
'#default_value' => $configuration['providers'],
'#options' => array_combine($plugin_definition['providers'], $plugin_definition['providers']),
'#description' => $this->t('Optionally select the allowed oEmbed providers for this media type. If left blank, all providers will be allowed.'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$configuration = $this->getConfiguration();
$configuration['providers'] = array_filter(array_values($configuration['providers']));
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$thumbnails_directory = $form_state->getValue('thumbnails_directory');
if (!file_valid_uri($thumbnails_directory)) {
$form_state->setErrorByName('thumbnails_directory', $this->t('@path is not a valid path.', [
'@path' => $thumbnails_directory,
]));
}
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'thumbnails_directory' => 'public://oembed_thumbnails',
'providers' => [],
] + parent::defaultConfiguration();
}
/**
* Returns the local URI for a resource thumbnail.
*
* If the thumbnail is not already locally stored, this method will attempt
* to download it.
*
* @param \Drupal\media\OEmbed\Resource $resource
* The oEmbed resource.
*
* @return string|null
* The local thumbnail URI, or NULL if it could not be downloaded, or if the
* resource has no thumbnail at all.
*
* @todo Determine whether or not oEmbed media thumbnails should be stored
* locally at all, and if so, whether that functionality should be
* toggle-able. See https://www.drupal.org/project/drupal/issues/2962751 for
* more information.
*/
protected function getLocalThumbnailUri(Resource $resource) {
// If there is no remote thumbnail, there's nothing for us to fetch here.
$remote_thumbnail_url = $resource->getThumbnailUrl();
if (!$remote_thumbnail_url) {
return NULL;
}
$remote_thumbnail_url = $remote_thumbnail_url->toString();
// Compute the local thumbnail URI, regardless of whether or not it exists.
$configuration = $this->getConfiguration();
$directory = $configuration['thumbnails_directory'];
$local_thumbnail_uri = "$directory/" . Crypt::hashBase64($remote_thumbnail_url) . '.' . pathinfo($remote_thumbnail_url, PATHINFO_EXTENSION);
// If the local thumbnail already exists, return its URI.
if (file_exists($local_thumbnail_uri)) {
return $local_thumbnail_uri;
}
// The local thumbnail doesn't exist yet, so try to download it. First,
// ensure that the destination directory is writable, and if it's not,
// log an error and bail out.
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
$this->logger->warning('Could not prepare thumbnail destination directory @dir for oEmbed media.', [
'@dir' => $directory,
]);
return NULL;
}
$error_message = 'Could not download remote thumbnail from {url}.';
$error_context = [
'url' => $remote_thumbnail_url,
];
try {
$response = $this->httpClient->get($remote_thumbnail_url);
if ($response->getStatusCode() === 200) {
$success = file_unmanaged_save_data((string) $response->getBody(), $local_thumbnail_uri, FILE_EXISTS_REPLACE);
if ($success) {
return $local_thumbnail_uri;
}
else {
$this->logger->warning($error_message, $error_context);
}
}
}
catch (RequestException $e) {
$this->logger->warning($e->getMessage());
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getSourceFieldConstraints() {
return [
'oembed_resource' => [],
];
}
/**
* {@inheritdoc}
*/
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
'type' => 'oembed',
]);
}
/**
* {@inheritdoc}
*/
public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) {
parent::prepareFormDisplay($type, $display);
$source_field = $this->getSourceFieldDefinition($type)->getName();
$display->setComponent($source_field, [
'type' => 'oembed_textfield',
'weight' => $display->getComponent($source_field)['weight'],
]);
$display->removeComponent('name');
}
/**
* {@inheritdoc}
*/
public function getProviders() {
$configuration = $this->getConfiguration();
return $configuration['providers'] ?: $this->getPluginDefinition()['providers'];
}
/**
* {@inheritdoc}
*/
public function createSourceField(MediaTypeInterface $type) {
$plugin_definition = $this->getPluginDefinition();
$label = (string) $this->t('@type URL', [
'@type' => $plugin_definition['label'],
]);
return parent::createSourceField($type)->set('label', $label);
}
}
@@ -0,0 +1,32 @@
<?php
namespace Drupal\media\Plugin\media\Source;
use Drupal\Component\Plugin\Derivative\DeriverBase;
/**
* Derives media source plugin definitions for supported oEmbed providers.
*
* @internal
* This is an internal part of the oEmbed system and should only be used by
* oEmbed-related code in Drupal core.
*/
class OEmbedDeriver extends DeriverBase {
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$this->derivatives = [
'video' => [
'id' => 'video',
'label' => t('Remote video'),
'description' => t('Use remote video URL for reusable media.'),
'providers' => ['YouTube', 'Vimeo'],
'default_thumbnail_filename' => 'video.png',
] + $base_plugin_definition,
];
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Drupal\media\Plugin\media\Source;
use Drupal\media\MediaSourceFieldConstraintsInterface;
/**
* Defines additional functionality for source plugins that use oEmbed.
*/
interface OEmbedInterface extends MediaSourceFieldConstraintsInterface {
/**
* Returns the oEmbed provider names.
*
* The allowed providers can be configured by the user. If it is not
* configured, all providers supported by the plugin are returned.
*
* @return string[]
* A list of oEmbed provider names.
*/
public function getProviders();
}
@@ -0,0 +1,14 @@
{#
/**
* @file
* Default theme implementation to display an oEmbed resource in an iframe.
*
* @ingroup themeable
*/
#}
<!DOCTYPE html>
<html>
<body style="margin: 0">
{{ media|raw }}
</body>
</html>
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="alternate" href="photo_flickr.json"
type="application/json+oembed" title="Druplicon FTW!">
</head>
<body></body>
</html>
@@ -0,0 +1,12 @@
{
"type": "photo",
"title": "Druplicon FTW!",
"width": "88",
"height": "100",
"url": "internal:\/core\/misc\/druplicon.png",
"thumbnail_url": "internal:\/core\/misc\/druplicon.png",
"thumbnail_width": 88,
"thumbnail_height": 100,
"provider_name": "Flickr",
"version": "1.0"
}
+61
View File
@@ -0,0 +1,61 @@
[
{
"provider_name": "Vimeo",
"provider_url": "https:\/\/vimeo.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/vimeo.com\/*",
"https:\/\/vimeo.com\/album\/*\/video\/*",
"https:\/\/vimeo.com\/channels\/*\/*",
"https:\/\/vimeo.com\/groups\/*\/videos\/*",
"https:\/\/vimeo.com\/ondemand\/*\/*",
"https:\/\/player.vimeo.com\/video\/*"
],
"url": "https:\/\/vimeo.com\/api\/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "Twitter",
"provider_url": "http:\/\/www.twitter.com\/",
"endpoints": [
{
"schemes": [
"https:\/\/twitter.com\/*\/status\/*",
"https:\/\/*.twitter.com\/*\/status\/*"
],
"url": "https:\/\/publish.twitter.com\/oembed"
}
]
},
{
"provider_name": "CollegeHumor",
"provider_url": "http:\/\/www.collegehumor.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/www.collegehumor.com\/video\/*"
],
"url": "http:\/\/www.collegehumor.com\/oembed.{format}",
"discovery": true
}
]
},
{
"provider_name": "Flickr",
"provider_url": "http:\/\/www.flickr.com\/",
"endpoints": [
{
"schemes": [
"http:\/\/*.flickr.com\/photos\/*",
"http:\/\/flic.kr\/p\/*"
],
"url": "http:\/\/www.flickr.com\/services\/oembed\/",
"discovery": true
}
]
}
]
@@ -0,0 +1,13 @@
{
"url": "https:\/\/twitter.com\/drupaldevdays\/status\/935643039741202432",
"author_name": "Drupal Dev Days",
"author_url": "https:\/\/twitter.com\/drupaldevdays",
"html": "<h1>By the power of Greyskull, Twitter works!</h1>",
"width": 550,
"height": 360,
"type": "rich",
"cache_age": "3153600000",
"provider_name": "Twitter",
"provider_url": "https:\/\/twitter.com",
"version": "1.0"
}
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="alternate" href="video_collegehumor.xml"
type="application/xml+oembed" title="Let&#039;s Not Get a Drink Sometime">
</head>
<body></body>
</html>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<oembed>
<type>video</type>
<version>1.0</version>
<title>Let's Not Get a Drink Sometime</title>
<https/>
<author_name>CollegeHumor</author_name>
<author_url>http://www.collegehumor.com</author_url>
<provider_name>CollegeHumor</provider_name>
<provider_url>http://www.collegehumor.com</provider_url>
<width>610</width>
<height>343</height>
<html><h1>By the power of Greyskull, CollegeHumor works!</h1>
</html>
<thumbnail_url>internal:/core/misc/druplicon.png</thumbnail_url>
<thumbnail_width>88</thumbnail_width>
<thumbnail_height>100</thumbnail_height>
</oembed>
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html lang="de">
<head>
<link rel="alternate" href="video_vimeo.json"
type="application/json+oembed" title="Drupal Rap Video - Schipulcon09">
</head>
<body></body>
</html>
@@ -0,0 +1,16 @@
{
"type": "video",
"version": "1.0",
"provider_name": "Vimeo",
"provider_url": "https:\/\/vimeo.com\/",
"title": "Drupal Rap Video - Schipulcon09",
"author_name": "Tendenci - The Open Source AMS",
"author_url": "https:\/\/vimeo.com\/schipul",
"html": "<h1>By the power of Greyskull, Vimeo works!</h1>",
"width": 480,
"height": 360,
"description": "Special thanks to Tendenci, formerly Schipul for sponsoring this video with training, equipment and time. The open source way. All creative however was self directed by the individuals - A. Hughes (www.schipul.com\/ahughes) featuring QCait (www.schipul.com\/qcait) - Hands On Drupal\n\nDrupal is a free software package that allows an individual or a community of users to easily publish, manage and organize a wide variety of content on a website.\n\nNeed a little Drupal help or just want to geek out with us? Visit our www.schipul.com\/drupal for more info - we'd love to connect!\n\nGo here for Drupal Common Terms and Suggested Modules : http:\/\/schipul.com\/en\/helpfiles\/v\/229",
"thumbnail_url": "internal:\/core\/misc\/druplicon.png",
"thumbnail_width": 295,
"thumbnail_height": 221
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* Contains database additions to drupal-8.4.0.bare.standard.php.gz for testing
* the upgrade path of https://www.drupal.org/node/2862422.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$role = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'user.role.authenticated')
->execute()
->fetchField();
$role = unserialize($role);
$role['permissions'][] = 'update media';
$role['permissions'][] = 'update any media';
$role['permissions'][] = 'delete media';
$role['permissions'][] = 'delete any media';
$role['permissions'][] = 'create media';
$connection->update('config')
->fields([
'data' => serialize($role),
'collection' => '',
'name' => 'user.role.authenticated',
])
->condition('collection', '')
->condition('name', 'user.role.authenticated')
->execute();
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
name: Media oEmbed test
description: 'Provides functionality to mimic an oEmbed provider.'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:media
@@ -0,0 +1,17 @@
<?php
/**
* @file
* Helper module for the Media oEmbed tests.
*/
use Drupal\media\OEmbed\Provider;
/**
* Implements hook_oembed_resource_url_alter().
*/
function media_test_oembed_oembed_resource_url_alter(array &$parsed_url, Provider $provider) {
if ($provider->getName() === 'Vimeo') {
$parsed_url['query']['altered'] = 1;
}
}
@@ -0,0 +1,6 @@
media_test_oembed.resource.get:
path: '/media_test_oembed/resource'
defaults:
_controller: '\Drupal\media_test_oembed\Controller\ResourceController::get'
requirements:
_access: 'TRUE'
@@ -0,0 +1,48 @@
<?php
namespace Drupal\media_test_oembed\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Test controller returning oEmbed resources from Media's test fixtures.
*/
class ResourceController {
/**
* Returns the contents of an oEmbed resource fixture.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Symfony\Component\HttpFoundation\Response
* The JSON response.
*/
public function get(Request $request) {
$asset_url = $request->query->get('url');
$resources = \Drupal::state()->get(static::class, []);
$content = file_get_contents($resources[$asset_url]);
$response = new Response($content);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
/**
* Maps an asset URL to a local fixture representing its oEmbed resource.
*
* @param string $asset_url
* The asset URL.
* @param string $resource_path
* The path of the oEmbed resource representing the asset.
*/
public static function setResourceUrl($asset_url, $resource_path) {
$resources = \Drupal::state()->get(static::class, []);
$resources[$asset_url] = $resource_path;
\Drupal::state()->set(static::class, $resources);
}
}
@@ -0,0 +1,26 @@
<?php
namespace Drupal\media_test_oembed;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Replaces oEmbed-related media services with testing versions.
*/
class MediaTestOembedServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
parent::alter($container);
$container->getDefinition('media.oembed.provider_repository')
->setClass(ProviderRepository::class);
$container->getDefinition('media.oembed.url_resolver')
->setClass(UrlResolver::class);
}
}
@@ -0,0 +1,55 @@
<?php
namespace Drupal\media_test_oembed;
use Drupal\media\OEmbed\Provider;
use Drupal\media\OEmbed\ProviderRepository as BaseProviderRepository;
/**
* Overrides the oEmbed provider repository service for testing purposes.
*
* This service does not use caching at all, and will always try to retrieve
* provider data from state before calling the parent methods.
*/
class ProviderRepository extends BaseProviderRepository {
/**
* {@inheritdoc}
*/
protected function cacheGet($cid) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getAll() {
return \Drupal::state()->get(static::class) ?: parent::getAll();
}
/**
* {@inheritdoc}
*/
public function get($provider_name) {
$providers = \Drupal::state()->get(static::class, []);
if (isset($providers[$provider_name])) {
return $providers[$provider_name];
}
return parent::get($provider_name);
}
/**
* Stores an oEmbed provider value object in state.
*
* @param \Drupal\media\OEmbed\Provider $provider
* The provider to store.
*/
public function setProvider(Provider $provider) {
$providers = \Drupal::state()->get(static::class, []);
$name = $provider->getName();
$providers[$name] = $provider;
\Drupal::state()->set(static::class, $providers);
}
}
@@ -0,0 +1,38 @@
<?php
namespace Drupal\media_test_oembed;
use Drupal\media\OEmbed\UrlResolver as BaseUrlResolver;
/**
* Overrides the oEmbed URL resolver service for testing purposes.
*/
class UrlResolver extends BaseUrlResolver {
/**
* Sets the endpoint URL for an oEmbed resource URL.
*
* @param string $url
* The resource URL.
* @param string $endpoint_url
* The endpoint URL.
*/
public static function setEndpointUrl($url, $endpoint_url) {
$urls = \Drupal::state()->get(static::class, []);
$urls[$url] = $endpoint_url;
\Drupal::state()->set(static::class, $urls);
}
/**
* {@inheritdoc}
*/
public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) {
$urls = \Drupal::state()->get(static::class, []);
if (isset($urls[$url])) {
return $urls[$url];
}
return parent::getResourceUrl($url, $max_width, $max_height);
}
}
@@ -5,5 +5,5 @@ core: 8.x
package: Testing
version: VERSION
dependencies:
- media
- media_test_source
- drupal:media
- drupal:media_test_source
@@ -0,0 +1,154 @@
langcode: en
status: true
dependencies:
module:
- media
- user
id: test_media_bulk_form
label: ''
module: views
description: ''
tag: ''
base_table: media_field_data
base_field: mid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
style:
type: table
row:
type: fields
fields:
media_bulk_form:
id: media_bulk_form
table: media
field: media_bulk_form
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
action_title: 'With selection'
include_exclude: exclude
selected_actions: { }
entity_type: media
plugin_id: bulk_form
name:
id: name
table: media_field_data
field: name
entity_type: media
entity_field: media
hide_empty: false
empty_zero: false
settings:
link_to_entity: false
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: 'Media name'
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
status:
id: status
table: media_field_data
field: status
relationship: none
group_type: group
admin_label: ''
label: Status
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: boolean
settings:
format: custom
format_custom_true: Published
format_custom_false: Unpublished
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: media
entity_field: status
plugin_id: field
sorts:
mid:
id: mid
table: media_field_data
field: mid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: media
entity_field: mid
plugin_id: standard
title: 'Entity bulk form test view'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
path: test-media-bulk-form
@@ -5,5 +5,5 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- media
- views
- drupal:media
- drupal:views
@@ -0,0 +1,173 @@
<?php
namespace Drupal\Tests\media\Functional\FieldFormatter;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\media\Entity\Media;
use Drupal\media_test_oembed\Controller\ResourceController;
use Drupal\media_test_oembed\UrlResolver;
use Drupal\Tests\media\Functional\MediaFunctionalTestBase;
use Drupal\Tests\media\Traits\OEmbedTestTrait;
/**
* @covers \Drupal\media\Plugin\Field\FieldFormatter\OEmbedFormatter
*
* @group media
*/
class OEmbedFormatterTest extends MediaFunctionalTestBase {
use OEmbedTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'field_ui',
'link',
'media_test_oembed',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->lockHttpClientToFixtures();
}
/**
* Data provider for testRender().
*
* @see ::testRender()
*
* @return array
*/
public function providerRender() {
return [
'Vimeo video' => [
'https://vimeo.com/7073899',
'video_vimeo.json',
[],
[
'iframe' => [
'src' => '/media/oembed?url=https%3A//vimeo.com/7073899',
'width' => 480,
'height' => 360,
],
],
],
'Vimeo video, resized' => [
'https://vimeo.com/7073899',
'video_vimeo.json?maxwidth=100&maxheight=100',
['max_width' => 100, 'max_height' => 100],
[
'iframe' => [
'src' => '/media/oembed?url=https%3A//vimeo.com/7073899',
'width' => 100,
'height' => 100,
],
],
],
'tweet' => [
'https://twitter.com/drupaldevdays/status/935643039741202432',
'rich_twitter.json',
[],
[
'iframe' => [
'src' => '/media/oembed?url=https%3A//twitter.com/drupaldevdays/status/935643039741202432',
'width' => 550,
'height' => 360,
],
],
],
'Flickr photo' => [
'https://www.flickr.com/photos/amazeelabs/26497866357',
'photo_flickr.json',
[],
[
'img' => [
'src' => '/core/misc/druplicon.png',
'width' => 88,
'height' => 100,
],
],
],
];
}
/**
* Tests that oEmbed media types' display can be configured correctly.
*/
public function testDisplayConfiguration() {
$account = $this->drupalCreateUser(['administer media display']);
$this->drupalLogin($account);
$media_type = $this->createMediaType('oembed:video');
$this->drupalGet('/admin/structure/media/manage/' . $media_type->id() . '/display');
$assert = $this->assertSession();
$assert->statusCodeEquals(200);
// Test that the formatter doesn't try to check applicability for fields
// which do not have a specific target bundle.
// @see https://www.drupal.org/project/drupal/issues/2976795.
$assert->pageTextNotContains('Can only flip STRING and INTEGER values!');
}
/**
* Tests the oEmbed field formatter.
*
* @param string $url
* The canonical URL of the media asset to test.
* @param string $resource_url
* The oEmebd resource URL of the media asset to test.
* @param mixed $formatter_settings
* Settings for the oEmbed field formatter.
* @param array $selectors
* An array of arrays. Each key is a CSS selector targeting an element in
* the rendered output, and each value is an array of attributes, keyed by
* name, that the element is expected to have.
*
* @dataProvider providerRender
*/
public function testRender($url, $resource_url, array $formatter_settings, array $selectors) {
$account = $this->drupalCreateUser(['view media']);
$this->drupalLogin($account);
$media_type = $this->createMediaType('oembed:video');
$source = $media_type->getSource();
$source_field = $source->getSourceFieldDefinition($media_type);
EntityViewDisplay::create([
'targetEntityType' => 'media',
'bundle' => $media_type->id(),
'mode' => 'full',
'status' => TRUE,
])->removeComponent('thumbnail')
->setComponent($source_field->getName(), [
'type' => 'oembed',
'settings' => $formatter_settings,
])
->save();
$this->hijackProviderEndpoints();
ResourceController::setResourceUrl($url, $this->getFixturesDirectory() . '/' . $resource_url);
UrlResolver::setEndpointUrl($url, $resource_url);
$entity = Media::create([
'bundle' => $media_type->id(),
$source_field->getName() => $url,
]);
$entity->save();
$this->drupalGet($entity->toUrl());
$assert = $this->assertSession();
$assert->statusCodeEquals(200);
foreach ($selectors as $selector => $attributes) {
foreach ($attributes as $attribute => $value) {
$assert->elementAttributeContains('css', $selector, $attribute, $value);
}
}
}
}
@@ -0,0 +1,228 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Core\Cache\Cache;
use Drupal\file\Entity\File;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\media\Functional\Rest\MediaResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\user\Entity\User;
/**
* @group hal
*/
class MediaHalJsonAnonTest extends MediaResourceTestBase {
use HalEntityNormalizationTrait;
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$default_normalization = parent::getExpectedNormalizedEntity();
$normalization = $this->applyHalFieldNormalization($default_normalization);
$file = File::load(1);
$thumbnail = File::load(2);
$author = User::load($this->entity->getOwnerId());
return $normalization + [
'_links' => [
'self' => [
'href' => $this->baseUrl . '/media/1?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/media/camelids',
],
$this->baseUrl . '/rest/relation/media/camelids/field_media_file' => [
[
'href' => $file->url(),
'lang' => 'en',
],
],
$this->baseUrl . '/rest/relation/media/camelids/revision_user' => [
[
'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
],
],
$this->baseUrl . '/rest/relation/media/camelids/thumbnail' => [
[
'href' => $thumbnail->url(),
'lang' => 'en',
],
],
$this->baseUrl . '/rest/relation/media/camelids/uid' => [
[
'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
'lang' => 'en',
],
],
],
'_embedded' => [
$this->baseUrl . '/rest/relation/media/camelids/field_media_file' => [
[
'_links' => [
'self' => [
'href' => $file->url(),
],
'type' => [
'href' => $this->baseUrl . '/rest/type/file/file',
],
],
'lang' => 'en',
'uuid' => [
[
'value' => $file->uuid(),
],
],
],
],
$this->baseUrl . '/rest/relation/media/camelids/revision_user' => [
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/user/user',
],
],
'uuid' => [
[
'value' => $author->uuid(),
],
],
],
],
$this->baseUrl . '/rest/relation/media/camelids/thumbnail' => [
[
'_links' => [
'self' => [
'href' => $thumbnail->url(),
],
'type' => [
'href' => $this->baseUrl . '/rest/type/file/file',
],
],
'lang' => 'en',
'uuid' => [
[
'value' => $thumbnail->uuid(),
],
],
],
],
$this->baseUrl . '/rest/relation/media/camelids/uid' => [
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/user/user',
],
],
'uuid' => [
[
'value' => $author->uuid(),
],
],
'lang' => 'en',
],
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedFileEntity() {
$normalization = parent::getExpectedNormalizedFileEntity();
$owner = static::$auth ? $this->account : User::load(0);
// Cannot use applyHalFieldNormalization() as it uses the $entity property
// from the test class, which in the case of file upload tests, is the
// parent entity test entity for the file that's created.
// The HAL normalization adds entity reference fields to '_links' and
// '_embedded'.
unset($normalization['uid']);
return $normalization + [
'_links' => [
'self' => [
// @todo This can use a proper link once
// https://www.drupal.org/project/drupal/issues/2907402 is complete.
// This link matches what is generated from from File::url(), a
// resource URL is currently not available.
'href' => file_create_url($normalization['uri'][0]['value']),
],
'type' => [
'href' => $this->baseUrl . '/rest/type/file/file',
],
$this->baseUrl . '/rest/relation/file/file/uid' => [
['href' => $this->baseUrl . '/user/' . $owner->id() . '?_format=hal_json'],
],
],
'_embedded' => [
$this->baseUrl . '/rest/relation/file/file/uid' => [
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/user/' . $owner->id() . '?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/user/user',
],
],
'uuid' => [
[
'value' => $owner->uuid(),
],
],
],
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return parent::getNormalizedPostEntity() + [
'_links' => [
'type' => [
'href' => $this->baseUrl . '/rest/type/media/camelids',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheTags() {
return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:hal.settings']);
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class MediaHalJsonBasicAuthTest extends MediaHalJsonAnonTest {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class MediaHalJsonCookieTest extends MediaHalJsonAnonTest {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Tests\media\Functional\Rest\MediaTypeResourceTestBase;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class MediaTypeHalJsonAnonTest extends MediaTypeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Tests\media\Functional\Rest\MediaTypeResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class MediaTypeHalJsonBasicAuthTest extends MediaTypeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\media\Functional\Hal;
use Drupal\Tests\media\Functional\Rest\MediaTypeResourceTestBase;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class MediaTypeHalJsonCookieTest extends MediaTypeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -39,7 +39,7 @@ class MediaAccessTest extends MediaFunctionalTestBase {
public function testMediaAccess() {
$assert_session = $this->assertSession();
$media_type = $this->createMediaType();
$media_type = $this->createMediaType('test');
// Create media.
$media = Media::create([
@@ -0,0 +1,113 @@
<?php
namespace Drupal\Tests\media\Functional;
use Drupal\media\Entity\Media;
use Drupal\views\Views;
/**
* Tests a media bulk form.
*
* @group media
*/
class MediaBulkFormTest extends MediaFunctionalTestBase {
/**
* Modules to be enabled.
*
* @var array
*/
public static $modules = ['media_test_views'];
/**
* The test media type.
*
* @var \Drupal\media\MediaTypeInterface
*/
protected $testMediaType;
/**
* The test media items.
*
* @var \Drupal\media\MediaInterface[]
*/
protected $mediaItems;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->testMediaType = $this->createMediaType('test');
// Create some test media items.
$this->mediaItems = [];
for ($i = 1; $i <= 5; $i++) {
$media = Media::create([
'bundle' => $this->testMediaType->id(),
]);
$media->save();
$this->mediaItems[] = $media;
}
}
/**
* Tests the media bulk form.
*/
public function testBulkForm() {
$session = $this->getSession();
$page = $session->getPage();
$assert_session = $this->assertSession();
// Check that all created items are present in the test view.
$view = Views::getView('test_media_bulk_form');
$view->execute();
$this->assertSame($view->total_rows, 5);
// Check the operations are accessible to the logged in user.
$this->drupalGet('test-media-bulk-form');
// Current available actions: Delete, Save, Publish, Unpublish.
$available_actions = [
'media_delete_action',
'media_publish_action',
'media_save_action',
'media_unpublish_action',
];
foreach ($available_actions as $action_name) {
$assert_session->optionExists('action', $action_name);
}
// Test unpublishing in bulk.
$page->checkField('media_bulk_form[0]');
$page->checkField('media_bulk_form[1]');
$page->checkField('media_bulk_form[2]');
$page->selectFieldOption('action', 'media_unpublish_action');
$page->pressButton('Apply to selected items');
$assert_session->pageTextContains('Unpublish media was applied to 3 items');
$this->assertFalse($this->storage->loadUnchanged(1)->isPublished(), 'The unpublish action failed in some of the media items.');
$this->assertFalse($this->storage->loadUnchanged(2)->isPublished(), 'The unpublish action failed in some of the media items.');
$this->assertFalse($this->storage->loadUnchanged(3)->isPublished(), 'The unpublish action failed in some of the media items.');
// Test publishing in bulk.
$page->checkField('media_bulk_form[0]');
$page->checkField('media_bulk_form[1]');
$page->selectFieldOption('action', 'media_publish_action');
$page->pressButton('Apply to selected items');
$assert_session->pageTextContains('Publish media was applied to 2 items');
$this->assertTrue($this->storage->loadUnchanged(1)->isPublished(), 'The publish action failed in some of the media items.');
$this->assertTrue($this->storage->loadUnchanged(2)->isPublished(), 'The publish action failed in some of the media items.');
// Test deletion in bulk.
$page->checkField('media_bulk_form[0]');
$page->checkField('media_bulk_form[1]');
$page->selectFieldOption('action', 'media_delete_action');
$page->pressButton('Apply to selected items');
$assert_session->pageTextContains('Are you sure you want to delete these media items?');
$page->pressButton('Delete');
$assert_session->pageTextContains('Deleted 2 items.');
$this->assertNull($this->storage->loadUnchanged(1), 'Could not delete some of the media items.');
$this->assertNull($this->storage->loadUnchanged(2), 'Could not delete some of the media items.');
}
}
@@ -4,7 +4,8 @@ namespace Drupal\Tests\media\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\media\Entity\Media;
use Drupal\system\Tests\Entity\EntityWithUriCacheTagsTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
/**
* Tests the media items cache tags.
@@ -13,7 +14,7 @@ use Drupal\system\Tests\Entity\EntityWithUriCacheTagsTestBase;
*/
class MediaCacheTagsTest extends EntityWithUriCacheTagsTestBase {
use MediaFunctionalTestCreateMediaTypeTrait;
use MediaTypeCreationTrait;
/**
* {@inheritdoc}
@@ -28,7 +29,7 @@ class MediaCacheTagsTest extends EntityWithUriCacheTagsTestBase {
*/
protected function createEntity() {
// Create a media type.
$mediaType = $this->createMediaType();
$mediaType = $this->createMediaType('test');
// Create a media item.
$media = Media::create([
@@ -23,7 +23,7 @@ class MediaContextualLinksTest extends MediaFunctionalTestBase {
*/
public function testMediaContextualLinks() {
// Create a media type.
$mediaType = $this->createMediaType();
$mediaType = $this->createMediaType('test');
// Create a media item.
$media = Media::create([
@@ -3,6 +3,7 @@
namespace Drupal\Tests\media\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
/**
* Base class for Media functional tests.
@@ -10,7 +11,7 @@ use Drupal\Tests\BrowserTestBase;
abstract class MediaFunctionalTestBase extends BrowserTestBase {
use MediaFunctionalTestTrait;
use MediaFunctionalTestCreateMediaTypeTrait;
use MediaTypeCreationTrait;
/**
* Modules to enable.
@@ -2,13 +2,24 @@
namespace Drupal\Tests\media\Functional;
use Drupal\media\Entity\MediaType;
@trigger_error(__NAMESPACE__ . '\MediaFunctionalTestCreateMediaTypeTrait is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use \Drupal\Tests\media\Traits\MediaTypeCreationTrait instead. See https://www.drupal.org/node/2981614.', E_USER_DEPRECATED);
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
/**
* Trait with helpers for Media functional tests.
*
* @deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. Use
* \Drupal\Tests\media\Traits\MediaTypeCreationTrait instead.
*
* @see https://www.drupal.org/node/2981614
*/
trait MediaFunctionalTestCreateMediaTypeTrait {
use MediaTypeCreationTrait {
createMediaType as traitCreateMediaType;
}
/**
* Creates a media type.
*
@@ -22,52 +33,7 @@ trait MediaFunctionalTestCreateMediaTypeTrait {
* A newly created media type.
*/
protected function createMediaType(array $values = [], $source = 'test') {
if (empty($values['bundle'])) {
$id = strtolower($this->randomMachineName());
}
else {
$id = $values['bundle'];
}
$values += [
'id' => $id,
'label' => $id,
'source' => $source,
'source_configuration' => [],
'field_map' => [],
'new_revision' => FALSE,
];
$media_type = MediaType::create($values);
$status = $media_type->save();
// @todo Rename to assertSame() when #1945040 is done.
// @see https://www.drupal.org/node/1945040
$this->assertIdentical(SAVED_NEW, $status, 'Media type was created successfully.');
// Ensure that the source field exists.
/** @var \Drupal\media\MediaSourceInterface $source */
$source = $media_type->getSource();
$source_field = $source->getSourceFieldDefinition($media_type);
if (!$source_field) {
$source_field = $source->createSourceField($media_type);
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $source_field->getFieldStorageDefinition();
$storage->save();
$source_field->save();
$media_type
->set('source_configuration', [
'source_field' => $source_field->getName(),
])
->save();
}
// Move source field in form display.
entity_get_form_display('media', $id, 'default')
->setComponent($source_field->getName())
->save();
return $media_type;
return $this->traitCreateMediaType($source, $values);
}
}
@@ -34,16 +34,11 @@ class MediaInstallTest extends BrowserTestBase {
// Uninstall the media module.
$this->container->get('module_installer')->uninstall(['media'], FALSE);
// Install the media module again, through a test module that depends on it.
// Note: We use a test module because in 8.4 the media module is hidden.
// @todo Simplify this in https://www.drupal.org/node/2897028 once it's
// shown again.
$this->drupalGet('/admin/modules');
$page->checkField('modules[media_test_views][enable]');
$page->checkField('modules[media][enable]');
$page->pressButton('Install');
$assert_session->pageTextContains('Some required modules must be enabled');
$page->pressButton('Continue');
$this->assertSession()->pageTextNotContains('could not be moved/copied because a file by that name already exists in the destination directory');
$assert_session->pageTextNotContains('could not be moved/copied because a file by that name already exists in the destination directory');
$assert_session->pageTextContains('Module Media has been enabled');
}
}
@@ -41,24 +41,24 @@ class MediaOverviewPageTest extends MediaFunctionalTestBase {
$assert_session->selectExists('langcode');
$assert_session->buttonExists('Filter');
$header = $assert_session->elementExists('css', 'th#view-thumbnail-target-id-table-column');
$this->assertEquals('Thumbnail', $header->getText());
$this->assertSame('Thumbnail', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-name-table-column');
$this->assertEquals('Media name', $header->getText());
$this->assertSame('Media name', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-bundle-table-column');
$this->assertEquals('Type', $header->getText());
$this->assertSame('Type', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-uid-table-column');
$this->assertEquals('Author', $header->getText());
$this->assertSame('Author', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-status-table-column');
$this->assertEquals('Status', $header->getText());
$this->assertSame('Status', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-changed-table-column');
$this->assertEquals('Updated Sort ascending', $header->getText());
$this->assertSame('Updated Sort ascending', $header->getText());
$header = $assert_session->elementExists('css', 'th#view-operations-table-column');
$this->assertEquals('Operations', $header->getText());
$assert_session->pageTextContains('No content available.');
$this->assertSame('Operations', $header->getText());
$assert_session->pageTextContains('No media available.');
// Create some content for the view.
$media_type1 = $this->createMediaType();
$media_type2 = $this->createMediaType();
$media_type1 = $this->createMediaType('test');
$media_type2 = $this->createMediaType('test');
$media1 = Media::create([
'bundle' => $media_type1->id(),
'name' => 'Media 1',
@@ -97,50 +97,50 @@ class MediaOverviewPageTest extends MediaFunctionalTestBase {
// Media names.
$name1 = $assert_session->elementExists('css', 'td.views-field-name a', $row1);
$this->assertEquals($media1->label(), $name1->getText());
$this->assertSame($media1->label(), $name1->getText());
$name2 = $assert_session->elementExists('css', 'td.views-field-name a', $row2);
$this->assertEquals($media2->label(), $name2->getText());
$this->assertSame($media2->label(), $name2->getText());
$name3 = $assert_session->elementExists('css', 'td.views-field-name a', $row3);
$this->assertEquals($media3->label(), $name3->getText());
$this->assertSame($media3->label(), $name3->getText());
$assert_session->linkByHrefExists('/media/' . $media1->id());
$assert_session->linkByHrefExists('/media/' . $media2->id());
$assert_session->linkByHrefExists('/media/' . $media3->id());
// Media types.
$type_element1 = $assert_session->elementExists('css', 'td.views-field-bundle', $row1);
$this->assertEquals($media_type1->label(), $type_element1->getText());
$this->assertSame($media_type1->label(), $type_element1->getText());
$type_element2 = $assert_session->elementExists('css', 'td.views-field-bundle', $row2);
$this->assertEquals($media_type2->label(), $type_element2->getText());
$this->assertSame($media_type2->label(), $type_element2->getText());
$type_element3 = $assert_session->elementExists('css', 'td.views-field-bundle', $row3);
$this->assertEquals($media_type1->label(), $type_element3->getText());
$this->assertSame($media_type1->label(), $type_element3->getText());
// Media authors.
$author_element1 = $assert_session->elementExists('css', 'td.views-field-uid', $row1);
$this->assertEquals($this->adminUser->getDisplayName(), $author_element1->getText());
$this->assertSame($this->adminUser->getDisplayName(), $author_element1->getText());
$author_element2 = $assert_session->elementExists('css', 'td.views-field-uid', $row2);
$this->assertEquals($this->adminUser->getDisplayName(), $author_element2->getText());
$this->assertSame($this->adminUser->getDisplayName(), $author_element2->getText());
$author_element3 = $assert_session->elementExists('css', 'td.views-field-uid', $row3);
$this->assertEquals($this->nonAdminUser->getDisplayName(), $author_element3->getText());
$this->assertSame($this->nonAdminUser->getDisplayName(), $author_element3->getText());
// Media publishing status.
$status_element1 = $assert_session->elementExists('css', 'td.views-field-status', $row1);
$this->assertEquals('Published', $status_element1->getText());
$this->assertSame('Published', $status_element1->getText());
$status_element2 = $assert_session->elementExists('css', 'td.views-field-status', $row2);
$this->assertEquals('Unpublished', $status_element2->getText());
$this->assertSame('Unpublished', $status_element2->getText());
$status_element3 = $assert_session->elementExists('css', 'td.views-field-status', $row3);
$this->assertEquals('Published', $status_element3->getText());
$this->assertSame('Published', $status_element3->getText());
// Timestamp.
$expected = \Drupal::service('date.formatter')->format($media1->getChangedTime(), 'short');
$changed_element1 = $assert_session->elementExists('css', 'td.views-field-changed', $row1);
$this->assertEquals($expected, $changed_element1->getText());
$this->assertSame($expected, $changed_element1->getText());
// Operations.
$edit_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.edit a', $row1);
$this->assertEquals('Edit', $edit_link1->getText());
$this->assertSame('Edit', $edit_link1->getText());
$assert_session->linkByHrefExists('/media/' . $media1->id() . '/edit');
$delete_link1 = $assert_session->elementExists('css', 'td.views-field-operations li.delete a', $row1);
$this->assertEquals('Delete', $delete_link1->getText());
$this->assertSame('Delete', $delete_link1->getText());
$assert_session->linkByHrefExists('/media/' . $media1->id() . '/delete');
}
@@ -25,7 +25,7 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
$media_storage = $this->container->get('entity.manager')->getStorage('media');
// Create a media type and media item.
$media_type = $this->createMediaType();
$media_type = $this->createMediaType('test');
$media = $media_storage->create([
'bundle' => $media_type->id(),
'name' => 'Unnamed',
@@ -80,7 +80,7 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
$uri = 'temporary://foo.txt';
file_put_contents($uri, $this->randomString(128));
$this->createMediaType(['bundle' => 'file', 'new_revision' => TRUE], 'file');
$this->createMediaType('file', ['id' => 'file', 'new_revision' => TRUE]);
// Create a media item.
$this->drupalGet('/media/add/file');
@@ -88,7 +88,7 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
$page->fillField('Name', 'Foobar');
$page->attachFileToField('File', $this->container->get('file_system')->realpath($uri));
$page->pressButton('Save');
$assert->addressMatches('/^\/media\/[0-9]+$/');
$assert->addressEquals('admin/content/media');
// The media item was just created, so it should only have one revision.
$media = $this->container
@@ -119,7 +119,7 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
public function testImageMediaRevision() {
$assert = $this->assertSession();
$this->createMediaType(['bundle' => 'image', 'new_revision' => TRUE], 'image');
$this->createMediaType('image', ['id' => 'image', 'new_revision' => TRUE]);
/** @var \Drupal\field\FieldConfigInterface $field */
// Disable the alt text field, because this is not a JavaScript test and
@@ -135,9 +135,9 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
$this->drupalGet('/media/add/image');
$page = $this->getSession()->getPage();
$page->fillField('Name', 'Foobar');
$page->attachFileToField('Image', \Drupal::root() . '/core/modules/media/tests/fixtures/example_1.jpeg');
$page->attachFileToField('Image', $this->root . '/core/modules/media/tests/fixtures/example_1.jpeg');
$page->pressButton('Save');
$assert->addressMatches('/^\/media\/[0-9]+$/');
$assert->addressEquals('admin/content/media');
// The media item was just created, so it should only have one revision.
$media = $this->container
@@ -190,8 +190,9 @@ class MediaRevisionTest extends MediaFunctionalTestBase {
$entity_type = $entity->getEntityType();
$count = $this->container
->get('entity.query')
->get($entity_type->id())
->get('entity_type.manager')
->getStorage($entity_type->id())
->getQuery()
->count()
->allRevisions()
->condition($entity_type->getKey('id'), $entity->id())
@@ -0,0 +1,35 @@
<?php
namespace Drupal\Tests\media\Functional;
/**
* Testing the media settings.
*
* @group media
*/
class MediaSettingsTest extends MediaFunctionalTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->createUser(['administer site configuration']));
}
/**
* Test that media warning appears if oEmbed media types exists.
*/
public function testStatusPage() {
$assert_session = $this->assertSession();
$this->drupalGet('admin/reports/status');
$assert_session->pageTextNotContains('It is potentially insecure to display oEmbed content in a frame');
$this->createMediaType('oembed:video');
$this->drupalGet('admin/reports/status');
$assert_session->pageTextContains('It is potentially insecure to display oEmbed content in a frame');
}
}
@@ -19,7 +19,7 @@ class MediaSourceFileTest extends MediaFunctionalTestBase {
$page = $session->getPage();
$assert_session = $this->assertSession();
$media_type = $this->createMediaType([], 'file');
$media_type = $this->createMediaType('file');
$media_type_id = $media_type->id();
$this->assertSame('txt doc docx pdf', FieldConfig::load("media.$media_type_id.field_media_file")->get('settings')['file_extensions']);
@@ -43,7 +43,7 @@ class MediaSourceFileTest extends MediaFunctionalTestBase {
* Ensure source field deletion is not possible.
*/
public function testPreventSourceFieldDeletion() {
$media_type = $this->createMediaType([], 'file');
$media_type = $this->createMediaType('file');
$media_type_id = $media_type->id();
$this->drupalGet("admin/structure/media/manage/$media_type_id/fields/media.$media_type_id.field_media_file/delete");
@@ -22,8 +22,7 @@ class MediaTemplateSuggestionsTest extends MediaFunctionalTestBase {
* Tests template suggestions from media_theme_suggestions_media().
*/
public function testMediaThemeHookSuggestions() {
$media_type = $this->createMediaType([
'new_revision' => FALSE,
$media_type = $this->createMediaType('test', [
'queue_thumbnail_downloads' => FALSE,
]);
@@ -40,7 +39,7 @@ class MediaTemplateSuggestionsTest extends MediaFunctionalTestBase {
$variables['elements'] = $build;
$suggestions = \Drupal::moduleHandler()->invokeAll('theme_suggestions_media', [$variables]);
$this->assertEquals($suggestions, ['media__full', 'media__' . $media_type->id(), 'media__' . $media_type->id() . '__full'], 'Found expected media suggestions.');
$this->assertSame($suggestions, ['media__full', 'media__' . $media_type->id(), 'media__' . $media_type->id() . '__full', 'media__source_' . $media_type->getSource()->getPluginId()], 'Found expected media suggestions.');
}
}
@@ -43,8 +43,7 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$page = $session->getPage();
$assert_session = $this->assertSession();
$media_type = $this->createMediaType([
'new_revision' => FALSE,
$media_type = $this->createMediaType('test', [
'queue_thumbnail_downloads' => FALSE,
]);
@@ -61,14 +60,18 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$source_field = $this->randomString();
$page->fillField('field_media_test[0][value]', $source_field);
$page->pressButton('Save');
$media_id = $this->container->get('entity.query')->get('media')->execute();
$media_id = $this->container->get('entity_type.manager')
->getStorage('media')
->getQuery()
->execute();
$media_id = reset($media_id);
/** @var \Drupal\media\MediaInterface $media */
$media = $this->container->get('entity_type.manager')
->getStorage('media')
->loadUnchanged($media_id);
$this->assertEquals($media->getRevisionLogMessage(), $revision_log_message);
$this->assertEquals($media->getName(), $media_name);
$this->assertSame($media->getRevisionLogMessage(), $revision_log_message);
$this->assertSame($media->getName(), $media_name);
$this->drupalGet('media/' . $media_id);
$assert_session->titleEquals($media_name . ' | Drupal');
// Tests media edit form.
@@ -84,7 +87,8 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$media = $this->container->get('entity_type.manager')
->getStorage('media')
->loadUnchanged($media_id);
$this->assertEquals($media->getName(), $media_name2);
$this->assertSame($media->getName(), $media_name2);
$this->drupalGet('media/' . $media_id);
$assert_session->titleEquals($media_name2 . ' | Drupal');
// Test that there is no empty vertical tabs element, if the container is
@@ -111,12 +115,13 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$page->fillField('name[0][value]', $media_name);
$page->fillField('revision_log_message[0][value]', $revision_log_message);
$page->pressButton('Save');
$this->drupalGet('media/' . $media_id);
$assert_session->titleEquals($media_name . ' | Drupal');
/** @var \Drupal\media\MediaInterface $media */
$media = $this->container->get('entity_type.manager')
->getStorage('media')
->loadUnchanged($media_id);
$this->assertEquals($media->getRevisionLogMessage(), $revision_log_message);
$this->assertSame($media->getRevisionLogMessage(), $revision_log_message);
$this->assertNotEquals($previous_revision_id, $media->getRevisionId());
// Test the status checkbox.
@@ -148,10 +153,10 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$assert_session = $this->assertSession();
// Tests and creates the first media type.
$first_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]);
$first_media_type = $this->createMediaType('test', ['description' => $this->randomMachineName()]);
// Test and create a second media type.
$second_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]);
$second_media_type = $this->createMediaType('test', ['description' => $this->randomMachineName()]);
// Test if media/add displays two media type options.
$this->drupalGet('media/add');
@@ -173,6 +178,7 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$this->drupalGet('media/' . $first_media_item->id());
$assert_session->statusCodeEquals(200);
$assert_session->pageTextContains($first_media_item->getName());
$assert_session->elementsCount('css', '.media--view-mode-full', 1);
// Go to second media item.
$this->drupalGet('media/' . $second_media_item->id());
@@ -280,7 +286,10 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$create_media_types[] = "media_type_$id";
$permissions[] = "create media_type_$id media";
}
$this->createMediaType(['bundle' => "media_type_$id"]);
$this->createMediaType('test', [
'id' => "media_type_$id",
'label' => "media_type_$id",
]);
$media_types["media_type_$id"] = "media_type_$id";
}
@@ -427,6 +436,49 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
}
}
/**
* Tests the redirect URL after creating a media item.
*/
public function testMediaCreateRedirect() {
$session = $this->getSession();
$page = $session->getPage();
$assert_session = $this->assertSession();
$this->createMediaType('test', [
'queue_thumbnail_downloads' => FALSE,
]);
// Test a redirect to the media canonical URL for a user without the 'access
// media overview' permission.
$this->drupalLogin($this->drupalCreateUser([
'view media',
'create media',
]));
$this->drupalGet('media/add');
$page->fillField('name[0][value]', $this->randomMachineName());
$page->fillField('field_media_test[0][value]', $this->randomString());
$page->pressButton('Save');
$media_id = $this->container->get('entity_type.manager')
->getStorage('media')
->getQuery()
->execute();
$media_id = reset($media_id);
$assert_session->addressEquals('media/' . $media_id);
// Test a redirect to the media overview for a user with the 'access media
// overview' permission.
$this->drupalLogin($this->drupalCreateUser([
'view media',
'create media',
'access media overview',
]));
$this->drupalGet('media/add');
$page->fillField('name[0][value]', $this->randomMachineName());
$page->fillField('field_media_test[0][value]', $this->randomString());
$page->pressButton('Save');
$assert_session->addressEquals('admin/content/media');
}
/**
* Asserts that the given texts are present exactly once.
*
@@ -480,7 +532,7 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$this->assertNotEmpty($link);
foreach ($attributes as $attribute => $value) {
$this->assertEquals($link->getAttribute($attribute), $value);
$this->assertSame($link->getAttribute($attribute), $value);
}
}
@@ -509,7 +561,7 @@ class MediaUiFunctionalTest extends MediaFunctionalTestBase {
$this->container->get('module_installer')->uninstall(['views']);
// Create a media type and media item.
$media_type = $this->createMediaType();
$media_type = $this->createMediaType('test');
$media = $media_storage->create([
'bundle' => $media_type->id(),
'name' => 'Unnamed',
@@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\media\Functional;
use Drupal\media\OEmbed\ProviderException;
/**
* Tests the oEmbed provider repository.
*
* @covers \Drupal\media\OEmbed\ProviderRepository
*
* @group media
*/
class ProviderRepositoryTest extends MediaFunctionalTestBase {
/**
* Tests that provider discovery fails if the provider database is empty.
*
* @param string $content
* The expected JSON content of the provider database.
*
* @dataProvider providerEmptyProviderList
*/
public function testEmptyProviderList($content) {
$response = $this->prophesize('\GuzzleHttp\Psr7\Response');
$response->getBody()->willReturn($content);
$client = $this->createMock('\GuzzleHttp\Client');
$client->method('request')->withAnyParameters()->willReturn($response->reveal());
$this->container->set('http_client', $client);
$this->setExpectedException(ProviderException::class, 'Remote oEmbed providers database returned invalid or empty list.');
$this->container->get('media.oembed.provider_repository')->getAll();
}
/**
* Data provider for testEmptyProviderList().
*
* @see ::testEmptyProviderList()
*
* @return array
*/
public function providerEmptyProviderList() {
return [
'empty array' => ['[]'],
'empty string' => [''],
];
}
/**
* Tests that provider discovery fails with a non-existent provider database.
*
* @param string $providers_url
* The URL of the provider database.
* @param string $exception_message
* The expected exception message.
*
* @dataProvider providerNonExistingProviderDatabase
*/
public function testNonExistingProviderDatabase($providers_url, $exception_message) {
$this->config('media.settings')
->set('oembed_providers_url', $providers_url)
->save();
$this->setExpectedException(ProviderException::class, $exception_message);
$this->container->get('media.oembed.provider_repository')->getAll();
}
/**
* Data provider for testEmptyProviderList().
*
* @see ::testEmptyProviderList()
*
* @return array
*/
public function providerNonExistingProviderDatabase() {
return [
[
'http://oembed1.com/providers.json',
'Could not retrieve the oEmbed provider database from http://oembed1.com/providers.json',
],
[
'http://oembed.com/providers1.json',
'Could not retrieve the oEmbed provider database from http://oembed.com/providers1.json',
],
];
}
}
@@ -0,0 +1,72 @@
<?php
namespace Drupal\Tests\media\Functional;
use Drupal\media\OEmbed\Resource;
use Drupal\Tests\media\Traits\OEmbedTestTrait;
/**
* Tests the oEmbed resource fetcher service.
*
* @coversDefaultClass \Drupal\media\OEmbed\ResourceFetcher
*
* @group media
*/
class ResourceFetcherTest extends MediaFunctionalTestBase {
use OEmbedTestTrait;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->useFixtureProviders();
$this->lockHttpClientToFixtures();
}
/**
* Data provider for testFetchResource().
*
* @return array
*/
public function providerFetchResource() {
return [
'JSON resource' => [
'video_vimeo.json',
'Vimeo',
'Drupal Rap Video - Schipulcon09',
],
'XML resource' => [
'video_collegehumor.xml',
'CollegeHumor',
"Let's Not Get a Drink Sometime",
],
];
}
/**
* Tests resource fetching.
*
* @param string $resource_url
* The URL of the resource to fetch, relative to the base URL.
* @param string $provider_name
* The expected name of the resource provider.
* @param string $title
* The expected title of the resource.
*
* @covers ::fetchResource
*
* @dataProvider providerFetchResource
*/
public function testFetchResource($resource_url, $provider_name, $title) {
/** @var \Drupal\media\OEmbed\Resource $resource */
$resource = $this->container->get('media.oembed.resource_fetcher')
->fetchResource($resource_url);
$this->assertInstanceOf(Resource::class, $resource);
$this->assertSame($provider_name, $resource->getProvider()->getName());
$this->assertSame($title, $resource->getTitle());
}
}
@@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\media\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class MediaJsonAnonTest extends MediaResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}
@@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\media\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class MediaJsonBasicAuthTest extends MediaResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}
@@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\media\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class MediaJsonCookieTest extends MediaResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}
@@ -0,0 +1,431 @@
<?php
namespace Drupal\Tests\media\Functional\Rest;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\media\Entity\Media;
use Drupal\media\Entity\MediaType;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\RoleInterface;
use GuzzleHttp\RequestOptions;
abstract class MediaResourceTestBase extends EntityResourceTestBase {
use BcTimestampNormalizerUnixTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['media'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'media';
/**
* @var \Drupal\media\MediaInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'changed' => NULL,
];
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['view media']);
break;
case 'POST':
$this->grantPermissionsToTestedRole(['create camelids media', 'access content']);
break;
case 'PATCH':
$this->grantPermissionsToTestedRole(['edit any camelids media']);
// @todo Remove this in https://www.drupal.org/node/2824851.
$this->grantPermissionsToTestedRole(['access content']);
break;
case 'DELETE':
$this->grantPermissionsToTestedRole(['delete any camelids media']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
if (!MediaType::load('camelids')) {
// Create a "Camelids" media type.
$media_type = MediaType::create([
'name' => 'Camelids',
'id' => 'camelids',
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
'source' => 'file',
]);
$media_type->save();
// Create the source field.
$source_field = $media_type->getSource()->createSourceField($media_type);
$source_field->getFieldStorageDefinition()->save();
$source_field->save();
$media_type
->set('source_configuration', [
'source_field' => $source_field->getName(),
])
->save();
}
// Create a file to upload.
$file = File::create([
'uri' => 'public://llama.txt',
]);
$file->setPermanent();
$file->save();
// Create a "Llama" media item.
$media = Media::create([
'bundle' => 'camelids',
'field_media_file' => [
'target_id' => $file->id(),
],
]);
$media
->setName('Llama')
->setPublished()
->setCreatedTime(123456789)
->setOwnerId(static::$auth ? $this->account->id() : 0)
->setRevisionUserId(static::$auth ? $this->account->id() : 0)
->save();
return $media;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$file = File::load(1);
$thumbnail = File::load(2);
$author = User::load($this->entity->getOwnerId());
return [
'mid' => [
[
'value' => 1,
],
],
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
'vid' => [
[
'value' => 1,
],
],
'langcode' => [
[
'value' => 'en',
],
],
'bundle' => [
[
'target_id' => 'camelids',
'target_type' => 'media_type',
'target_uuid' => MediaType::load('camelids')->uuid(),
],
],
'name' => [
[
'value' => 'Llama',
],
],
'field_media_file' => [
[
'description' => NULL,
'display' => NULL,
'target_id' => (int) $file->id(),
'target_type' => 'file',
'target_uuid' => $file->uuid(),
'url' => $file->url(),
],
],
'thumbnail' => [
[
'alt' => 'Thumbnail',
'width' => 180,
'height' => 180,
'target_id' => (int) $thumbnail->id(),
'target_type' => 'file',
'target_uuid' => $thumbnail->uuid(),
'title' => 'Llama',
'url' => $thumbnail->url(),
],
],
'status' => [
[
'value' => TRUE,
],
],
'created' => [
$this->formatExpectedTimestampItemValues(123456789),
],
'changed' => [
$this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
],
'revision_created' => [
$this->formatExpectedTimestampItemValues((int) $this->entity->getRevisionCreationTime()),
],
'default_langcode' => [
[
'value' => TRUE,
],
],
'uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'revision_user' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'revision_log_message' => [],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'bundle' => [
[
'target_id' => 'camelids',
],
],
'name' => [
[
'value' => 'Dramallama',
],
],
'field_media_file' => [
[
'description' => NULL,
'display' => NULL,
'target_id' => 3,
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPatchEntity() {
return array_diff_key($this->getNormalizedPostEntity(), ['field_media_file' => TRUE]);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET';
return "The 'view media' permission is required and the media item must be published.";
case 'POST':
return "The following permissions are required: 'administer media' OR 'create media' OR 'create camelids media'.";
case 'PATCH':
return "The following permissions are required: 'update any media' OR 'update own media' OR 'camelids: edit any media' OR 'camelids: edit own media'.";
case 'DELETE':
return "The following permissions are required: 'delete any media' OR 'delete own media' OR 'camelids: delete any media' OR 'camelids: delete own media'.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
/**
* {@inheritdoc}
*/
public function testPost() {
$file_storage = $this->container->get('entity_type.manager')->getStorage('file');
// Step 1: upload file, results in File entity marked temporary.
$this->uploadFile();
$file = $file_storage->loadUnchanged(3);
$this->assertTrue($file->isTemporary());
$this->assertFalse($file->isPermanent());
// Step 2: create Media entity using the File, makes File entity permanent.
parent::testPost();
$file = $file_storage->loadUnchanged(3);
$this->assertFalse($file->isTemporary());
$this->assertTrue($file->isPermanent());
}
/**
* This duplicates some of the 'file_upload' REST resource plugin test
* coverage, to be able to test it on a concrete use case.
*/
protected function uploadFile() {
// Enable the 'file_upload' REST resource for the current format + auth.
$this->resourceConfigStorage->create([
'id' => 'file.upload',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['POST'],
'formats' => [static::$format],
'authentication' => isset(static::$auth) ? [static::$auth] : [],
],
'status' => TRUE,
])->save();
$this->refreshTestStateAfterRestConfigChange();
$this->initAuthentication();
// POST to create a File entity.
$url = Url::fromUri('base:file/upload/media/camelids/field_media_file');
$url->setOption('query', ['_format' => static::$format]);
$request_options = [];
$request_options[RequestOptions::HEADERS] = [
// Set the required (and only accepted) content type for the request.
'Content-Type' => 'application/octet-stream',
// Set the required Content-Disposition header for the file name.
'Content-Disposition' => 'file; filename="drupal rocks 🤘.txt"',
];
$request_options[RequestOptions::BODY] = 'Drupal is the best!';
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
// Grant necessary permission, retry.
$this->grantPermissionsToTestedRole(['create camelids media']);
$response = $this->request('POST', $url, $request_options);
$this->assertSame(201, $response->getStatusCode());
$expected = $this->getExpectedNormalizedFileEntity();
static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
// To still run the complete test coverage for POSTing a Media entity, we
// must revoke the additional permissions that we granted.
$role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::AUTHENTICATED_ID);
$role->revokePermission('create camelids media');
$role->trustData()->save();
}
/**
* Gets the expected file entity.
*
* @return array
* The expected normalized data array.
*/
protected function getExpectedNormalizedFileEntity() {
$file = File::load(3);
$owner = static::$auth ? $this->account : User::load(0);
return [
'fid' => [
[
'value' => 3,
],
],
'uuid' => [
[
'value' => $file->uuid(),
],
],
'langcode' => [
[
'value' => 'en',
],
],
'uid' => [
[
'target_id' => (int) $owner->id(),
'target_type' => 'user',
'target_uuid' => $owner->uuid(),
'url' => base_path() . 'user/' . $owner->id(),
],
],
'filename' => [
[
'value' => 'drupal rocks 🤘.txt',
],
],
'uri' => [
[
'value' => 'public://' . date('Y-m') . '/drupal rocks 🤘.txt',
'url' => base_path() . $this->siteDirectory . '/files/' . date('Y-m') . '/drupal%20rocks%20%F0%9F%A4%98.txt',
],
],
'filemime' => [
[
'value' => 'text/plain',
],
],
'filesize' => [
[
'value' => 19,
],
],
'status' => [
[
'value' => FALSE,
],
],
'created' => [
$this->formatExpectedTimestampItemValues($file->getCreatedTime()),
],
'changed' => [
$this->formatExpectedTimestampItemValues($file->getChangedTime()),
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->addCacheTags(['media:1']);
}
}

Some files were not shown because too many files have changed in this diff Show More