| 1 | wakaba | 1.1 | # -*- perl -*- | 
| 2 |  |  | use strict; | 
| 3 |  |  |  | 
| 4 |  |  | package main; | 
| 5 | wakaba | 1.4 | our $VERSION = '2.'.do{my @r=(q$Revision: 1.3 $=~/\d+/g);sprintf "%d."."%02d" x $#r,@r}; | 
| 6 | wakaba | 1.1 | binmode STDOUT; binmode STDIN; | 
| 7 | wakaba | 1.3 | use Fcntl; | 
| 8 | wakaba | 1.1 | require Yuki::YukiWikiCache; | 
| 9 | wakaba | 1.3 | require SuikaWiki::Name::Space; | 
| 10 |  |  | require SuikaWiki::View; | 
| 11 | wakaba | 1.1 | require SuikaWiki::Plugin; | 
| 12 |  |  | our %fmt;       ## formatter objects | 
| 13 |  |  | our %embed_command = ( | 
| 14 |  |  | form    => qr/\[\[\#form(?:\(([A-Za-z0-9-]+)\))?:'((?:[^'\\]|\\.)*)':'((?:[^'\\]|\\.)*)'(?::'((?:[^'\\]|\\.)*)')?\]\]/, | 
| 15 |  |  | ); | 
| 16 |  |  | our ($modifier_dbtype,$url_cgi,%uri,%PathTo); | 
| 17 | wakaba | 1.3 | our (%PageName,$kanjicode,$lang); | 
| 18 | wakaba | 1.1 |  | 
| 19 |  |  | my %form; | 
| 20 |  |  | our %database; | 
| 21 |  |  | our $database = bless {}, 'wiki::dummy'; | 
| 22 |  |  | my %interwiki; | 
| 23 |  |  | my %command_do = ( | 
| 24 |  |  | default => \&do_view, | 
| 25 |  |  | adminchangepassword => \&do_adminchangepassword, | 
| 26 |  |  | write => \&do_write, | 
| 27 |  |  | searchform => \&do_searchform, | 
| 28 |  |  | comment => \&do_comment, | 
| 29 |  |  | RandomJump  => \&do_random_jump, | 
| 30 |  |  | wikiform    => \&do_wikiform, | 
| 31 |  |  | ); | 
| 32 |  |  | our $UA = '';  ## User agent name | 
| 33 |  |  | $| = 1; | 
| 34 |  |  |  | 
| 35 |  |  | sub main { | 
| 36 |  |  | $UA = $main::ENV{HTTP_USER_AGENT}; | 
| 37 |  |  | &open_db; | 
| 38 |  |  | &init_form; | 
| 39 |  |  | if ($command_do{$form{mycmd}}) { | 
| 40 |  |  | &{$command_do{$form{mycmd}}}; | 
| 41 |  |  | } else { | 
| 42 |  |  | &{$command_do{default}}; | 
| 43 |  |  | } | 
| 44 |  |  | &close_db; | 
| 45 |  |  | } | 
| 46 |  |  |  | 
| 47 |  |  | sub do_view { | 
| 48 |  |  | my $content = $database{$form{mypage}}; | 
| 49 |  |  | my $lm = $database->mtime ($form{mypage}); | 
| 50 |  |  | wiki::referer::add ($form{mypage}, $ENV{HTTP_REFERER}); | 
| 51 |  |  | wiki::useragent::add ($ENV{HTTP_USER_AGENT}); | 
| 52 |  |  | &load_formatter ('view'); | 
| 53 |  |  | my $view = $form{mycmd}; | 
| 54 |  |  | if ($view eq 'edit') { | 
| 55 |  |  | $view = 'adminedit' if $form{admin}; | 
| 56 |  |  | } elsif ($view =~ /[^0-9A-Za-z_]/) { | 
| 57 |  |  | $view = 'default' | 
| 58 |  |  | } | 
| 59 |  |  | if ($view eq 'default' || !$view) { | 
| 60 |  |  | ## BUG: this code is not strict | 
| 61 |  |  | if ($main::ENV{HTTP_COOKIE} =~ /SelectedMode=([0-9A-Za-z]+)/) { | 
| 62 |  |  | $view = $1; | 
| 63 |  |  | } else { | 
| 64 |  |  | $view = 'read'; | 
| 65 |  |  | } | 
| 66 |  |  | } | 
| 67 |  |  | my ($magic, $content) = &SuikaWiki::Plugin::magic_and_content (undef, $content); | 
| 68 |  |  | $magic ||= '#?SuikaWiki/0.9'; | 
| 69 |  |  | my $o = bless {param => \%form, page => $form{mypage}, toc => [], | 
| 70 |  |  | magic => $magic, content => $content, | 
| 71 |  |  | formatter => $fmt{view}, &_compatible_options ()}, 'SuikaWiki::Plugin'; | 
| 72 | wakaba | 1.3 | my $view_def = SuikaWiki::View->definition ($view); | 
| 73 |  |  | if (!$view_def->check ($o)) { | 
| 74 | wakaba | 1.1 | print "Status: 406 Unsupported Media Type\n"; | 
| 75 |  |  | $view = '-UnsupportedMediaType'; | 
| 76 | wakaba | 1.3 | $view_def = SuikaWiki::View->definition ($view); | 
| 77 | wakaba | 1.1 | } | 
| 78 | wakaba | 1.3 | my $media = $view_def->properties->{media}; | 
| 79 |  |  | if ($view_def->properties->{xmedia} && $UA =~ /Gecko/) { | 
| 80 |  |  | $media = $view_def->properties->{xmedia}; | 
| 81 | wakaba | 1.1 | $o->{media} = $media; | 
| 82 |  |  | } elsif ($UA =~ m#Mozilla/0\..+Windows#) { | 
| 83 |  |  | $kanjicode = 'shift_jis'; | 
| 84 |  |  | } | 
| 85 |  |  | if ($magic =~ m!^\#\?SuikaWiki/0.9!) { | 
| 86 |  |  | &print_header ($form{mypage}, -last_modified => ($magic =~ /interactive="yes"/ ? time : $lm), | 
| 87 |  |  | -expires => ($magic =~ /interactive="yes"/ ? 1 : undef), o => $o, | 
| 88 |  |  | -media => $media, -magic => $magic,  content => $content); | 
| 89 |  |  | } else { | 
| 90 |  |  | &print_header($form{mypage}, -media => $media, | 
| 91 |  |  | -magic => $magic, -last_modified => $lm, o => $o); | 
| 92 |  |  | } | 
| 93 |  |  | if ($kanjicode ne 'euc') { | 
| 94 | wakaba | 1.3 | my $s = $fmt{view}->replace ($view_def->as_string => $o, {formatter => $fmt{view}}); | 
| 95 | wakaba | 1.1 | print &code_convert (\$s => $kanjicode); | 
| 96 |  |  | } else { | 
| 97 | wakaba | 1.3 | print $fmt{view}->replace ($view_def->as_string => $o, {formatter => $fmt{view}}); | 
| 98 | wakaba | 1.1 | } | 
| 99 |  |  | } | 
| 100 |  |  |  | 
| 101 |  |  | sub _do_view_msg (%) { | 
| 102 |  |  | my %option = @_; | 
| 103 |  |  | &load_formatter ('view'); | 
| 104 |  |  | my $o = bless {param => \%form, page => $option{-page}, toc => [], condition => \%option, | 
| 105 |  |  | formatter => $fmt{view}, &_compatible_options ()}, 'SuikaWiki::Plugin'; | 
| 106 | wakaba | 1.3 | my $view_def = SuikaWiki::View->definition ($option{-view}); | 
| 107 |  |  | unless ($view_def->check ($o)) { | 
| 108 | wakaba | 1.1 | print "Status: 406 Unsupported Media Type\n"; | 
| 109 |  |  | $option{-view} = '-UnsupportedMediaType'; | 
| 110 | wakaba | 1.3 | $view_def = SuikaWiki::View->definition ($option{-view}); | 
| 111 | wakaba | 1.1 | } | 
| 112 | wakaba | 1.3 | my $media = $view_def->properties->{media}; | 
| 113 |  |  | if ($view_def->properties->{xmedia} && $UA =~ /Gecko/) { | 
| 114 |  |  | $media = $view_def->properties->{xmedia}; | 
| 115 | wakaba | 1.1 | $o->{media} = $media; | 
| 116 |  |  | } | 
| 117 |  |  | &print_header($option{-page}, -media => $media, o => $o, -goto => $option{-goto}); | 
| 118 | wakaba | 1.3 | print $fmt{view}->replace ($view_def->as_string => $o, {formatter => $fmt{view}}); | 
| 119 | wakaba | 1.1 | } | 
| 120 |  |  |  | 
| 121 |  |  | sub do_adminchangepassword { | 
| 122 |  |  | if ($form{mynewpassword} ne $form{mynewpassword2}) { | 
| 123 |  |  | &_do_view_msg (-view => '-error', -page => $form{mypage}, | 
| 124 |  |  | error_message => &Resource ('Error:PasswordMismatch')); | 
| 125 |  |  | return; | 
| 126 |  |  | } | 
| 127 |  |  | my ($validpassword_crypt) = $database->meta (AdminPassword => $PageName{AdminSpecialPage}); | 
| 128 |  |  | if ($validpassword_crypt) { | 
| 129 |  |  | if (not &valid_password($form{myoldpassword})) { | 
| 130 |  |  | &_do_view_msg (-view => '-error', -page => $form{mypage}, | 
| 131 |  |  | error_message => &Resource ('Error:PasswordIsIncorrect')); | 
| 132 |  |  | return; | 
| 133 |  |  | } | 
| 134 |  |  | } | 
| 135 |  |  | my ($sec, $min, $hour, $day, $mon, $year, $weekday) = localtime(time); | 
| 136 |  |  | my (@token) = ('0'..'9', 'A'..'Z', 'a'..'z'); | 
| 137 |  |  | my $salt1 = $token[(time | $$) % scalar(@token)]; | 
| 138 |  |  | my $salt2 = $token[($sec + $min*60 + $hour*60*60) % scalar(@token)]; | 
| 139 |  |  | my $crypted = crypt($form{mynewpassword}, "$salt1$salt2"); | 
| 140 |  |  | $database->meta (AdminPassword => $PageName{AdminSpecialPage} => $crypted); | 
| 141 |  |  |  | 
| 142 |  |  | &_do_view_msg (-view => '-wrote', -page => $form{mypage}); | 
| 143 |  |  | } | 
| 144 |  |  |  | 
| 145 |  |  | sub valid_password ($) { | 
| 146 |  |  | my ($validpassword_crypt) = $database->meta (AdminPassword => $PageName{AdminSpecialPage}); | 
| 147 |  |  | return crypt (shift, $validpassword_crypt) eq $validpassword_crypt ? 1 : 0; | 
| 148 |  |  | } | 
| 149 |  |  |  | 
| 150 |  |  | sub do_write { | 
| 151 |  |  | if (&frozen_reject()) { | 
| 152 |  |  | return; | 
| 153 |  |  | } | 
| 154 |  |  |  | 
| 155 |  |  | if (not &is_editable($form{mypage})) { | 
| 156 |  |  | &_do_view_msg (-view => '-error', -page => $form{mypage}, | 
| 157 |  |  | error_message => &Resource ('Error:ThisPageIsUneditable')); | 
| 158 |  |  | return; | 
| 159 |  |  | } | 
| 160 |  |  |  | 
| 161 |  |  | ## Check confliction | 
| 162 |  |  | if ($form{myLastModified} ne $database->mtime ($form{mypage})) { | 
| 163 |  |  | &_do_view_msg (-view => '-conflict', -page => $form{mypage}); | 
| 164 |  |  | return; | 
| 165 |  |  | } | 
| 166 |  |  |  | 
| 167 |  |  | if ($form{mymsg}) { | 
| 168 |  |  | if ($form{mytouch} || !ref $database) { | 
| 169 |  |  | $database{$form{mypage}} = $form{mymsg}; | 
| 170 |  |  | } else { | 
| 171 |  |  | $database->STORE ($form{mypage} => $form{mymsg}, -touch => 0); | 
| 172 |  |  | } | 
| 173 |  |  | $database->meta (IsFrozen => $form{mypage} => 0 + $form{myfrozen}); | 
| 174 |  |  | my $fragment = ''; | 
| 175 |  |  | $fragment .= qq(;after_edit_cmd=@{[&encode($form{after_edit_cmd})]}) if $form{after_edit_cmd}; | 
| 176 |  |  | if ($form{__comment_anchor_index}) { | 
| 177 |  |  | $fragment .= qq(#anchor-$form{__comment_anchor_index}); | 
| 178 |  |  | } elsif ($form{__wikiform_anchor_index}) { | 
| 179 |  |  | $fragment .= qq(#wikiform-$form{__wikiform_anchor_index}); | 
| 180 |  |  | } | 
| 181 |  |  | &_do_view_msg (-view => '-wrote', -page => $form{mypage}, -goto => $url_cgi.'?mycmd='.&encode($form{after_edit_cmd}||'default').';mypage='.&encode($form{mypage}).qq(;x-param=@{[time.[0..9]->[rand 10]]}$fragment)); | 
| 182 |  |  | } else { | 
| 183 |  |  | delete $database{$form{mypage}}; | 
| 184 |  |  | &_do_view_msg (-view => '-deleted', -page => $form{mypage}); | 
| 185 |  |  | } | 
| 186 |  |  | } | 
| 187 |  |  |  | 
| 188 |  |  | sub _compatible_options () { | 
| 189 |  |  | (use_anchor_name => ($UA =~ m#Mozilla/[12]\.|Microsoft Internet Explorer# ? 1 : 0)); | 
| 190 |  |  | } | 
| 191 |  |  |  | 
| 192 |  |  | sub get_search_result ($;%) { | 
| 193 |  |  | my $word = lc shift; | 
| 194 |  |  | my $SearchResult = SuikaWiki::Plugin->cache ('search'); | 
| 195 |  |  | my %option = @_; | 
| 196 |  |  | my @r; | 
| 197 |  |  | unless (defined $SearchResult->{$word}) { | 
| 198 |  |  | for my $page (keys %database) { | 
| 199 |  |  | next if !$option{-match_myself} && ($page eq $word); | 
| 200 |  |  | my $content = lc $database{$page}; | 
| 201 |  |  | $content =~ s/^[^\x0A\x0D]+[\x0D\x0A]+//s; | 
| 202 |  |  | if (index (lc $page, $word) > -1) { | 
| 203 |  |  | my $c = $content =~ s/\Q$word\E//g; | 
| 204 |  |  | push @r, [$page, $c+20]; | 
| 205 |  |  | } elsif (index ($word, lc $page) > -1) { | 
| 206 |  |  | my $c = $content =~ s/\Q$word\E//g; | 
| 207 |  |  | push @r, [$page, $c+10]; | 
| 208 |  |  | } elsif (my $c = $content =~ s/\Q$word\E//g) { | 
| 209 |  |  | push @r, [$page, $c]; | 
| 210 |  |  | } | 
| 211 |  |  | } | 
| 212 |  |  | @r = sort {$b->[1] <=> $a->[1] || $a->[0] cmp $b->[0]} @r; | 
| 213 |  |  | $SearchResult->{$word} = join "\x1E", map {$_->[0]."\x1F".$_->[1]} @r; | 
| 214 |  |  | } else { | 
| 215 |  |  | @r = map {[split /\x1F/, $_, 2]} split /\x1E/, $SearchResult->{$word}; | 
| 216 |  |  | } | 
| 217 |  |  | #my $em = sub { my $s = shift; $s =~ s#(\Q$word\E)#<em>$1</em>#gi; $s }; | 
| 218 |  |  | my $r = join "\n", map {qq(<li>[$_->[1]] <a href ="$url_cgi?@{[&encode($_->[0])]}" class="wiki">@{[&escape($_->[0])]}</a> <span class="wikipage-summary">@{[&escape(&get_subjectline($_->[0]))]}</span></li>)} @r; | 
| 219 |  |  | $r = qq|<ul class="search-result">$r</ul>| if $r; | 
| 220 |  |  | wantarray? ($r, scalar @r): $r; | 
| 221 |  |  | } | 
| 222 |  |  |  | 
| 223 |  |  | sub do_random_jump { | 
| 224 |  |  | my @list = keys %database; | 
| 225 |  |  | my $name = &encode ($list[rand @list]); | 
| 226 |  |  | print "Location: $uri{wiki}?$name\n"; | 
| 227 |  |  | print "\n"; | 
| 228 |  |  | } | 
| 229 |  |  |  | 
| 230 |  |  | sub print_header ($;%) { | 
| 231 |  |  | my ($page, %option) = @_; | 
| 232 |  |  | my @head; | 
| 233 |  |  | $option{o}->{-header}->{class} = &is_frozen($page) ? 'frozen' : ''; | 
| 234 |  |  | $option{o}->{-header}->{class} .= " wiki-page-obsoleted" if $option{-magic} =~ /obsoleted="yes"/; | 
| 235 |  |  | if ($option{-goto}) { | 
| 236 |  |  | if ($UA =~ m#Opera|MSIE 2\.#) { | 
| 237 |  |  | ## WARNING: This code may output unsafe HTML document if | 
| 238 |  |  | ##          $option{-goto} is not clean. | 
| 239 |  |  | $option{-goto} =~ tr/;/&/ if $UA =~ m#Opera#; | 
| 240 |  |  | print qq{Refresh: 0; url=$option{-goto}\n}; | 
| 241 |  |  | push @head, qq(<meta http-equiv="refresh" content="0; url=$option{-goto}">); | 
| 242 |  |  | } elsif ($UA =~ /Gecko/) { | 
| 243 |  |  | print qq{Refresh: 0; url="$option{-goto}"\n}; | 
| 244 |  |  | push @head, qq(<meta http-equiv="refresh" content="0; url="@{[&escape($option{-goto})]}"" />); | 
| 245 |  |  | } else { | 
| 246 |  |  | $option{-goto} =~ tr/;/&/ if $UA =~ m#Mozilla/[1-4]\.#; | 
| 247 |  |  | print qq{Refresh: 0; url="$option{-goto}"\n}; | 
| 248 |  |  | push @head, qq(<meta http-equiv="refresh" content="0; url="@{[&escape($option{-goto})]}"">); | 
| 249 |  |  | } | 
| 250 |  |  | } | 
| 251 |  |  | print qq{Last-Modified: @{[scalar gmtime $option{-last_modified}]}\n} if $option{-last_modified}; | 
| 252 |  |  | if ($option{-expires} != -1) { | 
| 253 |  |  | if (defined $option{-expires}) {  ## TODO: Don't use asctime | 
| 254 |  |  | print qq{Expires: @{[scalar gmtime (time + $option{-expires})]}\n}; | 
| 255 |  |  | } elsif ($option{-media}->{expires} != -1) { | 
| 256 |  |  | print qq{Expires: @{[scalar gmtime (time + $option{-media}->{expires})]}\n}; | 
| 257 |  |  | } | 
| 258 |  |  | } | 
| 259 |  |  | if ($option{-media}->{charset} && $UA =~ m#Mozilla/[12]\.#) { | 
| 260 |  |  | my $ct = qq{$option{-media}->{type}; charset=@{[&get_charset_name($kanjicode,compatible=>1)]}}; | 
| 261 |  |  | print qq{Content-Type: $ct\n}; | 
| 262 |  |  | $option{o}->{-header}->{meta_ct} = qq{<meta http-equiv="content-type" content="$ct">\n}; | 
| 263 |  |  | } elsif (!$option{-media}->{charset} || $UA =~ m#Infomosaic|Mozilla/0\.#) { | 
| 264 |  |  | print qq{Content-Type: $option{-media}->{type}\n}; | 
| 265 |  |  | $option{o}->{-header}->{meta_ct} = qq{<meta http-equiv="content-type" content="$option{-media}->{type}; charset=@{[&get_charset_name($kanjicode,compatible=>1)]}">\n}; | 
| 266 |  |  | } else { | 
| 267 |  |  | my $type = $option{-media}->{type}; | 
| 268 | wakaba | 1.4 | $type = 'application/xml' if ($type =~ m!^application/(?:rdf|rss)\+xml$!) && ($UA =~ m#Gecko#); | 
| 269 | wakaba | 1.1 | print qq{Content-Type: $type; charset=@{[&get_charset_name($kanjicode)]}\n}; | 
| 270 |  |  | } | 
| 271 | wakaba | 1.4 | ## TODO: more Vary: support | 
| 272 |  |  | print <<"EOD"; | 
| 273 |  |  | Vary: User-Agent,Accept-Language | 
| 274 | wakaba | 1.1 | Content-Style-Type: text/css | 
| 275 |  |  |  | 
| 276 |  |  | EOD | 
| 277 |  |  | $option{o}->{-header}->{links} = join "\n", (@head); | 
| 278 |  |  | } | 
| 279 |  |  |  | 
| 280 |  |  | sub get_charset_name ($;%) { | 
| 281 |  |  | my ($charset, %option) = (lc shift, @_); | 
| 282 |  |  | if ($charset =~ 'euc') { | 
| 283 |  |  | $charset = $option{compatible} ? 'x-euc-jp' : 'euc-jp'; | 
| 284 |  |  | } elsif ($charset =~ 'sjis' || $charset =~ 'shift') { | 
| 285 |  |  | $charset = $option{compatible} ? 'x-sjis' : 'shift_jis'; | 
| 286 |  |  | } elsif ($charset =~ 'jis') { | 
| 287 |  |  | $charset = 'iso-2022-jp'; | 
| 288 |  |  | } | 
| 289 |  |  | $charset; | 
| 290 |  |  | } | 
| 291 |  |  |  | 
| 292 |  |  | sub escape { | 
| 293 |  |  | my $s = shift; | 
| 294 |  |  | $s =~ s|&|&|g; | 
| 295 |  |  | $s =~ s|<|<|g; | 
| 296 |  |  | $s =~ s|>|>|g; | 
| 297 |  |  | $s =~ s|"|"|g; | 
| 298 |  |  | return $s; | 
| 299 |  |  | } | 
| 300 |  |  |  | 
| 301 |  |  | sub unescape { | 
| 302 |  |  | my $s = shift; | 
| 303 |  |  | $s =~ s|<|<|g; | 
| 304 |  |  | $s =~ s|>|>|g; | 
| 305 |  |  | $s =~ s|"|"|g; | 
| 306 |  |  | $s =~ s|&|&|g; | 
| 307 |  |  | return $s; | 
| 308 |  |  | } | 
| 309 |  |  |  | 
| 310 |  |  | sub convert_format ($$$;%) { | 
| 311 |  |  | my ($content, $d => $t, %option) = @_; | 
| 312 |  |  | my $f = SuikaWiki::Plugin->format_converter ($d => $t); | 
| 313 |  |  | if (ref $f) { | 
| 314 |  |  | $option{content} = $content; | 
| 315 |  |  | $option{from} = $d; | 
| 316 |  |  | $option{to} = $t; | 
| 317 |  |  | &$f ({}, bless (\%option, 'SuikaWiki::Plugin')); | 
| 318 |  |  | } elsif ($option{-error_no_return}) { | 
| 319 |  |  | return undef; | 
| 320 |  |  | } elsif ($t =~ /HTML|xml/) { | 
| 321 |  |  | length $content ? '<pre>'.&escape($content).'</pre>' : ''; | 
| 322 |  |  | } else { | 
| 323 |  |  | $content; | 
| 324 |  |  | } | 
| 325 |  |  | } | 
| 326 |  |  |  | 
| 327 |  |  | sub make_wikilink ($%) { | 
| 328 |  |  | my ($name, %option) = @_; | 
| 329 |  |  | my $ename = &escape (length $option{label} ? $option{label} : $name); | 
| 330 |  |  | $option{latest} = $option{latest} ? qq(mycmd=default;x-param=@{[time.[0..9]->[rand 10]]};mypage=) : ''; | 
| 331 | wakaba | 1.2 |  | 
| 332 |  |  | ## Namespace | 
| 333 | wakaba | 1.3 | #if ($SuikaWiki::Name::Space::VERSION) { | 
| 334 |  |  | $name = SuikaWiki::Name::Space::normalize_name (    ## Foo// + .//Bar -> Foo////Bar | 
| 335 |  |  | SuikaWiki::Name::Space::resolve_relative_name ( | 
| 336 |  |  | SuikaWiki::Name::Space::normalize_name ($option{base}, -might_be_ns_path => 1) | 
| 337 |  |  | => | 
| 338 |  |  | SuikaWiki::Name::Space::normalize_name ($name))); | 
| 339 |  |  | #} | 
| 340 | wakaba | 1.2 | $name ||= $PageName{FrontPage}; | 
| 341 |  |  |  | 
| 342 | wakaba | 1.1 | if ($database{$name}) { | 
| 343 | wakaba | 1.2 | my $subject = &escape ($name.&get_subjectline ($name)); | 
| 344 | wakaba | 1.1 | if ($option{anchor}) { | 
| 345 |  |  | return qq(<a title="$subject" href="$uri{wiki}?$option{latest}@{[&encode($name)]}#anchor-$option{anchor}" class="wiki">$ename>>$option{anchor}</a>); | 
| 346 |  |  | } else { | 
| 347 |  |  | return qq(<a title="$subject" href="$uri{wiki}?$option{latest}@{[&encode($name)]}" class="wiki">$ename</a>); | 
| 348 |  |  | } | 
| 349 |  |  | } else { | 
| 350 | wakaba | 1.2 | return qq(<a title="@{[&escape($name).&Resource('Title-Summary Delimiter',escape=>1).&Resource('JumpAndEditWikiPage',escape=>1)]}" href="$uri{wiki}?$option{latest}@{[&encode($name)]}" class="wiki not-exist">$ename<span class="mark">@{[&Resource('JumpAndEditWikiPageMark',escape=>1)]}</span></a>); | 
| 351 | wakaba | 1.1 | } | 
| 352 |  |  | } | 
| 353 |  |  |  | 
| 354 |  |  | sub make_urilink ($;%) { | 
| 355 |  |  | require URI; | 
| 356 |  |  | my $uri = shift; | 
| 357 |  |  | if ($uri =~ s/^IW://) {       ## InterWiki (not URI) | 
| 358 |  |  | $uri = &unescape ($uri); | 
| 359 |  |  | if ($uri =~ /^([^\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+|"(?:\\.|[^"\\])+"):([^\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+|"(?:\\.|[^"\\])+")$/) { | 
| 360 |  |  | my ($site, $name) = ($1, $2); | 
| 361 |  |  | for ($site, $name) { | 
| 362 |  |  | if (s/^"//) { s/"$//; s/\\(.)/$1/g } | 
| 363 |  |  | } | 
| 364 |  |  | &init_InterWikiName () unless $interwiki{'[[]]'}; | 
| 365 |  |  | if ($interwiki{$site}) { | 
| 366 |  |  | &load_formatter ('interwiki'); | 
| 367 |  |  | my $uri = &escape ($fmt{interwiki}->replace ($interwiki{$site} => {site => $site, name => $name})); | 
| 368 |  |  | $site = &escape ($site); $name = &escape ($name); | 
| 369 |  |  | qq(<<a href="$uri" class="out-of-wiki interwiki" title="$name ($site); URI: <$uri>"><span class="interwiki-site">$site:</span><span class="interwiki-name">$name</span></a>>); | 
| 370 |  |  | } else { | 
| 371 |  |  | qq(<@{[&Resource('Error:UnknownInterWikiName=',escape=>1)]}@{[&escape ($site)]}>); | 
| 372 |  |  | } | 
| 373 |  |  | } else { | 
| 374 |  |  | qq(<@{[&Resource('Error:InvalidInterWiki=',escape=>1)]}@{[&escape($uri)]}>); | 
| 375 |  |  | } | 
| 376 |  |  | } elsif ($uri =~ /^urn:/) {   ## URN | 
| 377 |  |  | my $uri2 = &escape (URI->new ('/uri-res/N2L?'.&unescape ($uri), 'http')->canonical); | 
| 378 |  |  | qq(<<a href="$uri2" title="URI: <$uri> (via <$uri2>)" class="out-of-wiki urn">$uri</a>>); | 
| 379 |  |  | } elsif ($uri =~ s/^MAIL://) {        ## mail address (not URI) | 
| 380 |  |  | my $uri2 = &escape (URI->new ('mailto:'.&unescape ($uri))->canonical); | 
| 381 |  |  | qq(<<a href="$uri2" class="out-of-wiki mail">$uri</a>>); | 
| 382 |  |  | } elsif ($uri =~ s/^IMG(?:\([^)]+\))?://) {   ## image (not URI itself) | 
| 383 |  |  | my $uri2 = &escape (URI->new (&unescape ($uri))->canonical); | 
| 384 |  |  | qq(<img src="$uri2" alt="" title="URI: <$uri2>" class="out-of-wiki">); | 
| 385 |  |  | } else {      ## misc. URI | 
| 386 |  |  | CGI::Carp::warningsToBrowser (0); | 
| 387 |  |  | my $uri2 = &escape (URI->new (&unescape ($uri))->canonical); | 
| 388 |  |  | CGI::Carp::warningsToBrowser (1); | 
| 389 |  |  | qq(<<a href="$uri2" title="URI: <$uri2>" class="out-of-wiki">$uri</a>>); | 
| 390 |  |  | } | 
| 391 |  |  | } | 
| 392 |  |  |  | 
| 393 |  |  | {my %FormIndex; | 
| 394 |  |  | sub make_custom_form ($$$$%) { | 
| 395 |  |  | my ($wfname, $definition, $template, $foption, $option) = @_; | 
| 396 |  |  | ## $template is currently not used in this procedure. | 
| 397 |  |  | #unless ($main::_EMBEDED) { | 
| 398 |  |  | $FormIndex{$option->{page}}++; | 
| 399 |  |  | if (length $definition) { | 
| 400 |  |  | my $param = bless {depth=>10}, 'SuikaWiki::Plugin'; | 
| 401 |  |  | my $lastmodified = $database->mtime ($option->{page}); | 
| 402 |  |  | &load_formatter (qw/form_input form_option/); | 
| 403 |  |  | $definition = &unescape ($definition); | 
| 404 |  |  | $definition =~ s/\\(['\\])/$1/g; | 
| 405 |  |  | $foption = &unescape ($foption); | 
| 406 |  |  | $foption =~ s/\\(['\\])/$1/g; | 
| 407 |  |  | $fmt{form_option}->replace ($foption, $param); | 
| 408 |  |  | $param->{output}->{form} = 1 unless defined $param->{output}->{form}; | 
| 409 |  |  | $param->{output}->{form} = 0 if $main::_EMBEDED; | 
| 410 |  |  | $definition .= ' %submit;' if $definition !~ /%submit/ && !$param->{output}->{nosubmit} && $param->{output}->{form}; | 
| 411 |  |  | $param->{output}->{page} ||= $option->{page}; | 
| 412 |  |  | $param->{form_disabled} = 1 if $database->meta (IsFrozen => $option->{page}); | 
| 413 |  |  | my $target_form = $param->{output}->{id}; | 
| 414 |  |  | my $r = ''; | 
| 415 |  |  | $r = <<EOH if $param->{output}->{form}; | 
| 416 |  |  | <form method="post" action="$url_cgi" id="wikiform-$FormIndex{$option->{page}}" class="wikiform"> | 
| 417 |  |  | <input type="hidden" name="mycmd" value="@{[$param->{form_disabled}?'default':'wikiform']}" /> | 
| 418 |  |  | <input type="hidden" name="mypage" value="@{[&escape($param->{output}->{page})]}" /> | 
| 419 |  |  | <input type="hidden" name="myLastModified" value="$lastmodified" /> | 
| 420 |  |  | <input type="hidden" name="mytouch" value="on" /> | 
| 421 |  |  | <input type="hidden" name="@{[$target_form? qq(wikiform_targetform" value="@{[&escape($target_form)]}) : qq(wikiform_index" value="$FormIndex{$option->{page}})]}" /> | 
| 422 |  |  | EOH | 
| 423 |  |  | $r .= qq(<a name="wikiform-$FormIndex{$option->{page}}"></a>) if $UA =~ m#Mozilla/[12]\.#; | 
| 424 |  |  | $r .= $fmt{form_input}->replace ($definition, $param); | 
| 425 |  |  | $r .= "</form>\n" if $param->{output}->{form}; | 
| 426 |  |  | $r; | 
| 427 |  |  | } else {  ## No input-interface WikiForm | 
| 428 |  |  | qq(<a id="wikiform-$FormIndex{$option->{page}}" name="wikiform-$FormIndex{$option->{page}}"><!-- #form --></a>); | 
| 429 |  |  | } | 
| 430 |  |  | #} else { | 
| 431 |  |  | #    qq(<ins class="wiki-error">@{[&Resource('Error:WikiForm:EmbedIsNotSupported',escape=>1)]}</ins>); | 
| 432 |  |  | #} | 
| 433 |  |  | }} | 
| 434 |  |  |  | 
| 435 |  |  | sub init_form { | 
| 436 |  |  | ## TODO: Support multipart/form-data | 
| 437 |  |  | my $query = ''; | 
| 438 |  |  | if (uc $main::ENV{REQUEST_METHOD} eq 'POST') { | 
| 439 |  |  | read STDIN, $query, $main::ENV{CONTENT_LENGTH}; | 
| 440 |  |  | } | 
| 441 |  |  | $query .= ($query ? ';' : '') . $main::ENV{QUERY_STRING}; | 
| 442 |  |  | if ($main::ENV{REQUEST_METHOD} ne 'POST' && $main::ENV{QUERY_STRING} && $main::ENV{QUERY_STRING} !~ /[&;=]/) { | 
| 443 |  |  | my $query = &decode($main::ENV{QUERY_STRING}); | 
| 444 |  |  | $query = &code_convert(\$query, $kanjicode); | 
| 445 |  |  | $form{mypage} = $query; | 
| 446 |  |  | $form{mycmd} = 'default'; | 
| 447 |  |  | } else { | 
| 448 |  |  | for (split /[;&]/, $query) { | 
| 449 |  |  | if (my ($n, $v) = split /=/, $_, 2) { | 
| 450 |  |  | for ($n, $v) {tr/+/ /; s/%([0-9A-Fa-f][0-9A-Fa-f])/pack 'C', hex $1/ge}; | 
| 451 |  |  | $form{$n} = $v; | 
| 452 |  |  | } | 
| 453 |  |  | } | 
| 454 |  |  | unless (defined $form{mypage}) { | 
| 455 |  |  | $form{mypage} = $form{epage}; | 
| 456 |  |  | $form{mypage} =~ s/([0-9A-F]{2})/ord hex $1/g; | 
| 457 |  |  | } | 
| 458 |  |  | $form{mypage} = &code_convert (\$form{mypage}, $kanjicode); | 
| 459 |  |  | } | 
| 460 |  |  | $form{mypage} ||= $PageName{FrontPage}; | 
| 461 |  |  | $form{mypage} =~ tr/\x00-\x1F\x7F//d; | 
| 462 | wakaba | 1.3 | $form{mypage} = SuikaWiki::Name::Space::normalize_name ($form{mypage}); | 
| 463 | wakaba | 1.1 | $form{mycmd} ||= 'default'; | 
| 464 | wakaba | 1.4 | $form{mycmd} =~ tr/-/_/; | 
| 465 | wakaba | 1.1 |  | 
| 466 |  |  | # mypreview_edit        -> do_edit, with preview. | 
| 467 |  |  | # mypreview_adminedit   -> do_adminedit, with preview. | 
| 468 |  |  | # mypreview_write       -> do_write, without preview. | 
| 469 |  |  | foreach (keys %form) { | 
| 470 |  |  | if (/^mypreview_(.*)$/) { | 
| 471 |  |  | $form{mycmd} = $1; | 
| 472 |  |  | $form{mypreview} = 1; | 
| 473 |  |  | } | 
| 474 |  |  | } | 
| 475 |  |  |  | 
| 476 |  |  | # | 
| 477 |  |  | # $form{mycmd} is frozen here. | 
| 478 |  |  | # | 
| 479 |  |  |  | 
| 480 | wakaba | 1.3 | for (grep /^(?:wikiform__|pi_)/, keys %form) { | 
| 481 | wakaba | 1.1 | $form{$_} = &code_convert (\$form{$_}, $kanjicode); | 
| 482 |  |  | } | 
| 483 |  |  | $form{mymsg} = &code_convert(\$form{mymsg}, $kanjicode); | 
| 484 |  |  | $form{myname} = &code_convert(\$form{myname}, $kanjicode); | 
| 485 |  |  | } | 
| 486 |  |  |  | 
| 487 |  |  | sub get_subjectline { | 
| 488 |  |  | my ($page, %option) = @_; | 
| 489 |  |  | my $SubjectLine = SuikaWiki::Plugin->cache ('headline'); | 
| 490 |  |  | unless (defined $SubjectLine->{$page}) { | 
| 491 |  |  | if (not &is_editable($page)) { | 
| 492 |  |  | $SubjectLine->{$page} = ""; | 
| 493 |  |  | } else { | 
| 494 |  |  | $SubjectLine->{$page} = do { | 
| 495 |  |  | my $s=$database{$page}; | 
| 496 |  |  | $s =~ s!^\#\?[^\x0A\x0D]+[\x0A\x0D]*!!s; | 
| 497 |  |  | $s =~ s/\x0D?\x0A.*//s; | 
| 498 |  |  | $s =~ s/^[-=]*\s*\[\d+\]\s*//; | 
| 499 |  |  | $s =~ s/'''?//g; | 
| 500 |  |  | $s =~ s/\[[A-Z]+(?:\([^)]+\))?\[([^]]+)\](?:\s\[([^]]+)\])?\]/$1$2/g; | 
| 501 |  |  | $s =~ s/\[\[([^]]+)\]\]/$1/g; | 
| 502 |  |  | $s}; | 
| 503 |  |  | } | 
| 504 |  |  | } | 
| 505 |  |  | if (length $SubjectLine->{$page}) { | 
| 506 |  |  | $option{delimiter} = defined $option{delimiter} ? $option{delimiter} : &Resource('Title-Summary Delimiter'); | 
| 507 |  |  | $option{delimiter}.$SubjectLine->{$page}.$option{tail}; | 
| 508 |  |  | } else { | 
| 509 |  |  | ''; | 
| 510 |  |  | } | 
| 511 |  |  | } | 
| 512 |  |  |  | 
| 513 |  |  | sub open_db { | 
| 514 |  |  | if ($modifier_dbtype eq 'dbmopen') { | 
| 515 |  |  | dbmopen(%database, $PathTo{WikiDataBase}, 0666) or die "(dbmopen) $PathTo{WikiDataBase}"; | 
| 516 |  |  | } elsif ($modifier_dbtype eq 'AnyDBM_File') { | 
| 517 |  |  | eval q{use AnyDBM_File}; | 
| 518 |  |  | tie(%database, "AnyDBM_File", $PathTo{WikiDataBase}, O_RDWR|O_CREAT, 0666) or die ("(tie AnyDBM_File) $PathTo{WikiDataBase}"); | 
| 519 |  |  | } elsif ($modifier_dbtype eq 'Yuki::YukiWikiDB') { | 
| 520 |  |  | eval q{use Yuki::YukiWikiDB}; | 
| 521 |  |  | tie(%database, "Yuki::YukiWikiDB", $PathTo{WikiDataBase}) or die ("(tie Yuki::YukiWikiDB) $PathTo{WikiDataBase}"); | 
| 522 |  |  | } else {    ## Yuki::YukiWikiDB || Yuki::YukiWikiDBMeta | 
| 523 |  |  | eval qq{use $modifier_dbtype}; | 
| 524 |  |  | $database = tie(%database, $modifier_dbtype => $PathTo{WikiDataBase}, -lock => 2, -backup => $wiki::diff::UseDiff) or die ("(tie $modifier_dbtype) $PathTo{WikiDataBase}"); | 
| 525 |  |  | } | 
| 526 |  |  | } | 
| 527 |  |  |  | 
| 528 |  |  | sub close_db { | 
| 529 |  |  | if ($modifier_dbtype eq 'dbmopen') { | 
| 530 |  |  | dbmclose(%database); | 
| 531 |  |  | } else { | 
| 532 |  |  | untie(%database); | 
| 533 |  |  | } | 
| 534 |  |  | } | 
| 535 |  |  |  | 
| 536 |  |  | sub editform (@) { | 
| 537 |  |  | my %option = @_; | 
| 538 |  |  | my $frozen = &is_frozen ($option{page}); | 
| 539 |  |  | $option{content} = $database{$option{page}} unless defined $option{content}; | 
| 540 |  |  | $option{content} = $database{NewPageTemplate} unless length $option{content}; | 
| 541 |  |  | $option{last_modified} = $database->mtime ($option{page}) unless defined $option{last_modified}; | 
| 542 |  |  | my $f = ''; | 
| 543 |  |  | my $magic = ''; | 
| 544 |  |  | $magic = $1 if $option{content} =~ m/^([^\x0A\x0D]+)/s; | 
| 545 |  |  |  | 
| 546 |  |  | my $selected = 'default'; | 
| 547 |  |  | if ($form{after_edit_cmd}) { | 
| 548 |  |  | $selected = $form{after_edit_cmd}; | 
| 549 |  |  | } elsif ($magic =~ /Const|Config|CSS/) { | 
| 550 |  |  | $selected = 'edit'; | 
| 551 |  |  | } | 
| 552 |  |  | my $afteredit = <<EOH; | 
| 553 |  |  | <select name="after_edit_cmd"> | 
| 554 |  |  | <option value="default" label="@{[&Resource('Edit:SaveAndDefault',escape=>1)]}"@{[$selected eq 'default' ? ' selected="selected"':'']}>@{[&Resource('Edit:SaveAndDefault',escape=>1)]}</option> | 
| 555 |  |  | <option value="read" label="@{[&Resource('Edit:SaveAndView',escape=>1)]}"@{[$selected eq 'read' ? ' selected="selected"':'']}>@{[&Resource('Edit:SaveAndView',escape=>1)]}</option> | 
| 556 |  |  | <option value="edit" label="@{[&Resource('Edit:SaveAndEdit',escape=>1)]}"@{[$selected eq 'edit' ? ' selected="selected"':'']}>@{[&Resource('Edit:SaveAndEdit',escape=>1)]}</option> | 
| 557 |  |  | </select> | 
| 558 |  |  | EOH | 
| 559 |  |  | $f .= <<"EOD"; | 
| 560 |  |  | <form action="$uri{wiki}" method="post"> | 
| 561 |  |  | @{[ $option{conflict} ? '' : qq(<label><input type="submit" name="mypreview_write" value="@{[&Resource('Edit:Save',escape=>1)]}" /><kbd>S</kbd></label>) ]} | 
| 562 |  |  | @{[ $option{admin} ? qq(<label>@{[&Resource('Edit:Password=',escape=>1)]}<input type="password" name="mypassword" value="" size="10" /></label>) : "" ]} [@{[&get_new_anchor_index($option{content})]}]<br /> | 
| 563 |  |  | <input type="hidden" name="myLastModified" value="$option{last_modified}" /> | 
| 564 |  |  | <input type="hidden" name="mypage" value="@{[&escape($form{mypage})]}" /> | 
| 565 |  |  | <textarea cols="@{[&Resource('Edit:Form:Cols')+0||80]}" rows="@{[&Resource('Edit:Form:Rows')+0||20]}" name="mymsg" tabindex="1">@{[&escape($option{content})]}</textarea><br /> | 
| 566 |  |  | @{[ | 
| 567 |  |  | $option{admin} ? | 
| 568 |  |  | qq( | 
| 569 |  |  | <label><input type="radio" name="myfrozen" value="1" @{[$frozen ? qq(checked="checked") : ""]} />@{[&Resource('Edit:Freeze',escape=>1)]}</label> | 
| 570 |  |  | <label><input type="radio" name="myfrozen" value="0" @{[$frozen ? "" : qq(checked="checked")]} />@{[&Resource('Edit:DontFreeze',escape=>1)]}</label><br />) | 
| 571 |  |  | : "" | 
| 572 |  |  | ]} | 
| 573 |  |  | @{[ | 
| 574 |  |  | $option{conflict} ? "" : | 
| 575 |  |  | qq( | 
| 576 |  |  | <label><input type="checkbox" name="mytouch" value="on" checked="checked" />@{[&Resource('Edit:UpdateTimeStamp',escape=>1)]}</label><br /> | 
| 577 |  |  | <label><input type="submit" name="mypreview_write" value="@{[&Resource('Edit:Save',escape=>1)]}" accesskey="S" /><kbd>S</kbd></label> | 
| 578 |  |  | $afteredit | 
| 579 |  |  | ) | 
| 580 |  |  | ]} | 
| 581 |  |  | </form> | 
| 582 |  |  | EOD | 
| 583 |  |  | $f; | 
| 584 |  |  | } | 
| 585 |  |  |  | 
| 586 |  |  | sub is_editable { | 
| 587 |  |  | my ($page) = @_; | 
| 588 | wakaba | 1.4 | return 0 unless SuikaWiki::Name::Space::validate_name ($page); | 
| 589 |  |  | return 0 if $page =~ /[\x00-\x20\[\]\x7F]/; | 
| 590 | wakaba | 1.2 | 1; | 
| 591 | wakaba | 1.1 | } | 
| 592 |  |  |  | 
| 593 |  |  | sub decode { | 
| 594 |  |  | my ($s) = @_; | 
| 595 |  |  | $s =~ tr/+/ /; | 
| 596 |  |  | $s =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack("C", hex($1))/eg; | 
| 597 |  |  | return $s; | 
| 598 |  |  | } | 
| 599 |  |  |  | 
| 600 |  |  | sub encode { | 
| 601 |  |  | my $s = shift; | 
| 602 |  |  | $s =~ s/([^0-9A-Za-z_-])/sprintf '%%%02X', ord $1/ge; | 
| 603 |  |  | $s; | 
| 604 |  |  | } | 
| 605 |  |  |  | 
| 606 |  |  | sub get_now { | 
| 607 |  |  | my ($sec, $min, $hour, $day, $mon, $year) = localtime(time); | 
| 608 |  |  | $year += 1900; | 
| 609 |  |  | $mon++; | 
| 610 |  |  | $mon = "0$mon" if $mon < 10; | 
| 611 |  |  | $day = "0$day" if $day < 10; | 
| 612 |  |  | $hour = "0$hour" if $hour < 10; | 
| 613 |  |  | $min = "0$min" if $min < 10; | 
| 614 |  |  | #$sec = "0$sec" if $sec < 10; | 
| 615 |  |  | return "$year-$mon-$day $hour:$min"; | 
| 616 |  |  | } | 
| 617 |  |  |  | 
| 618 |  |  | sub init_InterWikiName { | 
| 619 |  |  | my @content = split /\n/, $database{InterWikiName}; | 
| 620 |  |  | for (@content) { | 
| 621 |  |  | if (/^([^#]\S*)\s+(\S[^\x0A\x0D]+)/) { | 
| 622 |  |  | $interwiki{$1} = $2; | 
| 623 |  |  | } | 
| 624 |  |  | } | 
| 625 |  |  | $interwiki{'[[]]'} = 1;       ## dummy | 
| 626 |  |  | } | 
| 627 |  |  |  | 
| 628 |  |  | sub frozen_reject { | 
| 629 |  |  | my ($isfrozen) = $database->meta (IsFrozen => $form{mypage}); | 
| 630 |  |  | my ($willbefrozen) = $form{myfrozen}; | 
| 631 |  |  | if (not $isfrozen and not $willbefrozen) { | 
| 632 |  |  | # You need no check. | 
| 633 |  |  | return 0; | 
| 634 |  |  | } elsif (valid_password($form{mypassword})) { | 
| 635 |  |  | # You are admin. | 
| 636 |  |  | return 0; | 
| 637 |  |  | } else { | 
| 638 |  |  | &_do_view_msg (-view => '-error', -page => $form{mypage}, | 
| 639 |  |  | error_message => &Resource ('Error:PasswordIsIncorrect')); | 
| 640 |  |  | exit; | 
| 641 |  |  | } | 
| 642 |  |  | } | 
| 643 |  |  |  | 
| 644 |  |  | sub is_frozen ($) { $database->meta (IsFrozen => $_[0]) ? 1 : 0 } | 
| 645 |  |  |  | 
| 646 |  |  | sub do_comment { | 
| 647 |  |  | my ($content) = $database{$form{mypage}}; | 
| 648 |  |  | my $default_name;   ## this code is not strict. | 
| 649 |  |  | $default_name = $1 if $content =~ /default-name="([^"]+)"/; | 
| 650 |  |  | my $datestr = '[WEAK['.&get_now.']]'; | 
| 651 |  |  | my $namestr = $form{myname} || $default_name || &Resource('WikiForm:WikiComment:DefaultName'); | 
| 652 |  |  | ($namestr = '', $datestr = '') if $form{myname} eq 'nodate'; | 
| 653 |  |  | if ($namestr =~ /^(?:>>)?[0-9]/) { | 
| 654 |  |  | $namestr = qq( ''$namestr'': ); | 
| 655 |  |  | } elsif (length $namestr) { | 
| 656 |  |  | $namestr = qq( ''[[$namestr]]'': ); | 
| 657 |  |  | } | 
| 658 |  |  | my $anchor = &get_new_anchor_index ($content); | 
| 659 |  |  | my $i = 1;  my $o = 0; | 
| 660 |  |  | $content =~ s{(\[\[\#r?comment\]\])}{ | 
| 661 |  |  | my $embed = $1; | 
| 662 |  |  | if ($i == $form{comment_index}) { | 
| 663 |  |  | if ($embed ne '[[#rcomment]]') { | 
| 664 |  |  | $embed = "- [$anchor] $datestr$namestr$form{mymsg}\n$embed";  $o = 1; | 
| 665 |  |  | } else { | 
| 666 |  |  | $embed .= "\n- [$anchor] $datestr$namestr$form{mymsg}";  $o = 1; | 
| 667 |  |  | } | 
| 668 |  |  | } | 
| 669 |  |  | $i++; $embed; | 
| 670 |  |  | }ge; | 
| 671 |  |  | unless ($o) { | 
| 672 |  |  | $content = "#?SuikaWiki/0.9\n\n" unless $content; | 
| 673 |  |  | $content .= "\n" unless $content =~ /\n$/s; | 
| 674 |  |  | $content .= "- [$anchor] $datestr$namestr$form{mymsg}\n"; | 
| 675 |  |  | } | 
| 676 |  |  | $form{__comment_anchor_index} = $anchor; | 
| 677 |  |  | if ($form{mymsg} || $form{myname}) { | 
| 678 |  |  | $form{mymsg} = $content; | 
| 679 |  |  | $form{mytouch} = 'on'; | 
| 680 |  |  | &do_write; | 
| 681 |  |  | } else {    ## Don't write | 
| 682 |  |  | $form{mycmd} = 'default'; | 
| 683 |  |  | &do_view; | 
| 684 |  |  | } | 
| 685 |  |  | } | 
| 686 |  |  |  | 
| 687 |  |  | sub get_new_anchor_index ($) { | 
| 688 |  |  | my $content = shift; | 
| 689 |  |  | my $anchor = 0; | 
| 690 |  |  | $content =~ s/^(?:[-=]+\s*)?\[([0-9]+)\]/$anchor = $1 if $1 > $anchor; $&/mge; | 
| 691 |  |  | $anchor + 1; | 
| 692 |  |  | } | 
| 693 |  |  |  | 
| 694 |  |  | sub load_formatter (@) { | 
| 695 | wakaba | 1.4 | my $x = SuikaWiki::Plugin->feature ('SuikaWiki::Markup::XML'); | 
| 696 | wakaba | 1.1 | for my $t (@_) { | 
| 697 |  |  | unless ($fmt{$t}) { | 
| 698 |  |  | require Message::Util::Formatter; | 
| 699 |  |  | $fmt{$t} = Message::Util::Formatter->new; | 
| 700 |  |  | for (@{$SuikaWiki::Plugin::List{'wiki'.$t}||[]}) { | 
| 701 |  |  | $_->load_formatter ($fmt{$t}, type => 'wiki'.$t); | 
| 702 |  |  | } | 
| 703 | wakaba | 1.4 | $fmt{$t}->option (return_class => 'SuikaWiki::Markup::XML') if $x; | 
| 704 | wakaba | 1.1 | } | 
| 705 |  |  | } | 
| 706 |  |  | } | 
| 707 |  |  |  | 
| 708 |  |  | sub do_wikiform { | 
| 709 |  |  | my $content = $database{$form{mypage}}; | 
| 710 |  |  | my $anchor = &get_new_anchor_index ($content); | 
| 711 |  |  | &load_formatter (qw/form_template form_option/); | 
| 712 |  |  | my $write = 0; | 
| 713 |  |  | my $i = 1; | 
| 714 |  |  | $content =~ s{$embed_command{form}}{ | 
| 715 |  |  | my ($embed, $wfname, $template, $option) = ($&, $1, $3, $4); | 
| 716 |  |  | if (($wfname && $wfname eq $form{wikiform_targetform}) | 
| 717 |  |  | || $i == $form{wikiform_index}) { | 
| 718 |  |  | $template =~ s/\\([\\'])/$1/g; | 
| 719 |  |  | $option =~ s/\\([\\'])/$1/g; | 
| 720 |  |  | my $param = bless {depth=>10}, 'SuikaWiki::Plugin'; | 
| 721 |  |  | $param->{page} = $form{mypage}; | 
| 722 |  |  | $param->{form_index} = $i; | 
| 723 |  |  | $param->{form_name} = $wfname; | 
| 724 |  |  | $param->{anchor_index} = $anchor; | 
| 725 |  |  | $param->{argv} = \%form; | 
| 726 |  |  | $param->{default_name} = $1 if $content =~ /default-name="([^"]+)"/; | 
| 727 |  |  | $param->{default_name} ||= &Resource('WikiForm:WikiComment:DefaultName'); | 
| 728 |  |  | $fmt{form_option}->replace ($option, $param); | 
| 729 |  |  | my $t = 1; | 
| 730 |  |  | for (keys %{$param->{require}||{}}) { | 
| 731 |  |  | (undef $t, last) unless length $param->{argv}->{'wikiform__'.$_}; | 
| 732 |  |  | } | 
| 733 |  |  | $t = $fmt{form_template}->replace ($template, $param) if $t; | 
| 734 |  |  | if (length $t) { | 
| 735 |  |  | if ($param->{output}->{reverse}) { | 
| 736 |  |  | $embed .= "\n" . $t; | 
| 737 |  |  | } else { | 
| 738 |  |  | $embed = $t . "\n" . $embed; | 
| 739 |  |  | } | 
| 740 |  |  | $write = 1; | 
| 741 |  |  | $form{__comment_anchor_index} = $anchor | 
| 742 |  |  | if $param->{anchor_index_};  ## $anchor is used! | 
| 743 |  |  | } | 
| 744 |  |  | $form{__wikiform_anchor_index} = $i; | 
| 745 |  |  | undef $form{wikiform_targetform};  ## Make sure never to match | 
| 746 |  |  | undef $form{wikiform_index};       ## with WikiForm in rest of page! | 
| 747 |  |  | } | 
| 748 |  |  | $i++; $embed; | 
| 749 |  |  | }ge; | 
| 750 |  |  | unless ($write) { | 
| 751 |  |  | #$content = "#?SuikaWiki/0.9\n\n" unless $content; | 
| 752 |  |  | #$content .= "\n" unless $content =~ /\n$/s; | 
| 753 |  |  | # | 
| 754 |  |  | } | 
| 755 |  |  | if ($write) { | 
| 756 |  |  | $form{mymsg} = $content; | 
| 757 |  |  | $form{mytouch} = 'on'; | 
| 758 |  |  | &do_write; | 
| 759 |  |  | } else {    ## Don't write! | 
| 760 |  |  | $form{mycmd} = 'default'; | 
| 761 |  |  | &do_view; | 
| 762 |  |  | } | 
| 763 |  |  | } | 
| 764 |  |  |  | 
| 765 |  |  | sub code_convert { | 
| 766 |  |  | require Jcode; | 
| 767 |  |  | my ($contentref, $code) = (shift, shift || $kanjicode); | 
| 768 |  |  | if    ($code =~ /euc/) { $code = 'euc' } | 
| 769 |  |  | elsif ($code =~ /iso/) { $code = 'jis' } | 
| 770 |  |  | elsif ($code =~ /shi/) { $code = 'sjis' } | 
| 771 |  |  | elsif ($code =~ /utf/) { $code = 'utf8' } | 
| 772 |  |  | $$contentref = Jcode->new ($contentref)->tr ("\xA3\xB0-\xA3\xB9\xA3\xC1-\xA3\xDA\xA3\xE1-\xA3\xFA\xA1\xF5\xA1\xA4\xA1\xA5\xA1\xA7\xA1\xA8\xA1\xA9\xA1\xAA\xA1\xAE\xA1\xB0\xA1\xB2\xA1\xBF\xA1\xC3\xA1\xCA\xA1\xCB\xA1\xCE\xA1\xCF\xA1\xD0\xA1\xD1\xA1\xDC\xA1\xF0\xA1\xF3\xA1\xF4\xA1\xF6\xA1\xF7\xA1\xE1\xA2\xAF\xA2\xB0\xA2\xB2\xA2\xB1\xA1\xE4\xA1\xE3\xA1\xC0\xA1\xA1" => q(0-9A-Za-z&,.:;?!`^_/|()[]{}+$%#*@='"~-><\ ))->$code; | 
| 773 |  |  | return $$contentref; | 
| 774 |  |  | } | 
| 775 |  |  |  | 
| 776 |  |  | sub _rfc3339_date ($) { | 
| 777 |  |  | my @time = gmtime (shift); | 
| 778 |  |  | sprintf '%04d-%02d-%02dT%02d:%02d:%02d+00:00', $time[5]+1900,$time[4]+1,@time[3,2,1,0]; | 
| 779 |  |  | } | 
| 780 |  |  |  | 
| 781 |  |  | my %_Resource; | 
| 782 |  |  | sub Resource ($;%) { | 
| 783 |  |  | my ($s, %o) = @_; | 
| 784 |  |  | unless (defined $_Resource{$s}) { | 
| 785 |  |  | $_Resource{$_[0]} = &wiki::resource::get ($s, $_Resource{__option}); | 
| 786 |  |  | } | 
| 787 |  |  | $o{escape} ? &escape ($_Resource{$s}) : $_Resource{$s}; | 
| 788 |  |  | } | 
| 789 |  |  |  | 
| 790 |  |  | package wiki::referer; | 
| 791 |  |  | sub add ($$) { | 
| 792 |  |  | my $page = shift; | 
| 793 |  |  | my $uri = shift; | 
| 794 |  |  | unless (ref $uri) { | 
| 795 |  |  | require URI; | 
| 796 |  |  | $uri = URI->new ($uri); | 
| 797 |  |  | ## Some schemes do not have query part. | 
| 798 |  |  | eval q{ $uri->query (undef) if $uri->query =~ /^[0-9]{6,8}$/ }; | 
| 799 |  |  | $uri->fragment (undef); | 
| 800 |  |  | } | 
| 801 |  |  | $uri = $uri->canonical; | 
| 802 |  |  | return unless $uri; | 
| 803 |  |  | for my $regex (&get_dont_record) { | 
| 804 |  |  | return if $uri =~ /$regex/; | 
| 805 |  |  | } | 
| 806 |  |  | my %list = get ($page); | 
| 807 |  |  | $list{ $uri }++; | 
| 808 |  |  | set ($page, \%list); | 
| 809 |  |  | } | 
| 810 |  |  | sub get ($) { split /"/, $main::database->meta (Referer => $_[0]) } | 
| 811 |  |  | sub set ($%) { | 
| 812 |  |  | my $page = shift; | 
| 813 |  |  | my $list = shift; | 
| 814 |  |  | $main::database->meta (Referer => $page => join '"', %$list); | 
| 815 |  |  | } | 
| 816 |  |  |  | 
| 817 |  |  | sub get_dont_record () { | 
| 818 |  |  | map {s/\$/\\\$/g; s/\@/\\\@/g; $_} | 
| 819 |  |  | grep !/^#/, | 
| 820 |  |  | split /[\x0D\x0A]+/, $main::database{RefererDontRecord}; | 
| 821 |  |  | } | 
| 822 |  |  | sub get_site_name () { | 
| 823 |  |  | my @lines = grep /[^#]/, split /[\x0D\x0A]+/, $main::database{RefererSiteName}; | 
| 824 |  |  | my @item; | 
| 825 |  |  | for (@lines) { | 
| 826 |  |  | next if /^#/; | 
| 827 |  |  | my ($uri, $name) = split /\s+/, $_, 2; | 
| 828 |  |  | $uri =~ s/\$/\\\$/g;  $uri =~ s/\@/\\\@/g;  $uri =~ s/\//\\\//g; | 
| 829 |  |  | $name =~ s!([()/\\])!\\$1!g;  $name =~ s/\$([0-9]+)/).__decode (\${$1}).q(/g; | 
| 830 |  |  | push @item, [$uri, qq(q($name))]; | 
| 831 |  |  | } | 
| 832 |  |  | @item; | 
| 833 |  |  | } | 
| 834 |  |  |  | 
| 835 |  |  | sub list_html ($) { | 
| 836 |  |  | my $page = shift; | 
| 837 |  |  | my %list = get ($page); | 
| 838 |  |  | my $r = ''; | 
| 839 |  |  | my @name = get_site_name (); | 
| 840 |  |  | for my $uri (sort {$list{$b}<=>$list{$a}||$a cmp $b} keys %list) { | 
| 841 |  |  | my $title; | 
| 842 |  |  | for my $item (@name) { | 
| 843 |  |  | if ($uri =~ /$item->[0]/) { | 
| 844 |  |  | $title = $uri; | 
| 845 |  |  | eval qq{\$title =~ s/^.*$item->[0].*\$/$item->[1]/e} | 
| 846 |  |  | or die $@ ;#. qq{\$title =~ s/^.*$item->[0].*\$/$item->[1]/e}; | 
| 847 |  |  | last; | 
| 848 |  |  | } | 
| 849 |  |  | } | 
| 850 |  |  | my $euri = main::escape ($uri); | 
| 851 |  |  | if ($title) { | 
| 852 |  |  | $r .= qq(<li>{$list{$uri}} <a href="$euri" title="URI: <$euri>">@{[main::escape ($title)]}</a></li>\n); | 
| 853 |  |  | } else { | 
| 854 |  |  | $r .= qq(<li>{$list{$uri}} <<a href="$euri">$euri</a>></li>\n); | 
| 855 |  |  | } | 
| 856 |  |  | } | 
| 857 |  |  | $r ? qq(<ul>$r</ul>\n) : ''; | 
| 858 |  |  | } | 
| 859 |  |  |  | 
| 860 |  |  | sub __decode ($) { | 
| 861 |  |  | my $s = shift; | 
| 862 |  |  | $s =~ tr/+/ /; | 
| 863 |  |  | $s =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/chr hex $1/ge; | 
| 864 |  |  | main::code_convert (\$s); | 
| 865 |  |  | } | 
| 866 |  |  |  | 
| 867 |  |  | package wiki::useragent; | 
| 868 |  |  | our $UseLog; | 
| 869 |  |  |  | 
| 870 |  |  | sub add ($) { | 
| 871 |  |  | my $s = shift; | 
| 872 |  |  | return unless length $s; | 
| 873 |  |  | return unless $UseLog; | 
| 874 |  |  | $s =~ s/([\x00-\x08\x0A-\x1F\x25\x7F-\xFF])/sprintf '%%%02X', unpack 'C', $1/ge; | 
| 875 |  |  | my %ua; | 
| 876 |  |  | for (split /\n/, $main::database{$main::PageName{UserAgentList}}) { | 
| 877 |  |  | if (/^-\[(\d+)\] (.+)$/) { | 
| 878 |  |  | my ($t, $n) = ($1, $2); | 
| 879 |  |  | $n =~ tr/\x0A\x0D//d; | 
| 880 |  |  | $ua{$n} = $t; | 
| 881 |  |  | } | 
| 882 |  |  | } | 
| 883 |  |  | $ua{$s}++; | 
| 884 |  |  | my $s = qq(#?SuikaWiki/0.9\n); | 
| 885 |  |  | for (sort {$ua{$a} <=> $ua{$b}} keys %ua) { | 
| 886 |  |  | $s .= sprintf qq(-[%d] %s\n), $ua{$_}, $_; | 
| 887 |  |  | } | 
| 888 |  |  | $main::database->STORE ($main::PageName{UserAgentList} => $s, -touch => 0); | 
| 889 |  |  | } | 
| 890 |  |  |  | 
| 891 |  |  | package wiki::suikawikiconst; | 
| 892 |  |  |  | 
| 893 |  |  | sub to_hash ($;$) { | 
| 894 |  |  | my $page = shift; | 
| 895 |  |  | my $h = shift || {}; | 
| 896 |  |  | my $val; | 
| 897 |  |  | for my $line (split /\n/, $page) { | 
| 898 |  |  | next if $line =~ /^#/; | 
| 899 |  |  | $line =~ tr/\x0A\x0D//d; | 
| 900 |  |  | if ($val && $line =~ s/^\s+\\?//) { | 
| 901 |  |  | $h->{$val} .= length $h->{$val} ? "\n" . $line : $line; | 
| 902 |  |  | } elsif ($line =~ /^(.+):/) { | 
| 903 |  |  | $val = $1; $h->{$val} = ''; | 
| 904 |  |  | } | 
| 905 |  |  | } | 
| 906 |  |  | $h; | 
| 907 |  |  | } | 
| 908 |  |  |  | 
| 909 |  |  | package wiki::dummy; | 
| 910 |  |  | sub mtime (@) {undef} | 
| 911 |  |  | sub meta (@) {undef} | 
| 912 |  |  | sub Yuki::YukiWikiDB2::meta (@) {undef} | 
| 913 |  |  |  | 
| 914 |  |  | package SuikaWiki::Plugin; | 
| 915 |  |  | sub escape ($$) { main::escape ($_[1]) } | 
| 916 |  |  | sub unescape ($$) { main::unescape ($_[1]) } | 
| 917 |  |  | sub encode ($$) { main::encode ($_[1]) } | 
| 918 |  |  | sub decode ($$) { main::decode ($_[1]) } | 
| 919 |  |  | sub __get_datetime ($) { main::get_now () } | 
| 920 |  |  | sub resource ($$;%) { shift; &main::Resource (@_) } | 
| 921 |  |  | sub uri ($$) { $main::uri{$_[1]} } | 
| 922 |  |  | sub user_agent_names ($) { $main::UA } | 
| 923 | wakaba | 1.4 | sub _path_to ($$) { $main::PathTo{$_[1]} } | 
| 924 | wakaba | 1.3 |  | 
| 925 | wakaba | 1.1 | sub formatter ($$) { | 
| 926 |  |  | &main::load_formatter ($_[1]); | 
| 927 |  |  | $main::fmt{$_[1]}; | 
| 928 |  |  | } | 
| 929 |  |  | sub format_converter ($$$) { | 
| 930 |  |  | &main::load_formatter ('format'); | 
| 931 |  |  | $main::fmt{format}->{($_[1]=~/([A-Za-z0-9]\S+)/?$1:'SuikaWiki/0.9').'_to_'.$_[2]} | 
| 932 |  |  | || $main::fmt{format}->{($_[1]=~/([A-Za-z0-9](?:(?!\/)\S)+)/?$1:'SuikaWiki').'_to_'.$_[2]}; | 
| 933 |  |  | } | 
| 934 | wakaba | 1.4 | sub formatter_replace_if_not_parsed_yet ($$$$;$) { | 
| 935 |  |  | my ($o, $context, $p, $attr_name, $option) = @_; | 
| 936 | wakaba | 1.3 | &main::load_formatter ($context); | 
| 937 | wakaba | 1.4 | if ((ref $p->{$attr_name} && $p->{$attr_name}->flag ('parsed')) | 
| 938 |  |  | || (!ref $p->{$attr_name} && index ($p->{-option}->{$attr_name}, 'p') > -1)) { | 
| 939 |  |  | $p->{$attr_name};   ## Already parsed | 
| 940 |  |  | } else { | 
| 941 |  |  | $main::fmt{$context}->replace ($p->{$attr_name}, $o, {formatter => $main::fmt{$context}}); | 
| 942 |  |  | } | 
| 943 | wakaba | 1.3 | } | 
| 944 |  |  |  | 
| 945 | wakaba | 1.1 | sub cache ($$) { | 
| 946 |  |  | our %Cache; | 
| 947 |  |  | my (undef, $name, %option) = @_; | 
| 948 |  |  | unless (ref $Cache{$name}) { | 
| 949 |  |  | my %cache; | 
| 950 |  |  | tie (%cache, 'Yuki::YukiWikiCache', -file => $main::PathTo{CachePrefix}.$name, %option); | 
| 951 |  |  | $Cache{$name} = \%cache; | 
| 952 |  |  | } | 
| 953 |  |  | $Cache{$name}; | 
| 954 |  |  | } | 
| 955 |  |  | sub _database ($) { $main::database } | 
| 956 |  |  | sub _database_exist ($$) { exists $main::database{$_[1]} } | 
| 957 |  |  | sub _html_wikilink ($$%) { shift; &main::make_wikilink (@_) } | 
| 958 |  |  | sub _uri_wiki_page ($$%) { | 
| 959 |  |  | my (undef, $page, %option) = @_; | 
| 960 |  |  | $option{mode} ||= 'read'; | 
| 961 |  |  | length $page ? undef : ($page = $main::PageName{FrontPage}); | 
| 962 |  |  | $option{href} = $main::uri{wiki}.'?'; | 
| 963 |  |  | if ($option{up_to_date} || $option{mode} ne 'read' || $option{add_param}) { | 
| 964 |  |  | $option{href} .= qq(mypage=@{[&main::encode($page)]};mycmd=@{[&main::encode($option{mode})]}); | 
| 965 |  |  | $option{href} .= ';'.$option{add_param} if $option{add_param}; | 
| 966 |  |  | $option{href} .= ';x-d='.time if $option{up_to_date}; | 
| 967 |  |  | $option{href} .= ';x-lm='.($main::database->mtime ($page)||0) if $option{with_lm}; | 
| 968 |  |  | } else { | 
| 969 |  |  | $option{href} .= &main::encode ($page); | 
| 970 |  |  | } | 
| 971 |  |  | $option{href}; | 
| 972 |  |  | } | 
| 973 |  |  |  | 
| 974 |  |  |  | 
| 975 |  |  | package wiki::conneg; | 
| 976 |  |  |  | 
| 977 |  |  | ## BUG: this parser isn't strict. | 
| 978 |  |  | sub get_accept_lang (;$) { | 
| 979 |  |  | my $alang = shift || $main::ENV{HTTP_ACCEPT_LANGUAGE}; | 
| 980 |  |  | my %alang = (ja => 0.0002, en => 0.0001); | 
| 981 |  |  | if ($main::UA =~ m#Mozilla/0\.#) { | 
| 982 |  |  | $alang{ja} = 0.00001; | 
| 983 |  |  | } | 
| 984 |  |  | my $i = 0.1; | 
| 985 |  |  | for (split /\s*,\s*/, $alang) { | 
| 986 |  |  | tr/\x09\x0A\x0D\x20//d; | 
| 987 |  |  | if (/((?:(?!;q=).)+)(?:;q="?([0-9.]+)"?)?/) { | 
| 988 |  |  | my $l = lc $1; $l =~ tr/\x22\x5C//d; | 
| 989 |  |  | $alang{$l} = (defined $2 ? $2 : 1.000)*1000; | 
| 990 |  |  | $alang{$l} += $i unless $alang{$l} == 0; | 
| 991 |  |  | $i -= 0.001; | 
| 992 |  |  | } | 
| 993 |  |  | } | 
| 994 |  |  | \%alang; | 
| 995 |  |  | } | 
| 996 |  |  |  | 
| 997 |  |  | package wiki::resource; | 
| 998 |  |  |  | 
| 999 |  |  | sub get ($;\%) { | 
| 1000 |  |  | my ($resname, $option) = @_; | 
| 1001 |  |  | $option->{accept_language} ||= &wiki::conneg::get_accept_lang (); | 
| 1002 |  |  | $option->{resource} ||= {}; | 
| 1003 |  |  | my $v; | 
| 1004 |  |  | for my $lang (sort {$option->{accept_language}->{$b} <=> $option->{accept_language}->{$a}} grep {$option->{accept_language}->{$_}!=0} keys %{$option->{accept_language}}) { | 
| 1005 |  |  | while (length $lang) { | 
| 1006 |  |  | unless ($option->{accept_language}->{defined $option->{accept_language}->{$lang} ? $lang : '*'} == 0) { | 
| 1007 | wakaba | 1.3 | $option->{resource}->{$lang} ||= &wiki::suikawikiconst::to_hash ($main::database{$main::PageName{ResourceNS}.$lang}); | 
| 1008 | wakaba | 1.1 | $v = $option->{resource}->{$lang}->{$resname}; | 
| 1009 |  |  | last if defined $v; | 
| 1010 |  |  | } | 
| 1011 |  |  | $lang =~ s/[^+-]*$//; $lang =~ s/[+-]$//; | 
| 1012 |  |  | } | 
| 1013 |  |  | last if defined $v; | 
| 1014 |  |  | } | 
| 1015 |  |  | defined $v ? $v : $resname; | 
| 1016 |  |  | } | 
| 1017 |  |  |  | 
| 1018 |  |  | package main; | 
| 1019 |  |  | &SuikaWiki::Plugin::import_plugins (); | 
| 1020 |  |  | &main (); | 
| 1021 |  |  |  | 
| 1022 |  |  | =head1 NAME | 
| 1023 |  |  |  | 
| 1024 | wakaba | 1.4 | lib/suikawiki.pl --- SuikaWiki transitional library | 
| 1025 | wakaba | 1.1 |  | 
| 1026 |  |  | =head1 AUTHOR | 
| 1027 |  |  |  | 
| 1028 | wakaba | 1.4 | Hiroshi Yuki <hyuki@hyuki.com> <http://www.hyuki.com/yukiwiki/> (YukiWiki) | 
| 1029 | wakaba | 1.1 |  | 
| 1030 | wakaba | 1.4 | Makio Tsukamoto <http://digit.que.ne.jp/> (WalWiki) | 
| 1031 | wakaba | 1.1 |  | 
| 1032 |  |  | Wakaba <w@suika.fam.cx> | 
| 1033 |  |  |  | 
| 1034 |  |  | =head1 LICENSE | 
| 1035 |  |  |  | 
| 1036 | wakaba | 1.4 | Copyright AUTHORS 2000-2003 | 
| 1037 | wakaba | 1.1 |  | 
| 1038 |  |  | This program is free software; you can redistribute it and/or | 
| 1039 |  |  | modify it under the same terms as Perl itself. | 
| 1040 |  |  |  | 
| 1041 |  |  | =cut | 
| 1042 |  |  |  | 
| 1043 | wakaba | 1.4 | 1; # $Date: 2003/04/03 01:09:07 $ |