File Coverage

blib/lib/HTML/Scrubber.pm
Criterion Covered Total %
statement 160 163 98.1
branch 100 112 89.2
condition 10 15 66.6
subroutine 25 25 100.0
pod 11 11 100.0
total 306 326 93.8


line stmt bran cond sub pod time code
1             package HTML::Scrubber;
2              
3             # ABSTRACT: Perl extension for scrubbing/sanitizing HTML
4              
5              
6 16     16   234445 use 5.008; # enforce minimum perl version of 5.8
  16         71  
7 16     16   106 use strict;
  16         36  
  16         403  
8 16     16   90 use warnings;
  16         40  
  16         575  
9 16     16   8927 use HTML::Parser 3.47 ();
  16         103606  
  16         542  
10 16     16   145 use HTML::Entities;
  16         39  
  16         1106  
11 16     16   111 use Scalar::Util ('weaken');
  16         37  
  16         1259  
12 16     16   115 use List::Util qw(any);
  16         38  
  16         34365  
13              
14             our ( @_scrub, @_scrub_fh );
15              
16             our $VERSION = '0.16'; # TRIAL VERSION
17             our $AUTHORITY = 'cpan:NIGELM'; # AUTHORITY
18              
19             # my my my my, these here to prevent foolishness like
20             # http://perlmonks.org/index.pl?node_id=251127#Stealing+Lexicals
21             (@_scrub) = ( \&_scrub, "self, event, tagname, attr, attrseq, text" );
22             (@_scrub_fh) = ( \&_scrub_fh, "self, event, tagname, attr, attrseq, text" );
23              
24              
25             sub new {
26 23     23 1 20256 my $package = shift;
27 23         199 my $p = HTML::Parser->new(
28             api_version => 3,
29             default_h => \@_scrub,
30             marked_sections => 0,
31             strict_comment => 0,
32             unbroken_text => 1,
33             case_sensitive => 0,
34             boolean_attribute_value => undef,
35             empty_element_tags => 1,
36             );
37              
38 23         2705 my $self = {
39             _p => $p,
40             _rules => { '*' => 0, },
41             _comment => 0,
42             _process => 0,
43             _r => "",
44             _optimize => 1,
45             _script => 0,
46             _style => 0,
47             };
48              
49 23         124 $p->{"\0_s"} = bless $self, $package;
50 23         190 weaken( $p->{"\0_s"} );
51              
52 23 100       123 return $self unless @_;
53              
54 10         41 my (%args) = @_;
55              
56 10         36 for my $f (qw[ default allow deny rules process comment ]) {
57 60 100       185 next unless exists $args{$f};
58 12 100       45 if ( ref $args{$f} ) {
59 10         22 $self->$f( @{ $args{$f} } );
  10         52  
60             }
61             else {
62 2         6 $self->$f( $args{$f} );
63             }
64             }
65              
66 10         43 return $self;
67             }
68              
69              
70             sub comment {
71             return $_[0]->{_comment}
72 16 100   16 1 578 if @_ == 1;
73 7         23 $_[0]->{_comment} = $_[1];
74 7         26 return;
75             }
76              
77              
78             sub process {
79             return $_[0]->{_process}
80 10 100   10 1 73 if @_ == 1;
81 1         2 $_[0]->{_process} = $_[1];
82 1         2 return;
83             }
84              
85              
86             sub script {
87             return $_[0]->{_script}
88 3 100   3 1 389 if @_ == 1;
89 1         3 $_[0]->{_script} = $_[1];
90 1         2 return;
91             }
92              
93              
94             sub style {
95             return $_[0]->{_style}
96 3 100   3 1 14 if @_ == 1;
97 1         2 $_[0]->{_style} = $_[1];
98 1         2 return;
99             }
100              
101              
102             sub allow {
103 10     10 1 797 my $self = shift;
104 10         31 for my $k (@_) {
105 43         122 $self->{_rules}{ lc $k } = 1;
106             }
107 10         24 $self->{_optimize} = 1; # each time a rule changes, reoptimize when parse
108              
109 10         29 return;
110             }
111              
112              
113             sub deny {
114 2     2 1 10 my $self = shift;
115              
116 2         10 for my $k (@_) {
117 7         27 $self->{_rules}{ lc $k } = 0;
118             }
119              
120 2         9 $self->{_optimize} = 1; # each time a rule changes, reoptimize when parse
121              
122 2         7 return;
123             }
124              
125              
126             sub rules {
127 2     2 1 652 my $self = shift;
128 2         11 my (%rules) = @_;
129 2         9 for my $k ( keys %rules ) {
130 2         14 $self->{_rules}{ lc $k } = $rules{$k};
131             }
132              
133 2         6 $self->{_optimize} = 1; # each time a rule changes, reoptimize when parse
134              
135 2         7 return;
136             }
137              
138              
139             sub default {
140 19 100   19 1 1690 return $_[0]->{_rules}{'*'}
141             if @_ == 1;
142              
143 12 100       67 $_[0]->{_rules}{'*'} = $_[1] if defined $_[1];
144 12 100 66     86 $_[0]->{_rules}{'_'} = $_[2] if defined $_[2] and ref $_[2];
145 12         40 $_[0]->{_optimize} = 1; # each time a rule changes, reoptimize when parse
146              
147 12         35 return;
148             }
149              
150              
151             sub scrub_file {
152 2 50   2 1 2932 if ( @_ > 2 ) {
153 2 50       9 return unless defined $_[0]->_out( $_[2] );
154             }
155             else {
156 0         0 $_[0]->{_p}->handler( default => @_scrub );
157             }
158              
159 2         10 $_[0]->_optimize(); #if $_[0]->{_optimize};
160              
161 2         16 $_[0]->{_p}->parse_file( $_[1] );
162              
163 2 50       11 return delete $_[0]->{_r} unless exists $_[0]->{_out};
164 2 50       8 print { $_[0]->{_out} } $_[0]->{_r} if length $_[0]->{_r};
  0         0  
165 2         34 delete $_[0]->{_out};
166 2         9 return 1;
167             }
168              
169              
170             sub scrub {
171 63 100   63 1 19886 if ( @_ > 2 ) {
172 2 50       7 return unless defined $_[0]->_out( $_[2] );
173             }
174             else {
175 61         474 $_[0]->{_p}->handler( default => @_scrub );
176             }
177              
178 63         241 $_[0]->_optimize(); # if $_[0]->{_optimize};
179              
180 63 100       602 $_[0]->{_p}->parse( $_[1] ) if defined( $_[1] );
181 63         381 $_[0]->{_p}->eof();
182              
183 63 100       412 return delete $_[0]->{_r} unless exists $_[0]->{_out};
184 2         35 delete $_[0]->{_out};
185 2         7 return 1;
186             }
187              
188              
189             sub _out {
190 4     4   10 my ( $self, $o ) = @_;
191              
192 4 100 66     24 unless ( ref $o and ref \$o ne 'GLOB' ) {
193 2 50       86 open my $F, '>', $o or return;
194 2         8 binmode $F;
195 2         11 $self->{_out} = $F;
196             }
197             else {
198 2         6 $self->{_out} = $o;
199             }
200              
201 4         38 $self->{_p}->handler( default => @_scrub_fh );
202              
203 4         17 return 1;
204             }
205              
206              
207             sub _validate {
208 91     91   260 my ( $s, $t, $r, $a, $as ) = @_;
209 91 100       364 return "<$t>" unless %$a;
210              
211 41         93 $r = $s->{_rules}->{$r};
212 41         79 my %f;
213              
214 41         151 for my $k ( keys %$a ) {
215 61 100       189 my $check = exists $r->{$k} ? $r->{$k} : exists $r->{'*'} ? $r->{'*'} : next;
    100          
216              
217 53 100 33     296 if ( ref $check eq 'CODE' ) {
    50          
    100          
218 4         17 my @v = $check->( $s, $t, $k, $a->{$k}, $a, \%f );
219 4 100       67 next unless @v;
220 3         10 $f{$k} = shift @v;
221             }
222             elsif ( ref $check || length($check) > 1 ) {
223 0 0       0 $f{$k} = $a->{$k} if $a->{$k} =~ m{$check};
224             }
225             elsif ($check) {
226 47         125 $f{$k} = $a->{$k};
227             }
228             }
229              
230 41 100       127 if (%f) {
231 31         47 my %seen;
232             return "<$t $r>"
233             if $r = join ' ', map {
234             defined $f{$_}
235 50 100       515 ? qq[$_="] . encode_entities( $f{$_} ) . q["]
236             : $_; # boolean attribute (TODO?)
237 31 100       71 } grep { exists $f{$_} and !$seen{$_}++; } @$as;
  55 50       266  
238             }
239              
240 10         57 return "<$t>";
241             }
242              
243              
244             sub _scrub_str {
245 504     504   1309 my ( $p, $e, $t, $a, $as, $text ) = @_;
246              
247 504         987 my $s = $p->{"\0_s"};
248 504         958 my $outstr = '';
249              
250 504 100 66     2511 if ( $e eq 'start' ) {
    100          
    100          
    100          
    100          
    100          
251 109 100       380 if ( exists $s->{_rules}->{$t} ) # is there a specific rule
    100          
252             {
253 61 100       216 if ( ref $s->{_rules}->{$t} ) # is it complicated?(not simple;)
    50          
254             {
255 5         17 $outstr .= $s->_validate( $t, $t, $a, $as );
256             }
257             elsif ( $s->{_rules}->{$t} ) # validate using default attribute rule
258             {
259 56         146 $outstr .= $s->_validate( $t, '_', $a, $as );
260             }
261             }
262             elsif ( $s->{_rules}->{'*'} ) # default allow tags
263             {
264 30         114 $outstr .= $s->_validate( $t, '_', $a, $as );
265             }
266             }
267             elsif ( $e eq 'end' ) {
268              
269             # empty tags list taken from
270             # https://developer.mozilla.org/en/docs/Glossary/empty_element
271 79         330 my @empty_tags = qw(area base br col embed hr img input link meta param source track wbr);
272 79 100 100 940   748 return "" if $text ne '' && any { $t eq $_ } @empty_tags; # skip false closing empty tags
  940         1878  
273              
274 63         233 my $place = 0;
275 63 100       243 if ( exists $s->{_rules}->{$t} ) {
    100          
276 27 50       85 $place = 1 if $s->{_rules}->{$t};
277             }
278             elsif ( $s->{_rules}->{'*'} ) {
279 23         55 $place = 1;
280             }
281 63 100       188 if ($place) {
282 50 100       138 if ( length $text ) {
283 46         185 $outstr .= "";
284             }
285             else {
286 4         14 substr $s->{_r}, -1, 0, ' /';
287             }
288             }
289             }
290             elsif ( $e eq 'comment' ) {
291 14 100       50 if ( $s->{_comment} ) {
292              
293             # only copy comments through if they are well formed...
294 6 100       47 $outstr .= $text if ( $text =~ m|^$|ms );
295             }
296             }
297             elsif ( $e eq 'process' ) {
298 10 100       23 $outstr .= $text if $s->{_process};
299             }
300             elsif ( $e eq 'text' or $e eq 'default' ) {
301 162         489 $text =~ s/
302 162         350 $text =~ s/>/>/g;
303              
304 162         365 $outstr .= $text;
305             }
306             elsif ( $e eq 'start_document' ) {
307 65         144 $outstr = "";
308             }
309              
310 488         3698 return $outstr;
311             }
312              
313              
314             sub _scrub_fh {
315 38     38   262 my $self = $_[0]->{"\0_s"};
316 38 100       88 print { $self->{_out} } $self->{'_r'} if length $self->{_r};
  12         34  
317 38         72 $self->{'_r'} = _scrub_str(@_);
318             }
319              
320              
321             sub _scrub {
322              
323 466     466   1482 $_[0]->{"\0_s"}->{_r} .= _scrub_str(@_);
324             }
325              
326             sub _optimize {
327 65     65   155 my ($self) = @_;
328              
329 65         149 my (@ignore_elements) = grep { not $self->{"_$_"} } qw(script style);
  130         489  
330 65         323 $self->{_p}->ignore_elements(@ignore_elements); # if @ is empty, we reset ;)
331              
332 65 100       279 return unless $self->{_optimize};
333              
334             #sub allow
335             # return unless $self->{_optimize}; # till I figure it out (huh)
336              
337 26 100       152 if ( $self->{_rules}{'*'} ) { # default allow
338 8         35 $self->{_p}->report_tags(); # so clear it
339             }
340             else {
341              
342             my (@reports) =
343             grep { # report only tags we want
344 76         186 $self->{_rules}{$_}
345 18         55 } keys %{ $self->{_rules} };
  18         81  
346              
347             $self->{_p}->report_tags( # default deny, so optimize
348             @reports
349 18 100       121 ) if @reports;
350             }
351              
352             # sub deny
353             # return unless $self->{_optimize}; # till I figure it out (huh)
354             my (@ignores) =
355 26         84 grep { not $self->{_rules}{$_} } grep { $_ ne '*' } keys %{ $self->{_rules} };
  62         173  
  88         245  
  26         103  
356              
357             $self->{_p}->ignore_tags( # always ignore stuff we don't want
358             @ignores
359 26 100       154 ) if @ignores;
360              
361 26         72 $self->{_optimize} = 0;
362 26         95 return;
363             }
364              
365             1;
366              
367             #print sprintf q[ '%-12s => %s,], "$_'", $h{$_} for sort keys %h;# perl!
368             #perl -ne"chomp;print $_;print qq'\t\t# test ', ++$a if /ok\(/;print $/" test.pl >test2.pl
369             #perl -ne"chomp;print $_;if( /ok\(/ ){s/\#test \d+$//;print qq'\t\t# test ', ++$a }print $/" test.pl >test2.pl
370             #perl -ne"chomp;if(/ok\(/){s/# test .*$//;print$_,qq'\t\t# test ',++$a}else{print$_}print$/" test.pl >test2.pl
371              
372             __END__