New mechanism to transfer cookie consent from the widget (Z#23181715) (#4875)

* Cookie consent: Add separate storage layer for widget

* Widget: Move cookie consent out of widget_data

* Add consent parameter to forms
This commit is contained in:
Raphael Michel
2025-03-04 15:28:03 +01:00
committed by GitHub
parent 0e17ac6ea5
commit d9e8dd70e4
4 changed files with 100 additions and 36 deletions

View File

@@ -52,7 +52,6 @@ from .signals import (
footer_link, global_footer_link, global_html_footer, global_html_head, footer_link, global_footer_link, global_html_footer, global_html_head,
global_html_page_header, html_footer, html_head, html_page_header, global_html_page_header, html_footer, html_head, html_page_header,
) )
from .views.cart import cart_session, get_or_create_cart_id
from .views.theme import _get_source_cache_key from .views.theme import _get_source_cache_key
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -157,10 +156,9 @@ def _default_context(request):
ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales] ctx['languages'] = [get_language_info(code) for code in request.event.settings.locales]
ctx['cookie_providers'] = get_cookie_providers(request.event, request) ctx['cookie_providers'] = get_cookie_providers(request.event, request)
if get_or_create_cart_id(request, create=False): if 'requested_consent_from_widget' in request.session:
c = cart_session(request) # We only need to present this to the frontend once, JavaScript will then save it to localStorage/sessionStorage
if "widget_data" in c and c["widget_data"].get("consent"): ctx['cookie_consent_from_widget'] = request.session.pop("requested_consent_from_widget").split(",")
ctx['cookie_consent_from_widget'] = c["widget_data"].get("consent").split(",")
if request.resolver_match: if request.resolver_match:
ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '') ctx['cart_namespace'] = request.resolver_match.kwargs.get('cart_namespace', '')

View File

@@ -467,6 +467,9 @@ def iframe_entry_view_wrapper(view_func):
if 'iframe' in request.GET: if 'iframe' in request.GET:
request.session['iframe_session'] = True request.session['iframe_session'] = True
if request.GET.get("consent"):
request.session["requested_consent_from_widget"] = request.GET["consent"]
locale = request.GET.get('locale') locale = request.GET.get('locale')
if locale and locale in [lc for lc, ll in settings.LANGUAGES]: if locale and locale in [lc for lc, ll in settings.LANGUAGES]:
lng = locale lng = locale

View File

