File Coverage

blib/lib/HTML/Restrict.pm
Criterion Covered Total %
statement 165 166 99.4
branch 73 84 86.9
condition 19 24 79.1
subroutine 23 23 100.0
pod 1 1 100.0
total 281 298 94.3


line stmt bran cond sub pod time code
1 16     16   534833 use strict;
  16         139  
  16         410  
2 16     16   311 use 5.006;
  16         43  
3              
4             package HTML::Restrict;
5              
6 16     16   5885 use version;
  16         27127  
  16         80  
7             our $VERSION = 'v3.0.1';
8              
9 16     16   1310 use Carp qw( croak );
  16         28  
  16         961  
10 16     16   6787 use Data::Dump qw( dump );
  16         102268  
  16         887  
11 16     16   8201 use HTML::Parser ();
  16         86496  
  16         508  
12 16     16   99 use HTML::Entities qw( encode_entities );
  16         26  
  16         917  
13 16     16   8796 use Types::Standard 1.000001 qw[ Bool HashRef ArrayRef CodeRef ];
  16         1029310  
  16         139  
14 16     16   15704 use List::Util 1.33 qw( any none );
  16         238  
  16         1593  
15 16     16   109 use Scalar::Util qw( reftype weaken );
  16         37  
  16         658  
16 16     16   6856 use Sub::Quote qw( quote_sub );
  16         66124  
  16         809  
17 16     16   8155 use URI ();
  16         63418  
  16         461  
18              
19 16     16   8383 use Moo 1.002000;
  16         153823  
  16         116  
20 16     16   27610 use namespace::clean;
  16         155293  
  16         93  
