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 2640-2643 $star-selected: #EDB867; Link Here
2640
    margin-bottom: 1rem;
2640
    margin-bottom: 1rem;
2641
}
2641
}
2642
2642
2643
/* Cookie consent bar */
2644
#cookieConsentBar {
2645
    display: none;
2646
    padding: 20px;
2647
    position: fixed;
2648
    bottom: 0;
2649
    left: 0;
2650
    right: 0;
2651
    background: rgba(0,0,0,0.7);
2652
    color: rgba(255,255,255,0.9);
2653
    z-index: 1;
2654
    a:link, a:visited {
2655
        color: #c5ecff;
2656
    }
2657
    #consentButtons {
2658
        margin-top: 10px;
2659
        text-align: right;
2660
    }
2661
    button:not(:last-child) {
2662
        margin-right: 10px;
2663
    }
2664
}
2665
2666
/* Cookie consent modal */
2667
#cookieConsentModal {
2668
    .modal-dialog {
2669
        max-width: 1000px;
2670
    }
2671
    .consentModalItem {
2672
        display: flex;
2673
        align-items: center;
2674
        border: 1px solid #ccc;
2675
        padding: 10px 20px;
2676
        border-radius: 5px;
2677
    }
2678
    .consentModalItem:not(:last-child) {
2679
        margin-bottom: 20px;
2680
    }
2681
    .consentItemCheckbox {
2682
        padding-right: 20px;
2683
    }
2684
    .consentItemName {
2685
        font-weight: bold;
2686
    }
2687
    #cookieConsentPopupText {
2688
        margin: 20px 0;
2689
    }
2690
}
2691
2692
#cookieConsentButton {
2693
    position: fixed;
2694
    border: 0;
2695
    bottom: 0;
2696
    right: 0;
2697
    padding: 10px;
2698
    background: rgba(0,0,0,0.75);
2699
    color: rgba(255,255,255,0.8);
2700
    z-index: 1;
2701
}
2702
2703
#viewCookieConsents {
2704
    margin-top: 10px;
2705
}
2706
2707
#patronconsents h5 {
2708
    margin-top: 30px;
2709
}
2710
2643
@import "responsive";
2711
@import "responsive";
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+62 lines)
Lines 4-9 Link Here
4
[% USE Categories %]
4
[% USE Categories %]
5
[% USE AdditionalContents %]
5
[% USE AdditionalContents %]
6
[% PROCESS 'html_helpers.inc' %]
6
[% PROCESS 'html_helpers.inc' %]
7
[% IF Koha.Preference( 'CookieConsent' ) %]
8
    [% USE JSConsents %]
9
[% END %]
7
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
10
[% SET OpacLangSelectorMode = Koha.Preference('OpacLangSelectorMode') %]
8
[% SET OpacHeader = AdditionalContents.get( location => "opacheader", lang => lang, library => branchcode, blocktitle => 0 ) %]
11
[% SET OpacHeader = AdditionalContents.get( location => "opacheader", lang => lang, library => branchcode, blocktitle => 0 ) %]
9
[% SET OpacCustomSearch = AdditionalContents.get( location => "OpacCustomSearch", lang => lang, library => branchcode, blocktitle => 0 ) %]
12
[% SET OpacCustomSearch = AdditionalContents.get( location => "OpacCustomSearch", lang => lang, library => branchcode, blocktitle => 0 ) %]
Lines 417-419 Link Here
417
            </div> <!-- /.modal-content -->
420
            </div> <!-- /.modal-content -->
418
        </div> <!-- /.modal-dialog -->
421
        </div> <!-- /.modal-dialog -->
419
    </div>  <!-- /#modalAuth  -->
422
    </div>  <!-- /#modalAuth  -->
423
    [% IF Koha.Preference( 'CookieConsent' ) %]
424
        <!-- Cookie consent bar -->
425
        <div id="cookieConsentBar" aria-hidden="true">
426
            [% Koha.Preference( 'CookieConsentBar' ) | $raw %]
427
            <div id="consentButtons">
428
                <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
429
                [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
430
                    <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
431
                    <button type="button" class="btn btn-info" id="consentMoreInfo">More information</button>
432
                [% END %]
433
            </div>
434
        </div> <!-- /#cookieConsentBar -->
435
        <!-- Cookie consent button for non logged-in users -->
436
        [% IF !loggedinusername %]
437
            <button id="cookieConsentButton" style="display:none" aria-hidden="true">
438
                Your cookies
439
            </button>
440
        [% END %]
441
        <!-- Cookie consent modal -->
442
        <div id="cookieConsentModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="cookieConsentModalLabel" aria-hidden="true">
443
            <div class="modal-dialog">
444
                <div class="modal-content">
445
                    <div class="modal-header">
446
                        <h2 class="modal-title" id="cookieConsentModalLabel">Cookie consent</h2>
447
                    </div>
448
                    <div class="modal-body">
449
                        [% IF Koha.Preference( 'CookieConsentPopup' ) %]
450
                            <div id="cookieConsentPopupText">
451
                                [% Koha.Preference( 'CookieConsentPopup' ) | $raw %]
452
                            </div>
453
                        [% END %]
454
                        <div id="consentCookieList">
455
                            [% consents = JSConsents.all() %]
456
                            [% FOREACH consent IN consents %]
457
                                <div class="consentModalItem">
458
                                    <div class="consentItemCheckbox">
459
                                        <input class="consentCheckbox" type="checkbox" name="consentCheckbox" value="[% consent.id %]">
460
                                    </div>
461
                                    <div class="consentItemMeta">
462
                                        <div class="consentItemName">[% consent.name | html %]</div>
463
                                        <div class="consentItemDescription">[% consent.description | html %]</div>
464
                                    </div>
465
                                </div>
466
                            [% END %]
467
                        </div>
468
                    </div>
469
                    <div class="modal-footer">
470
                        <div id="consentButtons">
471
                            <button type="button" class="btn btn-primary consentAcceptAll">Accept all cookies</button>
472
                            [% IF ( Koha.Preference( 'ConsentJS' ).length > 0 ) %]
473
                                <button type="button" class="btn btn-primary consentAcceptEssential">Accept only essential cookies</button>
474
                                <button type="button" class="btn btn-warning" id="consentAcceptSelected">Accept selected non-essential cookies</button>
475
                            [% END %]
476
                        </div>
477
                    </div>
478
                </div> <!-- /.modal-content -->
479
            </div> <!-- /.modal-dialog -->
480
        </div>  <!-- /#cookieConsentModal  -->
481
    [% 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 => branchcode ) %]
6
[% SET opaccredits = AdditionalContents.get( location => "opaccredits", lang => lang, library => branchcode ) %]
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 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 332-337 $(document).ready(function() { Link Here
332
        </script>
343
        </script>
333
    [% END %]
344
    [% END %]
334
[% END %]
345
[% END %]
346
[% IF Koha.Preference( 'CookieConsent' ) %]
347
    [% Asset.js("js/cookieconsent.js") | $raw %]
348
[% END %]
335
[% KohaPlugins.get_plugins_opac_js | $raw %]
349
[% KohaPlugins.get_plugins_opac_js | $raw %]
336
</body>
350
</body>
337
</html>
351
</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