Blog

Code Snippet: Zustimmung für externe Inhalte

Version: 1.1.1 (15.11.2022)

Inhalt

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;"] ...

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"] ...

text

Im Code Snippet ist ein Hinweis-Text zu den Datenschutzrichtlinien in verschiedenen Sprachen hinterlegt:

SpracheText
DANår du har trykket, vil indholdet blive hentet fra eksterne servere. Se vores privatlivspolitik for flere informationer.
DEBei Klick wird dieser Inhalt von externen Servern geladen. Details siehe Datenschutzerklärung.
ENWhen clicked, this content is loaded from external servers. See our privacy policy for details.
ESAl hacer clic, este contenido se carga desde servidores externos. Consulte la política de privacidad para más detalles.
FREn cliquant, ce contenu est chargé depuis des serveurs externes. Voir la politique de confidentialité.
HUHa 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.
ITQuando viene cliccato, questo contenuto viene caricato da server esterni. Vedere l'informativa sulla privacy per i dettagli.

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."]

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;"] ...

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"] ...

button-text

Im Code Snippet ist die Button-Beschriftung in in verschiedenen Sprachen hinterlegt:

SpracheText
DAIndlæs eksternt indhold
DEExternen Inhalt laden
ENLoad external content
ESCargar contenido externo
FRCharger le contenu externe
HUKülső tartalom betöltése
ITCaricare il contenuto esterno

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"] ...

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;"] ...

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"] ...

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..."] ...

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.1, 2022-11-15

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.

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.

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.

  Bei Klick auf den Button wird eine Verbindung zu PayPal aufgebaut.

Spenden werden selbstverständlich ordnungsgemäß durch mich versteuert.

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.1
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-2022, Matthias Altmann



Version History:
Date		Version		Description
--------------------------------------------------------------------------------------------------------------
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.1';


	// ===== 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'],
		'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',
		'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'});


		$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>
				<a id="{$atts->id}__button" class="wp-block-button__link {$atts->slug}__button {$atts->{'button-class'}}" style="{$atts->{'button-style'}}">{$atts->{'button-text'}}</a>
			</div>
			<div id="{$atts->id}__content" class="{$atts->slug}__content" style="display:none">{$atts->contentb64}</div>
			<script>
			document.querySelectorAll('#{$atts->id}__button').forEach(function(btn){
				btn.addEventListener('click',function(){
					let id = this.closest('div.{$atts->slug}').id;
					let b64 = document.querySelector('#'+id+'__content').innerText;
					let content = decodeURIComponent(atob(b64).split('').map(function(c) {
						return '%'+('00'+c.charCodeAt(0).toString(16)).slice(-2);
					}).join(''));
					this.closest('div.{$atts->slug}').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 (isset($GLOBALS['MA_Content_Consent_Incompatibilities']) && 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' && isset($_REQUEST['action']) && ($_REQUEST['action'] != '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;

Erstveröffentlichung: 03.02.2022 auf Code Snippet: Zustimmung für externe Inhalte
magnifier