21              
22             has allow_comments => (
23             is => 'rw',
24             isa => Bool,
25             default => 0,
26             );
27              
28             has allow_declaration => (
29             is => 'rw',
30             isa => Bool,
31             default => 0,
32             );
33              
34             has debug => (
35             is => 'rw',
36             isa => Bool,
37             default => 0,
38             );
39              
40             has parser => (
41             is => 'ro',
42             lazy => 1,
43             builder => '_build_parser',
44             );
45              
46             has rules => (
47             is => 'rw',
48             isa => HashRef,
49             required => 0,
50             default => quote_sub(' {} '),
51             trigger => \&_build_parser,
52             reader => 'get_rules',
53             writer => 'set_rules',
54             );
55              
56             has strip_enclosed_content => (
57             is => 'rw',
58             isa => ArrayRef,
59             default => sub { [ 'script', 'style' ] },
60             );
61              
62             has replace_img => (
63             is => 'rw',
64             isa => Bool | CodeRef,
65             default => 0,
66             );
67              
68             has trim => (
69             is => 'rw',
70             isa => Bool,
71             default => 1,
72             );
73              
74             has filter_text => (
75             is => 'rw',
76             isa => Bool | CodeRef,
77             default => 1,
78             );
79              
80             has uri_schemes => (
81             is => 'rw',
82             isa => ArrayRef,
83             required => 0,
84             default => sub { [ undef, 'http', 'https' ] },
85             reader => 'get_uri_schemes',
86             writer => 'set_uri_schemes',
87             );
88              
89             has _processed => (
90             is => 'rw',
91             isa => quote_sub(
92             q{
93             die "$_[0] is not false or a string!"
94             unless !defined($_[0]) || $_[0] eq "" || "$_[0]" eq '0' || ref(\$_[0]) eq 'SCALAR'
95             }
96             ),
97             clearer => '_clear_processed',
98             );
99              
100             has _stripper_stack => (
101             is => 'rw',
102             isa => ArrayRef,
103             default => sub { [] },
104             );
105              
106             sub _build_parser {
107 41     41   4739 my $self = shift;
108 41         64 my $rules = shift;
109              
110             # don't allow any upper case tag or attribute names
111             # these rules would otherwise silently be ignored
112 41 100       102 if ($rules) {
113 16         29 foreach my $tag_name ( keys %{$rules} ) {
  16         53  
114 17 100       62 if ( lc $tag_name ne $tag_name ) {
115 1         120 croak 'All tag names must be lower cased';
116             }
117 16 50       75 if ( reftype $rules->{$tag_name} eq 'ARRAY' ) {
118 16         37 my @attr_names;
119 16         26 foreach my $attr_item ( @{ $rules->{$tag_name} } ) {
  16         38  
120 26 100       82 ref $attr_item eq 'HASH'
121             ? push( @attr_names, keys(%$attr_item) )
122             : push( @attr_names, $attr_item );
123             }
124 16         35 for (@attr_names) {
125 27 100       151 croak 'All attribute names must be lower cased'
126             if lc $_ ne $_;
127             }
128             }
129             }
130             }
131              
132 39         715 weaken($self);
133             return HTML::Parser->new(
134             empty_element_tags => 1,
135              
136             start_h => [
137             sub {
138 185     185   828 my ( $p, $tagname, $attr, $text ) = @_;
139 185 50       2587 print "starting tag: $tagname", "\n" if $self->debug;
140 185         1102 my $more = q{};
141              
142 185 100 100     628 if ( any { $_ eq $tagname } keys %{ $self->get_rules } ) {
  165 100       416  
  185 100       1014  
143 160 50       2168 print dump $attr if $self->debug;
144              
145 160         899 foreach my $source_type ( 'href', 'src', 'cite' ) {
146              
147 480         1042 my $link = $attr->{$source_type};
148              
149             # Remove unprintable ASCII control characters, which
150             # are 0..31. These characters are not valid in URLs,
151             # but they can prevent the URI parser from recognizing
152             # the scheme when they are used as leading characters.
153             # Browsers will helpfully ignore some of them, meaning
154             # that some of these characters (particularly 1..8 and
155             # 14..31) can be used to defeat HTML::Restrict when
156             # used as leading characters in a link. In our case we
157             # will strip them all regardless of where they are in
158             # the URL. See
159             # https://github.com/oalders/html-restrict/issues/30
160             # https://url.spec.whatwg.org/
161             # https://infra.spec.whatwg.org/#c0-control
162              
163 480 100       874 if ($link) {
164              
165             # C0 control chars (decimal 0..31)
166             # sort of like $link =~ s/[[:^print:]]//g
167 152         813 $link =~ s/[\00-\037]|&#x?0+;/ /g;
168              
169 152         535 my $url = URI->new($link);
170 152 100       76773 if ( defined $url->scheme ) {
171             delete $attr->{$source_type}
172 30         185 if none { $_ eq $url->scheme }
173 187         1193 grep { defined }
174 147 100       3126 @{ $self->get_uri_schemes };
  147         486  
175             }
176             else { # relative URL
177             delete $attr->{$source_type}
178 16         49 unless grep { !defined }
179 5 100       142 @{ $self->get_uri_schemes };
  5         16  
180             }
181             }
182             }
183              
184 160         226 foreach
185 160         466 my $attr_item ( @{ $self->get_rules->{$tagname} } ) {
186 181 100       451 if ( ref $attr_item eq 'HASH' ) {
187              
188             # validate or munge with regex or coderef contraints
189             #
190 13         38 for my $attr_name (
191             sort grep exists $attr->{$_},
192             keys %$attr_item
193             ) {
194 13         42 my $rule = $attr_item->{$attr_name};
195 13         17 my $value = $attr->{$attr_name};
196 13 100       69 if ( ref $rule eq 'CODE' ) {
    100          
197 2         5 $value = $rule->($value);
198             next
199 2 100       52 if !defined $value;
200             }
201             elsif ( $value =~ $rule ) {
202              
203             # ok
204             }
205             else {
206 3         7 next;
207             }
208 9         26 $more .= qq[ $attr_name="]
209             . encode_entities($value) . q{"};
210             }
211             }
212             else {
213 168         241 my $attr_name = $attr_item;
214 168 100       383 if ( exists $attr->{$attr_name} ) {
215             my $value
216 22         70 = encode_entities( $attr->{$attr_name} );
217 22 50       387 $more .= qq[ $attr_name="$value" ]
218             unless $attr_name eq '/';
219             }
220             }
221             }
222              
223             # closing slash should (naturally) close the tag
224 160 50 33     407 if ( exists $attr->{'/'} && $attr->{'/'} eq '/' ) {
225 0         0 $more .= ' /';
226             }
227              
228 160         342 my $elem = "<$tagname $more>";
229 160         685 $elem =~ s{\s*>}{>}gxms;
230 160         336 $elem =~ s{\s+}{ }gxms;
231              
232 160   100     2805 $self->_processed( ( $self->_processed || q{} ) . $elem );
233             }
234             elsif ( $tagname eq 'img' && $self->replace_img ) {
235 4         23 my $alt;
236 4 100       49 if ( ref $self->replace_img ) {
237 2         32 $alt = $self->replace_img->( $tagname, $attr, $text );
238             }
239             else {
240             $alt
241             = defined( $attr->{alt} )
242 2 50       12 ? ": $attr->{alt}"
243             : q{};
244 2         4 $alt = "[IMAGE$alt]";
245             }
246 4   50     66 $self->_processed( ( $self->_processed || q{} ) . $alt );
247             }
248 39         263 elsif ( any { $_ eq $tagname }
249 21         326 @{ $self->strip_enclosed_content } ) {
250 4 50       56 print "adding $tagname to strippers" if $self->debug;
251 4         23 push @{ $self->_stripper_stack }, $tagname;
  4         56  
252             }
253              
254             },
255             'self,tagname,attr,text'
256             ],
257              
258             end_h => [
259             sub {
260 180     180   6915 my ( $p, $tagname, $attr, $text ) = @_;
261 180 50       2517 print "end: $text\n" if $self->debug;
262 180 100       1311 if ( any { $_ eq $tagname } keys %{ $self->get_rules } ) {
  164 100       384  
  180         775  
263 159   50     2268 $self->_processed( ( $self->_processed || q{} ) . $text );
264             }
265 3         23 elsif ( any { $_ eq $tagname } @{ $self->_stripper_stack } ) {
  21         300  
266 3         12 $self->_delete_tag_from_stack($tagname);
267             }
268              
269             },
270             'self,tagname,attr,text'
271             ],
272              
273             text_h => [
274             sub {
275 187     187   7125 my ( $p, $text ) = @_;
276 187 100       2688 print "text: $text\n" if $self->debug;
277 187 100       3275 if ( ref $self->filter_text ) {
    100          
278 1         17 $text = $self->filter_text->($text);
279             }
280             elsif ( $self->filter_text ) {
281 183         3828 $text = _fix_text_encoding($text);
282             }
283 187 100       8249 if ( !@{ $self->_stripper_stack } ) {
  187         2614  
284 185   100     3269 $self->_processed( ( $self->_processed || q{} ) . $text );
285             }
286             },
287             'self,text'
288             ],
289              
290             comment_h => [
291             sub {
292 6     6   89 my ( $p, $text ) = @_;
293 6 50       100 print "comment: $text\n" if $self->debug;
294 6 100       107 if ( $self->allow_comments ) {
295 2   100     33 $self->_processed( ( $self->_processed || q{} ) . $text );
296             }
297             },
298             'self,text'
299             ],
300              
301             declaration_h => [
302             sub {
303 3     3   7 my ( $p, $text ) = @_;
304 3 50       47 print "declaration: $text\n" if $self->debug;
305 3 100       181 if ( $self->allow_declaration ) {
306 2   50     39 $self->_processed( ( $self->_processed || q{} ) . $text );
307             }
308             },
309 39         740 'self,text'
310             ],
311              
312             );
313             }
314              
315             sub process {
316 197     197 1 152205 my $self = shift;
317              
318             # returns undef if no value was passed
319 197 100       515 return if !@_;
320 196 100       390 return $_[0] if !$_[0];
321              
322 194         302 my ($content) = @_;
323 194 50       527 die 'content must be a string!'
324             unless ref( \$content ) eq 'SCALAR';
325 194         3896 $self->_clear_processed;
326              
327 194         3290 my $parser = $self->parser;
328 194         4026 $parser->parse($content);
329 194         6409 $parser->eof;
330              
331 194         3175 my $text = $self->_processed;
332              
333 194 100 100     3196 if ( $self->trim && $text ) {
334 184         1623 $text =~ s{\A\s*}{}gxms;
335 184         947 $text =~ s{\s*\z}{}gxms;
336             }
337 194         2988 $self->_processed($text);
338              
339             # ensure stripper stack is reset in case of broken html
340 194         5996 $self->_stripper_stack( [] );
341              
342 194         5939 return $self->_processed;
343             }
344              
345             # strip_enclosed_content tags could be nested in the source HTML, so we
346             # maintain a stack of these tags.
347              
348             sub _delete_tag_from_stack {
349 4     4   487 my $self = shift;
350 4         9 my $closing_tag = shift;
351              
352 4         8 my $found = 0;
353 4         10 my @tag_list = ();
354              
355 4         8 foreach my $tag ( reverse @{ $self->_stripper_stack } ) {
  4         54  
356 7 100 100     54 if ( $tag eq $closing_tag && $found == 0 ) {
357 4         7 $found = 1;
358 4         10 next;
359             }
360 3         4 push @tag_list, $tag;
361             }
362              
363 4         63 $self->_stripper_stack( [ reverse @tag_list ] );
364              
365 4         113 return;
366             }
367              
368             # regex for entities that don't require a terminating semicolon
369             my ($short_entity_re)
370             = map qr/$_/i,
371             join '|',
372             '#x[0-9a-f]+',
373             '#[0-9]+',
374             grep !/;\z/,
375             sort keys %HTML::Entities::entity2char;
376              
377             # semicolon required
378             my ($complete_entity_re)
379             = map qr/$_/i,
380             join '|',
381             grep /;\z/,
382             sort keys %HTML::Entities::entity2char;
383              
384             sub _fix_text_encoding {
385 183     183   297 my $text = shift;
386 183         11182 $text =~ s{
387             &
388             (?:
389             ($short_entity_re);?
390             |
391             ($complete_entity_re)
392             )?
393             }{
394 11 100       62 defined $1 ? "&$1;"
    100          
395             : defined $2 ? "&$2"
396             : "&"
397             }xgie;
398 183         636 return encode_entities( $text, '<>' );
399             }
400              
401             1; # End of HTML::Restrict
402              
403             # ABSTRACT: Strip unwanted HTML tags and attributes
404              
405             __END__