config('itk_video.settings');
+
+ $form['general'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('General Settings'),
+ '#collapsible' => FALSE,
+ ];
+
+ $form['general']['respect_cookie_information'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Whether to respect cookie information'),
+ '#description' => $this->t('If enabled the video will respect boundaries provided by cookie information https://cookieinformation.com/, And the users cookie consent.'),
+ '#default_value' => $config->get('respect_cookie_information') ?? TRUE,
+ ];
+
+ $form['providers'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Enabled Video Providers'),
+ '#collapsible' => FALSE,
+ ];
+
+ // Get available providers from the service.
+ $supportedProviders = SupportedVideoProvider::getConfig();
+ $availableProviders = [];
+ foreach ($supportedProviders as $provider => $providerConfig) {
+ $availableProviders[$provider] = $providerConfig['label'];
+ }
+ $enabledProviders = $config->get('providers_status') ?? [];
+
+ $form['providers']['providers_status'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Enable providers'),
+ '#options' => $availableProviders,
+ '#default_value' => array_keys(array_filter($enabledProviders)),
+ '#description' => $this->t('Select the video providers that should be available for use.'),
+ ];
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $config = $this->config('itk_video.settings');
+
+ $config->set('respect_cookie_information', $form_state->getValue('respect_cookie_information'));
+
+ $providers = $form_state->getValue('providers_status');
+ $providersStatus = array_map(function ($enabled) {
+ return (bool) $enabled;
+ }, $providers);
+
+ $config->set('providers_status', $providersStatus);
+ $config->save();
+
+ parent::submitForm($form, $form_state);
+ }
+
+}
diff --git a/src/Plugin/Field/FieldFormatter/VideoFormatter.php b/src/Plugin/Field/FieldFormatter/VideoFormatter.php
new file mode 100644
index 0000000..7bc924c
--- /dev/null
+++ b/src/Plugin/Field/FieldFormatter/VideoFormatter.php
@@ -0,0 +1,257 @@
+get('path.validator'),
+ $configuration['third_party_settings'],
+ $container->get('media.oembed.url_resolver'),
+ $container->get('http_client'),
+ $container->get('config.factory')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \GuzzleHttp\Exception\GuzzleException
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode): array {
+ $elements['#attached']['library'][] = 'itk_video/video';
+
+ foreach ($items as $delta => $item) {
+
+ if (!empty($item->getUrl()->toString())) {
+ $elements[$delta] = [
+ '#prefix' => '',
+ '#type' => 'inline_template',
+ '#template' => $this->createVideoIframe($item),
+ '#suffix' => '
',
+ ];
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * Render a video from an embed url or iframe.
+ *
+ * @param \Drupal\itk_video\Plugin\Field\FieldType\Video $value
+ * The video field type.
+ *
+ * @return string|null
+ * The rendered html.
+ *
+ * @throws \GuzzleHttp\Exception\GuzzleException
+ * An exception if guzzle fails.
+ */
+ private function createVideoIframe(Video $value): ?string {
+ $settings = $this->configFactory->get('itk_video.settings');
+
+ // Set string.
+ $url = $value->getUrl()->toString();
+
+ $video = $this->createVideoFromUrl($url, $settings);
+
+ if ($settings->get('respect_cookie_information')) {
+ $video = $this->applyCookieConsent($video, $value);
+ }
+
+ return $video['iframe'] ?? NULL;
+ }
+
+ /**
+ * Create an array containing video information.
+ *
+ * @param string $text
+ * The text input to create video from.
+ * @param \Drupal\Core\Config\ImmutableConfig $settings
+ * The settings for the field.
+ *
+ * @return array
+ * The resulting video array.
+ *
+ * @throws \GuzzleHttp\Exception\GuzzleException
+ * Exception if oembed fails.
+ */
+ private function createVideoFromUrl(string $text, ImmutableConfig $settings): array {
+ $video = [];
+ if (filter_var($text, FILTER_VALIDATE_URL)) {
+ $supportedProviders = SupportedVideoProvider::getConfig();
+ $providersStatus = $settings->get('providers_status');
+
+ $url = parse_url($text);
+ if (in_array($url['host'], SupportedVideoProvider::getProviderHosts())) {
+ $video['host'] = $url['host'];
+
+ $providerKey = $this->getProviderIdFromHost($supportedProviders, $video['host']);
+
+ if (!empty($providerKey && $providersStatus[$providerKey])) {
+ // Use oembed to create iframe if possible.
+ if ('oembed' === $supportedProviders[$providerKey]['type']) {
+ try {
+ $url = $this->urlResolver->getResourceUrl($text);
+ $request = $this->httpClient->request('GET', $url);
+ $status = $request->getStatusCode();
+ if (Response::HTTP_OK === $status) {
+ $video['oembed'] = Json::decode($request->getBody()->getContents());
+ $video['iframe'] = $video['oembed']['html'];
+ }
+ }
+ catch (\Exception $e) {
+ if ('No matching provider found.' === $e->getMessage()) {
+ $this->messenger->addWarning($this->t('Could not build video.'));
+ $video = [];
+ }
+ }
+ }
+ // If oembed is not an option create iframe from a url.
+ elseif ('custom' === $supportedProviders[$providerKey]['type']) {
+ $video['custom']['src'] = $text;
+ $video['iframe'] = '';
+ }
+ }
+ }
+ }
+
+ return $video;
+ }
+
+ /**
+ * Change iframe to support cookie consent.
+ *
+ * @param array $videoArray
+ * The video array.
+ * @param \Drupal\itk_video\Plugin\Field\FieldType\Video $fieldValue
+ * The video array.
+ *
+ * @return array
+ * The modified video array.
+ */
+ private function applyCookieConsent(array $videoArray, Video $fieldValue): array {
+ $supportedProviders = SupportedVideoProvider::getConfig();
+ if (in_array($videoArray['host'], SupportedVideoProvider::getProviderHosts())) {
+ $providerKey = $this->getProviderIdFromHost($supportedProviders, $videoArray['host']);
+ $requiredCookies = $supportedProviders[$providerKey]['requiredCookies'];
+
+ if (!empty($requiredCookies) && isset($videoArray['iframe'])) {
+ $videoArray['iframe'] = $this->consentifyOembed($videoArray['iframe'], $requiredCookies);
+ $blockedText = $this->t('Accept cookies to view this video:');
+ if ($fieldValue->title) {
+ $blockedText .= '
"' . $fieldValue->title . '"';
+ }
+ $videoArray['iframe'] = $videoArray['iframe'] . ' ' . $blockedText . '
';
+ }
+ }
+
+ return $videoArray;
+ }
+
+ /**
+ * Get provider id from supplied host.
+ *
+ * @param array $supportedProviders
+ * A list of all supported providers.
+ * @param string $host
+ * The host to get the provider id from.
+ *
+ * @return string|null
+ * A provider id or null if not found.
+ */
+ private function getProviderIdFromHost(array $supportedProviders, string $host): ?string {
+ foreach ($supportedProviders as $key => $value) {
+ if ($value['host'] === $host) {
+ return $key;
+ }
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Apply cookie consent attribute changes to iframe.
+ *
+ * @param string $content
+ * The original iframe content.
+ * @param string $requiredCookies
+ * The required cookies.
+ *
+ * @return false|string
+ * The resulting iframe content.
+ */
+ private function consentifyOembed(string $content, string $requiredCookies): bool|string {
+ $document = new \DOMDocument();
+ $document->loadHTML($content);
+ $iframe = $document->getElementsByTagName('iframe')->item(0);
+ if ($iframe && $iframe->hasAttribute('src')) {
+ $src = $iframe->getAttribute('src');
+ $iframe->setAttribute('src', '');
+ $iframe->setAttribute('data-consent-src', $src);
+ $iframe->setAttribute('data-category-consent', $requiredCookies);
+ }
+ else {
+ return 'Iframe src not found';
+ }
+
+ return $document->saveHtml($iframe);
+ }
+
+}
diff --git a/src/Plugin/Field/FieldType/Video.php b/src/Plugin/Field/FieldType/Video.php
new file mode 100644
index 0000000..5f005d5
--- /dev/null
+++ b/src/Plugin/Field/FieldType/Video.php
@@ -0,0 +1,42 @@
+ DRUPAL_REQUIRED,
+ 'link_type' => LinkItemInterface::LINK_EXTERNAL,
+ ] + parent::defaultFieldSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
+ $element = [];
+
+ return $element;
+ }
+
+}
diff --git a/src/Plugin/Field/FieldWidget/VideoWidget.php b/src/Plugin/Field/FieldWidget/VideoWidget.php
new file mode 100644
index 0000000..c33486e
--- /dev/null
+++ b/src/Plugin/Field/FieldWidget/VideoWidget.php
@@ -0,0 +1,186 @@
+get('config.factory'),
+ $container->get('current_user'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ // The contents of this method are in large parts copied from the parent
+ // class.
+ $config = $this->configFactory->get('itk_video.settings')->get('providers_status');
+ $enabledProviders = [];
+ foreach ($config as $provider => $enabled) {
+ if ($enabled) {
+ $enabledProviders[$provider] = SupportedVideoProvider::getConfig()[$provider]['label'];
+ }
+ }
+
+ /** @var \Drupal\link\LinkItemInterface $item */
+ $item = $items[$delta];
+
+ $display_uri = NULL;
+ if (!$item->isEmpty()) {
+ try {
+ // The current field value could have been entered by a different user.
+ // However, if it is inaccessible to the current user, do not display it
+ // to them.
+ if ($this->account->hasPermission('link to any page') || $item->getUrl()->access()) {
+ $display_uri = static::getUriAsDisplayableString($item->getUrl()->getUri());
+ }
+ }
+ catch (\InvalidArgumentException $e) {
+ // If $item->uri is invalid, show value as is, so the user can see what
+ // to edit.
+ // @todo Add logging here in https://www.drupal.org/project/drupal/issues/3348020
+ $display_uri = $item->getUrl()->getUri();
+ }
+ }
+ $element['uri'] = [
+ '#type' => 'url',
+ '#title' => $this->t('Video URL'),
+ '#placeholder' => $this->getSetting('placeholder_url'),
+ '#default_value' => $display_uri,
+ '#element_validate' => [[static::class, 'validateUriElement']],
+ '#maxlength' => 2048,
+ '#required' => $element['#required'],
+ '#link_type' => $this->getFieldSetting('link_type'),
+ ];
+
+ $providersText = $enabledProviders ? '' . $this->t('Supported providers: @providers', ['@providers' => implode(', ', $enabledProviders)]) . '
' : '';
+ $element['uri']['#description'] = $element['#description'] . $providersText;
+
+ // Make uri required on the front-end when title filled-in.
+ if (!$this->isDefaultValueWidget($form_state) && $this->getFieldSetting('title') !== DRUPAL_DISABLED && !$element['uri']['#required']) {
+ $parents = $element['#field_parents'];
+ $parents[] = $this->fieldDefinition->getName();
+ $selector = $root = array_shift($parents);
+ if ($parents) {
+ $selector = $root . '[' . implode('][', $parents) . ']';
+ }
+
+ $element['uri']['#states']['required'] = [
+ ':input[name="' . $selector . '[' . $delta . '][title]"]' => ['filled' => TRUE],
+ ];
+ }
+
+ $element['title'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Video title'),
+ '#placeholder' => $this->getSetting('placeholder_title'),
+ '#default_value' => $items[$delta]->title ?? NULL,
+ '#maxlength' => 255,
+ '#required' => !$this->isDefaultValueWidget($form_state) && $element['#required'],
+ ];
+ // Post-process the title field to make it conditionally required if URL is
+ // non-empty. Omit the validation on the field edit form, since the field
+ // settings cannot be saved otherwise.
+ //
+ // Validate that title field is filled out (regardless of uri) when it is a
+ // required field.
+ if (!$this->isDefaultValueWidget($form_state)) {
+ $element['#element_validate'][] = [static::class, 'validateTitleElement'];
+ $element['#element_validate'][] = [static::class, 'validateTitleNoLink'];
+
+ if (!$element['title']['#required']) {
+ // Make title required on the front-end when URI filled-in.
+ $parents = $element['#field_parents'];
+ $parents[] = $this->fieldDefinition->getName();
+ $selector = $root = array_shift($parents);
+ if ($parents) {
+ $selector = $root . '[' . implode('][', $parents) . ']';
+ }
+
+ $element['title']['#states']['required'] = [
+ ':input[name="' . $selector . '[' . $delta . '][uri]"]' => ['filled' => TRUE],
+ ];
+ }
+ }
+
+ // Exposing the attributes array in the widget is left for alternate and
+ // more advanced field widgets.
+ $element['attributes'] = [
+ '#type' => 'value',
+ '#tree' => TRUE,
+ '#value' => !empty($items[$delta]->options['attributes']) ? $items[$delta]->options['attributes'] : [],
+ '#attributes' => ['class' => ['link-field-widget-attributes']],
+ ];
+
+ // If cardinality is 1, ensure a proper label is output for the field.
+ if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) {
+ $element += [
+ '#type' => 'fieldset',
+ ];
+ }
+
+ return $element;
+ }
+
+}
diff --git a/src/SupportedVideoProvider.php b/src/SupportedVideoProvider.php
new file mode 100644
index 0000000..aa9a660
--- /dev/null
+++ b/src/SupportedVideoProvider.php
@@ -0,0 +1,55 @@
+value => [
+ 'label' => 'Video Tool',
+ 'host' => 'media.videotool.dk',
+ // Use custom code to create iframe.
+ 'type' => 'custom',
+ // Cookies that require acceptance from user. CookieInformation syntax.
+ 'requiredCookies' => 'cookie_cat_statistic',
+ ],
+ self::VIMEO->value => [
+ 'label' => 'Vimeo',
+ 'host' => 'vimeo.com',
+ // Use oembed endpoint when defining iframe.
+ 'type' => 'oembed',
+ // Cookies that require acceptance from user. CookieInformation syntax.
+ 'requiredCookies' => 'cookie_cat_statistic cookie_cat_marketing',
+ ],
+ ];
+ }
+
+ /**
+ * Get provider Urls.
+ *
+ * @return array
+ * A list of provider urls.
+ */
+ public static function getProviderHosts(): array {
+ $providerHosts = [];
+ $providers = SupportedVideoProvider::getConfig();
+ foreach ($providers as $config) {
+ $providerHosts[] = $config['host'];
+ }
+
+ return $providerHosts;
+ }
+
+}