File Coverage

blib/lib/Mail/SpamAssassin/HTML.pm
Criterion Covered Total %
statement 381 479 79.5
branch 197 314 62.7
condition 82 155 52.9
subroutine 34 37 91.8
pod 2 25 8.0
total 696 1010 68.9


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             # HTML decoding TODOs
19             # - add URIs to list for faster URI testing
20              
21              
22             use strict;
23 41     41   227 use warnings;
  41         65  
  41         1175  
24 41     41   209 use re 'taint';
  41         72  
  41         1186  
25 41     41   218  
  41         69  
  41         1732  
26             require 5.008; # need basic Unicode support for HTML::Parser::utf8_mode
27             # require 5.008008; # Bug 3787; [perl #37950]: Malformed UTF-8 character ...
28              
29             use HTML::Parser 3.43 ();
30 41     41   20425 use Mail::SpamAssassin::Logger;
  41         205064  
  41         1278  
31 41     41   283 use Mail::SpamAssassin::Constants qw(:sa);
  41         66  
  41         2287  
32 41     41   205 use Mail::SpamAssassin::Util qw(untaint_var);
  41         64  
  41         5104  
33 41     41   240  
  41         61  
  41         234889  
34             our @ISA = qw(HTML::Parser);
35              
36             # elements defined by the HTML 4.01 and XHTML 1.0 DTDs (do not change them!)
37             # does not include XML
38             my %elements = map {; $_ => 1 }
39             # strict
40             qw( a abbr acronym address area b base bdo big blockquote body br button caption cite code col colgroup dd del dfn div dl dt em fieldset form h1 h2 h3 h4 h5 h6 head hr html i img input ins kbd label legend li link map meta noscript object ol optgroup option p param pre q samp script select small span strong style sub sup table tbody td textarea tfoot th thead title tr tt ul var ),
41             # loose
42             qw( applet basefont center dir font frame frameset iframe isindex menu noframes s strike u ),
43             # non-standard tags
44             qw( nobr x-sigsep x-tab ),
45             ;
46              
47             # elements that we want to render, but not count as valid
48             my %tricks = map {; $_ => 1 }
49             # non-standard and non-valid tags
50             qw( bgsound embed listing plaintext xmp ),
51             # other non-standard tags handled in popfile
52             # blink ilayer multicol noembed nolayer spacer wbr
53             ;
54              
55             # elements that change text style
56             my %elements_text_style = map {; $_ => 1 }
57             qw( body font table tr th td big small basefont marquee span p div a ),
58             ;
59              
60             # elements that insert whitespace
61             my %elements_whitespace = map {; $_ => 1 }
62             qw( br div li th td dt dd p hr blockquote pre embed listing plaintext xmp title
63             h1 h2 h3 h4 h5 h6 ),
64             ;
65              
66             # elements that push URIs
67             my %elements_uri = map {; $_ => 1 }
68             qw( body table tr td a area link img frame iframe embed script form base bgsound ),
69             ;
70              
71             # style attribute not accepted
72             #my %elements_no_style = map {; $_ => 1 }
73             # qw( base basefont head html meta param script style title ),
74             #;
75              
76             # permitted element attributes
77             my %ok_attributes;
78             $ok_attributes{basefont}{$_} = 1 for qw( color face size );
79             $ok_attributes{body}{$_} = 1 for qw( text bgcolor link alink vlink background );
80             $ok_attributes{font}{$_} = 1 for qw( color face size );
81             $ok_attributes{marquee}{$_} = 1 for qw( bgcolor background );
82             $ok_attributes{table}{$_} = 1 for qw( bgcolor style );
83             $ok_attributes{td}{$_} = 1 for qw( bgcolor style );
84             $ok_attributes{th}{$_} = 1 for qw( bgcolor style );
85             $ok_attributes{tr}{$_} = 1 for qw( bgcolor style );
86             $ok_attributes{span}{$_} = 1 for qw( style );
87             $ok_attributes{p}{$_} = 1 for qw( style );
88             $ok_attributes{div}{$_} = 1 for qw( style );
89             $ok_attributes{a}{$_} = 1 for qw( style );
90              
91             my ($class, $character_semantics_input, $character_semantics_output) = @_;
92             my $self = $class->SUPER::new(
93 8     8 1 56 api_version => 3,
94 8         275 handlers => [
95             start_document => ["html_start", "self"],
96             start => ["html_tag", "self,tagname,attr,'+1'"],
97             end_document => ["html_end", "self"],
98             end => ["html_tag", "self,tagname,attr,'-1'"],
99             text => ["html_text", "self,dtext"],
100             comment => ["html_comment", "self,text"],
101             declaration => ["html_declaration", "self,text"],
102             ],
103             marked_sections => 1);
104             $self->{SA_character_semantics_input} = $character_semantics_input;
105             $self->{SA_encode_results} =
106 8         1163 $character_semantics_input && !$character_semantics_output;
107             $self;
108 8   33     38 }
109 8         26  
110             my ($self) = @_;
111              
112             # trigger HTML_MESSAGE
113 8     8 0 28 $self->put_results(html => 1);
114              
115             # initial display attributes
116 8         47 $self->{basefont} = 3;
117             my %default = (tag => "default",
118             fgcolor => "#000000",
119 8         28 bgcolor => "#ffffff",
120             size => $self->{basefont});
121             push @{ $self->{text_style} }, \%default;
122             }
123 8         58  
124 8         17 my ($self) = @_;
  8         200  
125              
126             delete $self->{text_style};
127              
128 8     8 0 24 my @uri;
129              
130 8         39 # add the canonicalized version of each uri to the detail list
131             if (defined $self->{uri}) {
132 8         18 @uri = keys %{$self->{uri}};
133             }
134              
135 8 50       28 # these keep backward compatibility, albeit a little wasteful
136 8         16 $self->put_results(uri => \@uri);
  8         97  
