View | Details | Raw Unified | Return to bug 27378
Collapse All | Expand All

(-)a/Koha/Template/Plugin/JSConsents.pm (+64 lines)
Line 0 Link Here
1
package Koha::Template::Plugin::JSConsents;
2
3
# Copyright 2021 PTFS Europe
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Template::Plugin;
21
use base qw( Template::Plugin );
22
use MIME::Base64 qw{ decode_base64 };
23
use JSON qw{ decode_json };
24
25
use C4::Context;
26
27
sub all {
28
    my ( $self ) = @_;
29
    my $consents = C4::Context->preference( 'ConsentJS' );
30
    if (length $consents > 0) {
31
        my $decoded = decode_base64($consents);
32
        return decode_json $decoded;
33
    } else {
34
        return [];
35
    }
36
}
37
38
1;
39
40
=head1 NAME
41
42
Koha::Template::Plugin::JSConsents - TT Plugin for Javascript consents
43
Provided by ConsentJS syspref
44
45
=head1 SYNOPSIS
46
47
[% USE JSConsents %]
48
49
[% JSConsents.all() %]
50
51
=head1 ROUTINES
52
53
=head2 all
54
55
In a template, you can get the all Javascript snippets
56
that require consent using the following TT:
57
[% JSConsents.all() %]
58
The consents are returned in an array of hashes
59
60
=head1 AUTHOR
61
62
Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
63
64
=cut
(-)a/admin/preferences.pl (-6 / +11 lines)
Lines 59-70 sub _get_chunk { Link Here
59
    if( $options{'syntax'} ){
59
    if( $options{'syntax'} ){
60
        $chunk->{'syntax'} = $options{'syntax'};
60
        $chunk->{'syntax'} = $options{'syntax'};
61
    }
61
    }
62
62
    if( $options{'type'} ) {
63
    if( $options{'type'} && $options{'type'} eq 'modalselect' ){
63
        if( $options{'type'} eq 'modalselect' ){
64
        $chunk->{'source'} = $options{'source'};
64
            $chunk->{'source'} = $options{'source'};
65
        $chunk->{'exclusions'} = $options{'exclusions'} // "";
65
            $chunk->{'exclusions'} = $options{'exclusions'} // "";
66
        $chunk->{'required'} = $options{'required'} // "";
66
            $chunk->{'required'} = $options{'required'} // "";
67
        $chunk->{'type'} = 'modalselect';
67
            $chunk->{'type'} = 'modalselect';
68
        } elsif( $options{'type'} eq 'modaljs' ){
69
            $chunk->{'type'} = 'modaljs';
70
            $chunk->{'initiator'} = $options{'initiator'};
71
            $chunk->{'processor'} = $options{'processor'};
72
        }
68
    }
73
    }
69
74
70
    if ( $options{'class'} && $options{'class'} eq 'password' ) {
75
    if ( $options{'class'} && $options{'class'} eq 'password' ) {
(-)a/installer/data/mysql/atomicupdate/bug_27378_add_ConsentJS_syspref.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do( q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('ConsentJS', '', 'Specify Javascript that requires user consent to run (e.g. tracking code)', '', 'Free'); | );
4
5
    # Always end with this (adjust the bug info)
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 27378 - Add ConsentJS syspref)\n";
8
}
(-)a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentBar_syspref.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do( q{ INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsentBar', '', 'Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen', '70|10', 'Textarea'); } );
4
5
    # Always end with this (adjust the bug info)
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsentBar syspref)\n";
8
}
(-)a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsentPopup_syspref.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do( q{INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsentPopup', '', 'Show the following HTML in the cookie consent popup', '70|10', 'Textarea');} );
4
5
    # Always end with this (adjust the bug info)
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsentPopup syspref)\n";
8
}
(-)a/installer/data/mysql/atomicupdate/bug_27378_add_CookieConsent_syspref.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do( q| INSERT IGNORE INTO systempreferences (variable, value, explanation, options, type) VALUES ('CookieConsent', '0', 'Require cookie consent to be displayed', '', 'YesNo'); | );
4
5
    # Always end with this (adjust the bug info)
6
    SetVersion( $DBversion );
7
    print "Upgrade to $DBversion done (Bug 27378 - Add CookieConsent syspref)\n";
8
}
(-)a/installer/data/mysql/mandatory/sysprefs.sql (+4 lines)
Lines 156-163 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
156
('ComponentSortField','title','call_number|pubdate|acqdate|title|author','Specify the default field used for sorting','Choice'),
156
('ComponentSortField','title','call_number|pubdate|acqdate|title|author','Specify the default field used for sorting','Choice'),
157
('ComponentSortOrder','asc','asc|dsc|az|za','Specify the default sort order','Choice'),
157
('ComponentSortOrder','asc','asc|dsc|az|za','Specify the default sort order','Choice'),
158
('ConfirmFutureHolds','0','','Number of days for confirming future holds','Integer'),
158
('ConfirmFutureHolds','0','','Number of days for confirming future holds','Integer'),
159
('ConsentJS', '', NULL, 'Specify Javascript that requires user consent to run (e.g. tracking code)', 'Free'),
159
('ConsiderOnSiteCheckoutsAsNormalCheckouts','1',NULL,'Consider on-site checkouts as normal checkouts','YesNo'),
160
('ConsiderOnSiteCheckoutsAsNormalCheckouts','1',NULL,'Consider on-site checkouts as normal checkouts','YesNo'),
160
('ContentWarningField', '', NULL, 'MARC field to use for content warnings', 'Free'),
161
('ContentWarningField', '', NULL, 'MARC field to use for content warnings', 'Free'),
162
('CookieConsent', '0', NULL, 'Require cookie consent to be displayed', 'YesNo'),
163
('CookieConsentBar', '', '70|10', 'Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen', 'Textarea'),
164
('CookieConsentPopup', '', '70|10', 'Show the following HTML in the cookie consent popup', 'Textarea'),
161
('CreateAVFromCataloguing', '1', '', 'Ability to create authorized values from the cataloguing module', 'YesNo'),
165
('CreateAVFromCataloguing', '1', '', 'Ability to create authorized values from the cataloguing module', 'YesNo'),
162
('CronjobLog','0',NULL,'If ON, log information from cron jobs.','YesNo'),
166
('CronjobLog','0',NULL,'If ON, log information from cron jobs.','YesNo'),
163
('CumulativeRestrictionPeriods',0,NULL,'Cumulate the restriction periods instead of keeping the highest','YesNo'),
167
('CumulativeRestrictionPeriods',0,NULL,'Cumulate the restriction periods instead of keeping the highest','YesNo'),
(-)a/koha-tmpl/intranet-tmpl/prog/css/preferences.css (-3 / +6 lines)
Lines 3-9 Link Here
3
.preference-multi,
3
.preference-multi,
4
.preference-long,
4
.preference-long,
5
.preference-file,
5
.preference-file,
6
.preference-modalselect {
6
.preference-modalselect,
7
.preference-modaljs {
7
    width: 20em;
8
    width: 20em;
8
}
9
}
9
10
Lines 15-25 Link Here
15
    width: 5em;
16
    width: 5em;
