commit cd13e758db69379fb4a582e40689de884cf8830e Author: thorsten Date: Sat Jun 7 06:39:19 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e114bd6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# v0.0.1 +## 05/07/2025 + +1. [](#new) + * Inital, non-public version of the plugin +2. [](#improved) +3. [](#bugfix) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b2ea33b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2023-25 Thorsten Dittmar + +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 new file mode 100644 index 0000000..4f1381a --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# YouTube Consent Plugin + +The **YouTube Consent** Plugin is an extension for [Grav CMS](https://github.com/getgrav/grav). It registers the new `youtube` shortcodes to link videos in a privacy-friendly manner. + +## Installation + +Installing the YouTube Consent plugin can be done in one of three ways: The GPM (Grav Package Manager) installation method lets you quickly install the plugin with a simple terminal command, the manual method lets you do so via a zip file, and the admin method lets you do so via the Admin Plugin. + +### GPM Installation (Preferred) + +To install the plugin via the [GPM](https://learn.getgrav.org/cli-console/grav-cli-gpm), through your system's terminal (also called the command line), navigate to the root of your Grav-installation, and enter: + + bin/gpm install youtubeconsent + +This will install the plugin into your `/user/plugins`-directory within Grav. Its files can be found under `/your/site/grav/user/plugins/youtubeconsent`. + +### Manual Installation + +To install the plugin manually, download the zip-version of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `youtubeconsent`. You can find these files on [here](https://gitea.dithosoft.de/thorsten/grav-youtubeconsent-plugin.git) or via [GetGrav.org](https://getgrav.org/downloads/plugins). + +You should now have all the plugin files under + + /your/site/grav/user/plugins/youtubeconsent + +### Admin Plugin + +If you use the Admin Plugin, you can install the plugin directly by browsing the `Plugins`-menu and clicking on the `Add` button. + +## Configuration + +Before configuring this plugin, you should copy the `user/plugins/youtubeconsent/youtubeconsent.yaml` to `user/config/plugins/youtubeconsent.yaml` and only edit that copy. + +Here is the default configuration and an explanation of available options: + +```yaml +# Enables or disables the plugin +enabled: true + +# Specifies the default width for the video container +default_width: 720 + +# Specifies the default height for the video container +default_height: 405 + +# Specifies the YouTube quality parameter +default_quality: 'hd1080' + +# Specifies the background color for the consent notice +notice_background: '#FFFFFF' + +# Specifies the foreground color for the consent notice +notice_foreground: '#333333' + +# Specifies additional CSS class names for the accept buttons +accept_link_class: button + +# Specifies the notice text (if empty, a default text is used) +notice_text: + +# Specifies the caption of the "Accept" button (if empty, a default text is used) +accept_text: + +# Specifies the caption of the "Accept for all" button (if empty, a default text is used) +accept_all_text: +``` + +Note that if you use the Admin Plugin, a file with your configuration named `youtubeconsent.yaml` will be saved in the `user/config/plugins/`-folder once the configuration is saved in the Admin. + +## Required plugins + +The YouTube consent plugin requires the following plugins to be installed and activated: + +| Plugin | URL | +| -------------- | ----------------------------------------------------- | +| Shortcode Core | https://github.com/getgrav/grav-plugin-shortcode-core | + +## Usage + +Once enabled and configured, you can use the `[youtube]` shortcode to include videos from YouTube in a privacy friendly manner. The shortcode is used as follows: + + [youtube start=... width=... height=... quality=...][/youtube] + +The following optional parameters are available: + +| Parameter | Default value | Description | +| --------- | ------------- | ----------- | +| start | 0 | The number of seconds into the video to start from | +| width | Configuration value | The width of the player frame in pixels | +| height | Configuration value | The height of the player frame in pixels | +| quality | Configuration value | The specifier for the default video quality | + +## Cookies + +The "Accept all" button will set the `grav_youtubeconsent` cookie for the site with the value of `true` and a lifetime of 365 days to remember the user's decision. +This can be considered a functional cookie that does not require visitor's consent. \ No newline at end of file diff --git a/assets/css/youtubeconsent.css b/assets/css/youtubeconsent.css new file mode 100644 index 0000000..cdde519 --- /dev/null +++ b/assets/css/youtubeconsent.css @@ -0,0 +1,50 @@ +.video__iframe:not([src]) { + display: none; +} + +.video__iframe[src]+.video__notice { + display: none; +} + +.video { + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-top: 1em; + margin-bottom: 1em; +} + +.video > form { + margin: 0; +} + +.video::before { + display: block; + content: ''; + width: 0; + height: 0; +} + +.video__notice { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background-position: center center; + background-size: cover; +} + +.video__notice > div { + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0.9; + padding: 1.5rem; + display: flex; + width: 100%; +} + +.video__notice > div > * { + max-width: 38rem; + margin: auto; +} diff --git a/assets/js/js.cookie.min.js b/assets/js/js.cookie.min.js new file mode 100644 index 0000000..90a7672 --- /dev/null +++ b/assets/js/js.cookie.min.js @@ -0,0 +1,2 @@ +/*! js-cookie v3.0.1 | MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t { + iframe.src = iframe.dataset.src; + }); +} + + +/** + * Activate only the video with the given video ID + */ +function activateVideo(videoId) { + const surroundingDiv = document.getElementById(videoId); + const iframes = surroundingDiv.querySelectorAll('.video__iframe[data-src*="youtube-nocookie.com"]'); + iframes.forEach((iframe) => { + iframe.src = iframe.dataset.src; + }); +} + + +/** + * Invoked by the accept buttons to activate one or all videos. + * @param videodId The ID of the video to activate. Undefined to activate all. + */ +function onConsent(videoId) { + // Activate only the given video if the videoId is defined. Otherwise, + // activate all videos permanently by setting our cookie valid for a year. + if (videoId) { + activateVideo(videoId); + } + else { + Cookies.set('grav_youtubeconsent', 'true', { expires: 365 }); + activateVideos(); + } + + return false; +} + + +/** + * When the page is loaded, activate all videos in case the "grav_youtubeconsent" cookie has the value 'true'. + */ +window.addEventListener("load", function () { + if (Cookies.get('grav_youtubeconsent') === 'true') { + activateVideos(); + } +}); \ No newline at end of file diff --git a/blueprints.yaml b/blueprints.yaml new file mode 100644 index 0000000..f9591e2 --- /dev/null +++ b/blueprints.yaml @@ -0,0 +1,78 @@ +name: YouTube Consent +slug: youtubeconsent +type: plugin +version: 0.1.1 +description: Registers the new [youtube] shortcode to embed YouTube videos in a privacy-friendly manner. +icon: code +author: + name: Thorsten Dittmar + email: support@dithosoft.de +homepage: https://gitea.dithosoft.de/thorsten/grav-youtubeconsent-plugin.git +keywords: grav, plugin, shortcode, youtube, privacy +bugs: https://gitea.dithosoft.de/thorsten/grav-youtubeconsent-plugin.git/issues +docs: https://gitea.dithosoft.de/thorsten/grav-youtubeconsent-plugin/src/branch/main/README.md +license: MIT + +dependencies: + - { name: grav, version: '>=1.7.0' } + - shortcode-core + +form: + validation: strict + fields: + enabled: + type: toggle + label: PLUGIN_ADMIN.PLUGIN_STATUS + highlight: 1 + default: 0 + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + default_width: + label: PLUGIN_YOUTUBECONSENT.DEFAULT_WIDTH + help: PLUGIN_YOUTUBECONSENT.DEFAULT_WIDTH_HELP + type: number + validate: + min: 0 + size: small + default_height: + label: PLUGIN_YOUTUBECONSENT.DEFAULT_HEIGHT + help: PLUGIN_YOUTUBECONSENT.DEFAULT_HEIGHT_HELP + type: number + validate: + min: 0 + size: small + default_quality: + type: text + default: 'hd1080' + label: PLUGIN_YOUTUBECONSENT.DEFAULT_QUALITY + help: PLUGIN_YOUTUBECONSENT.DEFAULT_QUALITY_HELP + notice_background: + type: text + default: '#FFFFFF' + label: PLUGIN_YOUTUBECONSENT.NOTICE_BACKGROUND + help: PLUGIN_YOUTUBECONSENT.NOTICE_BACKGROUND_HELP + notice_foreground: + type: text + default: '#333333' + label: PLUGIN_YOUTUBECONSENT.NOTICE_FOREGROUND + help: PLUGIN_YOUTUBECONSENT.NOTICE_FOREGROUND_HELP + accept_link_class: + type: text + default: button + label: PLUGIN_YOUTUBECONSENT.ACCEPT_LINK_CLASS + help: PLUGIN_YOUTUBECONSENT.ACCEPT_LINK_CLASS_HELP + notice_text: + type: text + label: PLUGIN_YOUTUBECONSENT.NOTICE_TEXT + help: PLUGIN_YOUTUBECONSENT.NOTICE_TEXT_HELP + accept_text: + type: text + label: PLUGIN_YOUTUBECONSENT.ACCEPT_TEXT + help: PLUGIN_YOUTUBECONSENT.ACCEPT_TEXT_HELP + accept_all_text: + type: text + label: PLUGIN_YOUTUBECONSENT.ACCEPT_ALL_TEXT + help: PLUGIN_YOUTUBECONSENT.ACCEPT_ALL_TEXT_HELP diff --git a/languages.yaml b/languages.yaml new file mode 100644 index 0000000..be4c98d --- /dev/null +++ b/languages.yaml @@ -0,0 +1,49 @@ +en: + PLUGIN_YOUTUBECONSENT: + ERROR_NO_ID: '[YouTube Consent] Error: YouTube video ID needs to be provided in the shortcode content.' + DEFAULT_NOTICE: 'This content is included from YouTube. Since YouTube may collect personal data, we only load the video after your consent. Please also refer to YouTube''s privacy terms.' + DEFAULT_ACCEPT: 'Accept for this video' + DEFAULT_ACCEPT_ALL: 'Accept for all videos permanently' + NOTICE_BACKGROUND: Notice Background Color + NOTICE_BACKGROUND_HELP: HTML color code for the notice background + NOTICE_FOREGROUND: Notice Foreground Color + NOTICE_FOREGROUND_HELP: HTML color code for the notice foreground + ACCEPT_LINK_CLASS: Accept Link Class + ACCEPT_LINK_CLASS_HELP: A CSS class to use for the "Accept" link + DEFAULT_WIDTH: Default width + DEFAULT_WIDTH_HELP: Specify the default width of the video frame in pixels + DEFAULT_HEIGHT: Default height + DEFAULT_HEIGHT_HELP: Specify the default height of the video frame in pixels + DEFAULT_QUALITY: Default quality + DEFAULT_QUALITY_HELP: Specify the default video quality (e.g. hd1080) + NOTICE_TEXT: Consent message + NOTICE_TEXT_HELP: The message shown to the user asking him for consent + ACCEPT_TEXT: '"Accept" button caption' + ACCEPT_TEXT_HELP: 'The caption of the "Accept" button' + ACCEPT_ALL_TEXT: '"Accept all" button caption' + ACCEPT_ALL_TEXT_HELP: 'The caption of the "Accept all" button' + +de: + PLUGIN_YOUTUBECONSENT: + ERROR_NO_ID: '[YouTube Consent] Fehler: YouTube Video-ID muss im Shortcode-Inhalt angegeben werden.' + DEFAULT_NOTICE: 'Dieser Inhalt wird von YouTube eingebunden. Da YouTube eventuell persönliche Daten erhebt, laden wir das Video erst nach Deiner Zustimmung. Bitte beachte dazu auch YouTubes Datenschutzerklärung.' + DEFAULT_ACCEPT: 'Für dieses Video akzeptieren' + DEFAULT_ACCEPT_ALL: 'Für alle Videos dauerhaft akzeptieren' + NOTICE_BACKGROUND: Hinweis-Hintergrundfarbe + NOTICE_BACKGROUND_HELP: HTML Farbcode für den Hintergrund des Hinweises + NOTICE_FOREGROUND: Hinweis-Vordergrundfarbe + NOTICE_FOREGROUND_HELP: HTML Farbcode für den Vordergrund des Hinweises + ACCEPT_LINK_CLASS: Klasse für Akzeptieren-Link + ACCEPT_LINK_CLASS_HELP: CSS Klassen, die auf den "Akzeptieren"-Link angewendet werden. + DEFAULT_WIDTH: Standardbreite + DEFAULT_WIDTH_HELP: Die Standardbreite des Video-Frame in Pixeln + DEFAULT_HEIGHT: Standardhöhe + DEFAULT_HEIGHT_HELP: Die Standardhöhe des Video-Frame in Pixeln + DEFAULT_QUALITY: Standardquailtät + DEFAULT_QUALITY_HELP: Die Standard-Abspielqualität (z.B. hd1080) + NOTICE_TEXT: Bestätigungstext + NOTICE_TEXT_HELP: Der Text, der dem Benutzer zur Bestätigung angezeigt wird + ACCEPT_TEXT: Text für "Akzeptieren"-Knopf + ACCEPT_TEXT_HELP: Der im "Akzeptieren"-Knopf angezeigte Text + ACCEPT_ALL_TEXT: Text für "Alle akzeptieren"-Knopf + ACCEPT_ALL_TEXT_HELP: Der im "Alle akzeptieren"-Knopf angezeigte Text diff --git a/shortcodes/YouTubeConsentShortcode.php b/shortcodes/YouTubeConsentShortcode.php new file mode 100644 index 0000000..8f40233 --- /dev/null +++ b/shortcodes/YouTubeConsentShortcode.php @@ -0,0 +1,122 @@ +shortcode->getHandlers()->add('youtube', function (ShortcodeInterface $shortcode) { + // get default settings + $pluginConfig = $this->config->get('plugins.youtubeconsent'); + + // get the current page in process (i.e. the page where the shortcode is being processed) + // warning, it can be different from $this->grav['page'], if ever we browse a collection + // this is exactly what the Feed plugin does + $currentPage = $this->grav['plugins']->getPlugin('youtubeconsent')->getCurrentPage(); + + // values to check if we are in a feed (RSS, Atom, JSON) + $type = $this->grav['uri']->extension(); // Get current page extension + $feed_config = $this->grav['config']->get('plugins.feed'); + $feed_types = array('rss','atom'); + if ($feed_config && $feed_config['enable_json_feed']) + $feed_types[] = 'json'; + + // check if the rendered page will be cached or not + $renderingCacheDisabled = !is_null($currentPage) + && isset($currentPage->header()->cache_enable) + && !$currentPage->header()->cache_enable + || !$this->grav['config']->get('system.cache.enabled'); + + // check if we are in a feed (RSS, Atom, JSON) + // we also check that the page will not be cached once rendered (otherwise the iframe will not be generated on the normal page) + if ( $renderingCacheDisabled && // if the current page does not cache its rendering + $feed_config && $feed_config['enabled'] && // and the Feed plugin is enabled + isset($this->grav['page']->header()->content) && // and the current page has a collection + $feed_types && in_array($type, $feed_types) ) { // and the Feed plugin handles it + return $shortcode->getContent(); // return unprocessed content (because in RSS, Javascripts don't work) + } + + // get parameters the user provided (or set to defaults) + $start = $shortcode->getParameter('start', 0); + $width = $shortcode->getParameter('width', $pluginConfig['default_width']); + $height = $shortcode->getParameter('height', $pluginConfig['default_height']); + $quality = $shortcode->getParameter('quality', $pluginConfig['default_quality']); + + // get the YouTube video ID from the shortcode content + $videoId = trim(preg_replace('(

