Bugzilla – Attachment 154264 Details for
Bug 27378
Enable compliance with EU Cookie Legislation via cookie consent
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 27378: Introduce cookie consent to OPAC and staff client
Bug-27378-Introduce-cookie-consent-to-OPAC-and-sta.patch (text/plain), 62.31 KB, created by
Katrin Fischer
on 2023-08-04 13:08:41 UTC
(
hide
)
Description:
Bug 27378: Introduce cookie consent to OPAC and staff client
Filename:
MIME Type:
Creator:
Katrin Fischer
Created:
2023-08-04 13:08:41 UTC
Size:
62.31 KB
patch
obsolete
>From c102fe2f33f9366c4eda97265ad5295fe2442bb7 Mon Sep 17 00:00:00 2001 >From: Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >Date: Mon, 11 Jan 2021 14:50:17 +0000 >Subject: [PATCH] Bug 27378: Introduce cookie consent to OPAC and staff client > >To avoid confusion around commit messages and the content of this enhancement, this first commit is a squashed commit of all the original code submited to this bug. Following a few years of inactivity, it has been rebased and re-submitted with some fixes and concept changes contained in the more recent commits. > >Signed-ff-by: Barry Cannon <bc@interleaf.ie> >Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de> >--- > Koha/Template/Plugin/JSConsents.pm | 64 ++++ > admin/preferences.pl | 17 +- > .../bug_27378_add_ConsentJS_syspref.perl | 8 + > ...ug_27378_add_CookieConsentBar_syspref.perl | 8 + > ..._27378_add_CookieConsentPopup_syspref.perl | 8 + > .../bug_27378_add_CookieConsent_syspref.perl | 8 + > installer/data/mysql/mandatory/sysprefs.sql | 4 + > .../intranet-tmpl/prog/css/preferences.css | 9 +- > .../prog/css/src/staff-global.scss | 80 ++++ > .../prog/en/includes/intranet-bottom.inc | 69 ++++ > .../prog/en/modules/admin/preferences.tt | 2 + > .../en/modules/admin/preferences/patrons.pref | 24 ++ > .../intranet-tmpl/prog/js/cookieconsent.js | 183 +++++++++ > .../prog/js/pages/preferences.js | 360 +++++++++++++++--- > .../opac-tmpl/bootstrap/css/src/opac.scss | 57 +++ > .../bootstrap/en/includes/masthead.inc | 65 ++++ > .../bootstrap/en/includes/opac-bottom.inc | 14 + > .../bootstrap/en/includes/usermenu.inc | 2 +- > .../en/modules/opac-patron-consent.tt | 5 + > .../opac-tmpl/bootstrap/js/cookieconsent.js | 199 ++++++++++ > .../Koha/Template/Plugin/JSConsents.t | 25 ++ > 21 files changed, 1157 insertions(+), 54 deletions(-) > create mode 100644 Koha/Template/Plugin/JSConsents.pm > create mode 100644 installer/data/mysql/atomicupdate/bug_27378_add_ConsentJS_syspref.perl > create mode 100644 installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentBar_syspref.perl > create mode 100644 installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentPopup_syspref.perl > create mode 100644 installer/data/mysql/atomicupdate/bug_27378_add_CookieConsent_syspref.perl > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/cookieconsent.js > create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/cookieconsent.js > create mode 100755 t/db_dependent/Koha/Template/Plugin/JSConsents.t > >diff --git a/Koha/Template/Plugin/JSConsents.pm b/Koha/Template/Plugin/JSConsents.pm >new file mode 100644 >index 0000000000..a21ae178c3 >--- /dev/null >+++ b/Koha/Template/Plugin/JSConsents.pm >@@ -0,0 +1,64 @@ >+package Koha::Template::Plugin::JSConsents; >+ >+# Copyright 2021 PTFS Europe >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Template::Plugin; >+use base qw( Template::Plugin ); >+use MIME::Base64 qw{ decode_base64 }; >+use JSON qw{ decode_json }; >+ >+use C4::Context; >+ >+sub all { >+ my ( $self ) = @_; >+ my $consents = C4::Context->preference( 'ConsentJS' ); >+ if (length $consents > 0) { >+ my $decoded = decode_base64($consents); >+ return decode_json $decoded; >+ } else { >+ return []; >+ } >+} >+ >+1; >+ >+=head1 NAME >+ >+Koha::Template::Plugin::JSConsents - TT Plugin for Javascript consents >+Provided by ConsentJS syspref >+ >+=head1 SYNOPSIS >+ >+[% USE JSConsents %] >+ >+[% JSConsents.all() %] >+ >+=head1 ROUTINES >+ >+=head2 all >+ >+In a template, you can get the all Javascript snippets >+that require consent using the following TT: >+[% JSConsents.all() %] >+The consents are returned in an array of hashes >+ >+=head1 AUTHOR >+ >+Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >+ >+=cut >diff --git a/admin/preferences.pl b/admin/preferences.pl >index 45a5891c62..73b8c442e3 100755 >--- a/admin/preferences.pl >+++ b/admin/preferences.pl >@@ -59,12 +59,17 @@ sub _get_chunk { > if( $options{'syntax'} ){ > $chunk->{'syntax'} = $options{'syntax'}; > } >- >- if( $options{'type'} && $options{'type'} eq 'modalselect' ){ >- $chunk->{'source'} = $options{'source'}; >- $chunk->{'exclusions'} = $options{'exclusions'} // ""; >- $chunk->{'required'} = $options{'required'} // ""; >- $chunk->{'type'} = 'modalselect'; >+ if( $options{'type'} ) { >+ if( $options{'type'} eq 'modalselect' ){ >+ $chunk->{'source'} = $options{'source'}; >+ $chunk->{'exclusions'} = $options{'exclusions'} // ""; >+ $chunk->{'required'} = $options{'required'} // ""; >+ $chunk->{'type'} = 'modalselect'; >+ } elsif( $options{'type'} eq 'modaljs' ){ >+ $chunk->{'type'} = 'modaljs'; >+ $chunk->{'initiator'} = $options{'initiator'}; >+ $chunk->{'processor'} = $options{'processor'}; >+ } > } > > if ( $options{'class'} && $options{'class'} eq 'password' ) { >diff --git a/installer/data/mysql/atomicupdate/bug_27378_add_ConsentJS_syspref.perl b/installer/data/mysql/atomicupdate/bug_27378_add_ConsentJS_syspref.perl >new file mode 100644 >index 0000000000..b219ead1e7 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_27378_add_ConsentJS_syspref.perl >@@ -0,0 +1,8 @@ >+$DBversion = 'XXX'; # will be replaced by the RM >+if( CheckVersion( $DBversion ) ) { >+ $dbh->do( q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('ConsentJS', '', 'Specify Javascript that requires user consent to run (e.g. tracking code)', '', 'Free'); | ); >+ >+ # Always end with this (adjust the bug info) >+ SetVersion( $DBversion ); >+ print "Upgrade to $DBversion done (Bug 27378 - Add ConsentJS syspref)\n"; >+} >diff --git a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentBar_syspref.perl b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentBar_syspref.perl >new file mode 100644 >index 0000000000..c1f2f82cf6 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentBar_syspref.perl >@@ -0,0 +1,8 @@ >+$DBversion = 'XXX'; # will be replaced by the RM >+if( CheckVersion( $DBversion ) ) { >+ $dbh->do( q{ INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsentBar', '', 'Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen', '70|10', 'Textarea'); } ); >+ >+ # Always end with this (adjust the bug info) >+ SetVersion( $DBversion ); >+ print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsentBar syspref)\n"; >+} >diff --git a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentPopup_syspref.perl b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentPopup_syspref.perl >new file mode 100644 >index 0000000000..0420876959 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentPopup_syspref.perl >@@ -0,0 +1,8 @@ >+$DBversion = 'XXX'; # will be replaced by the RM >+if( CheckVersion( $DBversion ) ) { >+ $dbh->do( q{INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsentPopup', '', 'Show the following HTML in the cookie consent popup', '70|10', 'Textarea');} ); >+ >+ # Always end with this (adjust the bug info) >+ SetVersion( $DBversion ); >+ print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsentPopup syspref)\n"; >+} >diff --git a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsent_syspref.perl b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsent_syspref.perl >new file mode 100644 >index 0000000000..3592a64248 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsent_syspref.perl >@@ -0,0 +1,8 @@ >+$DBversion = 'XXX'; # will be replaced by the RM >+if( CheckVersion( $DBversion ) ) { >+ $dbh->do( q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsent', '0', 'Require cookie consent to be displayed', '', 'YesNo'); | ); >+ >+ # Always end with this (adjust the bug info) >+ SetVersion( $DBversion ); >+ print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsent syspref)\n"; >+} >diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql >index 30197591a8..b726cf069a 100644 >--- a/installer/data/mysql/mandatory/sysprefs.sql >+++ b/installer/data/mysql/mandatory/sysprefs.sql >@@ -156,8 +156,12 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('ComponentSortField','title','call_number|pubdate|acqdate|title|author','Specify the default field used for sorting','Choice'), > ('ComponentSortOrder','asc','asc|dsc|az|za','Specify the default sort order','Choice'), > ('ConfirmFutureHolds','0','','Number of days for confirming future holds','Integer'), >+('ConsentJS', '', NULL, 'Specify Javascript that requires user consent to run (e.g. tracking code)', 'Free'), > ('ConsiderOnSiteCheckoutsAsNormalCheckouts','1',NULL,'Consider on-site checkouts as normal checkouts','YesNo'), > ('ContentWarningField', '', NULL, 'MARC field to use for content warnings', 'Free'), >+('CookieConsent', '0', NULL, 'Require cookie consent to be displayed', 'YesNo'), >+('CookieConsentBar', '', '70|10', 'Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen', 'Textarea'), >+('CookieConsentPopup', '', '70|10', 'Show the following HTML in the cookie consent popup', 'Textarea'), > ('CreateAVFromCataloguing', '1', '', 'Ability to create authorized values from the cataloguing module', 'YesNo'), > ('CronjobLog','0',NULL,'If ON, log information from cron jobs.','YesNo'), > ('CumulativeRestrictionPeriods',0,NULL,'Cumulate the restriction periods instead of keeping the highest','YesNo'), >diff --git a/koha-tmpl/intranet-tmpl/prog/css/preferences.css b/koha-tmpl/intranet-tmpl/prog/css/preferences.css >index ce15265389..d0f41db136 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/preferences.css >+++ b/koha-tmpl/intranet-tmpl/prog/css/preferences.css >@@ -3,7 +3,8 @@ > .preference-multi, > .preference-long, > .preference-file, >-.preference-modalselect { >+.preference-modalselect, >+.preference-modaljs { > width: 20em; > } > >@@ -15,11 +16,13 @@ > width: 5em; > } > >-input[type="text"].modalselect { >+input[type="text"].modalselect, >+input[type="text"].modaljs { > cursor: pointer; > } > >-input[type="text"].modalselect:hover { >+input[type="text"].modalselect:hover, >+input[type="text"].modaljs:hover { > background-color: #FFC; > } > >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index abbc4a82b7..4c05c3862c 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -4295,6 +4295,86 @@ div .suggestion_note { > } > } > >+#consentJSItems { >+ #consentJSWarning { >+ margin: 20px 0; >+ text-align: center; >+ font-weight: 800; >+ font-size: 120%; >+ } >+ .consentJSItem { >+ margin-bottom: 30px; >+ padding: 10px; >+ border: 1px solid #ccc; >+ } >+ .consentRow { >+ display: flex; >+ margin-bottom: 20px; >+ .consentItem { >+ margin-right: 40px; >+ } >+ .consentItem:last-child { >+ margin-right: 0; >+ } >+ } >+ .codeRow { >+ display: block; >+ } >+ .consentDelete { >+ display: block; >+ text-align: right; >+ } >+} >+ >+/* Cookie consent bar */ >+#cookieConsentBar { >+ display: none; >+ padding: 20px; >+ position: fixed; >+ bottom: 0; >+ left: 0; >+ right: 0; >+ background: rgba(0,0,0,0.7); >+ color: rgba(255,255,255,0.9); >+ z-index: 1; >+ a:link, a:visited { >+ color: #d7ebff; >+ } >+ #consentButtons { >+ margin-top: 10px; >+ text-align: right; >+ } >+ button:not(:last-child) { >+ margin-right: 10px; >+ } >+} >+ >+/* Cookie consent modal */ >+#cookieConsentModal { >+ .modal-dialog { >+ width: 700px; >+ } >+ .consentModalItem { >+ display: flex; >+ align-items: center; >+ border: 1px solid #ccc; >+ padding: 10px 20px; >+ border-radius: 5px; >+ } >+ .consentModalItem:not(:last-child) { >+ margin-bottom: 20px; >+ } >+ .consentItemCheckbox { >+ padding-right: 20px; >+ } >+ .consentItemName { >+ font-weight: bold; >+ } >+ #cookieConsentPopupText { >+ margin: 20px 0; >+ } >+} >+ > @import "header"; > @import "toolbar"; > @import "forms"; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/intranet-bottom.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/intranet-bottom.inc >index 27ba415943..3cbf7f4c1f 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/intranet-bottom.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/intranet-bottom.inc >@@ -1,6 +1,10 @@ > [% USE raw %] > [% USE Koha %] > [% USE KohaPlugins %] >+[% USE Asset %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% USE JSConsents %] >+[% END %] > </div> > [% IF ( ( languages_loop ) && ( ! popup_window ) && ( Koha.Preference('StaffLangSelectorMode') == 'both' || Koha.Preference('StaffLangSelectorMode') == 'footer') ) %] > [% UNLESS ( one_language_enabled ) %] >@@ -74,5 +78,70 @@ > [% END %] > [% KohaPlugins.get_plugins_intranet_js | $raw %] > >+ <!-- Cookie consent --> >+ [% IF Koha.Preference( 'CookieConsent' ) %] >+ <!-- Cookie consent bar --> >+ <div id="cookieConsentBar" aria-hidden="true"> >+ [% Koha.Preference( 'CookieConsentBar' ) | $raw %] >+ <div id="consentButtons"> >+ <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button> >+ [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %] >+ <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button> >+ <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button> >+ [% END %] >+ </div> >+ </div> <!-- /#cookieConsentBar --> >+ <!-- Cookie consent modal --> >+ <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true"> >+ <div class="modal-dialog"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2> >+ </div> >+ <div class="modal-body"> >+ [% IF Koha.Preference( 'CookieConsentPopup' ) %] >+ <div id="cookieConsentPopupText"> >+ [% Koha.Preference( 'CookieConsentPopup' ) | $raw %] >+ </div> >+ [% END %] >+ <div id="consentCookieList"> >+ [% consents = JSConsents.all() %] >+ [% FOREACH consent IN consents %] >+ <div class="consentModalItem"> >+ <div class="consentItemCheckbox"> >+ <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]"> >+ </div> >+ <div class="consentItemMeta"> >+ <div class="consentItemName">[% consent.name | html %]</div> >+ <div class="consentItemDescription">[% consent.description | html %]</div> >+ </div> >+ </div> >+ [% END %] >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <div id="consentButtons"> >+ <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button> >+ [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %] >+ <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button> >+ <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button> >+ [% END %] >+ </div> >+ </div> >+ </div> <!-- /.modal-content --> >+ </div> <!-- /.modal-dialog --> >+ </div> <!-- /#cookieConsentModal --> >+ [% END %] >+ <!-- ConsentJS code that may run --> >+ [% IF Koha.Preference( 'CookieConsent' ) && Koha.Preference( 'ConsentJS' ).length > 0 %] >+ [% consents = JSConsents.all() %] >+ [% FOREACH consent IN consents %] >+ <div class="consentCode" style="display:none" aria-hidden="true" data-consent-id="[% consent.id | html %]" data-consent-code="[% consent.code | html %]" data-consent-match-pattern="[% consent.matchPattern | html %]" data-consent-cookie-domain="[% consent.cookieDomain | html %]" data-consent-cookie-path="[% consent.cookiePath | html %]" data-requires-consent="[% consent.staffConsent ? 'true' : 'false' | html %]"></div> >+ [% END %] >+ [% END %] >+ [% IF Koha.Preference( 'CookieConsent' ) %] >+ [% Asset.js("js/cookieconsent.js") | $raw %] >+ [% END %] >+ > </body> > </html> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences.tt >index 1bc1458141..21da1e7115 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences.tt >@@ -130,6 +130,8 @@ > </select> > [% ELSIF ( CHUNK.type_modalselect ) %] > <input type="text" name="pref_[% CHUNK.name | html %]" id="pref_[% CHUNK.name | html %]" class="modalselect preference preference-[% CHUNK.type | html %]" data-source="[% CHUNK.source | html %]" data-required="[% CHUNK.required | html %]" data-exclusions="[% CHUNK.exclusions | html %]" readonly="readonly" value="[% CHUNK.value | html %]"/> >+ [% ELSIF ( CHUNK.type_modaljs ) %] >+ <input type="text" name="pref_[% CHUNK.name | html %]" data-initiator="[% CHUNK.initiator | html %]" data-processor="[% CHUNK.processor %]" id="pref_[% CHUNK.name | html %]" class="modaljs preference preference-[% CHUNK.type | html %]" readonly="readonly" value="[% CHUNK.value | html %]"/> > [% ELSIF ( CHUNK.type_multiple ) %] > <select name="pref_[% CHUNK.name | html %]" id="pref_[% CHUNK.name | html %]" class="preference preference-[% CHUNK.class or "choice" | html %]" multiple="multiple"> > [% FOREACH CHOICE IN CHUNK.CHOICES %][% IF ( CHOICE.selected ) %]<option value="[% CHOICE.value | html %]" selected="selected">[% ELSE %]<option value="[% CHOICE.value | html %]">[% END %][% CHOICE.text | html %]</option>[% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >index 32cd34f706..16ede50769 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >@@ -388,6 +388,30 @@ Patrons: > - days and remove anonymized patron accounts after > - pref: PatronRemovalDelay > - "days.<br>IMPORTANT: No action is performed when these delays are empty (no text). But a zero value ('0') is interpreted as no delay (do it now)! The actions are performed by the cleanup database cron job." >+ - >+ - pref: CookieConsent >+ choices: >+ yes: Require >+ no: "Don't require" >+ - cookie consent to be displayed >+ - >+ - Specify Javascript that requires user consent to run (e.g. tracking code) >+ - pref: ConsentJS >+ type: modaljs >+ initiator: populateConsentJS >+ processor: prepareConsentJS >+ - >+ - Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen >+ - pref: CookieConsentBar >+ type: textarea >+ syntax: text/html >+ class: code >+ - >+ - Show the following HTML in the cookie consent popup >+ - pref: CookieConsentPopup >+ type: textarea >+ syntax: text/html >+ class: code > Security: > - > - Login passwords for staff and patrons must be at least >diff --git a/koha-tmpl/intranet-tmpl/prog/js/cookieconsent.js b/koha-tmpl/intranet-tmpl/prog/js/cookieconsent.js >new file mode 100644 >index 0000000000..b74dba1066 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/cookieconsent.js >@@ -0,0 +1,183 @@ >+(function () { >+ // Has the user previously consented, establish our >+ // initial state >+ let selected = []; >+ let existingConsent = ''; >+ // The presence of a 'cookieConsent' local storage item indicates >+ // that previous consent has been set. If the value is empty, then >+ // no consent to non-essential cookies was given >+ const hasStoredConsent = localStorage.getItem('cookieConsent'); >+ getExistingConsent(); >+ >+ // The consent bar may not be in the DOM if it is not being used >+ const consentBar = $('#cookieConsentBar'); >+ if (!consentBar) { >+ return; >+ } >+ if (hasStoredConsent === null) { >+ showConsentBar(); >+ } >+ addButtonHandlers(); >+ >+ // When the modal is opened, populate our state based on currently >+ // selected values >+ $('#cookieConsentModal').on('shown.bs.modal', function () { >+ initialiseSelected(); >+ }); >+ >+ // Initialise existing consent based on local storage >+ function getExistingConsent() { >+ existingConsent = localStorage.getItem('cookieConsent') ? >+ localStorage.getItem('cookieConsent') : >+ []; >+ } >+ >+ function showConsentBar() { >+ const consentBar = $('#cookieConsentBar'); >+ consentBar.attr('aria-hidden', 'false'); >+ consentBar.show(); >+ } >+ >+ function hideConsentBar() { >+ const consentBar = $('#cookieConsentBar'); >+ consentBar.attr('aria-hidden', 'true'); >+ consentBar.hide(); >+ } >+ >+ // Hides the appropriate consent container, depending on what >+ // is currently visible >+ function hideContainer() { >+ if ($('#cookieConsentModal').hasClass('in')) { >+ $('#cookieConsentModal').modal('hide'); >+ } else { >+ hideConsentBar(); >+ } >+ } >+ >+ // Initialise our state of selected item and enable/disable >+ // the "Accept selected" button appropriately >+ function initialiseSelected() { >+ selected = []; >+ $('.consentCheckbox').each(function () { >+ const val = $(this).val(); >+ if (existingConsent.indexOf(val) > -1 ) { >+ $(this).prop('checked', true); >+ selected.push(val); >+ } else { >+ $(this).prop('checked', false); >+ } >+ }); >+ enableDisableSelectedButton(); >+ } >+ >+ // Maintain our state of selected items and enable/disable >+ // the "Accept selected" button appropriately >+ function maintainSelected() { >+ selected = []; >+ $('.consentCheckbox:checked').each(function () { >+ selected.push($(this).val()); >+ }); >+ enableDisableSelectedButton(); >+ } >+ >+ // Set the enabled / disabled state of the >+ // "Accept selected button based on the checkbox >+ // states >+ function enableDisableSelectedButton() { >+ $('#consentAcceptSelected').prop( >+ 'disabled', >+ selected.length === 0 >+ ); >+ } >+ >+ function runConsentedCode() { >+ getExistingConsent(); >+ $('.consentCode').each(function () { >+ // The user has explicitly consented to this code, or the >+ // code doesn't require consent in the staff view >+ if ( >+ existingConsent.indexOf($(this).data('consent-id')) > -1 || >+ !$(this).data('requires-consent') >+ ) { >+ const code = atob($(this).data('consent-code')); >+ const func = Function(code); >+ func(); >+ } else { >+ // This code doesn't have consent to run, we may need to remove >+ // any cookies it has previously set >+ const matchPattern = $(this).data('consent-match-pattern'); >+ const cookieDomain = $(this).data('consent-cookie-domain'); >+ const cookiePath = $(this).data('consent-cookie-path'); >+ if (matchPattern.length > 0) { >+ const regex = new RegExp(matchPattern); >+ const allCookies = document.cookie.split('; '); >+ allCookies.forEach(function (cookie) { >+ const name = cookie.split('=')[0]; >+ if (regex.test(name)) { >+ document.cookie = name + '=; expires=Thu, 01 Jan 1970 00: 00: 01 GMT; domain=' + cookieDomain + '; path=' + cookiePath; >+ } >+ }); >+ >+ } >+ } >+ }); >+ } >+ >+ function addButtonHandlers() { >+ // "Accept all" handler >+ $('.consentAcceptAll').on('click', function(e) { >+ e.preventDefault(); >+ let toSave = []; >+ $('.consentCheckbox').each(function () { >+ const val = $(this).val(); >+ toSave.push(val); >+ }); >+ localStorage.setItem('cookieConsent', toSave); >+ hideContainer(); >+ runConsentedCode(); >+ }); >+ >+ // "Accept essential" handler >+ $('.consentAcceptEssential').on('click', function(e) { >+ e.preventDefault(); >+ localStorage.setItem('cookieConsent', []); >+ hideContainer(); >+ runConsentedCode(); >+ }); >+ >+ // "Accept selected" handler >+ $('#consentAcceptSelected').on('click', function(e) { >+ e.preventDefault(); >+ const toSave = selected.length > 0 ? selected : []; >+ localStorage.setItem('cookieConsent', toSave); >+ hideContainer(); >+ runConsentedCode(); >+ }); >+ >+ // "More information" handler >+ $('#consentMoreInfo').on( >+ 'click', >+ function (e) { >+ e.preventDefault(); >+ hideConsentBar(); >+ // Ensure we're up to date with the existing consent >+ getExistingConsent(); >+ // Prevent the modal from being closed with anything >+ // but the buttons >+ $('#cookieConsentModal') >+ .modal({ >+ backdrop: 'static', >+ keyboard: false, >+ focus: true, >+ show: true >+ }); >+ } >+ ); >+ $('.consentCheckbox').on('click', function () { >+ maintainSelected(); >+ }); >+ } >+ >+ // On page load, run any code that has been given consent >+ runConsentedCode(); >+})(); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/pages/preferences.js b/koha-tmpl/intranet-tmpl/prog/js/pages/preferences.js >index f5da1d92c4..0d5972ddb1 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/pages/preferences.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/pages/preferences.js >@@ -64,6 +64,80 @@ window.onbeforeunload = function () { > } > }; > >+// Add event handlers to any elements with .expand-textarea classes >+// this allows the CodeMirror widget to be displayed >+function addExpandHandler() { >+ // Don't duplicate click event handlers >+ const ev = $._data($('.expand-textarea'), 'events'); >+ if (ev && ev.click) { >+ return; >+ } >+ $(".expand-textarea").on("click", function (e) { >+ e.preventDefault(); >+ $(this).hide(); >+ var target = $(this).data("target"); >+ var syntax = $(this).data("syntax"); >+ $("#collapse_" + target).show(); >+ if (syntax) { >+ var editor = CodeMirror.fromTextArea(document.getElementById("pref_" + target), { >+ lineNumbers: true, >+ mode: syntax, >+ lineWrapping: true, >+ viewportMargin: Infinity, >+ gutters: ["CodeMirror-lint-markers"], >+ lint: true >+ }); >+ editor.on("change", function () { >+ mark_modified.call($("#pref_" + target)[0]); >+ }); >+ editor.on("blur", function () { >+ editor.save(); >+ }); >+ } else { >+ $("#pref_" + target).show(); >+ } >+ }); >+} >+ >+// Add event handlers to any elements with .collapse-textarea classes >+// this allows the hiding of the CodeMirror widget >+function addCollapseHandler() { >+ // Don't duplicate click event handlers >+ const ev = $._data($('.collapse-textarea'), 'events'); >+ if (ev && ev.click) { >+ return; >+ } >+ $(".collapse-textarea").on("click", function (e) { >+ e.preventDefault(); >+ $(this).hide(); >+ var target = $(this).data("target"); >+ var syntax = $(this).data("syntax"); >+ $("#expand_" + target).show(); >+ if (syntax) { >+ var editor = $("#pref_" + target).next(".CodeMirror")[0].CodeMirror; >+ editor.toTextArea(); >+ } >+ $("#pref_" + target).hide(); >+ }); >+} >+ >+// Add a handler for any consent delete links >+function addConsentDeleteHandler() { >+ // Don't duplicate click event handlers >+ const ev = $._data($('.consentDelete'), 'events'); >+ if (ev && ev.click) { >+ return; >+ } >+ $('.consentDelete').on('click', function (e) { >+ e.preventDefault(); >+ const target = $(this).data('target'); >+ const proceed = confirm(__('Are you sure you want to delete this consent item?')); >+ if (proceed) { >+ $('#' + target).remove(); >+ } >+ }); >+} >+ > $( document ).ready( function () { > > $("table.preferences").dataTable($.extend(true, {}, dataTablesDefaults, { >@@ -107,44 +181,9 @@ $( document ).ready( function () { > return false; > }); > >- $( ".expand-textarea" ).on("click", function(e){ >- e.preventDefault(); >- $(this).hide(); >- var target = $(this).data("target"); >- var syntax = $(this).data("syntax"); >- $("#collapse_" + target ).show(); >- if( syntax ){ >- var editor = CodeMirror.fromTextArea( document.getElementById( "pref_" + target ), { >- lineNumbers: true, >- mode: syntax, >- lineWrapping: true, >- viewportMargin: Infinity, >- gutters: ["CodeMirror-lint-markers"], >- lint: true >- }); >- editor.on("change", function(){ >- mark_modified.call( $("#pref_" + target )[0]); >- }); >- editor.on("blur", function(){ >- editor.save(); >- }); >- } else { >- $("#pref_" + target ).show(); >- } >- }); >+ addExpandHandler(); > >- $( ".collapse-textarea" ).on("click", function(e){ >- e.preventDefault(); >- $(this).hide(); >- var target = $(this).data("target"); >- var syntax = $(this).data("syntax"); >- $("#expand_" + target ).show(); >- if( syntax ){ >- var editor = $("#pref_" + target ).next(".CodeMirror")[0].CodeMirror; >- editor.toTextArea(); >- } >- $("#pref_" + target ).hide(); >- }); >+ addCollapseHandler(); > > $("h3").attr("class", "expanded").attr("title", __("Click to collapse this section")); > var collapsible = $(".collapsed,.expanded"); >@@ -262,24 +301,257 @@ $( document ).ready( function () { > }).appendTo("#prefModalForm"); > > $("#saveModalPrefs").data("target", this.id ); >+ $("#saveModalPrefs").data("type", "modalselect" ); > $("#prefModalLabel").text( pref_name ); > $("#prefModal").modal("show"); > }); > >- $("#saveModalPrefs").on("click", function(){ >- var formfieldid = $("#" + $(this).data("target") ); >+ // Initialise the content of our modal, using the function >+ // specified in the data-initiator attribute >+ $('.modaljs').on('click', function () { >+ const init = $(this).data('initiator'); >+ if (init) { >+ window[init](this); >+ $("#prefModal").modal("show"); >+ } >+ }); >+ >+ // Initialise the content of our modal, if we are dealing >+ // with a modalselect modal >+ function prepareModalSelect(formfieldid) { > var prefs = []; > $("#prefModal input[type='checkbox']").each(function(){ > if( $(this).prop("checked") ){ > prefs.push( this.value ); > } > }); >+ return prefs.join("|"); >+ } >+ >+ // Return a checkbox with an appropriate checked state >+ function checkBox(id, className, state) { >+ return state ? >+ '<input id="' + id + '" type="checkbox" class="' + className + '" checked>' : >+ '<input id="' + id + '" type="checkbox" class="' + className + '">'; >+ } >+ >+ // Create a consentJS item, correctly populated >+ function createConsentJSItem(item, idx) { >+ const id = 'ConsentJS_' + idx; >+ const code = item.code && item.code.length > 0 ? atob(item.code) : ''; >+ const itemId = item.id && item.id.length > 0 ? item.id : ''; >+ return '<div id="' + id + '" class="consentJSItem" data-id="' + itemId + '">' + >+ ' <div class="consentRow">' + >+ ' <div class="consentItem">' + >+ ' <label class="required" for="name_' + id + '">' + __('Name') + ':</label>' + >+ ' <input id="name_' + id + '" class="metaName" type="text" value="' + item.name + '"><span class="required">' + __('Required') + '</span>' + >+ ' </div >' + >+ ' <div class="consentItem">' + >+ ' <label class="required" for="description_' + id + '">' + __('Description') + ':</label>' + >+ ' <input id="description_' + id + '" class="metaDescription" type="text" value="' + item.description + '"><span class="required">' + __('Required') + '</span>' + >+ ' </div>' + >+ ' <div class="consentItem">' + >+ ' <label for="opacConsent_' + id + '">' + __('Requires consent in OPAC') + ':</label>' + >+ checkBox('opacConsent_' + id, 'opacConsent', item.opacConsent) + >+ ' </div>' + >+ ' <div class="consentItem">' + >+ ' <label for="staffConsent_' + id + '">' + __('Requires consent in staff interface') + ':</label>' + >+ checkBox('staffConsent_' + id, 'staffConsent', item.staffConsent) + >+ ' </div >' + >+ ' <div class="consentItem">' + >+ ' <label for="matchPattern_' + id + '">' + __('String used to identify cookie name') + ':</label>' + >+ ' <input id="matchPattern_' + id + '" class="metaMatchPattern" type="text" value="' + item.matchPattern + '"><span class="required">' + __('Required') + '</span>' + >+ ' </div >' + >+ ' <div class="consentItem">' + >+ ' <label for="cookieDomain' + id + '">' + __('Cookie domain') + ':</label>' + >+ ' <input id="cookieDomain' + id + '" class="metaCookieDomain" type="text" value="' + item.cookieDomain + '"><span class="required">' + __('Required') + '</span>' + >+ ' </div >' + >+ ' <div class="consentItem">' + >+ ' <label for="cookiePath' + id + '">' + __('Cookie path') + ':</label>' + >+ ' <input id="cookiePath' + id + '" class="metaCookiePath" type="text" value="' + item.cookiePath + '"><span class="required">' + __('Required') + '</span>' + >+ ' </div >' + >+ ' </div >' + >+ ' <div class="consentRow codeRow">' + >+ ' <textarea style="display:none;" id="pref_' + id + '" class="preference preference-code codemirror" rows="10" cols="40">' + code + '</textarea>' + >+ ' <div>' + >+ ' <a id="expand_' + id + '" class="expand-textarea" data-target="' + id + '" data-syntax="javascript" href="#">' + __('Click to expand') + '</a>' + >+ ' <a id="collapse_' + id + '" class="collapse-textarea" data-target="' + id + '" data-syntax="javascript" href="#" style="display:none">' + __('Click to collapse') + '</a>' + >+ ' </div >' + >+ ' </div>' + >+ ' <a class="consentDelete" data-target="' + id + '" href="#">Delete</a>' + >+ '</div > '; >+ } >+ >+ // Return the markup for all consentJS items concatenated >+ function populateConsentMarkup(items) { >+ return items.reduce(function (acc, current, idx) { >+ return acc + createConsentJSItem(current, idx); >+ }, ''); >+ } >+ >+ // Return the markup for a validation warning >+ function populateValidationWarning() { >+ return '<div id="consentJSWarning" class="error" style="display:none;">' + __('You must complete all fields') + '</div>'; >+ } >+ >+ // Return a new, empty consent item >+ function emptyConsentItem() { >+ return { >+ name: '', >+ description: '', >+ matchPattern: '', >+ cookieDomain: '', >+ cookiePath: '', >+ code: '', >+ consentInOpac: false, >+ consentInStaff: false >+ }; >+ } >+ >+ // Add the handler for a new empty consent item >+ function addNewHandler() { >+ $("#consentJSAddNew").on("click", function (e) { >+ e.preventDefault(); >+ const currentLen = $('.consentJSItem').length; >+ const newItem = emptyConsentItem(); >+ const markup = createConsentJSItem(newItem, currentLen); >+ $('#prefModal .modal-body #consentJSItems').append($(markup)); >+ addExpandHandler(); >+ addCollapseHandler(); >+ addConsentDeleteHandler(); >+ }); >+ } >+ >+ // Populate the consentJS modal, we also initialise any >+ // event handlers that are required. This function is added >+ // to the window object so we can call it if we are passed it's name >+ // as a data-initiator attribute >+ // (e.g.) >+ // const f = 'populateConsentJS'; >+ // window[f](); >+ window.populateConsentJS = function(el) { >+ let items = []; >+ let decoded = ''; >+ if (el.value && el.value.length > 0) { >+ try { >+ decoded = atob(el.value); >+ } catch (err) { >+ throw (__( >+ 'Unable to Base64 decode value stored in ConsentJS syspref: ' + >+ err.message >+ )); >+ } >+ try { >+ items = JSON.parse(decoded); >+ } catch (err) { >+ throw (__( >+ 'Unable to JSON parse decoded value stored in ConsentJS syspref: ' + >+ err.message >+ )); >+ } >+ } >+ const markup = populateConsentMarkup(items); >+ const validationWarning = populateValidationWarning(); >+ const pref_name = el.id.replace(/pref_/, ''); >+ $('#saveModalPrefs').data('target', el.id); >+ $('#prefModalLabel').text( pref_name ); >+ $('#prefModal .modal-body').html($('<div id="consentJSItems">' + validationWarning + markup + '</div><div><a href="#" id="consentJSAddNew">' + __('Add new code') + '</a></div>')); >+ addExpandHandler(); >+ addCollapseHandler(); >+ addNewHandler(); >+ addConsentDeleteHandler(); >+ } >+ >+ // Prepare the data in the UI for sending back as a syspref. >+ // We validate that everything is what we expect. This function is added >+ // to the window object so we can call it if we are passed it's name >+ // as a data-initiator attribute >+ // e.g. >+ // const f = 'prepareConsentJS'; >+ // window[f](); >+ window.prepareConsentJS = function () { >+ const items = $('.consentJSItem'); >+ const invalid = []; >+ const valid = []; >+ items.each(function () { >+ const id = $(this).data('id').length > 0 ? >+ $(this).data('id') : >+ '_' + Math.random().toString(36).substr(2, 9); >+ const name = $(this).find('.metaName').val(); >+ const desc = $(this).find('.metaDescription').val(); >+ const matchPattern = $(this).find('.metaMatchPattern').val(); >+ const cookieDomain = $(this).find('.metaCookieDomain').val(); >+ const cookiePath = $(this).find('.metaCookiePath').val(); >+ const opacConsent = $(this).find('.opacConsent').is(':checked') >+ const staffConsent = $(this).find('.staffConsent').is(':checked'); >+ const code = $(this).find('.preference-code').val(); >+ // If the name, description, match pattern code are empty, then they've >+ // added a new entry, but not filled it in, we can skip it >+ if ( >+ name.length === 0 && >+ desc.length === 0 && >+ matchPattern.length === 0 && >+ cookieDomain.length === 0 && >+ cookiePath.length === 0 && >+ code.length === 0 >+ ) { >+ return; >+ } >+ // They've filled in at least some info >+ if ( >+ (name.length === 0) || >+ (desc.length === 0) || >+ (matchPattern.length === 0) || >+ (cookiePath.length === 0) || >+ (code.length === 0) >+ ) { >+ invalid.push(this); >+ } else { >+ const obj = { >+ id: id, >+ name: name, >+ description: desc, >+ matchPattern: matchPattern, >+ cookieDomain: cookieDomain, >+ cookiePath: cookiePath, >+ opacConsent: opacConsent, >+ staffConsent: staffConsent, >+ code: btoa(code) >+ } >+ valid.push(obj); >+ } >+ }); >+ // We failed validation >+ if (invalid.length > 0) { >+ $('#consentJSWarning').show(); >+ return false; >+ } >+ $('#consentJSWarning').hide(); >+ if (valid.length === 0) { >+ return ''; >+ } >+ const json = JSON.stringify(valid); >+ const base64 = btoa(json); >+ return base64; >+ } > >- formfieldid.val( prefs.join("|") ) >- .addClass("modified"); >- mark_modified.call( formfieldid ); >- KOHA.Preferences.Save( formfieldid.closest("form") ); >- $("#prefModal").modal("hide"); >+ $("#saveModalPrefs").on("click", function(){ >+ var formfieldid = $("#" + $(this).data("target")); >+ let finalString = ""; >+ if ($(this).data("type") == "modalselect") { >+ finalString = prepareModalSelect(formfieldid); >+ } else { >+ const processor = $(".modaljs").data("processor"); >+ finalString = window[processor](); >+ } >+ // A processor can return false if any of the submitted >+ // data has failed validation >+ if (finalString !== false) { >+ formfieldid.val(finalString).addClass("modified"); >+ mark_modified.call( formfieldid ); >+ KOHA.Preferences.Save( formfieldid.closest("form") ); >+ $("#prefModal").modal("hide"); >+ } > }); > > $("#prefModal").on("hide.bs.modal", function(){ >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >index e48225df2e..6c42a256b1 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >@@ -2874,4 +2874,61 @@ $star-selected: #EDB867; > } > } > >+/* Cookie consent bar */ >+#cookieConsentBar { >+ display: none; >+ padding: 20px; >+ position: fixed; >+ bottom: 0; >+ left: 0; >+ right: 0; >+ background: rgba(0,0,0,0.7); >+ color: rgba(255,255,255,0.9); >+ z-index: 1; >+ a:link, a:visited { >+ color: #c5ecff; >+ } >+ #consentButtons { >+ margin-top: 10px; >+ text-align: right; >+ } >+ button:not(:last-child) { >+ margin-right: 10px; >+ } >+} >+ >+/* Cookie consent modal */ >+#cookieConsentModal { >+ .modal-dialog { >+ max-width: 1000px; >+ } >+ .consentModalItem { >+ display: flex; >+ align-items: center; >+ border: 1px solid #ccc; >+ padding: 10px 20px; >+ border-radius: 5px; >+ } >+ .consentModalItem:not(:last-child) { >+ margin-bottom: 20px; >+ } >+ .consentItemCheckbox { >+ padding-right: 20px; >+ } >+ .consentItemName { >+ font-weight: bold; >+ } >+ #cookieConsentPopupText { >+ margin: 20px 0; >+ } >+} >+ >+#viewCookieConsents { >+ margin-top: 10px; >+} >+ >+#patronconsents h5 { >+ margin-top: 30px; >+} >+ > @import "responsive"; >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >index 8ea412792d..cc95fd830b 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >@@ -5,6 +5,9 @@ > [% USE AuthClient %] > [% USE AdditionalContents %] > [% PROCESS 'html_helpers.inc' %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% USE JSConsents %] >+[% END %] > [% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %] > [% SET OpacHeader = AdditionalContents.get( location => "opacheader", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] > [% SET OpacCustomSearch = AdditionalContents.get( location => "OpacCustomSearch", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] >@@ -88,6 +91,15 @@ > </a> > </li> > [% END %] >+ <!-- Cookie consent button for non logged-in users --> >+ [% IF !loggedinusername %] >+ <li style="display:none" id="cookieConsentDivider" class="divider-vertical" aria-hidden="true"></li> >+ <li style="display:none" id="cookieConsentLi" class="nav-item" aria-hidden="true"> >+ <a id="cookieConsentButton" href="#" name="Your cookies" class="nav-link" title="View and amend cookies you have consented to" role="button"> >+ <span class="cookieconsentlabel">Your cookies</span> >+ </a> >+ </li> >+ [% END %] > </ul> <!-- / .navbar-nav --> > > [% IF Koha.Preference( 'opacuserlogin' ) == 1 || Koha.Preference( 'EnableOpacSearchHistory') || Koha.Preference( 'opaclanguagesdisplay' ) %] >@@ -440,3 +452,56 @@ > </div> <!-- /.modal-content --> > </div> <!-- /.modal-dialog --> > </div> <!-- /#modalAuth --> >+ [% IF Koha.Preference( 'CookieConsent' ) %] >+ <!-- Cookie consent bar --> >+ <div id="cookieConsentBar" aria-hidden="true"> >+ [% Koha.Preference( 'CookieConsentBar' ) | $raw %] >+ <div id="consentButtons"> >+ <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button> >+ [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %] >+ <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button> >+ <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button> >+ [% END %] >+ </div> >+ </div> <!-- /#cookieConsentBar --> >+ <!-- Cookie consent modal --> >+ <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true"> >+ <div class="modal-dialog"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2> >+ </div> >+ <div class="modal-body"> >+ [% IF Koha.Preference( 'CookieConsentPopup' ) %] >+ <div id="cookieConsentPopupText"> >+ [% Koha.Preference( 'CookieConsentPopup' ) | $raw %] >+ </div> >+ [% END %] >+ <div id="consentCookieList"> >+ [% consents = JSConsents.all() %] >+ [% FOREACH consent IN consents %] >+ <div class="consentModalItem"> >+ <div class="consentItemCheckbox"> >+ <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]"> >+ </div> >+ <div class="consentItemMeta"> >+ <div class="consentItemName">[% consent.name | html %]</div> >+ <div class="consentItemDescription">[% consent.description | html %]</div> >+ </div> >+ </div> >+ [% END %] >+ </div> >+ </div> >+ <div class="modal-footer"> >+ <div id="consentButtons"> >+ <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button> >+ [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %] >+ <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button> >+ <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button> >+ [% END %] >+ </div> >+ </div> >+ </div> <!-- /.modal-content --> >+ </div> <!-- /.modal-dialog --> >+ </div> <!-- /#cookieConsentModal --> >+ [% END %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >index 7e4442477d..27de6f8ded 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >@@ -5,6 +5,9 @@ > [% USE Asset %] > [% SET opaccredits = AdditionalContents.get( location => "opaccredits", lang => lang, library => logged_in_user.branchcode || default_branch ) %] > [% PROCESS 'html_helpers.inc' %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% USE JSConsents %] >+[% END %] > [% UNLESS ( is_popup ) %] > [% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %] > [% IF ( opaccredits ) %] >@@ -121,6 +124,14 @@ > [% INCLUDE 'modals/checkout.inc' %] > [% END %] > >+<!-- ConsentJS code that may run --> >+[% IF Koha.Preference( 'CookieConsent' ) && Koha.Preference( 'ConsentJS' ).length > 0 %] >+ [% consents = JSConsents.all() %] >+ [% FOREACH consent IN consents %] >+ <div class="consentCode" style="display:none" aria-hidden="true" data-consent-id="[% consent.id | html %]" data-consent-code="[% consent.code | html %]" data-consent-match-pattern="[% consent.matchPattern | html %]" data-consent-cookie-domain="[% consent.cookieDomain | html %]" data-consent-cookie-path="[% consent.cookiePath | html %]" data-requires-consent="[% consent.opacConsent ? 'true' : 'false' | html %]"></div> >+ [% END %] >+[% END %] >+ > <!-- JavaScript includes --> > [% Asset.js("lib/jquery/jquery-3.6.0.min.js") | $raw %] > [% Asset.js("lib/jquery/jquery-migrate-3.3.2.min.js") | $raw %] >@@ -309,6 +320,9 @@ $(document).ready(function() { > [% IF Koha.Preference( 'OpacTrustedCheckout' ) %] > [% Asset.js("js/modals/checkout.js") | $raw %] > [% END %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% Asset.js("js/cookieconsent.js") | $raw %] >+[% END %] > [% KohaPlugins.get_plugins_opac_js | $raw %] > </body> > </html> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >index 5f8b48cda8..e53c087de0 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >@@ -24,7 +24,7 @@ > [% END %] > <a href="/cgi-bin/koha/opac-memberentry.pl">Personal details</a></li> > >- [% IF Koha.Preference('PrivacyPolicyConsent') # remove when extending %] >+ [% IF Koha.Preference('PrivacyPolicyConsent') || Koha.Preference('CookieConsent') # remove when extending %] > [% IF consentview %]<li class="active">[% ELSE %]<li>[% END %] > <a href="/cgi-bin/koha/opac-patron-consent.pl">Consents</a> > </li> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >index d6fe1dcafe..896e9df4d2 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >@@ -70,6 +70,11 @@ > > </form> > >+ [% IF Koha.Preference('CookieConsent') %] >+ <h5>Cookie consents</h5> >+ <button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button> >+ [% END %] >+ > </div> <!-- / #userpasswd --> > </div> <!-- / .col-lg-10 --> > </div> <!-- / .row --> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/cookieconsent.js b/koha-tmpl/opac-tmpl/bootstrap/js/cookieconsent.js >new file mode 100644 >index 0000000000..ec4372216c >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/js/cookieconsent.js >@@ -0,0 +1,199 @@ >+(function () { >+ // Has the user previously consented, establish our >+ // initial state >+ let selected = []; >+ let existingConsent = ''; >+ // The presence of a 'cookieConsent' local storage item indicates >+ // that previous consent has been set. If the value is empty, then >+ // no consent to non-essential cookies was given >+ const hasStoredConsent = localStorage.getItem('cookieConsent'); >+ getExistingConsent(); >+ >+ // The consent bar may not be in the DOM if it is not being used >+ const consentBar = $('#cookieConsentBar'); >+ if (!consentBar) { >+ return; >+ } >+ if (hasStoredConsent === null) { >+ showConsentBar(); >+ } else { >+ showYourCookies(); >+ } >+ addButtonHandlers(); >+ >+ // When the modal is opened, populate our state based on currently >+ // selected values >+ $('#cookieConsentModal').on('shown.bs.modal', function () { >+ initialiseSelected(); >+ }); >+ >+ // Initialise existing consent based on local storage >+ function getExistingConsent() { >+ existingConsent = localStorage.getItem('cookieConsent') ? >+ localStorage.getItem('cookieConsent') : >+ []; >+ } >+ >+ function showConsentBar() { >+ const consentBar = $('#cookieConsentBar'); >+ consentBar.attr('aria-hidden', 'false'); >+ consentBar.show(); >+ } >+ >+ function hideConsentBar() { >+ const consentBar = $('#cookieConsentBar'); >+ consentBar.attr('aria-hidden', 'true'); >+ consentBar.hide(); >+ } >+ >+ // Hides the appropriate consent container, depending on what >+ // is currently visible >+ function hideContainer() { >+ if ($('#cookieConsentModal').hasClass('show')) { >+ $('#cookieConsentModal').modal('hide'); >+ } else { >+ hideConsentBar(); >+ } >+ } >+ >+ // Show the unauthenticated user's "Your cookies" button >+ function showYourCookies() { >+ // If the user is unauthenticated, there will be a >+ // cookieConsentsButton element in the DOM. The user >+ // has previously consented, so we need to display this >+ // button >+ if ($('#cookieConsentButton').length > 0) { >+ $('#cookieConsentDivider').show().attr('aria-hidden', 'false'); >+ $('#cookieConsentLi').show().attr('aria-hidden', 'false'); >+ } >+ } >+ >+ // Initialise our state of selected item and enable/disable >+ // the "Accept selected" button appropriately >+ function initialiseSelected() { >+ selected = []; >+ $('.consentCheckbox').each(function () { >+ const val = $(this).val(); >+ if (existingConsent.indexOf(val) > -1 ) { >+ $(this).prop('checked', true); >+ selected.push(val); >+ } else { >+ $(this).prop('checked', false); >+ } >+ }); >+ enableDisableSelectedButton(); >+ } >+ >+ // Maintain our state of selected items and enable/disable >+ // the "Accept selected" button appropriately >+ function maintainSelected() { >+ selected = []; >+ $('.consentCheckbox:checked').each(function () { >+ selected.push($(this).val()); >+ }); >+ enableDisableSelectedButton(); >+ } >+ >+ // Set the enabled / disabled state of the >+ // "Accept selected button based on the checkbox >+ // states >+ function enableDisableSelectedButton() { >+ $('#consentAcceptSelected').prop( >+ 'disabled', >+ selected.length === 0 >+ ); >+ } >+ >+ function runConsentedCode() { >+ getExistingConsent(); >+ $('.consentCode').each(function () { >+ // The user has explicitly consented to this code, or the >+ // code doesn't require consent in the OPAC >+ if ( >+ existingConsent.indexOf($(this).data('consent-id')) > -1 || >+ !$(this).data('requires-consent') >+ ) { >+ const code = atob($(this).data('consent-code')); >+ const func = Function(code); >+ func(); >+ } else { >+ // This code doesn't have consent to run, we may need to remove >+ // any cookies it has previously set >+ const matchPattern = $(this).data('consent-match-pattern'); >+ const cookieDomain = $(this).data('consent-cookie-domain'); >+ const cookiePath = $(this).data('consent-cookie-path'); >+ if (matchPattern.length > 0) { >+ const regex = new RegExp(matchPattern); >+ const allCookies = document.cookie.split('; '); >+ allCookies.forEach(function (cookie) { >+ const name = cookie.split('=')[0]; >+ if (regex.test(name)) { >+ document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=' + cookieDomain +'; path=' + cookiePath; >+ } >+ }); >+ } >+ } >+ }); >+ } >+ >+ function addButtonHandlers() { >+ // "Accept all" handler >+ $('.consentAcceptAll').on('click', function(e) { >+ e.preventDefault(); >+ let toSave = []; >+ $('.consentCheckbox').each(function () { >+ const val = $(this).val(); >+ toSave.push(val); >+ }); >+ localStorage.setItem('cookieConsent', toSave); >+ hideContainer(); >+ runConsentedCode(); >+ showYourCookies(); >+ }); >+ >+ // "Accept essential" handler >+ $('.consentAcceptEssential').on('click', function(e) { >+ e.preventDefault(); >+ localStorage.setItem('cookieConsent', []); >+ hideContainer(); >+ runConsentedCode(); >+ showYourCookies(); >+ }); >+ >+ // "Accept selected" handler >+ $('#consentAcceptSelected').on('click', function(e) { >+ e.preventDefault(); >+ const toSave = selected.length > 0 ? selected : []; >+ localStorage.setItem('cookieConsent', toSave); >+ hideContainer(); >+ runConsentedCode(); >+ showYourCookies(); >+ }); >+ >+ // "More information" handler >+ $('#consentMoreInfo, #cookieConsentButton, #viewCookieConsents').on( >+ 'click', >+ function (e) { >+ e.preventDefault(); >+ hideConsentBar(); >+ // Ensure we're up to date with the existing consent >+ getExistingConsent(); >+ // Prevent the modal from being closed with anything >+ // but the buttons >+ $('#cookieConsentModal') >+ .modal({ >+ backdrop: 'static', >+ keyboard: false, >+ focus: true, >+ show: true >+ }); >+ } >+ ); >+ $('.consentCheckbox').on('click', function () { >+ maintainSelected(); >+ }); >+ } >+ >+ // On page load, run any code that has been given consent >+ runConsentedCode(); >+})(); >diff --git a/t/db_dependent/Koha/Template/Plugin/JSConsents.t b/t/db_dependent/Koha/Template/Plugin/JSConsents.t >new file mode 100755 >index 0000000000..4210f5f4b1 >--- /dev/null >+++ b/t/db_dependent/Koha/Template/Plugin/JSConsents.t >@@ -0,0 +1,25 @@ >+#!/usr/bin/perl >+ >+use Modern::Perl; >+ >+use C4::Context; >+ >+use Test::MockModule; >+use Test::More tests => 3; >+use t::lib::Mocks; >+ >+BEGIN { >+ use_ok('Koha::Template::Plugin::JSConsents', "Can use Koha::Template::Plugin::JSConsents"); >+} >+ >+ok( my $consents = Koha::Template::Plugin::JSConsents->new(), 'Able to instantiate template plugin' ); >+ >+subtest "all" => sub { >+ plan tests => 1; >+ >+ t::lib::Mocks::mock_preference( 'ConsentJS', 'eyAidGVzdCI6ICJvbmUiIH0=' ); >+ >+ is_deeply( $consents->all(), { test => 'one' }, 'Returns a Base64 decoded JSON object converted into a data structure'); >+}; >+ >+1; >-- >2.30.2
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 27378
:
115544
|
115545
|
115546
|
115547
|
115548
|
115756
|
116397
|
116398
|
116399
|
116400
|
116401
|
116402
|
116403
|
116416
|
116417
|
116418
|
116419
|
116420
|
116421
|
116422
|
116423
|
116978
|
116979
|
116980
|
116981
|
116982
|
116983
|
116984
|
116985
|
116986
|
121728
|
121729
|
121730
|
121731
|
121732
|
121733
|
121734
|
121735
|
121995
|
121996
|
121997
|
121998
|
121999
|
122000
|
122001
|
122002
|
122635
|
122636
|
122637
|
122638
|
122639
|
122640
|
122641
|
122642
|
124957
|
124958
|
124959
|
124960
|
124961
|
124962
|
124963
|
124964
|
124965
|
130072
|
130073
|
130074
|
130075
|
130076
|
130077
|
130078
|
130079
|
130080
|
135309
|
135310
|
135311
|
135312
|
135313
|
135314
|
135315
|
135316
|
135317
|
135318
|
151735
|
151736
|
151737
|
151738
|
151739
|
151740
|
151741
|
151742
|
151743
|
151744
|
151745
|
151746
|
151747
|
151748
|
151749
|
151750
|
151751
|
151752
|
151753
|
151826
|
151827
|
151828
|
151829
|
151830
|
151831
|
151832
|
151833
|
151834
|
151835
|
151836
|
151837
|
151838
|
151839
|
151840
|
151841
|
151842
|
151843
|
151844
|
153326
|
153327
|
153328
|
153329
|
153330
|
153331
|
153332
|
153333
|
153334
|
153335
|
153336
|
154015
|
154016
|
154017
|
154018
|
154019
|
154020
|
154021
|
154022
|
154023
|
154024
|
154025
|
154093
| 154264 |
154265
|
154266
|
154267
|
154268
|
154269
|
154270
|
154271
|
154272
|
154273
|
154274
|
154275
|
154276