File Coverage

blib/lib/Labyrinth/MLUtils.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Labyrinth::MLUtils;
2              
3 8     8   34023 use warnings;
  8         12  
  8         203  
4 8     8   27 use strict;
  8         9  
  8         143  
5 8     8   26 use utf8;
  8         9  
  8         33  
6              
7 8     8   140 use vars qw($VERSION @ISA %EXPORT_TAGS @EXPORT @EXPORT_OK);
  8         7  
  8         865  
8             $VERSION = '5.32';
9              
10             =head1 NAME
11              
12             Labyrinth::MLUtils - Markup Language Utilities for Labyrinth.
13              
14             =head1 SYNOPSIS
15              
16             use Labyrinth::MLUtils;
17              
18             =cut
19              
20             # -------------------------------------
21             # Export Details
22              
23             require Exporter;
24             @ISA = qw(Exporter);
25             %EXPORT_TAGS = ( 'all' => [ qw(
26             LegalTag LegalTags CleanTags
27             CleanHTML SafeHTML CleanLink CleanWords LinkTitles
28             DropDownList DropDownListText
29             DropDownRows DropDownRowsText
30             DropDownMultiList DropDownMultiRows
31             ErrorText ErrorSymbol
32             LinkSpam
33              
34             create_inline_styles
35             demoroniser
36             process_html escape_html
37             ) ] );
38              
39             @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
40             @EXPORT = ( @{ $EXPORT_TAGS{'all'} } );
41              
42             # -------------------------------------
43             # Library Modules
44              
45 8     8   3117 use Encode::ZapCP1252;
  8         65087  
  8         413  
46 8     8   3314 use HTML::Entities;
  8         32061  
  8         526  
47 8     8   3523 use Regexp::Common qw /profanity/;
  8         13389  
  8         24  
48              
49 8     8   6331 use Labyrinth::Audit;
  8         9  
  8         947  
50 8     8   3009 use Labyrinth::Variables;
  0            
  0            
51              
52             # -------------------------------------
53             # Variables
54              
55             my $DEFAULTTAGS = 'p,a,br,b,strong,center,hr,ol,ul,li,i,img,u,em,strike,h1,h2,h3,h4,h5,h6,table,thead,tr,th,tbody,td,sup,address,pre';
56             my ($HTMLTAGS,%HTMLTAGS);
57              
58             # -------------------------------------
59             # The Public Interface Subs
60              
61             =head1 FUNCTIONS
62              
63             =head2 HTML Tag handling
64              
65             =over 4
66              
67             =item LegalTag
68              
69             Returns TRUE or FALSE as to whether the given HTML tag is accepted by the
70             system.
71              
72             =item LegalTags
73              
74             Returns the list of HTML tags that are accepted by the system.
75              
76             =item CleanTags
77              
78             For a given text string, attempts to clean the use of any HTML tags. Any HTML
79             tags found that are not accepted by the system are encoded into HTML entities.
80              
81             =item CleanHTML
82              
83             For a given text string, removes all existence of any HTML tag. Mostly used in
84             input text box cleaning.
85              
86             =item SafeHTML
87              
88             For a given text string, encodes all HTML tags to HTML entities. Mostly used in
89             input textarea edit preparation.
90              
91             =item CleanLink
92              
93             Attempts to remove known spam style links.
94              
95             =item CleanWords
96              
97             Attempts to remove known profanity words.
98              
99             =item LinkTitles
100              
101             Given a XHTML snippet, will look for basic links and add title attributes.
102             Titles are of rhe format 'External Site: $domain', where $domain is the domain
103             used in the link.
104              
105             =back
106              
107             =cut
108              
109             sub LegalTag {
110             my $tag = lc shift;
111              
112             my %tags = _buildtags();
113             return 1 if($tags{$tag});
114             return 0;
115             }
116              
117             sub LegalTags {
118             my %tags = _buildtags();
119             my $tags = join(", ", sort keys %tags);
120             $tags =~ s/, ([^,]+)$/ and $1/;
121             return $tags;
122             }
123              
124             sub CleanTags {
125             my $text = shift;
126             return '' unless($text);
127              
128             $text =~ s!]*>!!sig;
129             $text =~ s!<(br|hr)>!<$1 />!sig;
130             $text =~ s!

