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

(-)a/misc/devel/add_csp_nonces.pl (-1 / +201 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
2
3
# This file is part of Koha.
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 <https://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
add_csp_nonces.pl - Add CSP nonces to inline script tags in templates
21
22
=head1 SYNOPSIS
23
24
    # Dry run (default) - show what would be changed
25
    perl misc/devel/add_csp_nonces.pl
26
27
    # Actually modify files
28
    perl misc/devel/add_csp_nonces.pl --apply
29
30
    # Verbose output
31
    perl misc/devel/add_csp_nonces.pl --verbose
32
33
    # Process specific directory
34
    perl misc/devel/add_csp_nonces.pl --dir koha-tmpl/opac-tmpl
35
36
=head1 DESCRIPTION
37
38
This script adds CSP (Content Security Policy) nonce attributes to inline
39
<script> tags in Koha templates. This is required for CSP compliance.
40
41
The script:
42
- Finds all .tt and .inc files in koha-tmpl/
43
- Identifies inline <script> tags (those without src= attribute)
44
- Adds nonce="[% Koha.CSPNonce | $raw %]" attribute
45
- Skips tags that already have nonce attributes
46
- Skips external scripts (those with src= attribute)
47
48
=cut
49
50
use Modern::Perl;
51
use Carp qw( carp );
52
use File::Find;
53
use Getopt::Long;
54
use Pod::Usage;
55
56
my $apply   = 0;
57
my $verbose = 0;
58
my $help    = 0;
59
my $dir     = 'koha-tmpl';
60
61
GetOptions(
62
    'apply'     => \$apply,
63
    'verbose|v' => \$verbose,
64
    'help|h'    => \$help,
65
    'dir=s'     => \$dir,
66
) or pod2usage(2);
67
68
pod2usage(1) if $help;
69
70
# The nonce attribute to add
71
my $nonce_attr = 'nonce="[% Koha.CSPNonce | $raw %]"';
72
73
# Patterns to skip (library files, etc.)
74
my @skip_patterns = (
75
    qr{/lib/},             # Third-party libraries
76
    qr{/vendor/},          # Vendor files
77
    qr{\.min\.},           # Minified files
78
    qr{/node_modules/},    # Node modules
79
);
80
81
my %stats = (
82
    files_scanned  => 0,
83
    files_modified => 0,
84
    tags_modified  => 0,
85
);
86
87
sub should_skip_file {
88
    my ($file) = @_;
89
    for my $pattern (@skip_patterns) {
90
        return 1 if $file =~ $pattern;
91
    }
92
    return 0;
93
}
94
95
sub process_file {
96
    my ($file) = @_;
97
98
    return unless -f $file;
99
    return unless $file =~ /\.(tt|inc)$/;
100
    return if should_skip_file($file);
101
102
    $stats{files_scanned}++;
103
104
    open my $fh, '<:encoding(UTF-8)', $file or do {
105
        carp "Cannot read $file: $!";
106
        return;
107
    };
108
    my $content = do { local $/; <$fh> };
109
    close $fh;
110
111
    my $original = $content;
112
    my $modified = 0;
113
114
    # Match <script tags that:
115
    # - Don't have src= attribute (inline scripts)
116
    # - Don't already have nonce= attribute
117
    #
118
    # We use a callback replacement to handle each match individually
119
    $content =~ s{
120
        (<script)                    # Capture opening <script
121
        (                            # Capture existing attributes
122
            (?:
123
                \s+                  # Whitespace before attribute
124
                (?!src\s*=)          # Not followed by src=
125
                (?!nonce\s*=)        # Not followed by nonce=
126
                [a-zA-Z][\w-]*       # Attribute name
127
                (?:                  # Optional attribute value
128
                    \s*=\s*
129
                    (?:
130
                        "[^"]*"      # Double-quoted value
131
                        |'[^']*'     # Single-quoted value
132
                        |[^\s>]+     # Unquoted value
133
                    )
134
                )?
135
            )*
136
        )
137
        (\s*)                        # Whitespace before >
138
        (>)                          # Closing >
139
    }{
140
        my ($open, $attrs, $ws, $close) = ($1, $2, $3, $4);
141
142
        # Check if this tag has src= (external script - skip)
143
        if ($attrs =~ /\bsrc\s*=/i) {
144
            "$open$attrs$ws$close";  # Return unchanged
145
        }
146
        # Check if already has nonce= (skip)
147
        elsif ($attrs =~ /\bnonce\s*=/i) {
148
            "$open$attrs$ws$close";  # Return unchanged
149
        }
150
        else {
151
            $modified++;
152
            "$open $nonce_attr$attrs$ws$close";
153
        }
154
    }xsge;
155
156
    if ($modified) {
157
        $stats{files_modified}++;
158
        $stats{tags_modified} += $modified;
159
160
        if ( $verbose || !$apply ) {
161
            say "File: $file ($modified tag" . ( $modified > 1 ? 's' : '' ) . ")";
162
        }
163
164
        if ($apply) {
165
            open my $out, '>:encoding(UTF-8)', $file or do {
166
                carp "Cannot write $file: $!";
167
                return;
168
            };
169
            print $out $content;
170
            close $out;
171
            say "  Modified" if $verbose;
172
        }
173
    }
174
}
175
176
# Find and process all template files
177
find(
178
    {
179
        wanted => sub {
180
            process_file($File::Find::name);
181
        },
182
        no_chdir => 1,
183
    },
184
    $dir
185
);
186
187
# Print summary
188
say "";
189
say "=" x 60;
190
say "Summary:";
191
say "  Files scanned:  $stats{files_scanned}";
192
say "  Files modified: $stats{files_modified}";
193
say "  Tags modified:  $stats{tags_modified}";
194
say "=" x 60;
195
196
if ( !$apply && $stats{tags_modified} > 0 ) {
197
    say "";
198
    say "This was a dry run. Use --apply to actually modify files.";
199
}
200
201
exit( $stats{tags_modified} > 0 ? 0 : 1 );

Return to bug 38365