From 95d1996205e3a7014670f59f24dcef8ac11ba337 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 30 Aug 2024 16:06:40 -0400 Subject: [PATCH 01/10] Bug 1879792 - Add a cookie banner to BMO --- Bugzilla/Constants.pm | 9 +- extensions/GoogleAnalytics/Config.pm | 16 -- extensions/GoogleAnalytics/Extension.pm | 23 -- extensions/GoogleAnalytics/lib/Config.pm | 38 --- .../admin/params/googleanalytics.html.tmpl | 20 -- .../global/header-additional_header.html.tmpl | 24 -- .../hook/global/header-start.html.tmpl | 19 -- .../GoogleAnalytics/web/js/analytics.js | 29 --- index.cgi | 2 - js/consent.js | 223 ++++++++++++++++++ js/cookie-helper.js | 117 +++++++++ js/global.js | 38 +++ skins/standard/consent.css | 151 ++++++++++++ template/en/default/global/footer.html.tmpl | 20 ++ template/en/default/global/header.html.tmpl | 4 + 15 files changed, 554 insertions(+), 179 deletions(-) delete mode 100644 extensions/GoogleAnalytics/Config.pm delete mode 100644 extensions/GoogleAnalytics/Extension.pm delete mode 100644 extensions/GoogleAnalytics/lib/Config.pm delete mode 100644 extensions/GoogleAnalytics/template/en/default/admin/params/googleanalytics.html.tmpl delete mode 100644 extensions/GoogleAnalytics/template/en/default/hook/global/header-additional_header.html.tmpl delete mode 100644 extensions/GoogleAnalytics/template/en/default/hook/global/header-start.html.tmpl delete mode 100644 extensions/GoogleAnalytics/web/js/analytics.js create mode 100644 js/consent.js create mode 100644 js/cookie-helper.js create mode 100644 skins/standard/consent.css diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 7af4609f34..3b133a0156 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -753,7 +753,7 @@ sub DEFAULT_CSP { my %policy = ( default_src => ['self'], script_src => - ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'], + ['self', 'nonce', 'unsafe-inline'], frame_src => [ # This is for extensions/BMO/web/js/firefox-crash-table.js 'https://crash-stop-addon.herokuapp.com', @@ -768,9 +768,6 @@ sub DEFAULT_CSP { # This is for extensions/BMO/web/js/firefox-crash-table.js 'https://product-details.mozilla.org', - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - # This is from extensions/OrangeFactor/web/js/orange_factor.js 'https://treeherder.mozilla.org/api/failurecount/', @@ -813,7 +810,6 @@ sub SHOW_BUG_MODAL_CSP { script_src => [ 'self', 'nonce', 'unsafe-inline', 'unsafe-eval', - 'https://www.google-analytics.com' ], img_src => ['self', 'data:', 'https://secure.gravatar.com'], media_src => ['self'], @@ -823,9 +819,6 @@ sub SHOW_BUG_MODAL_CSP { # This is for extensions/BMO/web/js/firefox-crash-table.js 'https://product-details.mozilla.org', - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - # This is from extensions/OrangeFactor/web/js/orange_factor.js 'https://treeherder.mozilla.org/api/failurecount/', ], diff --git a/extensions/GoogleAnalytics/Config.pm b/extensions/GoogleAnalytics/Config.pm deleted file mode 100644 index f4699db378..0000000000 --- a/extensions/GoogleAnalytics/Config.pm +++ /dev/null @@ -1,16 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -package Bugzilla::Extension::GoogleAnalytics; - -use 5.10.1; -use strict; -use warnings; - -use constant NAME => 'GoogleAnalytics'; - -__PACKAGE__->NAME; diff --git a/extensions/GoogleAnalytics/Extension.pm b/extensions/GoogleAnalytics/Extension.pm deleted file mode 100644 index fb7e8adaee..0000000000 --- a/extensions/GoogleAnalytics/Extension.pm +++ /dev/null @@ -1,23 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -package Bugzilla::Extension::GoogleAnalytics; - -use 5.10.1; -use strict; -use warnings; -use parent qw(Bugzilla::Extension); - -our $VERSION = '0.1'; - -sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{GoogleAnalytics} = "Bugzilla::Extension::GoogleAnalytics::Config"; -} - -__PACKAGE__->NAME; diff --git a/extensions/GoogleAnalytics/lib/Config.pm b/extensions/GoogleAnalytics/lib/Config.pm deleted file mode 100644 index 1c453ff74b..0000000000 --- a/extensions/GoogleAnalytics/lib/Config.pm +++ /dev/null @@ -1,38 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -package Bugzilla::Extension::GoogleAnalytics::Config; - -use 5.10.1; -use strict; -use warnings; - -use Bugzilla::Config::Common; - -sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'google_analytics_tracking_id', - type => 't', - default => '', - checker => sub { - my ($tracking_id) = (@_); - - return 'must be like UA-XXXXXX-X' - unless $tracking_id =~ m{^(UA-[[:xdigit:]]+-[[:xdigit:]]+)?$}; - return ''; - } - }, - {name => 'google_analytics_debug', type => 'b', default => 0}, - ); - - return @params; -} - -1; diff --git a/extensions/GoogleAnalytics/template/en/default/admin/params/googleanalytics.html.tmpl b/extensions/GoogleAnalytics/template/en/default/admin/params/googleanalytics.html.tmpl deleted file mode 100644 index 3fdce57e6d..0000000000 --- a/extensions/GoogleAnalytics/template/en/default/admin/params/googleanalytics.html.tmpl +++ /dev/null @@ -1,20 +0,0 @@ -[%# This Source Code Form is subject to the terms of the Mozilla Public - # License, v. 2.0. If a copy of the MPL was not distributed with this - # file, You can obtain one at http://mozilla.org/MPL/2.0/. - # - # This Source Code Form is "Incompatible With Secondary Licenses", as - # defined by the Mozilla Public License, v. 2.0. - #%] - -[% - title = "Google Analytics" - desc = "Configure Google Analytics" -%] - -[% - param_descs = { - google_analytics_tracking_id => "Google Analytics Tracking ID", - google_analytics_debug => "If this option is set, the debug version of the analytics.js " _ - "library will be used.", - } -%] diff --git a/extensions/GoogleAnalytics/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/GoogleAnalytics/template/en/default/hook/global/header-additional_header.html.tmpl deleted file mode 100644 index f96e0d0f9a..0000000000 --- a/extensions/GoogleAnalytics/template/en/default/hook/global/header-additional_header.html.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -[%# This Source Code Form is subject to the terms of the Mozilla Public - # License, v. 2.0. If a copy of the MPL was not distributed with this - # file, You can obtain one at http://mozilla.org/MPL/2.0/. - # - # This Source Code Form is "Incompatible With Secondary Licenses", as - # defined by the Mozilla Public License, v. 2.0. - #%] - -[%# Disable tracking of DNT-enabled users as well as private bugs #%] -[% RETURN IF !Bugzilla.params.google_analytics_tracking_id || Bugzilla.cgi.http('dnt') == '1' || - (bug.defined && bug.groups_in.size) %] - - - diff --git a/extensions/GoogleAnalytics/template/en/default/hook/global/header-start.html.tmpl b/extensions/GoogleAnalytics/template/en/default/hook/global/header-start.html.tmpl deleted file mode 100644 index 020cbb94a0..0000000000 --- a/extensions/GoogleAnalytics/template/en/default/hook/global/header-start.html.tmpl +++ /dev/null @@ -1,19 +0,0 @@ -[%# This Source Code Form is subject to the terms of the Mozilla Public - # License, v. 2.0. If a copy of the MPL was not distributed with this - # file, You can obtain one at http://mozilla.org/MPL/2.0/. - # - # This Source Code Form is "Incompatible With Secondary Licenses", as - # defined by the Mozilla Public License, v. 2.0. - #%] - -[% USE Bugzilla %] - -[%# Disable tracking of DNT-enabled users as well as private bugs #%] -[% RETURN IF !Bugzilla.params.google_analytics_tracking_id || Bugzilla.cgi.http('dnt') == '1' || - (bug.defined && bug.groups_in.size) %] - -[% IF !javascript_urls %] - [% javascript_urls = [] %] -[% END %] - -[% javascript_urls.push('extensions/GoogleAnalytics/web/js/analytics.js') %] diff --git a/extensions/GoogleAnalytics/web/js/analytics.js b/extensions/GoogleAnalytics/web/js/analytics.js deleted file mode 100644 index 1765611db6..0000000000 --- a/extensions/GoogleAnalytics/web/js/analytics.js +++ /dev/null @@ -1,29 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. - * - * This Source Code Form is "Incompatible With Secondary Licenses", as - * defined by the Mozilla Public License, v. 2.0. */ - -window.addEventListener('DOMContentLoaded', () => { - 'use strict'; - - const $meta = document.querySelector('meta[name="google-analytics"]'); - - if (!$meta) { - return; - } - - // Activate Google Analytics - window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date; - ga('create', $meta.content, 'auto'); - ga('set', 'anonymizeIp', true); - ga('set', 'transport', 'beacon'); - // Record a crafted location (template name) and title instead of actual URL - ga('set', 'location', $meta.dataset.location); - ga('set', 'title', $meta.dataset.title); - // Custom Dimension: logged in (true) or out (false) - ga('set', 'dimension1', !!BUGZILLA.user.login); - // Track page view - ga('send', 'pageview'); -}, { once: true }); diff --git a/index.cgi b/index.cgi index db836248d5..6d228946ea 100755 --- a/index.cgi +++ b/index.cgi @@ -69,8 +69,6 @@ if ( } else { my $template = Bugzilla->template; - $C->content_security_policy( - script_src => ['self', 'https://www.google-analytics.com']); # Return the appropriate HTTP response headers. print $cgi->header( diff --git a/js/consent.js b/js/consent.js new file mode 100644 index 0000000000..23a2782c4d --- /dev/null +++ b/js/consent.js @@ -0,0 +1,223 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +var MozConsentBanner = {}; +var EVENT_NAME_STATUS = 'mozConsentStatus'; +var EVENT_NAME_OPEN = 'mozConsentOpen'; +var EVENT_NAME_CLOSE = 'mozConsentClose'; +var COOKIE_ID = 'moz-consent-pref'; +var COOKIE_EXPIRY_DAYS = 182; // 6 months +var MSG_OPTIONS_ERROR_DISABLED = 'verifyOptions(): cookies are disabled.'; +var MSG_OPTIONS_ERROR_EXPIRY = 'verifyOptions(): options.cookieExpiryDays not set.'; +var MSG_OPTIONS_ERROR_HELPER = 'verifyOptions(): options.helper not set.'; +var MSG_OPTIONS_ERROR_OPT_OUT = 'verifyOptions(): options.optOut not set.'; +var MSG_OPTIONS_VALID = 'verifyOptions(): success.'; +var options; + +/** + * Sets cookie indicating consent choice. + * @param {Object} data - consent settings. + */ +MozConsentBanner.setConsentCookie = function (data) { + try { + var date = new Date(); + date.setDate(date.getDate() + options.cookieExpiryDays); + var expires = date.toUTCString(); + options.helper.setItem(options.cookieID, JSON.stringify(data), expires, '/', options.cookieDomain, false, 'lax'); + return true; + } catch (e) { + return false; + } +}; + +/** + * Get consent cookie value if set. + * @returns {Boolean} + */ +MozConsentBanner.getConsentCookie = function () { + try { + return options.helper.hasItem(options.cookieID) && JSON.parse(options.helper.getItem(options.cookieID)); + } catch (e) { + return false; + } +}; + +/** + * Returns the domain to set for the consent cookie. When in production + * we want to specify '.mozilla.org' instead of 'www.mozilla.org', + * so that multiple Mozilla sub domains can use the same consent cookie. + * @returns {domain} string or null. + */ +MozConsentBanner.getHostName = function (hostname) { + var url = typeof hostname === 'string' ? hostname : window.location.hostname; + var domain = null; + if (url.indexOf('.allizom.org') !== -1) { + domain = '.allizom.org'; + } + if (url.indexOf('.mozilla.org') !== -1) { + domain = '.mozilla.org'; + } + return domain; +}; + +/** + * Helper for dispatching a custom event with a + * given name and data payload. + * @param {Object} eventName + * @param {Object} eventData + */ +MozConsentBanner.dispatchEvent = function (eventName, eventData) { + if (typeof window.CustomEvent === 'function') { + // Modern browsers + window.dispatchEvent(new CustomEvent(eventName, { + detail: eventData + })); + } else if (typeof document.createEvent === 'function') { + // Internet Explorer 10 + var customEvent = document.createEvent('CustomEvent'); + customEvent.initCustomEvent(eventName, false, false, eventData); + window.dispatchEvent(customEvent); + } else if (typeof document.createEventObject === 'function') { + // Internet Explorer 9 + var customEventObj = document.createEventObject(); + customEventObj.type = eventName; + customEventObj.bubbles = false; + customEventObj.cancelable = false; + customEventObj.detail = eventData; // Optional data for the event + window.fireEvent('on' + customEventObj.type, customEventObj); + } +}; + +/** + * Event handler for accepting cookies. + */ +MozConsentBanner.onAcceptClick = function () { + var preferences = { + analytics: true, + preference: true + }; + MozConsentBanner.setConsentCookie(preferences); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, preferences); + MozConsentBanner.dispatchEvent(EVENT_NAME_CLOSE, {}); +}; + +/** + * Event handler for rejecting cookies. + */ +MozConsentBanner.onRejectClick = function () { + var preferences = { + analytics: false, + preference: false + }; + MozConsentBanner.setConsentCookie(preferences); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, preferences); + MozConsentBanner.dispatchEvent(EVENT_NAME_CLOSE, {}); +}; + +/** + * Validates BannerOptions configuration object. + * @param {BannerOptions} object containing one or more properties. + * @returns {String} message to indicate either success or error. + */ +MozConsentBanner.verifyOptions = function (options) { + // Verify cookie expiry has been set. + if (typeof options.cookieExpiryDays !== 'number') { + return MSG_OPTIONS_ERROR_EXPIRY; + } + + // Verify cookie helper dependency is defined. + if (!options.helper || typeof options.helper.setItem !== 'function') { + return MSG_OPTIONS_ERROR_HELPER; + } + + // Verify that optOut flag is set. + if (typeof options.optOut !== 'boolean') { + return MSG_OPTIONS_ERROR_OPT_OUT; + } + + // Verify that cookies are enabled in the browser. + if (!options.helper.enabled()) { + return MSG_OPTIONS_ERROR_DISABLED; + } + return MSG_OPTIONS_VALID; +}; + +/** + * MozConsentBanner.init() configuration options: + * @typedef {Object} BannerOptions + * @property {string} cookieDomain - Sets a consent cookie for a specific host name. + * @property {number} cookieExpiryDays - Consent cookie expiry by number of days. + * @property {string} cookieID - Consent cookie identifier. + * @property {object} helper (required) - reference to @mozmeao/cookie-helper peer dependency. + * @property {object} optOut - Sets the banner to be opt-out instead of the default opt-in. + */ + +/** + * Initializes cookie banner and binds events. + * @param {BannerOptions} object containing one or more properties. + * @returns {Boolean} to indicate if initialization succeeded. + */ +MozConsentBanner.init = function (config) { + /** + * Default configuration options that can be overridden via init(). + */ + var defaults = { + cookieDomain: null, + cookieExpiryDays: COOKIE_EXPIRY_DAYS, + cookieID: COOKIE_ID, + optOut: false, + eventName: EVENT_NAME_STATUS, + helper: null + }; + for (var opt in config) { + if (Object.prototype.hasOwnProperty.call(config, opt)) { + defaults[opt] = config[opt]; + } + } + options = defaults; + + /** + * If domain for consent cookie has not be supplied via config, + * set it via getHostName() + */ + if (!options.cookieDomain) { + options.cookieDomain = MozConsentBanner.getHostName(); + } + + /** + * Validate options object before trying to show the banner. + */ + var configValidationMsg = MozConsentBanner.verifyOptions(options); + if (configValidationMsg !== MSG_OPTIONS_VALID) { + if (window.console && window.console.error) { + console.error(configValidationMsg); // eslint-disable-line no-console + } + return false; + } + + /** + * If cookie exists and consent has previously been given, + * despatch consent event. Otherwise show the banner. + */ + var consent = MozConsentBanner.getConsentCookie(); + if (consent) { + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, consent); + } else { + MozConsentBanner.dispatchEvent(EVENT_NAME_OPEN, {}); + + /** + * If the banner is opt out, despatch event to indicate + * cookies and analytics are OK to load. + */ + if (options.optOut) { + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, { + preference: true, + analytics: true + }); + } + } + return true; +}; diff --git a/js/cookie-helper.js b/js/cookie-helper.js new file mode 100644 index 0000000000..d84f54bb5e --- /dev/null +++ b/js/cookie-helper.js @@ -0,0 +1,117 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +var CookieHelper = { + getItem: function getItem(sKey) { + 'use strict'; + + if (!sKey) { + return null; + } + return decodeURIComponent(document.cookie.replace(new RegExp('(?:(?:^|.*;)\\s*' + encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1')) || null; + }, + setItem: function setItem(sKey, sValue, vEnd, sPath, sDomain, bSecure, vSamesite) { + 'use strict'; + + if (!sKey || /^(?:expires|max-age|path|domain|secure|samesite)$/i.test(sKey)) { + return false; + } + var sExpires = ''; + if (vEnd) { + switch (vEnd.constructor) { + case Number: + sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : '; max-age=' + vEnd; + break; + case String: + sExpires = '; expires=' + vEnd; + break; + case Date: + sExpires = '; expires=' + vEnd.toUTCString(); + break; + } + } + vSamesite = this.checkSameSite(vSamesite); + + // setting the samesite attribute to 'none' requires the cookie to be 'secure' + if (vSamesite === 'none') { + bSecure = true; + } + document.cookie = encodeURIComponent(sKey) + '=' + encodeURIComponent(sValue) + sExpires + (sDomain ? '; domain=' + sDomain : '') + (sPath ? '; path=' + sPath : '') + (bSecure ? '; secure' : '') + (!vSamesite ? '' : '; samesite=' + vSamesite); + return true; + }, + removeItem: function removeItem(sKey, sPath, sDomain, bSecure, vSamesite) { + 'use strict'; + + if (!this.hasItem(sKey)) { + return false; + } + return this.setItem(sKey, '', 'Thu, 01 Jan 1970 00:00:00 GMT', sPath, sDomain, bSecure, vSamesite); + }, + hasItem: function hasItem(sKey) { + 'use strict'; + + if (!sKey) { + return false; + } + return new RegExp('(?:^|;\\s*)' + encodeURIComponent(sKey).replace(/[-.+*]/g, '\\$&') + '\\s*\\=').test(document.cookie); + }, + keys: function keys() { + 'use strict'; + + var aKeys = document.cookie.replace( + // see issue 11338. + // eslint-disable-next-line no-useless-backreference + /((?:^|\s*;)[^=]+)(?=;|$)|^\s*|\s*(?:=[^;]*)?(?:\1|$)/g, '').split(/\s*(?:=[^;]*)?;\s*/); + for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) { + aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]); + } + return aKeys; + }, + enabled: function enabled() { + 'use strict'; + + /** + * Cookies feature detect lifted from Modernizr + * https://github.com/Modernizr/Modernizr/blob/master/feature-detects/cookies.js + * + * navigator.cookieEnabled cannot detect custom or nuanced cookie blocking + * configurations. For example, when blocking cookies via the Advanced + * Privacy Settings in IE9, it always returns true. And there have been + * issues in the past with site-specific exceptions. + * Don't rely on it. + * try..catch because in some situations `document.cookie` is exposed but throws a + * SecurityError if you try to access it; e.g. documents created from data URIs + * or in sandboxed iframes (depending on flags/context) + */ + try { + // Create cookie + document.cookie = 'cookietest=1; SameSite=Lax'; + var ret = document.cookie.indexOf('cookietest=') !== -1; + // Delete cookie + document.cookie = 'cookietest=1; SameSite=Lax; expires=Thu, 01-Jan-1970 00:00:01 GMT'; + return ret; + } catch (e) { + return false; + } + }, + checkSameSite: function checkSameSite(vSamesite) { + 'use strict'; + + /** + * valid vSamesite values are 'lax', 'strict' and 'none' (case insensitive). + * otherwise it will be 'lax' + */ + if (!vSamesite) { + return null; + } + vSamesite = vSamesite.toString().toLowerCase(); + if (vSamesite === 'lax' || vSamesite === 'none' || vSamesite === 'strict') { + return vSamesite; + } else { + return 'lax'; + } + } +}; diff --git a/js/global.js b/js/global.js index 3e0a69c4cb..fe085d1329 100644 --- a/js/global.js +++ b/js/global.js @@ -257,6 +257,32 @@ const scroll_element_into_view = ($target, complete) => { document.documentElement.scrollTop = $target.offsetTop - 20; } +const openBanner = () => { + // Bind click event listeners for banner buttons + document + .getElementById("moz-consent-banner-button-accept") + .addEventListener("click", MozConsentBanner.onAcceptClick, false); + document + .getElementById("moz-consent-banner-button-reject") + .addEventListener("click", MozConsentBanner.onRejectClick, false); + + // Show the banner + document.getElementById("moz-consent-banner").classList.add("is-visible"); +}; + +const closeBanner = () => { + // Unbind click event listeners + document + .getElementById("moz-consent-banner-button-accept") + .removeEventListener("click", MozConsentBanner.onAcceptClick, false); + document + .getElementById("moz-consent-banner-button-reject") + .removeEventListener("click", MozConsentBanner.onRejectClick, false); + + // Hide the banner + document.getElementById("moz-consent-banner").classList.remove("is-visible"); +}; + window.addEventListener('DOMContentLoaded', focus_main_content, { once: true }); window.addEventListener('load', detect_blocked_gravatars, { once: true }); window.addEventListener('load', adjust_scroll_onload, { once: true }); @@ -283,4 +309,16 @@ window.addEventListener('DOMContentLoaded', () => { } }); } + + // Mozilla Consent Banner + // Bind open and close events before calling init(). + window.addEventListener('mozConsentOpen', openBanner, false); + window.addEventListener('mozConsentClose', closeBanner, false); + window.addEventListener('mozConsentStatus', (e) => { + console.log(e.detail); // eslint-disable-line no-console + }); + MozConsentBanner.init({ + helper: CookieHelper, + }); + }, { once: true }); diff --git a/skins/standard/consent.css b/skins/standard/consent.css new file mode 100644 index 0000000000..301c0dcced --- /dev/null +++ b/skins/standard/consent.css @@ -0,0 +1,151 @@ +@keyframes a-slide-up { + 0% { + opacity: 0; + transform: translateY(60px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} +.moz-consent-banner { + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + background: #ffffff; + color: #000000; + display: none; + font-family: Inter, sans-serif; + text-size-adjust: 100%; +} +.moz-consent-banner.is-visible { + display: block; +} +.moz-consent-banner .moz-consent-banner-content { + background: #ffffff; + margin: 0 auto; + max-width: 1152px; + padding: 16px 16px 0 16px; +} +.moz-consent-banner .moz-consent-banner-heading { + color: #000000; + font-family: Inter, sans-serif; + font-size: 1.5rem; + font-size: 24px; + margin: 0 0 16px; +} +.moz-consent-banner .moz-consent-banner-copy p { + font-size: 16px; + font-size: 1rem; + max-width: 60em; + line-height: 1.5; +} +.moz-consent-banner .moz-consent-banner-copy a:link, +.moz-consent-banner .moz-consent-banner-copy a:visited { + color: #000000; + text-decoration: underline; +} +.moz-consent-banner .moz-consent-banner-copy a:hover, +.moz-consent-banner .moz-consent-banner-copy a:active, +.moz-consent-banner .moz-consent-banner-copy a:focus { + text-decoration: none; +} +.moz-consent-banner .moz-consent-banner-controls { + text-align: center; +} +.moz-consent-banner .moz-consent-banner-controls a { + display: inline-block; + font-size: 16px; + font-size: 1rem; + margin-bottom: 16px; +} +.moz-consent-banner .moz-consent-banner-button { + background-color: #000000; + border-radius: 4px; + border: 2px solid #000000; + box-sizing: border-box; + color: #ffffff; + cursor: pointer; + display: inline-block; + font-family: Inter, sans-serif; + font-size: 16px; + font-size: 1rem; + font-weight: bold; + line-height: 1.5; + margin-bottom: 16px; + padding: 6px 24px; + text-align: center; + transition: background-color 100ms, box-shadow 100ms, color 100ms; + width: 100%; +} +.moz-consent-banner .moz-consent-banner-button:focus { + border-color: #0060df; + box-shadow: 0 0 0 2px rgba(0, 144, 237, 0.5); + outline-offset: 1px; +} +.moz-consent-banner .moz-consent-banner-button:hover { + background-color: #ededf0; + border-color: #000000; + box-shadow: none; + color: #000000; +} +.moz-consent-banner .moz-consent-banner-button:active { + background-color: #ededf0; + border-color: #5e5e72; + color: #000000; +} +@media (min-width: 768px) { + .moz-consent-banner .moz-consent-banner-content { + padding: 24px 48px 0 48px; + } + .moz-consent-banner .moz-consent-banner-controls { + text-align: left; + } + [dir=rtl] .moz-consent-banner .moz-consent-banner-controls { + text-align: right; + } + .moz-consent-banner .moz-consent-banner-controls .moz-consent-banner-button, + .moz-consent-banner .moz-consent-banner-controls a { + display: inline-block; + margin-bottom: 24px; + margin-right: 16px; + } + [dir=rtl] .moz-consent-banner .moz-consent-banner-controls .moz-consent-banner-button, + [dir=rtl] .moz-consent-banner .moz-consent-banner-controls a { + margin-left: 16px; + margin-right: 0; + } + .moz-consent-banner .moz-consent-banner-controls .moz-consent-banner-button { + width: auto; + } +} +@media (min-height: 600px) { + .moz-consent-banner { + animation: a-slide-up 600ms ease 0ms both; + background: transparent; + bottom: 0; + left: 0; + padding: 8px; + position: fixed; + right: 0; + z-index: 1000; + } + .moz-consent-banner .moz-consent-banner-content { + border-radius: 16px; + box-shadow: 0 5px 16px 2px rgba(29, 17, 51, 0.25); + } +} +@media (min-height: 600px) and (min-width: 768px) { + .moz-consent-banner { + padding: 16px; + } +} +@media (min-height: 600px) and (min-width: 1024px) { + .moz-consent-banner { + padding: 24px; + } +} +@media (min-height: 600px) and (prefers-reduced-motion: reduce) { + .moz-consent-banner { + animation: none; + } +} diff --git a/template/en/default/global/footer.html.tmpl b/template/en/default/global/footer.html.tmpl index f1ad69fc9f..de412cf952 100644 --- a/template/en/default/global/footer.html.tmpl +++ b/template/en/default/global/footer.html.tmpl @@ -25,6 +25,26 @@ [% Hook.process("end") %] + diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index d971e73a51..5d8aa3c847 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -85,6 +85,10 @@ [% javascript_urls.push('js/lib/prism.js') %] [% style_urls.push('skins/lib/prism.css') %] +[%# Enable Mozilla Consent Banner %] +[% javascript_urls.push('js/consent.js', 'js/cookie-helper.js') %] +[% style_urls.push('skins/standard/consent.css') %] + [%# We should be able to set the default value of the header variable # to the value of the title variable using the DEFAULT directive, # but that doesn't work if a caller sets header to the empty string From e663eee4fef16e8f9cad9a06f8c7ee969284457d Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Sat, 28 Sep 2024 21:45:21 -0400 Subject: [PATCH 02/10] additional checks and cleanups --- Bugzilla/CGI.pm | 33 +++++++++- Bugzilla/Config/Admin.pm | 6 ++ Bugzilla/Constants.pm | 17 +++++ .../default/bug/create/create-modal.html.tmpl | 1 + .../en/default/bug_modal/header.html.tmpl | 6 +- extensions/BugModal/web/bug_modal.js | 22 +++---- extensions/BugModal/web/create.js | 4 +- js/account.js | 11 ++++ js/consent.js | 64 ++++++++----------- js/global.js | 30 +++++---- js/util.js | 32 ++++++---- .../default/account/prefs/account.html.tmpl | 21 ++++-- .../en/default/admin/params/admin.html.tmpl | 4 +- template/en/default/global/footer.html.tmpl | 7 +- template/en/default/global/header.html.tmpl | 6 +- template/en/default/index.html.tmpl | 6 +- 16 files changed, 182 insertions(+), 88 deletions(-) diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 817f61a391..3fcd93329d 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -22,6 +22,7 @@ use Bugzilla::Util; use Bugzilla::Search::Recent; use File::Basename; +use List::Util qw(any none); use URI; BEGIN { @@ -493,7 +494,7 @@ sub param { sub _fix_utf8 { my $input = shift; - + # The is_utf8 is here in case CGI gets smart about UTF-8 someday. utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); return $input; @@ -514,6 +515,16 @@ sub should_set { sub send_cookie { my ($self, %paramhash) = @_; + # We check to see if the cookie be set is essential and if + # not we check to see if the user has given consent to set it + if (Bugzilla->params->{cookie_consent_enabled} + && $self->cookie_consent_required + && !$self->cookie_consented + ) + { + return undef if none { $_ eq $paramhash{'-name'} } ESSENTIAL_COOKIES; + } + # Complain if -value is not given or empty (bug 268146). if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { ThrowCodeError('cookies_need_value'); @@ -666,6 +677,26 @@ sub set_dated_content_disp { $self->{'_content_disp'} = $disposition; } +# Return true/false if a user has consent to non-essential cookies +# 1. If cookie is not present then no consent +# 2. If cookie is present and equal to 'yes' then we have consent +# 3. Any other value we do not have consent +sub cookie_consented { + my ($self) = @_; + return 0 if !defined $self->cookie(CONSENT_COOKIE); + return 1 if $self->cookie(CONSENT_COOKIE) eq 'yes'; + return 0; # Anything other than yes is a no +} + +# Return true if client is accessing this site +# from within a required consent country +sub cookie_consent_required { + my ($self) = @_; + my $client_region = $self->http('X-Client-Region') || ''; + return 1 if any { $client_region eq $_ } COOKIE_CONSENT_COUNTRIES; + return 1; +} + ########################## # Vars TIEHASH Interface # ########################## diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm index ac52e59269..ca3a7c0998 100644 --- a/Bugzilla/Config/Admin.pm +++ b/Bugzilla/Config/Admin.pm @@ -65,6 +65,12 @@ sub get_param_list { type => 't', default => 'https://product-details.mozilla.org/1.0', }, + + { + name => 'cookie_consent_enabled', + type => 'b', + default => 0, + } ); return @param_list; } diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 3b133a0156..79e2fd5109 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -208,6 +208,10 @@ use Memoize; JOB_QUEUE_VIEW_MAX_JOBS BOUNCE_COUNT_MAX + + CONSENT_COOKIE + ESSENTIAL_COOKIES + COOKIE_CONSENT_COUNTRIES ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -678,6 +682,19 @@ use constant JOB_QUEUE_VIEW_MAX_JOBS => 2500; # before the account is completely disabled. use constant BOUNCE_COUNT_MAX => 5; +# Consent cookie name +use constant CONSENT_COOKIE => 'moz-consent-pref'; + +# List of essential cookies that cannot be opted out +use constant ESSENTIAL_COOKIES => + qw(bugzilla Bugzilla_login Bugzilla_logincookie Bugzilla_login_request_cookie + bugzilla github_state github_token sudo); + +# List of countries the require cookie consent +use constant COOKIE_CONSENT_COUNTRIES => qw( + AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IS IT LV + LI LT LU MT NL NO PL PT RO SK SI ES SE CH GB ); + sub bz_locations { # Force memoize() to re-compute data per project, to avoid diff --git a/extensions/BugModal/template/en/default/bug/create/create-modal.html.tmpl b/extensions/BugModal/template/en/default/bug/create/create-modal.html.tmpl index 5391ca154a..887a4a37c8 100644 --- a/extensions/BugModal/template/en/default/bug/create/create-modal.html.tmpl +++ b/extensions/BugModal/template/en/default/bug/create/create-modal.html.tmpl @@ -15,6 +15,7 @@ "js/bug.js", # Possible Duplicates table "js/attachment.js", "extensions/BugModal/web/create.js", + "js/util.js" ] style_urls = [ "skins/standard/attachment.css", diff --git a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl index 2ca7619c35..870e5d514a 100644 --- a/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl +++ b/extensions/BugModal/template/en/default/bug_modal/header.html.tmpl @@ -63,7 +63,8 @@ "extensions/ComponentWatching/web/js/overlay.js", "js/bugzilla-readable-status-min.js", "js/field.js", - "js/comments.js" + "js/comments.js", + "js/util.js" ); jquery.push( "contextMenu", @@ -116,7 +117,8 @@ remember_collapsed: [% user.settings.ui_remember_collapsed.value == "on" ? "true" : "false" %], inline_attachments: [% user.settings.inline_attachments.value == "on" ? "true" : "false" %], autosize_comments: [% user.settings.autosize_comments.value == "on" ? "true" : "false" %] - } + }, + cookie_consent: [% Bugzilla.cgi.consent_cookie ? "true" : "false" %] }; [% IF user.id %] BUGZILLA.default_assignee = '[% bug.component_obj.default_assignee.login FILTER js %]'; diff --git a/extensions/BugModal/web/bug_modal.js b/extensions/BugModal/web/bug_modal.js index e0be325aef..788218f936 100644 --- a/extensions/BugModal/web/bug_modal.js +++ b/extensions/BugModal/web/bug_modal.js @@ -21,7 +21,7 @@ function slide_module(module, action, fast) { 'aria-label': is_visible ? latch.data('label-expanded') : latch.data('label-collapsed'), }); if (BUGZILLA.user.settings.remember_collapsed && module.is(':visible')) - localStorage.setItem(module.attr('id') + '.visibility', is_visible ? 'show' : 'hide'); + Bugzilla.Storage.set(module.attr('id') + '.visibility', is_visible ? 'show' : 'hide'); } if (action == 'show') { @@ -43,7 +43,7 @@ function init_module_visibility() { var id = that.attr('id'); if (!id) return; if (that.data('non-stick')) return; - var stored = localStorage.getItem(id + '.visibility'); + var stored = Bugzilla.Storage.get(id + '.visibility'); if (stored) { slide_module(that, stored, true); } @@ -139,7 +139,7 @@ $(function() { // restore edit mode after navigating back function restoreEditMode() { if (!$('#editing').val()) { - if (localStorage.getItem('modal-perm-edit-mode') === 'true') { + if (Bugzilla.Storage.get('modal-perm-edit-mode') === 'true') { $('#mode-btn').click(); $('#action-enable-perm-edit').attr('aria-checked', 'true'); } @@ -170,7 +170,7 @@ $(function() { text: text, savedAt: Date.now() }; - localStorage.setItem(bugCommentCacheKey, JSON.stringify(value)); + Bugzilla.Storage.set(bugCommentCacheKey, JSON.stringify(value)); } /** @@ -180,7 +180,7 @@ $(function() { * to take such special cases into account. Otherwise the current bug’s comment cache will be removed. */ const clearSavedBugComment = (bug_id = BUGZILLA.bug_id) => { - localStorage.removeItem(`bug-modal-saved-comment-${bug_id}`); + Bugzilla.Storage.delete(`bug-modal-saved-comment-${bug_id}`); }; /** @@ -200,7 +200,7 @@ $(function() { function restoreSavedBugComment() { expireSavedComments(); - let value = JSON.parse(localStorage.getItem(bugCommentCacheKey)); + let value = Bugzilla.Storage.get(bugCommentCacheKey); if (value){ let commentBox = document.querySelector("textarea#comment"); commentBox.value = value['text']; @@ -213,10 +213,10 @@ $(function() { function expireSavedComments() { const AGE_THRESHOLD = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds. let expiredKeys = []; - for (let i = 0; i < localStorage.length; i++) { - let key = localStorage.key(i); + for (let i = 0; i < window.localStorage.length; i++) { + let key = window.localStorage.key(i); if (key.match(/^bug-modal-saved-comment-/)) { - let value = JSON.parse(localStorage.getItem(key)); + let value = Bugzilla.Storage.get(key); let savedAt = value['savedAt'] || 0; let age = Date.now() - savedAt; if (age < 0 || age > AGE_THRESHOLD) { @@ -225,7 +225,7 @@ $(function() { } } expiredKeys.forEach((key) => { - localStorage.removeItem(key); + Bugzilla.Storage.delete(key); }); } @@ -532,7 +532,7 @@ $(function() { event.preventDefault(); const enabled = $(this).attr('aria-checked') !== 'true'; $(this).attr('aria-checked', enabled); - localStorage.setItem('modal-perm-edit-mode', enabled); + Bugzilla.Storage.set('modal-perm-edit-mode', enabled); }); // reset diff --git a/extensions/BugModal/web/create.js b/extensions/BugModal/web/create.js index 4a4d6325b3..1cd98922cd 100644 --- a/extensions/BugModal/web/create.js +++ b/extensions/BugModal/web/create.js @@ -59,7 +59,7 @@ window.addEventListener('DOMContentLoaded', () => { $toggleAdvanced.textContent = $toggleAdvanced.dataset[advancedStateStr]; if (cache) { - window.localStorage.setItem('create-form.advanced', advancedStateStr); + Bugzilla.Storage.set('create-form.advanced', advancedStateStr); } }; @@ -73,7 +73,7 @@ window.addEventListener('DOMContentLoaded', () => { // Check the local storage or the TUI cookie used on the legacy form to see if the user wants // to show advanced fields on the bug form. let showAdvanced = - window.localStorage.getItem('create-form.advanced') === 'show' + Bugzilla.Storage.get('create-form.advanced') === 'show' || /\bTUI=\S*?expert_fields=1\b/.test(document.cookie); if (showAdvanced) { diff --git a/js/account.js b/js/account.js index e716fdc8ed..8a2491bc84 100644 --- a/js/account.js +++ b/js/account.js @@ -208,4 +208,15 @@ $(function() { $('#new_description').focus(); } }); + + // Toggle cookie consent preference + $('#cookie_consent_setting') + .change(function() { + if ($(this).is(':checked')) { + MozConsentBanner.setConsentCookie(true); + } + else { + MozConsentBanner.setConsentCookie(false); + } + }); }); diff --git a/js/consent.js b/js/consent.js index 23a2782c4d..82004372f4 100644 --- a/js/consent.js +++ b/js/consent.js @@ -21,48 +21,38 @@ var options; * Sets cookie indicating consent choice. * @param {Object} data - consent settings. */ -MozConsentBanner.setConsentCookie = function (data) { +MozConsentBanner.setConsentCookie = function (value) { try { var date = new Date(); date.setDate(date.getDate() + options.cookieExpiryDays); var expires = date.toUTCString(); - options.helper.setItem(options.cookieID, JSON.stringify(data), expires, '/', options.cookieDomain, false, 'lax'); + options.helper.setItem(options.cookieID, (value ? 'yes' : 'no'), + expires, '/', options.cookieDomain, false, 'lax'); return true; } catch (e) { return false; } }; +/** + * Removes cookie indicating consent choice. + */ +MozConsentBanner.clearConsentCookie = function () { + return options.helper.removeItem(options.cookieID, '/', options.cookieDomain, false, 'lax'); +}; + /** * Get consent cookie value if set. * @returns {Boolean} */ MozConsentBanner.getConsentCookie = function () { try { - return options.helper.hasItem(options.cookieID) && JSON.parse(options.helper.getItem(options.cookieID)); + return options.helper.hasItem(options.cookieID) && options.helper.getItem(options.cookieID); } catch (e) { return false; } }; -/** - * Returns the domain to set for the consent cookie. When in production - * we want to specify '.mozilla.org' instead of 'www.mozilla.org', - * so that multiple Mozilla sub domains can use the same consent cookie. - * @returns {domain} string or null. - */ -MozConsentBanner.getHostName = function (hostname) { - var url = typeof hostname === 'string' ? hostname : window.location.hostname; - var domain = null; - if (url.indexOf('.allizom.org') !== -1) { - domain = '.allizom.org'; - } - if (url.indexOf('.mozilla.org') !== -1) { - domain = '.mozilla.org'; - } - return domain; -}; - /** * Helper for dispatching a custom event with a * given name and data payload. @@ -95,12 +85,8 @@ MozConsentBanner.dispatchEvent = function (eventName, eventData) { * Event handler for accepting cookies. */ MozConsentBanner.onAcceptClick = function () { - var preferences = { - analytics: true, - preference: true - }; - MozConsentBanner.setConsentCookie(preferences); - MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, preferences); + MozConsentBanner.setConsentCookie(true); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, true); MozConsentBanner.dispatchEvent(EVENT_NAME_CLOSE, {}); }; @@ -108,15 +94,17 @@ MozConsentBanner.onAcceptClick = function () { * Event handler for rejecting cookies. */ MozConsentBanner.onRejectClick = function () { - var preferences = { - analytics: false, - preference: false - }; - MozConsentBanner.setConsentCookie(preferences); - MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, preferences); + MozConsentBanner.setConsentCookie(false); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, false); MozConsentBanner.dispatchEvent(EVENT_NAME_CLOSE, {}); }; +MozConsentBanner.onClearClick = function () { + MozConsentBanner.clearConsentCookie(); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, false); + MozConsentBanner.dispatchEvent(EVENT_NAME_OPEN, {}); +} + /** * Validates BannerOptions configuration object. * @param {BannerOptions} object containing one or more properties. @@ -181,10 +169,11 @@ MozConsentBanner.init = function (config) { /** * If domain for consent cookie has not be supplied via config, - * set it via getHostName() + * set it via default BUGZILLA urlbase */ if (!options.cookieDomain) { - options.cookieDomain = MozConsentBanner.getHostName(); + const url = new URL(BUGZILLA.config.urlbase); + options.cookieDomain = url.hostname; } /** @@ -213,10 +202,7 @@ MozConsentBanner.init = function (config) { * cookies and analytics are OK to load. */ if (options.optOut) { - MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, { - preference: true, - analytics: true - }); + MozConsentBanner.dispatchEvent(EVENT_NAME_STATUS, true); } } return true; diff --git a/js/global.js b/js/global.js index fe085d1329..0b90267438 100644 --- a/js/global.js +++ b/js/global.js @@ -297,12 +297,12 @@ window.addEventListener('DOMContentLoaded', () => { fetch(url, { method: "POST" }).then( response => announcement.style.display = "none" ); - localStorage.setItem("announcement_checksum", checksum); + Bugzilla.Storage.set("announcement_checksum", checksum); } announcement.addEventListener('click', hide_announcement); window.addEventListener('visibilitychange', () => { if (!window.hidden) { - const hidden_checksum = localStorage.getItem("announcement_checksum"); + const hidden_checksum = Bugzilla.Storage.get("announcement_checksum"); if (hidden_checksum && hidden_checksum == announcement.dataset.checksum) { announcement.style.display = "none"; } @@ -311,14 +311,22 @@ window.addEventListener('DOMContentLoaded', () => { } // Mozilla Consent Banner - // Bind open and close events before calling init(). - window.addEventListener('mozConsentOpen', openBanner, false); - window.addEventListener('mozConsentClose', closeBanner, false); - window.addEventListener('mozConsentStatus', (e) => { - console.log(e.detail); // eslint-disable-line no-console - }); - MozConsentBanner.init({ - helper: CookieHelper, - }); + // Bind open and close events before calling init(). + if (BUGZILLA.config.cookie_consent_enabled) { + window.addEventListener('mozConsentOpen', openBanner, false); + window.addEventListener('mozConsentReset', openBanner, false); + window.addEventListener('mozConsentClose', closeBanner, false); + window.addEventListener('mozConsentStatus', (e) => { + console.log(e.detail); // eslint-disable-line no-console + }); + MozConsentBanner.init({ + helper: CookieHelper, + }); + // Listen for click to reset cookie preference + let $reset_cookie_consent = document.getElementById('reset_cookie_consent'); + if ($reset_cookie_consent) { + $reset_cookie_consent.addEventListener('click', MozConsentBanner.onClearClick); + } + } }, { once: true }); diff --git a/js/util.js b/js/util.js index fbd9025bb8..511789eb98 100644 --- a/js/util.js +++ b/js/util.js @@ -715,21 +715,19 @@ Bugzilla.Storage = class LocalStorage { * Get a value. * @param {string} key A storage key. * @param {any} [fallback] Whether to return `{}` instead of `null` when the value is unavailable. - * @returns {object | null} A storage value. + * @returns A storage value or null. */ - static get(key, fallback = false) { + static get(key) { const cache = window.localStorage.getItem(key); - const fallbackValue = fallback ? {} : null; - - if (cache === null) { - return fallbackValue; - } - - try { - return JSON.parse(cache); - } catch { - return fallbackValue; + let value = null; + if (cache !== null) { + try { + value = JSON.parse(cache); + } catch { + value = cache; + } } + return value; } /** @@ -738,7 +736,15 @@ Bugzilla.Storage = class LocalStorage { * @param {object} value A storage value. */ static set(key, value) { - window.localStorage.setItem(key, JSON.stringify(value)); + if (BUGZILLA.user.cookie_consent === 'no' + && !BUGZILLA.config.essential_cookies.includes(key)) + { + return null; + } + if (typeof value === 'object' && value !== null) { + value = JSON.stringify(value); + } + window.localStorage.setItem(key, value); } /** diff --git a/template/en/default/account/prefs/account.html.tmpl b/template/en/default/account/prefs/account.html.tmpl index 89962c37af..5730563ae1 100644 --- a/template/en/default/account/prefs/account.html.tmpl +++ b/template/en/default/account/prefs/account.html.tmpl @@ -24,6 +24,8 @@ # new_login_name: string. The user's new Bugzilla login whilst not confirmed. (optional) #%] +[% USE Bugzilla %] + [%# BMO - add hook for displaying user-profile link %] [% Hook.process('start') %] @@ -48,6 +50,21 @@ [%# BMO - moved field hook from end of file to here to group with other account fields %] [% Hook.process('field') %] + [% IF Param('cookie_consent_enabled') %] + + +
+ + + Cookies: + + + + + + [% END %] + [% SET can_change = [] %] [% IF user.authorizer.can_change_email && Param('allowemailchange') %] [% can_change.push('email address') %] @@ -173,10 +190,6 @@ [% END %] - - -
- diff --git a/template/en/default/admin/params/admin.html.tmpl b/template/en/default/admin/params/admin.html.tmpl index 3b2927986e..5ecdc6ba37 100644 --- a/template/en/default/admin/params/admin.html.tmpl +++ b/template/en/default/admin/params/admin.html.tmpl @@ -57,5 +57,7 @@ over 60 seconds. Valid keys are get_b[%''%]ug which covers JSON-RPC log_user_requests => "This option controls logging of authenticated requests in the user_request_log table" - product_details_endpoint => "Endpoint used for getting version details of Mozilla products"} + product_details_endpoint => "Endpoint used for getting version details of Mozilla products" + + cookie_consent_enabled => "Turn on cookie consent banner for specific countries."} %] diff --git a/template/en/default/global/footer.html.tmpl b/template/en/default/global/footer.html.tmpl index de412cf952..0d87633b07 100644 --- a/template/en/default/global/footer.html.tmpl +++ b/template/en/default/global/footer.html.tmpl @@ -25,9 +25,12 @@ [% Hook.process("end") %] +[% USE Bugzilla %] +[% IF Param("cookie_consent_enabled") + && Bugzilla.cgi.cookie_consent_required %] +[% END %] diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl index 5d8aa3c847..ff944285d7 100644 --- a/template/en/default/global/header.html.tmpl +++ b/template/en/default/global/header.html.tmpl @@ -112,10 +112,14 @@ [%- js_BUGZILLA = { config => { basepath => basepath, + urlbase => urlbase, + cookie_consent_enabled => Param("cookie_consent_enabled") + essential_cookies => constants.ESSENTIAL_COOKIES, } user => { # TODO: Move all properties form bug_modal/header.html.tmpl - login => user.login, + login => user.login, + cookie_consent => Bugzilla.cgi.consent_cookie, }, param => { maxattachmentsize => Param('maxattachmentsize'), diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl index 8165cb0bbe..f8041ac331 100644 --- a/template/en/default/index.html.tmpl +++ b/template/en/default/index.html.tmpl @@ -160,7 +160,11 @@ + [% IF Param("cookie_consent_enabled") && Bugzilla.cgi.cookie(constants.CONSENT_COOKIE) %] +
+ Reset Current Cookie Preferences +
+ [% END %] - [% PROCESS global/footer.html.tmpl %] From 8b462da0bee125ee96e04d1135c2d4107a2b891e Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 8 Nov 2024 19:01:17 -0500 Subject: [PATCH 03/10] Fixes and improvements --- Bugzilla/Constants.pm | 2 +- js/account.js | 11 ----------- .../en/default/account/prefs/account.html.tmpl | 11 ++++++++--- template/en/default/global/footer.html.tmpl | 8 ++++---- userprefs.cgi | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 79e2fd5109..cceca990a8 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -688,7 +688,7 @@ use constant CONSENT_COOKIE => 'moz-consent-pref'; # List of essential cookies that cannot be opted out use constant ESSENTIAL_COOKIES => qw(bugzilla Bugzilla_login Bugzilla_logincookie Bugzilla_login_request_cookie - bugzilla github_state github_token sudo); + bugzilla github_state github_token sudo moz-consent-pref); # List of countries the require cookie consent use constant COOKIE_CONSENT_COUNTRIES => qw( diff --git a/js/account.js b/js/account.js index 8a2491bc84..e716fdc8ed 100644 --- a/js/account.js +++ b/js/account.js @@ -208,15 +208,4 @@ $(function() { $('#new_description').focus(); } }); - - // Toggle cookie consent preference - $('#cookie_consent_setting') - .change(function() { - if ($(this).is(':checked')) { - MozConsentBanner.setConsentCookie(true); - } - else { - MozConsentBanner.setConsentCookie(false); - } - }); }); diff --git a/template/en/default/account/prefs/account.html.tmpl b/template/en/default/account/prefs/account.html.tmpl index 5730563ae1..fc6f5c846c 100644 --- a/template/en/default/account/prefs/account.html.tmpl +++ b/template/en/default/account/prefs/account.html.tmpl @@ -58,9 +58,14 @@ Cookies: - - + + [% END %] diff --git a/template/en/default/global/footer.html.tmpl b/template/en/default/global/footer.html.tmpl index 0d87633b07..958c45b9f4 100644 --- a/template/en/default/global/footer.html.tmpl +++ b/template/en/default/global/footer.html.tmpl @@ -33,15 +33,15 @@ diff --git a/userprefs.cgi b/userprefs.cgi index d09bd1ad5e..0ee2c6a0cf 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -72,6 +72,10 @@ sub DoAccount { } } } + + $vars->{cookie_consent_changed} + = Bugzilla->request_cache->{cookie_consent_changed} + if Bugzilla->request_cache->{cookie_consent_changed}; } sub SaveAccount { @@ -175,6 +179,20 @@ sub SaveAccount { # display 2fa verification $user->mfa_provider->verify_prompt($mfa_event); } + + # Reset cookie consent preferences if needed + if (Bugzilla->params->{cookie_consent_enabled}) { + my $old_cookie_consent = $cgi->cookie_consented; + my $new_cookie_consent = $cgi->param('cookie_consent'); + if (!$old_cookie_consent && $new_cookie_consent) { + Bugzilla->request_cache->{cookie_consent_changed} = 'yes'; + $cgi->send_cookie(-name => 'moz-consent-pref', -value => 'yes'); + } + elsif ($old_cookie_consent && !$new_cookie_consent) { + Bugzilla->request_cache->{cookie_consent_changed} = 'no'; + $cgi->send_cookie(-name => 'moz-consent-pref', -value => 'no'); + } + } } sub MfaAccount { From 3cc6fab4a95a7f6167c7690e85bfc262b830f086 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 25 Nov 2024 18:08:48 -0500 Subject: [PATCH 04/10] Added an automated test for basic consent --- qa/t/1_test_cookie_consent.t | 119 +++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 qa/t/1_test_cookie_consent.t diff --git a/qa/t/1_test_cookie_consent.t b/qa/t/1_test_cookie_consent.t new file mode 100644 index 0000000000..6dc669562d --- /dev/null +++ b/qa/t/1_test_cookie_consent.t @@ -0,0 +1,119 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is 'Incompatible With Secondary Licenses', as +# defined by the Mozilla Public License, v. 2.0. + +use strict; +use warnings; +use lib qw(lib ../../lib ../../local/lib/perl5); + +use Test::More 'no_plan'; + +use List::Util qw(first none); +use QA::Util; + +my ($sel, $config) = get_selenium(); + +# Turn on cookie consent +log_in($sel, $config, 'admin'); +set_parameters($sel, + {'Administrative Policies' => {'cookie_consent_enabled-on' => undef}}); + +# Accept all cookies for admin user +$sel->click_ok('moz-consent-banner-button-accept'); + +# Make sure that a 'moz-consent-pref' cookie was set to yes +my $cookies = $sel->driver->get_all_cookies; +my $pref_cookie = first { $_->{name} eq 'moz-consent-pref' } @{$cookies}; +ok($pref_cookie && $pref_cookie->{value} eq 'yes', + 'Consent cookie set to yes properly'); + +# Create a test bug +file_bug_in_product($sel, 'TestProduct'); +my $bug_summary = 'Cookie consent test bug'; +$sel->type_ok('short_desc', $bug_summary); +my $bug1_id = create_bug($sel, $bug_summary); + +# Run a buglist query to set a cookie such as LASTORDER which is non-essential +open_advanced_search_page($sel); +$sel->type_ok('short_desc', 'Cookie consent test bug'); +$sel->select_ok('order', 'label=Bug Number', 'Select order by bug number'); +$sel->click_ok('Search'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is('Bug List'); +$sel->is_text_present_ok('One bug found'); +$sel->is_text_present_ok('Cookie consent test bug'); + +$cookies = $sel->driver->get_all_cookies; +my $last_order_cookie = first { $_->{name} eq 'LASTORDER' } @{$cookies}; +ok($last_order_cookie, 'Last order cookie set properly'); + +# Under user preferences, change cookie preferences to reject +$sel->click_ok('header-account-menu-button'); +$sel->click_ok('link=Preferences'); +$sel->wait_for_page_to_load(WAIT_TIME); +$sel->title_is('User Preferences'); +$sel->click_ok('link=Account'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is('User Preferences'); +$sel->uncheck_ok('cookie_consent'); +$sel->click_ok('update'); +$sel->wait_for_page_to_load(WAIT_TIME); +$sel->title_is('User Preferences'); + +# Clear all cookies except the consent cookie and login stuff +$cookies = $sel->driver->get_all_cookies; +foreach my $cookie (@{$cookies}) { + my $name = $cookie->{name}; + unless ($name eq 'Bugzilla_login' + || $name eq 'Bugzilla_logincookie' + || $name eq 'moz-consent-pref') + { + $sel->driver->delete_cookie_named($name); + } +} + +# Verify that LASTORDER and COLUMNLIST are no longer set +open_advanced_search_page($sel); +$sel->type_ok('short_desc', 'Cookie consent test bug'); +$sel->select_ok('order', 'label=Bug Number', 'Select order by bug number'); +$sel->click_ok('Search'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is('Bug List'); +$sel->is_text_present_ok('One bug found'); +$sel->is_text_present_ok('Cookie consent test bug'); + +$cookies = $sel->driver->get_all_cookies; +$last_order_cookie = first { $_->{name} eq 'LASTORDER' } @{$cookies}; +ok(!$last_order_cookie, 'Last order cookie not set properly'); + +# Logout and clear all cookies. Then we will reject all cookies and verify +logout($sel); +$sel->driver->delete_all_cookies(); +$sel->open_ok('/home', 'Go to home page'); +$sel->click_ok('moz-consent-banner-button-reject'); + +log_in($sel, $config, 'admin'); +open_advanced_search_page($sel); +$sel->type_ok('short_desc', 'Cookie consent test bug'); +$sel->select_ok('order', 'label=Bug Number', 'Select order by bug number'); +$sel->click_ok('Search'); +$sel->wait_for_page_to_load_ok(WAIT_TIME); +$sel->title_is('Bug List'); +$sel->is_text_present_ok('One bug found'); +$sel->is_text_present_ok('Cookie consent test bug'); + +$cookies = $sel->driver->get_all_cookies; +$last_order_cookie = first { $_->{name} eq 'LASTORDER' } @{$cookies}; +ok(!$last_order_cookie, 'Last order cookie not set properly'); + +## Turn off cookie consent +set_parameters($sel, + {'Administrative Policies' => {'cookie_consent_enabled-off' => undef}}); +logout($sel); + +done_testing; + +1; From 9e77c1a7940012d4df1bbbef5463eeb21004bbbc Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Wed, 27 Nov 2024 17:15:58 -0500 Subject: [PATCH 05/10] Fixed logic in function to not require consent if not in specific country --- Bugzilla/CGI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 3fcd93329d..832ee4d76b 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -694,7 +694,7 @@ sub cookie_consent_required { my ($self) = @_; my $client_region = $self->http('X-Client-Region') || ''; return 1 if any { $client_region eq $_ } COOKIE_CONSENT_COUNTRIES; - return 1; + return 0; } ########################## From de0c57e4686987bfa7a40619aecd1af3dac7c352 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Wed, 27 Nov 2024 22:03:40 -0500 Subject: [PATCH 06/10] Add exception for country check if running under CI --- Bugzilla/CGI.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 832ee4d76b..319a8d66e3 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -692,6 +692,7 @@ sub cookie_consented { # from within a required consent country sub cookie_consent_required { my ($self) = @_; + return 1 if $ENV{CI}; my $client_region = $self->http('X-Client-Region') || ''; return 1 if any { $client_region eq $_ } COOKIE_CONSENT_COUNTRIES; return 0; From a8ef5b130683f2faced93396fab814ad695f4682 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Wed, 27 Nov 2024 23:25:57 -0500 Subject: [PATCH 07/10] Fixed failing test (backported from master) --- qa/t/1_test_custom_fields.t | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/t/1_test_custom_fields.t b/qa/t/1_test_custom_fields.t index 2db50944c2..9cfc43a88e 100644 --- a/qa/t/1_test_custom_fields.t +++ b/qa/t/1_test_custom_fields.t @@ -213,6 +213,7 @@ my $bug2_id = create_bug($sel, $bug_summary2); go_to_bug($sel, $bug2_id); $sel->type_ok("cf_qa_freetext_$bug1_id", "bonsai"); $sel->selected_label_is("cf_qa_list_$bug1_id", "---"); +$sel->click_ok('top-btn'); $sel->select_ok("bug_status", "label=SUSPENDED"); edit_bug($sel, $bug2_id); @@ -222,6 +223,7 @@ $sel->select_ok("cf_qa_list_$bug1_id", "label=storage"); # FIXME: The reverse description is not displaying properly on bug modal page #$sel->is_text_present_ok("IsRef$bug1_id: $bug2_id"); +$sel->click_ok('top-btn'); $sel->select_ok("bug_status", "RESOLVED"); $sel->select_ok("resolution", "UPSTREAM"); edit_bug_and_return($sel, $bug1_id, $bug_summary); From 421aeaf84a03d367e1ae328532cc140f4e9a4368 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Fri, 6 Dec 2024 18:38:41 -0500 Subject: [PATCH 08/10] - Created a separate cookies settings page instead of user preferences. This doesn't require authenticated sessions. - Added a cookie settings link to the user drop down below preferences and also a cookie settings link on the home page. --- Bugzilla/CGI.pm | 9 ++- Bugzilla/Constants.pm | 2 +- js/consent-settings.js | 44 +++++++++++ js/global.js | 2 +- skins/standard/page.css | 5 +- .../default/account/prefs/account.html.tmpl | 20 ----- template/en/default/global/footer.html.tmpl | 3 +- template/en/default/global/header.html.tmpl | 8 +- template/en/default/index.html.tmpl | 8 +- template/en/default/pages/cookies.html.tmpl | 74 +++++++++++++++++++ userprefs.cgi | 14 ---- 11 files changed, 141 insertions(+), 48 deletions(-) create mode 100644 js/consent-settings.js create mode 100644 template/en/default/pages/cookies.html.tmpl diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 319a8d66e3..2479cdce8f 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -683,9 +683,12 @@ sub set_dated_content_disp { # 3. Any other value we do not have consent sub cookie_consented { my ($self) = @_; - return 0 if !defined $self->cookie(CONSENT_COOKIE); - return 1 if $self->cookie(CONSENT_COOKIE) eq 'yes'; - return 0; # Anything other than yes is a no + if (defined $self->cookie(CONSENT_COOKIE) + && $self->cookie(CONSENT_COOKIE) eq 'yes') + { + return 1; + } + return 0; } # Return true if client is accessing this site diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index cceca990a8..6487b6e2c2 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -690,7 +690,7 @@ use constant ESSENTIAL_COOKIES => qw(bugzilla Bugzilla_login Bugzilla_logincookie Bugzilla_login_request_cookie bugzilla github_state github_token sudo moz-consent-pref); -# List of countries the require cookie consent +# List of countries that require cookie consent use constant COOKIE_CONSENT_COUNTRIES => qw( AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IS IT LV LI LT LU MT NL NO PL PT RO SK SI ES SE CH GB ); diff --git a/js/consent-settings.js b/js/consent-settings.js new file mode 100644 index 0000000000..504cbaf749 --- /dev/null +++ b/js/consent-settings.js @@ -0,0 +1,44 @@ +(function () { + "use strict"; + function getFormData() { + return (document.querySelector( + 'input[name="cookie-radio-preference"]:checked', + ).value === "yes") + ? true + : false; + } + function setFormData(preference) { + if (preference === "yes") { + document + .getElementById("cookie-radio-preference-yes") + .setAttribute("checked", ""); + document + .getElementById("cookie-radio-preference-no") + .removeAttribute("checked"); + } else { + document + .getElementById("cookie-radio-preference-yes") + .removeAttribute("checked"); + document + .getElementById("cookie-radio-preference-no") + .setAttribute("checked", ""); + } + } + function onFormSubmit(e) { + e.preventDefault(); + MozConsentBanner.setConsentCookie(getFormData()); + showSuccessMessage(); + } + function showSuccessMessage() { + var e = document.getElementById("cookie-consent-form-submit-success"); + e.style.display = "block"; + e.focus(); + } + + window.addEventListener('DOMContentLoaded', () => { + document + .getElementById("cookie-consent-form") + .addEventListener("submit", onFormSubmit); + setFormData(MozConsentBanner.getConsentCookie()); + }); +})(); diff --git a/js/global.js b/js/global.js index 0b90267438..82a2d7480f 100644 --- a/js/global.js +++ b/js/global.js @@ -312,7 +312,7 @@ window.addEventListener('DOMContentLoaded', () => { // Mozilla Consent Banner // Bind open and close events before calling init(). - if (BUGZILLA.config.cookie_consent_enabled) { + if (BUGZILLA.config.cookie_consent_enabled && BUGZILLA.config.cookie_consent_required) { window.addEventListener('mozConsentOpen', openBanner, false); window.addEventListener('mozConsentReset', openBanner, false); window.addEventListener('mozConsentClose', closeBanner, false); diff --git a/skins/standard/page.css b/skins/standard/page.css index 5451c227dc..3239288833 100644 --- a/skins/standard/page.css +++ b/skins/standard/page.css @@ -10,9 +10,8 @@ template/en/default/pages/ directory. */ #main-inner { - margin: 15px auto; - /* People have an easier time reading narrower columns of text. */ - max-width: 50em; + margin: 8px auto; + max-width: 1024px; } #main-inner p, diff --git a/template/en/default/account/prefs/account.html.tmpl b/template/en/default/account/prefs/account.html.tmpl index fc6f5c846c..8c08387e6d 100644 --- a/template/en/default/account/prefs/account.html.tmpl +++ b/template/en/default/account/prefs/account.html.tmpl @@ -50,26 +50,6 @@ [%# BMO - moved field hook from end of file to here to group with other account fields %] [% Hook.process('field') %] - [% IF Param('cookie_consent_enabled') %] - - -
- - - Cookies: - - - - - - [% END %] - [% SET can_change = [] %] [% IF user.authorizer.can_change_email && Param('allowemailchange') %] [% can_change.push('email address') %] diff --git a/template/en/default/global/footer.html.tmpl b/template/en/default/global/footer.html.tmpl index 958c45b9f4..3e81099fc5 100644 --- a/template/en/default/global/footer.html.tmpl +++ b/template/en/default/global/footer.html.tmpl @@ -34,7 +34,8 @@ - [% IF Param("cookie_consent_enabled") && Bugzilla.cgi.cookie(constants.CONSENT_COOKIE) %] - + [% IF Param('cookie_consent_enabled') && Bugzilla.cgi.cookie_consent_required %] + [% END %] diff --git a/template/en/default/pages/cookies.html.tmpl b/template/en/default/pages/cookies.html.tmpl new file mode 100644 index 0000000000..1bb7a52b4e --- /dev/null +++ b/template/en/default/pages/cookies.html.tmpl @@ -0,0 +1,74 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% INCLUDE global/header.html.tmpl + title = "Cookie Settings" + style_urls = ['skins/standard/page.css'] + javascript_urls = ['js/consent-settings.js'] +%] + +
+ +

Cookie settings

+ +

Cookies are small files containing pieces of information that are saved to your computer + or device when you visit a website. [% terms.BugzillaTitle %] uses Cookies to help make + our website work.

+ +

This page describes the different types of Cookies, JavaScript, and local storage (hereafter, “Cookies”) + that [% terms.BugzillaTitle %] may use, and gives you control over which types of data we may collect.

+ +

How [% terms.BugzillaTitle %] uses Cookies

+ +

Necessary

+ +
+

What are Necessary Cookies?

+

These technologies are required to support essential website features, such as logging into a secure area of the website, and cannot be turned off.

+ +

How does [% terms.BugzillaTitle %] use this data?

+

[% terms.BugzillaTitle %] uses Necessary Cookies only to provide essential website features, such as logging in using your [% terms.BugzillaTitle %] account. Without these technologies, essential website features may not function.

+
+ +

Preference

+ +
+

What are Preference Cookies?

+

These technologies are used to remember choices you have made during a previous visit to a website. + Examples might include which language you prefer reading in, or which color theme is your favorite.

+ +

How does [% terms.BugzillaTitle %] use this data?

+

[% terms.BugzillaTitle %] uses Preference Cookies to honor your preferences and improve your experience when visiting our website. They are not used for analytics purposes. Preference Cookies support features in a similar way to Necessary Cookies, however a website can still operate without them. As such, you can choose to opt out of their use.

+ + + + + + +
+ +
+ +[% INCLUDE global/footer.html.tmpl %] diff --git a/userprefs.cgi b/userprefs.cgi index 0ee2c6a0cf..b50c73a3fe 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -179,20 +179,6 @@ sub SaveAccount { # display 2fa verification $user->mfa_provider->verify_prompt($mfa_event); } - - # Reset cookie consent preferences if needed - if (Bugzilla->params->{cookie_consent_enabled}) { - my $old_cookie_consent = $cgi->cookie_consented; - my $new_cookie_consent = $cgi->param('cookie_consent'); - if (!$old_cookie_consent && $new_cookie_consent) { - Bugzilla->request_cache->{cookie_consent_changed} = 'yes'; - $cgi->send_cookie(-name => 'moz-consent-pref', -value => 'yes'); - } - elsif ($old_cookie_consent && !$new_cookie_consent) { - Bugzilla->request_cache->{cookie_consent_changed} = 'no'; - $cgi->send_cookie(-name => 'moz-consent-pref', -value => 'no'); - } - } } sub MfaAccount { From b0b8aa6e4627173457bdd9b99557f0704572d129 Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Mon, 16 Dec 2024 10:17:07 -0500 Subject: [PATCH 09/10] Adding additional non-essential cookie 'mfa_verification_token' --- Bugzilla/Constants.pm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 6487b6e2c2..5e34ef1831 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -686,9 +686,17 @@ use constant BOUNCE_COUNT_MAX => 5; use constant CONSENT_COOKIE => 'moz-consent-pref'; # List of essential cookies that cannot be opted out -use constant ESSENTIAL_COOKIES => - qw(bugzilla Bugzilla_login Bugzilla_logincookie Bugzilla_login_request_cookie - bugzilla github_state github_token sudo moz-consent-pref); +use constant ESSENTIAL_COOKIES => qw( + bugzilla + Bugzilla_login + Bugzilla_logincookie + Bugzilla_login_request_cookie + github_state + github_token + mfa_verification_token + moz-consent-pref + sudo +); # List of countries that require cookie consent use constant COOKIE_CONSENT_COUNTRIES => qw( From 9e6a8a339f1a38413c13371c5cfc713174e0ce1a Mon Sep 17 00:00:00 2001 From: David Lawrence Date: Thu, 19 Dec 2024 13:02:43 -0500 Subject: [PATCH 10/10] Updated automated test to new cookies settings page --- qa/t/1_test_cookie_consent.t | 16 ++++++---------- template/en/default/pages/cookies.html.tmpl | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/qa/t/1_test_cookie_consent.t b/qa/t/1_test_cookie_consent.t index 6dc669562d..d87d6cebec 100644 --- a/qa/t/1_test_cookie_consent.t +++ b/qa/t/1_test_cookie_consent.t @@ -50,18 +50,14 @@ $cookies = $sel->driver->get_all_cookies; my $last_order_cookie = first { $_->{name} eq 'LASTORDER' } @{$cookies}; ok($last_order_cookie, 'Last order cookie set properly'); -# Under user preferences, change cookie preferences to reject +# Change cookie preferences to reject $sel->click_ok('header-account-menu-button'); -$sel->click_ok('link=Preferences'); +$sel->click_ok('link=Cookies'); $sel->wait_for_page_to_load(WAIT_TIME); -$sel->title_is('User Preferences'); -$sel->click_ok('link=Account'); -$sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is('User Preferences'); -$sel->uncheck_ok('cookie_consent'); -$sel->click_ok('update'); -$sel->wait_for_page_to_load(WAIT_TIME); -$sel->title_is('User Preferences'); +$sel->title_is('Cookie Settings'); +$sel->click_ok('cookie-radio-preference-no'); +$sel->click_ok('cookie-consent-save'); +$sel->is_text_present_ok('Your Cookie settings have been updated'); # Clear all cookies except the consent cookie and login stuff $cookies = $sel->driver->get_all_cookies; diff --git a/template/en/default/pages/cookies.html.tmpl b/template/en/default/pages/cookies.html.tmpl index 1bb7a52b4e..f3ac748b87 100644 --- a/template/en/default/pages/cookies.html.tmpl +++ b/template/en/default/pages/cookies.html.tmpl @@ -58,7 +58,7 @@

- +