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:
parent
272a080cb3
commit
49bfb9ff39
|
|
@ -63,6 +63,8 @@ location is one of this:
|
|||
- OpacLoginInstructions
|
||||
- OpacSuggestionInstructions
|
||||
- ArticleRequestsDisclaimerText
|
||||
- CookieConsentBar
|
||||
- CookieConsentPopup
|
||||
|
||||
=cut
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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' ) {
|
||||
|
|
|
|||
|
|
@ -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'";
|
||||
},
|
||||
};
|
||||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 %]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})();
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue