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>

Bug 27378: (follow-up) Add missing filters

(cherry picked from commit 6b8565b949b62269f6d850e6d412458d0dbcfb37)
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: Fix accessibility issues

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: Update to new atomicupdate structure

This patch consolidates the previous 4 database update files into one atomicupdate file in line with the new structure

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: Change ConsentJS to CookieConsentedJS

This patch updates the name of the ConsentJS syspref to CookieConsentedJS and amends the description to be more clear what the syspref is for

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: Stop the codemirror editor and delete confirmation from duplicating

Previously, if the "Add new code button" was clicked in the CookieConsentedJS editor, the original entry would have duplicated CodeMirror editors.
This was exponential, i.e adding two new lines would result in three codemirror editors appearing on the first entry, two on the second and so on.
The click event was not being applied properly and was being applied to every element with the .expand-textarea class, rather than specifically the new elements being created. The addExpandHandler function now loops through each element individually and decides whether to apply the click event handler.

Similarly, the delete confirmation was dupliacting for the same reason. This has also been resolved.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: Remove two sysprefs and replace with html customisations

Currently there are two sysprefs - CookieConsentBar and CookieConsentPopup. These allow the user to select what text they would like to see in the consent bar and modal. These have been removed and replaced with HTML customisations to allow more flexible customisations and different languages.

Sponsored by: PTFS-Europe

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: (QA follow-up) Small fixes and tidy-ups

This patch does the following:
- Realphabetizes the lines in sysprefs.sql
- Fixes a formatting error in patrons.pref
- Adjusts the position of the cookie consent bar if the language selector is visible
- Fixes translatability on the syspref modal

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: (QA follow-up) Allow staff to view their cookie consents

This patch allows staff to view their cookie consents through a link in the dropdown menu in the navbar. Previously staff had no way of accessing their cookie consents

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: (QA follow-up) Add cancel button to cookie modal

This patch adds a cancel button to the modal for reviewing cookie consents. Previously there was no way to exit without selecting one of the cookie options

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: (QA follow-up) Add filtering for OPAC only and staff only cookies

This patch fixes an issue where cookies selected as OPAC only would still show in the staff client and vise versa. The cookies are now filtered and only the correct cookies will be used in the OPAC and staff client

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Bug 27378: (QA follow-up) Fix tests and character encoding

This patch fixes an encoding issue when using diacritics. It also fixes a failing test, corrects the format of the "Cancel" links in the modal and perltidy has been used on all relevant files

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Matt Blenkinsop 2021-01-11 14:50:17 +00:00 committed by Tomas Cohen Arazi
parent 272a080cb3
commit 49bfb9ff39
No known key found for this signature in database
GPG Key ID: 0A272EA1B2F3C15F
21 changed files with 1236 additions and 55 deletions

View File

@ -63,6 +63,8 @@ location is one of this:
- OpacLoginInstructions
- OpacSuggestionInstructions
- ArticleRequestsDisclaimerText
- CookieConsentBar
- CookieConsentPopup
=cut

View File

@ -0,0 +1,69 @@
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 Encode qw{ encode_utf8 };
use C4::Context;
sub all {
my ( $self, $filter ) = @_;
my $consents = C4::Context->preference('CookieConsentedJS');
if ( length $consents > 0 ) {
my $decoded_consents = decode_base64($consents);
my $convert_to_utf8 = encode_utf8($decoded_consents);
my $consents_array = decode_json $convert_to_utf8;
my @filtered_consents = grep { $_->{$filter} } @{$consents_array};
return \@filtered_consents;
} else {
return [];
}
}
1;
=head1 NAME
Koha::Template::Plugin::JSConsents - TT Plugin for Javascript consents
Provided by CookieConsentedJS 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

View File

@ -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' ) {

View File

