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,
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
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['cookie_providers'] = get_cookie_providers(request.event, request)
if get_or_create_cart_id(request, create=False):
c = cart_session(request)
if "widget_data" in c and c["widget_data"].get("consent"):
ctx['cookie_consent_from_widget'] = c["widget_data"].get("consent").split(",")
if 'requested_consent_from_widget' in request.session:
# We only need to present this to the frontend once, JavaScript will then save it to localStorage/sessionStorage
ctx['cookie_consent_from_widget'] = request.session.pop("requested_consent_from_widget").split(",")
if request.resolver_match:
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:
request.session['iframe_session'] = True
if request.GET.get("consent"):
request.session["requested_consent_from_widget"] = request.GET["consent"]
locale = request.GET.get('locale')
if locale and locale in [lc for lc, ll in settings.LANGUAGES]:
lng = locale

View File

@@ -4,8 +4,21 @@ $(function () {
window.pretix = window.pretix || {};
var storage_key = $("#cookie-consent-storage-key").text();
function update_consent(consent) {
if (storage_key && window.localStorage) window.localStorage[storage_key] = JSON.stringify(consent);
var widget_consent = $("#cookie-consent-from-widget").text();
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;
// Event() is not supported by IE11, see ployfill here:
@@ -16,44 +29,69 @@ $(function () {
}
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;
}
if (!window.localStorage) {
// Consent not supported. Even IE8 supports it, so we're on a weird embedded device.
// Let's just say we don't consent then.
update_consent({})
update_consent({}, false)
return;
}
var storage_val = window.localStorage[storage_key];
var show_dialog = !storage_val;
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = $("#cookie-consent-modal");
var widget_consent = $("#cookie-consent-from-widget").text();
if (widget_consent) {
var storage_val, consent_source, save_for_session_only;
if (window.sessionStorage[storage_key]) {
// A manual input was given inside a widget. This is the user's last explicit choice and takes precedence
// as long as they are in the widget.
storage_val = JSON.parse(window.sessionStorage[storage_key]);
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);
storage_val = {}
storage_val = {};
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1;
})
show_dialog = false;
$("#cookie-consent-reopen").hide();
} else if (storage_val) {
storage_val = JSON.parse(storage_val);
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
show_dialog = true;
} else if (storage_val[this.name]) {
this.checked = true;
}
})
});
consent_source = 'widget';
save_for_session_only = !!window.localStorage[storage_key];
} else if (window.localStorage[storage_key]) {
// The user made a specific selection, let's use that.
storage_val = JSON.parse(window.localStorage[storage_key]);
consent_source = 'localStorage';
save_for_session_only = false;
} 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 () {
var btn = $("#cookie-consent-button-no");
@@ -83,7 +121,8 @@ $(function () {
consent[this.name] = this.checked = consent_all || this.checked;
});
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);
$("#cookie-consent-reopen").on("click", function (e) {

View File

@@ -313,9 +313,9 @@ Vue.component('availbox', {
waiting_list_url: function () {
var u
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 {
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) {
u += '&subevent=' + this.$root.subevent
@@ -786,6 +786,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams;
}
redirect_url += this.$root.consent_parameter;
this.$root.overlay.frame_src = redirect_url;
},
voucher_open: function (voucher) {
@@ -797,6 +798,7 @@ var shared_methods = {
if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams;
}
redirect_url += this.$root.consent_parameter;
if (this.$root.useIframe) {
this.$root.overlay.frame_src = redirect_url;
} else {
@@ -815,7 +817,7 @@ var shared_methods = {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
}
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) {
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="subevent" :value="$root.subevent" />'
+ '<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
+ '<div class="pretix-widget-error-message" v-if="$root.error">{{ $root.error }}</div>'
@@ -1072,6 +1075,7 @@ Vue.component('pretix-widget-event-form', {
+ '</div>'
+ '<input type="hidden" name="subevent" :value="$root.subevent" />'
+ '<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 + '" />'
+ '<div class="pretix-widget-voucher-button-wrap">'
+ '<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="locale" :value="$root.lang" />'
+ '<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" />'
+ '<button class="pretix-button" @click="buy" v-html="$root.button_text"></button>'
+ '</form>'
@@ -1923,6 +1928,7 @@ var shared_root_methods = {
if (this.$root.additionalURLParams) {
redirect_url += '&' + this.$root.additionalURLParams;
}
redirect_url += this.$root.consent_parameter;
if (this.$root.useIframe) {
this.$root.overlay.frame_src = redirect_url;
} else {
@@ -2033,8 +2039,26 @@ var shared_root_computed = {
}
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 () {
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 () {
if (!window.location.search.indexOf('utm_')) {