Code Snippet: Zustimmung für externe Inhalte

Einführung
Auf vielen Websites werden externe Inhalte, wie beispielsweise Google Karten eingebunden.
Nutzt man hierzu die in WordPress (Classic Editor, Gutenberg) oder Page Buildern (Oxygen, und viele andere) vorgesehenen Standard-Verfahren und fragt nicht vorher die Zustimmung des Besuchers ab, verstößt man in der EU gegen die Datenschutzrichtlinien (DSGVO, GDPR): Bei der Anzeige einer Seite, die externe Inhalte nachlädt, wird automatisch eine Verbindung zu einem externen Server aufgebaut. Dabei wird die IP-Adresse des Besuchers, und damit eine personenbezogene Information, an den externen Server übertragen. Das ist nicht erlaubt ohne explizite Zustimmung.
Wie kann man also externe Inhalte auf der Website DSGVO-konform einbinden?
Lösung
Das hier beschriebene Code Snippet stellt einen WordPress Shortcode zur Verfügung, der eine DSGVO-konforme und gleichzeitig möglichst einfache und effiziente Einbettung von externen Inhalten ermöglicht.
Mittels dieses Shortcodes wird zunächst ein Hinweis-Text und ein Zustimmungs-Button angezeigt. Erst nach Klick durch den Besucher wird der externe Inhalt geladen. Dieses Verfahren ist DSGVO-konform, weil der Besucher zunächst aktiv bestätigen muss, dass er die externen Inhalte laden will.
Und so ganz nebenbei sollte sich diese Lösung auch deutlich positiv auf die bekannten Speed Test Tools (Page Speed Insights, GTMetrix, ...) auswirken, da ohne vorherige Bestätigung keine Daten von externen Servern geladen werden.
Shortcode
Der Shortcode besteht aus einem öffnenden und einem schließenden Tag. Dazwischen wird der zu bestätigende Inhalt eingebettet.
[ma-content-consent] externer Inhalt [/ma-content-consent]
Hier ein Beispiel für die Einbettung einer Google Karte:
[ma-content-consent]
<iframe src="https://www.google.com/maps/embed?pb=..." width="100%" height="300"
style="border:0;" allowfullscreen="" loading="lazy"></iframe>
[/ma-content-consent]
Auf der Seite wird nun zunächst ein Hinweis-Text mit Link zur Datenschutzerklärung angezeigt. Darunter fordert ein Button die Zustimmung vom Besucher an, die externen Inhalte zu laden. Erst nach Klick auf den Button wird tatsächlich eine Verbindung zum externen Server aufgebaut und der eigentliche Inhalt geladen und angezeigt.
Shortcode Parameter
Das Code Snippet implementiert ein recht einfaches Design für den Hinweis-Text und den Button. Mittels Shortcode-Parametern kann das Aussehen beeinflusst werden.
id
Bei der Ausgabe erhält das HTML Element eine automatisch generierte, eindeutige HTML ID nach dem Muster ma-content-consent-61fbc4de4ba92
. Mit dem Parameter id
kann eine eigene HTML ID vergeben werden.
block-style
Der Zustimmungs-Block hat standardmäßig grundlegende CSS Styles zugewiesen:
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
background-color: #efefef;
border: 1px solid lightgray;
padding: 1em;
width: 100%;
Mit dem Shortcode-Parameter block-style
können diese CSS Styles überschrieben oder weitere ergänzt werden.
Beispiel:
[ma-content-consent block-style="height: 300px; border-radius: 5px;"] ... [/ma-content-consent]
block-class
Sollen mehrere Zustimmungs-Blöcke ein identisches Design erhalten, kann mit dem Parameter block-class
eine CSS Klasse zugewiesen werden.
Beispiel:
[ma-content-consent block-class="my-block"] ... [/ma-content-consent]
text
Im Code Snippet ist ein Hinweis-Text zu den Datenschutzrichtlinien in verschiedenen Sprachen hinterlegt:
Sprache | Text |
---|---|
DA | Når du har trykket, vil indholdet blive hentet fra eksterne servere. Se vores privatlivspolitik for flere informationer. |
DE | Bei Klick wird dieser Inhalt von externen Servern geladen. Details siehe Datenschutzerklärung. |
EN | When clicked, this content is loaded from external servers. See our privacy policy for details. |
ES | Al hacer clic, este contenido se carga desde servidores externos. Consulte la política de privacidad para más detalles. |
FI | Kun tätä sisältöä napsautetaan, se ladataan ulkoisilta palvelimilta. Lisätietoja on tietosuojaselosteessa. |
FR | En cliquant, ce contenu est chargé depuis des serveurs externes. Voir la politique de confidentialité. |
HU | Ha rákattint, ez a tartalom külső szerverekről töltődik be. A részletekért olvassa el az Adatkezelési Tájékoztatót oldalt. |
IT | Quando viene cliccato, questo contenuto viene caricato da server esterni. Vedere l'informativa sulla privacy per i dettagli. |
JA | クリックすると、以下のコンテンツが外部サーバーから読み込まれます。弊社のプライバシーポリシーの詳細は、プライバシーポリシー |
Die Sprache wird basierend auf der Sprache der Website oder Seite ausgewählt. Die Unterstützung von Polylang-Sprachen ist im Snippet enthalten.
Wenn kein Standardtext für die Sprache der Seite verfügbar ist, wird stattdessen Englisch verwendet.
Der Hinweis-Text kann per Parameter text
an die eigenen Anforderungen, beispielsweise andere Sprachen, angepasst werden, und kann folgende Platzhalter enthalten:{privacy-policy-url}
wird ersetzt durch den in WordPress konfigurierten URL der Seite zur Datenschutzerklärung.{privacy-policy-link}
wird ersetzt durch einen vollständigen Link zur in WordPress konfigurierten Seite zur Datenschutzerklärung.
Hinweis: Die vordefinierten Standardtexte enthalten einen Link zur Datenschutzerklärung, wenn diese in WordPress korrekt konfiguriert ist.
[ma-content-consent text="Wanneer erop geklikt wordt, wordt deze inhoud van externe servers geladen. Zie het {privacy-policy-link} voor details."] ... [/ma-content-consent]
text-style
Der Hinweis-Text hat standardmäßig grundlegende CSS Styles zugewiesen:
font-size: .7em;
Mit dem Shortcode-Parameter text-style
können diese CSS Styles überschrieben oder weitere ergänzt werden.
Beispiel:
[ma-content-consent text-style="text-shadow: 1px 1px 2px black; color: white;"] ... [/ma-content-consent]
text-class
Sollen die Texte in mehreren Zustimmungs-Blöcken ein identisches Design erhalten, kann mit dem Parameter text-class
eine CSS Klasse zugewiesen werden.
Beispiel:
[ma-content-consent text-class="my-text"] ... [/ma-content-consent]
button-text
Im Code Snippet ist die Button-Beschriftung in in verschiedenen Sprachen hinterlegt:
Sprache | Text |
---|---|
DA | Indlæs eksternt indhold |
DE | Externen Inhalt laden |
EN | Load external content |
ES | Cargar contenido externo |
FI | Lataa ulkoista sisältöä |
FR | Charger le contenu externe |
HU | Külső tartalom betöltése |
IT | Caricare il contenuto esterno |
JA | 外部のコンテンツを読み込ませます。 |
Die Sprache wird basierend auf der Sprache der Website oder Seite ausgewählt. Die Unterstützung von Polylang-Sprachen ist im Snippet enthalten.
Wenn kein Standardtext für die Sprache der Seite verfügbar ist, wird stattdessen Englisch verwendet.
Die Button-Beschriftung kann per Parameter button-text
an die eigenen Anforderungen, beispielsweise andere Sprachen, angepasst werden.
Beispiel:
[ma-content-consent button-text="Karte laden"] ... [/ma-content-consent]
button-style
Der Button hat standardmäßig keine CSS Styles zugewiesen. Er bezieht sein Design über die CSS Klasse wp-block-button__link
vom Standard-WordPress Button.
Mit dem Shortcode-Parameter button-style
kann das Design des Buttons angepasst werden.
Beispiel:
[ma-content-consent button-style="background-color:#2a4b9b; padding:2px 10px; border-radius:5px;"] ... [/ma-content-consent]
button-class
Sollen die Buttons in mehreren Zustimmungs-Blöcken ein identisches Design erhalten, kann mit dem Parameter button-class
eine CSS Klasse zugewiesen werden.
Beispiel:
[ma-content-consent button-class="my-button"] ... [/ma-content-consent]
background-image
Optional kann dem Zustimmungs-Block mit dem Shortcode-Parameter background-image
ein Hintergrund-Bild zugewiesen werden.
Beispiel:
[ma-content-consent background-image="https://.../wp-content/uploads/2022/02..."] ... [/ma-content-consent]
Wurde mit diesem Parameter ein Hintergrund-Bild zugewiesen, werden außerdem automatisch die folgenden CSS Styles gesetzt:
background-size: cover;
background-position: center center;
Diese CSS Styles können mit dem oben beschriebenen Parameter block-styles
jedoch überschrieben und damit bei Bedarf angepasst werden.
Download
Das Code Snippet steht hier zum Download zur Verfügung:
ma-content-consent.json
Version 1.1.2, 2023-02-01
Zur Installation und Nutzung dieser JSON Datei wird das Plugin Code Snippets oder Advanced Scripts benötigt.
Dort kann diese JSON Datei mit der Funktion "Import" hochgeladen und anschließend aktiviert werden.
Alternativ: Am Ende dieser Seite kann der vollständige Source Code des Snippets eingesehen und kopiert werden.
Im Change Log sind neue Funktionalitäten und Fehlerbehebungen dokumentiert.
Spenden
Es macht mir viel Freude, Code Snippets zu entwickeln und damit Anforderungen zu lösen. Die Snippets stelle ich kostenfrei zur Verfügung.
Wenn Du möchtest, kannst Du meine vielen Stunden Arbeit mit einer kleinen Kaffee-Spende über PayPal honorieren.
Spenden werden selbstverständlich ordnungsgemäß durch mich versteuert.
Disclaimer
Das Code Snippet habe ich nach bestem Wissen und Gewissen mit der aktuellsten WordPress Version entwickelt und getestet.
Ich stelle das Code Snippet zur freien Verwendung zur Verfügung.
Eine Garantie für die Funktionalität in allen denkbaren WordPress Umgebungen kann ich nicht geben.
Download und Nutzung dieses Code Snippets erfolgen auf eigene Gefahr und Verantwortung.
Change Log
Siehe "Version History" in Source Code
Source Code
<?php /* Plugin Name: MA Content Consent Description: Shortcode for requesting user consent before loading external content. Author: <a href="https://www.altmann.de/">Matthias Altmann</a> Project: Code Snippet: MA Content Consent Version: 1.1.2 Plugin URI: https://www.altmann.de/en/blog-en/code-snippet-shortcode-content-consent/ Description: en: https://www.altmann.de/en/blog-en/code-snippet-consent-for-external-content/ de: https://www.altmann.de/blog/code-snippet-zustimmung-fuer-externe-inhalte/ Copyright: © 2021-2023, Matthias Altmann Version History: Date Version Description -------------------------------------------------------------------------------------------------------------- 2023-02-01 1.1.2 Changes: - Added Finnish translations for notice text and button label - Changed button tag from <a> to <span> to avoid Lighthouse SEO warning "Links not crawlable" Fixes: - Support for Oxygen Repeater (due to numbered IDs) (Thanks to André Slotta for reporting and supporting with code adaptions) 2022-11-15 1.1.1 Changes: - Added Japanese translations for notice text and button label (Thanks to Viorel-Cosmin Miron) - Added support for WPML (Thanks to Viorel-Cosmin Miron) Please note there's a WPML bug preventing the creation a policy link with the correct language (https://wpml.org/forums/topic/get_the_privacy_policy_link-should-be-translated/#post-8153387) Fixes: - Support for UTF-8 content in JS base64 decoding (Thanks to Tobias Przybilla for reporting) 2022-10-21 1.1.0 Changes: - Added new shortcode parameters block-class, text-class, button-class - Migrated JavaScript from jQuery to vanilla JS (ES6) to eliminate jQuery dependency. (Thanks to André Slotta for the provided code sample) Fixes: - Removed double '.' for German GDPR text. (Thanks to Tobias Maximilian Hietsch for reporting) 2022-02-04 1.0.1 - Added Dansk translations for notice text and button label (Thanks to Theis L. Soelberg) - Evaluating nested shortcodes (Thanks to Anja Kretzer) 2022-02-03 1.0.0 Initial release as Code Snippet 2021-12-29 0.0.1 Initial version for client project -------------------------------------------------------------------------------------------------------------- */ if (!class_exists('MA_Content_Consent')) : class MA_Content_Consent { const TITLE = 'MA Content Consent'; const SLUG = 'ma-content-consent'; const VERSION = '1.1.2'; // ===== CONFIGURATION ============================================================================================== public static $timing = false; // Write timing info to wordpress debug.log if WP_DEBUG enabled public static $debug = false; // Write debug info to wordpress debug.log if WP_DEBUG enabled private static $default_consent_text = [ // consent text in different languages 'da' => ['Når du har trykket, vil indholdet blive hentet fra eksterne servere. Se vores %s for flere informationer.','privatlivspolitik'], 'de' => ['Bei Klick wird dieser Inhalt von externen Servern geladen. Details siehe %s.', 'Datenschutzerklärung'], 'en' => ['When clicked, this content is loaded from external servers. See our %s for details.', 'privacy policy'], 'es' => ['Al hacer clic, este contenido se carga desde servidores externos. Consulte la %s para más detalles.', 'política de privacidad'], 'fi' => ['Kun tätä sisältöä napsautetaan, se ladataan ulkoisilta palvelimilta. Lisätietoja on %s.','tietosuojaselosteessa'], 'fr' => ['En cliquant, ce contenu est chargé depuis des serveurs externes. Voir la %s.', 'politique de confidentialité'], 'hu' => ['Ha rákattint, ez a tartalom külső szerverekről töltődik be. A részletekért olvassa el az %s oldalt.', 'Adatkezelési Tájékoztatót'], 'it' => ['Quando viene cliccato, questo contenuto viene caricato da server esterni. Vedere %s per i dettagli.', 'l\'informativa sulla privacy'], 'ja' => ['クリックすると、以下のコンテンツが外部サーバーから読み込まれます。弊社のプライバシーポリシーの詳細は、%s', 'プライバシーポリシー'], ]; private static $default_button_text = [ // button text in different languages 'da' => 'Indlæs eksternt indhold', 'de' => 'Externen Inhalt laden', 'en' => 'Load external content', 'es' => 'Cargar contenido externo', 'fi' => 'Lataa ulkoista sisältöä', 'fr' => 'Charger le contenu externe', 'hu' => 'Külső tartalom betöltése', 'it' => 'Caricare il contenuto esterno', 'ja' => '外部のコンテンツを読み込ませます。', ]; //------------------------------------------------------------------------------------------------------------------- public static function init() { $st = microtime(true); add_shortcode('ma-content-consent', [__CLASS__, 'shortcode']); $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '', __CLASS__, __FUNCTION__, $et-$st));} } //------------------------------------------------------------------------------------------------------------------- private static function get_current_language(){ $retval = get_locale(); // Is Polylang available? if (function_exists('pll_current_language')) {$retval = pll_current_language();} elseif (function_exists('wpml_current_language')) {$retval = wpml_current_language();} $retval = str_replace('_','-',$retval); $retval = explode('-',@$retval)[0]; return $retval; } //------------------------------------------------------------------------------------------------------------------- private static function get_privacy_policy_link($text) { // return a link to the privacy policy (if configured) or just the passed text $pplink = get_the_privacy_policy_link(); return $pplink ? $pplink : $text; } //------------------------------------------------------------------------------------------------------------------- public static function shortcode($atts, $content = '') { $st = microtime(true); $lang = self::get_current_language(); if (!is_array($atts)) $atts = []; // fix Gutenberg content: remove </p> ... <p> $content = preg_replace('/^<\/p>[ \r\n]*/','',$content); // remove leading </p> tag, spaces, line breaks from shortcode start tag $content = preg_replace('/[ \r\n]*<p>$/','',$content); // remove trailing line breaks, spaces, <p> tag from shortcode end tag // evaluate nested shortcodes $content = do_shortcode($content); // get defaults for unspecified atts $atts = (object)array_merge([ 'slug' => self::SLUG, 'id' => uniqid(self::SLUG . '-'), 'block-style' => '', 'block-class' => '', 'text' => isset(self::$default_consent_text[$lang]) ? sprintf(self::$default_consent_text[$lang][0],self::get_privacy_policy_link(self::$default_consent_text[$lang][1])) : sprintf(self::$default_consent_text['en'][0],self::get_privacy_policy_link(self::$default_consent_text['en'][1])), 'text-style' => '', 'text-class' => '', 'button-text' => self::$default_button_text[$lang] ?? self::$default_button_text['en'], 'button-style' => '', 'button-class' => '', 'background-image' => null, 'contentb64' => base64_encode($content), ], $atts); $atts->{'background-style'} = $atts->{'background-image'} ? 'background-image:url('.$atts->{'background-image'}.'); background-size:cover; background-position:center center;' : ''; $atts->{'text'} = str_replace('{privacy-policy-url}', get_privacy_policy_url(), $atts->{'text'}); $atts->{'text'} = str_replace('{privacy-policy-link}', get_the_privacy_policy_link(), $atts->{'text'}); // base64 decode supporting UTF-8 from https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings#answer-30106551 $html = <<<END_OF_HTML <div id="{$atts->id}" class="{$atts->slug} {$atts->{'block-class'}}" style="display:flex; flex-direction:column; justify-content:center; align-content:center; background-color:#efefef; border: 1px solid lightgray; padding:1em; width: 100%; {$atts->{'background-style'}} {$atts->{'block-style'}} "> <div style="text-align:center"> <p class="{$atts->slug}__text {$atts->{'text-class'}}" style="font-size:.7em; {$atts->{'text-style'}}">{$atts->{'text'}}</p> <span id="{$atts->id}__button" class="wp-block-button__link {$atts->slug}__button {$atts->{'button-class'}}" style="display:inline-block; {$atts->{'button-style'}}">{$atts->{'button-text'}}</span> </div> <div id="{$atts->id}__content" class="{$atts->slug}__content" style="display:none">{$atts->contentb64}</div> <script> document.querySelector('[id^="{$atts->id}__button"]').addEventListener('click',function(){ let consent = this.closest('div.ma-content-consent'); let b64 = consent.querySelector('.ma-content-consent__content').innerText; let content = decodeURIComponent(atob(b64).split('').map(function(c) { return '%'+('00'+c.charCodeAt(0).toString(16)).slice(-2); }).join('')); consent.outerHTML = content; }); </script> </div> END_OF_HTML; $et = microtime(true); if (WP_DEBUG && self::$timing) {error_log(sprintf('%s%s::%s() Timing: %.5f sec.', '-> ', __CLASS__, __FUNCTION__, $et-$st));} return $html; } } //=================================================================================================================== // Initialize // Warn about incompatibilities (currently none) add_action('wp_loaded',function(){ if (is_admin()) { if (!isset($GLOBALS['MA_Content_Consent_Incompatibilities'])) {$GLOBALS['MA_Content_Consent_Incompatibilities'] = [];} if (count($GLOBALS['MA_Content_Consent_Incompatibilities'])) { if (WP_DEBUG && (MA_Content_Consent::$debug || MA_Content_Consent::$timing)) {error_log('MA_Content_Consent / Incompatibilities: '.print_r($GLOBALS['MA_Content_Consent_Incompatibilities'],true));} add_action('admin_notices', function(){ if (WP_DEBUG ) {error_log('MA_Content_Consent / Incompatibilities: '.print_r($GLOBALS['MA_Content_Consent_Incompatibilities'],true));} $implementation = basename(__FILE__) == 'ma-content-consent.php' ? 'Plugin' : 'Code Snippet'; echo '<div class="notice notice-warning xis-dismissible"> <p>The '.$implementation.' "MA Content Consent" is skipped: '.implode(' or ',$GLOBALS['MA_Content_Consent_Incompatibilities']).'</p> </div>'; }); } } }, 1000); add_action('wp_loaded',function(){ if (count($GLOBALS['MA_Content_Consent_Incompatibilities']??[])) return; if (wp_doing_ajax()) return; // don't run for AJAX requests if (wp_doing_cron()) return; // don't run for CRON requests if (wp_is_json_request()) return; // don't run for JSON requests if (is_favicon()) return; // don't run for favicon request if (($_SERVER['QUERY_STRING']??'') == 'service-worker') return; // don't run for service-worker if (($_SERVER['REQUEST_URI']??'') == '/favicon.ico') return; // don't run for favicon if ((strpos($_SERVER['REQUEST_URI']??'','/wp-content/') === 0)) return; // don't run for dynamic wp-content file if (is_admin()) { global $pagenow; if ( ($pagenow != 'post-new.php') && ( ($pagenow != 'post.php') || ($pagenow == 'post.php' && (($_REQUEST['action']??null) != 'edit')) ) ) return; // only load on specific requests where Gutenberg is involved } $implementation = basename(__FILE__) == 'ma-content-consent.php' ? 'Plugin' : 'Code Snippet'; if (WP_DEBUG && (MA_Content_Consent::$debug || MA_Content_Consent::$timing)) {error_log(sprintf('MA_Content_Consent: Initializing %s for request URI="%s" action="%s"', $implementation, $_SERVER['REQUEST_URI']??'', $_REQUEST['action']??''));} MA_Content_Consent::init(); }, 1200); endif;