File Coverage

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 = 'Hello';
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 .= "";
209 12         40 pop @stack;
210             }
211             }
212             elsif ($self->{encode_invalid_tags}) {
213 1         4 $output .= encode_entities("");
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 or L.
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;