Moved some stuff.
[wakaba-kareha.git] / kareha / wakautils.pl
blob0866b3a0f1bb6f507be7646b7f70129bdf0f0555
1 # wakautils.pl v8.12
3 use strict;
5 use Time::Local;
6 use Socket;
8 my $has_md5=0;
9 eval 'use Digest::MD5 qw(md5)';
10 $has_md5=1 unless $@;
12 my $has_encode=0;
13 eval 'use Encode qw(decode)';
14 $has_encode=1 unless $@;
17 use constant MAX_UNICODE => 1114111;
20 # HTML utilities
23 my $protocol_re=qr{(?:http://|https://|ftp://|mailto:|news:|irc:)};
24 my $url_re=qr{(${protocol_re}[^\s<>()"]*?(?:\([^\s<>()"]*?\)[^\s<>()"]*?)*)((?:\s|<|>|"|\.||\]|!|\?|,|&#44;|&quot;)*(?:[\s<>()"]|$))};
26 sub protocol_regexp() { return $protocol_re }
28 sub url_regexp() { return $url_re }
30 sub abbreviate_html($$$)
32 my ($html,$max_lines,$approx_len)=@_;
33 my ($lines,$chars,@stack);
35 return undef unless($max_lines);
37 while($html=~m!(?:([^<]+)|<(/?)(\w+).*?(/?)>)!g)
39 my ($text,$closing,$tag,$implicit)=($1,$2,lc($3),$4);
41 if($text) { $chars+=length $text; }
42 else
44 push @stack,$tag if(!$closing and !$implicit);
45 pop @stack if($closing);
47 if(($closing or $implicit) and ($tag eq "p" or $tag eq "blockquote" or $tag eq "pre"
48 or $tag eq "li" or $tag eq "ol" or $tag eq "ul" or $tag eq "br"))
50 $lines+=int($chars/$approx_len)+1;
51 $lines++ if($tag eq "p" or $tag eq "blockquote");
52 $chars=0;
55 if($lines>=$max_lines)
57 # check if there's anything left other than end-tags
58 return undef if (substr $html,pos $html)=~m!^(?:\s*</\w+>)*\s*$!s;
60 my $abbrev=substr $html,0,pos $html;
61 while(my $tag=pop @stack) { $abbrev.="</$tag>" }
63 return $abbrev;
68 return undef;
71 sub sanitize_html($%)
73 my ($html,%tags)=@_;
74 my (@stack,$clean);
75 my $entity_re=qr/&(?!\#[0-9]+;|\#x[0-9a-fA-F]+;|amp;|lt;|gt;)/;
77 while($html=~/(?:([^<]+)|<([^<>]*)>|(<))/sg)
79 my ($text,$tag,$lt)=($1,$2,$3);
81 if($lt)
83 $clean.="&lt;";
85 elsif($text)
87 $text=~s/$entity_re/&amp;/g;
88 $text=~s/>/&gt;/g;
89 $clean.=$text;
91 else
93 if($tag=~m!^\s*(/?)\s*([a-z0-9_:\-\.]+)(?:\s+(.*?)|)\s*(/?)\s*$!si)
95 my ($closing,$name,$args,$implicit)=($1,lc($2),$3,$4);
97 if($tags{$name})
99 if($closing)
101 if(grep { $_ eq $name } @stack)
103 my $entry;
105 do {
106 $entry=pop @stack;
107 $clean.="</$entry>";
108 } until $entry eq $name;
111 else
113 my %args;
115 $args=~s/\s/ /sg;
117 while($args=~/([a-z0-9_:\-\.]+)(?:\s*=\s*(?:'([^']*?)'|"([^"]*?)"|['"]?([^'" ]*))|)/gi)
119 my ($arg,$value)=(lc($1),defined($2)?$2:defined($3)?$3:$4);
120 $value=$arg unless defined($value);
122 my $type=$tags{$name}{args}{$arg};
124 if($type)
126 my $passes=1;
128 if($type=~/url/i) { $passes=0 unless $value=~/(?:^${protocol_re}|^[^:]+$)/ }
129 if($type=~/number/i) { $passes=0 unless $value=~/^[0-9]+$/ }
131 if($passes)
133 $value=~s/$entity_re/&amp;/g;
134 $args{$arg}=$value;
139 $args{$_}=$tags{$name}{forced}{$_} for (keys %{$tags{$name}{forced}}); # override forced arguments
141 my $cleanargs=join " ",map {
142 my $value=$args{$_};
143 $value=~s/'/%27/g;
144 "$_='$value'";
145 } keys %args;
147 $implicit="/" if($tags{$name}{empty});
149 push @stack,$name unless $implicit;
151 $clean.="<$name";
152 $clean.=" $cleanargs" if $cleanargs;
153 #$clean.=" $implicit" if $implicit;
154 $clean.=">";
155 $clean.="</$name>" if $implicit;
162 my $entry;
163 while($entry=pop @stack) { $clean.="</$entry>" }
165 return $clean;
168 sub describe_allowed(%)
170 my (%tags)=@_;
172 return join ", ",map { $_.($tags{$_}{args}?" (".(join ", ",sort keys %{$tags{$_}{args}}).")":"") } sort keys %tags;
175 sub do_wakabamark($;$$)
177 my ($text,$handler,$simplify)=@_;
178 my $res;
180 my @lines=split /(?:\r\n|\n|\r)/,$text;
182 while(defined($_=$lines[0]))
184 if(/^\s*$/) { shift @lines; } # skip empty lines
185 elsif(/^(1\.|[\*\+\-]) /) # lists
187 my ($tag,$re,$skip,$html);
189 if($1 eq "1.") { $tag="ol"; $re=qr/[0-9]+\./; $skip=1; }
190 else { $tag="ul"; $re=qr/\Q$1\E/; $skip=0; }
192 while($lines[0]=~/^($re)(?: |\t)(.*)/)
194 my $spaces=(length $1)+1;
195 my $item="$2\n";
196 shift @lines;
198 while($lines[0]=~/^(?: {1,$spaces}|\t)(.*)/) { $item.="$1\n"; shift @lines }
199 $html.="<li>".do_wakabamark($item,$handler,1)."</li>";
201 if($skip) { while(@lines and $lines[0]=~/^\s*$/) { shift @lines; } } # skip empty lines
203 $res.="<$tag>$html</$tag>";
205 elsif(/^(?: |\t)/) # code sections
207 my @code;
208 while($lines[0]=~/^(?: |\t)(.*)/) { push @code,$1; shift @lines; }
209 $res.="<pre><code>".(join "<br />",@code)."</code></pre>";
211 elsif(/^&gt;/) # quoted sections
213 my @quote;
214 while($lines[0]=~/^(&gt;.*)/) { push @quote,$1; shift @lines; }
215 $res.="<blockquote>".do_spans($handler,@quote)."</blockquote>";
217 #while($lines[0]=~/^&gt;(.*)/) { push @quote,$1; shift @lines; }
218 #$res.="<blockquote>".do_blocks($handler,@quote)."</blockquote>";
220 else # normal paragraph
222 my @text;
223 while($lines[0]!~/^(?:\s*$|1\. |[\*\+\-] |&gt;| |\t)/) { push @text,shift @lines; }
224 if(!defined($lines[0]) and $simplify) { $res.=do_spans($handler,@text) }
225 else { $res.="<p>".do_spans($handler,@text)."</p>" }
227 $simplify=0;
230 return $res;
233 sub do_spans($@)
235 my $handler=shift;
236 return join "<br />",map
238 my $line=$_;
239 my @hidden;
241 # hide <code> sections
242 $line=~s{ (?<![\x80-\x9f\xe0-\xfc]) (`+) ([^<>]+?) (?<![\x80-\x9f\xe0-\xfc]) \1}{push @hidden,"<code>$2</code>"; "<!--$#hidden-->"}sgex;
244 # make URLs into links and hide them
245 $line=~s{$url_re}{push @hidden,"<a href=\"$1\" rel=\"nofollow\">$1\</a>"; "<!--$#hidden-->$2"}sge;
247 # do <strong>
248 $line=~s{ (?<![0-9a-zA-Z\*_\x80-\x9f\xe0-\xfc]) (\*\*|__) (?![<>\s\*_]) ([^<>]+?) (?<![<>\s\*_\x80-\x9f\xe0-\xfc]) \1 (?![0-9a-zA-Z\*_]) }{<strong>$2</strong>}gx;
250 # do <em>
251 $line=~s{ (?<![0-9a-zA-Z\*_\x80-\x9f\xe0-\xfc]) (\*|_) (?![<>\s\*_]) ([^<>]+?) (?<![<>\s\*_\x80-\x9f\xe0-\xfc]) \1 (?![0-9a-zA-Z\*_]) }{<em>$2</em>}gx;
253 # do ^H
254 if($]>5.007)
256 my $regexp;
257 $regexp=qr/(?:&#?[0-9a-zA-Z]+;|[^&<>])(?<!\^H)(??{$regexp})?\^H/;
258 $line=~s{($regexp)}{"<del>".(substr $1,0,(length $1)/3)."</del>"}gex;
261 $line=$handler->($line) if($handler);
263 # fix up hidden sections
264 $line=~s{<!--([0-9]+)-->}{$hidden[$1]}ge;
266 $line;
267 } @_;
270 sub compile_template($;$)
272 my ($str,$nostrip)=@_;
273 my $code;
275 unless($nostrip)
277 $str=~s/^\s+//;
278 $str=~s/\s+$//;
279 $str=~s/\n\s*/ /sg;
282 while($str=~m!(.*?)(<(/?)(var|const|if|loop)(?:|\s+(.*?[^\\]))>|$)!sg)
284 my ($html,$tag,$closing,$name,$args)=($1,$2,$3,$4,$5);
286 $html=~s/(['\\])/\\$1/g;
287 $code.="\$res.='$html';" if(length $html);
288 $args=~s/\\>/>/g;
290 if($tag)
292 if($closing)
294 if($name eq 'if') { $code.='}' }
295 elsif($name eq 'loop') { $code.='$$_=$__ov{$_} for(keys %__ov);}}' }
297 else
299 if($name eq 'var') { $code.='$res.=eval{'.$args.'};' }
300 elsif($name eq 'const') { my $const=eval $args; $const=~s/(['\\])/\\$1/g; $code.='$res.=\''.$const.'\';' }
301 elsif($name eq 'if') { $code.='if(eval{'.$args.'}){' }
302 elsif($name eq 'loop')
303 { $code.='my $__a=eval{'.$args.'};if($__a){for(@$__a){my %__v=%{$_};my %__ov;for(keys %__v){$__ov{$_}=$$_;$$_=$__v{$_};}' }
308 my $sub=eval
309 'no strict; sub { '.
310 'my $port=$ENV{SERVER_PORT}==80?"":":$ENV{SERVER_PORT}";'.
311 'my $self=$ENV{SCRIPT_NAME};'.
312 'my $absolute_self="http://$ENV{SERVER_NAME}$port$ENV{SCRIPT_NAME}";'.
313 'my ($path)=$ENV{SCRIPT_NAME}=~m!^(.*/)[^/]+$!;'.
314 'my $absolute_path="http://$ENV{SERVER_NAME}$port$path";'.
315 'my %__v=@_;my %__ov;for(keys %__v){$__ov{$_}=$$_;$$_=$__v{$_};}'.
316 'my $res;'.
317 $code.
318 '$$_=$__ov{$_} for(keys %__ov);'.
319 'return $res; }';
321 die "Template format error" unless $sub;
323 return $sub;
326 sub template_for($$$)
328 my ($var,$start,$end)=@_;
329 return [map +{$var=>$_},($start..$end)];
332 sub include($)
334 my ($filename)=@_;
336 open FILE,$filename or return '';
337 my $file=do { local $/; <FILE> };
339 $file=~s/^\s+//;
340 $file=~s/\s+$//;
341 $file=~s/\n\s*/ /sg;
343 return $file;
347 sub forbidden_unicode($;$)
349 my ($dec,$hex)=@_;
350 return 1 if length($dec)>7 or length($hex)>7; # too long numbers
351 my $ord=($dec or hex $hex);
353 return 1 if $ord>MAX_UNICODE; # outside unicode range
354 return 1 if $ord<32; # control chars
355 return 1 if $ord>=0x7f and $ord<=0x84; # control chars
356 return 1 if $ord>=0xd800 and $ord<=0xdfff; # surrogate code points
357 return 1 if $ord>=0x202a and $ord<=0x202e; # text direction
358 return 1 if $ord>=0xfdd0 and $ord<=0xfdef; # non-characters
359 return 1 if $ord % 0x10000 >= 0xfffe; # non-characters
360 return 0;
363 sub clean_string($;$)
365 my ($str,$cleanentities)=@_;
367 if($cleanentities) { $str=~s/&/&amp;/g } # clean up &
368 else
370 $str=~s/&(#([0-9]+);|#x([0-9a-fA-F]+);|)/
371 if($1 eq "") { '&amp;' } # change simple ampersands
372 elsif(forbidden_unicode($2,$3)) { "" } # strip forbidden unicode chars
373 else { "&$1" } # and leave the rest as-is.
374 /ge # clean up &, excluding numerical entities
377 $str=~s/\</&lt;/g; # clean up brackets for HTML tags
378 $str=~s/\>/&gt;/g;
379 $str=~s/"/&quot;/g; # clean up quotes for HTML attributes
380 $str=~s/'/&#39;/g;
381 $str=~s/,/&#44;/g; # clean up commas for some reason I forgot
383 $str=~s/[\x00-\x08\x0b\x0c\x0e-\x1f]//g; # remove control chars
385 return $str;
388 sub decode_string($;$$)
390 my ($str,$charset,$noentities)=@_;
391 my $use_unicode=$has_encode && $charset;
393 $str=decode($charset,$str) if $use_unicode;
395 $str=~s{(&#([0-9]*)([;&])|&#([x&])([0-9a-f]*)([;&]))}{
396 my $ord=($2 or hex $5);
397 if($3 eq '&' or $4 eq '&' or $5 eq '&') { $1 } # nested entities, leave as-is.
398 elsif(forbidden_unicode($2,$5)) { "" } # strip forbidden unicode chars
399 elsif($ord==35 or $ord==38) { $1 } # don't convert & or #
400 elsif($use_unicode) { chr $ord } # if we have unicode support, convert all entities
401 elsif($ord<128) { chr $ord } # otherwise just convert ASCII-range entities
402 else { $1 } # and leave the rest as-is.
403 }gei unless $noentities;
405 $str=~s/[\x00-\x08\x0b\x0c\x0e-\x1f]//g; # remove control chars
407 return $str;
410 sub escamp($)
412 my ($str)=@_;
413 $str=~s/&/&amp;/g;
414 return $str;
417 sub urlenc($)
419 my ($str)=@_;
420 $str=~s/([^\w ])/"%".sprintf("%02x",ord $1)/sge;
421 $str=~s/ /+/sg;
422 return $str;
425 sub clean_path($)
427 my ($str)=@_;
428 $str=~s!([^\w/._\-])!"%".sprintf("%02x",ord $1)!sge;
429 return $str;
435 # Javascript utilities
438 sub clean_to_js($)
440 my $str=shift;
442 $str=~s/&amp;/\\x26/g;
443 $str=~s/&lt;/\\x3c/g;
444 $str=~s/&gt;/\\x3e/g;
445 $str=~s/&quot;/\\x22/g; #"
446 $str=~s/(&#39;|')/\\x27/g;
447 $str=~s/&#44;/,/g;
448 $str=~s/&#[0-9]+;/sprintf "\\u%04x",$1/ge;
449 $str=~s/&#x[0-9a-f]+;/sprintf "\\u%04x",hex($1)/gie;
450 $str=~s/(\r\n|\r|\n)/\\n/g;
452 return "'$str'";
455 sub js_string($)
457 my $str=shift;
459 $str=~s/\\/\\\\/g;
460 $str=~s/'/\\'/g;
461 $str=~s/([\x00-\x1f\x80-\xff<>&])/sprintf "\\x%02x",ord($1)/ge;
462 eval '$str=~s/([\x{100}-\x{ffff}])/sprintf "\\u%04x",ord($1)/ge';
463 $str=~s/(\r\n|\r|\n)/\\n/g;
465 return "'$str'";
468 sub js_array(@)
470 return "[".(join ",",@_)."]";
473 sub js_hash(%)
475 my %hash=@_;
476 return "{".(join ",",map "'$_':$hash{$_}",keys %hash)."}";
481 # HTTP utilities
484 # LIGHTWEIGHT HTTP/1.1 CLIENT
485 # by fatalM4/coda, modified by WAHa.06x36
487 use constant CACHEFILE_PREFIX => 'cache-'; # you can make this a directory (e.g. 'cachedir/cache-' ) if you'd like
488 use constant FORCETIME => '0.04'; # If the cache is less than (FORCETIME) days old, don't even attempt to refresh.
489 # Saves everyone some bandwidth. 0.04 days is ~ 1 hour. 0.0007 days is ~ 1 min.
490 eval 'use IO::Socket::INET'; # Will fail on old Perl versions!
492 sub get_http($;$$$)
494 my ($url,$maxsize,$referer,$cacheprefix)=@_;
495 my ($host,$port,$doc)=$url=~m!^(?:http://|)([^/]+)(:[0-9]+|)(.*)$!;
496 $port=80 unless($port);
498 my $hash=encode_base64(rc4(null_string(6),"$host:$port$doc",0),"");
499 $hash=~tr!/+!_-!; # remove / and +
500 my $cachefile=($cacheprefix or CACHEFILE_PREFIX).($doc=~m!([^/]{0,15})$!)[0]."-$hash"; # up to 15 chars of filename
501 my ($modified,$cache);
503 if(open CACHE,"<",$cachefile) # get modified date and cache contents
505 $modified=<CACHE>;
506 $cache=join "",<CACHE>;
507 chomp $modified;
508 close CACHE;
510 return $cache if((-M $cachefile)<FORCETIME);
513 my $sock=IO::Socket::INET->new("$host:$port") or return $cache;
514 print $sock "GET $doc HTTP/1.1\r\nHost: $host\r\nConnection: close\r\n";
515 print $sock "If-Modified-Since: $modified\r\n" if $modified;
516 print $sock "Referer: $referer\r\n" if $referer;
517 print $sock "\r\n"; #finished!
519 # header
520 my ($line,$statuscode,$lastmod);
521 do {
522 $line=<$sock>;
523 $statuscode=$1 if($line=~/^HTTP\/1\.1 (\d+)/);
524 $lastmod=$1 if($line=~/^Last-Modified: (.*)/);
525 } until ($line=~/^\r?\n/);
527 # body
528 my ($line,$output);
529 while($line=<$sock>)
531 $output.=$line;
532 last if $maxsize and $output>=$maxsize;
534 undef $sock;
536 if($statuscode=="200")
538 #navbar changed, update cache
539 if(open CACHE,">$cachefile")
541 print CACHE "$lastmod\n";
542 print CACHE $output;
543 close CACHE or die "close cache: $!";
545 return $output;
547 else # touch and return cache, or nothing if no cache
549 utime(time,time,$cachefile);
550 return $cache;
554 sub make_http_forward($;$)
556 my ($location,$alternate_method)=@_;
558 if($alternate_method)
560 print "Content-Type: text/html\n";
561 print "\n";
562 print "<html><head>";
563 print '<meta http-equiv="refresh" content="0; url='.$location.'" />';
564 print '<script type="text/javascript">document.location="'.$location.'";</script>';
565 print '</head><body><a href="'.$location.'">'.$location.'</a></body></html>';
567 else
569 print "Status: 303 Go West\n";
570 print "Location: $location\n";
571 print "Content-Type: text/html\n";
572 print "\n";
573 print '<html><body><a href="'.$location.'">'.$location.'</a></body></html>';
577 sub make_cookies(%)
579 my (%cookies)=@_;
581 my $charset=$cookies{'-charset'};
582 my $expires=($cookies{'-expires'} or time+14*24*3600);
583 my $autopath=$cookies{'-autopath'};
584 my $path=$cookies{'-path'};
586 my $date=make_date($expires,"cookie");
588 unless($path)
590 if($autopath eq 'current') { ($path)=$ENV{SCRIPT_NAME}=~m!^(.*/)[^/]+$! }
591 elsif($autopath eq 'parent') { ($path)=$ENV{SCRIPT_NAME}=~m!^(.*?/)(?:[^/]+/)?[^/]+$! }
592 else { $path='/'; }
595 foreach my $name (keys %cookies)
597 next if($name=~/^-/); # skip entries that start with a dash
599 my $value=$cookies{$name};
600 $value="" unless(defined $value);
602 $value=cookie_encode($value,$charset);
604 print "Set-Cookie: $name=$value; path=$path; expires=$date;\n";
608 sub cookie_encode($;$)
610 my ($str,$charset)=@_;
612 if($]>5.007) # new perl, use Encode.pm
614 if($charset)
616 require Encode;
617 $str=Encode::decode($charset,$str);
618 $str=~s/&\#([0-9]+);/chr $1/ge;
619 $str=~s/&\#x([0-9a-f]+);/chr hex $1/gei;
622 $str=~s/([^0-9a-zA-Z])/
623 my $c=ord $1;
624 sprintf($c>255?'%%u%04x':'%%%02x',$c);
625 /sge;
627 else # do the hard work ourselves
629 if($charset=~/\butf-?8$/i)
631 $str=~s{([\xe0-\xef][\x80-\xBF][\x80-\xBF]|[\xc0-\xdf][\x80-\xBF]|&#([0-9]+);|&#[xX]([0-9a-fA-F]+);|[^0-9a-zA-Z])}{ # convert UTF-8 to URL encoding - only handles up to U-FFFF
632 my $c;
633 if($2) { $c=$2 }
634 elsif($3) { $c=hex $3 }
635 elsif(length $1==1) { $c=ord $1 }
636 elsif(length $1==2)
638 my @b=map { ord $_ } split //,$1;
639 $c=(($b[0]-0xc0)<<6)+($b[1]-0x80);
641 elsif(length $1==3)
643 my @b=map { ord $_ } split //,$1;
644 $c=(($b[0]-0xe0)<<12)+(($b[1]-0x80)<<6)+($b[2]-0x80);
646 sprintf($c>255?'%%u%04x':'%%%02x',$c);
647 }sge;
649 elsif($charset=~/\b(?:shift.*jis|sjis)$/i) # old perl, using shift_jis
651 require 'sjis.pl';
652 my $sjis_table=get_sjis_table();
654 $str=~s{([\x80-\x9f\xe0-\xfc].|&#([0-9]+);|&#[xX]([0-9a-fA-F]+);|[^0-9a-zA-Z])}{ # convert Shift_JIS to URL encoding
655 my $c=($2 or ($3 and hex $3) or $$sjis_table{$1});
656 sprintf($c>255?'%%u%04x':'%%%02x',$c);
657 }sge;
659 else
661 $str=~s/([^0-9a-zA-Z])/sprintf('%%%02x',ord $1)/sge;
665 return $str;
668 sub get_xhtml_content_type(;$$)
670 my ($charset,$usexhtml)=@_;
671 my $type;
673 if($usexhtml and $ENV{HTTP_ACCEPT}=~/application\/xhtml\+xml/) { $type="application/xhtml+xml"; }
674 else { $type="text/html"; }
676 $type.="; charset=$charset" if($charset);
678 return $type;
681 sub expand_filename($)
683 my ($filename)=@_;
684 return $filename if($filename=~m!^/!);
685 return $filename if($filename=~m!^\w+:!);
687 my ($self_path)=$ENV{SCRIPT_NAME}=~m!^(.*/)[^/]+$!;
688 return $self_path.$filename;
692 # Network utilities
695 sub resolve_host($)
697 my $ip=shift;
698 return (gethostbyaddr inet_aton($ip),AF_INET or $ip);
703 # Data utilities
706 sub process_tripcode($;$$$$)
708 my ($name,$tripkey,$secret,$charset,$nonamedecoding)=@_;
709 $tripkey="!" unless($tripkey);
711 if($name=~/^(.*?)((?<!&)#|\Q$tripkey\E)(.*)$/)
713 my ($namepart,$marker,$trippart)=($1,$2,$3);
714 my $trip;
716 $namepart=decode_string($namepart,$charset) unless $nonamedecoding;
717 $namepart=clean_string($namepart);
719 if($secret and $trippart=~s/(?:\Q$marker\E)(?<!&#)(?:\Q$marker\E)*(.*)$//) # do we want secure trips, and is there one?
721 my $str=$1;
722 my $maxlen=255-length($secret);
723 $str=substr $str,0,$maxlen if(length($str)>$maxlen);
724 # $trip=$tripkey.$tripkey.encode_base64(rc4(null_string(6),"t".$str.$secret),"");
725 $trip=$tripkey.$tripkey.hide_data($1,6,"trip",$secret,1);
726 return ($namepart,$trip) unless($trippart); # return directly if there's no normal tripcode
729 # 2ch trips are processed as Shift_JIS whenever possible
730 eval 'use Encode qw(decode encode)';
731 unless($@)
733 $trippart=decode_string($trippart,$charset);
734 $trippart=encode("Shift_JIS",$trippart,0x0200);
737 $trippart=clean_string($trippart);
738 my $salt=substr $trippart."H..",1,2;
739 $salt=~s/[^\.-z]/./g;
740 $salt=~tr/:;<=>?@[\\]^_`/ABCDEFGabcdef/;
741 $trip=$tripkey.(substr crypt($trippart,$salt),-10).$trip;
743 return ($namepart,$trip);
746 return clean_string($name) if $nonamedecoding;
747 return (clean_string(decode_string($name,$charset)),"");
750 sub make_date($$;@)
752 my ($time,$style,@locdays)=@_;
753 my @days=qw(Sun Mon Tue Wed Thu Fri Sat);
754 my @months=qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
755 @locdays=@days unless(@locdays);
757 if($style eq "2ch")
759 my @ltime=localtime($time);
761 return sprintf("%04d-%02d-%02d %02d:%02d",
762 $ltime[5]+1900,$ltime[4]+1,$ltime[3],$ltime[2],$ltime[1]);
764 elsif($style eq "futaba" or $style eq "0")
766 my @ltime=localtime($time);
768 return sprintf("%02d/%02d/%02d(%s)%02d:%02d",
769 $ltime[5]-100,$ltime[4]+1,$ltime[3],$locdays[$ltime[6]],$ltime[2],$ltime[1]);
771 elsif($style eq "localtime")
773 return scalar(localtime($time));
775 elsif($style eq "tiny")
777 my @ltime=localtime($time);
779 return sprintf("%02d/%02d %02d:%02d",
780 $ltime[4]+1,$ltime[3],$ltime[2],$ltime[1]);
782 elsif($style eq "http")
784 my ($sec,$min,$hour,$mday,$mon,$year,$wday)=gmtime($time);
785 return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT",
786 $days[$wday],$mday,$months[$mon],$year+1900,$hour,$min,$sec);
788 elsif($style eq "cookie")
790 my ($sec,$min,$hour,$mday,$mon,$year,$wday)=gmtime($time);
791 return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
792 $days[$wday],$mday,$months[$mon],$year+1900,$hour,$min,$sec);
794 elsif($style eq "month")
796 my ($sec,$min,$hour,$mday,$mon,$year,$wday)=gmtime($time);
797 return sprintf("%s %d",
798 $months[$mon],$year+1900);
800 elsif($style eq "2ch-sep93")
802 my $sep93=timelocal(0,0,0,1,8,93);
803 return make_date($time,"2ch") if($time<$sep93);
805 my @ltime=localtime($time);
807 return sprintf("%04d-%02d-%02d %02d:%02d",
808 1993,9,int ($time-$sep93)/86400+1,$ltime[2],$ltime[1]);
812 sub parse_http_date($)
814 my ($date)=@_;
815 my %months=(Jan=>0,Feb=>1,Mar=>2,Apr=>3,May=>4,Jun=>5,Jul=>6,Aug=>7,Sep=>8,Oct=>9,Nov=>10,Dec=>11);
817 if($date=~/^[SMTWF][a-z][a-z], (\d\d) ([JFMASOND][a-z][a-z]) (\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$/)
818 { return eval { timegm($6,$5,$4,$1,$months{$2},$3-1900) } }
820 return undef;
823 sub cfg_expand($%)
825 my ($str,%grammar)=@_;
826 $str=~s/%(\w+)%/
827 my @expansions=@{$grammar{$1}};
828 cfg_expand($expansions[rand @expansions],%grammar);
829 /ge;
830 return $str;
833 sub encode_base64($;$) # stolen from MIME::Base64::Perl
835 my ($data,$eol)=@_;
836 $eol="\n" unless(defined $eol);
838 my $res=pack "u",$data;
839 $res=~s/^.//mg; # remove length counts
840 $res=~s/\n//g; # remove newlines
841 $res=~tr|` -_|AA-Za-z0-9+/|; # translate to base64
843 my $padding=(3-length($data)%3)%3; # fix padding at the end
844 $res=~s/.{$padding}$/'='x$padding/e if($padding);
846 $res=~s/(.{1,76})/$1$eol/g if(length $eol); # break encoded string into lines of no more than 76 characters each
848 return $res;
851 sub decode_base64($) # stolen from MIME::Base64::Perl
853 my ($str)=@_;
855 $str=~tr|A-Za-z0-9+=/||cd; # remove non-base64 characters
856 $str=~s/=+$//; # remove padding
857 $str=~tr|A-Za-z0-9+/| -_|; # translate to uuencode
858 return "" unless(length $str);
859 return unpack "u",join '',map { chr(32+length($_)*3/4).$_ } $str=~/(.{1,60})/gs;
862 sub dot_to_dec($)
864 return unpack('N',pack('C4',split(/\./, $_[0]))); # wow, magic.
867 sub dec_to_dot($)
869 return join('.',unpack('C4',pack('N',$_[0])));
872 sub mask_ip($$;$)
874 my ($ip,$key,$algorithm)=@_;
876 $ip=dot_to_dec($ip) if $ip=~/\./;
878 my ($block,$stir)=setup_masking($key,$algorithm);
879 my $mask=0x80000000;
881 for(1..32)
883 my $bit=$ip&$mask?"1":"0";
884 $block=$stir->($block);
885 $ip^=$mask if(ord($block)&0x80);
886 $block=$bit.$block;
887 $mask>>=1;
890 return sprintf "%08x",$ip;
893 sub unmask_ip($$;$)
895 my ($id,$key,$algorithm)=@_;
897 $id=hex($id);
899 my ($block,$stir)=setup_masking($key,$algorithm);
900 my $mask=0x80000000;
902 for(1..32)
904 $block=$stir->($block);
905 $id^=$mask if(ord($block)&0x80);
906 my $bit=$id&$mask?"1":"0";
907 $block=$bit.$block;
908 $mask>>=1;
911 return dec_to_dot($id);
914 sub setup_masking($$)
916 my ($key,$algorithm)=@_;
918 $algorithm=$has_md5?"md5":"rc6" unless $algorithm;
920 my ($block,$stir);
922 if($algorithm eq "md5")
924 return (md5($key),sub { md5(shift) })
926 else
928 setup_rc6($key);
929 return (null_string(16),sub { encrypt_rc6(shift) })
933 sub make_random_string($)
935 my ($num)=@_;
936 my $chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
937 my $str;
939 $str.=substr $chars,rand length $chars,1 for(1..$num);
941 return $str;
944 sub null_string($) { "\0"x(shift) }
946 sub make_key($$$)
948 my ($key,$secret,$length)=@_;
949 return rc4(null_string($length),$key.$secret);
952 sub hide_data($$$$;$)
954 my ($data,$bytes,$key,$secret,$base64)=@_;
956 my $crypt=rc4(null_string($bytes),make_key($key,$secret,32).$data);
958 return encode_base64($crypt,"") if $base64;
959 return $crypt;
965 # File utilities
968 sub read_array($)
970 my ($file)=@_;
972 if(ref $file eq "GLOB")
974 return map { s/\r?\n?$//; $_ } <$file>;
976 else
978 open FILE,$file or return ();
979 binmode FILE;
980 my @array=map { s/\r?\n?$//; $_ } <FILE>;
981 close FILE;
982 return @array;
986 sub write_array($@)
988 my ($file,@array)=@_;
990 if(ref $file eq "GLOB")
992 print $file join "\n",@array;
994 else # super-paranoid atomic write
996 my $rndname1="__".make_random_string(12).".dat";
997 my $rndname2="__".make_random_string(12).".dat";
998 if(open FILE,">$rndname1")
1000 binmode FILE;
1001 if(print FILE join "\n",@array)
1003 close FILE;
1004 rename $file,$rndname2 if -e $file;
1005 if(rename $rndname1,$file)
1007 unlink $rndname2 if -e $rndname2;
1008 return;
1012 close FILE;
1013 die "Couldn't write to file \"$file\"";
1020 # Spam utilities
1023 sub spam_check($$) # Deprecated function
1025 my ($text,$spamfile)=@_;
1026 return compile_spam_checker($spamfile)->($text);
1029 sub compile_spam_checker(@)
1031 my @re=map {
1032 s{(\\?\\?&\\?#([0-9]+)\\?;|\\?&\\?#x([0-9a-f]+)\\?;)}{
1033 sprintf("\\x{%x}",($2 or hex $3));
1034 }gei if $has_encode;
1036 } map {
1037 s/(^|\s+)#.*//; s/^\s+//; s/\s+$//; # strip perl-style comments and whitespace
1038 if(!length) { () } # nothing left, skip
1039 elsif(m!^/(.*)/$!) { $1 } # a regular expression
1040 elsif(m!^/(.*)/([xism]+)$!) { "(?$2)$1" } # a regular expression with xism modifiers
1041 else { quotemeta } # a normal string
1042 } map read_array($_),@_;
1044 return eval 'sub {
1045 $_=shift;
1046 # study; # causes a strange bug - moved to spam_engine()
1047 return '.(join "||",map "/$_/mo",(@re)).';
1051 sub spam_engine(%)
1053 my %args=@_;
1054 my @spam_files=@{$args{spam_files}||[]};
1055 my @trap_fields=@{$args{trap_fields}||[]};
1056 my @included_fields=@{$args{included_fields}||[]};
1057 my %excluded_fields=map ($_=>1),@{$args{excluded_fields}||[]};
1058 my $query=$args{query}||new CGI;
1059 my $charset=$args{charset};
1061 for(@trap_fields) { spam_screen($query) if $query->param($_) }
1063 my $spam_checker=compile_spam_checker(@spam_files);
1064 my @fields=@included_fields?@included_fields:$query->param;
1065 @fields=grep !$excluded_fields{$_},@fields if %excluded_fields;
1066 # my $fulltext=join "\n",map decode_string($query->param($_),$charset),@fields;
1067 my $fulltext=join "\n",map $query->param($_),@fields;
1068 study $fulltext;
1070 spam_screen($query) if $spam_checker->($fulltext);
1073 sub spam_screen($)
1075 my $query=shift;
1077 print "Content-Type: text/html\n\n";
1078 print "<html><body>";
1079 print "<h1>Anti-spam filters triggered.</h1>";
1080 print "<p>If you are not a spammer, you are probably accidentially ";
1081 print "trying to use an URL that is listed in the spam file. Try ";
1082 print "editing your post to remove it. Sorry for any inconvenience.</p>";
1083 print "<small style='color:white'><small>";
1084 print "$_<br>" for(map $query->param($_),$query->param);
1085 print "</small></small>";
1087 exit 0;
1092 # Image utilities
1095 sub analyze_image($$)
1097 my ($file,$name)=@_;
1098 my (@res);
1100 safety_check($file);
1102 return ("jpg",@res) if(@res=analyze_jpeg($file));
1103 return ("png",@res) if(@res=analyze_png($file));
1104 return ("gif",@res) if(@res=analyze_gif($file));
1106 # find file extension for unknown files
1107 my ($ext)=$name=~/\.([^\.]+)$/;
1108 return (lc($ext),0,0);
1111 sub safety_check($file)
1113 my ($file)=@_;
1115 # Check for IE MIME sniffing XSS exploit - thanks, MS, totally appreciating this
1116 read $file,my $buffer,256;
1117 seek $file,0,0;
1118 die "Possible IE XSS exploit in file" if $buffer=~/<(?:body|head|html|img|plaintext|pre|script|table|title|a href|channel|scriptlet)/;
1121 sub analyze_jpeg($)
1123 my ($file)=@_;
1124 my ($buffer);
1126 read($file,$buffer,2);
1128 if($buffer eq "\xff\xd8")
1130 OUTER:
1131 for(;;)
1133 for(;;)
1135 last OUTER unless(read($file,$buffer,1));
1136 last if($buffer eq "\xff");
1139 last unless(read($file,$buffer,3)==3);
1140 my ($mark,$size)=unpack("Cn",$buffer);
1141 last if($mark==0xda or $mark==0xd9); # SOS/EOI
1142 die "Possible virus in image" if($size<2); # MS GDI+ JPEG exploit uses short chunks
1144 if($mark>=0xc0 and $mark<=0xc2) # SOF0..SOF2 - what the hell are the rest?
1146 last unless(read($file,$buffer,5)==5);
1147 my ($bits,$height,$width)=unpack("Cnn",$buffer);
1148 seek($file,0,0);
1150 return($width,$height);
1153 seek($file,$size-2,1);
1157 seek($file,0,0);
1159 return ();
1162 sub analyze_png($)
1164 my ($file)=@_;
1165 my ($bytes,$buffer);
1167 $bytes=read($file,$buffer,24);
1168 seek($file,0,0);
1169 return () unless($bytes==24);
1171 my ($magic1,$magic2,$length,$ihdr,$width,$height)=unpack("NNNNNN",$buffer);
1173 return () unless($magic1==0x89504e47 and $magic2==0x0d0a1a0a and $ihdr==0x49484452);
1175 return ($width,$height);
1178 sub analyze_gif($)
1180 my ($file)=@_;
1181 my ($bytes,$buffer);
1183 $bytes=read($file,$buffer,10);
1184 seek($file,0,0);
1185 return () unless($bytes==10);
1187 my ($magic,$width,$height)=unpack("A6 vv",$buffer);
1189 return () unless($magic eq "GIF87a" or $magic eq "GIF89a");
1191 return ($width,$height);
1194 sub make_thumbnail($$$$$;$)
1196 my ($filename,$thumbnail,$width,$height,$quality,$convert)=@_;
1198 # first try ImageMagick
1200 my $magickname=$filename;
1201 $magickname.="[0]" if($magickname=~/\.gif$/);
1203 $convert="convert" unless($convert);
1204 `$convert -background white -flatten -size ${width}x${height} -geometry ${width}x${height}! -quality $quality $magickname $thumbnail`;
1206 return 1 unless($?);
1208 # if that fails, try pnmtools instead
1210 if($filename=~/\.jpg$/)
1212 `djpeg $filename | pnmscale -width $width -height $height | cjpeg -quality $quality > $thumbnail`;
1213 # could use -scale 1/n
1214 return 1 unless($?);
1216 elsif($filename=~/\.png$/)
1218 `pngtopnm $filename | pnmscale -width $width -height $height | cjpeg -quality $quality > $thumbnail`;
1219 return 1 unless($?);
1221 elsif($filename=~/\.gif$/)
1223 `giftopnm $filename | pnmscale -width $width -height $height | cjpeg -quality $quality > $thumbnail`;
1224 return 1 unless($?);
1227 # try Mac OS X's sips
1229 `sips -z $height $width -s formatOptions normal -s format jpeg $filename --out $thumbnail >/dev/null`; # quality setting doesn't seem to work
1230 return 1 unless($?);
1232 # try PerlMagick (it sucks)
1234 eval 'use Image::Magick';
1235 unless($@)
1237 my ($res,$magick);
1239 $magick=Image::Magick->new;
1241 $res=$magick->Read($magickname);
1242 return 0 if "$res";
1243 $res=$magick->Scale(width=>$width, height=>$height);
1244 #return 0 if "$res";
1245 $res=$magick->Write(filename=>$thumbnail, quality=>$quality);
1246 #return 0 if "$res";
1248 return 1;
1251 # try GD lib (also sucks, and untested)
1252 eval 'use GD';
1253 unless($@)
1255 my $src;
1256 if($filename=~/\.jpg$/i) { $src=GD::Image->newFromJpeg($filename) }
1257 elsif($filename=~/\.png$/i) { $src=GD::Image->newFromPng($filename) }
1258 elsif($filename=~/\.gif$/i)
1260 if(defined &GD::Image->newFromGif) { $src=GD::Image->newFromGif($filename) }
1261 else
1263 `gif2png $filename`; # gif2png taken from futallaby
1264 $filename=~s/\.gif/\.png/;
1265 $src=GD::Image->newFromPng($filename);
1268 else { return 0 }
1270 my ($img_w,$img_h)=$src->getBounds();
1271 my $thumb=GD::Image->new($width,$height);
1272 $thumb->copyResized($src,0,0,0,0,$width,$height,$img_w,$img_h);
1273 my $jpg=$thumb->jpeg($quality);
1274 open THUMBNAIL,">$thumbnail";
1275 binmode THUMBNAIL;
1276 print THUMBNAIL $jpg;
1277 close THUMBNAIL;
1278 return 1 unless($!);
1281 return 0;
1286 # Crypto code
1289 sub rc4($$;$)
1291 my ($message,$key,$skip)=@_;
1292 my @s=0..255;
1293 my @k=unpack 'C*',$key;
1294 my @message=unpack 'C*',$message;
1295 my ($x,$y);
1296 $skip=256 unless(defined $skip);
1298 $y=0;
1299 for $x (0..255)
1301 $y=($y+$s[$x]+$k[$x%@k])%256;
1302 @s[$x,$y]=@s[$y,$x];
1305 $x=0; $y=0;
1306 for(1..$skip)
1308 $x=($x+1)%256;
1309 $y=($y+$s[$x])%256;
1310 @s[$x,$y]=@s[$y,$x];
1313 for(@message)
1315 $x=($x+1)%256;
1316 $y=($y+$s[$x])%256;
1317 @s[$x,$y]=@s[$y,$x];
1318 $_^=$s[($s[$x]+$s[$y])%256];
1321 return pack 'C*',@message;
1324 my @S;
1326 sub setup_rc6($)
1328 my ($key)=@_;
1330 $key.="\0"x(4-(length $key)&3); # pad key
1332 my @L=unpack "V*",$key;
1334 $S[0]=0xb7e15163;
1335 $S[$_]=add($S[$_-1],0x9e3779b9) for(1..43);
1337 my $v=@L>44 ? @L*3 : 132;
1338 my ($A,$B,$i,$j)=(0,0,0,0);
1340 for(1..$v)
1342 $A=$S[$i]=rol(add($S[$i],$A,$B),3);
1343 $B=$L[$j]=rol(add($L[$j]+$A+$B),add($A+$B));
1344 $i=($i+1)%@S;
1345 $j=($j+1)%@L;
1349 sub encrypt_rc6($)
1351 my ($block,)=@_;
1352 my ($A,$B,$C,$D)=unpack "V4",$block."\0"x16;
1354 $B=add($B,$S[0]);
1355 $D=add($D,$S[1]);
1357 for(my $i=1;$i<=20;$i++)
1359 my $t=rol(mul($B,rol($B,1)|1),5);
1360 my $u=rol(mul($D,rol($D,1)|1),5);
1361 $A=add(rol($A^$t,$u),$S[2*$i]);
1362 $C=add(rol($C^$u,$t),$S[2*$i+1]);
1364 ($A,$B,$C,$D)=($B,$C,$D,$A);
1367 $A=add($A,$S[42]);
1368 $C=add($C,$S[43]);
1370 return pack "V4",$A,$B,$C,$D;
1373 sub decrypt_rc6($)
1375 my ($block,)=@_;
1376 my ($A,$B,$C,$D)=unpack "V4",$block."\0"x16;
1378 $C=add($C,-$S[43]);
1379 $A=add($A,-$S[42]);
1381 for(my $i=20;$i>=1;$i--)
1383 ($A,$B,$C,$D)=($D,$A,$B,$C);
1384 my $u=rol(mul($D,add(rol($D,1)|1)),5);
1385 my $t=rol(mul($B,add(rol($B,1)|1)),5);
1386 $C=ror(add($C,-$S[2*$i+1]),$t)^$u;
1387 $A=ror(add($A,-$S[2*$i]),$u)^$t;
1391 $D=add32($D,-$S[1]);
1392 $B=add32($B,-$S[0]);
1394 return pack "V4",$A,$B,$C,$D;
1397 sub setup_xtea($)
1401 sub encrypt_xtea($)
1405 sub decrypt_xtea($)
1409 sub add(@) { my ($sum,$term); while(defined ($term=shift)) { $sum+=$term } return $sum%4294967296 }
1410 sub rol($$) { my ($x,$n); ( $x = shift ) << ( $n = 31 & shift ) | 2**$n - 1 & $x >> 32 - $n; }
1411 sub ror($$) { rol(shift,32-(31&shift)); } # rorororor
1412 sub mul($$) { my ($a,$b)=@_; return ( (($a>>16)*($b&65535)+($b>>16)*($a&65535))*65536+($a&65535)*($b&65535) )%4294967296 }