137             $self->put_results(anchor => $self->{anchor});
138              
139             $self->put_results(uri_detail => $self->{uri});
140 8         40 $self->put_results(uri_truncated => $self->{uri_truncated});
141 8         29  
142             # final results scalars
143 8         37 $self->put_results(image_area => $self->{image_area});
144 8         47 $self->put_results(length => $self->{length});
145             $self->put_results(min_size => $self->{min_size});
146             $self->put_results(max_size => $self->{max_size});
147 8         43 if (exists $self->{tags}) {
148 8         40 $self->put_results(closed_extra_ratio =>
149 8         29 ($self->{closed_extra} / $self->{tags}));
150 8         30 }
151 8 50       29  
152             # final result arrays
153 8         47 $self->put_results(comment => $self->{comment});
154             $self->put_results(script => $self->{script});
155             $self->put_results(title => $self->{title});
156              
157 8         46 # final result hashes
158 8         44 $self->put_results(inside => $self->{inside});
159 8         44  
160             # end-of-document result values that don't require looking at the text
161             if (exists $self->{backhair}) {
162 8         35 $self->put_results(backhair_count => scalar keys %{ $self->{backhair} });
163             }
164             if (exists $self->{elements} && exists $self->{tags}) {
165 8 50       32 $self->put_results(bad_tag_ratio =>
166 0         0 ($self->{tags} - $self->{elements}) / $self->{tags});
  0         0  
167             }
168 8 50 33     114 if (exists $self->{elements_seen} && exists $self->{tags_seen}) {
169             $self->put_results(non_element_ratio =>
170 8         52 ($self->{tags_seen} - $self->{elements_seen}) /
171             $self->{tags_seen});
172 8 50 33     75 }
173             if (exists $self->{tags} && exists $self->{obfuscation}) {
174             $self->put_results(obfuscation_ratio =>
175 8         47 $self->{obfuscation} / $self->{tags});
176             }
177 8 50 33     98 }
178              
179 0         0 my $self = shift;
180             my %results = @_;
181              
182             while (my ($k, $v) = each %results) {
183             $self->{results}{$k} = $v;
184 132     132 0 159 }
185 132         274 }
186              
187 132         360 my ($self) = @_;
188 132         459  
189             return $self->{results};
190             }
191              
192             my $self = shift;
193 8     8 0 23 my %options = @_;
194              
195 8         26 return join('', @{ $self->{text} }) unless %options;
196              
197             my $mask;
198             while (my ($k, $v) = each %options) {
199 24     24 0 39 next if !defined $self->{"text_$k"};
200 24         52 if (!defined $mask) {
201             $mask |= $v ? $self->{"text_$k"} : ~ $self->{"text_$k"};
202 24 100       60 }
  8         136  
203             else {
204 16         28 $mask &= $v ? $self->{"text_$k"} : ~ $self->{"text_$k"};
205 16         58 }
206 16 50       71 }
207 16 50       53  
208 16 100       112 my $text = '';
209             my $i = 0;
210             for (@{ $self->{text} }) { $text .= $_ if vec($mask, $i++, 1); }
211 0 0       0 return $text;
212             }
213              
214             my ($self, $text) = @_;
215 16         53  
216 16         29 $self->{image_area} = 0;
217 16 100       23 $self->{title_index} = -1;
  16         40  
  1726         2547  
218 16         83 $self->{max_size} = 3; # start at default size
219             $self->{min_size} = 3; # start at default size
220             $self->{closed_html} = 0;
221             $self->{closed_body} = 0;
222 8     8 1 49 $self->{closed_extra} = 0;
223             $self->{text} = []; # rendered text
224 8         30 $self->{length} += untaint_var(length($text));
225 8         25  
226 8         27 # NOTE: We *only* need to fix the rendering when we verify that it
227 8         20 # differs from what people see in their MUA. Testing is best done with
228 8         23 # the most common MUAs and browsers, if you catch my drift.
229 8         20  
230 8         20 # NOTE: HTML::Parser can cope with: <?xml pis>, <? with space>, so we
231 8         28 # don't need to fix them here.
232 8         58  
233             # # (outdated claim) HTML::Parser converts &nbsp; into a question mark ("?")
234             # # for some reason, so convert them to spaces. Confirmed in 3.31, at least.
235             # ... Actually it doesn't, it is correctly converted into Unicode NBSP,
236             # nevertheless it does not hurt to treat it as a space.
237             $text =~ s/&nbsp;/ /g;
238              
239             # bug 4695: we want "<br/>" to be treated the same as "<br>", and
240             # the HTML::Parser API won't do it for us
241             $text =~ s/<(\w+)\s*\/>/<$1>/gi;
242              
243             if (!$self->UNIVERSAL::can('utf8_mode')) {
244             # utf8_mode is cleared by default, only warn if it would need to be set
245 8         69 warn "message: cannot set utf8_mode, module HTML::Parser is too old\n"
246             if !$self->{SA_character_semantics_input};
247             } else {
248             $self->SUPER::utf8_mode($self->{SA_character_semantics_input} ? 0 : 1);
249 8         189 my $utf8_mode = $self->SUPER::utf8_mode;
250             dbg("message: HTML::Parser utf8_mode %s",
251 8 50       92 $utf8_mode ? "on (assumed UTF-8 octets)"
252             : "off (default, assumed Unicode characters)");
253             }
254 0 0       0  
255             eval {
256 8 50       62 local $SIG{__WARN__} = sub {
257 8         31 my $err = $_[0];
258 8 50       64 $err =~ s/\s+/ /gs; $err =~ s/(.*) at .*/$1/s;
259             info("message: HTML::Parser warning: $err");
260             };
261             $self->SUPER::parse($text);
262             };
263 8         21  
264             # bug 7437: deal gracefully with HTML::Parser misbehavior on unclosed <style> and <script> tags
265 0     0   0 # (typically from not passing the entire message to spamc, but possibly a DoS attack)
266 0         0 $self->SUPER::parse("</style>") while exists $self->{inside}{style} && $self->{inside}{style} > 0;
  0         0  