16
}
17
}
17
18
18
input[type="text"].modalselect {
19
input[type="text"].modalselect,
20
input[type="text"].modaljs {
19
    cursor: pointer;
21
    cursor: pointer;
20
}
22
}
21
23
22
input[type="text"].modalselect:hover {
24
input[type="text"].modalselect:hover,
25
input[type="text"].modaljs:hover {
23
    background-color: #FFC;
26
    background-color: #FFC;
24
}
27
}
25
28
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+80 lines)
Lines 4295-4300 div .suggestion_note { Link Here
4295
    }
4295
    }
4296
}
4296
}
4297
4297
4298
#consentJSItems {
4299
    #consentJSWarning {
4300
        margin: 20px 0;
4301
        text-align: center;
4302
        font-weight: 800;
4303
        font-size: 120%;
4304
    }
4305
    .consentJSItem {
4306
        margin-bottom: 30px;
4307
        padding: 10px;
4308
        border: 1px solid #ccc;
4309
    }
4310
    .consentRow {
4311
        display: flex;
4312
        margin-bottom: 20px;
4313
        .consentItem {
4314
            margin-right: 40px;
4315
        }
4316
        .consentItem:last-child {
4317
            margin-right: 0;
4318
        }
4319
    }
4320
    .codeRow {
4321
        display: block;
4322
    }
4323
    .consentDelete {
4324
        display: block;
4325
        text-align: right;
4326
    }
4327
}
4328
4329
/* Cookie consent bar */
4330
#cookieConsentBar {
4331
    display: none;
4332
    padding: 20px;
4333
    position: fixed;
4334
    bottom: 0;
4335
    left: 0;
4336
    right: 0;
4337
    background: rgba(0,0,0,0.7);
4338
    color: rgba(255,255,255,0.9);
4339
    z-index: 1;
4340
    a:link, a:visited {
4341
        color: #d7ebff;
4342
    }
4343
    #consentButtons {
4344
        margin-top: 10px;
4345
        text-align: right;
4346
    }
4347
    button:not(:last-child) {
4348
        margin-right: 10px;
4349
    }
4350
}
4351
4352
/* Cookie consent modal */
4353
#cookieConsentModal {
4354
    .modal-dialog {
4355
        width: 700px;
4356
    }
4357
    .consentModalItem {
4358
        display: flex;
4359
        align-items: center;
4360
        border: 1px solid #ccc;
4361
        padding: 10px 20px;
4362
        border-radius: 5px;
4363
    }
4364
    .consentModalItem:not(:last-child) {
4365
        margin-bottom: 20px;
4366
    }
4367
    .consentItemCheckbox {
4368
        padding-right: 20px;
4369
    }
4370
    .consentItemName {
4371
        font-weight: bold;
4372
    }
4373
    #cookieConsentPopupText {
4374
        margin: 20px 0;
4375
    }
4376
}
4377
4298
@import "header";
4378
@import "header";
4299
@import "toolbar";
4379
@import "toolbar";
4300
@import "forms";
4380
@import "forms";
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/intranet-bottom.inc (+69 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% USE Koha %]
2
[% USE Koha %]
3
[% USE KohaPlugins %]
3
[% USE KohaPlugins %]
4
[% USE Asset %]
5
[% IF Koha.Preference( 'CookieConsent' ) %]
6
    [% USE JSConsents %]
7
[% END %]
4
        </div>
8
        </div>
5
[% IF ( ( languages_loop ) && ( ! popup_window ) && ( Koha.Preference('StaffLangSelectorMode') == 'both' || Koha.Preference('StaffLangSelectorMode') == 'footer') ) %]
9
[% IF ( ( languages_loop ) && ( ! popup_window ) && ( Koha.Preference('StaffLangSelectorMode') == 'both' || Koha.Preference('StaffLangSelectorMode') == 'footer') ) %]
6
    [% UNLESS ( one_language_enabled ) %]
10
    [% UNLESS ( one_language_enabled ) %]
Lines 74-78 Link Here
74
    [% END %]
78
    [% END %]
75
[% KohaPlugins.get_plugins_intranet_js | $raw %]
79
[% KohaPlugins.get_plugins_intranet_js | $raw %]
76
80
81
    <!-- Cookie consent -->
82
    [% IF Koha.Preference( 'CookieConsent' ) %]
83
        <!-- Cookie consent bar -->
84
        <div id="cookieConsentBar" aria-hidden="true">
85
            [% Koha.Preference( 'CookieConsentBar' ) | $raw %]
86
            <div id="consentButtons">
87
                <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
88
                [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
89
                    <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
90
                    <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button>
91
                [% END %]
92
            </div>
93
        </div> <!-- /#cookieConsentBar -->
94
        <!-- Cookie consent modal -->
95
        <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true">
96
            <div class="modal-dialog">
97
                <div class="modal-content">
98
                    <div class="modal-header">
99
                        <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2>
100
                    </div>
101
                    <div class="modal-body">
102
                        [% IF Koha.Preference( 'CookieConsentPopup' ) %]
103
                            <div id="cookieConsentPopupText">
104
                                [% Koha.Preference( 'CookieConsentPopup' ) | $raw %]
105
                            </div>
106
                        [% END %]
107
                        <div id="consentCookieList">
108
                            [% consents = JSConsents.all() %]
109
                            [% FOREACH consent IN consents %]
110
                                <div class="consentModalItem">
111
                                    <div class="consentItemCheckbox">
112
                                        <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]">
113
                                    </div>
114
                                    <div class="consentItemMeta">
115
                                        <div class="consentItemName">[% consent.name | html %]</div>
116
                                        <div class="consentItemDescription">[% consent.description | html %]</div>
117
                                    </div>
118
                                </div>
119
                            [% END %]
120
                        </div>
121
                    </div>
122
                    <div class="modal-footer">
123
                        <div id="consentButtons">
124
                            <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
125
                            [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
126
                                <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
127
                                <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button>
128
                            [% END %]
129
                        </div>
130
                    </div>
131
                </div> <!-- /.modal-content -->
132
            </div> <!-- /.modal-dialog -->
133
        </div>  <!-- /#cookieConsentModal  -->
134
    [% END %]
135
    <!-- ConsentJS code that may run -->
136
    [% IF Koha.Preference( 'CookieConsent' ) && Koha.Preference( 'ConsentJS' ).length > 0 %]
137
        [% consents = JSConsents.all() %]
138
        [% FOREACH consent IN consents %]
139
            <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>
140
        [% END %]
141
    [% END %]
142
    [% IF Koha.Preference( 'CookieConsent' ) %]
143
        [% Asset.js("js/cookieconsent.js") | $raw %]
144
    [% END %]
145
77
</body>
146
</body>
78
</html>
147
</html>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences.tt (+2 lines)
Lines 130-135 Link Here
130
                    </select>
130
                    </select>
131
                    [% ELSIF ( CHUNK.type_modalselect ) %]
131
                    [% ELSIF ( CHUNK.type_modalselect ) %]
132
                        <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 %]"/>
132
                        <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 %]"/>
133
                    [% ELSIF ( CHUNK.type_modaljs ) %]