@ -0,0 +1,20 @@
use Modern::Perl;
return {
bug_number => "27378",
description => "Adds the sysprefs for cookie consents",
up => sub {
my ($args) = @_;
my ( $dbh, $out ) = @$args{qw(dbh out)};
$dbh->do(
q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsentedJS', '', 'Add Javascript code that will run if cookie consent is provided (e.g. tracking code).', '', 'Free'); |
);
say $out "Added new system preference 'CookieConsentedJS'";
$dbh->do(
q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsent', '0', 'Require cookie consent to be displayed', '', 'YesNo'); |
);
say $out "Added new system preference 'CookieConsent'";
},
};

View File

@ -158,6 +158,8 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('ConfirmFutureHolds','0','','Number of days for confirming future holds','Integer'),
('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'),
('CookieConsentedJS', '', NULL, 'Add Javascript code that will run if cookie consent is provided (e.g. tracking code).', 'Free'),
('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'),

View File

@ -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;
}

View File

@ -2952,6 +2952,17 @@ button,
}
}
&.btn-info {
color: #fff;
background-color: #194EA3;
border-color: #0B439D;
}
&.btn-success {
background-color: #419641;
border-color: #398339;
}
&.btn-danger {
background-color: #CC3333;
border-color: #9B2323;
@ -4302,6 +4313,86 @@ div .suggestion_note {
}
}
#cookieConsentedJSItems {
#cookieConsentedJSWarning {
margin: 20px 0;
text-align: center;
font-weight: 800;
font-size: 120%;
}
.cookieConsentedJSItem {
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";

View File

@ -3,6 +3,9 @@
[% USE Desks %]
[% USE Registers %]
[% USE Koha %]
[% IF Koha.Preference( 'CookieConsent' ) %]
[% USE JSConsents %]
[% END %]
<nav class="navbar">
<div class="navbar-header">
<a href="#" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#header" aria-expanded="false" aria-controls="header">
@ -200,6 +203,11 @@
<a class="toplinks" href="/cgi-bin/koha/circ/circulation.pl?borrowernumber=[% loggedinusernumber | html %]">My checkouts</a>
</li>
[% END %]
[% IF Koha.Preference( 'CookieConsent' ) && JSConsents.all('staffConsent').size %]
<li class="toplinks-myconsents">
<a id="viewCookieConsents" href="#" class="toplinks">My consents</a>
</li>
[% END %]
<li>
<a id="logout" class="toplinks" href="/cgi-bin/koha/mainpage.pl?logout.x=1">Log out</a>
</li>

View File

@ -1,6 +1,13 @@
[% USE raw %]
[% USE Koha %]
[% USE KohaPlugins %]
[% USE Asset %]
[% USE AdditionalContents %]
[% IF Koha.Preference( 'CookieConsent' ) %]
[% SET CookieConsentBar = AdditionalContents.get( location => "CookieConsentBar", lang => lang, library => branchcode || default_branch ) %]
[% SET CookieConsentPopup = AdditionalContents.get( location => "CookieConsentPopup", lang => lang, library => branchcode || default_branch ) %]
[% USE JSConsents %]
[% END %]
</div>
[% IF ( ( languages_loop ) && ( ! popup_window ) && ( Koha.Preference('StaffLangSelectorMode') == 'both' || Koha.Preference('StaffLangSelectorMode') == 'footer') ) %]
[% UNLESS ( one_language_enabled ) %]
@ -74,5 +81,79 @@
[% END %]
[% KohaPlugins.get_plugins_intranet_js | $raw %]
<!-- Cookie consent -->
[% IF Koha.Preference( 'CookieConsent' ) %]
<!-- Cookie consent bar -->
<div id="cookieConsentBar" aria-hidden="true">
[% IF ( CookieConsentBar && CookieConsentBar.content && CookieConsentBar.content.count >0 ) %]
<div id="cookieConsentBarText">
[%- FOREACH item IN CookieConsentBar.content -%]
[%- item.content | $raw -%]
[%- END -%]
</div>
[% END %]
<div id="consentButtons">
<button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
[% IF ( JSConsents.all('staffConsent').size ) %]
<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 ( CookieConsentPopup && CookieConsentPopup.content && CookieConsentPopup.content.count >0 ) %]
<div id="cookieConsentPopupText">
[%- FOREACH item IN CookieConsentPopup.content -%]
[%- item.content | $raw -%]
[%- END -%]
</div>
[% END %]
<div id="consentCookieList">
[% consents = JSConsents.all('staffConsent') %]
[% FOREACH consent IN consents %]
<div class="consentModalItem">
<div class="consentItemCheckbox">
<input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id | html %]" aria-label="Consent to cookie">
</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 ( JSConsents.all('staffConsent').size ) %]
<button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
<button type="button" class="btn btn-success" id="consentAcceptSelected">Accept selected non-essential cookies</button>
[% END %]
<a type="button" href="#" class="btn btn-secondary consentCloseModal">Cancel</a>
</div>
</div>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /#cookieConsentModal -->
[% END %]
<!-- CookieConsentedJS code that may run -->
[% IF Koha.Preference( 'CookieConsent' ) && JSConsents.all('staffConsent').size %]
[% consents = JSConsents.all('staffConsent') %]
[% 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>

View File

@ -135,6 +135,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 | html %]" 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 %]

View File

@ -388,6 +388,18 @@ 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:
1: Require
0: "Don't require"
- cookie consent to be displayed in the OPAC and staff interface.
-
- Run the following JavaScript if cookie consent is provided (e.g. tracking code).
- pref: CookieConsentedJS
type: modaljs
initiator: populateCookieConsentedJS
processor: prepareCookieConsentedJS
Security:
-
- Login passwords for staff and patrons must be at least

View File

@ -494,7 +494,7 @@
[% END %]
[% END %]
[% ELSE %]
[% SET available_options = [ 'OpacNavRight', 'opacheader', 'OpacCustomSearch', 'OpacMainUserBlock', 'opaccredits', 'OpacLoginInstructions', 'OpacNav', 'OpacNavBottom', 'OpacSuggestionInstructions', 'ArticleRequestsDisclaimerText', 'OpacMoreSearches', 'OpacMySummaryNote', 'OpacLibraryInfo', 'CatalogConcernHelp', 'CatalogConcernTemplate' ] %]
[% SET available_options = [ 'OpacNavRight', 'opacheader', 'OpacCustomSearch', 'OpacMainUserBlock', 'opaccredits', 'OpacLoginInstructions', 'OpacNav', 'OpacNavBottom', 'OpacSuggestionInstructions', 'ArticleRequestsDisclaimerText', 'OpacMoreSearches', 'OpacMySummaryNote', 'OpacLibraryInfo', 'CatalogConcernHelp', 'CatalogConcernTemplate', 'CookieConsentBar', 'CookieConsentPopup' ] %]
[% FOREACH l IN available_options.sort %]
[% IF l == location %]
<option value="[% l | html %]" selected="selected">[% l | html %]</option>

View File

@ -0,0 +1,195 @@
(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');
const langmenu = $('#changelanguage');
if(langmenu) {
const height = langmenu.height();
consentBar.css('bottom', height);
};
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();
});
// "Cancel" handler
$('.consentCloseModal').on('click', function(e) {
e.preventDefault();
hideContainer();
showConsentBar();
});
// "More information" handler
$('#consentMoreInfo, #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();
})();

View File