267 0         0 $self->SUPER::parse("</script>") while exists $self->{inside}{script} && $self->{inside}{script} > 0;
268 8         116  
269 8         117 $self->SUPER::eof;
270              
271             return $self->{text};
272             }
273              
274 8   66     58 my ($self, $tag, $attr, $num) = @_;
275 8   33     42 utf8::encode($tag) if $self->{SA_encode_results};
276              
277 8         80 my $maybe_namespace = ($tag =~ m@^(?:o|st\d):[\w-]+/?$@);
278              
279 8         42 if (exists $elements{$tag} || $maybe_namespace) {
280             $self->{elements}++;
281             $self->{elements_seen}++ if !exists $self->{inside}{$tag};
282             }
283 1001     1001 0 2096 $self->{tags}++;
284 1001 50       1638 $self->{tags_seen}++ if !exists $self->{inside}{$tag};
285             $self->{inside}{$tag} += $num;
286 1001         1485 if ($self->{inside}{$tag} < 0) {
287             $self->{inside}{$tag} = 0;
288 1001 50 33     2160 $self->{closed_extra}++;
289 1001         1305 }
290 1001 100       1739  
291             return if $maybe_namespace;
292 1001         1138  
293 1001 100       1551 # ignore non-elements
294 1001         1808 if (exists $elements{$tag} || exists $tricks{$tag}) {
295 1001 100       1916 $self->text_style($tag, $attr, $num) if exists $elements_text_style{$tag};
296 17         28  
297 17         22 # bug 5009: things like <p> and </p> both need dealing with
298             $self->html_whitespace($tag) if exists $elements_whitespace{$tag};
299              
300 1001 50       1489 # start tags
301             if ($num == 1) {
302             $self->html_uri($tag, $attr) if exists $elements_uri{$tag};
303 1001 50 33     2159 $self->html_tests($tag, $attr, $num);
304 1001 100       2643 }
305             # end tags
306             else {
307 1001 100       2531 $self->{closed_html} = 1 if $tag eq "html";
308             $self->{closed_body} = 1 if $tag eq "body";
309             }
310 1001 100       1819 }
311 553 100       1339 }
312 553         1187  
313             my ($self, $tag) = @_;
314              
315             # ordered by frequency of tag groups, note: whitespace is always "visible"
316 448 100       733 if ($tag eq "br" || $tag eq "div") {
317 448 100       2635 $self->display_text("\n", whitespace => 1);
318             }
319             elsif ($tag =~ /^(?:li|t[hd]|d[td]|embed|h\d)$/) {
320             $self->display_text(" ", whitespace => 1);
321             }
322             elsif ($tag =~ /^(?:p|hr|blockquote|pre|listing|plaintext|xmp|title)$/) {
323 202     202 0 336 $self->display_text("\n\n", whitespace => 1);
324             }
325             }
326 202 100 100     884  
    100          
    50          
327 93         184 # puts the uri onto the internal array
328             # note: uri may be blank (<a href=""></a> obfuscation, etc.)
329             my ($self, $type, $uri) = @_;
330 15         26  
331             $uri = $self->canon_uri($uri);
332             utf8::encode($uri) if $self->{SA_encode_results};
333 94         200  
334             my $target = target_uri($self->{base_href} || "", $uri);
335              
336             # skip things like <iframe src="" ...>
337             $self->{uri}->{$uri}->{types}->{$type} = 1 if $uri ne '';
338             }
339              
340 301     301 0 449 my ($self, $uri) = @_;
341              
342 301         432 # URIs don't have leading/trailing whitespace ...
343 301 50       563 $uri =~ s/^\s+//;
344             $uri =~ s/\s+$//;
345 301   50     922  
346             # Make sure all the URIs are nice and short
347             if (length $uri > MAX_URI_LENGTH) {
348 301 50       1373 $self->{'uri_truncated'} = 1;
349             $uri = substr $uri, 0, MAX_URI_LENGTH;
350             }
351              
352 504     504 0 691 return $uri;
353             }
354              
355 504         922 my ($self, $tag, $attr) = @_;
356 504         731  
357             # ordered by frequency of tag groups
358             if ($tag =~ /^(?:body|table|tr|td)$/) {
359 504 50       831 if (defined $attr->{background}) {
360 0         0 $self->push_uri($tag, $attr->{background});
361 0         0 }
362             }
363             elsif ($tag =~ /^(?:a|area|link)$/) {
364 504         892 if (defined $attr->{href}) {
365             $self->push_uri($tag, $attr->{href});
366             }
367             if (defined $attr->{'data-saferedirecturl'}) {
368 235     235 0 441 $self->push_uri($tag, $attr->{'data-saferedirecturl'});
369             }
370             }
371 235 100       971 elsif ($tag =~ /^(?:img|frame|iframe|embed|script|bgsound)$/) {
    100          
    50          
    0          
    0          
372 25 100       77 if (defined $attr->{src}) {
373 6         15 $self->push_uri($tag, $attr->{src});
374             }
375             }
376             elsif ($tag eq "form") {
377 203 50       422 if (defined $attr->{action}) {
378 203         450 $self->push_uri($tag, $attr->{action});
379             }
380 203 100       428 }
381 85         146 elsif ($tag eq "base") {
382             if (my $uri = $attr->{href}) {
383             $uri = $self->canon_uri($uri);
384              
385 7 50       15 # use <BASE HREF="URI"> to turn relative links into absolute links
386 7         15  
387             # even if it is a base URI, handle like a normal URI as well
388             $self->push_uri($tag, $uri);
389              
390 0 0       0 # a base URI will be ignored by browsers unless it is an absolute
391 0         0 # URI of a standard protocol
392             if ($uri =~ m@^(?:https?|ftp):/{0,2}@i) {
393             # remove trailing filename, if any; base URIs can have the
394             # form of "http://foo.com/index.html"
395 0 0       0 $uri =~ s@^([a-z]+:/{0,2}[^/]+/.*?)[^/\.]+\.[^/\.]{2,4}$@$1@i;
396 0         0  
397             # Make sure it ends in a slash
398             $uri .= "/" unless $uri =~ m@/$@;
399             utf8::encode($uri) if $self->{SA_encode_results};
400             $self->{base_href} = $uri;
401 0         0 }
402             }
403             }
404             }
405 0 0       0  
406             # this might not be quite right, may need to pay attention to table nesting
407             my ($self, $tag) = @_;
408 0         0  
409             # don't close if never opened
410             return unless grep { $_->{tag} eq $tag } @{ $self->{text_style} };
411 0 0       0  
412 0 0       0 my $top;
413 0         0 while (@{ $self->{text_style} } && ($top = $self->{text_style}[-1]->{tag})) {
414             if (($tag eq "td" && ($top eq "font" || $top eq "td")) ||
415             ($tag eq "tr" && $top =~ /^(?:font|td|tr)$/))
416             {
417             pop @{ $self->{text_style} };
418             }
419             else {
420             last;
421 15     15 0 20 }
422             }
423             }
424 15 50       16  
  66         121  
  15         27  
425             my ($self, $tag) = @_;
426 0         0  
427 0   0     0 # don't close if never opened
  0         0  
428 0 0 0     0 return if !grep { $_->{tag} eq $tag } @{ $self->{text_style} };
      0        
      0        
      0        
