Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,182 @@
<?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 "{format}" 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 = $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.
*
* In most situations this function should not be used. Your are probably
* looking for \Drupal\media\OEmbed\UrlResolver::getResourceUrl(), because it
* is alterable and also cached.
*
* @param string $url
* The canonical media URL.
*
* @return string
* URL of the oEmbed endpoint.
*
* @see \Drupal\media\OEmbed\UrlResolver::getResourceUrl()
*/
public function buildResourceUrl($url) {
$query = ['url' => $url];
return $this->getUrl() . '?' . UrlHelper::buildQuery($query);
}
}

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) {
$this->name = $name;
if (!UrlHelper::isValid($url, TRUE) || !UrlHelper::isExternal($url)) {
throw new ProviderException('Provider @name does not define a valid external URL.', $this);
}
$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) {
// 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;
}
}

View File

@ -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 \Throwable $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, ?Provider $provider = NULL, ?\Throwable $previous = NULL) {
$this->provider = $provider;
$message = str_replace('@name', $provider ? $provider->getName() : '<unknown>', $message);
parent::__construct($message, 0, $previous);
}
}

View File

@ -0,0 +1,161 @@
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
/**
* Retrieves and caches information about oEmbed providers.
*/
class ProviderRepository implements ProviderRepositoryInterface {
/**
* 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;
/**
* The key-value store.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* 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\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key-value store factory.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger channel factory.
* @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, KeyValueFactoryInterface $key_value_factory, LoggerChannelFactoryInterface $logger_factory, int $max_age = 604800) {
$this->httpClient = $http_client;
$this->providersUrl = $config_factory->get('media.settings')->get('oembed_providers_url');
$this->time = $time;
$this->maxAge = $max_age;
$this->keyValue = $key_value_factory->get('media');
$this->logger = $logger_factory->get('media');
}
/**
* {@inheritdoc}
*/
public function getAll() {
$current_time = $this->time->getCurrentTime();
$stored = $this->keyValue->get('oembed_providers');
// If we have stored data that hasn't yet expired, return that. We need to
// store the data in a key-value store because, if the remote provider
// database is unavailable, we'd rather return stale data than throw an
// exception. This means we cannot use a normal cache backend or expirable
// key-value store, since those could delete the stale data at any time.
if ($stored && $stored['expires'] > $current_time) {
return $stored['data'];
}
try {
$response = $this->httpClient->request('GET', $this->providersUrl);
}
catch (ClientExceptionInterface $e) {
if (isset($stored['data'])) {
// Use the stale data to fall back gracefully, but warn site
// administrators that we used stale data.
$this->logger->warning('Remote oEmbed providers could not be retrieved due to error: @error. Using previously stored data. This may contain out of date information.', [
'@error' => $e->getMessage(),
]);
return $stored['data'];
}
// We have no previous data and the request failed.
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)) {
if (isset($stored['data'])) {
// Use the stale data to fall back gracefully, but as above, warn site
// administrators that we used stale data.
$this->logger->warning('Remote oEmbed providers database returned invalid or empty list. Using previously stored data. This may contain out of date information.');
return $stored['data'];
}
// We have no previous data and the current data is corrupt.
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) {
// Skip invalid providers, but log the exception message to help with
// debugging.
$this->logger->warning($e->getMessage());
}
}
$this->keyValue->set('oembed_providers', [
'data' => $keyed_providers,
'expires' => $current_time + $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];
}
}

View File

@ -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);
}

View File

@ -0,0 +1,529 @@
<?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
* (optional) 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 = NULL, ?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
* (optional) 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 = NULL, ?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
* (optional) 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 = 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 = 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
* width.
*/
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
* height.
*/
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|null $width
* The width of the resource.
* @param int|null $height
* The height of the resource.
*
* @throws \InvalidArgumentException
* If either $width or $height are not numbers greater than zero.
*/
protected function setDimensions($width, $height) {
if ((isset($width) && $width <= 0) || (isset($height) && $height <= 0)) {
throw new \InvalidArgumentException('The dimensions must be NULL or numbers greater than zero.');
}
$this->width = isset($width) ? (int) $width : NULL;
$this->height = isset($height) ? (int) $height : NULL;
}
}

View File

@ -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 \Throwable $previous
* (optional) The previous exception, if any.
*/
public function __construct($message, $url, array $data = [], ?\Throwable $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;
}
}

View File

