diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml new file mode 100644 index 0000000..483da6e --- /dev/null +++ b/.github/workflows/changelog.yaml @@ -0,0 +1,29 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/changelog.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Changelog +### +### Checks that changelog has been updated + +name: Changelog + +on: + pull_request: + +jobs: + changelog: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- CHANGELOG.md && exit 1 || exit 0 diff --git a/.github/workflows/composer.yaml b/.github/workflows/composer.yaml new file mode 100644 index 0000000..6c3a30c --- /dev/null +++ b/.github/workflows/composer.yaml @@ -0,0 +1,80 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/composer.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Composer +### +### Validates composer.json and checks that it's normalized. +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [ergebnis/composer-normalize](https://github.com/ergebnis/composer-normalize) +### is a dev requirement in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev ergebnis/composer-normalize +### ``` +### +### Normalize `composer.json` by running +### +### ``` shell +### docker compose run --rm phpfpm composer normalize +### ``` + +name: Composer + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + composer-validate: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer validate --strict + + composer-normalized: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm composer normalize --dry-run + + composer-audit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer audit diff --git a/.github/workflows/markdown.yaml b/.github/workflows/markdown.yaml new file mode 100644 index 0000000..f8bcf09 --- /dev/null +++ b/.github/workflows/markdown.yaml @@ -0,0 +1,44 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/markdown.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Markdown +### +### Lints Markdown files (`**/*.md`) in the project. +### +### [markdownlint-cli configuration +### files](https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration), +### `.markdownlint.jsonc` and `.markdownlintignore`, control what is actually +### linted and how. +### +### #### Assumptions +### +### 1. A docker compose service named `markdownlint` for running `markdownlint` +### (from +### [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli)) +### exists. + +name: Markdown + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + markdown-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm markdownlint markdownlint '**/*.md' diff --git a/.github/workflows/php.yaml b/.github/workflows/php.yaml new file mode 100644 index 0000000..95c22ed --- /dev/null +++ b/.github/workflows/php.yaml @@ -0,0 +1,59 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/drupal-module/php.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Drupal module PHP +### +### Checks that PHP code adheres to the [Drupal coding +### standards](https://www.drupal.org/docs/develop/standards). +### +### #### Assumptions +### +### 1. A docker compose service named `phpfpm` can be run and `composer` can be +### run inside the `phpfpm` service. +### 2. [drupal/coder](https://www.drupal.org/project/coder) is a dev requirement +### in `composer.json`: +### +### ``` shell +### docker compose run --rm phpfpm composer require --dev drupal/coder +### ``` +### +### Clean up and check code by running +### +### ``` shell +### docker compose run --rm phpfpm vendor/bin/phpcbf +### docker compose run --rm phpfpm vendor/bin/phpcs +### ``` +### +### > [!NOTE] +### > The template adds `.phpcs.xml.dist` as [a configuration file for +### > PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file) +### > and this makes it possible to override the actual configuration used in a +### > project by adding a more important configuration file, e.g. `.phpcs.xml`. + +name: PHP + +env: + COMPOSE_USER: root + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + coding-standards: + name: PHP - Check Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm phpfpm composer install + docker compose run --rm phpfpm vendor/bin/phpcs diff --git a/.github/workflows/styles.yaml b/.github/workflows/styles.yaml new file mode 100644 index 0000000..9e75d75 --- /dev/null +++ b/.github/workflows/styles.yaml @@ -0,0 +1,37 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/drupal-module/styles.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### Drupal module Styles (CSS and SCSS) +### +### Validates styles files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. + +name: Styles + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + styles-lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier 'css/**/*.css' --check diff --git a/.github/workflows/yaml.yaml b/.github/workflows/yaml.yaml new file mode 100644 index 0000000..8c60963 --- /dev/null +++ b/.github/workflows/yaml.yaml @@ -0,0 +1,41 @@ +# Do not edit this file! Make a pull request on changing +# github/workflows/yaml.yaml in +# https://github.com/itk-dev/devops_itkdev-docker if need be. + +### ### YAML +### +### Validates YAML files. +### +### #### Assumptions +### +### 1. A docker compose service named `prettier` for running +### [Prettier](https://prettier.io/) exists. +### +### #### Symfony YAML +### +### Symfony's YAML config files use 4 spaces for indentation and single quotes. +### Therefore we use a [Prettier configuration +### file](https://prettier.io/docs/configuration), `.prettierrc.yaml`, to make +### Prettier format YAML files in the `config/` folder like Symfony expects. + +name: YAML + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + yaml-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create docker network + run: | + docker network create frontend + + - run: | + docker compose run --rm prettier '**/*.{yml,yaml}' --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..386533b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.idea +vendor +composer.lock \ No newline at end of file diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..0253096 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,22 @@ +// This file is copied from config/markdown/.markdownlint.jsonc in https://github.com/itk-dev/devops_itkdev-docker. +// Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +// markdownlint-cli configuration file (cf. https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#configuration) +{ + "default": true, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + "line-length": { + "line_length": 120, + "code_blocks": false, + "tables": false + }, + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md + "no-duplicate-heading": { + "siblings_only": true + }, + // https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections#creating-a-collapsed-section + // https://github.com/DavidAnson/markdownlint/blob/main/doc/md033.md + "no-inline-html": { + "allowed_elements": ["details", "summary"] + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..f1389fd --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,8 @@ +# This file is copied from config/markdown/.markdownlintignore in https://github.com/itk-dev/devops_itkdev-docker. +# Feel free to edit the file, but consider making a pull request if you find a general issue with the file. + +# https://github.com/igorshubovych/markdownlint-cli?tab=readme-ov-file#ignoring-files +vendor/ +node_modules/ +LICENSE.md +js/lib/ \ No newline at end of file diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..5c625c6 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,31 @@ + + + + + + The coding standard. + + src/ + + + node_modules + vendor + *.css + *.js + + + + + + + + + + + + + + + + + diff --git a/.twig-cs-fixer.dist.php b/.twig-cs-fixer.dist.php new file mode 100644 index 0000000..0a0f295 --- /dev/null +++ b/.twig-cs-fixer.dist.php @@ -0,0 +1,16 @@ +in(__DIR__); +// … that are not ignored by VCS +$finder->ignoreVCSIgnored(true); + +$config = new TwigCsFixer\Config\Config(); +$config->setFinder($finder); + +return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2965a3b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) + +## [Unreleased] + +- [PR-1](https://github.com/itk-dev/itk_video/pull/1) + + - Base functionality with a new itk_video field type + - Support for Videotool and Vimeo + - Support for CookieInformation.com + - Config page. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27e745f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 ITK Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index a976bf4..997ac51 100644 --- a/README.md +++ b/README.md @@ -1 +1,70 @@ -# itk_video +# ITK Video + +Module that supplies video integration through a dedicated itk_video field +type. + +- Supports [CookieInformation](https://cookieinformation.com) +- Supports [Videotool](https://videotool.dk/) +- Supports [Vimeo](https://vimeo.com/) + +## How it works + +The supported video providers are defined in [SupportedVideoProvider.php](src/SupportedVideoProvide.php). +The definitions include + +- host: Used to identify the provider from the supplied video URL. +- type: Used to determine if videoinformation should be retrieved bu this +module (custom) or Oembed +- requiredCookies: The cookies that the provider sets when embedding a video +from that provider. See [CookieInformation](https://support.cookieinformation.com/en/articles/5444629-block-third-party-cookies-with-a-script) +about what categories are used. + +If a provider is enabled in the module settings videos from this provider are +displayed. + +## How to use + +The module includes a permission to access the configuration page: "Administer ITK Video settings" + +If allowed access the settings are set on `/admin/config/media/itk-video`. +Settings include: + +- Respect limitations provided by cookieinformation.com +- Enable/disable supported video providers + +## Code + +To check coding standards run the following commands: + +Install dependencies: + +```bash +docker compose run --rm phpfpm composer install +``` + +Check composer and security updates: + +```bash +docker compose run --rm phpfpm composer normalize --dry-run +docker compose run --rm phpfpm composer audit +``` + +Check assets: + +```bash +docker compose run --rm prettier 'css/**/*.css' --check +``` + +Check php, code sniffer and code-analysis: + +```bash +docker compose run --rm phpfpm vendor/bin/phpcs +./scripts/code-analysis +``` + +Check markdown and yaml: + +```bash +docker compose run --rm markdownlint markdownlint '**/*.md' +docker compose run --rm prettier '**/*.{yml,yaml}' --check +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..626a541 --- /dev/null +++ b/composer.json @@ -0,0 +1,58 @@ +{ + "name": "itk-dev/itk_video", + "description": "Module that provides Video integration", + "license": "MIT", + "type": "drupal-module", + "keywords": [ + "Drupal" + ], + "require": { + "ext-dom": "*", + "drush/drush": "^12 || ^13", + "psr/cache": "^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", + "drupal/coder": "^8.3", + "ergebnis/composer-normalize": "^2.44", + "mglaman/phpstan-drupal": "^2.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-deprecation-rules": "^2.0" + }, + "repositories": [ + { + "type": "composer", + "url": "https://packages.drupal.org/8" + } + ], + "minimum-stability": "dev", + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + }, + "sort-packages": true + }, + "scripts": { + "code-analysis": [ + "@code-analysis/drupal-check" + ], + "code-analysis/drupal-check": [ + "vendor/bin/drupal-check --deprecations --analysis --exclude-dir=vendor *.* src" + ], + "coding-standards-apply": [ + "@coding-standards-apply/phpcs" + ], + "coding-standards-apply/phpcs": [ + "vendor/bin/phpcbf --standard=phpcs.xml.dist" + ], + "coding-standards-check": [ + "@coding-standards-check/phpcs" + ], + "coding-standards-check/phpcs": [ + "vendor/bin/phpcs --standard=phpcs.xml.dist" + ] + } +} diff --git a/config/schema/itk_video.schema.yml b/config/schema/itk_video.schema.yml new file mode 100644 index 0000000..9960bfd --- /dev/null +++ b/config/schema/itk_video.schema.yml @@ -0,0 +1,4 @@ +respect_cookie_information: true +supported_providers: + vimeo: false + video_tool: true diff --git a/css/itk_video.css b/css/itk_video.css new file mode 100644 index 0000000..1d93086 --- /dev/null +++ b/css/itk_video.css @@ -0,0 +1,28 @@ +.itk-video-responsive { + overflow: hidden; + padding-bottom: 56.25%; + position: relative; + height: 0; + margin-bottom: 1.5em; +} + +.itk-video-responsive > iframe { + background: #e2ecee; + border: 3px solid #d5e4e6; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.itk-blocked-text { + z-index: 1; + position: absolute; + margin: 0 auto; + left: 0; + right: 0; + text-align: center; + transform: translateY(-50%); + top: 50%; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0b9f650 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +# itk-version: 3.2.4 + +services: + phpfpm: + image: itkdev/php8.4-fpm:latest + user: ${COMPOSE_USER:-deploy} + volumes: + - .:/app + + # Code checks tools + markdownlint: + image: itkdev/markdownlint + profiles: + - dev + volumes: + - ./:/md + + prettier: + # Prettier does not (yet, fcf. + # https://github.com/prettier/prettier/issues/15206) have an official + # docker image. + # https://hub.docker.com/r/jauderho/prettier is good candidate (cf. https://hub.docker.com/search?q=prettier&sort=updated_at&order=desc) + image: jauderho/prettier + profiles: + - dev + volumes: + - ./:/work diff --git a/itk_video.info.yml b/itk_video.info.yml new file mode 100644 index 0000000..0e947fd --- /dev/null +++ b/itk_video.info.yml @@ -0,0 +1,5 @@ +name: ITK Video +type: module +description: "Module that supplies a Video field, with support for VideoTool and respect to CookieInformation." +package: ITK +core_version_requirement: ^10 || ^11 diff --git a/itk_video.libraries.yml b/itk_video.libraries.yml new file mode 100644 index 0000000..6c2f831 --- /dev/null +++ b/itk_video.libraries.yml @@ -0,0 +1,4 @@ +video: + css: + theme: + css/itk_video.css: {} diff --git a/itk_video.links.menu.yml b/itk_video.links.menu.yml new file mode 100644 index 0000000..c425b5f --- /dev/null +++ b/itk_video.links.menu.yml @@ -0,0 +1,5 @@ +itk_video.settings: + title: "ITK Video" + description: "Configure ITK Video settings and supported providers." + parent: system.admin_config_media + route_name: itk_video.settings diff --git a/itk_video.permissions.yml b/itk_video.permissions.yml new file mode 100644 index 0000000..d34f626 --- /dev/null +++ b/itk_video.permissions.yml @@ -0,0 +1,3 @@ +administer itk video settings: + title: "Administer ITK Video settings" + description: "Configure ITK Video module settings and supported providers." diff --git a/itk_video.routing.yml b/itk_video.routing.yml new file mode 100644 index 0000000..ab5360c --- /dev/null +++ b/itk_video.routing.yml @@ -0,0 +1,7 @@ +itk_video.settings: + path: "/admin/config/media/itk-video" + defaults: + _form: 'Drupal\itk_video\Form\ItkVideoSettingsForm' + _title: "ITK Video Settings" + requirements: + _permission: "administer itk video settings" diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..fe16319 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,22 @@ + + + The coding standard. + + . + + vendor/ + + + + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..4362b58 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + paths: + - . + level: 5 + customRulesetUsed: true + reportUnmatchedIgnoredErrors: false + excludePaths: + - src/ProxyClass/* + - vendor/* diff --git a/scripts/.env b/scripts/.env new file mode 100644 index 0000000..a029f91 --- /dev/null +++ b/scripts/.env @@ -0,0 +1,2 @@ +COMPOSE_PROJECT_NAME=drupal-module +MODULE_NAME=itk_video diff --git a/scripts/base b/scripts/base new file mode 100644 index 0000000..495d86e --- /dev/null +++ b/scripts/base @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +set -o errexit -o errtrace -o noclobber -o nounset -o pipefail +IFS=$'\n\t' + +execute_name=execute + +usage() { + (cat >&2 </dev/null); then + (cat >&2 <&2 <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; + } + +}