|

)', '', $shortcode->getContent())); + + // check validity + if ($videoId === "") + { + $errorMsg = $this->grav['language']->translate('PLUGIN_YOUTUBECONSENT.ERROR_NO_ID'); + return $this->twig->processTemplate( + 'partials/shortcodeerror.html.twig', + [ + 'errorMessage' => $errorMsg + ] + ); + } + + // a random id for each gallery + $id = mt_rand(); + + // give JS and CSS so that they can be cached + $this->shortcode->addAssets('css', "plugin://youtubeconsent/assets/css/youtubeconsent.css"); + $this->shortcode->addAssets('js', "plugin://youtubeconsent/assets/js/js.cookie.min.js"); + $this->shortcode->addAssets('js', "plugin://youtubeconsent/assets/js/youtubeconsent.js"); + + // Assemble parameter string + $parameterList = []; + $parameters = ''; + + if ($start > 0) $parameterList[] = "start=" . $start; + if ($quality !== '') $parameterList[] = "vq=" . $quality; + + foreach ($parameterList as $param) { + if (strlen($parameters) === 0) + $parameters = "?"; + else + $parameters = $parameters . "&"; + + $parameters = $parameters . $param; + } + + // Get the notice from the configuration or - if empty - use the default + $noticeText = $pluginConfig['notice_text']; + $acceptText = $pluginConfig['accept_text']; + $acceptAllText = $pluginConfig['accept_all_text']; + if (!$noticeText) { + $noticeText = $this->grav['language']->translate('PLUGIN_YOUTUBECONSENT.DEFAULT_NOTICE'); + } + if (!$acceptText) { + $acceptText = $this->grav['language']->translate('PLUGIN_YOUTUBECONSENT.DEFAULT_ACCEPT'); + } + if (!$acceptAllText) { + $acceptAllText = $this->grav['language']->translate('PLUGIN_YOUTUBECONSENT.DEFAULT_ACCEPT_ALL'); + } + + return $this->twig->processTemplate( + 'partials/youtubeconsent.html.twig', + [ + 'page' => $this->grav['page'], // used for image resizing + 'id' => $id, + 'videoId' => $videoId, + 'videoStart' => $start, + 'videoWidth' => $width, + 'videoHeight' => $height, + 'parameterString' => $parameters, + 'noticeForeground' => $pluginConfig['notice_foreground'], + 'noticeBackground' => $pluginConfig['notice_background'], + 'acceptLinkClass' => $pluginConfig['accept_link_class'], + 'notice' => $noticeText, + 'accept' => $acceptText, + 'acceptAll' => $acceptAllText + ] + ); + }); + } + +} diff --git a/templates/partials/shortcodeerror.html.twig b/templates/partials/shortcodeerror.html.twig new file mode 100644 index 0000000..c753b46 --- /dev/null +++ b/templates/partials/shortcodeerror.html.twig @@ -0,0 +1,3 @@ +
+