134
                        <input type="text" name="pref_[% CHUNK.name | html %]" data-initiator="[% CHUNK.initiator | html %]" data-processor="[% CHUNK.processor %]" id="pref_[% CHUNK.name | html %]" class="modaljs preference preference-[% CHUNK.type | html %]" readonly="readonly" value="[% CHUNK.value | html %]"/>
133
                    [% ELSIF ( CHUNK.type_multiple ) %]
135
                    [% ELSIF ( CHUNK.type_multiple ) %]
134
                    <select name="pref_[% CHUNK.name | html %]" id="pref_[% CHUNK.name | html %]" class="preference preference-[% CHUNK.class or "choice" | html %]" multiple="multiple">
136
                    <select name="pref_[% CHUNK.name | html %]" id="pref_[% CHUNK.name | html %]" class="preference preference-[% CHUNK.class or "choice" | html %]" multiple="multiple">
135
                        [% 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 %]
137
                        [% 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 %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref (+24 lines)
Lines 388-393 Patrons: Link Here
388
         - days and remove anonymized patron accounts after
388
         - days and remove anonymized patron accounts after
389
         - pref: PatronRemovalDelay
389
         - pref: PatronRemovalDelay
390
         - "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."
390
         - "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."
391
     -
392
         - pref: CookieConsent
393
           choices:
394
               yes: Require
395
               no: "Don't require"
396
         - cookie consent to be displayed
397
     -
398
         - Specify Javascript that requires user consent to run (e.g. tracking code)
399
         - pref: ConsentJS
400
           type: modaljs
401
           initiator: populateConsentJS
402
           processor: prepareConsentJS
403
     -
404
         - Show the following HTML in the cookie consent bar that is displayed at the bottom of the screen
405
         - pref: CookieConsentBar
406
           type: textarea
407
           syntax: text/html
408
           class: code
409
     -
410
         - Show the following HTML in the cookie consent popup
411
         - pref: CookieConsentPopup
412
           type: textarea
413
           syntax: text/html
414
           class: code
391
    Security:
415
    Security:
392
     -
416
     -
393
         - Login passwords for staff and patrons must be at least
417
         - Login passwords for staff and patrons must be at least
(-)a/koha-tmpl/intranet-tmpl/prog/js/cookieconsent.js (+183 lines)
Line 0 Link Here
1
(function () {
2
    // Has the user previously consented, establish our
3
    // initial state
4
    let selected = [];
5
    let existingConsent = '';
6
    // The presence of a 'cookieConsent' local storage item indicates
7
    // that previous consent has been set. If the value is empty, then
8
    // no consent to non-essential cookies was given
9
    const hasStoredConsent = localStorage.getItem('cookieConsent');
10
    getExistingConsent();
11
12
    // The consent bar may not be in the DOM if it is not being used
13
    const consentBar = $('#cookieConsentBar');
14
    if (!consentBar) {
15
        return;
16
    }
17
    if (hasStoredConsent === null) {
18
        showConsentBar();
19
    }
20
    addButtonHandlers();
21
22
    // When the modal is opened, populate our state based on currently
23
    // selected values
24
    $('#cookieConsentModal').on('shown.bs.modal', function () {
25
        initialiseSelected();
26
    });
27
28
    // Initialise existing consent based on local storage
29
    function getExistingConsent() {
30
        existingConsent = localStorage.getItem('cookieConsent') ?
31
            localStorage.getItem('cookieConsent') :
32
            [];
33
    }
34
35
    function showConsentBar() {
36
        const consentBar = $('#cookieConsentBar');
37
        consentBar.attr('aria-hidden', 'false');
38
        consentBar.show();
39
    }
40
41
    function hideConsentBar() {
42
        const consentBar = $('#cookieConsentBar');
43
        consentBar.attr('aria-hidden', 'true');
44
        consentBar.hide();
45
    }
46
47
    // Hides the appropriate consent container, depending on what
48
    // is currently visible
49
    function hideContainer() {
50
        if ($('#cookieConsentModal').hasClass('in')) {
51
            $('#cookieConsentModal').modal('hide');
52
        } else {
53
            hideConsentBar();
54
        }
55
    }
56
57
    // Initialise our state of selected item and enable/disable
58
    // the "Accept selected" button appropriately
59
    function initialiseSelected() {
60
        selected = [];
61
        $('.consentCheckbox').each(function () {
62
            const val = $(this).val();
63
            if (existingConsent.indexOf(val) > -1 ) {
64
                $(this).prop('checked', true);
65
                selected.push(val);
66
            } else {
67
                $(this).prop('checked', false);
68
            }
69
        });
70
        enableDisableSelectedButton();
71
    }
72
73
    // Maintain our state of selected items and enable/disable
74
    // the "Accept selected" button appropriately
75
    function maintainSelected() {
76
        selected = [];
77
        $('.consentCheckbox:checked').each(function () {
78
            selected.push($(this).val());
79
        });
80
        enableDisableSelectedButton();
81
    }
82
83
    // Set the enabled / disabled state of the
84
    // "Accept selected button based on the checkbox
85
    // states
86
    function enableDisableSelectedButton() {
87
        $('#consentAcceptSelected').prop(
88
            'disabled',
89
            selected.length === 0
90
        );
91
    }
92
93
    function runConsentedCode() {
94
        getExistingConsent();
95
        $('.consentCode').each(function () {
96
            // The user has explicitly consented to this code, or the
97
            // code doesn't require consent in the staff view
98
            if (
99
                existingConsent.indexOf($(this).data('consent-id')) > -1 ||
100
                !$(this).data('requires-consent')
101
            ) {
102
                const code = atob($(this).data('consent-code'));
103
                const func = Function(code);
104
                func();
105
            } else {
106
                // This code doesn't have consent to run, we may need to remove
107
                // any cookies it has previously set
108
                const matchPattern = $(this).data('consent-match-pattern');
109
                const cookieDomain = $(this).data('consent-cookie-domain');
110
                const cookiePath = $(this).data('consent-cookie-path');
111
                if (matchPattern.length > 0) {
112
                    const regex = new RegExp(matchPattern);
113
                    const allCookies = document.cookie.split('; ');
114
                    allCookies.forEach(function (cookie) {
115
                        const name = cookie.split('=')[0];
116
                        if (regex.test(name)) {
117
                            document.cookie = name + '=; expires=Thu, 01 Jan 1970 00: 00: 01 GMT; domain=' + cookieDomain + '; path=' + cookiePath;
118
                        }
119
                    });
120
121
                }
122
            }
123
        });
124
    }
125
126
    function addButtonHandlers() {
127
        // "Accept all" handler
128
        $('.consentAcceptAll').on('click', function(e) {
129
            e.preventDefault();
130
            let toSave = [];
131
            $('.consentCheckbox').each(function () {
132
                const val = $(this).val();
133
                toSave.push(val);
134
            });
135
            localStorage.setItem('cookieConsent', toSave);
136
            hideContainer();
137
            runConsentedCode();
138
        });
139
140
        // "Accept essential" handler
141
        $('.consentAcceptEssential').on('click', function(e) {
142
            e.preventDefault();
143
            localStorage.setItem('cookieConsent', []);
144
            hideContainer();
145
            runConsentedCode();
146
        });
147
148
        // "Accept selected" handler
149
        $('#consentAcceptSelected').on('click', function(e) {
150
            e.preventDefault();
151
            const toSave = selected.length > 0 ? selected : [];
152
            localStorage.setItem('cookieConsent', toSave);
153
            hideContainer();
154
            runConsentedCode();
155
        });
156
157
        // "More information" handler
158
        $('#consentMoreInfo').on(
159
            'click',
160
            function (e) {
161
                e.preventDefault();
162
                hideConsentBar();
163
                // Ensure we're up to date with the existing consent
164
                getExistingConsent();
165
                // Prevent the modal from being closed with anything
166
                // but the buttons
167
                $('#cookieConsentModal')
168
                    .modal({
169
                        backdrop: 'static',
170
                        keyboard: false,
171
                        focus: true,
172
                        show: true
173
                    });
174
            }
175
        );
176
        $('.consentCheckbox').on('click', function () {
177
            maintainSelected();
178
        });
179
    }
180
181
    // On page load, run any code that has been given consent
182
    runConsentedCode();
183
})();
(-)a/koha-tmpl/intranet-tmpl/prog/js/pages/preferences.js (-44 / +316 lines)
Lines 64-69 window.onbeforeunload = function () { Link Here
64
    }
64
    }
