| blib/lib/HTML/Purifier.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 71 | 72 | 98.6 |
| branch | 18 | 20 | 90.0 |
| condition | 12 | 20 | 60.0 |
| subroutine | 12 | 12 | 100.0 |
| pod | 2 | 2 | 100.0 |
| total | 115 | 126 | 91.2 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package HTML::Purifier; | ||||||
| 2 | |||||||
| 3 | 1 | 1 | 228403 | use strict; | |||
| 1 | 1 | ||||||
| 1 | 30 | ||||||
| 4 | 1 | 1 | 3 | use warnings; | |||
| 1 | 2 | ||||||
| 1 | 64 | ||||||
| 5 | |||||||
| 6 | 1 | 1 | 584 | use HTML::Parser; | |||
| 1 | 5746 | ||||||
| 1 | 38 | ||||||
| 7 | 1 | 1 | 5 | use HTML::Entities qw(encode_entities); | |||
| 1 | 3 | ||||||
| 1 | 64 | ||||||
| 8 | 1 | 1 | 539 | use Params::Get; | |||
| 1 | 10586 | ||||||
| 1 | 61 | ||||||
| 9 | 1 | 1 | 687 | use Params::Validate::Strict; | |||
| 1 | 40434 | ||||||
| 1 | 811 | ||||||
| 10 | |||||||
| 11 | our $VERSION = '0.01'; | ||||||
| 12 | |||||||
| 13 | =head1 NAME | ||||||
| 14 | |||||||
| 15 | HTML::Purifier - Basic HTML purification | ||||||
| 16 | |||||||
| 17 | =head1 VERSION | ||||||
| 18 | |||||||
| 19 | Version 0.01 | ||||||
| 20 | |||||||
| 21 | =head1 DESCRIPTION | ||||||
| 22 | |||||||
| 23 | HTML::Purifier provides basic HTML purification capabilities. | ||||||
| 24 | It allows you to define a whitelist of allowed tags and attributes, and it removes or encodes any HTML that is not on the whitelist. | ||||||
| 25 | This helps to prevent cross-site scripting (XSS) vulnerabilities. | ||||||
| 26 | |||||||
| 27 | =head1 SYNOPSIS | ||||||
| 28 | |||||||
| 29 | =head2 Basic Usage | ||||||
| 30 | |||||||
| 31 | use HTML::Purifier; | ||||||
| 32 | |||||||
| 33 | my $purifier = HTML::Purifier->new( | ||||||
| 34 | allow_tags => [qw(p b i a)], | ||||||
| 35 | allow_attributes => { | ||||||
| 36 | a => [qw(href title)], | ||||||
| 37 | }, | ||||||
| 38 | ); | ||||||
| 39 | |||||||
| 40 | my $input_html = ' Hello, world '; |
||||||
| 41 | my $purified_html = $purifier->purify($input_html); | ||||||
| 42 | |||||||
| 43 | print $purified_html; # Output: Hello, world |
||||||
| 44 | |||||||
| 45 | =head2 Allowing Comments | ||||||
| 46 | |||||||
| 47 | use HTML::Purifier; | ||||||
| 48 | |||||||
| 49 | my $purifier = HTML::Purifier->new( | ||||||
| 50 | allow_tags => [qw(p b i a)], | ||||||
| 51 | allow_attributes => { | ||||||
| 52 | a => [qw(href title)], | ||||||
| 53 | }, | ||||||
| 54 | strip_comments => 0, # Do not strip comments | ||||||
| 55 | ); | ||||||
| 56 | |||||||
| 57 | my $input_html = ' Hello, '; |
||||||
| 58 | my $purified_html = $purifier->purify($input_html); | ||||||
| 59 | |||||||
| 60 | print $purified_html; # Output: Hello, |
||||||
| 61 | |||||||
| 62 | =head2 Encoding Invalid Tags | ||||||
| 63 | |||||||
| 64 | use HTML::Purifier; | ||||||
| 65 | |||||||
| 66 | my $ourified = HTML::Purifier->new( | ||||||
| 67 | allow_tags => [qw(p b i a)], | ||||||
| 68 | allow_attributes => { | ||||||
| 69 | a => [qw(href title)], | ||||||
| 70 | }, | ||||||
| 71 | encode_invalid_tags => 1, # Encode invalid tags. | ||||||
| 72 | ); | ||||||
| 73 | |||||||
| 74 | my $input_html = ' |
||||||
| 75 | my $purified_html = $purifier->purify($input_html); | ||||||
| 76 | |||||||
| 77 | print $purified_html; # Output: <my-custom-tag>Hello</my-custom-tag> | ||||||
| 78 | |||||||
| 79 | =head1 METHODS | ||||||
| 80 | |||||||
| 81 | =head2 new(%args) | ||||||
| 82 | |||||||
| 83 | Creates a new HTML::Purifier object. | ||||||
| 84 | |||||||
| 85 | =over 4 | ||||||
| 86 | |||||||
| 87 | =item allow_tags | ||||||
| 88 | |||||||
| 89 | An array reference containing the allowed HTML tags (case-insensitive). | ||||||
| 90 | |||||||
| 91 | =item allow_attributes | ||||||
| 92 | |||||||
| 93 | A hash reference where the keys are allowed tags (lowercase), and the values are array references of allowed attributes for that tag. | ||||||
| 94 | |||||||
| 95 | =item strip_comments | ||||||
| 96 | |||||||
| 97 | A boolean value (default: 1) indicating whether HTML comments should be removed. | ||||||
| 98 | |||||||
| 99 | =item encode_invalid_tags | ||||||
| 100 | |||||||
| 101 | A boolean value (default: 1) indicating whether invalid tags should be encoded or removed. | ||||||
| 102 | |||||||
| 103 | =back | ||||||
| 104 | |||||||
| 105 | =cut | ||||||
| 106 | |||||||
| 107 | sub new { | ||||||
| 108 | 7 | 7 | 1 | 189942 | my $class = shift; | ||
| 109 | 7 | 39 | my $params = Params::Validate::Strict::validate_strict({ | ||||
| 110 | args => Params::Get::get_params(undef, \@_), | ||||||
| 111 | schema => { | ||||||
| 112 | allow_tags => { | ||||||
| 113 | type => 'arrayref', | ||||||
| 114 | optional => 1, | ||||||
| 115 | }, 'allow_attributes' => { | ||||||
| 116 | type => 'hashref', | ||||||
| 117 | optional => 1, | ||||||
| 118 | }, 'strip_comments' => { | ||||||
| 119 | type => 'boolean', | ||||||
| 120 | optional => 1, | ||||||
| 121 | }, 'encode_invalid_tags' => { | ||||||
| 122 | type => 'boolean', | ||||||
| 123 | optional => 1, | ||||||
| 124 | } | ||||||
| 125 | } | ||||||
| 126 | }); | ||||||
| 127 | |||||||
| 128 | return bless { | ||||||
| 129 | allow_tags => $params->{allow_tags} || [], | ||||||
| 130 | allow_attributes => $params->{allow_attributes} || {}, | ||||||
| 131 | strip_comments => $params->{strip_comments} // 1, # Default to stripping comments | ||||||
| 132 | 7 | 50 | 2807 | encode_invalid_tags => $params->{encode_invalid_tags} // 1, # Default to encoding invalid tags | |||
| 50 | |||||||
| 100 | |||||||
| 100 | |||||||
| 133 | }, $class; | ||||||
| 134 | } | ||||||
| 135 | |||||||
| 136 | =head2 purify($html) | ||||||
| 137 | |||||||
| 138 | Purifies the given HTML string. | ||||||
| 139 | |||||||
| 140 | =over 4 | ||||||
| 141 | |||||||
| 142 | =item $html | ||||||
| 143 | |||||||
| 144 | The HTML string to be purified. | ||||||
| 145 | |||||||
| 146 | =back | ||||||
| 147 | |||||||
| 148 | Returns the purified HTML string. | ||||||
| 149 | |||||||
| 150 | =cut | ||||||
| 151 | |||||||
| 152 | sub purify { | ||||||
| 153 | 7 | 7 | 1 | 53 | my $self = shift; | ||
| 154 | 7 | 26 | my $params = Params::Validate::Strict::validate_strict({ | ||||
| 155 | args => Params::Get::get_params('html', \@_), | ||||||
| 156 | schema => { html => { type => 'string' } } | ||||||
| 157 | }); | ||||||
| 158 | 7 | 1123 | my $html = $params->{'html'}; | ||||
| 159 | |||||||
| 160 | 7 | 15 | my $output = ''; | ||||
| 161 | 7 | 13 | my $skip_content = 0; | ||||
| 162 | 7 | 12 | my @stack; | ||||
| 163 | |||||||
| 164 | my $parser = HTML::Parser->new( | ||||||
| 165 | api_version => 3, | ||||||
| 166 | marked_sections => 1, | ||||||
| 167 | handlers => { | ||||||
| 168 | start => [ sub { | ||||||
| 169 | 15 | 15 | 94 | my ($tag, $attr, $text) = @_; | |||
| 170 | 15 | 34 | my $lc_tag = lc $tag; | ||||
| 171 | |||||||
| 172 | 15 | 100 | 66 | 81 | if ($lc_tag eq 'script' || $lc_tag eq 'style') { | ||
| 173 | 1 | 3 | $skip_content = 1; | ||||
| 174 | 1 | 3 | push @stack, $lc_tag; | ||||
| 175 | 1 | 4 | return; | ||||
| 176 | } | ||||||
| 177 | |||||||
| 178 | 14 | 100 | 23 | if (grep { lc $_ eq $lc_tag } @{$self->{allow_tags}}) { | |||
| 53 | 100 | 129 | |||||
| 14 | 39 | ||||||
| 179 | 12 | 26 | $output .= "<$lc_tag"; | ||||
| 180 | 12 | 26 | foreach my $attr_name (keys %$attr) { | ||||
| 181 | 2 | 5 | my $lc_attr = lc $attr_name; | ||||
| 182 | 2 | 50 | 33 | 10 | if (exists $self->{allow_attributes}->{$lc_tag} | ||
| 183 | 3 | 11 | && grep { lc $_ eq $lc_attr } @{$self->{allow_attributes}->{$lc_tag}}) { | ||||
| 2 | 5 | ||||||
| 184 | 2 | 23 | $output .= " $lc_attr=\"" . encode_entities($attr->{$attr_name}) . "\""; | ||||
| 185 | } | ||||||
| 186 | } | ||||||
| 187 | 12 | 81 | $output .= '>'; | ||||
| 188 | 12 | 60 | push @stack, $lc_tag; | ||||
| 189 | } | ||||||
| 190 | elsif ($self->{encode_invalid_tags}) { | ||||||
| 191 | 1 | 7 | $output .= encode_entities("<$tag" . (join " ", map {$_ . "=\"" . encode_entities($attr->{$_}) . "\""} keys %$attr) . ">"); | ||||
| 0 | 0 | ||||||
| 192 | } | ||||||
| 193 | }, "tagname, attr, text"], | ||||||
| 194 | |||||||
| 195 | end => [ sub { | ||||||
| 196 | 15 | 15 | 203 | my ($tag, $text) = @_; | |||
| 197 | 15 | 42 | my $lc_tag = lc $tag; | ||||
| 198 | |||||||
| 199 | 15 | 100 | 66 | 42 | if ($skip_content && $lc_tag eq $stack[-1]) { | ||
| 200 | 1 | 2 | pop @stack; | ||||
| 201 | 1 | 2 | $skip_content = 0; | ||||
| 202 | 1 | 4 | return; | ||||
| 203 | } | ||||||
| 204 | |||||||
| 205 | 14 | 100 | 20 | if (grep { lc $_ eq $lc_tag } @{$self->{allow_tags}}) { | |||
| 53 | 100 | 116 | |||||
| 14 | 33 | ||||||
| 206 | # Close only if it was opened | ||||||
| 207 | 12 | 50 | 33 | 52 | if ($stack[-1] && $stack[-1] eq $lc_tag) { | ||
| 208 | 12 | 27 | $output .= "$lc_tag>"; | ||||
| 209 | 12 | 40 | pop @stack; | ||||
| 210 | } | ||||||
| 211 | } | ||||||
| 212 | elsif ($self->{encode_invalid_tags}) { | ||||||
| 213 | 1 | 4 | $output .= encode_entities("$tag>"); | ||||
| 214 | } | ||||||
| 215 | }, "tagname, text"], | ||||||
| 216 | |||||||
| 217 | text => [ sub { | ||||||
| 218 | 10 | 10 | 68 | my ($text) = @_; | |||
| 219 | 10 | 100 | 33 | return if $skip_content; | |||
| 220 | 9 | 28 | $output .= encode_entities($text); | ||||
| 221 | }, "text"], | ||||||
| 222 | |||||||
| 223 | comment => [ sub { | ||||||
| 224 | 2 | 2 | 7 | my ($comment) = @_; | |||
| 225 | 2 | 100 | 11 | if (!$self->{strip_comments}) { | |||
| 226 | 1 | 6 | $comment =~ s/^$//; | ||||
| 228 | 1 | 6 | $output .= ""; | ||||
| 229 | } | ||||||
| 230 | 7 | 168 | }, "text"], | ||||
| 231 | } | ||||||
| 232 | ); | ||||||
| 233 | |||||||
| 234 | 7 | 650 | $parser->parse($html); | ||||
| 235 | 7 | 57 | $parser->eof; | ||||
| 236 | 7 | 189 | return $output; | ||||
| 237 | } | ||||||
| 238 | |||||||
| 239 | =head1 DEPENDENCIES | ||||||
| 240 | |||||||
| 241 | * HTML::Parser | ||||||
| 242 | * HTML::Entities | ||||||
| 243 | |||||||
| 244 | =head1 CAVEATS | ||||||
| 245 | |||||||
| 246 | This is a basic HTML purifier. | ||||||
| 247 | For production environments, consider using more mature and actively maintained libraries like C |
||||||
| 248 | |||||||
| 249 | =head1 SUPPORT | ||||||
| 250 | |||||||
| 251 | This module is provided as-is without any warranty. | ||||||
| 252 | |||||||
| 253 | =head1 AUTHOR | ||||||
| 254 | |||||||
| 255 | Nigel Horne C< << njh @ nigelhorne.com >> > | ||||||
| 256 | |||||||
| 257 | =head1 LICENCE AND COPYRIGHT | ||||||
| 258 | |||||||
| 259 | Copyright 2025 Nigel Horne | ||||||
| 260 | |||||||
| 261 | Usage is subject to licence terms. | ||||||
| 262 | |||||||
| 263 | The licence terms of this software are as follows: | ||||||
| 264 | |||||||
| 265 | =over 4 | ||||||
| 266 | |||||||
| 267 | =item * Personal single user, single computer use: GPL2 | ||||||
| 268 | |||||||
| 269 | =item * All other users (including Commercial, Charity, Educational, Government) | ||||||
| 270 | must apply in writing for a licence for use from Nigel Horne at the | ||||||
| 271 | above e-mail. | ||||||
| 272 | |||||||
| 273 | =back | ||||||
| 274 | |||||||
| 275 | =cut | ||||||
| 276 | |||||||
| 277 | 1; |