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/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss (+68 lines)
Lines 2596-2599 $star-selected: #EDB867; Link Here
2596
    }
2596
    }
2597
}
2597
}
2598
2598
2599
/* Cookie consent bar */
2600
#cookieConsentBar {
2601
    display: none;
2602
    padding: 20px;
2603
    position: fixed;
2604
    bottom: 0;
2605
    left: 0;
2606
    right: 0;
2607
    background: rgba(0,0,0,0.7);
2608
    color: rgba(255,255,255,0.9);
2609
    z-index: 1;
2610
    a:link, a:visited {
2611
        color: #c5ecff;
2612
    }
2613
    #consentButtons {
2614
        margin-top: 10px;
2615
        text-align: right;
2616
    }
2617
    button:not(:last-child) {
2618
        margin-right: 10px;
2619
    }
2620
}
2621
2622
/* Cookie consent modal */
2623
#cookieConsentModal {
2624
    .modal-dialog {
2625
        max-width: 1000px;
2626
    }
2627
    .consentModalItem {
2628
        display: flex;
2629
        align-items: center;
2630
        border: 1px solid #ccc;
2631
        padding: 10px 20px;
2632
        border-radius: 5px;
2633
    }
2634
    .consentModalItem:not(:last-child) {
2635
        margin-bottom: 20px;
2636
    }
2637
    .consentItemCheckbox {
2638
        padding-right: 20px;
2639
    }
2640
    .consentItemName {
2641
        font-weight: bold;
2642
    }
2643
    #cookieConsentPopupText {
2644
        margin: 20px 0;
2645
    }
2646
}
2647
2648
#cookieConsentButton {
2649
    position: fixed;
2650
    border: 0;
2651
    bottom: 0;
2652
    right: 0;
2653
    padding: 10px;
2654
    background: rgba(0,0,0,0.75);
2655
    color: rgba(255,255,255,0.8);
2656
    z-index: 1;
2657
}
2658
2659
#viewCookieConsents {
2660
    margin-top: 10px;
2661
}
2662
2663
#patronconsents h5 {
2664
    margin-top: 30px;
2665
}
2666
2599
@import "responsive";
2667
@import "responsive";
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+62 lines)
Lines 2-7 Link Here
2
[% USE Koha %]
2
[% USE Koha %]
3
[% USE Branches %]
3
[% USE Branches %]
4
[% USE Categories %]
4
[% USE Categories %]
5
[% IF Koha.Preference( 'CookieConsent' ) %]
6
    [% USE JSConsents %]
7
[% END %]
5
[% USE KohaNews %]
8
[% USE KohaNews %]
6
[% PROCESS 'html_helpers.inc' %]
9
[% PROCESS 'html_helpers.inc' %]
7
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
10
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
Lines 410-412 Link Here
410
            </div> <!-- /.modal-content -->
413
            </div> <!-- /.modal-content -->
411
        </div> <!-- /.modal-dialog -->
414
        </div> <!-- /.modal-dialog -->
412
    </div>  <!-- /#modalAuth  -->
415
    </div>  <!-- /#modalAuth  -->
416
    [% IF Koha.Preference( 'CookieConsent' ) %]
417
        <!-- Cookie consent bar -->
418
        <div id="cookieConsentBar" aria-hidden="true">
419
            [% Koha.Preference( 'CookieConsentBar' ) | $raw %]
420
            <div id="consentButtons">
421
                <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
422
                [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
423
                    <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
424
                    <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button>
425
                [% END %]
426
            </div>
427
        </div> <!-- /#cookieConsentBar -->
428
        <!-- Cookie consent button for non logged-in users -->
429
        [% IF !loggedinusername %]
430
            <button id="cookieConsentButton" style="display:none" aria-hidden="true">
431
                Your cookies
432
            </button>
433
        [% END %]
434
        <!-- Cookie consent modal -->
435
        <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true">
436
            <div class="modal-dialog">
437
                <div class="modal-content">
438
                    <div class="modal-header">
439
                        <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2>
440
                    </div>
441
                    <div class="modal-body">
442
                        [% IF Koha.Preference( 'CookieConsentPopup' ) %]
443
                            <div id="cookieConsentPopupText">
444
                                [% Koha.Preference( 'CookieConsentPopup' ) | $raw %]
445
                            </div>
446
                        [% END %]
447
                        <div id="consentCookieList">
448
                            [% consents = JSConsents.all() %]
449
                            [% FOREACH consent IN consents %]
450
                                <div class="consentModalItem">
451
                                    <div class="consentItemCheckbox">
452
                                        <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]">
