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!?(span|tbody)[^>]*>!!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!?(\w+)(?:\s+[^>]*)?>!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! |
|||||||
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! |
|||||||
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! |
|||||||
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!! |
|||||||
635 | $text =~ s!
!
|
|||||||
636 | $text =~ s! \s*!!gsi; |
|||||||
637 | $text =~ s!
|
|||||||
638 | $text =~ s!
|
|||||||
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 | # "foobar" will be converted to the text |
|||||||
754 | # "foobar". |
|||||||
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 | (?: ([a-z0-9]+)> ) | | |||||||
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 | (?: ([a-z0-9]+)> ) | | |||||||
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)*\1>##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__ |