@@ -4,8 +4,21 @@ $(function () {
window.pretix = window.pretix || {}; window.pretix = window.pretix || {};
var storage_key = $("#cookie-consent-storage-key").text(); var storage_key = $("#cookie-consent-storage-key").text();
function update_consent(consent) { var widget_consent = $("#cookie-consent-from-widget").text();
if (storage_key && window.localStorage) window.localStorage[storage_key] = JSON.stringify(consent); var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = $("#cookie-consent-modal");
function update_consent(consent, sessionOnly) {
if (storage_key && window.sessionStorage && sessionOnly) {
if (!window.localStorage[storage_key] || window.localStorage[storage_key] !== JSON.stringify(consent)) {
// No need to write to sessionStorage if the value is identical to the one in localStorage
window.sessionStorage[storage_key] = JSON.stringify(consent);
}
} else if (storage_key && window.localStorage) {
window.localStorage[storage_key] = JSON.stringify(consent);
// When saving permanent storage, clear session storage
window.sessionStorage.removeItem(storage_key);
}
window.pretix.cookie_consent = consent; window.pretix.cookie_consent = consent;
// Event() is not supported by IE11, see ployfill here: // Event() is not supported by IE11, see ployfill here:
@@ -16,44 +29,69 @@ $(function () {
} }
if (!storage_key) { if (!storage_key) {
update_consent(null); // We are not on a page where the consent should run, fire the change event with empty consent but don't
// actually store anything.
update_consent(null, false);
return; return;
} }
if (!window.localStorage) { if (!window.localStorage) {
// Consent not supported. Even IE8 supports it, so we're on a weird embedded device. // Consent not supported. Even IE8 supports it, so we're on a weird embedded device.
// Let's just say we don't consent then. // Let's just say we don't consent then.
update_consent({}) update_consent({}, false)
return; return;
} }
var storage_val = window.localStorage[storage_key]; var storage_val, consent_source, save_for_session_only;
var show_dialog = !storage_val; if (window.sessionStorage[storage_key]) {
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]"); // A manual input was given inside a widget. This is the user's last explicit choice and takes precedence
var consent_modal = $("#cookie-consent-modal"); // as long as they are in the widget.
var widget_consent = $("#cookie-consent-from-widget").text(); storage_val = JSON.parse(window.sessionStorage[storage_key]);
if (widget_consent) { consent_source = 'sessionStorage';
save_for_session_only = true;
} else if (widget_consent) {
// An input was given through the widget. This takes precedence over localStorage as we need to assume the
// widget embedder is doing a correct job. If the user never visited the page without the widget, we also
// use it to prefill local storage to save the user from seeing more cookie banners. (This will stop working
// when browsers partition local storage of iframes, anyway.) If the user does have visited the page without
// the widget before and has a consent setting in localStorage, we respect the widget consent *only* within
// the widget -- hence, we save it into sessionStorage. We need to save it into sessionStorage because the
// widget_data value itself will not "survive" the entire lifetime of the tab, i.e. it is no longer present
// after the order was confirmed.
widget_consent = JSON.parse(widget_consent); widget_consent = JSON.parse(widget_consent);
storage_val = {} storage_val = {};
consent_checkboxes.each(function () { consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1; this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1;
}) });
show_dialog = false; consent_source = 'widget';
$("#cookie-consent-reopen").hide(); save_for_session_only = !!window.localStorage[storage_key];
} else if (storage_val) { } else if (window.localStorage[storage_key]) {
storage_val = JSON.parse(storage_val); // The user made a specific selection, let's use that.
consent_checkboxes.each(function () { storage_val = JSON.parse(window.localStorage[storage_key]);
if (typeof storage_val[this.name] === "undefined") { consent_source = 'localStorage';
// A new cookie type has been added that we haven't asked for yet save_for_session_only = false;
show_dialog = true;
} else if (storage_val[this.name]) {
this.checked = true;
}
})
} else { } else {
storage_val = {} // No consent given, dialog will be shown.
storage_val = {};
consent_source = 'new';
save_for_session_only = false;
} }
update_consent(storage_val);
var show_dialog = false;
consent_checkboxes.each(function () {
if (typeof storage_val[this.name] === "undefined") {
// A new cookie type has been added that we haven't asked for yet
if (consent_source === "widget") {
// Trust the widget, keep it as "no consent"
} else {
show_dialog = true;
}
} else if (storage_val[this.name]) {
this.checked = true;
}
})
update_consent(storage_val, save_for_session_only);
function _set_button_text () { function _set_button_text () {
var btn = $("#cookie-consent-button-no"); var btn = $("#cookie-consent-button-no");
@@ -83,7 +121,8 @@ $(function () {
consent[this.name] = this.checked = consent_all || this.checked; consent[this.name] = this.checked = consent_all || this.checked;
}); });
if (consent_all) _set_button_text(); if (consent_all) _set_button_text();
update_consent(consent); // Always save explicit consent to permanent storage
update_consent(consent, false);
}); });
consent_checkboxes.on("change", _set_button_text); consent_checkboxes.on("change", _set_button_text);
$("#cookie-consent-reopen").on("click", function (e) { $("#cookie-consent-reopen").on("click", function (e) {

View File

@@ -313,9 +313,9 @@ Vue.component('availbox', {
waiting_list_url: function () { waiting_list_url: function () {
var u var u
if (this.item.has_variations) { if (this.item.has_variations) {
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&var=' + this.variation.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
} else { } else {
u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); u = this.$root.target_url + 'w/' + widget_id + '/waitinglist/?item=' + this.item.id + '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
} }
if (this.$root.subevent) { if (this.$root.subevent) {
u += '&subevent=' + this.$root.subevent u += '&subevent=' + this.$root.subevent
@@ -786,6 +786,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) { if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
redirect_url += this.$root.consent_parameter;
this.$root.overlay.frame_src = redirect_url; this.$root.overlay.frame_src = redirect_url;
}, },
voucher_open: function (voucher) { voucher_open: function (voucher) {
@@ -797,6 +798,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) { if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
redirect_url += this.$root.consent_parameter;
if (this.$root.useIframe) { if (this.$root.useIframe) {
this.$root.overlay.frame_src = redirect_url; this.$root.overlay.frame_src = redirect_url;
} else { } else {
@@ -815,7 +817,7 @@ var shared_methods = {
redirect_url += '&take_cart_id=' + this.$root.cart_id; redirect_url += '&take_cart_id=' + this.$root.cart_id;
} }
if (this.$root.widget_data) { if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json); redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json) + this.$root.consent_parameter;
} }
if (this.$root.additionalURLParams) { if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
@@ -1017,6 +1019,7 @@ Vue.component('pretix-widget-event-form', {
+ '<input type="hidden" name="_voucher_code" :value="$root.voucher_code" v-if="$root.voucher_code">' + '<input type="hidden" name="_voucher_code" :value="$root.voucher_code" v-if="$root.voucher_code">'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />' + '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />' + '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />'
+ '<input v-if="$root.consent_parameter_value" type="hidden" name="consent" :value="$root.consent_parameter_value" />'
// Error message // Error message
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>' + '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
@@ -1072,6 +1075,7 @@ Vue.component('pretix-widget-event-form', {
+ '</div>' + '</div>'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />' + '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />' + '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />'
+ '<input v-if="$root.consent_parameter_value" type="hidden" name="consent" :value="$root.consent_parameter_value" />'
+ '<input type="hidden" name="locale" value="' + lang + '" />' + '<input type="hidden" name="locale" value="' + lang + '" />'
+ '<div class="pretix-widget-voucher-button-wrap">' + '<div class="pretix-widget-voucher-button-wrap">'
+ '<button @click="$parent.redeem">' + strings.redeem + '</button>' + '<button @click="$parent.redeem">' + strings.redeem + '</button>'
@@ -1706,6 +1710,7 @@ Vue.component('pretix-button', {
+ '<input type="hidden" name="subevent" :value="$root.subevent" />' + '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<input type="hidden" name="locale" :value="$root.lang" />' + '<input type="hidden" name="locale" :value="$root.lang" />'
+ '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />' + '<input type="hidden" name="widget_data" :value="$root.widget_data_json" />'
+ '<input v-if="$root.consent_parameter_value" type="hidden" name="consent" :value="$root.consent_parameter_value" />'
+ '<input type="hidden" v-for="item in $root.items" :name="item.item" :value="item.count" />' + '<input type="hidden" v-for="item in $root.items" :name="item.item" :value="item.count" />'
+ '<button class="pretix-button" @click="buy" v-html="$root.button_text"></button>' + '<button class="pretix-button" @click="buy" v-html="$root.button_text"></button>'
+ '</form>' + '</form>'
@@ -1923,6 +1928,7 @@ var shared_root_methods = {
if (this.$root.additionalURLParams) { if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams; redirect_url += '&' + this.$root.additionalURLParams;
} }
redirect_url += this.$root.consent_parameter;
if (this.$root.useIframe) { if (this.$root.useIframe) {
this.$root.overlay.frame_src = redirect_url; this.$root.overlay.frame_src = redirect_url;
} else { } else {
@@ -2033,8 +2039,26 @@ var shared_root_computed = {
} }
return has_priced || cnt_items > 1; return has_priced || cnt_items > 1;
}, },
consent_parameter_value: function () {
if (typeof this.widget_data["consent"] !== "undefined") {
return encodeURIComponent(this.widget_data["consent"]);
}
return "";
},
consent_parameter: function () {
if (typeof this.widget_data["consent"] !== "undefined") {
return "&consent=" + encodeURIComponent(this.widget_data["consent"]);
}
return "";
},
widget_data_json: function () { widget_data_json: function () {
return JSON.stringify(this.widget_data); var cloned_data = Object.assign({}, this.widget_data);
if (typeof cloned_data["consent"] !== "undefined") {
// Remove consent as we pass it differently. We still keep it as widget_data in the input to avoid breaking
// the JS API of the widget.
delete cloned_data["consent"];
}
return JSON.stringify(cloned_data);
}, },
additionalURLParams: function () { additionalURLParams: function () {
if (!window.location.search.indexOf('utm_')) { if (!window.location.search.indexOf('utm_')) {