429              
430             # close everything up to and including tag
431 0         0 while (my %current = %{ pop @{ $self->{text_style} } }) {
  0         0  
432             last if $current{tag} eq $tag;
433             }
434 0         0 }
435              
436             my ($self, $tag, $attr, $num) = @_;
437              
438             # treat <th> as <td>
439             $tag = "td" if $tag eq "th";
440 377     377 0 523  
441             # open
442             if ($num == 1) {
443 377 100       411 # HTML browsers generally only use first <body> for colors,
  1354         2478  
  377         639  
444             # so only push if we haven't seen a body tag yet
445             if ($tag eq "body") {
446 362         464 # TODO: skip if we've already seen body
  373         369  
  373         1700  
447 373 100       1210 }
448              
449             # change basefont (only change size)
450             if ($tag eq "basefont" &&
451             exists $attr->{size} && $attr->{size} =~ /^\s*(\d+)/)
452 776     776 0 1267 {
453             $self->{basefont} = $1;
454             return;
455 776 50       1262 }
456              
457             # close elements with optional end tags
458 776 100       1191 $self->close_table_tag($tag) if ($tag eq "td" || $tag eq "tr");
459              
460             # copy current text state
461 390 100       606 my %new = %{ $self->{text_style}[-1] };
462              
463             # change tag name!
464             $new{tag} = $tag;
465              
466 390 0 33     602 # big and small tags
      0        
467             if ($tag eq "big") {
468             $new{size} += 1;
469 0         0 push @{ $self->{text_style} }, \%new;
470 0         0 return;
471             }
472             if ($tag eq "small") {
473             $new{size} -= 1;
474 390 100 100     1150 push @{ $self->{text_style} }, \%new;
475             return;
476             }
477 390         395  
  390         1536  
478             # tag attributes
479             for my $name (keys %$attr) {
480 390         698 next unless exists $ok_attributes{$tag}{$name};
481             if ($name eq "text" || $name eq "color") {
482             # two different names for text color
483 390 50       634 $new{fgcolor} = name_to_rgb($attr->{$name});
484 0         0 }
485 0         0 elsif ($name eq "size") {
  0         0  
486 0         0 if ($attr->{size} =~ /^\s*([+-]\d+)/) {
487             # relative font size
488 390 50       575 $new{size} = $self->{basefont} + $1;
489 0         0 }
490 0         0 elsif ($attr->{size} =~ /^\s*(\d+)/) {
  0         0  
491 0         0 # absolute font size
492             $new{size} = $1;
493             }
494             }
495 390         917 elsif ($name eq 'style') {
496 753 100       1410 $new{style} = $attr->{style};
497 244 100 66     786 my @parts = split(/;/, $new{style});
    100          
    100          
    100          
498             foreach (@parts) {
499 90         191 if (/^\s*(background-)?color:\s*(.+?)\s*$/i) {
500             my $whcolor = $1 ? 'bgcolor' : 'fgcolor';
501             my $value = lc $2;
502 105 50       412  
    50          
503             if ($value =~ /rgb/) {
504 0         0 $value =~ tr/0-9,//cd;
505             my @rgb = split(/,/, $value);
506             $new{$whcolor} = sprintf("#%02x%02x%02x",
507             map { !$_ ? 0 : $_ > 255 ? 255 : $_ }
508 105         241 @rgb[0..2]);
509             }
510             elsif ($value eq 'inherit') {
511             # do nothing, just prevent parsing of the valid
512 8         14 # CSS3 property value as 'invalid color' (Bug 7778)
513 8         24 }
514 8         11 elsif ($value eq '!important') {
515 8 50       64 # do nothing, just prevent parsing of the valid
    50          
516 0 0       0 # CSS3 property value as 'invalid color' (Bug 7892)
517 0         0 }
518             else {
519 0 0       0 $new{$whcolor} = name_to_rgb($value);
    0          
    0          
520 0         0 }
521 0         0 }
522             elsif (/^\s*([a-z_-]+)\s*:\s*(\S.*?)\s*$/i) {
523 0 0       0 # "display: none", "visibility: hidden", etc.
  0 0       0  
524             $new{'style_'.$1} = $2;
525             }
526             }
527             }
528             elsif ($name eq "bgcolor") {
529             # overwrite with hex value, $new{bgcolor} is set below
530             $attr->{bgcolor} = name_to_rgb($attr->{bgcolor});
531             }
532             else {
533             # attribute is probably okay
534             $new{$name} = $attr->{$name};
535 0         0 }
536              
537             if ($new{size} > $self->{max_size}) {
538             $self->{max_size} = $new{size};
539             }
540 0         0 elsif ($new{size} < $self->{min_size}) {
541             $self->{min_size} = $new{size};
542             }
543             }
544             push @{ $self->{text_style} }, \%new;
545             }
546 6         14 # explicitly close a tag
547             else {
548             if ($tag ne "body") {
549             # don't close body since browsers seem to render text after </body>
550 35         54 $self->close_tag($tag);
551             }
552             }
553 244 100       680 }
    50          
554 15         37  
555             my ($self, $text) = @_;
556              
557 0         0 my $fg = $self->{text_style}[-1]->{fgcolor};
558             my $bg = $self->{text_style}[-1]->{bgcolor};
559             my $size = $self->{text_style}[-1]->{size};
560 390         490 my $display = $self->{text_style}[-1]->{style_display};
  390         950  