65
};
65
};
66
66
67
// Add event handlers to any elements with .expand-textarea classes
68
// this allows the CodeMirror widget to be displayed
69
function addExpandHandler() {
70
    // Don't duplicate click event handlers
71
    const ev = $._data($('.expand-textarea'), 'events');
72
    if (ev && ev.click) {
73
        return;
74
    }
75
    $(".expand-textarea").on("click", function (e) {
76
        e.preventDefault();
77
        $(this).hide();
78
        var target = $(this).data("target");
79
        var syntax = $(this).data("syntax");
80
        $("#collapse_" + target).show();
81
        if (syntax) {
82
            var editor = CodeMirror.fromTextArea(document.getElementById("pref_" + target), {
83
                lineNumbers: true,
84
                mode: syntax,
85
                lineWrapping: true,
86
                viewportMargin: Infinity,
87
                gutters: ["CodeMirror-lint-markers"],
88
                lint: true
89
            });
90
            editor.on("change", function () {
91
                mark_modified.call($("#pref_" + target)[0]);
92
            });
93
            editor.on("blur", function () {
94
                editor.save();
95
            });
96
        } else {
97
            $("#pref_" + target).show();
98
        }
99
    });
100
}
101
102
// Add event handlers to any elements with .collapse-textarea classes
103
// this allows the hiding of the CodeMirror widget
104
function addCollapseHandler() {
105
    // Don't duplicate click event handlers
106
    const ev = $._data($('.collapse-textarea'), 'events');
107
    if (ev && ev.click) {
108
        return;
109
    }
110
    $(".collapse-textarea").on("click", function (e) {
111
        e.preventDefault();
112
        $(this).hide();
113
        var target = $(this).data("target");
114
        var syntax = $(this).data("syntax");
115
        $("#expand_" + target).show();
116
        if (syntax) {
117
            var editor = $("#pref_" + target).next(".CodeMirror")[0].CodeMirror;
118
            editor.toTextArea();
119
        }
120
        $("#pref_" + target).hide();
121
    });
122
}
123
124
// Add a handler for any consent delete links
125
function addConsentDeleteHandler() {
126
    // Don't duplicate click event handlers
127
    const ev = $._data($('.consentDelete'), 'events');
128
    if (ev && ev.click) {
129
        return;
130
    }
131
    $('.consentDelete').on('click', function (e) {
132
        e.preventDefault();
133
        const target = $(this).data('target');
134
        const proceed = confirm(__('Are you sure you want to delete this consent item?'));
135
        if (proceed) {
136
            $('#' + target).remove();
137
        }
138
    });
139
}
140
67
$( document ).ready( function () {
141
$( document ).ready( function () {
68
142
69
    $("table.preferences").dataTable($.extend(true, {}, dataTablesDefaults, {
143
    $("table.preferences").dataTable($.extend(true, {}, dataTablesDefaults, {
Lines 107-150 $( document ).ready( function () { Link Here
107
        return false;
181
        return false;
108
    });
182
    });
109
183
110
    $( ".expand-textarea" ).on("click", function(e){
184
    addExpandHandler();
111
        e.preventDefault();
112
        $(this).hide();
113
        var target = $(this).data("target");
114
        var syntax = $(this).data("syntax");
115
        $("#collapse_" + target ).show();
116
        if( syntax ){
117
            var editor = CodeMirror.fromTextArea( document.getElementById( "pref_" + target ), {
118
                lineNumbers: true,
119
                mode: syntax,
120
                lineWrapping: true,
121
                viewportMargin: Infinity,
122
                gutters: ["CodeMirror-lint-markers"],
123
                lint: true
124
            });
125
            editor.on("change", function(){
126
                mark_modified.call( $("#pref_" + target )[0]);
127
            });
128
            editor.on("blur", function(){
129
                editor.save();
130
            });
131
        } else {
132
            $("#pref_" + target ).show();
133
        }
134
    });
135
185
136
    $( ".collapse-textarea" ).on("click", function(e){
186
    addCollapseHandler();
137
        e.preventDefault();
138
        $(this).hide();
139
        var target = $(this).data("target");
140
        var syntax = $(this).data("syntax");
141
        $("#expand_" + target ).show();
142
        if( syntax ){
143
            var editor = $("#pref_" + target ).next(".CodeMirror")[0].CodeMirror;
144
            editor.toTextArea();
145
        }
146
        $("#pref_" + target ).hide();
147
    });
148
187
149
    $("h3").attr("class", "expanded").attr("title", __("Click to collapse this section"));
188
    $("h3").attr("class", "expanded").attr("title", __("Click to collapse this section"));
150
    var collapsible = $(".collapsed,.expanded");
189
    var collapsible = $(".collapsed,.expanded");
Lines 262-285 $( document ).ready( function () { Link Here
262
        }).appendTo("#prefModalForm");
301
        }).appendTo("#prefModalForm");
263
302
264
        $("#saveModalPrefs").data("target", this.id );
303
        $("#saveModalPrefs").data("target", this.id );
304
        $("#saveModalPrefs").data("type", "modalselect" );
265
        $("#prefModalLabel").text( pref_name );
305
        $("#prefModalLabel").text( pref_name );
266
        $("#prefModal").modal("show");
306
        $("#prefModal").modal("show");
267
    });
307
    });
268
308
269
    $("#saveModalPrefs").on("click", function(){
309
    // Initialise the content of our modal, using the function
270
        var formfieldid = $("#" + $(this).data("target") );
310
    // specified in the data-initiator attribute
311
    $('.modaljs').on('click', function () {
312
        const init = $(this).data('initiator');
313
        if (init) {
314
            window[init](this);
315
            $("#prefModal").modal("show");
316
        }
317
    });
318
319
    // Initialise the content of our modal, if we are dealing
320
    // with a modalselect modal
321
    function prepareModalSelect(formfieldid) {
271
        var prefs = [];
322
        var prefs = [];
272
        $("#prefModal input[type='checkbox']").each(function(){
323
        $("#prefModal input[type='checkbox']").each(function(){
273
            if( $(this).prop("checked") ){
324
            if( $(this).prop("checked") ){
274
                prefs.push( this.value );
325
                prefs.push( this.value );
275
            }
326
            }
276
        });
327
        });
328
        return prefs.join("|");
329
    }
330
331
    // Return a checkbox with an appropriate checked state
332
    function checkBox(id, className, state) {
333
        return state ?
334
            '<input id="' + id + '" type="checkbox" class="' + className + '" checked>' :
335
            '<input id="' + id + '" type="checkbox" class="' + className + '">';
336
    }
337
338
    // Create a consentJS item, correctly populated
339
    function createConsentJSItem(item, idx) {
340
        const id = 'ConsentJS_' + idx;
341
        const code = item.code && item.code.length > 0 ? atob(item.code) : '';
342
        const itemId = item.id && item.id.length > 0 ? item.id : '';
343
        return '<div id="' + id + '" class="consentJSItem" data-id="' + itemId + '">' +
344
               '    <div class="consentRow">' +
345
               '        <div class="consentItem">' +
346
               '            <label class="required" for="name_' + id + '">' + __('Name') + ':</label>' +
347
               '            <input id="name_' + id + '" class="metaName" type="text" value="' + item.name + '"><span class="required">' + __('Required') + '</span>' +
348
               '        </div >' +
349
               '        <div class="consentItem">' +
350
               '            <label class="required" for="description_' + id + '">' + __('Description') + ':</label>' +
351
               '            <input id="description_' + id + '" class="metaDescription" type="text" value="' + item.description + '"><span class="required">' + __('Required') + '</span>' +
352
               '        </div>' +
353
               '        <div class="consentItem">' +
354
               '            <label for="opacConsent_' + id + '">' + __('Requires consent in OPAC') + ':</label>' +
355
                            checkBox('opacConsent_' + id, 'opacConsent', item.opacConsent) +
356
               '        </div>' +
357
               '        <div class="consentItem">' +
358
               '            <label for="staffConsent_' + id + '">' + __('Requires consent in staff interface') + ':</label>' +
359
                            checkBox('staffConsent_' + id, 'staffConsent', item.staffConsent) +
360
               '        </div >' +
361
               '        <div class="consentItem">' +
362
               '            <label for="matchPattern_' + id + '">' + __('String used to identify cookie name') + ':</label>' +
363
               '            <input id="matchPattern_' + id + '" class="metaMatchPattern" type="text" value="' + item.matchPattern + '"><span class="required">' + __('Required') + '</span>' +
364
               '        </div >' +
365
               '        <div class="consentItem">' +
366
               '            <label for="cookieDomain' + id + '">' + __('Cookie domain') + ':</label>' +
367
               '            <input id="cookieDomain' + id + '" class="metaCookieDomain" type="text" value="' + item.cookieDomain + '"><span class="required">' + __('Required') + '</span>' +
368
               '        </div >' +
369
               '        <div class="consentItem">' +
370
               '            <label for="cookiePath' + id + '">' + __('Cookie path') + ':</label>' +
371
               '            <input id="cookiePath' + id + '" class="metaCookiePath" type="text" value="' + item.cookiePath + '"><span class="required">' + __('Required') + '</span>' +
372
               '        </div >' +
373
               '    </div >' +
374
               '    <div class="consentRow codeRow">' +
375
               '        <textarea style="display:none;" id="pref_' + id + '" class="preference preference-code codemirror" rows="10" cols="40">' + code + '</textarea>' +
376
               '        <div>' +
377
               '            <a id="expand_' + id + '" class="expand-textarea" data-target="' + id + '" data-syntax="javascript" href="#">' + __('Click to expand') + '</a>' +
378
               '            <a id="collapse_' + id + '" class="collapse-textarea" data-target="' + id + '" data-syntax="javascript" href="#" style="display:none">' + __('Click to collapse') + '</a>' +
379
               '        </div >' +
380
               '    </div>' +
381
               '    <a class="consentDelete" data-target="' + id + '" href="#">Delete</a>' +
382
               '</div > ';
383
    }
384
385
    // Return the markup for all consentJS items concatenated
386
    function populateConsentMarkup(items) {
387
        return items.reduce(function (acc, current, idx) {
388
            return acc + createConsentJSItem(current, idx);
389
        }, '');
390
    }
391
392
    // Return the markup for a validation warning
393
    function populateValidationWarning() {
394
        return '<div id="consentJSWarning" class="error" style="display:none;">' + __('You must complete all fields') + '</div>';
395
    }
396
397
    // Return a new, empty consent item
398
    function emptyConsentItem() {
399
        return {
400
            name: '',
401
            description: '',
402
            matchPattern: '',
403
            cookieDomain: '',
404
            cookiePath: '',
405
            code: '',
406
            consentInOpac: false,
407
            consentInStaff: false
408
        };
409
    }
410
411
    // Add the handler for a new empty consent item
412
    function addNewHandler() {
413
        $("#consentJSAddNew").on("click", function (e) {
414
            e.preventDefault();
415
            const currentLen = $('.consentJSItem').length;
416
            const newItem = emptyConsentItem();
417
            const markup = createConsentJSItem(newItem, currentLen);
418
            $('#prefModal .modal-body #consentJSItems').append($(markup));
419
            addExpandHandler();
420
            addCollapseHandler();
421
            addConsentDeleteHandler();
422
        });
423
    }
424
425
    // Populate the consentJS modal, we also initialise any
426
    // event handlers that are required. This function is added
427
    // to the window object so we can call it if we are passed it's name
428
    // as a data-initiator attribute
429
    // (e.g.)
430
    // const f = 'populateConsentJS';
431
    // window[f]();
432
    window.populateConsentJS = function(el) {
433
        let items = [];
434
        let decoded = '';
435
        if (el.value && el.value.length > 0) {
436
            try {
437
                decoded = atob(el.value);
438
            } catch (err) {
439
                throw (__(
440
                    'Unable to Base64 decode value stored in ConsentJS syspref: ' +
441
                    err.message
442
                ));
443
            }
444
            try {
445
                items = JSON.parse(decoded);
446
            } catch (err) {
447
                throw (__(
448
                    'Unable to JSON parse decoded value stored in ConsentJS syspref: ' +
449
                    err.message
450
                ));
451
            }
452
        }
453
        const markup = populateConsentMarkup(items);
454
        const validationWarning = populateValidationWarning();
455
        const pref_name = el.id.replace(/pref_/, '');
456
        $('#saveModalPrefs').data('target', el.id);
457
        $('#prefModalLabel').text( pref_name );
458
        $('#prefModal .modal-body').html($('<div id="consentJSItems">' + validationWarning + markup + '</div><div><a href="#" id="consentJSAddNew">' + __('Add new code') + '</a></div>'));
459
        addExpandHandler();
460
        addCollapseHandler();
461
        addNewHandler();
462
        addConsentDeleteHandler();
463
    }
464
465
    // Prepare the data in the UI for sending back as a syspref.
466
    // We validate that everything is what we expect. This function is added
467
    // to the window object so we can call it if we are passed it's name
468
    // as a data-initiator attribute
469
    // e.g.
470
    // const f = 'prepareConsentJS';
471
    // window[f]();
472
    window.prepareConsentJS = function () {
473
        const items = $('.consentJSItem');
474
        const invalid = [];
475
        const valid = [];
476
        items.each(function () {
477
            const id = $(this).data('id').length > 0 ?
478
                $(this).data('id') :
479
                '_' + Math.random().toString(36).substr(2, 9);
480
            const name = $(this).find('.metaName').val();
481
            const desc = $(this).find('.metaDescription').val();
482
            const matchPattern = $(this).find('.metaMatchPattern').val();
483
            const cookieDomain = $(this).find('.metaCookieDomain').val();
484
            const cookiePath = $(this).find('.metaCookiePath').val();
485
            const opacConsent = $(this).find('.opacConsent').is(':checked')
486
            const staffConsent = $(this).find('.staffConsent').is(':checked');
487
            const code = $(this).find('.preference-code').val();
488
            // If the name, description, match pattern code are empty, then they've
489
            // added a new entry, but not filled it in, we can skip it
490
            if (
491
                name.length === 0 &&
492
                desc.length === 0 &&
493
                matchPattern.length === 0 &&
494
                cookieDomain.length === 0 &&
495
                cookiePath.length === 0 &&
496
                code.length === 0
497
            ) {
498
                return;
499
            }
500
            // They've filled in at least some info
501
            if (
502
                (name.length === 0) ||
503
                (desc.length === 0) ||
504
                (matchPattern.length === 0) ||
505
                (cookiePath.length === 0) ||
506
                (code.length === 0)
507
            ) {
508
                invalid.push(this);
509
            } else {
510
                const obj = {
511
                    id: id,
512
                    name: name,
513
                    description: desc,
514
                    matchPattern: matchPattern,
515
                    cookieDomain: cookieDomain,
516
                    cookiePath: cookiePath,
517
                    opacConsent: opacConsent,
518
                    staffConsent: staffConsent,
519
                    code: btoa(code)
520
                }
521
                valid.push(obj);
522
            }
523
        });
524
        // We failed validation
525
        if (invalid.length > 0) {
526
            $('#consentJSWarning').show();
527
            return false;
528
        }
529
        $('#consentJSWarning').hide();
530
        if (valid.length === 0) {
531
            return '';
532
        }
533
        const json = JSON.stringify(valid);
534
        const base64 = btoa(json);
535
        return base64;
536
    }
277
537
278
        formfieldid.val( prefs.join("|") )
538
    $("#saveModalPrefs").on("click", function(){
279
            .addClass("modified");
539
        var formfieldid = $("#" + $(this).data("target"));
280
        mark_modified.call( formfieldid );
540
        let finalString = "";
281
        KOHA.Preferences.Save( formfieldid.closest("form") );
541
        if ($(this).data("type") == "modalselect") {
282
        $("#prefModal").modal("hide");
542
            finalString = prepareModalSelect(formfieldid);
543
        } else {
544
            const processor = $(".modaljs").data("processor");
545
            finalString = window[processor]();
546
        }
547
        // A processor can return false if any of the submitted
548
        // data has failed validation
549
        if (finalString !== false) {
550
            formfieldid.val(finalString).addClass("modified");
551
            mark_modified.call( formfieldid );
552
            KOHA.Preferences.Save( formfieldid.closest("form") );
553
            $("#prefModal").modal("hide");
554
        }
283
    });
555
    });
284
556
285
    $("#prefModal").on("hide.bs.modal", function(){
557
    $("#prefModal").on("hide.bs.modal", function(){
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss (+57 lines)
Lines 2874-2877 $star-selected: #EDB867; Link Here
2874
    }
2874
    }
2875
}
2875
}
2876
2876
2877
/* Cookie consent bar */
2878
#cookieConsentBar {
2879
    display: none;
2880
    padding: 20px;
2881
    position: fixed;
2882
    bottom: 0;
2883
    left: 0;
2884
    right: 0;
2885
    background: rgba(0,0,0,0.7);
2886
    color: rgba(255,255,255,0.9);
2887
    z-index: 1;
2888
    a:link, a:visited {
2889
        color: #c5ecff;
2890
    }
2891
    #consentButtons {
2892
        margin-top: 10px;
2893
        text-align: right;
2894
    }
2895
    button:not(:last-child) {
2896
        margin-right: 10px;
2897
    }
2898
}
2899
2900
/* Cookie consent modal */
2901
#cookieConsentModal {
2902
    .modal-dialog {
2903
        max-width: 1000px;
2904
    }
