Skip to content

Commit

Permalink
mod_cookie_consent: implements cookie consent handling and settings (#…
Browse files Browse the repository at this point in the history
…3184)

* mod_cookie_consent: implements cookie consent handling and settings

* Use figure/figcaption for placeholder

* Add link to explanation page and nl texts

* Use category 'other'

* Run widgetManager of replace cookie content

* Default reload - changes to admin page for consent text
  • Loading branch information
mworrell committed Nov 21, 2022
1 parent fa9c775 commit 2004bd7
Show file tree
Hide file tree
Showing 23 changed files with 1,491 additions and 117 deletions.
13 changes: 13 additions & 0 deletions include/zotonic_notifications.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,19 @@
%% Return {ok, ResourceId} or undefined
-record(media_stillimage, {id, props=[]}).

%% @doc Optionally wrap HTML with external content so that it adheres to the cookie/privacy
%% settings of the current site visitor. Typically called with a 'first' by the code that
%% generated the media viewer HTML, as that code has the knowledge if viewing the generated code
%% has any privacy or cookie implications.
%% Return {ok, HTML} or undefined
-record(media_viewer_consent, {
id :: m_rsc:resource_id() | undefined,
consent = all :: functional | stats | all,
html :: iodata(),
viewer_props = [] :: list(),
viewer_options = [] :: list()
}).


%% @doc Fetch lisy of handlers. (foldr)
-record(survey_get_handlers, {}).
Expand Down
41 changes: 32 additions & 9 deletions modules/mod_base/lib/js/apps/z.widgetmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ limitations under the License.
{
widgetManager: function(context)
{
context = context || document.body;
var stack = [context];
let stack = [context || document.body];
let nodes = [];

// 1. Collect nodes
while (stack.length > 0)
{
var defaults, element = stack.pop();
var defaults;
var element = stack.pop();

if (typeof element.className == "string")
{
var objectClass = element.className.match(/do_[a-zA-Z0-9_]+/g);
Expand All @@ -44,7 +47,10 @@ limitations under the License.
var functionName = objectClass[i].substring(3);
var defaultsName = functionName;

if ('dialog' == functionName) functionName = 'show_dialog'; // work around to prevent ui.dialog redefinition
if ('dialog' == functionName)
{
functionName = 'show_dialog'; // work around to prevent ui.dialog redefinition
}

if (typeof $(element)[functionName] == "function")
{
Expand All @@ -56,7 +62,12 @@ limitations under the License.
{
defaults = {}
}
$(element)[functionName]( $.extend({}, defaults, $(element).metadata(defaultsName)) );
nodes.push({
element: element,
functionName: functionName,
defaults: defaults,
defaultsName: defaultsName
});
}
}
}
Expand All @@ -73,6 +84,12 @@ limitations under the License.
}
}
}

while (nodes.length > 0)
{
let n = nodes.pop();
$(n.element)[n.functionName]( $.extend({}, n.defaults, $(n.element).metadata(n.defaultsName)) );
}
},

misc:
Expand Down Expand Up @@ -160,23 +177,29 @@ limitations under the License.
if(typeof data === "undefined")
{
data = elem.getAttribute("data-"+functionName);
if (data) {
if (data.substr(0,1) == "{") {
if (data)
{
if (data.substr(0,1) == "{")
{
try {
data = JSON.parse(data);
} catch (e) {
console.error("Error parsing JSON in widget data attribute:", data);
data = {};
}
} else {
}
else
{
try {
data = eval("({" + data.replace(/[\n\r]/g,' ') + "})");
} catch (e) {
console.error("Error evaluating widget data attribute:", data);
data = {};
}
}
} else {
}
else
{
data = {};
}
$(elem).data(data_name, data);
Expand Down
149 changes: 149 additions & 0 deletions modules/mod_base/lib/js/apps/zotonic-1.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ function z_event_register(name, func)
z_registered_events[name] = func;
}

function z_event_remove(name)
{
delete z_registered_events[name];
}

function z_event(name, extraParams)
{
if (z_registered_events[name])
Expand Down Expand Up @@ -1108,6 +1113,14 @@ function z_translate(text)
return text;
}

function z_translation_set(text, trans)
{
if (typeof z_translations == "undefined") {
z_translations = {};
}
z_translations[text] = trans;
}


/* Render text as html nodes
---------------------------------------------------------- */
Expand Down Expand Up @@ -2200,6 +2213,142 @@ function z_update_iframe(name, doc)
}
}


// Store the current cookie consent status
function z_cookie_consent_store( status )
{
if (status !== 'all') {
z_cookie_remove_all();
}
switch (status) {
case "functional":
case "stats":
case "all":
const prev = z_cookie_consent_cache;
window.z_cookie_consent_cache = status;
try {
// Use stringify to be compatible with model.localStorage
localStorage.setItem('z_cookie_consent', JSON.stringify(status));
} catch (e) {
}
const ev = new CustomEvent("zotonic:cookie-consent", {
detail: {
cookie_consent: status
}
});
if (prev != status) {
window.dispatchEvent(ev);
}
break;
default:
console.error("Cookie consent status must be one of 'all', 'stats' or 'functional'", status);
break;
}
}

