File Coverage

blib/lib/HTML/Restrict.pm
Criterion Covered Total %
statement 167 168 99.4
branch 77 88 87.5
condition 27 34 79.4
subroutine 23 23 100.0
pod 1 1 100.0
total 295 314 93.9


line stmt bran cond sub pod time code
1 17     17   628280 use strict;
  17         180  
  17         459  
2 17     17   479 use 5.006;
  17         58  
3              
4             package HTML::Restrict;
5              
6 17     17   8052 use version;
  17         34267  
  17         100  
7             our $VERSION = 'v3.0.2';
8              
9 17     17   1610 use Carp qw( croak );
  17         43  
  17         1186  
10 17     17   10399 use Data::Dump qw( dump );
  17         134108  
  17         1065  
11 17     17   10589 use HTML::Parser ();
  17         113526  
  17         675  
12 17     17   117 use HTML::Entities qw( encode_entities );
  17         28  
  17         1115  
13 17     17   10338 use Types::Standard 1.000001 qw[ Bool HashRef ArrayRef CodeRef ];
  17         1285132  
  17         193  
14 17     17   20516 use List::Util 1.33 qw( any none );
  17         287  
  17         2044  
15 17     17   151 use Scalar::Util qw( reftype weaken );
  17         38  
  17         836  
16 17     17   9085 use Sub::Quote qw( quote_sub );
  17         84083  
  17         925  
17 17     17   9941 use URI ();
  17         80620  
  17         578  
18              
19 17     17   10379 use Moo 1.002000;
  17         196238  
  17         123  