453
                                    </div>
454
                                    <div class="consentItemMeta">
455
                                        <div class="consentItemName">[% consent.name | html %]</div>
456
                                        <div class="consentItemDescription">[% consent.description | html %]</div>
457
                                    </div>
458
                                </div>
459
                            [% END %]
460
                        </div>
461
                    </div>
462
                    <div class="modal-footer">
463
                        <div id="consentButtons">
464
                            <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
465
                            [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
466
                                <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
467
                                <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button>
468
                            [% END %]
469
                        </div>
470
                    </div>
471
                </div> <!-- /.modal-content -->
472
            </div> <!-- /.modal-dialog -->
473
        </div>  <!-- /#cookieConsentModal  -->
474
    [% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+14 lines)
Lines 3-8 Link Here
3
[% USE KohaNews %]
3
[% USE KohaNews %]
4
[%- USE KohaPlugins -%]
4
[%- USE KohaPlugins -%]
5
[% USE Asset %]
5
[% USE Asset %]
6
[% IF Koha.Preference( 'CookieConsent' ) %]
7
    [% USE JSConsents %]
8
[% END %]
6
[% SET opaccredits = KohaNews.get( location => "opaccredits", lang => lang, library => branchcode ) %]
9
[% SET opaccredits = KohaNews.get( location => "opaccredits", lang => lang, library => branchcode ) %]
7
[% PROCESS 'html_helpers.inc' %]
10
[% PROCESS 'html_helpers.inc' %]
8
[% UNLESS ( is_popup ) %]
11
[% UNLESS ( is_popup ) %]
Lines 117-122 Link Here
117
    [% END # /IF OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer' %]
120
    [% END # /IF OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer' %]
118
[% END # / UNLESS is_popup %]
121
[% END # / UNLESS is_popup %]
119
122
123
<!-- ConsentJS code that may run -->
124
[% IF Koha.Preference( 'CookieConsent' ) && Koha.Preference( 'ConsentJS' ).length > 0 %]
125
    [% consents = JSConsents.all() %]
126
    [% FOREACH consent IN consents %]
127
        <div class="consentCode" style="display:none" aria-hidden="true" data-consent-id="[% consent.id %]" data-consent-code="[% consent.code %]" data-requires-consent="[% consent.opacConsent ? 'true' : 'false' %]"></div>
128
    [% END %]
129
[% END %]
130
120
<!-- JavaScript includes -->
131
<!-- JavaScript includes -->
121
[% Asset.js("lib/jquery/jquery-3.4.1.min.js") | $raw %]
132
[% Asset.js("lib/jquery/jquery-3.4.1.min.js") | $raw %]
122
[% Asset.js("lib/jquery/jquery-migrate-3.1.0.min.js") | $raw %]
133
[% Asset.js("lib/jquery/jquery-migrate-3.1.0.min.js") | $raw %]
Lines 333-338 $(document).ready(function() { Link Here
333
        </script>
344
        </script>
334
    [% END %]
345
    [% END %]
335
[% END %]
346
[% END %]
347
[% IF Koha.Preference( 'CookieConsent' ) %]
348
    [% Asset.js("js/cookieconsent.js") | $raw %]
349
[% END %]
336
[% KohaPlugins.get_plugins_opac_js | $raw %]
350
[% KohaPlugins.get_plugins_opac_js | $raw %]
337
</body>
351
</body>
338
</html>
352
</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">your personal details</a></li>
25
                <a href="/cgi-bin/koha/opac-memberentry.pl">your personal details</a></li>
26
26
27
            [% IF Koha.Preference('GDPR_Policy') # remove when extending %]
27
            [% IF Koha.Preference('GDPR_Policy') || 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">your consents</a>
29
                    <a href="/cgi-bin/koha/opac-patron-consent.pl">your consents</a>
30
                </li>
30
                </li>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt (+5 lines)
Lines 66-71 Link Here
66
66
67
                    </form>
67
                    </form>
68
68
69
                    [% IF Koha.Preference('CookieConsent') %]
70
                        <h5>Cookie consents</h5>
71
                        <button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button>
72
                    [% END %]
73
69
                </div> <!-- / #userpasswd -->
74
                </div> <!-- / #userpasswd -->
70
            </div> <!-- / .col-lg-10 -->
75
            </div> <!-- / .col-lg-10 -->
71
        </div> <!-- / .row -->
76
        </div> <!-- / .row -->
(-)a/koha-tmpl/opac-tmpl/bootstrap/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
    } 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
        const consentButton = $('#cookieConsentButton');
66
        if (consentButton.length > 0) {
67
            consentButton.attr('aria-hidden', 'false');
68
            consentButton.show();
69
        }
70
    }
71
72
    // Initialise our state of selected item and enable/disable
73
    // the "Accept selected" button appropriately
74
    function initialiseSelected() {
75
        selected = [];
76
        $('.consentCheckbox').each(function () {
77
            const val = $(this).val();
78
            if (existingConsent.indexOf(val) > -1 ) {
79
                $(this).prop('checked', true);
80
                selected.push(val);
81
            } else {
82
                $(this).prop('checked', false);
83
            }
84
        });
85
        enableDisableSelectedButton();
86
    }
87
88
    // Maintain our state of selected items and enable/disable
89
    // the "Accept selected" button appropriately
90
    function maintainSelected() {
91
        selected = [];
92
        $('.consentCheckbox:checked').each(function () {
93
            selected.push($(this).val());
94
        });
95
        enableDisableSelectedButton();
96
    }
97
98
    // Set the enabled / disabled state of the
99
    // "Accept selected button based on the checkbox
100
    // states
101
    function enableDisableSelectedButton() {
102
        $('#consentAcceptSelected').prop(
103
            'disabled',
104
            selected.length === 0
105
        );
106
    }
107
108
    function runConsentedCode() {
109
        getExistingConsent();
110
        $('.consentCode').each(function () {
111
            // The user has explicitly consented to this code, or the
112
            // code doesn't require consent in the OPAC
113
            if (
114
                existingConsent.indexOf($(this).data('consent-id')) > -1 ||
115
                !$(this).data('requires-consent')
116
            ) {
117
                const code = atob($(this).data('consent-code'));
118
                const func = Function(code);
119
                func();
120
            }
121
        });
122
    }
123
124
    function addButtonHandlers() {
125
        // "Accept all" handler
126
        $('.consentAcceptAll').on('click', function(e) {
127
            e.preventDefault();
128
            let toSave = [];
129
            $('.consentCheckbox').each(function () {
130
                const val = $(this).val();
131
                toSave.push(val);
132
            });
133
            localStorage.setItem('cookieConsent', toSave);
134
            hideContainer();
135
            runConsentedCode();
136
            showYourCookies();
137
        });
138
139
        // "Accept essential" handler
140
        $('.consentAcceptEssential').on('click', function(e) {
141
            e.preventDefault();
142
            localStorage.setItem('cookieConsent', []);
143
            hideContainer();
144
            showYourCookies();
145
        });
146
147
        // "Accept selected" handler
148
        $('#consentAcceptSelected').on('click', function(e) {
149
            e.preventDefault();
150
            const toSave = selected.length > 0 ? selected : [];
151
            localStorage.setItem('cookieConsent', toSave);
152
            hideContainer();
153
            runConsentedCode();
154
            showYourCookies();
155
        });
156
157
        // "More information" handler
158
        $('#consentMoreInfo, #cookieConsentButton, #viewCookieConsents').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/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