2905
    .consentModalItem {
2906
        display: flex;
2907
        align-items: center;
2908
        border: 1px solid #ccc;
2909
        padding: 10px 20px;
2910
        border-radius: 5px;
2911
    }
2912
    .consentModalItem:not(:last-child) {
2913
        margin-bottom: 20px;
2914
    }
2915
    .consentItemCheckbox {
2916
        padding-right: 20px;
2917
    }
2918
    .consentItemName {
2919
        font-weight: bold;
2920
    }
2921
    #cookieConsentPopupText {
2922
        margin: 20px 0;
2923
    }
2924
}
2925
2926
#viewCookieConsents {
2927
    margin-top: 10px;
2928
}
2929
2930
#patronconsents h5 {
2931
    margin-top: 30px;
2932
}
2933
2877
@import "responsive";
2934
@import "responsive";
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+65 lines)
Lines 5-10 Link Here
5
[% USE AuthClient %]
5
[% USE AuthClient %]
6
[% USE AdditionalContents %]
6
[% USE AdditionalContents %]
7
[% PROCESS 'html_helpers.inc' %]
7
[% PROCESS 'html_helpers.inc' %]
8
[% IF Koha.Preference( 'CookieConsent' ) %]
9
    [% USE JSConsents %]
10
[% END %]
8
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
11
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
9
[% SET OpacHeader = AdditionalContents.get( location => "opacheader", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
12
[% SET OpacHeader = AdditionalContents.get( location => "opacheader", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
10
[% SET OpacCustomSearch = AdditionalContents.get( location => "OpacCustomSearch", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
13
[% SET OpacCustomSearch = AdditionalContents.get( location => "OpacCustomSearch", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
Lines 88-93 Link Here
88
                        </a>
91
                        </a>
89
                    </li>
92
                    </li>
90
                [% END %]
93
                [% END %]
94
                <!-- Cookie consent button for non logged-in users -->
95
                [% IF !loggedinusername %]
96
                    <li style="display:none" id="cookieConsentDivider" class="divider-vertical" aria-hidden="true"></li>
97
                    <li style="display:none" id="cookieConsentLi" class="nav-item" aria-hidden="true">
98
                        <a id="cookieConsentButton" href="#" name="Your cookies" class="nav-link" title="View and amend cookies you have consented to" role="button">
99
                            <span class="cookieconsentlabel">Your cookies</span>
100
                        </a>
101
                    </li>
102
                [% END %]
91
            </ul> <!-- / .navbar-nav -->
103
            </ul> <!-- / .navbar-nav -->
92
104
93
            [% IF Koha.Preference( 'opacuserlogin' ) == 1 ||  Koha.Preference( 'EnableOpacSearchHistory') || Koha.Preference( 'opaclanguagesdisplay' ) %]
105
            [% IF Koha.Preference( 'opacuserlogin' ) == 1 ||  Koha.Preference( 'EnableOpacSearchHistory') || Koha.Preference( 'opaclanguagesdisplay' ) %]
Lines 440-442 Link Here
440
            </div> <!-- /.modal-content -->
452
            </div> <!-- /.modal-content -->
441
        </div> <!-- /.modal-dialog -->
453
        </div> <!-- /.modal-dialog -->
442
    </div>  <!-- /#modalAuth  -->
454
    </div>  <!-- /#modalAuth  -->
455
    [% IF Koha.Preference( 'CookieConsent' ) %]
456
        <!-- Cookie consent bar -->
457
        <div id="cookieConsentBar" aria-hidden="true">
458
            [% Koha.Preference( 'CookieConsentBar' ) | $raw %]
459
            <div id="consentButtons">
460
                <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
461
                [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
462
                    <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
463
                    <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button>
464
                [% END %]
465
            </div>
466
        </div> <!-- /#cookieConsentBar -->
467
        <!-- Cookie consent modal -->
468
        <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true">
469
            <div class="modal-dialog">
470
                <div class="modal-content">
471
                    <div class="modal-header">
472
                        <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2>
473
                    </div>
474
                    <div class="modal-body">
475
                        [% IF Koha.Preference( 'CookieConsentPopup' ) %]
476
                            <div id="cookieConsentPopupText">
477
                                [% Koha.Preference( 'CookieConsentPopup' ) | $raw %]
478
                            </div>
479
                        [% END %]
480
                        <div id="consentCookieList">
481
                            [% consents = JSConsents.all() %]
482
                            [% FOREACH consent IN consents %]
483
                                <div class="consentModalItem">
484
                                    <div class="consentItemCheckbox">
485
                                        <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]">
486
                                    </div>
487
                                    <div class="consentItemMeta">
488
                                        <div class="consentItemName">[% consent.name | html %]</div>
489
                                        <div class="consentItemDescription">[% consent.description | html %]</div>
490
                                    </div>
491
                                </div>
492
                            [% END %]
493
                        </div>
494
                    </div>
495
                    <div class="modal-footer">
496
                        <div id="consentButtons">
497
                            <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
498
                            [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
499
                                <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
500
                                <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button>
501
                            [% END %]
502
                        </div>
503
                    </div>
504
                </div> <!-- /.modal-content -->
505
            </div> <!-- /.modal-dialog -->
506
        </div>  <!-- /#cookieConsentModal  -->
507
    [% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+14 lines)
Lines 5-10 Link Here
5
[% USE Asset %]
5
[% USE Asset %]
6
[% SET opaccredits = AdditionalContents.get( location => "opaccredits", lang => lang, library => logged_in_user.branchcode || default_branch ) %]
6
[% SET opaccredits = AdditionalContents.get( location => "opaccredits", lang => lang, library => logged_in_user.branchcode || default_branch ) %]
7
[% PROCESS 'html_helpers.inc' %]
7
[% PROCESS 'html_helpers.inc' %]
8
[% IF Koha.Preference( 'CookieConsent' ) %]
9
    [% USE JSConsents %]
10
[% END %]
8
[% UNLESS ( is_popup ) %]
11
[% UNLESS ( is_popup ) %]
9
        [% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
12
        [% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
10
        [% IF ( opaccredits ) %]
13
        [% IF ( opaccredits ) %]
Lines 121-126 Link Here
121
    [% INCLUDE 'modals/checkout.inc' %]
124
    [% INCLUDE 'modals/checkout.inc' %]
122
[% END %]
125
[% END %]
123
126
127
<!-- ConsentJS code that may run -->
128
[% IF Koha.Preference( 'CookieConsent' ) && Koha.Preference( 'ConsentJS' ).length > 0 %]
129
    [% consents = JSConsents.all() %]
130
    [% FOREACH consent IN consents %]
131
        <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>
132
    [% END %]
133
[% END %]
134
124
<!-- JavaScript includes -->
135
<!-- JavaScript includes -->
125
[% Asset.js("lib/jquery/jquery-3.6.0.min.js") | $raw %]
136
[% Asset.js("lib/jquery/jquery-3.6.0.min.js") | $raw %]
126
[% Asset.js("lib/jquery/jquery-migrate-3.3.2.min.js") | $raw %]
137
[% Asset.js("lib/jquery/jquery-migrate-3.3.2.min.js") | $raw %]
Lines 309-314 $(document).ready(function() { Link Here
309
[% IF Koha.Preference( 'OpacTrustedCheckout' ) %]
320
[% IF Koha.Preference( 'OpacTrustedCheckout' ) %]
310
    [% Asset.js("js/modals/checkout.js") | $raw %]
321
    [% Asset.js("js/modals/checkout.js") | $raw %]
311
[% END %]
322
[% END %]
323
[% IF Koha.Preference( 'CookieConsent' ) %]
324
    [% Asset.js("js/cookieconsent.js") | $raw %]
325
[% END %]
312
[% KohaPlugins.get_plugins_opac_js | $raw %]
326
[% KohaPlugins.get_plugins_opac_js | $raw %]
313
</body>
327
</body>
314
</html>
328
</html>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc (-1 / +1 lines)
Lines 24-30 Link Here
24
            [% END %]
24
            [% END %]
25
                <a href="/cgi-bin/koha/opac-memberentry.pl">Personal details</a></li>
25
                <a href="/cgi-bin/koha/opac-memberentry.pl">Personal details</a></li>
26
26
27
            [% IF Koha.Preference('PrivacyPolicyConsent') # remove when extending %]
27
            [% IF Koha.Preference('PrivacyPolicyConsent') || Koha.Preference('CookieConsent') # remove when extending %]
28
                [% IF consentview %]<li class="active">[% ELSE %]<li>[% END %]
28
                [% IF consentview %]<li class="active">[% ELSE %]<li>[% END %]
29
                    <a href="/cgi-bin/koha/opac-patron-consent.pl">Consents</a>
29
                    <a href="/cgi-bin/koha/opac-patron-consent.pl">Consents</a>
30
                </li>
30
                </li>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt (+5 lines)
Lines 70-75 Link Here
70
70
71
                    </form>
71
                    </form>
72
72
73
                    [% IF Koha.Preference('CookieConsent') %]
74
                        <h5>Cookie consents</h5>
75
                        <button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button>
76
                    [% END %]
77
73
                </div> <!-- / #userpasswd -->
78
                </div> <!-- / #userpasswd -->
74
            </div> <!-- / .col-lg-10 -->
79
            </div> <!-- / .col-lg-10 -->
75
        </div> <!-- / .row -->
80
        </div> <!-- / .row -->
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/cookieconsent.js (+199 lines)
Line 0 Link Here
1
(function () {
2
    // Has the user previously consented, establish our
3
    // initial state
4
    let selected = [];
5
    let existingConsent = '';
6
    // The presence of a 'cookieConsent' local storage item indicates
7
    // that previous consent has been set. If the value is empty, then
8
    // no consent to non-essential cookies was given
9
    const hasStoredConsent = localStorage.getItem('cookieConsent');
10
    getExistingConsent();
11
12
    // The consent bar may not be in the DOM if it is not being used
13
    const consentBar = $('#cookieConsentBar');
14
    if (!consentBar) {
15
        return;
16
    }
17
    if (hasStoredConsent === null) {
18
        showConsentBar();
19
    } else {
20
        showYourCookies();
21
    }
22
    addButtonHandlers();
23
24
    // When the modal is opened, populate our state based on currently
25
    // selected values
26
    $('#cookieConsentModal').on('shown.bs.modal', function () {
27
        initialiseSelected();
28
    });
29
30
    // Initialise existing consent based on local storage
31
    function getExistingConsent() {
32
        existingConsent = localStorage.getItem('cookieConsent') ?
33
            localStorage.getItem('cookieConsent') :
34
            [];
35
    }
36
37
    function showConsentBar() {
38
        const consentBar = $('#cookieConsentBar');
39
        consentBar.attr('aria-hidden', 'false');
40
        consentBar.show();
41
    }
42
43
    function hideConsentBar() {
44
        const consentBar = $('#cookieConsentBar');
45
        consentBar.attr('aria-hidden', 'true');
46
        consentBar.hide();
47
    }
48
49
    // Hides the appropriate consent container, depending on what
50
    // is currently visible
51
    function hideContainer() {
52
        if ($('#cookieConsentModal').hasClass('show')) {
53
            $('#cookieConsentModal').modal('hide');
54
        } else {
55
            hideConsentBar();
56
        }
57
    }
58
59
    // Show the unauthenticated user's "Your cookies" button
60
    function showYourCookies() {
61
        // If the user is unauthenticated, there will be a
62
        // cookieConsentsButton element in the DOM. The user
63
        // has previously consented, so we need to display this
64
        // button
65
        if ($('#cookieConsentButton').length > 0) {
66
            $('#cookieConsentDivider').show().attr('aria-hidden', 'false');
67
            $('#cookieConsentLi').show().attr('aria-hidden', 'false');
68
        }
69
    }
70
71
    // Initialise our state of selected item and enable/disable
72
    // the "Accept selected" button appropriately
73
    function initialiseSelected() {
74
        selected = [];
75
        $('.consentCheckbox').each(function () {
76
            const val = $(this).val();
77
            if (existingConsent.indexOf(val) > -1 ) {
78
                $(this).prop('checked', true);
79
                selected.push(val);
80
            } else {
81
                $(this).prop('checked', false);
82
            }
83
        });
84
        enableDisableSelectedButton();
85
    }
86
87
    // Maintain our state of selected items and enable/disable
88
    // the "Accept selected" button appropriately
89
    function maintainSelected() {
90
        selected = [];
91
        $('.consentCheckbox:checked').each(function () {
92
            selected.push($(this).val());
93
        });
94
        enableDisableSelectedButton();
95
    }
96
97
    // Set the enabled / disabled state of the
98
    // "Accept selected button based on the checkbox
99
    // states
100
    function enableDisableSelectedButton() {
101
        $('#consentAcceptSelected').prop(
102
            'disabled',
103
            selected.length === 0
104
        );
105
    }
106
107
    function runConsentedCode() {
108
        getExistingConsent();
109
        $('.consentCode').each(function () {
110
            // The user has explicitly consented to this code, or the
111
            // code doesn't require consent in the OPAC
112
            if (
113
                existingConsent.indexOf($(this).data('consent-id')) > -1 ||
114
                !$(this).data('requires-consent')
115
            ) {
116
                const code = atob($(this).data('consent-code'));
117
                const func = Function(code);
118
                func();
119
            } else {
120
                // This code doesn't have consent to run, we may need to remove
121
                // any cookies it has previously set
122
                const matchPattern = $(this).data('consent-match-pattern');
123
                const cookieDomain = $(this).data('consent-cookie-domain');
124
                const cookiePath = $(this).data('consent-cookie-path');
125
                if (matchPattern.length > 0) {
126
                    const regex = new RegExp(matchPattern);
127
                    const allCookies = document.cookie.split('; ');
128
                    allCookies.forEach(function (cookie) {
129
                        const name = cookie.split('=')[0];
130
                        if (regex.test(name)) {
131
                            document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=' + cookieDomain +'; path=' + cookiePath;
132
                        }
133
                    });
134
                }
135
            }
136
        });
137
    }
138
139
    function addButtonHandlers() {
140
        // "Accept all" handler
141
        $('.consentAcceptAll').on('click', function(e) {
142
            e.preventDefault();
143
            let toSave = [];
144
            $('.consentCheckbox').each(function () {
145
                const val = $(this).val();
146
                toSave.push(val);
147
            });
148
            localStorage.setItem('cookieConsent', toSave);
149
            hideContainer();
150
            runConsentedCode();
151
            showYourCookies();
152
        });
153
154
        // "Accept essential" handler
155
        $('.consentAcceptEssential').on('click', function(e) {
156
            e.preventDefault();
157
            localStorage.setItem('cookieConsent', []);
158
            hideContainer();
159
            runConsentedCode();
160
            showYourCookies();
161
        });
162
163
        // "Accept selected" handler
164
        $('#consentAcceptSelected').on('click', function(e) {
165
            e.preventDefault();
166
            const toSave = selected.length > 0 ? selected : [];
167
            localStorage.setItem('cookieConsent', toSave);
168
            hideContainer();
169
            runConsentedCode();
170
            showYourCookies();
171
        });
172
173
        // "More information" handler
174
        $('#consentMoreInfo, #cookieConsentButton, #viewCookieConsents').on(
175
            'click',
176
            function (e) {
177
                e.preventDefault();
178
                hideConsentBar();
179
                // Ensure we're up to date with the existing consent
180
                getExistingConsent();
181
                // Prevent the modal from being closed with anything
182
                // but the buttons
183
                $('#cookieConsentModal')
184
                    .modal({
185
                        backdrop: 'static',
186
                        keyboard: false,
187
                        focus: true,
188
                        show: true
189
                    });
190
            }
191
        );
192
        $('.consentCheckbox').on('click', function () {
193
            maintainSelected();
194
        });
195
    }
196
197
    // On page load, run any code that has been given consent
198
    runConsentedCode();
199
})();
(-)a/t/db_dependent/Koha/Template/Plugin/JSConsents.t (-1 / +25 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use C4::Context;
6
7
use Test::MockModule;
8
use Test::More tests => 3;
9
use t::lib::Mocks;
10
11
BEGIN {
12
    use_ok('Koha::Template::Plugin::JSConsents', "Can use Koha::Template::Plugin::JSConsents");
13
}
14
15
ok( my $consents = Koha::Template::Plugin::JSConsents->new(), 'Able to instantiate template plugin' );
16
17
subtest "all" => sub {
18
    plan tests => 1;
19
20
    t::lib::Mocks::mock_preference( 'ConsentJS', 'eyAidGVzdCI6ICJvbmUiIH0=' );
21
22
    is_deeply( $consents->all(), { test => 'one' }, 'Returns a Base64 decoded JSON object converted into a data structure');
23
};
24
25
1;

Return to bug 27378