@ -0,0 +1,230 @@
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\RequestOptions;
use Psr\Http\Client\ClientExceptionInterface;
// cspell:ignore nocdata
/**
* Fetches and caches oEmbed resources.
*/
class ResourceFetcher implements ResourceFetcherInterface {
/**
* Constructs a ResourceFetcher object.
*
* @param \GuzzleHttp\ClientInterface $httpClient
* The HTTP client.
* @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers
* The oEmbed provider repository service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cacheBackend
* The cache backend.
* @param int $timeout
* The length of time to wait for the request before the request
* should time out.
*/
public function __construct(
protected ClientInterface $httpClient,
protected ProviderRepositoryInterface $providers,
protected CacheBackendInterface $cacheBackend,
protected int $timeout = 5,
) {
}
/**
* {@inheritdoc}
*/
public function fetchResource($url) {
$cache_id = "media:oembed_resource:$url";
$cached = $this->cacheBackend->get($cache_id);
if ($cached) {
return $this->createResource($cached->data, $url);
}
try {
$response = $this->httpClient->request('GET', $url, [
RequestOptions::TIMEOUT => $this->timeout,
]);
}
catch (ClientExceptionInterface $e) {
throw new ResourceException('Could not retrieve the oEmbed resource.', $url, [], $e);
}
[$format] = $response->getHeader('Content-Type');
$content = (string) $response->getBody();
if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
$data = $this->parseResourceXml($content, $url);
}
// By default, try to parse the resource data as JSON.
else {
$data = Json::decode($content);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ResourceException('Error decoding oEmbed resource: ' . json_last_error_msg(), $url);
}
}
if (empty($data) || !is_array($data)) {
throw new ResourceException('The oEmbed resource could not be decoded.', $url);
}
$this->cacheBackend->set($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);
}
}
/**
* Parses XML resource data.
*
* @param string $data
* The raw XML for the resource.
* @param string $url
* The resource URL.
*
* @return array
* The parsed resource data.
*
* @throws \Drupal\media\OEmbed\ResourceException
* If the resource data could not be parsed.
*/
protected function parseResourceXml($data, $url) {
// Enable userspace error handling.
$was_using_internal_errors = libxml_use_internal_errors(TRUE);
libxml_clear_errors();
$content = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
// Restore the previous error handling behavior.
libxml_use_internal_errors($was_using_internal_errors);
$error = libxml_get_last_error();
if ($error) {
libxml_clear_errors();
throw new ResourceException($error->message, $url);
}
elseif ($content === FALSE) {
throw new ResourceException('The fetched resource could not be parsed.', $url);
}
// Convert XML to JSON so that the parsed resource has a consistent array
// structure, regardless of any XML attributes or quirks of the XML parser.
$data = Json::encode($content);
return Json::decode($data);
}
}

View File

@ -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);
}

View File

@ -0,0 +1,210 @@
<?php
namespace Drupal\media\OEmbed;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use GuzzleHttp\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
// cspell:ignore omitscript
/**
* Converts oEmbed media URLs into endpoint-specific resource URLs.
*/
class UrlResolver implements UrlResolverInterface {
/**
* 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 = [];
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;
/**
* 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
* The cache backend.
*/
public function __construct(ProviderRepositoryInterface $providers, ResourceFetcherInterface $resource_fetcher, ClientInterface $http_client, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend) {
$this->providers = $providers;
$this->resourceFetcher = $resource_fetcher;
$this->httpClient = $http_client;
$this->moduleHandler = $module_handler;
$this->cacheBackend = $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.
*/
protected function discoverResourceUrl($url) {
try {
$response = $this->httpClient->get($url);
}
catch (ClientExceptionInterface) {
return FALSE;
}
$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_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->cacheBackend->get($cache_id);
if ($cached) {
$this->urlCache[$url] = $cached->data;
return $this->urlCache[$url];
}
$provider = $this->getProviderByUrl($url);
$resource_url = $this->getEndpointMatchingUrl($url, $provider);
$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->cacheBackend->set($cache_id, $resource_url);
return $resource_url;
}
/**
* For the given media item URL find an endpoint with schemes that match.
*
* @param string $url
* The media URL used to lookup the matching endpoint.
* @param \Drupal\media\OEmbed\Provider $provider
* The oEmbed provider for the asset.
*
* @return string
* The resource URL.
*/
protected function getEndpointMatchingUrl($url, Provider $provider) {
$endpoints = $provider->getEndpoints();
$resource_url = reset($endpoints)->buildResourceUrl($url);
foreach ($endpoints as $endpoint) {
if ($endpoint->matchUrl($url)) {
$resource_url = $endpoint->buildResourceUrl($url);
break;
}
}
return $resource_url ?? reset($endpoints)->buildResourceUrl($url);
}
}

View File

@ -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);
}