561             my $visibility = $self->{text_style}[-1]->{style_visibility};
562              
563             # invisibility
564 386 100       689 if (substr($fg,-6) eq substr($bg,-6)) {
565             $self->put_results(font_low_contrast => 1);
566 377         646 return 1;
567             # near-invisibility
568             } elsif ($fg =~ /^\#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/) {
569             my ($r1, $g1, $b1) = (hex($1), hex($2), hex($3));
570              
571             if ($bg =~ /^\#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/) {
572 300     300 0 471 my ($r2, $g2, $b2) = (hex($1), hex($2), hex($3));
573              
574 300         404 my $r = ($r1 - $r2);
575 300         366 my $g = ($g1 - $g2);
576 300         377 my $b = ($b1 - $b2);
577 300         369  
578 300         349 # geometric distance weighted by brightness
579             # maximum distance is 191.151823601032
580             my $distance = ((0.2126*$r)**2 + (0.7152*$g)**2 + (0.0722*$b)**2)**0.5;
581 300 50       1351  
    50          
582 0         0 # the text is very difficult to read if the distance is under 12,
583 0         0 # a limit of 14 to 16 might be okay if the usage significantly
584             # increases (near-invisible text is at about 0.95% of spam and
585             # 1.25% of HTML spam right now), but please test any changes first
586 300         909 if ($distance < 12) {
587             $self->put_results(font_low_contrast => 1);
588 300 50       758 return 1;
589 300         642 }
590             }
591 300         383 }
592 300         328  
593 300         309
594             # invalid color
595             if ($fg eq 'invalid' or $bg eq 'invalid') {
596             $self->put_results(font_invalid_color => 1);
597 300         896 return 1;
598             }
599              
600             # size too small
601             if ($size <= 1) {
602             return 1;
603 300 50       675 }
604 0         0  
605 0         0 # <span style="display: none">
606             if ($display && lc $display eq 'none') {
607             return 1;
608             }
609              
610             if ($visibility && lc $visibility eq 'hidden') {
611             return 1;
612 300 50 33     949 }
613 0         0  
614 0         0 return 0;
615             }
616              
617             my ($self, $tag, $attr, $num) = @_;
618 300 50       473  
619 0         0 if ($tag eq "font" && exists $attr->{face}) {
620             # Fixes from Bug 5956, 7312
621             # Examples seen in ham:
622             # "Tahoma", Verdana, Arial, sans-serif
623 300 50 33     518 # 'Montserrat', sans-serif
624 0         0 # Arial,Helvetica,Sans-Serif;
625             # .SFUIDisplay
626             # hirakakupro-w3
627 300 50 33     485 # TODO: There's still the problem completely foreign unicode strings,
628 0         0 # probably this rule should be deprecated.
629             if ($attr->{face} !~ /^\s*["'.]?[a-z ][a-z -]*[a-z]\d?["']?(?:,\s*["']?[a-z][a-z -]*[a-z]\d?["']?)*;?$/i) {
630             $self->put_results(font_face_bad => 1);
631 300         527 }
632             }
633             if ($tag eq "img" && exists $self->{inside}{a} && $self->{inside}{a} > 0) {
634             my $uri = $self->{anchor_last};
635 553     553 0 884 utf8::encode($uri) if $self->{SA_encode_results};
636             $self->{uri}->{$uri}->{anchor_text}->[-1] .= "<img>\n";
637 553 100 100     1187 $self->{anchor}->[-1] .= "<img>\n";
638             }
639              
640             if ($tag eq "img" && exists $attr->{width} && exists $attr->{height}) {
641             my $width = 0;
642             my $height = 0;
643             my $area = 0;
644              
645             # assume 800x600 screen for percentage values
646             if ($attr->{width} =~ /^(\d+)(\%)?$/) {
647 34 100       187 $width = $1;
648 4         8 $width *= 8 if (defined $2 && $2 eq "%");
649             }
650             if ($attr->{height} =~ /^(\d+)(\%)?$/) {
651 553 100 100     1064 $height = $1;
      100        
652 1         3 $height *= 6 if (defined $2 && $2 eq "%");
653 1 50       4 }
654 1         4 # guess size
655 1         3 $width = 200 if $width <= 0;
656             $height = 200 if $height <= 0;
657             if ($width > 0 && $height > 0) {
658 553 50 66     1008 $area = $width * $height;
      33        
659 7         12 $self->{image_area} += $area;
660 7         7 }
661 7         8 }
662             if ($tag eq "form" && exists $attr->{action}) {
663             $self->put_results(form_action_mailto => 1) if $attr->{action} =~ /mailto:/i
664 7 50       30 }
665 0         0 if ($tag eq "object" || $tag eq "embed") {
666 0 0 0     0 $self->put_results(embeds => 1);
667             }
668 7 50       17  
669 0         0 # special text delimiters - <a> and <title>
670 0 0 0     0 if ($tag eq "a") {
671             my $uri = $self->{anchor_last} =
672             (exists $attr->{href} ? $self->canon_uri($attr->{href}) : "");
673 7 50       14 utf8::encode($uri) if $self->{SA_encode_results};
674 7 50       16 push(@{$self->{uri}->{$uri}->{anchor_text}}, '');
675 7 50 33     20 push(@{$self->{anchor}}, '');
676 7         10 }
677 7         13 if ($tag eq "title") {
678             $self->{title_index}++;
679             $self->{title}->[$self->{title_index}] = "";
680 553 0 33     927 }
681 0 0       0  
682             if ($tag eq "meta" &&
683 553 50 33     1839 exists $attr->{'http-equiv'} &&
684 0         0 exists $attr->{content} &&
685             $attr->{'http-equiv'} =~ /Content-Type/i &&
686             $attr->{content} =~ /\bcharset\s*=\s*["']?([^"']+)/i)
687             {
688 553 100       962 $self->{charsets} .= exists $self->{charsets} ? " $1" : $1;
689             }
690 203 50       479 }
691 203 50       384  
692 203         232 my $self = shift;
  203         482  
693 203         231 my $text = shift;
  203         339  
694             my %display = @_;
695 553 100       984  
696 1         2 # Unless it's specified to be invisible, then it's not invisible. ;)
697 1         3 if (!exists $display{invisible}) {
698             $display{invisible} = 0;
699             }
700 553 100 66     4633  
      33        
      66        
      66        
701             if ($display{whitespace}) {
702             # trim trailing whitespace from previous element if it was not whitespace
703             # and it was not invisible
704             if (@{ $self->{text} } &&
705             (!defined $self->{text_whitespace} ||
706 2 50       40 !vec($self->{text_whitespace}, $#{$self->{text}}, 1)) &&
707             (!defined $self->{text_invisible} ||
708             !vec($self->{text_invisible}, $#{$self->{text}}, 1)))
709             {
710             $self->{text}->[-1] =~ s/ $//;
711 863     863 0 992 }
712 863         984 }
713 863         1256 else {
714             # NBSP: UTF-8: C2 A0, ISO-8859-*: A0
715             $text =~ s/[ \t\n\r\f\x0b]+|\xc2\xa0/ /gs;
716 863 50       1388 # trim leading whitespace if previous element was whitespace
717 863         1111 # and current element is not invisible
718             if (@{ $self->{text} } && !$display{invisible} &&
719             defined $self->{text_whitespace} &&
720 863 100       1251 vec($self->{text_whitespace}, $#{$self->{text}}, 1))
721             {
722             $text =~ s/^ //;
723 202 100 100     200 }
  202   66     623  
      33        
      66        
724             }
725             push @{ $self->{text} }, $text;
726             while (my ($k, $v) = each %display) {
727             my $textvar = "text_".$k;
728             if (!exists $self->{$textvar}) { $self->{$textvar} = ''; }
729 185         420 vec($self->{$textvar}, $#{$self->{text}}, 1) = $v;
730             }
731             }
732              
733             my ($self, $text) = @_;
734 661         2687 utf8::encode($text) if $self->{SA_encode_results};
735              
736             # text that is not part of body
737 661 100 66     860 if (exists $self->{inside}{script} && $self->{inside}{script} > 0)
  661   100     2863  
      100        
738             {
739 293         824 push @{ $self->{script} }, $text;
740             return;
741 185         429 }
742             if (exists $self->{inside}{style} && $self->{inside}{style} > 0) {
743             return;
744 863         1069 }
  863         1586  
745 863         2229  
746 1065         1503 # text that is part of body and also stored separately
747 1065 100       1730 if (exists $self->{inside}{a} && $self->{inside}{a} > 0) {
  14         47  
748 1065         1170 # this doesn't worry about nested anchors
  1065         7954  
749             my $uri = $self->{anchor_last};
750             utf8::encode($uri) if $self->{SA_encode_results};
751             $self->{uri}->{$uri}->{anchor_text}->[-1] .= $text;
752             $self->{anchor}->[-1] .= $text;
753 662     662 0 1311 }
754 662 50       1161 if (exists $self->{inside}{title} && $self->{inside}{title} > 0) {
755             $self->{title}->[$self->{title_index}] .= $text;
756             }
757 662 50 33     1227  
758             my $invisible_for_bayes = 0;
759 0         0  
  0         0  
760 0         0 # NBSP: UTF-8: C2 A0, ISO-8859-*: A0
761             # Bug 7374 - regex recursion limit exceeded
762 662 100 100     1255 #if ($text !~ /^(?:[ \t\n\r\f\x0b]|\xc2\xa0)*\z/s) {
763 1         7 # .. alternative way, remove from string and see if there's anything left
764             if (do {(my $tmp = $text) =~ s/(?:[ \t\n\r\f\x0b]|\xc2\xa0)//gs; length($tmp)}) {
765             $invisible_for_bayes = $self->html_font_invisible($text);
766             }
767 661 100 100     1889  
768             if (exists $self->{text}->[-1]) {
769 202         270 # ideas discarded since they would be easy to evade:
770 202 50       342 # 1. using \w or [A-Za-z] instead of \S or non-punctuation
771 202         396 # 2. exempting certain tags
772 202         311 # no re "strict"; # since perl 5.21.8: Ranges of ASCII printables...
773             if ($text =~ /^[^\s\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]/s &&
774 661 100 100     1196 $self->{text}->[-1] =~ /[^\s\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]\z/s)
775 1         4 {
776             $self->{obfuscation}++;
777             }
778 661         734 if ($self->{text}->[-1] =~
779             /\b([^\s\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]{1,7})\z/s)
780             {
781             my $start = length($1);
782             if ($text =~ /^([^\s\x21-\x2f\x3a-\x40\x5b-\x60\x7b-\x7e]{1,7})\b/s) {
783             $self->{backhair}->{$start . "_" . length($1)}++;
784 661 100       646 }
  661         2975  
  661         1427  
785 300         574 }
786             }
787              
788 661 100       1233 if ($invisible_for_bayes) {
789             $self->display_text($text, invisible => 1);
790             }
791             else {
792             $self->display_text($text);
793 653 50 66     1924 }
794             }
795              
796 0         0 # note: $text includes <!-- and -->
797             my ($self, $text) = @_;
798 653 100       1715 utf8::encode($text) if $self->{SA_encode_results};
799              
800             push @{ $self->{comment} }, $text;
801 192         388 }
802 192 50       442  
803 0         0 my ($self, $text) = @_;
804             utf8::encode($text) if $self->{SA_encode_results};
805              
806             if ($text =~ /^<!doctype/i) {
807             my $tag = "!doctype";
808 661 50       980 $self->{elements}++;
809 0         0 $self->{tags}++;
810             $self->{inside}{$tag} = 0;
811             }
812 661         1181 }
813              
814             ###########################################################################
815              
816             my %html_color = (
817             # HTML 4 defined 16 colors
818 1     1 0 4 aqua => 0x00ffff,
819 1 50       3 black => 0x000000,
820             blue => 0x0000ff,
821 1         11 fuchsia => 0xff00ff,
  1         9  
822             gray => 0x808080,
823             green => 0x008000,
824             lime => 0x00ff00,
825 0     0 0 0 maroon => 0x800000,
826 0 0       0 navy => 0x000080,
827             olive => 0x808000,
828 0 0       0 purple => 0x800080,
829 0         0 red => 0xff0000,
830 0         0 silver => 0xc0c0c0,
831 0         0 teal => 0x008080,
832 0         0 white => 0xffffff,
833             yellow => 0xffff00,
834             # colors specified in CSS3 color module
835             aliceblue => 0xf0f8ff,
836             antiquewhite => 0xfaebd7,
837             aqua => 0x00ffff,
838             aquamarine => 0x7fffd4,
839             azure => 0xf0ffff,
840             beige => 0xf5f5dc,
841             bisque => 0xffe4c4,
842             black => 0x000000,
843             blanchedalmond => 0xffebcd,
844             blue => 0x0000ff,
845             blueviolet => 0x8a2be2,
846             brown => 0xa52a2a,
847             burlywood => 0xdeb887,
848             cadetblue => 0x5f9ea0,
849             chartreuse => 0x7fff00,
850             chocolate => 0xd2691e,
851             coral => 0xff7f50,
852             cornflowerblue => 0x6495ed,
853             cornsilk => 0xfff8dc,
854             crimson => 0xdc143c,
855             cyan => 0x00ffff,
856             darkblue => 0x00008b,
857             darkcyan => 0x008b8b,
858             darkgoldenrod => 0xb8860b,
859             darkgray => 0xa9a9a9,
860             darkgreen => 0x006400,
861             darkgrey => 0xa9a9a9,
862             darkkhaki => 0xbdb76b,
863             darkmagenta => 0x8b008b,
864             darkolivegreen => 0x556b2f,
865             darkorange => 0xff8c00,
866             darkorchid => 0x9932cc,
867             darkred => 0x8b0000,
868             darksalmon => 0xe9967a,
869             darkseagreen => 0x8fbc8f,
870             darkslateblue => 0x483d8b,
871             darkslategray => 0x2f4f4f,
872             darkslategrey => 0x2f4f4f,
873             darkturquoise => 0x00ced1,
874             darkviolet => 0x9400d3,
875             deeppink => 0xff1493,
876             deepskyblue => 0x00bfff,
877             dimgray => 0x696969,
878             dimgrey => 0x696969,
879             dodgerblue => 0x1e90ff,
880             firebrick => 0xb22222,
881             floralwhite => 0xfffaf0,
882             forestgreen => 0x228b22,
883             fuchsia => 0xff00ff,
884             gainsboro => 0xdcdcdc,
885             ghostwhite => 0xf8f8ff,
886             gold => 0xffd700,
887             goldenrod => 0xdaa520,
888             gray => 0x808080,
889             green => 0x008000,
890             greenyellow => 0xadff2f,
891             grey => 0x808080,
892             honeydew => 0xf0fff0,
893             hotpink => 0xff69b4,
894             indianred => 0xcd5c5c,
895             indigo => 0x4b0082,
896             ivory => 0xfffff0,
897             khaki => 0xf0e68c,
898             lavender => 0xe6e6fa,
899             lavenderblush => 0xfff0f5,
900             lawngreen => 0x7cfc00,
901             lemonchiffon => 0xfffacd,
902             lightblue => 0xadd8e6,
903             lightcoral => 0xf08080,
904             lightcyan => 0xe0ffff,
905             lightgoldenrodyellow => 0xfafad2,
906             lightgray => 0xd3d3d3,
907             lightgreen => 0x90ee90,
908             lightgrey => 0xd3d3d3,
909             lightpink => 0xffb6c1,
910             lightsalmon => 0xffa07a,
911             lightseagreen => 0x20b2aa,
912             lightskyblue => 0x87cefa,
913             lightslategray => 0x778899,
914             lightslategrey => 0x778899,
915             lightsteelblue => 0xb0c4de,
916             lightyellow => 0xffffe0,
917             lime => 0x00ff00,
918             limegreen => 0x32cd32,
919             linen => 0xfaf0e6,
920             magenta => 0xff00ff,
921             maroon => 0x800000,
922             mediumaquamarine => 0x66cdaa,
923             mediumblue => 0x0000cd,
924             mediumorchid => 0xba55d3,
925             mediumpurple => 0x9370db,
926             mediumseagreen => 0x3cb371,
927             mediumslateblue => 0x7b68ee,
928             mediumspringgreen => 0x00fa9a,
929             mediumturquoise => 0x48d1cc,
930             mediumvioletred => 0xc71585,
931             midnightblue => 0x191970,
932             mintcream => 0xf5fffa,
933             mistyrose => 0xffe4e1,
934             moccasin => 0xffe4b5,
935             navajowhite => 0xffdead,
936             navy => 0x000080,
937             oldlace => 0xfdf5e6,
938             olive => 0x808000,
939             olivedrab => 0x6b8e23,
940             orange => 0xffa500,
941             orangered => 0xff4500,
942             orchid => 0xda70d6,
943             palegoldenrod => 0xeee8aa,
944             palegreen => 0x98fb98,
945             paleturquoise => 0xafeeee,
946             palevioletred => 0xdb7093,
947             papayawhip => 0xffefd5,
948             peachpuff => 0xffdab9,
949             peru => 0xcd853f,
950             pink => 0xffc0cb,
951             plum => 0xdda0dd,
952             powderblue => 0xb0e0e6,
953             purple => 0x800080,
954             red => 0xff0000,
955             rosybrown => 0xbc8f8f,
956             royalblue => 0x4169e1,
957             saddlebrown => 0x8b4513,
958             salmon => 0xfa8072,
959             sandybrown => 0xf4a460,
960             seagreen => 0x2e8b57,
961             seashell => 0xfff5ee,
962             sienna => 0xa0522d,
963             silver => 0xc0c0c0,
964             skyblue => 0x87ceeb,
965             slateblue => 0x6a5acd,
966             slategray => 0x708090,
967             slategrey => 0x708090,
968             snow => 0xfffafa,
969             springgreen => 0x00ff7f,
970             steelblue => 0x4682b4,
971             tan => 0xd2b48c,
972             teal => 0x008080,
973             thistle => 0xd8bfd8,
974             tomato => 0xff6347,
975             turquoise => 0x40e0d0,
976             violet => 0xee82ee,
977             wheat => 0xf5deb3,
978             white => 0xffffff,
979             whitesmoke => 0xf5f5f5,
980             yellow => 0xffff00,
981             yellowgreen => 0x9acd32,
982             );
983              
984             my $color = lc $_[0];
985              
986             # note: Mozilla strips leading and trailing whitespace at this point,
987             # but IE does not
988              
989             # named colors
990             my $hex = $html_color{$color};
991             if (defined $hex) {
992             return sprintf("#%06x", $hex);
993             }
994              
995             # Flex Hex: John Graham-Cumming, http://www.jgc.org/pdf/lisa2004.pdf
996             # strip optional # character
997             $color =~ s/^#//;
998             # pad right-hand-side to a multiple of three
999             $color .= "0" x (3 - (length($color) % 3)) if (length($color) % 3);
1000             # split into triplets
1001             my $length = length($color) / 3;
1002             my @colors = ($color =~ /(.{$length})(.{$length})(.{$length})/);
1003             # truncate each color to a DWORD, take MSB, left pad nibbles
1004             foreach (@colors) { s/.*(.{8})$/$1/; s/(..).*/$1/; s/^(.)$/0$1/ };
1005             # the color
1006             $color = join("", @colors);
1007 0     0 0 0 # replace non-hex characters with 0
1008             $color =~ tr/0-9a-f/0/c;
1009              
1010             return "#" . $color;
1011             }
1012              
1013 0         0 my $color = lc $_[0];
1014 0 0       0 my $before = $color;
1015 0         0  
1016             # strip leading and ending whitespace
1017             $color =~ s/^\s*//;
1018             $color =~ s/\s*$//;
1019              
1020 0         0 # named colors
1021             my $hex = $html_color{$color};
1022 0 0       0 if (defined $hex) {
1023             return sprintf("#%06x", $hex);
1024 0         0 }
1025 0         0  
1026             # IF NOT A NAME, IT SHOULD BE A HEX COLOR, HEX SHORTHAND or rgb values
1027 0         0 if ($color =~ m/^[#a-f0-9]*$|rgb\([\d%, ]*\)/i) {
  0         0  
  0         0  
  0         0  
1028              
1029 0         0 #Convert the RGB values to hex values so we can fall through on the programming
1030              
1031 0         0 #RGB PERCENTS TO HEX
1032             if ($color =~ m/rgb\((\d+)%,\s*(\d+)%,\s*(\d+)%\s*\)/i) {
1033 0         0 $color = "#".dec2hex(int($1/100*255)).dec2hex(int($2/100*255)).dec2hex(int($3/100*255));
1034             }
1035              
1036             #RGB DEC TO HEX
1037 124     124 0 10216 if ($color =~ m/rgb\((\d+),\s*(\d+),\s*(\d+)\s*\)/i) {
1038 124         149 $color = "#".dec2hex($1).dec2hex($2).dec2hex($3);
1039             }
1040              
1041 124         418 #PARSE THE HEX
1042 124         434 if ($color =~ m/^#/) {
1043             # strip to hex only
1044             $color =~ s/[^a-f0-9]//ig;
1045 124         219  
1046 124 100       221 # strip to 6 if greater than 6
1047 4         16 if (length($color) > 6) {
1048             $color=substr($color,0,6);
1049             }
1050              
1051 120 100       303 # strip to 3 if length < 6)
1052             if (length($color) > 3 && length($color) < 6) {
1053             $color=substr($color,0,3);
1054             }
1055              
1056 105 100       180 # pad right-hand-side to a multiple of three
1057 2         11 $color .= "0" x (3 - (length($color) % 3)) if (length($color) % 3);
1058              
1059             #DUPLICATE SHORTHAND HEX
1060             if (length($color) == 3) {
1061 105 100       169 $color =~ m/(.)(.)(.)/;
1062 2         6 $color = "$1$1$2$2$3$3";
1063             }
1064              
1065             } else {
1066 105 100       219 return "invalid";
1067             }
1068 99         274  
1069             } else {
1070             #INVALID
1071 99 100       191  
1072 1         2 #??RETURN BLACK SINCE WE DO NOT KNOW HOW THE MUA / BROWSER WILL PARSE
1073             #$color = "000000";
1074              
1075             return "invalid";
1076 99 50 66     301 }
1077 0         0  
1078             #print "DEBUG: before/after name_to_rgb new version: $before/$color\n";
1079              
1080             return "#" . $color;
1081 99 100       193 }
1082              
1083             my ($dec) = @_;
1084 99 100       198 my ($pre) = '';
1085 3         7  
1086 3         14 if ($dec < 16) {
1087             $pre = '0';
1088             }
1089              
1090 6         13 return sprintf("$pre%lx", $dec);
1091             }
1092              
1093              
1094             use constant URI_STRICT => 0;
1095              
1096             # resolving relative URIs as defined in RFC 2396 (steps from section 5.2)
1097             # using draft http://www.gbiv.com/protocols/uri/rev-2002/rfc2396bis.html
1098             my ($u) = @_;
1099 15         36 my %u;
1100             ($u{scheme}, $u{authority}, $u{path}, $u{query}, $u{fragment}) =
1101             $u =~ m|^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?|;
1102             return %u;
1103             }
1104 99         230  
1105             my ($input) = @_;
1106             my $output = "";
1107              
1108 12     12 0 19 $input =~ s@^(?:\.\.?/)@/@;
1109 12         14  
1110             while ($input) {
1111 12 50       19 if ($input =~ s@^/\.(?:$|/)@/@) {
1112 0         0 }
1113             elsif ($input =~ s@^/\.\.(?:$|/)@/@) {
1114             $output =~ s@/?[^/]*$@@;
1115 12         40 }
1116             elsif ($input =~ s@(/?[^/]*)@@) {
1117             $output .= $1;
1118             }
1119 41     41   501 }
  41         90  
  41         36017  
1120             return $output;
1121             }
1122              
1123             my ($base_authority, $base_path, $r_path) = @_;
1124 686     686   1223  
1125 686         695 if (defined $base_authority && !$base_path) {
1126 686         3139 return "/" . $r_path;
1127             }
1128 686         3703 else {
1129             if ($base_path =~ m|/|) {
1130             $base_path =~ s|(?<=/)[^/]*$||;
1131             }
1132 340     340   483 else {
1133 340         388 $base_path = "";
1134             }
1135 340         479 return $base_path . $r_path;
1136             }
1137 340         593 }
1138 382 100       2596  
    100          
    50          
1139             my ($base, $r) = @_;
1140              
1141 20         63 my %r = _parse_uri($r); # parsed relative URI
1142             my %base = _parse_uri($base); # parsed base URI
1143             my %t; # generated temporary URI
1144 353         968  
1145             if ((not URI_STRICT) and
1146             (defined $r{scheme} && defined $base{scheme}) and
1147 340         638 ($r{scheme} eq $base{scheme}))
1148             {
1149             undef $r{scheme};
1150             }
1151 149     149   252  
1152             if (defined $r{scheme}) {
1153 149 50 66     339 $t{scheme} = $r{scheme};
1154 0         0 $t{authority} = $r{authority};
1155             $t{path} = _remove_dot_segments($r{path});
1156             $t{query} = $r{query};
1157 149 100       280 }
1158 34         139 else {
1159             if (defined $r{authority}) {
1160             $t{authority} = $r{authority};
1161 115         143 $t{path} = _remove_dot_segments($r{path});
1162             $t{query} = $r{query};
1163 149         425 }
1164             else {
1165             if ($r{path} eq "") {
1166             $t{path} = $base{path};
1167             if (defined $r{query}) {
1168 343     343 0 12202 $t{query} = $r{query};
1169             }
1170 343         508 else {
1171 343         635 $t{query} = $base{query};
1172 343         469 }
1173             }
1174 343 100 100     1013 else {
      100        
1175             if ($r{path} =~ m|^/|) {
1176             $t{path} = _remove_dot_segments($r{path});
1177             }
1178 1         4 else {
1179             $t{path} = _merge_uri($base{authority}, $base{path}, $r{path});
1180             $t{path} = _remove_dot_segments($t{path});
1181 343 100       572 }
1182 187         298 $t{query} = $r{query};
1183 187         225 }
1184 187         288 $t{authority} = $base{authority};
1185 187         298 }
1186             $t{scheme} = $base{scheme};
1187             }
1188 156 100       236 $t{fragment} = $r{fragment};
1189 1         4  
1190 1         5 # recompose URI
1191 1         4 my $result = "";
1192             if ($t{scheme}) {
1193             $result .= $t{scheme} . ":";
1194 155 100       252 }
1195 3         5 elsif (defined $t{authority}) {
1196 3 100       8 # this block is not part of the RFC
1197 1         3 # TODO: figure out what MUAs actually do with unschemed URIs
1198             # maybe look at URI::Heuristic
1199             if ($t{authority} =~ /^www\d*\./i) {
1200 2         3 # some spammers are using unschemed URIs to escape filters
1201             $result .= "http:";
1202             }
1203             elsif ($t{authority} =~ /^ftp\d*\./i) {
1204 152 100       295 $result .= "ftp:";
1205 3         9 }
1206             }
1207             if ($t{authority}) {
1208 149         256 $result .= "//" . $t{authority};
1209 149         287 }
1210             $result .= $t{path};
1211 152         256 if (defined $t{query}) {
1212             $result .= "?" . $t{query};
1213 155         249 }
1214             if (defined $t{fragment}) {
1215 156         181 $result .= "#" . $t{fragment};
1216             }
1217 343         436 return $result;
1218             }
1219              
1220 343         407 1;