(?:\s| )+(?:

)?<(table|p|ul|ol|div|pre)!<$1!sig;
131             $text =~ s!\s+&\s+! & !sg;
132             $text =~ s!&[lr]squo;!"!mg;
133             $text =~ s{&(?!\#\d+;|[a-z0-9]+;)}{&}sig;
134              
135             # decode TinyMCE encodings
136             $text =~ s!<(.*?)>!<$1>!sig;
137              
138             # clean paragraphs
139             $text =~ s!

\s+

!

!sig;

140             $text =~ s!\s*

\s*!

!sig;

141              
142             my %tags = _buildtags();
143             my @found = ($text =~ m!]*)?>!gm);
144             for my $tag (@found) {
145             $tag = lc $tag;
146             next if($tags{$tag});
147              
148             $text =~ s!<(/?$tag(?:[^>]*)?)>!<$1>!igm;
149             $tags{$tag} = 1;
150             }
151              
152             process_html($text,0,1);
153             }
154              
155             sub CleanHTML {
156             my $text = shift;
157             return '' unless($text);
158              
159             $text =~ s!<[^>]+>!!gm; # remove any tags
160             $text =~ s!\s{2,}! !mg;
161             $text =~ s!&[lr]squo;!"!mg;
162             $text =~ s{&(?!\#\d+;|[a-z0-9]+;)}{&}sig;
163              
164             process_html($text,0,0);
165             }
166              
167             sub SafeHTML {
168             my $text = shift;
169             return '' unless($text);
170              
171             $text =~ s!
172             $text =~ s!>!>!gm;
173             $text =~ s!\s+&\s+! & !mg;
174             $text =~ s!&[lr]squo;!"!mg;
175             $text =~ s{&(?!\#\d+;|[a-z0-9]+;)}{&}sig;
176              
177             process_html($text,0,0);
178             }
179              
180             sub CleanLink {
181             my $text = shift;
182             return '' unless($text);
183              
184             # remove embedded script tags
185             $text =~ s!!!gis; # open and close script tags
186             $text =~ s!
187             $text =~ s!.*/script>!!gis; # close, but on open, removed from te beginning of string
188              
189             # remove anything that looks like a link
190             $text =~ s!https?://[^\s]*!!gis;
191             $text =~ s!!!gis;
192             $text =~ s!\[url.*?url\]!!gis;
193             $text =~ s!\[link.*?link\]!!gis;
194             # $text =~ s!$settings{urlregex}!!gis;
195              
196             CleanTags($text);
197             }
198              
199             sub CleanWords {
200             my $text = shift;
201              
202             $text =~ s/$RE{profanity}//gis;
203             my $filter = join("|", map {$_->[1]} $dbi->GetQuery('array','AllBadWords'));
204             $text =~ s/$filter//gis;
205              
206             return $text;
207             }
208              
209             sub LinkTitles {
210             my $text = shift;
211              
212             for my $href ($text =~ m!()!g) {
213             my ($link1,$path,$link2) = ($href =~ m!(!);
214             $href =~ s!([\\\?\+\-\.()\[\]])!\\$1!sig;
215              
216             my $title;
217             $title ||= $settings{pathmap}{$path} if($settings{pathmap}{$path});
218             $title ||= $settings{titlemap}{$link2} if($settings{titlemap}{$link2});
219             $title ||= "External Site: $link2";
220             $text =~ s!$href!$link1$path" title="$title">!sgi;
221             }
222              
223             return $text;
224             }
225              
226             sub _buildtags {
227             return %HTMLTAGS if(%HTMLTAGS);
228              
229             if(defined $settings{htmltags} && $settings{htmltags} =~ /^\+(.*)/) {
230             $settings{htmltags} = $1 . ',' . $DEFAULTTAGS;
231             } elsif(!$settings{htmltags}) {
232             $settings{htmltags} = $DEFAULTTAGS;
233             }
234              
235             %HTMLTAGS = map {$_ => 1} split(",",$settings{htmltags});
236             return %HTMLTAGS;
237             }
238              
239             =head2 Drop Down Boxes
240              
241             =over 4
242              
243             =item DropDownList($opt,$name,@items)
244              
245             Returns a dropdown selection box given a list of numbers. Can optionally pass
246             a option value to be pre-selected. The name of the form element is used as
247             both the element name and id.
248              
249             =item DropDownListText($opt,$name,@items)
250              
251             Returns a dropdown selection box given a list of strings. Can optionally pass
252             a option value to be pre-selected. The name of the form element is used as
253             both the element name and id.
254              
255             =item DropDownRows($opt,$name,$index,$value,@items)
256              
257             Returns a dropdown selection box given a list of rows. Can optionally pass
258             a option value to be pre-selected. The name of the form element is used as
259             both the element name and id. The 'index' and 'value' refence the field names
260             within each row hash.
261              
262             =item DropDownRowsText($opt,$name,$index,$value,@items)
263              
264             Returns a dropdown selection box given a list of strings. Can optionally pass
265             a option value to be pre-selected. The name of the form element is used as
266             both the element name and id. The 'index' and 'value' refence the field names
267             within each row hash.
268              
269             =item DropDownMultiList($opts,$name,$count,@items)
270              
271             Returns a dropdown multi-selection box given a list of strings. The name of the
272             form element is used as both the element name and id. The default number of
273             rows visible is 5, but this can be changed by providing a value for 'count'.
274              
275             Can optionally pass an option value to be pre-selected. The option can be a
276             comma separated list (as a single string) of values or an arrayref to a list
277             of values.
278              
279             =item DropDownMultiRows($opts,$name,$index,$value,$count,@items)
280              
281             Returns a dropdown multi-selection box given a list of rows. The name of the
282             form element is used as both the element name and id. The default number of
283             rows visible is 5, but this can be changed by providing a value for 'count'.
284             The 'index' and 'value' refence the field names within each row hash.
285              
286             Can optionally pass an option value to be pre-selected. The option can be a
287             comma separated list (as a single string) of values or an arrayref to a list
288             of values.
289              
290             =back
291              
292             =cut
293              
294             sub DropDownList {
295             my ($opt,$name,@items) = @_;
296             $opt = undef if(defined $opt && $opt !~ /^\d+$/); # opt must be a number
297              
298             return qq|
299             join("",(map { qq|
300             (defined $opt && $opt == $_ ? ' selected="selected"' : '').
301             ">$_" } @items)) .
302             "";
303             }
304              
305             sub DropDownListText {
306             my ($opt,$name,@items) = @_;
307              
308             return qq|
309             join("",(map { qq|
310             (defined $opt && $opt eq $_ ? ' selected="selected"' : '').
311             ">$_" } @items)) .
312             "";
313             }
314              
315             sub DropDownRows {
316             my ($opt,$name,$index,$value,@items) = @_;
317             $opt = undef if(defined $opt && $opt !~ /^\d+$/); # opt must be a number
318              
319             return qq|
320             join("",(map { qq|
321             (defined $opt && $opt == $_->{$index} ? ' selected="selected"' : '').
322             ">$_->{$value}" } @items)) .
323             "";
324             }
325              
326             sub DropDownRowsText {
327             my ($opt,$name,$index,$value,@items) = @_;
328              
329             return qq|
330             join("",(map { qq|
331             (defined $opt && $opt eq $_->{$index} ? ' selected="selected"' : '').
332             ">$_->{$value}" } @items)) .
333             "";
334             }
335              
336             sub DropDownMultiList {
337             my ($opts,$name,$count,@items) = @_;
338             my %opts;
339              
340             if(defined $opts) {
341             if(ref($opts) eq 'ARRAY') {
342             %opts = map {$_ => 1} @$opts;
343             } elsif($opts =~ /,/) {
344             %opts = map {$_ => 1} split(/,/,$opts);
345             } elsif($opts) {
346             %opts = ("$opts" => 1);
347             }
348             }
349              
350             return qq|
351             join("",(map { qq|
352             (defined $opts && $opts{$_} ? ' selected="selected"' : '').
353             ">$_" } @items)) .
354             "";
355             }
356              
357             sub DropDownMultiRows {
358             my ($opts,$name,$index,$value,$count,@items) = @_;
359             my %opts;
360              
361             if(defined $opts) {
362             if(ref($opts) eq 'ARRAY') {
363             %opts = map {$_ => 1} @$opts;
364             } elsif($opts =~ /,/) {
365             %opts = map {$_ => 1} split(/,/,$opts);
366             } elsif($opts) {
367             %opts = ("$opts" => 1);
368             }
369             }
370              
371             return qq|
372             join("",(map { qq|
373             (defined $opts && $opts{$_->{$index}} ? ' selected="selected"' : '').
374             ">$_->{$value}" } @items)) .
375             "";
376             }
377              
378             =head2 Error Functions
379              
380             =over 4
381              
382             =item ErrorText
383              
384             Returns the given error string in a HTML span tag, with the configured error
385             class, which by default is called "alert". In your CSS sytle sheet you will
386             need to specify an appropriate class declaration, such as:
387              
388             .alert { color: red; font-weight: bold; }
389              
390             Set the value of 'errorclass' in your site config file to change the class
391             name used.
392              
393             =item ErrorSymbol
394              
395             Flags to the system that an error has occured and returns the configured error
396             symbol, which by is the 'empty' symbol '∅', which can then be used as the
397             error field indicator.
398              
399             Set the value of 'errorsymbol' in your site config file to change the symbol
400             used.
401              
402             =back
403              
404             =cut
405              
406             sub ErrorText {
407             my $text = shift;
408             $settings{errorclass} ||= 'alert';
409             return qq!$text!;
410             }
411              
412             sub ErrorSymbol {
413             $tvars{errmess} = 1;
414             $tvars{errcode} = 'ERROR';
415             return $settings{errorsymbol} || '∅';
416             }
417              
418             =head2 Protection Functions
419              
420             =over 4
421              
422             =item LinkSpam
423              
424             Checks whether any links exist in the given text that could indicate comment spam.
425              
426             =back
427              
428             =cut
429              
430             sub LinkSpam {
431             my $text = shift;
432             return 1 if($text =~ m!https?://[^\s]*!is);
433             return 1 if($text =~ m!!is);
434             return 1 if($text =~ m!\[url.*?url\]!is);
435             return 1 if($text =~ m!\[link.*?link\]!is);
436             return 1 if($text =~ m!$settings{urlregex}!is);
437             return 0;
438             }
439              
440             =head2 CSS Handling Code
441              
442             =over 4
443              
444             =item create_inline_styles ( HASHREF )
445              
446             Create inline CSS style sheet block. Key value pairs should match the label
447             (tag, identifier or class patterns) and its contents. For example:
448              
449             my %css = ( '#label p' => 'font-weight: normal; color: #fff;' );
450              
451             or
452              
453             my %css = ( '#label p' => { 'font-weight' => 'normal', 'color' => '#fff' } );
454              
455              
456             The exception to this is the label 'media', which can be used to specify the
457             medium for which the CSS will be used. Typically these are 'screen' or 'print'.
458              
459             =back
460              
461             =cut
462              
463             sub create_inline_styles {
464             my $hash = shift || return;
465             my $media = $hash->{media} ? ' media="' . $hash->{media} . '"' : '';
466              
467             my $text = qq|\n|;
483             return $text;
484             }
485              
486             =head2 HTML Demoroniser Code
487              
488             =over 4
489              
490             =item demoroniser ( INPUT )
491              
492             Given a string, will replace the Microsoft "smart" characters with sensible
493             ACSII versions.
494              
495             =back
496              
497             =cut
498              
499             sub demoroniser {
500             my $str = shift;
501              
502             zap_cp1252($str);
503              
504             $str =~ s/\xE2\x80\x9A/,/g; # 82
505             $str =~ s/\xE2\x80\x9E/,,/g; # 84
506             $str =~ s/\xE2\x80\xA6/.../g; # 85
507              
508             $str =~ s/\xCB\x86/^/g; # 88
509              
510             $str =~ s/\xE2\x80\x98/`/g; # 91
511             $str =~ s/\xE2\x80\x99/'/g; # 92
512             $str =~ s/\xE2\x80\x9C/"/g; # 93
513             $str =~ s/\xE2\x80\x9D/"/g; # 94
514             $str =~ s/\xE2\x80\xA2/*/g; # 95
515             $str =~ s/\xE2\x80\x93/-/g; # 96
516             $str =~ s/\xE2\x80\x94/-/g; # 97
517              
518             $str =~ s/\xE2\x80\xB9/
519             $str =~ s/\xE2\x80\xBA/>/g; # 9B
520              
521             return $str;
522             }
523              
524             =head2 HTML Handling Code
525              
526             The following functions disassemble and reassemble the HTML code snippets,
527             validating and cleaning the code to fix any errors that may exist between the
528             template and content of the database.
529              
530             =over 4
531              
532             =item process_html ( INPUT [,LINE_BREAKS [,ALLOW]] )
533              
534             =item escape_html ( INPUT )
535              
536             =item unescape_html ( INPUT )
537              
538             =item cleanup_attr_style
539              
540             =item cleanup_attr_number
541              
542             =item cleanup_attr_multilength
543              
544             =item cleanup_attr_text
545              
546             =item cleanup_attr_length
547              
548             =item cleanup_attr_color
549              
550             =item cleanup_attr_uri
551              
552             =item cleanup_attr_tframe
553              
554             =item cleanup_attr_trules
555              
556             =item cleanup_html
557              
558             =item cleanup_tag
559              
560             =item cleanup_close
561              
562             =item cleanup_cdata
563              
564             =item cleanup_no_number
565              
566             =item check_url_valid
567              
568             =item cleanup_attr_inputtype
569              
570             =item cleanup_attr_method
571              
572             =item cleanup_attr_scriptlang
573              
574             =item cleanup_attr_scripttype
575              
576             =item strip_nonprintable
577              
578             =back
579              
580             =cut
581              
582             # Configuration
583             my $allow_html = 0;
584             my $line_breaks = 1;
585             # End configuration
586              
587             ##################################################################
588             #
589             # HTML handling code
590             #
591             # The code below provides some functions for manipulating HTML.
592             #
593             # process_html ( INPUT [,LINE_BREAKS [,ALLOW]] )
594             #
595             # Returns a modified version of the HTML string INPUT, with
596             # any potentially malicious HTML constructs (such as java,
597             # javascript and IMG tags) removed.
598             #
599             # If the LINE_BREAKS parameter is present and true then
600             # line breaks in the input will be converted to html
601             # tags in the output.
602             #
603             # If the ALLOW parameter is present and true then most
604             # harmless tags will be left in, otherwise all tags will be
605             # removed.
606             #
607             # escape_html ( INPUT )
608             #
609             # Returns a copy of the string INPUT with any HTML
610             # metacharacters replaced with character escapes.
611             #
612             # unescape_html ( INPUT )
613             #
614             # Returns a copy of the string INPUT with HTML character
615             # entities converted to literal characters where possible.
616             # Note that some entites have no 8-bit character equivalent,
617             # see "http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent"
618             # for some examples. unescape_html() leaves these entities
619             # in their encoded form.
620             #
621              
622             use vars qw(%html_entities $html_safe_chars %escape_html_map $escape_html_map);
623             use vars qw(%safe_tags %safe_style %tag_is_empty %closetag_is_optional
624             %closetag_is_dependent %force_closetag %transpose_tag
625             $convert_nl %auto_deinterleave $auto_deinterleave_pattern);
626              
627             # check the validity of a URL.
628              
629             sub process_html {
630             my ($text, $line_breaks, $allow_html) = @_;
631              
632             # cleanup erroneous XHTML patterns
633             if($text) {
634             $text =~ s!
!
!gsi;
635             $text =~ s!
    \s*
    !
      !gsi;
636             $text =~ s!
\s*!!gsi;
637             $text =~ s!
    \s*
!!gsi;
638             $text =~ s!
    \s*
!!gsi;
639             }
640              
641             # clean text of any nasties
642             #$text =~ s/[\x201A\x2018\x2019`]/'/g; # nasty single quotes
643             #$text =~ s/[\x201E\x201C\x201D]/"/g; # nasty double quotes
644              
645             cleanup_html( $text, $line_breaks, ($allow_html ? \%safe_tags : {}));
646             }
647              
648             BEGIN
649             {
650             %html_entities = (
651             'lt' => '<',
652             'gt' => '>',
653             'quot' => '"',
654             'amp' => '&',
655              
656             'nbsp' => "\240", 'iexcl' => "\241",
657             'cent' => "\242", 'pound' => "\243",
658             'curren' => "\244", 'yen' => "\245",
659             'brvbar' => "\246", 'sect' => "\247",
660             'uml' => "\250", 'copy' => "\251",
661             'ordf' => "\252", 'laquo' => "\253",
662             'not' => "\254", 'shy' => "\255",
663             'reg' => "\256", 'macr' => "\257",
664             'deg' => "\260", 'plusmn' => "\261",
665             'sup2' => "\262", 'sup3' => "\263",
666             'acute' => "\264", 'micro' => "\265",
667             'para' => "\266", 'middot' => "\267",
668             'cedil' => "\270", 'supl' => "\271",
669             'ordm' => "\272", 'raquo' => "\273",
670             'frac14' => "\274", 'frac12' => "\275",
671             'frac34' => "\276", 'iquest' => "\277",
672              
673             'Agrave' => "\300", 'Aacute' => "\301",
674             'Acirc' => "\302", 'Atilde' => "\303",
675             'Auml' => "\304", 'Aring' => "\305",
676             'AElig' => "\306", 'Ccedil' => "\307",
677             'Egrave' => "\310", 'Eacute' => "\311",
678             'Ecirc' => "\312", 'Euml' => "\313",
679             'Igrave' => "\314", 'Iacute' => "\315",
680             'Icirc' => "\316", 'Iuml' => "\317",
681             'ETH' => "\320", 'Ntilde' => "\321",
682             'Ograve' => "\322", 'Oacute' => "\323",
683             'Ocirc' => "\324", 'Otilde' => "\325",
684             'Ouml' => "\326", 'times' => "\327",
685             'Oslash' => "\330", 'Ugrave' => "\331",
686             'Uacute' => "\332", 'Ucirc' => "\333",
687             'Uuml' => "\334", 'Yacute' => "\335",
688             'THORN' => "\336", 'szlig' => "\337",
689              
690             'agrave' => "\340", 'aacute' => "\341",
691             'acirc' => "\342", 'atilde' => "\343",
692             'auml' => "\344", 'aring' => "\345",
693             'aelig' => "\346", 'ccedil' => "\347",
694             'egrave' => "\350", 'eacute' => "\351",
695             'ecirc' => "\352", 'euml' => "\353",
696             'igrave' => "\354", 'iacute' => "\355",
697             'icirc' => "\356", 'iuml' => "\357",
698             'eth' => "\360", 'ntilde' => "\361",
699             'ograve' => "\362", 'oacute' => "\363",
700             'ocirc' => "\364", 'otilde' => "\365",
701             'ouml' => "\366", 'divide' => "\367",
702             'oslash' => "\370", 'ugrave' => "\371",
703             'uacute' => "\372", 'ucirc' => "\373",
704             'uuml' => "\374", 'yacute' => "\375",
705             'thorn' => "\376", 'yuml' => "\377",
706             );
707              
708             #
709             # Build a map for representing characters in HTML.
710             #
711             $html_safe_chars = '()[]{}/?.,\\|;:@#~=+-_*^%$! ' . "\'\r\n\t";
712             $escape_html_map = qr{[\w\(\)\[\]\{\}\/\?\.\,\\\|;:\@#~=\+\-\*\^\%\$\!\s\']+};
713             %escape_html_map =
714             map {$_,$_} ( 'A'..'Z', 'a'..'z', '0'..'9',
715             split(//, $html_safe_chars)
716             );
717             foreach my $ent (keys %html_entities) {
718             $escape_html_map{$html_entities{$ent}} = "&$ent;";
719             }
720             foreach my $c (0..255) {
721             unless ( exists $escape_html_map{chr $c} ) {
722             $escape_html_map{chr $c} = sprintf '&#%d;', $c;
723             }
724             }
725              
726             #
727             # Tables for use by cleanup_html() (below).
728             #
729             # The main table is %safe_tags, which is a hash by tag name of
730             # all the tags that it's safe to leave in. The value for each
731             # tag is another hash, and each key of that hash defines an
732             # attribute that the tag is allowed to have.
733             #
734             # The values in the tag attribute hash can be undef (for an
735             # attribute that takes no value, for example the nowrap
736             # attribute in the tag ) or they can
737             # be coderefs pointing to subs for cleaning up the attribute
738             # values.
739             #
740             # These subs will called with the attribute value in $_, and
741             # they can return either a cleaned attribute value or undef.
742             # If undef is returned then the attribute will be deleted
743             # from the tag.
744             #
745             # The list of tags and attributes was taken from
746             # "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
747             #
748             # The %tag_is_empty table defines the set of tags that have
749             # no corresponding close tag.
750             #
751             # cleanup_html() moves close tags around to force all tags to
752             # be closed in the correct sequence. For example, the text
753             # "

foo

bar" will be converted to the text
754             # "

foo

bar".
755             #
756             # The %auto_deinterleave table defines the set of tags which
757             # should be automatically reopened if they're closed early
758             # in this way. All the tags involved must be in
759             # %auto_deinterleave for the tag to be reopened. For example,
760             # the text "bbbiii" will be converted into the
761             # text "bbbiii" rather than into the
762             # text "bbbiii", because *both* "b" and "i" are
763             # in %auto_deinterleave.
764             #
765             %tag_is_empty = (
766             'hr' => 1, 'link' => 1, 'param' => 1, 'img' => 1,
767             'br' => 1, 'area' => 1, 'input' => 1, 'basefont' => 1
768             );
769             %closetag_is_optional = ( );
770             %closetag_is_dependent = ( );
771             %force_closetag = (
772             'pre' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
773             'p' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
774             'h1' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
775             'h2' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
776             'h3' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
777             'h4' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
778             'h5' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
779             'h6' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
780             'table' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'pre' => 1, 'ul' => 1, 'ol' => 1 },
781             'ul' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1 },
782             'ol' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1 },
783             'li' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1, 'li' => 1 },
784             'form' => { 'p' => 1, 'h1' => 1, 'h2' => 1, 'h3' => 1, 'h4' => 1, 'h5' => 1, 'h6' => 1 },
785             );
786             %transpose_tag = ( 'b' => 'strong', 'u' => 'em' );
787             %auto_deinterleave = map {$_,1} qw(
788             tt i b big small u s strike font basefont
789             em strong dfn code q sub sup samp kbd var
790             cite abbr acronym span
791             );
792             $auto_deinterleave_pattern = join '|', keys %auto_deinterleave;
793             my %attr = (
794             'style' => \&cleanup_attr_style,
795             'name' => \&cleanup_attr_text,
796             'id' => \&cleanup_attr_text,
797             'class' => \&cleanup_attr_text,
798             'title' => \&cleanup_attr_text,
799             'onmouseover' => \&cleanup_attr_text,
800             'onmouseout' => \&cleanup_attr_text,
801             'onclick' => \&cleanup_attr_text,
802             'onfocus' => \&cleanup_attr_text,
803             'ondblclick' => \&cleanup_attr_text,
804             );
805             my %font_attr = (
806             %attr,
807             size => sub { /^([-+]?\d{1,3})$/ ? $1 : undef },
808             face => sub { /^([\w\-, ]{2,100})$/ ? $1 : undef },
809             color => \&cleanup_attr_color,
810             );
811             my %insdel_attr = (
812             %attr,
813             'cite' => \&cleanup_attr_uri,
814             'datetime' => \&cleanup_attr_text,
815             );
816             my %texta_attr = (
817             %attr,
818             align => sub { s/middle/center/i;
819             /^(left|center|right|justify)$/i ? lc $1 : undef
820             },
821             );
822             my %cellha_attr = (
823             align => sub { s/middle/center/i;
824             /^(left|center|right|justify|char)$/i
825             ? lc $1 : undef
826             },
827             char => sub { /^([\w\-])$/ ? $1 : undef },
828             charoff => \&cleanup_attr_length,
829             );
830             my %cellva_attr = (
831             valign => sub { s/center/middle/i;
832             /^(top|middle|bottom|baseline)$/i ? lc $1 : undef
833             },
834             );
835             my %cellhv_attr = ( %attr, %cellha_attr, %cellva_attr );
836             my %col_attr = (
837             %attr,
838             width => \&cleanup_attr_multilength,
839             span => \&cleanup_attr_number,
840             %cellhv_attr,
841             );
842             my %thtd_attr = (
843             %attr,
844             abbr => \&cleanup_attr_text,
845             axis => \&cleanup_attr_text,
846             headers => \&cleanup_attr_text,
847             scope => sub { /^(row|col|rowgroup|colgroup)$/i ? lc $1 : undef },
848             rowspan => \&cleanup_attr_number,
849             colspan => \&cleanup_attr_number,
850             %cellhv_attr,
851             nowrap => undef,
852             bgcolor => \&cleanup_attr_color,
853             width => \&cleanup_attr_number,
854             height => \&cleanup_attr_number,
855             );
856             my $none = {};
857             %safe_tags = (
858             # FORM CONTROLS
859             'form' => { %attr,
860             'method' => \&cleanup_attr_method,
861             'action' => \&cleanup_attr_text,
862             'enctype' => \&cleanup_attr_text,
863             'onsubmit' => \&cleanup_attr_text,
864             },
865             'button' => { %attr,
866             'type' => \&cleanup_attr_inputtype,
867             },
868             'input' => { %attr,
869             'type' => \&cleanup_attr_inputtype,
870             'size' => \&cleanup_attr_number,
871             'maxlength' => \&cleanup_attr_number,
872             'value' => \&cleanup_attr_text,
873             'checked' => \&cleanup_attr_text,
874             'readonly' => \&cleanup_attr_text,
875             'disabled' => \&cleanup_attr_text,
876             'src' => \&cleanup_attr_uri,
877             'width' => \&cleanup_attr_length,
878             'height' => \&cleanup_attr_length,
879             'alt' => \&cleanup_attr_text,
880             'onchange' => \&cleanup_attr_text,
881             },
882             'select' => { %attr,
883             'size' => \&cleanup_attr_number,
884             'title' => \&cleanup_attr_text,
885             'value' => \&cleanup_attr_text,
886             'multiple' => \&cleanup_attr_text,
887             'disabled' => \&cleanup_attr_text,
888             'onchange' => \&cleanup_attr_text,
889             },
890             'option' => { %attr,
891             'value' => \&cleanup_attr_text,
892             'selected' => \&cleanup_attr_text,
893             },
894             'textarea' => { %attr,
895             'rows' => \&cleanup_attr_number,
896             'cols' => \&cleanup_attr_number,
897             },
898             'label' => { %attr,
899             'for' => \&cleanup_attr_text,
900             },
901              
902             # LAYOUT STYLE
903             'style' => {
904             'type' => \&cleanup_attr_text,
905             },
906             'br' => { 'clear' => sub { /^(left|right|all|none)$/i ? lc $1 : undef }
907             },
908             'hr' => \%attr,
909             'em' => \%attr,
910             'strong' => \%attr,
911             'dfn' => \%attr,
912             'code' => \%attr,
913             'samp' => \%attr,
914             'kbd' => \%attr,
915             'var' => \%attr,
916             'cite' => \%attr,
917             'abbr' => \%attr,
918             'acronym' => \%attr,
919             'q' => { %attr, 'cite' => \&cleanup_attr_uri },
920             'blockquote' => { %attr, 'cite' => \&cleanup_attr_uri },
921             'sub' => \%attr,
922             'sup' => \%attr,
923             'tt' => \%attr,
924             'i' => \%attr,
925             'b' => \%attr,
926             'big' => \%attr,
927             'small' => \%attr,
928             'u' => \%attr,
929             's' => \%attr,
930             'font' => \%font_attr,
931             'h1' => \%texta_attr,
932             'h2' => \%texta_attr,
933             'h3' => \%texta_attr,
934             'h4' => \%texta_attr,
935             'h5' => \%texta_attr,
936             'h6' => \%texta_attr,
937             'p' => \%texta_attr,
938             'div' => \%texta_attr,
939             'span' => \%texta_attr,
940             'ul' => { %attr,
941             'type' => sub { /^(disc|square|circle)$/i ? lc $1 : undef },
942             'compact' => undef,
943             },
944             'ol' => { %attr,
945             'type' => \&cleanup_attr_text,
946             'compact' => undef,
947             'start' => \&cleanup_attr_number,
948             },
949             'li' => { %attr,
950             'type' => \&cleanup_attr_text,
951             'value' => \&cleanup_no_number,
952             },
953             'dl' => { %attr, 'compact' => undef },
954             'dt' => \%attr,
955             'dd' => \%attr,
956             'address' => \%attr,
957             'pre' => { %attr, 'width' => \&cleanup_attr_number },
958             'center' => \%attr,
959             'nobr' => $none,
960              
961             # FUNCTIONAL TAGS
962             'iframe' => { %attr,
963             'src' => \&cleanup_attr_uri,
964             'width' => \&cleanup_attr_length,
965             'height' => \&cleanup_attr_length,
966             'border' => \&cleanup_attr_number,
967             'alt' => \&cleanup_attr_text,
968             'align' => sub { s/middle/center/i;
969             /^(left|center|right)$/i ? lc $1 : undef
970             },
971             'title' => \&cleanup_attr_text,
972             },
973             'img' => { %attr,
974             'src' => \&cleanup_attr_uri,
975             'width' => \&cleanup_attr_length,
976             'height' => \&cleanup_attr_length,
977             'border' => \&cleanup_attr_number,
978             'alt' => \&cleanup_attr_text,
979             'align' => sub { s/middle/center/i;
980             /^(left|center|right)$/i ? lc $1 : undef
981             },
982             'title' => \&cleanup_attr_text,
983             'usemap' => \&cleanup_attr_text,
984             },
985             'map' => { %attr,
986             },
987             'area' => { %attr,
988             'shape' => \&cleanup_attr_text,
989             'coords' => \&cleanup_attr_text,
990             'href' => \&cleanup_attr_uri,
991             },
992             'table' => { %attr,
993             'frame' => \&cleanup_attr_tframe,
994             'rules' => \&cleanup_attr_trules,
995             %texta_attr,
996             'bgcolor' => \&cleanup_attr_color,
997             'width' => \&cleanup_attr_length,
998             'cellspacing' => \&cleanup_attr_length,
999             'cellpadding' => \&cleanup_attr_length,
1000             'border' => \&cleanup_attr_number,
1001             'summary' => \&cleanup_attr_text,
1002             },
1003             'caption' => { %attr,
1004             'align' => sub { /^(top|bottom|left|right)$/i ? lc $1 : undef },
1005             },
1006             'colgroup' => \%col_attr,
1007             'col' => \%col_attr,
1008             'thead' => \%cellhv_attr,
1009             'tfoot' => \%cellhv_attr,
1010             'tbody' => \%cellhv_attr,
1011             'tr' => { %attr,
1012             bgcolor => \&cleanup_attr_color,
1013             %cellhv_attr,
1014             },
1015             'th' => \%thtd_attr,
1016             'td' => \%thtd_attr,
1017             'ins' => \%insdel_attr,
1018             'del' => \%insdel_attr,
1019             'a' => { %attr,
1020             href => \&cleanup_attr_uri,
1021             style => \&cleanup_attr_text,
1022             target => \&cleanup_attr_text,
1023             rel => \&cleanup_attr_text,
1024             },
1025              
1026             'script' => {
1027             language => \&cleanup_attr_scriptlang,
1028             type => \&cleanup_attr_scripttype,
1029             src => \&cleanup_attr_uri,
1030             },
1031             'noscript' => { %attr,
1032             },
1033             'link' => { %attr,
1034             href => \&cleanup_attr_uri,
1035             'rel' => \&cleanup_attr_text,
1036             'type' => \&cleanup_attr_text,
1037             'media' => \&cleanup_attr_text,
1038             },
1039             'object' => { %attr,
1040             'width' => \&cleanup_attr_length,
1041             'height' => \&cleanup_attr_length,
1042             style => \&cleanup_attr_text,
1043             type => \&cleanup_attr_text,
1044             data => \&cleanup_attr_text,
1045             classid => \&cleanup_attr_text,
1046             codebase => \&cleanup_attr_text,
1047             },
1048             'param' => {
1049             name => \&cleanup_attr_text,
1050             value => \&cleanup_attr_text,
1051             },
1052             'embed' => { %attr,
1053             'src' => \&cleanup_attr_uri,
1054             'bgcolor' => \&cleanup_attr_color,
1055             'width' => \&cleanup_attr_length,
1056             'height' => \&cleanup_attr_length,
1057             'pluginspage' => \&cleanup_attr_uri,
1058             flashvars => \&cleanup_attr_text,
1059             type => \&cleanup_attr_text,
1060             quality => \&cleanup_attr_text,
1061             allowScriptAccess => \&cleanup_attr_text,
1062             allowNetworking => \&cleanup_attr_text,
1063             },
1064             );
1065              
1066             %safe_style = (
1067             'animation' => \&cleanup_attr_text,
1068             'animation-name' => \&cleanup_attr_text,
1069             'animation-duration' => \&cleanup_attr_text,
1070             'animation-timing-function' => \&cleanup_attr_text,
1071             'animation-delay' => \&cleanup_attr_text,
1072             'animation-iteration-count' => \&cleanup_attr_text,
1073             'animation-direction' => \&cleanup_attr_text,
1074             'animation-play-state' => \&cleanup_attr_text,
1075             'appearance' => \&cleanup_attr_text,
1076             'backface-visibility' => \&cleanup_attr_text,
1077             'background' => \&cleanup_attr_text,
1078             'background-attachment' => \&cleanup_attr_text,
1079             'background-color' => \&cleanup_attr_color,
1080             'background-image' => \&cleanup_attr_text,
1081             'background-position' => \&cleanup_attr_text,
1082             'background-repeat' => \&cleanup_attr_text,
1083             'background-clip' => \&cleanup_attr_text,
1084             'background-origin' => \&cleanup_attr_text,
1085             'background-size' => \&cleanup_attr_text,
1086             'border' => \&cleanup_attr_text,
1087             'border-bottom' => \&cleanup_attr_text,
1088             'border-bottom-color' => \&cleanup_attr_color,
1089             'border-bottom-style' => \&cleanup_attr_text,
1090             'border-bottom-width' => \&cleanup_attr_length,
1091             'border-collapse' => \&cleanup_attr_text,
1092             'border-color' => \&cleanup_attr_color,
1093             'border-left' => \&cleanup_attr_text,
1094             'border-left-color' => \&cleanup_attr_color,
1095             'border-left-style' => \&cleanup_attr_text,
1096             'border-left-width' => \&cleanup_attr_length,
1097             'border-right' => \&cleanup_attr_text,
1098             'border-right-color' => \&cleanup_attr_color,
1099             'border-right-style' => \&cleanup_attr_text,
1100             'border-right-width' => \&cleanup_attr_length,
1101             'border-spacing' => \&cleanup_attr_text,
1102             'border-style' => \&cleanup_attr_text,
1103             'border-top' => \&cleanup_attr_text,
1104             'border-top-color' => \&cleanup_attr_color,
1105             'border-top-style' => \&cleanup_attr_text,
1106             'border-top-width' => \&cleanup_attr_length,
1107             'border-width' => \&cleanup_attr_length,
1108             'border-bottom-left-radius' => \&cleanup_attr_text,
1109             'border-bottom-right-radius' => \&cleanup_attr_text,
1110             'border-image' => \&cleanup_attr_text,
1111             'border-image-outset' => \&cleanup_attr_text,
1112             'border-image-repeat' => \&cleanup_attr_text,
1113             'border-image-slice' => \&cleanup_attr_text,
1114             'border-image-source' => \&cleanup_attr_text,
1115             'border-image-width' => \&cleanup_attr_length,
1116             'border-radius' => \&cleanup_attr_text,
1117             'border-top-left-radius' => \&cleanup_attr_text,
1118             'border-top-right-radius' => \&cleanup_attr_text,
1119             'bottom' => \&cleanup_attr_text,
1120             'box' => \&cleanup_attr_text,
1121             'box-align' => \&cleanup_attr_text,
1122             'box-direction' => \&cleanup_attr_text,
1123             'box-flex' => \&cleanup_attr_text,
1124             'box-flex-group' => \&cleanup_attr_text,
1125             'box-lines' => \&cleanup_attr_text,
1126             'box-ordinal-group' => \&cleanup_attr_text,
1127             'box-orient' => \&cleanup_attr_text,
1128             'box-pack' => \&cleanup_attr_text,
1129             'box-sizing' => \&cleanup_attr_text,
1130             'box-shadow' => \&cleanup_attr_text,
1131             'caption-side' => \&cleanup_attr_text,
1132             'clear' => \&cleanup_attr_text,
1133             'clip' => \&cleanup_attr_text,
1134             'color' => \&cleanup_attr_color,
1135             'column' => \&cleanup_attr_text,
1136             'column-count' => \&cleanup_attr_text,
1137             'column-fill' => \&cleanup_attr_text,
1138             'column-gap' => \&cleanup_attr_text,
1139             'column-rule' => \&cleanup_attr_text,
1140             'column-rule-color' => \&cleanup_attr_text,
1141             'column-rule-style' => \&cleanup_attr_text,
1142             'column-rule-width' => \&cleanup_attr_length,
1143             'column-span' => \&cleanup_attr_text,
1144             'column-width' => \&cleanup_attr_length,
1145             'columns' => \&cleanup_attr_text,
1146             'content' => \&cleanup_attr_text,
1147             'counter-increment' => \&cleanup_attr_text,
1148             'counter-reset' => \&cleanup_attr_text,
1149             'cursor' => \&cleanup_attr_text,
1150             'direction' => \&cleanup_attr_text,
1151             'display' => \&cleanup_attr_text,
1152             'empty-cells' => \&cleanup_attr_text,
1153             'float' => \&cleanup_attr_text,
1154             'font' => \&cleanup_attr_text,
1155             'font-family' => \&cleanup_attr_text,
1156             'font-size' => \&cleanup_attr_text,
1157             'font-style' => \&cleanup_attr_text,
1158             'font-variant' => \&cleanup_attr_text,
1159             'font-weight' => \&cleanup_attr_length,
1160             '@font-face' => \&cleanup_attr_text,
1161             'font-size-adjust' => \&cleanup_attr_text,
1162             'font-stretch' => \&cleanup_attr_text,
1163             'grid-columns' => \&cleanup_attr_text,
1164             'grid-rows' => \&cleanup_attr_text,
1165             'hanging-punctuation' => \&cleanup_attr_text,
1166             'height' => \&cleanup_attr_length,
1167             'icon' => \&cleanup_attr_text,
1168             '@keyframes' => \&cleanup_attr_text,
1169             'left' => \&cleanup_attr_length,
1170             'letter-spacing' => \&cleanup_attr_text,
1171             'line-height' => \&cleanup_attr_text,
1172             'list-style' => \&cleanup_attr_text,
1173             'list-style-image' => \&cleanup_attr_text,
1174             'list-style-position' => \&cleanup_attr_text,
1175             'list-style-type' => \&cleanup_attr_text,
1176             'margin' => \&cleanup_attr_text,
1177             'margin-bottom' => \&cleanup_attr_length,
1178             'margin-left' => \&cleanup_attr_length,
1179             'margin-right' => \&cleanup_attr_length,
1180             'margin-top' => \&cleanup_attr_length,
1181             'max-height' => \&cleanup_attr_length,
1182             'max-width' => \&cleanup_attr_length,
1183             'min-height' => \&cleanup_attr_length,
1184             'min-width' => \&cleanup_attr_length,
1185             'nav' => \&cleanup_attr_text,
1186             'nav-down' => \&cleanup_attr_text,
1187             'nav-index' => \&cleanup_attr_text,
1188             'nav-left' => \&cleanup_attr_text,
1189             'nav-right' => \&cleanup_attr_text,
1190             'nav-up' => \&cleanup_attr_text,
1191             'opacity' => \&cleanup_attr_text,
1192             'outline' => \&cleanup_attr_text,
1193             'outline-color' => \&cleanup_attr_color,
1194             'outline-offset' => \&cleanup_attr_text,
1195             'outline-style' => \&cleanup_attr_text,
1196             'outline-width' => \&cleanup_attr_length,
1197             'overflow' => \&cleanup_attr_text,
1198             'overflow-x' => \&cleanup_attr_text,
1199             'overflow-y' => \&cleanup_attr_text,
1200             'padding' => \&cleanup_attr_text,
1201             'padding-bottom' => \&cleanup_attr_length,
1202             'padding-left' => \&cleanup_attr_length,
1203             'padding-right' => \&cleanup_attr_length,
1204             'padding-top' => \&cleanup_attr_length,
1205             'page-break' => \&cleanup_attr_text,
1206             'page-break-after' => \&cleanup_attr_text,
1207             'page-break-before' => \&cleanup_attr_text,
1208             'page-break-inside' => \&cleanup_attr_text,
1209             'perspective' => \&cleanup_attr_text,
1210             'perspective-origin' => \&cleanup_attr_text,
1211             'position' => \&cleanup_attr_text,
1212             'punctuation-trim' => \&cleanup_attr_text,
1213             'quotes' => \&cleanup_attr_text,
1214             'resize' => \&cleanup_attr_text,
1215             'right' => \&cleanup_attr_length,
1216             'rotation' => \&cleanup_attr_text,
1217             'rotation-point' => \&cleanup_attr_text,
1218             'table-layout' => \&cleanup_attr_text,
1219             'target' => \&cleanup_attr_text,
1220             'target-name' => \&cleanup_attr_text,
1221             'target-new' => \&cleanup_attr_text,
1222             'target-position' => \&cleanup_attr_text,
1223             'text' => \&cleanup_attr_text,
1224             'text-align' => \&cleanup_attr_text,
1225             'text-decoration' => \&cleanup_attr_text,
1226             'text-indent' => \&cleanup_attr_text,
1227             'text-justify' => \&cleanup_attr_text,
1228             'text-outline' => \&cleanup_attr_text,
1229             'text-overflow' => \&cleanup_attr_text,
1230             'text-shadow' => \&cleanup_attr_text,
1231             'text-transform' => \&cleanup_attr_text,
1232             'text-wrap' => \&cleanup_attr_text,
1233             'top' => \&cleanup_attr_length,
1234             'transform' => \&cleanup_attr_text,
1235             'transform-origin' => \&cleanup_attr_text,
1236             'transform-style' => \&cleanup_attr_text,
1237             'transition' => \&cleanup_attr_text,
1238             'transition-property' => \&cleanup_attr_text,
1239             'transition-duration' => \&cleanup_attr_text,
1240             'transition-timing-function' => \&cleanup_attr_text,
1241             'transition-delay' => \&cleanup_attr_text,
1242             'vertical-align' => \&cleanup_attr_text,
1243             'visibility' => \&cleanup_attr_text,
1244             'width' => \&cleanup_attr_length,
1245             'white-space' => \&cleanup_attr_text,
1246             'word-spacing' => \&cleanup_attr_text,
1247             'word-break' => \&cleanup_attr_text,
1248             'word-wrap' => \&cleanup_attr_text,
1249             'z-index' => \&cleanup_attr_text
1250             );
1251             }
1252              
1253              
1254             sub cleanup_attr_style {
1255             my @clean = ();
1256             foreach my $elt (split /;/, $_) {
1257             next if $elt =~ m#^\s*$#;
1258             if ( $elt =~ m#^\s*([\w\-]+)\s*:\s*(.+?)\s*$#s ) {
1259             my ($key, $val) = (lc $1, $2);
1260             local $_ = $val;
1261             my $sub = $safe_style{$key};
1262             if (defined $sub) {
1263             my $cleanval = &{$sub}();
1264             if (defined $cleanval) {
1265             push @clean, "$key:$val";
1266             }
1267             }
1268             }
1269             }
1270             return join '; ', @clean;
1271             }
1272             sub cleanup_attr_number {
1273             /^(\d+)$/ ? $1 : undef;
1274             }
1275             sub cleanup_attr_method {
1276             /^(get|post)$/i ? lc $1 : 'post';
1277             }
1278             sub cleanup_attr_inputtype {
1279             /^(text|password|checkbox|radio|submit|reset|file|hidden|image|button)$/i ? lc $1 : undef;
1280             }
1281             sub cleanup_attr_multilength {
1282             /^(\d+(?:\.\d+)?[*%]?)$/ ? $1 : undef;
1283             }
1284             sub cleanup_attr_text {
1285             tr/-a-zA-Z0-9_()[]{}\/?.,\\|;:&@#~=+*^%$'! \xc0-\xff//dc;
1286             $_;
1287             }
1288             sub cleanup_attr_length {
1289             /^(\d+(\%|px|em)?)$/ ? $1 : undef;
1290             }
1291             sub cleanup_attr_color {
1292             /^(\w{2,20}|#[\da-fA-F]{3}|#[\da-fA-F]{6})$/ or die "color <<$_>> bad";
1293             /^(\w{2,20}|#[\da-fA-F]{3}|#[\da-fA-F]{6})$/ ? $1 : undef;
1294             }
1295             sub cleanup_attr_uri {
1296             check_url_valid($_) ? $_ : undef;
1297             }
1298             sub cleanup_attr_tframe {
1299             /^(void|above|below|hsides|lhs|rhs|vsides|box|border)$/i
1300             ? lc $1 : undef;
1301             }
1302             sub cleanup_attr_trules {
1303             /^(none|groups|rows|cols|all)$/i ? lc $1 : undef;
1304             }
1305              
1306             sub cleanup_attr_scriptlang {
1307             /^(javascript)$/i ? lc $1 : undef;
1308             }
1309             sub cleanup_attr_scripttype {
1310             /^(text\/javascript)$/i ? lc $1 : undef;
1311             }
1312              
1313             use vars qw(@stack $safe_tags $convert_nl);
1314             sub cleanup_html {
1315             local ($_, $convert_nl, $safe_tags) = @_;
1316             local @stack = ();
1317              
1318             return '' unless($_);
1319              
1320             my $ignore_comments = 0;
1321             if($ignore_comments) {
1322             s[
1323             (?: ) |
1324             (?: <[?!].*?> ) |
1325             (?: <([a-z0-9]+)\b((?:[^>'"]|"[^"]*"|'[^']*')*)> ) |
1326             (?: ) |
1327             (?: (.[^<]*) )
1328             ][
1329             defined $1 ? cleanup_tag(lc $1, $2) :
1330             defined $3 ? cleanup_close(lc $3) :
1331             defined $4 ? cleanup_cdata($4) :
1332             ''
1333             ]igesx;
1334             } else {
1335             s[
1336             (?: () ) |
1337             (?: ) |
1338             (?: <[?!].*?> ) |
1339             (?: <([a-z0-9]+)\b((?:[^>'"]|"[^"]*"|'[^']*')*)> ) |
1340             (?: ) |
1341             (?: (.[^<]*) )
1342             ][
1343             defined $1 ? $1 :
1344             defined $2 ? cleanup_tag(lc $2, $3) :
1345             defined $4 ? cleanup_close(lc $4) :
1346             defined $5 ? cleanup_cdata($5) :
1347             ''
1348             ]igesx;
1349             }
1350              
1351             # Close anything that was left open
1352             $_ .= join '', map "{NAME}>", @stack;
1353              
1354             # Where we turned foo into foo,
1355             # take out the pointless .
1356             1 while s#<($auto_deinterleave_pattern)\b[^>]*>( |\s)*##go;
1357              
1358             # cleanup p elements
1359             s!\s+

!

!g;
1360             s!

!!g;
1361              
1362             # Element pre is not declared in p list of possible children
1363             s!

\s*(

.*?
)\s*

!$1!g;
1364              
1365             return $_;
1366             }
1367              
1368             sub cleanup_tag {
1369             my ($tag, $attrs) = @_;
1370             unless (exists $safe_tags->{$tag}) {
1371             return '';
1372             }
1373              
1374             # for XHTML conformity
1375             $tag = $transpose_tag{$tag} if($transpose_tag{$tag});
1376              
1377             my $html = '';
1378             if($force_closetag{$tag}) {
1379             while (scalar @stack and $force_closetag{$tag}{$stack[0]{NAME}}) {
1380             $html = cleanup_close($stack[0]{NAME});
1381             }
1382             }
1383              
1384             my $t = $safe_tags->{$tag};
1385             my $safe_attrs = '';
1386             while ($attrs =~ s#^\s*(\w+)(?:\s*=\s*(?:([^"'>\s]+)|"([^"]*)"|'([^']*)'))?##) {
1387             my $attr = lc $1;
1388             my $val = ( defined $2 ? $2 :
1389             defined $3 ? unescape_html($3) :
1390             defined $4 ? unescape_html($4) :
1391             '$attr'
1392             );
1393             unless (exists $t->{$attr}) {
1394             next;
1395             }
1396             if (defined $t->{$attr}) {
1397             local $_ = $val;
1398             my $cleaned = &{ $t->{$attr} }();
1399             if (defined $cleaned) {
1400             $safe_attrs .= qq| $attr="${\( escape_html($cleaned) )}"|;
1401             }
1402             } else {
1403             $safe_attrs .= " $attr";
1404             }
1405             }
1406              
1407             my $str;
1408             if (exists $tag_is_empty{$tag}) {
1409             $str = "$html<$tag$safe_attrs />";
1410             } elsif (exists $closetag_is_optional{$tag}) {
1411             $str = "$html<$tag$safe_attrs>";
1412             # } elsif (exists $closetag_is_dependent{$tag} && $safe_attrs =~ /$closetag_is_dependent{$tag}=/) {
1413             # return "$html<$tag$safe_attrs />";
1414             } else {
1415             my $full = "<$tag$safe_attrs>";
1416             unshift @stack, { NAME => $tag, FULL => $full };
1417             $str = "$html$full";
1418             }
1419             #LogDebug("cleanup_tag: str=$str");
1420             return $str;
1421             }
1422              
1423             sub cleanup_close {
1424             my $tag = shift;
1425              
1426             # for XHTML conformity
1427             $tag = $transpose_tag{$tag} if($transpose_tag{$tag});
1428              
1429             # Ignore a close without an open
1430             unless (grep {$_->{NAME} eq $tag} @stack) {
1431             return '';
1432             }
1433              
1434             # Close open tags up to the matching open
1435             my @close = ();
1436             while (scalar @stack and $stack[0]{NAME} ne $tag) {
1437             push @close, shift @stack;
1438             }
1439             push @close, shift @stack;
1440              
1441             my $html = join '', map {"{NAME}>"} @close;
1442              
1443             # Reopen any we closed early if all that were closed are
1444             # configured to be auto deinterleaved.
1445             unless (grep {! exists $auto_deinterleave{$_->{NAME}} } @close) {
1446             pop @close;
1447             $html .= join '', map {$_->{FULL}} reverse @close;
1448             unshift @stack, @close;
1449             }
1450              
1451             return $html;
1452             }
1453              
1454             sub cleanup_cdata {
1455             local $_ = shift;
1456              
1457             return $_ if(scalar @stack and $stack[0]{NAME} eq 'script');
1458              
1459             s[ (?: & (
1460             [a-zA-Z0-9]{2,15} |
1461             [#][0-9]{2,6} |
1462             [#][xX][a-fA-F0-9]{2,6} | ) \b ;?
1463             ) | ($escape_html_map) | (.)
1464             ][
1465             defined $1 ? "&$1;" : defined $2 ? $2 : $3
1466             ]gesx;
1467              
1468             # substitute newlines in the input for html line breaks if required.
1469             s%\cM?\n%
\n%g if $convert_nl;
1470              
1471             return $_;
1472             }
1473              
1474             # subroutine to escape the necessary characters to the appropriate HTML
1475             # entities
1476              
1477             sub escape_html {
1478             my $str = shift or return '';
1479             $str = encode_entities($str);
1480             $str =~ s/&(#x?\d+;)/&$1/g; # avoid double encoding of hex/dec characters
1481             return $str;
1482             }
1483              
1484             # subroutine to unescape escaped HTML entities. Note that some entites
1485             # have no 8-bit character equivalent, see
1486             # "http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent" for some examples.
1487             # unescape_html() leaves these entities in their encoded form.
1488              
1489             sub unescape_html {
1490             my $str = shift or return '';
1491             $str = decode_entities($str);
1492             return strip_nonprintable($str);
1493             }
1494              
1495             sub check_url_valid {
1496             my $url = shift;
1497              
1498             $url = "$tvars{cgipath}/$tvars{script}$url" if($url =~ /^\?/);
1499              
1500             # allow in page URLs
1501             return 1 if $url =~ m!^\#!;
1502              
1503             # allow relative URLs with sane values
1504             return 1 if $url =~ m!^[a-z0-9_\-\.\,\+\/#]+$!i;
1505              
1506             # allow mailto email addresses
1507             return 1 if $url =~ m#mailto:([-+=\w\'.\&\\//]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)#i;
1508              
1509             # allow javascript calls
1510             return 1 if $url =~ m#^javascript:#i;
1511              
1512             # $url =~ m< ^ ((?:ftp|http|https):// [\w\-\.]+ (?:\:\d+)?)?
1513             # (?: /? [\w\-.!~*'(|);/\@+\$,%#]* )?
1514             # (?: \? [\w\-.!~*'(|);/\@&=+\$,%#]* )?
1515             # $
1516             # >x ? 1 : 0;
1517             return $url =~ m< ^ $settings{urlregex} $ >x ? 1 : 0;
1518             }
1519              
1520             sub strip_nonprintable {
1521             my $text = shift;
1522             return '' unless defined $text;
1523              
1524             $text=~ tr#\t\n\040-\176\241-\377# #cs;
1525             return $text;
1526             }
1527              
1528             #
1529             # End of HTML handling code
1530             #
1531             ##################################################################
1532              
1533             1;
1534              
1535             __END__