@ -64,6 +64,96 @@ window.onbeforeunload = function () {
}
};
// Add event handlers to any elements with .expand-textarea classes
// this allows the CodeMirror widget to be displayed
function addExpandHandler() {
const textAreas = $('.expand-textarea').map(function() {
return this
}).get();
textAreas.forEach((expandTextArea) => {
// Don't duplicate click event handlers
const ev = $._data($(expandTextArea).get(0), 'events');
if (ev && ev.click) {
return;
}
// Add the click event
$(expandTextArea).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() {
const textAreas = $('.collapse-textarea').map(function() {
return this
}).get();
textAreas.forEach((collapseTextArea) => {
// Don't duplicate click event handlers
const ev = $._data($(collapseTextArea).get(0), 'events');
if (ev && ev.click) {
return;
}
$(collapseTextArea).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() {
const deleteButtons = $('.consentDelete').map(function() {
return this
}).get();
deleteButtons.forEach((deleteButton) => {
// Don't duplicate click event handlers
const ev = $._data($(deleteButton).get(0), 'events');
if (ev && ev.click) {
return;
}
$(deleteButton).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 +197,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 +317,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("|");
}
formfieldid.val( prefs.join("|") )
.addClass("modified");
mark_modified.call( formfieldid );
KOHA.Preferences.Save( formfieldid.closest("form") );
$("#prefModal").modal("hide");
// 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 cookieConsentedJS item, correctly populated
function createCookieConsentedJSItem(item, idx) {
const id = 'CookieConsentedJS_' + 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="cookieConsentedJSItem" 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 cookieConsentedJS items concatenated
function populateConsentMarkup(items) {
return items.reduce(function (acc, current, idx) {
return acc + createCookieConsentedJSItem(current, idx);
}, '');
}
// Return the markup for a validation warning
function populateValidationWarning() {
return '<div id="cookieConsentedJSWarning" 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: '',
opacConsent: false,
staffConsent: false
};
}
// Add the handler for a new empty consent item
function addNewHandler() {
$("#cookieConsentedJSAddNew").on("click", function (e) {
e.preventDefault();
const currentLen = $('.cookieConsentedJSItem').length;
const newItem = emptyConsentItem();
const markup = createCookieConsentedJSItem(newItem, currentLen);
$('#prefModal .modal-body #cookieConsentedJSItems').append($(markup));
addExpandHandler();
addCollapseHandler();
addConsentDeleteHandler();
});
}
// Populate the cookieConsentedJS 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 = 'populateCookieConsentedJS';
// window[f]();
window.populateCookieConsentedJS = 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 CookieConsentedJS syspref: ' +
err.message
));
}
try {
items = JSON.parse(decoded);
} catch (err) {
throw (__(
'Unable to JSON parse decoded value stored in CookieConsentedJS 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="cookieConsentedJSItems">' + validationWarning + markup + '</div><div><a href="#" id="cookieConsentedJSAddNew">' + __('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 = 'prepareCookieConsentedJS';
// window[f]();
window.prepareCookieConsentedJS = function () {
const items = $('.cookieConsentedJSItem');
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) {
$('#cookieConsentedJSWarning').show();
return false;
}
$('#cookieConsentedJSWarning').hide();
if (valid.length === 0) {
return '';
}
const json = JSON.stringify(valid);
const base64 = btoa(json);
return base64;
}
$("#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(){

View File

@ -2870,4 +2870,62 @@ $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 h2 {
margin-top: 30px;
font-size: 1.1rem;
}
@import "responsive";

View File

@ -5,11 +5,16 @@
[% 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 ) %]
[% SET OpacLoginInstructions = AdditionalContents.get( location => "OpacLoginInstructions", lang => lang, library => branchcode || default_branch ) %]
[% SET OpacMoreSearches = AdditionalContents.get( location => "OpacMoreSearches", lang => lang, library => branchcode || default_branch ) %]
[% SET CookieConsentBar = AdditionalContents.get( location => "CookieConsentBar", lang => lang, library => branchcode || default_branch ) %]
[% SET CookieConsentPopup = AdditionalContents.get( location => "CookieConsentPopup", lang => lang, library => branchcode || default_branch ) %]
<button id="scrolltocontent">Skip to main content</button>
<div id="wrapper">
@ -88,6 +93,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 +454,59 @@
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /#modalAuth -->
[% IF Koha.Preference( 'CookieConsent' ) %]
<!-- Cookie consent bar -->
<div id="cookieConsentBar" aria-hidden="true">
[% IF ( CookieConsentBar ) %]
[% PROCESS koha_news_block news => CookieConsentBar %]
[% END %]
<div id="consentButtons">
<button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
[% IF ( JSConsents.all('opacConsent').size ) %]
<button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
<button type="button" class="btn btn-info" id="consentMoreInfo" aria-label="Click to view more information about your cookie consents">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 ( CookieConsentPopup ) %]
<div id="cookieConsentPopupText">
[% PROCESS koha_news_block news => CookieConsentPopup %]
</div>
[% END %]
<div id="consentCookieList">
[% consents = JSConsents.all('opacConsent') %]
[% FOREACH consent IN consents %]
<div class="consentModalItem">
<div class="consentItemCheckbox">
<input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id | html %]" aria-label="Consent to cookie">
</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 ( JSConsents.all('opacConsent').size ) %]
<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 %]
<a type="button" href="#" class="btn btn-secondary consentCloseModal">Cancel</a>
</div>
</div>
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /#cookieConsentModal -->
[% END %]

View File

@ -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 %]
<!-- CookieConsentedJS code that may run -->
[% IF Koha.Preference( 'CookieConsent' ) && JSConsents.all('opacConsent').size %]
[% consents = JSConsents.all('opacConsent') %]
[% 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>

View File

@ -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>

View File

@ -70,6 +70,11 @@
</form>
[% IF Koha.Preference('CookieConsent') %]
<h2>Cookie consents</h2>
<button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button>
[% END %]
</div> <!-- / #userpasswd -->
</div> <!-- / .col-lg-10 -->
</div> <!-- / .row -->

View File

@ -0,0 +1,211 @@
(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');
const langmenu = $('#changelanguage');
if(langmenu) {
const height = langmenu.height();
consentBar.css('bottom', height);
};
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();
});
// "Cancel" handler
$('.consentCloseModal').on('click', function(e) {
e.preventDefault();
hideContainer();
showConsentBar();
});
// "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();
})();

View File

@ -0,0 +1,45 @@
#!/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(
'CookieConsentedJS',
'W3siaWQiOiJfbGFrZGhjOW11IiwibmFtZSI6InRlc3QiLCJkZXNjcmlwdGlvbiI6InRlc3QiLCJtYXRjaFBhdHRlcm4iOiJ0ZXN0MSIsImNvb2tpZURvbWFpbiI6ImxvY2FsaG9zdCIsImNvb2tpZVBhdGgiOiIvIiwib3BhY0NvbnNlbnQiOnRydWUsInN0YWZmQ29uc2VudCI6dHJ1ZSwiY29kZSI6IktHWjFibU4wYVc5dUtDa2dleUFLSUNBZ0lHTnZibk52YkdVdWJHOW5LQ2RJWld4c2J5Qm1jbTl0SUhSbGMzUXhKeWs3SUFvZ0lDQWdaRzlqZFcxbGJuUXVZMjl2YTJsbElEMGdJblJsYzNReFBYUmxjM1JwYm1jN0lHUnZiV0ZwYmoxc2IyTmhiR2h2YzNRN0lIQmhkR2c5THpzZ1UyRnRaVk5wZEdVOVRtOXVaVHNnVTJWamRYSmxJanNnQ24wcEtDazcifV0='
);
is_deeply(
$consents->all('opacConsent'),
[
{
'name' => 'test',
'cookiePath' => '/',
'code' =>
'KGZ1bmN0aW9uKCkgeyAKICAgIGNvbnNvbGUubG9nKCdIZWxsbyBmcm9tIHRlc3QxJyk7IAogICAgZG9jdW1lbnQuY29va2llID0gInRlc3QxPXRlc3Rpbmc7IGRvbWFpbj1sb2NhbGhvc3Q7IHBhdGg9LzsgU2FtZVNpdGU9Tm9uZTsgU2VjdXJlIjsgCn0pKCk7',
'staffConsent' => 1,
'matchPattern' => 'test1',
'description' => 'test',
'cookieDomain' => 'localhost',
'opacConsent' => 1,
'id' => '_lakdhc9mu'
}
],
'Returns a Base64 decoded JSON object converted into a data structure'
);
};
1;