20 17     17   34523 use namespace::clean;
  17         197280  
  17         130  
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 create_newlines => (
35             is => 'rw',
36             isa => Bool,
37             default => 0,
38             );
39              
40             has debug => (
41             is => 'rw',
42             isa => Bool,
43             default => 0,
44             );
45              
46             has parser => (
47             is => 'ro',
48             lazy => 1,
49             builder => '_build_parser',
50             );
51              
52             has rules => (
53             is => 'rw',
54             isa => HashRef,
55             required => 0,
56             default => quote_sub(' {} '),
57             trigger => \&_build_parser,
58             reader => 'get_rules',
59             writer => 'set_rules',
60             );
61              
62             has strip_enclosed_content => (
63             is => 'rw',
64             isa => ArrayRef,
65             default => sub { [ 'script', 'style' ] },
66             );
67              
68             has replace_img => (
69             is => 'rw',
70             isa => Bool | CodeRef,
71             default => 0,
72             );
73              
74             has trim => (
75             is => 'rw',
76             isa => Bool,
77             default => 1,
78             );
79              
80             has filter_text => (
81             is => 'rw',
82             isa => Bool | CodeRef,
83             default => 1,
84             );
85              
86             has uri_schemes => (
87             is => 'rw',
88             isa => ArrayRef,
89             required => 0,
90             default => sub { [ undef, 'http', 'https' ] },
91             reader => 'get_uri_schemes',
92             writer => 'set_uri_schemes',
93             );
94              
95             has _processed => (
96             is => 'rw',
97             isa => quote_sub(
98             q{
99             die "$_[0] is not false or a string!"
100             unless !defined($_[0]) || $_[0] eq "" || "$_[0]" eq '0' || ref(\$_[0]) eq 'SCALAR'
101             }
102             ),
103             clearer => '_clear_processed',
104             );
105              
106             has _stripper_stack => (
107             is => 'rw',
108             isa => ArrayRef,
109             default => sub { [] },
110             );
111              
112             sub _build_parser {
113 43     43   7222 my $self = shift;
114 43         74 my $rules = shift;
115              
116             # don't allow any upper case tag or attribute names
117             # these rules would otherwise silently be ignored
118 43 100       132 if ($rules) {
119 17         44 foreach my $tag_name ( keys %{$rules} ) {
  17         75  
120 18 100       95 if ( lc $tag_name ne $tag_name ) {
121 1         187 croak 'All tag names must be lower cased';
122             }
123 17 50       95 if ( reftype $rules->{$tag_name} eq 'ARRAY' ) {
124 17         35 my @attr_names;
125 17         30 foreach my $attr_item ( @{ $rules->{$tag_name} } ) {
  17         49  
126 26 100       86 ref $attr_item eq 'HASH'
127             ? push( @attr_names, keys(%$attr_item) )
128             : push( @attr_names, $attr_item );
129             }
130 17         54 for (@attr_names) {
131 27 100       177 croak 'All attribute names must be lower cased'
132             if lc $_ ne $_;
133             }
134             }
135             }
136             }
137              
138 41         158 weaken($self);
139             return HTML::Parser->new(
140             empty_element_tags => 1,
141              
142             start_h => [
143             sub {
144 195     195   1309 my ( $p, $tagname, $attr, $text ) = @_;
145 195 50       3264 print "starting tag: $tagname", "\n" if $self->debug;
146 195         1333 my $more = q{};
147              
148 195 100 100     760 if ( any { $_ eq $tagname } keys %{ $self->get_rules } ) {
  167 100 100     478  
  195 100 100     1380  
    100          
    100          
149 161 50       2850 print dump $attr if $self->debug;
150              
151 161         1030 foreach my $source_type ( 'href', 'src', 'cite' ) {
152              
153 483         1190 my $link = $attr->{$source_type};
154              
155             # Remove unprintable ASCII control characters, which
156             # are 0..31. These characters are not valid in URLs,
157             # but they can prevent the URI parser from recognizing
158             # the scheme when they are used as leading characters.
159             # Browsers will helpfully ignore some of them, meaning
160             # that some of these characters (particularly 1..8 and
161             # 14..31) can be used to defeat HTML::Restrict when
162             # used as leading characters in a link. In our case we
163             # will strip them all regardless of where they are in
164             # the URL. See
165             # https://github.com/oalders/html-restrict/issues/30
166             # https://url.spec.whatwg.org/
167             # https://infra.spec.whatwg.org/#c0-control
168              
169 483 100       957 if ($link) {
170              
171             # C0 control chars (decimal 0..31)
172             # sort of like $link =~ s/[[:^print:]]//g
173 152         1034 $link =~ s/[\00-\037]|&#x?0+;/ /g;
174              
175 152         601 my $url = URI->new($link);
176 152 100       84418 if ( defined $url->scheme ) {
177             delete $attr->{$source_type}
178 30         284 if none { $_ eq $url->scheme }
179 187         1323 grep { defined }
180 147 100       3529 @{ $self->get_uri_schemes };
  147         500  
181             }
182             else { # relative URL
183             delete $attr->{$source_type}
184 16         58 unless grep { !defined }
185 5 100       162 @{ $self->get_uri_schemes };
  5         17  
186             }
187             }
188             }
189              
190 161         225 foreach
191 161         471 my $attr_item ( @{ $self->get_rules->{$tagname} } ) {
192 181 100       515 if ( ref $attr_item eq 'HASH' ) {
193              
194             # validate or munge with regex or coderef contraints
195             #
196 13         45 for my $attr_name (
197             sort grep exists $attr->{$_},
198             keys %$attr_item
199             ) {
200 13         46 my $rule = $attr_item->{$attr_name};
201 13         19 my $value = $attr->{$attr_name};
202 13 100       61 if ( ref $rule eq 'CODE' ) {
    100          
203 2         6 $value = $rule->($value);
204             next
205 2 100       59 if !defined $value;
206             }
207             elsif ( $value =~ $rule ) {
208              
209             # ok
210             }
211             else {
212 3         8 next;
213             }
214 9         28 $more .= qq[ $attr_name="]
215             . encode_entities($value) . q{"};
216             }
217             }
218             else {
219 168         242 my $attr_name = $attr_item;
220 168 100       431 if ( exists $attr->{$attr_name} ) {
221             my $value
222 22         69 = encode_entities( $attr->{$attr_name} );
223 22 50       416 $more .= qq[ $attr_name="$value" ]
224             unless $attr_name eq '/';
225             }
226             }
227             }
228              
229             # closing slash should (naturally) close the tag
230 161 50 33     442 if ( exists $attr->{'/'} && $attr->{'/'} eq '/' ) {
231 0         0 $more .= ' /';
232             }
233              
234 161         372 my $elem = "<$tagname $more>";
235 161         748 $elem =~ s{\s*>}{>}gxms;
236 161         376 $elem =~ s{\s+}{ }gxms;
237              
238 161   100     3449 $self->_processed( ( $self->_processed || q{} ) . $elem );
239             }
240             elsif ( $tagname eq 'img' && $self->replace_img ) {
241 4         30 my $alt;
242 4 100       58 if ( ref $self->replace_img ) {
243 2         39 $alt = $self->replace_img->( $tagname, $attr, $text );
244             }
245             else {
246             $alt
247             = defined( $attr->{alt} )
248 2 50       19 ? ": $attr->{alt}"
249             : q{};
250 2         5 $alt = "[IMAGE$alt]";
251             }
252 4   50     83 $self->_processed( ( $self->_processed || q{} ) . $alt );
253             }
254             elsif ( $tagname eq 'br' && $self->create_newlines ) {
255 4   50     633 $self->_processed( ( $self->_processed || q{} ) . "\n" );
256             }
257             elsif ( $tagname eq 'p' && $self->create_newlines ) {
258 3   50     73 $self->_processed(
259             ( $self->_processed || q{} ) . "\n\n" );
260             }
261 43         423 elsif ( any { $_ eq $tagname }
262 23         483 @{ $self->strip_enclosed_content } ) {
263 4 50       67 print "adding $tagname to strippers" if $self->debug;
264 4         35 push @{ $self->_stripper_stack }, $tagname;
  4         88  
265             }
266              
267             },
268             'self,tagname,attr,text'
269             ],
270              
271             end_h => [
272             sub {
273 183     183   8454 my ( $p, $tagname, $attr, $text ) = @_;
274 183 50       3033 print "end: $text\n" if $self->debug;
275 183 100       1475 if ( any { $_ eq $tagname } keys %{ $self->get_rules } ) {
  164 100       424  
  183         833  
276 159   50     2623 $self->_processed( ( $self->_processed || q{} ) . $text );
277             }
278 3         52 elsif ( any { $_ eq $tagname } @{ $self->_stripper_stack } ) {
  24         400  
279 3         11 $self->_delete_tag_from_stack($tagname);
280             }
281              
282             },
283             'self,tagname,attr,text'
284             ],
285              
286             text_h => [
287             sub {
288 205     205   9121 my ( $p, $text ) = @_;
289 205 100       3579 print "text: $text\n" if $self->debug;
290 205 100       4394 if ( ref $self->filter_text ) {
    100          
291 1         22 $text = $self->filter_text->($text);
292             }
293             elsif ( $self->filter_text ) {
294 201         4929 $text = _fix_text_encoding($text);
295             }
296 205 100       10043 if ( !@{ $self->_stripper_stack } ) {
  205         3229  
297 203   100     4296 $self->_processed( ( $self->_processed || q{} ) . $text );
298             }
299             },
300             'self,text'
301             ],
302              
303             comment_h => [
304             sub {
305 6     6   106 my ( $p, $text ) = @_;
306 6 50       101 print "comment: $text\n" if $self->debug;
307 6 100       128 if ( $self->allow_comments ) {
308 2   100     39 $self->_processed( ( $self->_processed || q{} ) . $text );
309             }
310             },
311             'self,text'
312             ],
313              
314             declaration_h => [
315             sub {
316 3     3   8 my ( $p, $text ) = @_;
317 3 50       53 print "declaration: $text\n" if $self->debug;
318 3 100       170 if ( $self->allow_declaration ) {
319 2   50     50 $self->_processed( ( $self->_processed || q{} ) . $text );
320             }
321             },
322 41         969 'self,text'
323             ],
324              
325             );
326             }
327              
328             sub process {
329 201     201 1 176767 my $self = shift;
330              
331             # returns undef if no value was passed
332 201 100       621 return if !@_;
333 200 100       584 return $_[0] if !$_[0];
334              
335 198         400 my ($content) = @_;
336 198 50       600 die 'content must be a string!'
337             unless ref( \$content ) eq 'SCALAR';
338 198         4636 $self->_clear_processed;
339              
340 198         4007 my $parser = $self->parser;
341 198         4835 $parser->parse($content);
342 198         8173 $parser->eof;
343              
344 198         4172 my $text = $self->_processed;
345              
346 198 100 100     3920 if ( $self->trim && $text ) {
347 188         1962 $text =~ s{\A\s*}{}gxms;
348 188         1152 $text =~ s{\s*\z}{}gxms;
349             }
350 198         3330 $self->_processed($text);
351              
352             # ensure stripper stack is reset in case of broken html
353 198         7735 $self->_stripper_stack( [] );
354              
355 198         7147 return $self->_processed;
356             }
357              
358             # strip_enclosed_content tags could be nested in the source HTML, so we
359             # maintain a stack of these tags.
360              
361             sub _delete_tag_from_stack {
362 4     4   622 my $self = shift;
363 4         8 my $closing_tag = shift;
364              
365 4         9 my $found = 0;
366 4         10 my @tag_list = ();
367              
368 4         9 foreach my $tag ( reverse @{ $self->_stripper_stack } ) {
  4         68  
369 7 100 100     75 if ( $tag eq $closing_tag && $found == 0 ) {
370 4         8 $found = 1;
371 4         15 next;
372             }
373 3         6 push @tag_list, $tag;
374             }
375              
376 4         73 $self->_stripper_stack( [ reverse @tag_list ] );
377              
378 4         134 return;
379             }
380              
381             # regex for entities that don't require a terminating semicolon
382             my ($short_entity_re)
383             = map qr/$_/i,
384             join '|',
385             '#x[0-9a-f]+',
386             '#[0-9]+',
387             grep !/;\z/,
388             sort keys %HTML::Entities::entity2char;
389              
390             # semicolon required
391             my ($complete_entity_re)
392             = map qr/$_/i,
393             join '|',
394             grep /;\z/,
395             sort keys %HTML::Entities::entity2char;
396              
397             sub _fix_text_encoding {
398 201     201   407 my $text = shift;
399 201         14457 $text =~ s{
400             &
401             (?:
402             ($short_entity_re);?
403             |
404             ($complete_entity_re)
405             )?
406             }{
407 11 100       83 defined $1 ? "&$1;"
    100          
408             : defined $2 ? "&$2"
409             : "&"
410             }xgie;
411 201         766 return encode_entities( $text, '<>' );
412             }
413              
414             1; # End of HTML::Restrict
415              
416             # ABSTRACT: Strip unwanted HTML tags and attributes
417              
418             __END__