// Trigger on consent changes in other windows/tabs
window.addEventListener("storage", function(ev) {
if (ev.key == 'z_cookie_consent') {
if (ev.newValue === null) {
window.z_cookie_consent_cache = 'functional';
} else if (ev.oldValue != ev.newValue) {
z_cookie_consent_store(ev.newValue);
}
}
}, false);


// Fetch the current cookie consent status - default to 'functional'
function z_cookie_consent_fetch()
{
if (window.z_cookie_consent_cache) {
return window.z_cookie_consent_cache;
} else {
let status;

try {
status = localStorage.getItem('z_cookie_consent');
} catch (e) {
status = null;
}

if (status !== null) {
status = JSON.parse(status);
} else {
status = 'functional';
}
window.z_cookie_consent_cache = status
return status;
}
}

// Check is the user consented to some cookies
function z_cookie_consent_given()
{
try {
return typeof (localStorage.getItem('z_cookie_consent')) === 'string';
} catch (e) {
return false;
}
}


// Check if something is allowed according to the stored consent status
function z_cookie_consented( wanted )
{
const consent = z_cookie_consent_fetch();

switch (wanted) {
case 'functional':
return true;
case 'stats':
return consent === 'all' || consent === 'stats';
case 'all':
return consent === 'all';
default:
return false;
}
}

// Remove all non-functional cookies from the current document domain
function z_cookie_remove_all()
{
for ( const cookie of document.cookie.split(';') ){
const cookieName = cookie.split('=')[0].trim();

switch (cookieName) {
case "z_sid":
case "z_rldid":
case "z_ua":
case "z.sid":
case "z.lang":
case "z.auth":
case "z.autologon":
// Functional - keep the cookie
break;
default:
// Non-functional - remove the cookie
let domains = window.location.hostname.split('.');
while ( domains.length > 0 ) {
const domain = domains.join('.');
const cookieReset = encodeURIComponent(cookieName) + '=; expires=Thu, 01-Jan-1970 00:00:01 GMT';

document.cookie = cookieReset;
document.cookie = cookieReset + '; domain=' + domain + ' ;path=/';

let pathSegments = location.pathname.split('/');
while ( pathSegments.length > 0 ){
const path = pathSegments.join('/');
document.cookie = cookieReset + '; domain=' + domain + ' ;path=' + path;
pathSegments.pop();
}
domains.shift();
}
break;
}
}
}

// From: http://malsup.com/jquery/form/jquery.form.js

/*
Expand Down
71 changes: 71 additions & 0 deletions modules/mod_cookie_consent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
Cookie consent
==============

Wrap external content in such a way that it is only loaded if the user consented to the
inclusion of the content (and subsequent cookies).

The consent is one of:

- `functional` this is always allowed
- `stats` if consent for statistics tracking is given
- `all` for any other kind of cookies

For elements this defaults to `all`. This means that they are only rendered if all consent is given.

How to use
----------

Ensure that your base template has an all-include of `_html_head.tpl` and `_html_body.tpl`.

Also, if you are using IFRAMEs, JS or CSS that sets non-functional cookies, check the changes below.

HTML
----

Media embedded via mod_oembed or mod_video_embed are automatically wrapped according
to this method.

<pre>
<figure class="cookie-consent-preview do_cookie_consent mediaclass-..." data-cookie-consent="all">
<img src="..." alt="Media preview">
<figcaption>Please consent to cookies to display external content.</figcaption>
<script type="text/x-cookie-consented">
{% filter escape %}
<iframe width="560" height="315" src="https://www.youtube.com/embed/IdIb5RPabjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
{% endfilter %}
</script>
</figure>
</pre>

If there is no script tag then the page is reloaded after cookies are accepted.


IFRAME
------

Use the `data-cookie-consent-src` attribute to define the `src` if the cookie consent has been
given.

<pre>
<iframe width="560" height="315" data-cookie-consent-src="https://www.youtube.com/embed/...."></iframe>
</pre>


JAVASCRIPT
----------

Use the special `type="text/x-cookie-consent"` and optionally the `data-cookie-consent="..."` attribute:

<pre>
<script type="text/x-cookie-consent" data-cookie-consent="stats" src="https://..."></script>
</pre>


CSS
---

Use the special `type="text/x-cookie-consent"` and optionally the `data-cookie-consent="..."` attribute:

<pre>
<link type="text/x-cookie-consent" data-cookie-consent="stats" href="https://..."></script>
</pre>

0 comments on commit 2004bd7

Please sign in to comment.