Initial commit

This commit is contained in:
thorsten 2025-06-07 06:39:19 +02:00
commit cd13e758db
14 changed files with 582 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

7
CHANGELOG.md Normal file
View File

@ -0,0 +1,7 @@
# v0.0.1
## 05/07/2025
1. [](#new)
* Inital, non-public version of the plugin
2. [](#improved)
3. [](#bugfix)

21
LICENSE Normal file
View File

@ -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.

95
README.md Normal file
View File

@ -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 video ID>[/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.

View File

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

2
assets/js/js.cookie.min.js vendored Normal file
View File

@ -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<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}return function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"})}));

View File

@ -0,0 +1,50 @@
/**
* Activate all the videos on this page.
*/
function activateVideos() {
const iframes = document.querySelectorAll('.video__iframe[data-src*="youtube-nocookie.com"]');
iframes.forEach((iframe) => {
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();
}
});

78
blueprints.yaml Normal file
View File

@ -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

49
languages.yaml Normal file
View File

@ -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 <a href="https://www.youtube.com/t/privacy" target="_blank">privacy terms<a>.'
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 <a href="https://www.youtube.com/t/privacy" target="_blank">Datenschutzerklärung<a>.'
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

View File

@ -0,0 +1,122 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class YouTubeConsentShortcode extends Shortcode
{
public function init()
{
$this->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('(<p>|</p>)', '', $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
]
);
});
}
}

View File

@ -0,0 +1,3 @@
<div class="notices red">
<p>{{ errorMessage }}</p>
</div>

View File

@ -0,0 +1,22 @@
{# html #}
<div id="{{ id }}" class='video'>
<iframe
class='video__iframe'
width='{{ videoWidth }}'
height='{{ videoHeight }}'
data-src="https://www.youtube-nocookie.com/embed/{{ videoId }}{{ parameterString }}"
frameborder='0'
allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowfullscreen='allowfullscreen'></iframe>
<div class='video__notice' style="width: {{ videoWidth }}px; height: {{ videoHeight }}px; background-image: url('https://i.ytimg.com/vi/{{ videoId }}/hqdefault.jpg');">
<div style='color:{{ noticeForeground }}; background-color:{{ noticeBackground }};'>
<form>
<p>{{ notice | raw }}</p>
<a class="{{ acceptLinkClass }}" onclick="return onConsent({{ id }})">{{ accept | raw }}</a>
<a class="{{ acceptLinkClass }}" onclick="return onConsent()">{{ acceptAll | raw }}</a>
</form>
</div>
</div>
</div>

72
youtubeconsent.php Normal file
View File

@ -0,0 +1,72 @@
<?php
namespace Grav\Plugin;
use Grav\Common\Plugin;
use RocketTheme\Toolbox\Event\Event;
use Grav\Common\Page\Page;
/**
* Class OptInVideoLinksPlugin
* @package Grav\Plugin
*/
class YouTubeConsentPlugin extends Plugin
{
private $currentPage = null;
/**
* @return array
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents(): array
{
return [
'onShortcodeHandlers' => ['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');
}
}

10
youtubeconsent.yaml Normal file
View File

@ -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: