Bugzilla – Attachment 121731 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: Add cookie consent display to OPAC
Bug-27378-Add-cookie-consent-display-to-OPAC.patch (text/plain), 19.59 KB, created by
Andrew Isherwood
on 2021-06-09 11:10:08 UTC
(
hide
)
Description:
Bug 27378: Add cookie consent display to OPAC
Filename:
MIME Type:
Creator:
Andrew Isherwood
Created:
2021-06-09 11:10:08 UTC
Size:
19.59 KB
patch
obsolete
>From 6089afbfdcc67daaae9de37e7d9b7cf40efa4e39 Mon Sep 17 00:00:00 2001 >From: Andrew Isherwood <andrew.isherwood@ptfs-europe.com> >Date: Mon, 18 Jan 2021 12:13:12 +0000 >Subject: [PATCH] Bug 27378: Add cookie consent display to OPAC > >This commit adds the display of the cookie consent bar and modal to the >OPAC. > >- Adds a new JSConsents template plugin that enables a template to be >populated with the contents of the ConsentJS syspref in a prepared, >usable form >- Adds a new cookieconsent.js script that drives the display and >functionality of the cookie consent bar and modal >- Adds styles for the cookie consent bar and modal >- Adds the creation and population of the cookie consent bar and modal, >if appropriate >- Adds the ability for both authenticated and unauthenticated users to >view and modify their consents either via "your consents" or a new >unobtrusive "Your cookies" button in the bottom right of the screen. >- Adds unit test for JSConsents.pm >--- > Koha/Template/Plugin/JSConsents.pm | 64 ++++++ > .../opac-tmpl/bootstrap/css/src/opac.scss | 68 +++++++ > .../bootstrap/en/includes/masthead.inc | 62 ++++++ > .../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 | 183 ++++++++++++++++++ > .../Koha/Template/Plugin/JSConsents.t | 25 +++ > 8 files changed, 422 insertions(+), 1 deletion(-) > create mode 100644 Koha/Template/Plugin/JSConsents.pm > 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/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >index 391a10f296..f17bbf0356 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >@@ -2626,4 +2626,72 @@ $star-selected: #EDB867; > margin-bottom: 1rem; > } > >+/* 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; >+ } >+} >+ >+#cookieConsentButton { >+ position: fixed; >+ border: 0; >+ bottom: 0; >+ right: 0; >+ padding: 10px; >+ background: rgba(0,0,0,0.75); >+ color: rgba(255,255,255,0.8); >+ z-index: 1; >+} >+ >+#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 28c8ff23d8..1c7ed2f557 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >@@ -2,6 +2,9 @@ > [% USE Koha %] > [% USE Branches %] > [% USE Categories %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% USE JSConsents %] >+[% END %] > [% USE KohaNews %] > [% PROCESS 'html_helpers.inc' %] > [% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %] >@@ -416,3 +419,62 @@ > </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 button for non logged-in users --> >+ [% IF !loggedinusername %] >+ <button id="cookieConsentButton" style="display:none" aria-hidden="true"> >+ Your cookies >+ </button> >+ [% END %] >+ <!-- 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 %] >\ No newline at end of file >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 c103f0ec4c..5f5d220f28 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >@@ -3,6 +3,9 @@ > [% USE KohaNews %] > [%- USE KohaPlugins -%] > [% USE Asset %] >+[% IF Koha.Preference( 'CookieConsent' ) %] >+ [% USE JSConsents %] >+[% END %] > [% SET opaccredits = KohaNews.get( location => "opaccredits", lang => lang, library => branchcode ) %] > [% PROCESS 'html_helpers.inc' %] > [% UNLESS ( is_popup ) %] >@@ -117,6 +120,14 @@ > [% END # /IF OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer' %] > [% END # / UNLESS is_popup %] > >+<!-- 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 %]" data-consent-code="[% consent.code %]" data-requires-consent="[% consent.opacConsent ? 'true' : 'false' %]"></div> >+ [% END %] >+[% END %] >+ > <!-- JavaScript includes --> > [% Asset.js("lib/jquery/jquery-3.4.1.min.js") | $raw %] > [% Asset.js("lib/jquery/jquery-migrate-3.1.0.min.js") | $raw %] >@@ -333,6 +344,9 @@ $(document).ready(function() { > </script> > [% END %] > [% 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 b426065ed6..b862acabe3 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">your personal details</a></li> > >- [% IF Koha.Preference('GDPR_Policy') # remove when extending %] >+ [% IF Koha.Preference('GDPR_Policy') || Koha.Preference('CookieConsent') # remove when extending %] > [% IF consentview %]<li class="active">[% ELSE %]<li>[% END %] > <a href="/cgi-bin/koha/opac-patron-consent.pl">your 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 498390545e..2b14f0b9e1 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 >@@ -66,6 +66,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..ae10eeac86 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/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(); >+ } 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 >+ const consentButton = $('#cookieConsentButton'); >+ if (consentButton.length > 0) { >+ consentButton.attr('aria-hidden', 'false'); >+ consentButton.show(); >+ } >+ } >+ >+ // 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(); >+ } >+ }); >+ } >+ >+ 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(); >+ 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.20.1
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