{{ errorMessage }}

+
\ No newline at end of file diff --git a/templates/partials/youtubeconsent.html.twig b/templates/partials/youtubeconsent.html.twig new file mode 100644 index 0000000..7942d36 --- /dev/null +++ b/templates/partials/youtubeconsent.html.twig @@ -0,0 +1,22 @@ +{# html #} + +
diff --git a/youtubeconsent.php b/youtubeconsent.php new file mode 100644 index 0000000..dc91227 --- /dev/null +++ b/youtubeconsent.php @@ -0,0 +1,72 @@ + ['onShortcodeHandlers', 0], + 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + 'onPageContentRaw' => ['onPageContentRaw', 1000], // before the Shortcode Core plugin + 'onPageContentProcessed' => ['onPageContentProcessed', 1000], // before the Shortcode Core plugin + ]; + } + + /** + * Add current directory to twig lookup paths. + */ + public function onTwigTemplatePaths() + { + $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; + } + + /** + * Detect which page is being processed, even if it is in a collection. + * We store it so that our shortcode can use it. + */ + public function onPageContentRaw(Event $event) + { + $this->currentPage = $event['page']; + } + + public function onPageContentProcessed(Event $event) + { + $this->currentPage = $event['page']; + } + + public function getCurrentPage() + { + return $this->currentPage; + } + + /** + * Initialize configuration + */ + public function onShortcodeHandlers() + { + $this->grav['debugger']->addMessage('Adding shortcodes from ' . __DIR__ . '/shortcodes'); + $this->grav['shortcode']->registerAllShortcodes(__DIR__ . '/shortcodes'); + } +} diff --git a/youtubeconsent.yaml b/youtubeconsent.yaml new file mode 100644 index 0000000..5c25faa --- /dev/null +++ b/youtubeconsent.yaml @@ -0,0 +1,10 @@ +enabled: true +default_width: 720 +default_height: 405 +default_quality: 'hd1080' +notice_background: '#FFFFFF' +notice_foreground: '#333333' +accept_link_class: button +notice_text: +accept_text: +accept_all_text: \ No newline at end of file