/[suikacvs]/webroot/swe/lib/suikawiki/main.pl
Suika

Contents of /webroot/swe/lib/suikawiki/main.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.57 - (hide annotations) (download)
Sun Dec 27 11:38:50 2009 UTC (15 years, 3 months ago) by wakaba
Branch: MAIN
CVS Tags: HEAD
Changes since 1.56: +6 -5 lines
File MIME type: text/plain
++ swe/lib/suikawiki/ChangeLog	27 Dec 2009 11:38:31 -0000
	* main.pl: Don't invoke |update_tfidf| until the HTTP redirect has
	been dispatched to the client.

2009-12-27  Wakaba  <wakaba@suika.fam.cx>

1 wakaba 1.1 use strict;
2     use feature 'state';
3    
4     ## --- Configurations
5    
6     our $db_dir_name;
7     my $db_sw3_dir_name = $db_dir_name . 'sw3pages/';
8     my $db_global_lock_dir_name = $db_dir_name;
9     my $db_id_dir_name = $db_dir_name . q[ids/];
10     my $db_name_dir_name = $db_dir_name . q[names/];
11    
12     ## --- Common modules
13    
14     require Encode;
15    
16     require Message::DOM::DOMImplementation;
17     my $dom = Message::DOM::DOMImplementation->new;
18    
19     use Message::CGI::Util qw/percent_encode percent_encode_na
20 wakaba 1.3 percent_decode htescape get_absolute_url
21     datetime_in_content/;
22 wakaba 1.1
23     require Char::Normalize::FullwidthHalfwidth;
24    
25 wakaba 1.38 use SWE::Lang qw/@ContentMediaType/;
26    
27 wakaba 1.1 ## --- Prepares database access variables (commonly used ones)
28    
29 wakaba 1.7 require SWE::DB;
30     my $db = SWE::DB->new;
31 wakaba 1.9 $db->db_dir_name = $db_dir_name;
32 wakaba 1.7 $db->global_lock_dir_name = $db_global_lock_dir_name;
33     $db->id_dir_name = $db_id_dir_name;
34     $db->name_dir_name = $db_name_dir_name;
35 wakaba 1.9 $db->sw3db_dir_name = $db_sw3_dir_name;
36 wakaba 1.7
37 wakaba 1.1 require SWE::DB::SuikaWiki3PageList2;
38     my $sw3_pages = SWE::DB::SuikaWiki3PageList2->new;
39     $sw3_pages->{root_directory_name} = $db_sw3_dir_name;
40    
41     require SWE::DB::Lock;
42     my $names_lock = SWE::DB::Lock->new;
43     $names_lock->{file_name} = $db_global_lock_dir_name . 'ids.lock';
44 wakaba 1.17 $names_lock->lock_type ('Names');
45 wakaba 1.1 ## NOTE: This lock MUST be used when $sw3pages or $name_prop_db is updated.
46    
47 wakaba 1.40 # XXX $db->id_prop
48 wakaba 1.1 require SWE::DB::IDProps;
49     my $id_prop_db = SWE::DB::IDProps->new;
50     $id_prop_db->{root_directory_name} = $db_id_dir_name;
51     $id_prop_db->{leaf_suffix} = '.props';
52    
53     require SWE::DB::IDLocks;
54     my $id_locks = SWE::DB::IDLocks->new;
55     $id_locks->{root_directory_name} = $db_id_dir_name;
56     $id_locks->{leaf_suffix} = '.lock';
57    
58     require SWE::DB::HashedProps;
59     my $name_prop_db = SWE::DB::HashedProps->new;
60     $name_prop_db->{root_directory_name} = $db_name_dir_name;
61     $name_prop_db->{leaf_suffix} = '.props';
62    
63     my $cache_prop_db = SWE::DB::IDProps->new;
64     $cache_prop_db->{root_directory_name} = $db_id_dir_name;
65     $cache_prop_db->{leaf_suffix} = '.cacheprops';
66    
67     require SWE::DB::IDText;
68     my $content_db = SWE::DB::IDText->new;
69     $content_db->{root_directory_name} = $db_id_dir_name;
70     $content_db->{leaf_suffix} = '.txt';
71    
72     ## --- Process Request-URI
73    
74     require Message::CGI::HTTP;
75     my $cgi = Message::CGI::HTTP->new;
76     $cgi->{decoder}->{'#default'} = sub {
77     return Encode::decode ('utf-8', $_[1]);
78     };
79    
80     ## NOTE: This script requires the server set REQUEST_URI and
81     ## SCRIPT_NAME which is the part of the REQUEST_URI that identifies
82     ## the script.
83    
84 wakaba 1.54 my $ru;
85 wakaba 1.55 if ($ENV{QUERY_STRING} =~ s/(?:^|&)ru=([^&]+)//) {
86 wakaba 1.54 $ru = $1;
87     }
88     if (defined $ru and length $ru) {
89     $ENV{REQUEST_URI} = $ru;
90 wakaba 1.55 #warn "RU: $ru";
91 wakaba 1.54 }
92    
93 wakaba 1.1 my $rurl = $dom->create_uri_reference ($cgi->request_uri)
94     ->get_uri_reference;
95     my $sname = $dom->create_uri_reference
96     (percent_encode_na ($cgi->get_meta_variable ('SCRIPT_NAME')))
97     ->get_absolute_reference ($rurl)
98     ->get_uri_reference;
99     my $path = $rurl->get_relative_reference ($sname);
100     $path->uri_query (undef);
101     $path->uri_fragment (undef);
102    
103     my $param;
104     if ($path =~ s[;([^/]*)\z][]) {
105     $param = percent_decode ($1);
106     }
107     my $dollar;
108     if ($path =~ s[\$([^/]*)\z][]) {
109     $dollar = percent_decode ($1);
110     }
111    
112     my @path = map { s/\+/%2F/g; percent_decode ($_) } split m#/#, $path, -1;
113     shift @path; # script's name
114    
115     ## --- Process request and generate response
116    
117     sub HTML_NS () { q<http://www.w3.org/1999/xhtml> }
118    
119     if ($path[0] eq 'n' and @path == 2) {
120     my $name = normalize_name ($path[1]);
121    
122     unless (length $name) {
123     our $homepage_name;
124     http_redirect (303, 'See other', get_page_url ($homepage_name, undef));
125 wakaba 1.56 exit;
126 wakaba 1.1 }
127    
128     unless (defined $param) {
129 wakaba 1.23 my ($id, $ids) = prepare_by_name ($name, $dollar);
130 wakaba 1.1
131 wakaba 1.23 if (defined $dollar and not defined $id) {
132     http_redirect (301, 'Not found', get_page_url ($name, undef));
133 wakaba 1.56 exit;
134 wakaba 1.1 }
135    
136     my $format = $cgi->get_parameter ('format') // 'html';
137    
138     ## TODO: Is it semantically valid that there is path?format=html
139     ## (200) but no path?format=xml (404)?
140 wakaba 1.42
141 wakaba 1.36 require SWE::Object::Document;
142     my $docobj = SWE::Object::Document->new (id => $id, db => $db);
143 wakaba 1.39 $docobj->rebless;
144 wakaba 1.36
145 wakaba 1.1 if ($format eq 'text' and defined $id) {
146 wakaba 1.36 $docobj->{content_db} = $content_db; ## XXX
147     $docobj->{id_prop_db} = $id_prop_db; ## XXX
148 wakaba 1.1
149     binmode STDOUT, ':encoding(utf-8)';
150 wakaba 1.41 print qq[X-SW-Hash: @{[ $id_prop_db->get_data ($id)->{hash} ]}\n];
151 wakaba 1.36 print qq[Content-Type: @{[$docobj->to_text_media_type]}; charset=utf-8\n\n];
152     print ${$docobj->to_text};
153 wakaba 1.1 exit;
154     } elsif ($format eq 'xml' and defined $id) {
155 wakaba 1.36 ## XXX
156 wakaba 1.40 $docobj->{content_db} = $content_db; ## XXX
157 wakaba 1.36 $docobj->{id_prop_db} = $id_prop_db;
158     $docobj->{cache_prop_db} = $cache_prop_db;
159     $docobj->{swml_to_xml} = \&get_xml_data;
160    
161     my $xmldoc = $docobj->to_xml (styled => scalar $cgi->get_parameter ('styled'));
162     if ($xmldoc) {
163     binmode STDOUT, ':encoding(utf-8)';
164     print qq[Content-Type: @{[$docobj->to_xml_media_type]}; charset=utf-8\n\n];
165    
166     print $xmldoc->inner_html;
167     } else {
168     http_error (406, q{format=xml is not supported for this page});
169 wakaba 1.1 }
170     exit;
171     } elsif ($format eq 'html') {
172     my $html_doc;
173     my $html_container;
174     my $title_text;
175 wakaba 1.3 my $id_prop;
176 wakaba 1.42
177 wakaba 1.1 if (defined $id) {
178 wakaba 1.37 # XXX
179     $docobj->{id_prop_db} = $id_prop_db;
180     $docobj->{cache_prop_db} = $cache_prop_db;
181 wakaba 1.40 $docobj->{content_db} = $content_db; ## XXX
182 wakaba 1.37 $docobj->{swml_to_xml} = \&get_xml_data;
183     $docobj->{name} = $name;
184     $docobj->{get_page_url} = \&get_page_url;
185    
186 wakaba 1.36 $docobj->lock;
187 wakaba 1.37 ($html_doc, $html_container) = $docobj->to_html_fragment;
188 wakaba 1.1
189 wakaba 1.44 $id_prop = $id_prop_db->get_data ($id);
190 wakaba 1.1 $title_text = $id_prop->{title};
191     ## TODO: $title_type
192    
193 wakaba 1.36 $docobj->unlock;
194 wakaba 1.37 }
195    
196     unless ($html_doc) {
197 wakaba 1.1 $html_doc = $dom->create_document;
198     $html_doc->strict_error_checking (0);
199     $html_doc->dom_config->set_parameter
200     ('http://suika.fam.cx/www/2006/dom-config/strict-document-children' => 0);
201     $html_doc->manakai_is_html (1);
202     }
203    
204     $html_doc->inner_html ('<!DOCTYPE HTML><title></title>');
205    
206     $html_doc->get_elements_by_tag_name ('title')->[0]->text_content ($name);
207 wakaba 1.40
208     my @link;
209    
210     my $tmt = $docobj->to_text_media_type;
211     if (defined $tmt) {
212     push @link, {rel => 'alternate', type => $tmt,
213     href => get_page_url ($name, undef, $id) . '?format=text'};
214     }
215    
216     my $xmt = $docobj->to_xml_media_type;
217     if (defined $xmt) {
218     push @link, {rel => 'alternate', type => $xmt,
219     href => get_page_url ($name, undef, $id) . '?format=xml'};
220     }
221    
222     push @link, {rel => 'archives',
223 wakaba 1.2 href => get_page_url ($name, undef, undef) . ';history',
224 wakaba 1.40 title => 'History of the page name'};
225 wakaba 1.2 if (defined $id) {
226     push @link, {rel => 'archives',
227     href => '../i/' . $id . ';history',
228     title => 'History of the page content'};
229     }
230     set_head_content ($html_doc, $id, \@link);
231 wakaba 1.1
232     my $body_el = $html_doc->last_child->last_child;
233    
234     my $h1_el = $html_doc->create_element_ns (HTML_NS, 'h1');
235     my $a_el = $html_doc->create_element_ns (HTML_NS, 'a');
236     $a_el->set_attribute (href => get_page_url ($name, undef));
237     $a_el->set_attribute (rel => 'bookmark');
238     $a_el->text_content ($name);
239     $h1_el->append_child ($a_el);
240     $body_el->append_child ($h1_el);
241    
242     if (@$ids) {
243     my $nav_el = $html_doc->create_element_ns (HTML_NS, 'div');
244     $nav_el->set_attribute (class => 'nav swe-ids');
245     $nav_el->manakai_append_text
246     (@$ids == 1 ? 'There is another page with same name:'
247     : 'There are other pages with same name:');
248     my $ul_el = $html_doc->create_element_ns (HTML_NS, 'ul');
249     for my $id (@$ids) {
250     my $li_el = $html_doc->create_element_ns (HTML_NS, 'li');
251     $li_el->inner_html (q[<a></a>]);
252     my $a_el = $li_el->first_child;
253     my $id_prop = $id_prop_db->get_data ($id);
254     $a_el->text_content
255     (length $id_prop->{title} ? $id_prop->{title}
256     : [keys %{$id_prop->{name}}]->[0] // $id); ## TODO: title-type
257     $a_el->set_attribute (href => get_page_url ($name, $name, $id));
258     $ul_el->append_child ($li_el);
259     }
260     $nav_el->append_child ($ul_el);
261     $body_el->append_child ($nav_el);
262     }
263    
264     my $nav_el = $html_doc->create_element_ns (HTML_NS, 'div');
265 wakaba 1.4 $nav_el->set_attribute (class => 'nav tools');
266 wakaba 1.1 $nav_el->inner_html (q[<a rel=edit>Edit</a> <a href="../new-page">New</a>]);
267     if (defined $id) {
268     $nav_el->first_child->set_attribute (href => '../i/' . $id . ';edit');
269 wakaba 1.50 $body_el->set_attribute ('data-doc-id' => $id);
270 wakaba 1.1 } else {
271     $nav_el->first_child->set_attribute
272     (href => '../new-page?names=' . percent_encode ($name));
273     }
274     $body_el->append_child ($nav_el);
275    
276     if ($html_container) {
277     my $article_el = $html_doc->create_element_ns (HTML_NS, 'div');
278     $article_el->set_attribute (class => 'article');
279    
280     my $h2_el = $html_doc->create_element_ns (HTML_NS, 'h2');
281     $h2_el->text_content (length $title_text ? $title_text : $name);
282     ## TODO: {'title-type'};
283     $article_el->append_child ($h2_el);
284    
285     while (@{$html_container->child_nodes}) {
286     $article_el->append_child ($html_container->first_child);
287     }
288 wakaba 1.3
289     my $modified = $id_prop->{modified};
290     if (defined $modified) {
291     my $footer_el = $html_doc->create_element_ns (HTML_NS, 'div');
292     $footer_el->set_attribute (class => 'footer swe-updated');
293     $footer_el->inner_html ('Updated: <time></time>');
294     $footer_el->last_child->text_content
295     (datetime_in_content ($modified));
296     $article_el->append_child ($footer_el);
297     }
298 wakaba 1.1
299     $body_el->append_child ($article_el);
300     }
301    
302 wakaba 1.43 my $ad_el = $html_doc->create_element_ns (HTML_NS, 'aside');
303     $ad_el->set_attribute (class => 'swe-ad');
304     $ad_el->inner_html (q[
305     <script>
306     google_ad_client = "pub-6943204637055835";
307     google_ad_slot = "4290129344";
308     google_ad_width = 728;
309     google_ad_height = 90;
310     </script>
311     <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
312     </script>
313     ]);
314     $body_el->append_child ($ad_el);
315    
316 wakaba 1.1 my $footer_el = $html_doc->create_element_ns (HTML_NS, 'footer');
317     $footer_el->set_attribute (class => 'footer');
318     $footer_el->inner_html (q[<p class=copyright><small>&copy; Authors. See <a rel=license>license terms</a>. There might also be additional terms applied for this page.</small>]);
319     $body_el->append_child ($footer_el);
320    
321     my $a_el = $footer_el->get_elements_by_tag_name ('a')->[0];
322     our $license_name;
323     $a_el->set_attribute (href => get_page_url ($license_name));
324 wakaba 1.18
325     set_foot_content ($html_doc);
326 wakaba 1.1
327     binmode STDOUT, ':encoding(utf-8)';
328     print qq[Content-Type: text/html; charset=utf-8\n\n];
329     print $html_doc->inner_html;
330     exit;
331     }
332    
333     exit;
334 wakaba 1.2 } elsif ($param eq 'history' and not defined $dollar) {
335 wakaba 1.27 my $name_history_db = $db->name_history;
336 wakaba 1.2 my $history = $name_history_db->get_data ($name);
337    
338     binmode STDOUT, ':encoding(utf-8)';
339     print "Content-Type: text/html; charset=utf-8\n\n";
340    
341     my $doc = $dom->create_document;
342     $doc->manakai_is_html (1);
343     $doc->inner_html (q[<!DOCTYPE HTML><html lang=en><title></title><h1></h1>
344     <div class=section><h2>History</h2><table>
345    
346     <thead>
347     <tr><th scope=col>Time<th scope=col>Change
348    
349     <tbody>
350    
351     </table></div>]);
352     set_head_content ($doc, undef, [], []);
353    
354     my $title_el = $doc->get_elements_by_tag_name ('title')->[0];
355     $title_el->inner_html ('History &mdash; ');
356     $title_el->manakai_append_text ($name);
357    
358     my $h1_el = $doc->get_elements_by_tag_name ('h1')->[0];
359     $h1_el->text_content ($name);
360    
361     my $table_el = $doc->get_elements_by_tag_name ('table')->[0];
362     if ($history) {
363     my $tbody_el = $table_el->last_child;
364    
365     for my $entry (@$history) {
366     my $tr_el = $doc->create_element_ns (HTML_NS, 'tr');
367    
368     my $date_cell = $doc->create_element_ns (HTML_NS, 'td');
369     my $date = gmtime ($entry->[0] || 0); ## TODO: ...
370     $date_cell->inner_html ('<time>' . $date . '</time>');
371     $tr_el->append_child ($date_cell);
372    
373     my $change_cell = $doc->create_element_ns (HTML_NS, 'td');
374     if ($entry->[1] eq 'c') {
375     $change_cell->manakai_append_text ('Created');
376     } elsif ($entry->[1] eq 'a') {
377     $change_cell->manakai_append_text ('Associated with ');
378     my $a_el = $doc->create_element_ns (HTML_NS, 'a');
379     $a_el->set_attribute (href => '../i/' . $entry->[2] . ';history');
380     $a_el->text_content ($entry->[2]);
381     $change_cell->append_child ($a_el);
382     } elsif ($entry->[1] eq 'r') {
383     $change_cell->manakai_append_text ('Disassociated from ');
384     my $a_el = $doc->create_element_ns (HTML_NS, 'a');
385     $a_el->set_attribute (href => '../i/' . $entry->[2] . ';history');
386     $a_el->text_content ($entry->[2]);
387     $change_cell->append_child ($a_el);
388     } elsif ($entry->[1] eq 't') {
389     $change_cell->manakai_append_text
390     ('Converted from SuikaWiki3 database');
391     } else {
392     $change_cell->manakai_append_text ($entry->[1]);
393     }
394     $tr_el->append_child ($change_cell);
395    
396     $tbody_el->append_child ($tr_el);
397     }
398     } else {
399     my $p_el = $doc->create_element_ns (HTML_NS, 'p');
400     $p_el->text_content ('No history data.');
401     $table_el->parent_node->replace_child ($p_el, $table_el);
402     }
403    
404 wakaba 1.18 set_foot_content ($doc);
405    
406 wakaba 1.2 print $doc->inner_html;
407     exit;
408 wakaba 1.5 } elsif ($param eq 'search' and not defined $dollar) {
409     my $names = [];
410     for_unique_words ($name => sub {
411     push @$names, shift;
412     });
413    
414 wakaba 1.7 my $names_index_db = $db->name_inverted_index;
415 wakaba 1.5 my $index = {};
416     {
417     my $name = shift @$names;
418     last unless defined $name;
419     $index = $names_index_db->get_data ($name);
420     }
421    
422     ## TOOD: "NOT" operation
423    
424     for my $name (@$names) {
425     my $ids = $names_index_db->get_data ($name);
426     my @index = keys %$index;
427     for my $id (@index) {
428     delete $index->{$id} unless $ids->{$id};
429     }
430     for my $id (keys %$ids) {
431     if (defined $index->{$id}) {
432     $index->{$id} *= $ids->{$id};
433     }
434     }
435     }
436    
437     binmode STDOUT, ':encoding(utf-8)';
438     print "Content-Type: text/plain; charset=utf-8\n\n";
439    
440     for my $id (sort {$index->{$b} <=> $index->{$a}} keys %$index) {
441    
442     my $id_prop = $id_prop_db->get_data ($id);
443     my $name = [keys %{$id_prop->{name}}]->[0] // $id;
444    
445     print $index->{$id}, "\t", $id, "\t", $name, "\n";
446     }
447     exit;
448 wakaba 1.24 } elsif ($param eq 'posturl') {
449     my ($id, undef) = prepare_by_name ($name, $dollar);
450    
451     if (defined $dollar and not defined $id) {
452     http_error (404, 'Not found');
453     }
454    
455 wakaba 1.32 if ($cgi->request_method eq 'POST') {
456 wakaba 1.31 my $user = '(anon)'; #$cgi->remote_user // '(anon)';
457 wakaba 1.35 my $added_text = '<' . ($cgi->get_parameter ('url') // '') . '>';
458     {
459     my $credit = $cgi->get_parameter ('credit') // '';
460     $added_text = '(' . $credit . ")\n" . $added_text if length $credit;
461    
462     my $timestamp = $cgi->get_parameter ('timestamp') // '';
463     if (length $timestamp) {
464     $added_text = '(Referenced: [TIME[' . $timestamp . "]])\n". $added_text;
465     }
466    
467     my $title = $cgi->get_parameter ('title') // '';
468     if (length $title) {
469     $title =~ s/(\[|\])/'''$1'''/g;
470     my $tl = $cgi->get_parameter ('title-lang') // '';
471     if (length $tl) {
472     $title = '[CITE@' . $tl . '[' . $title . ']]';
473     } else {
474     $title = '[CITE[' . $title . ']]';
475     }
476     $added_text = $title . "\n" . $added_text;
477     }
478     }
479 wakaba 1.25 normalize_content (\$added_text);
480    
481 wakaba 1.35 my $anchor = 1;
482 wakaba 1.25 APPEND: { if (defined $id) { ## Existing document
483     ## This must be done before the ID lock.
484     $db->name_inverted_index->lock;
485 wakaba 1.24
486 wakaba 1.25 my $id_lock = $id_locks->get_lock ($id);
487     $id_lock->lock;
488    
489     my $id_prop = $id_prop_db->get_data ($id);
490     last APPEND unless $id_prop;
491     last APPEND if $id_prop->{'content-type'} ne 'text/x-suikawiki';
492    
493     my $textref = $content_db->get_data ($id);
494 wakaba 1.34 my $max = 0;
495     while ($$textref =~ /\[([0-9]+)\]/g) {
496     $max = $1 if $max < $1;
497     }
498     $max++;
499     $$textref .= "\n\n[$max] " . $added_text;
500 wakaba 1.35 $anchor = $max;
501 wakaba 1.25
502     $id_prop->{modified} = time;
503     $id_prop->{hash} = get_hash ($textref);
504    
505 wakaba 1.26 my $vc = $db->vc;
506 wakaba 1.25 local $content_db->{version_control} = $vc;
507     local $id_prop_db->{version_control} = $vc;
508    
509     $content_db->set_data ($id => $textref);
510     $id_prop_db->set_data ($id => $id_prop);
511    
512     $vc->commit_changes ("updated by $user");
513    
514     ## TODO: non-default content-type support
515     my $cache_prop = $cache_prop_db->get_data ($id);
516     my $doc = $id_prop ? get_xml_data ($id, $id_prop, $cache_prop) : undef;
517    
518     if (defined $doc) {
519     update_tfidf ($id, $doc);
520     }
521    
522 wakaba 1.35 if ($cgi->get_parameter ('redirect')) {
523     http_redirect (303, 'Appended', get_page_url ($name, undef, $id) . '#anchor-' . $anchor);
524     } else {
525     print qq[Status: 204 Appended\n\n];
526     }
527 wakaba 1.25 exit;
528     }} # APPEND
529    
530     { ## New document
531 wakaba 1.31 my $new_names = {$name => 1};
532     my $content = '[1] ' . $added_text;
533 wakaba 1.24
534 wakaba 1.31 $names_lock->lock;
535     my $time = time;
536    
537     require SWE::Object::Document;
538     my $document = SWE::Object::Document->new_id (db => $db);
539     $document->{name_prop_db} = $name_prop_db; ## TODO: ...
540     $document->{sw3_pages} = $sw3_pages; ## TODO: ...
541    
542     my $id = $document->id;
543    
544     {
545     ## This must be done before the ID lock.
546     $db->name_inverted_index->lock;
547    
548     my $id_lock = $id_locks->get_lock ($id);
549     $id_lock->lock;
550    
551     my $vc = $db->vc;
552     local $content_db->{version_control} = $vc;
553     local $id_prop_db->{version_control} = $vc;
554     $vc->add_file ($db->id->{file_name});
555    
556     my $id_history_db = $db->id_history;
557     local $id_history_db->{version_control} = $vc;
558    
559     $content_db->set_data ($id => \$content);
560    
561     my $id_props = {};
562    
563     $id_history_db->append_data ($id => [$time, 'c']);
564     $id_props->{modified} = $time;
565    
566     for (keys %$new_names) {
567     $id_props->{name}->{$_} = 1;
568     $id_history_db->append_data ($id => [$time, 'a', $_]);
569     }
570    
571     $id_props->{'content-type'} = 'text/x-suikawiki';
572     $id_props->{hash} = get_hash (\$content);
573     $id_prop_db->set_data ($id => $id_props);
574    
575     $vc->commit_changes ("created by $user");
576    
577     ## TODO: non-default content-type support
578     my $cache_prop = $cache_prop_db->get_data ($id);
579     my $doc = $id_props ? get_xml_data ($id, $id_props, $cache_prop) : undef;
580    
581     if (defined $doc) {
582     $document->update_tfidf ($doc);
583     }
584    
585     $id_lock->unlock;
586     }
587    
588     $document->associate_names ($new_names, user => $user, time => $time);
589    
590 wakaba 1.35 if ($cgi->get_parameter ('redirect')) {
591     http_redirect (303, 'Appended', get_page_url ($name, undef, $id) . '#anchor-' . $anchor);
592 wakaba 1.56 exit;
593 wakaba 1.35 } else {
594     print qq[Status: 204 Appended\n\n];
595     }
596 wakaba 1.31 exit;
597 wakaba 1.24 }
598     } else {
599     http_error (405, 'Method not allowed', 'POST');
600     }
601 wakaba 1.1 } else {
602     $name .= '$' . $dollar if defined $dollar;
603     $name .= ';' . $param;
604     http_redirect (301, 'Not found', get_page_url ($name, undef));
605 wakaba 1.56 exit;
606 wakaba 1.1 }
607     } elsif ($path[0] eq 'i' and @path == 2 and not defined $dollar) {
608     unless (defined $param) {
609     if ($cgi->request_method eq 'POST' or
610     $cgi->request_method eq 'PUT') {
611     my $id = $path[1] + 0;
612    
613 wakaba 1.21 ## This must be done before the ID lock.
614     $db->name_inverted_index->lock;
615    
616 wakaba 1.1 my $id_lock = $id_locks->get_lock ($id);
617     $id_lock->lock;
618    
619     my $id_prop = $id_prop_db->get_data ($id);
620     if ($id_prop) {
621     my $ct = get_content_type_parameter ();
622    
623     my $prev_hash = $cgi->get_parameter ('hash') // '';
624     my $current_hash = $id_prop->{hash} //
625     get_hash ($content_db->get_data ($id) // '');
626     unless ($prev_hash eq $current_hash) {
627     ## TODO: conflict
628     exit;
629     }
630    
631     my $textref = \ ($cgi->get_parameter ('text') // '');
632     normalize_content ($textref);
633    
634     $id_prop->{'content-type'} = $ct;
635     $id_prop->{modified} = time;
636     $id_prop->{hash} = get_hash ($textref);
637    
638 wakaba 1.41 my $title = $cgi->get_parameter ('title');
639     if (defined $title) {
640     normalize_content (\$title);
641     $id_prop->{title} = $title;
642     $id_prop->{'title-type'} = 'text/plain'; ## TODO: get_parameter
643     }
644 wakaba 1.1
645 wakaba 1.26 my $vc = $db->vc;
646 wakaba 1.1 local $content_db->{version_control} = $vc;
647     local $id_prop_db->{version_control} = $vc;
648    
649     $content_db->set_data ($id => $textref);
650     $id_prop_db->set_data ($id => $id_prop);
651    
652     my $user = '(anon)'; #$cgi->remote_user // '(anon)';
653     $vc->commit_changes ("updated by $user");
654    
655 wakaba 1.21 ## TODO: non-default content-type support
656     my $cache_prop = $cache_prop_db->get_data ($id);
657     my $doc = $id_prop ? get_xml_data ($id, $id_prop, $cache_prop) : undef;
658    
659 wakaba 1.1 my $url = get_page_url ([keys %{$id_prop->{name} or {}}]->[0],
660     undef, 0 + $id);
661 wakaba 1.41 print "X-SW-Hash: $id_prop->{hash}\n";
662     if ($cgi->get_parameter ('no-redirect')) {
663     http_redirect (201, 'Saved', $url);
664     } else {
665     http_redirect (301, 'Saved', $url);
666     }
667 wakaba 1.57
668     if (defined $doc) {
669     update_tfidf ($id, $doc);
670     }
671    
672 wakaba 1.56 exit;
673 wakaba 1.1 } else {
674     http_error (404, 'Not found');
675     }
676     } else {
677     http_error (405, 'Method not allowed', 'PUT');
678     }
679     } elsif ($param eq 'edit') {
680     my $id = $path[1] + 0;
681    
682     my $textref = $content_db->get_data ($id);
683     if (defined $textref) {
684     binmode STDOUT, ':encoding(utf-8)';
685     print qq[Content-Type: text/html; charset=utf-8\n\n];
686 wakaba 1.41
687     require SWE::Object::Document;
688     my $doc = SWE::Object::Document->new (db => $db, id => $id);
689    
690     my $id_prop = $id_prop_db->get_data ($id);
691     my $names = $id_prop->{name} || {};
692     my $hash = $id_prop->{hash} // get_hash ($textref);
693    
694     my $custom_edit = '';
695     if ($doc->content_media_type eq 'image/x-canvas-instructions+text') {
696     my $request_uri = $cgi->request_uri;
697     my $source_url = get_absolute_url (get_page_url ([keys %$names]->[0], undef, $id) . '?format=text', $request_uri);
698     my $post_url = get_absolute_url ($id, $request_uri);
699     my $url = 'http://suika.fam.cx/swe/pages/canvas?input-url=' . percent_encode ($source_url) . ';post-url=' . percent_encode($post_url);
700     $custom_edit = q[<a href="].$url.q[">Edit image</a>];
701    
702     $post_url = get_absolute_url ('../new-page', $request_uri);
703     $url = 'http://suika.fam.cx/swe/pages/canvas?input-url=' . percent_encode ($source_url) . ';post-url=' . percent_encode($post_url);
704     $custom_edit .= q[ <a href="].$url.q[">Clone image</a>];
705     }
706 wakaba 1.1
707     ## TODO: <select name=title-type>
708     my $html_doc = $dom->create_document;
709     $html_doc->manakai_is_html (1);
710     $html_doc->inner_html (q[<!DOCTYPE HTML><title>Edit</title>
711     <h1>Edit</h1>
712     <div class="nav swe-names"></div>
713    
714     <div class=section>
715     <h2>Page</h2>
716    
717 wakaba 1.41 ].$custom_edit.q[
718    
719 wakaba 1.1 <form method=post accept-charset=utf-8>
720     <p><button type=submit>Update</button>
721     <p><label><strong>Page title</strong>:<br>
722     <input name=title></label>
723     <p><label for=page-body-text><strong>Page body</strong></label>:
724     <span class=text-toolbar></span><br>
725     <textarea name=text id=page-body-text></textarea>
726     <p><button type=submit>Update</button>
727     <input type=hidden name=hash>
728     <select name=content-type></select>
729 wakaba 1.3 [<a rel=help>Help</a> / <a rel=license>License</a>]
730 wakaba 1.1 </form>
731    
732     </div>
733    
734     <div class=section>
735     <h2>Page name(s)</h2>
736    
737     <form method=post accept-charset=utf-8>
738     <p><textarea name=names></textarea>
739     <p><button type=submit>Save</button>
740     </form>
741     </div>
742     ]);
743     set_head_content ($html_doc, $id, [],
744     [{name => 'ROBOTS', content => 'NOINDEX'}]);
745     my $form_el = $html_doc->get_elements_by_tag_name ('form')->[0];
746     $form_el->set_attribute (action => $id);
747     my $ta_el = $form_el->get_elements_by_tag_name ('textarea')->[0];
748     $ta_el->text_content ($$textref);
749    
750     my $title_field = $form_el->get_elements_by_tag_name ('input')->[0];
751     $title_field->set_attribute (value => $id_prop->{title} // '');
752    
753     my $hash_field = $form_el->get_elements_by_tag_name ('input')->[1];
754     $hash_field->set_attribute (value => $hash);
755    
756     my $ct = $id_prop->{'content-type'} // 'text/x-suikawiki';
757     set_content_type_options
758     ($html_doc,
759     $form_el->get_elements_by_tag_name ('select')->[0] => $ct);
760    
761     my $a_el = $form_el->get_elements_by_tag_name ('a')->[0];
762 wakaba 1.3 our $help_page_name;
763     $a_el->set_attribute (href => get_page_url ($help_page_name));
764    
765     $a_el = $form_el->get_elements_by_tag_name ('a')->[1];
766 wakaba 1.1 our $license_name;
767     $a_el->set_attribute (href => get_page_url ($license_name));
768    
769     $form_el = $html_doc->get_elements_by_tag_name ('form')->[1];
770     $form_el->set_attribute (action => $id . ';names');
771    
772     $ta_el = $form_el->get_elements_by_tag_name ('textarea')->[0];
773     $ta_el->text_content (join "\x0A", keys %$names);
774    
775     my $nav_el = $html_doc->get_elements_by_tag_name ('div')->[0];
776     for (keys %$names) {
777     my $a_el = $html_doc->create_element_ns (HTML_NS, 'a');
778     $a_el->set_attribute (href => get_page_url ($_, undef, $id));
779     $a_el->text_content ($_);
780     $nav_el->append_child ($a_el);
781     $nav_el->manakai_append_text (' ');
782     }
783 wakaba 1.18
784     set_foot_content ($html_doc);
785 wakaba 1.1
786     print $html_doc->inner_html;
787     exit;
788     }
789     } elsif ($param eq 'names') {
790     if ($cgi->request_method eq 'POST' or
791     $cgi->request_method eq 'PUT') {
792     my $id = $path[1] + 0;
793    
794 wakaba 1.26 my $vc = $db->vc;
795 wakaba 1.1 local $name_prop_db->{version_control} = $vc;
796     local $id_prop_db->{version_control} = $vc;
797    
798 wakaba 1.27 my $id_history_db = $db->id_history;
799     local $id_history_db->{version_control} = $vc;
800    
801     my $names_history_db = $db->name_history;
802     local $names_history_db->{version_control} = $vc;
803 wakaba 1.2
804     my $time = time;
805    
806 wakaba 1.1 $names_lock->lock;
807    
808     my $id_prop = $id_prop_db->get_data ($id);
809     my $old_names = $id_prop->{name} || {};
810    
811     my $new_names = {};
812     for (split /\x0D\x0A?|\x0A/, $cgi->get_parameter ('names')) {
813     $new_names->{normalize_name ($_)} = 1;
814     }
815     delete $new_names->{''};
816     $new_names->{'(no title)'} = 1 unless keys %$new_names;
817    
818     my $added_names = {};
819     my $removed_names = {%$old_names};
820     for (keys %$new_names) {
821     if ($old_names->{$_}) {
822     delete $removed_names->{$_};
823     } else {
824     $added_names->{$_} = 1;
825     }
826     }
827    
828     for my $new_name (keys %$added_names) {
829     my $new_name_prop = $name_prop_db->get_data ($new_name);
830     unless (defined $new_name_prop) {
831     my $sw3id = $sw3_pages->get_data ($new_name);
832     convert_sw3_page ($sw3id => $new_name);
833     $new_name_prop = $name_prop_db->get_data ($new_name);
834 wakaba 1.2 unless (defined $new_name_prop) {
835     $names_history_db->append_data ($new_name => [$time, 'c']);
836     }
837 wakaba 1.1 }
838     $new_name_prop->{name} = $new_name;
839     push @{$new_name_prop->{id} ||= []}, $id;
840     $name_prop_db->set_data ($new_name => $new_name_prop);
841 wakaba 1.2
842     $id_history_db->append_data ($id => [$time, 'a', $new_name]);
843     $names_history_db->append_data ($new_name => [$time, 'a', $id]);
844 wakaba 1.1 }
845    
846     for my $removed_name (keys %$removed_names) {
847     my $removed_name_prop = $name_prop_db->get_data ($removed_name);
848     for (0..$#{$removed_name_prop->{id} or []}) {
849     if ($removed_name_prop->{id}->[$_] eq $id) {
850     splice @{$removed_name_prop->{id}}, $_, 1, ();
851     last;
852     }
853     }
854     $name_prop_db->set_data ($removed_name => $removed_name_prop);
855 wakaba 1.2
856     $id_history_db->append_data ($id => [$time, 'r', $removed_name]);
857     $names_history_db->append_data ($removed_name => [$time, 'r', $id]);
858 wakaba 1.1 }
859    
860     $id_prop->{name} = $new_names;
861     $id_prop_db->set_data ($id => $id_prop);
862    
863     my $user = $cgi->remote_user // '(anon)';
864     $vc->commit_changes ("id-name association changed by $user");
865    
866     $names_lock->unlock;
867    
868     print "Status: 204 Changed\n\n";
869    
870     exit;
871     } else {
872     http_error (405, 'Method not allowed', 'PUT');
873     }
874 wakaba 1.50 } elsif ($param eq 'neighbors') {
875     require SWE::Object::Repository;
876     my $repo = SWE::Object::Repository->new (db => $db);
877 wakaba 1.52
878 wakaba 1.50 my $graph = $repo->graph;
879     $graph->lock;
880    
881     my $id = $path[1] + 0;
882     my $doc = $repo->get_document_by_id ($id);
883 wakaba 1.52 $doc->lock;
884    
885 wakaba 1.50 my $node = $doc->get_or_create_graph_node;
886    
887     if ($node) {
888     binmode STDOUT, ':encoding(utf-8)';
889     print "Content-Type: text/plain; charset=UTF-8\n\n";
890    
891     for my $doc (@{$node->neighbor_documents}) {
892 wakaba 1.53 print join "\t", $doc->id, $doc->name;
893 wakaba 1.50 print "\n";
894     }
895    
896     close STDOUT;
897    
898     $graph->schelling_update ($node->id);
899 wakaba 1.52
900     $doc->unlock;
901 wakaba 1.50 $graph->unlock;
902    
903     exit;
904     } else {
905 wakaba 1.52 $doc->unlock;
906 wakaba 1.50 $graph->unlock;
907     #
908     }
909 wakaba 1.2 } elsif ($param eq 'history' and not defined $dollar) {
910     my $id = $path[1] + 0;
911    
912 wakaba 1.27 my $id_history_db = $db->id_history;
913 wakaba 1.2 my $history = $id_history_db->get_data ($id);
914    
915     binmode STDOUT, ':encoding(utf-8)';
916     print "Content-Type: text/html; charset=utf-8\n\n";
917    
918     my $doc = $dom->create_document;
919     $doc->manakai_is_html (1);
920     $doc->inner_html (q[<!DOCTYPE HTML><html lang=en><title></title><h1></h1>
921     <div class=section><h2>History</h2><table>
922    
923     <thead>
924     <tr><th scope=col>Time<th scope=col>Change
925    
926     <tbody>
927    
928     </table></div>]);
929     set_head_content ($doc, undef, [], []);
930    
931     my $title_el = $doc->get_elements_by_tag_name ('title')->[0];
932     $title_el->inner_html ('History &mdash; #');
933     $title_el->manakai_append_text ($id);
934    
935     my $h1_el = $doc->get_elements_by_tag_name ('h1')->[0];
936     $h1_el->text_content ('#' . $id);
937    
938     my $table_el = $doc->get_elements_by_tag_name ('table')->[0];
939     if ($history) {
940     my $tbody_el = $table_el->last_child;
941    
942     for my $entry (@$history) {
943     my $tr_el = $doc->create_element_ns (HTML_NS, 'tr');
944    
945     my $date_cell = $doc->create_element_ns (HTML_NS, 'td');
946     my $date = gmtime ($entry->[0] || 0); ## TODO: ...
947     $date_cell->inner_html ('<time>' . $date . '</time>');
948     $tr_el->append_child ($date_cell);
949    
950     my $change_cell = $doc->create_element_ns (HTML_NS, 'td');
951     if ($entry->[1] eq 'c') {
952     $change_cell->manakai_append_text ('Created');
953     } elsif ($entry->[1] eq 'a') {
954     $change_cell->manakai_append_text ('Associated with ');
955     my $a_el = $doc->create_element_ns (HTML_NS, 'a');
956     $a_el->set_attribute (href => get_page_url ($entry->[2], undef)
957     . ';history');
958     $a_el->text_content ($entry->[2]);
959     $change_cell->append_child ($a_el);
960     } elsif ($entry->[1] eq 'r') {
961     $change_cell->manakai_append_text ('Disassociated from ');
962     my $a_el = $doc->create_element_ns (HTML_NS, 'a');
963     $a_el->set_attribute (href => get_page_url ($entry->[2], undef)
964     . ';history');
965     $a_el->text_content ($entry->[2]);
966     $change_cell->append_child ($a_el);
967     } elsif ($entry->[1] eq 't') {
968     $change_cell->manakai_append_text
969     ('Converted from SuikaWiki3 database');
970     } else {
971     $change_cell->manakai_append_text ($entry->[1]);
972     }
973     $tr_el->append_child ($change_cell);
974    
975     $tbody_el->append_child ($tr_el);
976     }
977     } else {
978     my $p_el = $doc->create_element_ns (HTML_NS, 'p');
979     $p_el->text_content ('No history data.');
980     $table_el->parent_node->replace_child ($p_el, $table_el);
981     }
982    
983 wakaba 1.18 set_foot_content ($doc);
984    
985 wakaba 1.2 print $doc->inner_html;
986     exit;
987 wakaba 1.5 } elsif ($param eq 'terms' and not defined $dollar) {
988     my $id = $path[1] + 0;
989 wakaba 1.20
990     ## This must be done before the ID lock.
991     $db->name_inverted_index->lock;
992 wakaba 1.5
993     my $id_lock = $id_locks->get_lock ($id);
994     $id_lock->lock;
995    
996     my $id_prop = $id_prop_db->get_data ($id);
997     my $cache_prop = $cache_prop_db->get_data ($id);
998     my $doc = $id_prop ? get_xml_data ($id, $id_prop, $cache_prop) : undef;
999    
1000     if (defined $doc) {
1001     update_tfidf ($id, $doc);
1002 wakaba 1.20
1003     $id_lock->unlock;
1004 wakaba 1.27
1005 wakaba 1.5 binmode STDOUT, ':encoding(utf-8)';
1006     print "Content-Type: text/plain; charset=utf-8\n\n";
1007 wakaba 1.27
1008 wakaba 1.7 print ${ $db->id_tfidf->get_data ($id) };
1009 wakaba 1.5
1010     exit;
1011 wakaba 1.20 } else {
1012     $id_lock->unlock;
1013 wakaba 1.5 }
1014 wakaba 1.8 } elsif ($param =~ /^(un)?related-([0-9]+)$/ and not defined $dollar) {
1015 wakaba 1.51 ## Allow GET to not require Basic auth.
1016     #if ($cgi->request_method eq 'POST') {
1017 wakaba 1.46 my $id1 = $path[1] + 0;
1018     my $id2 = $2 + 0;
1019     my $answer = $1 ? -1 : 1;
1020    
1021     require SWE::Object::Repository;
1022     my $repo = SWE::Object::Repository->new (db => $db);
1023 wakaba 1.49
1024     my $vc = $db->vc;
1025 wakaba 1.46
1026 wakaba 1.49 $repo->weight_lock;
1027     local $db->global_prop->{version_control} = $vc;
1028 wakaba 1.46 my $y = $repo->are_related_ids ($id1, $id2, $answer);
1029     $repo->save_term_weight_vector;
1030 wakaba 1.49 $vc->commit_changes ('term weight vector update');
1031     $repo->weight_unlock;
1032 wakaba 1.46
1033     print "Content-Type: text/ping\n\n";
1034     print "PING";
1035     exit;
1036 wakaba 1.51 #} else {
1037     # http_error (405, 'Method not allowed', 'POST');
1038     #}
1039 wakaba 1.1 }
1040     } elsif ($path[0] eq 'new-page' and @path == 1) {
1041     if ($cgi->request_method eq 'POST') {
1042     my $new_names = {};
1043     for (split /\x0D\x0A?|\x0A/, scalar $cgi->get_parameter ('names')) {
1044     $new_names->{normalize_name ($_)} = 1;
1045     }
1046     delete $new_names->{''};
1047     $new_names->{'(no title)'} = 1 unless keys %$new_names;
1048    
1049     my $user = '(anon)'; #$cgi->remote_user // '(anon)';
1050     my $ct = get_content_type_parameter ();
1051    
1052     my $content = $cgi->get_parameter ('text') // '';
1053     normalize_content (\$content);
1054    
1055     $names_lock->lock;
1056 wakaba 1.2 my $time = time;
1057 wakaba 1.1
1058 wakaba 1.29 require SWE::Object::Document;
1059 wakaba 1.30 my $document = SWE::Object::Document->new_id (db => $db);
1060 wakaba 1.29 $document->{name_prop_db} = $name_prop_db; ## TODO: ...
1061     $document->{sw3_pages} = $sw3_pages; ## TODO: ...
1062    
1063 wakaba 1.30 my $id = $document->id;
1064 wakaba 1.41 my $id_prop = {};
1065 wakaba 1.30
1066 wakaba 1.1 {
1067 wakaba 1.22 ## This must be done before the ID lock.
1068     $db->name_inverted_index->lock;
1069    
1070 wakaba 1.1 my $id_lock = $id_locks->get_lock ($id);
1071     $id_lock->lock;
1072 wakaba 1.26
1073     my $vc = $db->vc;
1074 wakaba 1.1 local $content_db->{version_control} = $vc;
1075     local $id_prop_db->{version_control} = $vc;
1076 wakaba 1.30 $vc->add_file ($db->id->{file_name});
1077 wakaba 1.1
1078 wakaba 1.27 my $id_history_db = $db->id_history;
1079     local $id_history_db->{version_control} = $vc;
1080 wakaba 1.2
1081 wakaba 1.1 $content_db->set_data ($id => \$content);
1082 wakaba 1.2
1083     $id_history_db->append_data ($id => [$time, 'c']);
1084 wakaba 1.41 $id_prop->{modified} = $time;
1085 wakaba 1.2
1086     for (keys %$new_names) {
1087 wakaba 1.41 $id_prop->{name}->{$_} = 1;
1088 wakaba 1.2 $id_history_db->append_data ($id => [$time, 'a', $_]);
1089     }
1090    
1091 wakaba 1.41 $id_prop->{'content-type'} = $ct;
1092     $id_prop->{hash} = get_hash (\$content);
1093     $id_prop->{title} = $cgi->get_parameter ('title') // '';
1094     normalize_content (\($id_prop->{title}));
1095     $id_prop->{'title-type'} = 'text/plain'; ## TODO: get_parameter
1096     $id_prop_db->set_data ($id => $id_prop);
1097 wakaba 1.1
1098     $vc->commit_changes ("created by $user");
1099    
1100 wakaba 1.22 ## TODO: non-default content-type support
1101     my $cache_prop = $cache_prop_db->get_data ($id);
1102 wakaba 1.41 my $doc = $id_prop ? get_xml_data ($id, $id_prop, $cache_prop) : undef;
1103 wakaba 1.22
1104     if (defined $doc) {
1105 wakaba 1.29 $document->update_tfidf ($doc);
1106 wakaba 1.22 }
1107    
1108 wakaba 1.1 $id_lock->unlock;
1109     }
1110    
1111 wakaba 1.28 $document->associate_names ($new_names, user => $user, time => $time);
1112 wakaba 1.1
1113 wakaba 1.41 print "X-SW-Hash: $id_prop->{hash}\n";
1114    
1115     my $post_url = get_absolute_url ("i/$id", $cgi->request_uri);
1116     print "X-SW-Post-URL: $post_url\n";
1117    
1118 wakaba 1.1 my $url = get_page_url ([keys %$new_names]->[0], undef, 0 + $id);
1119 wakaba 1.41 if ($cgi->get_parameter ('no-redirect')) {
1120     http_redirect (201, 'Created', $url);
1121     } else {
1122     http_redirect (301, 'Created', $url);
1123     }
1124 wakaba 1.56 exit;
1125 wakaba 1.1 } else {
1126     binmode STDOUT, ':encoding(utf8)';
1127     print "Content-Type: text/html; charset=utf-8\n\n";
1128    
1129 wakaba 1.41 my $custom_edit = '';
1130     my $request_uri = $cgi->request_uri;
1131     my $post_url = get_absolute_url ('new-page', $request_uri);
1132 wakaba 1.42 my $names = $cgi->get_parameter ('names') // '';
1133     my $url = 'http://suika.fam.cx/swe/pages/canvas?post-url=' . percent_encode($post_url) . ';names=' . percent_encode ($names);
1134 wakaba 1.41 $custom_edit = q[<a href="].$url.q[">Image</a>];
1135    
1136 wakaba 1.1 ## TODO: select name=title-type
1137     my $doc = $dom->create_document;
1138     $doc->manakai_is_html (1);
1139     $doc->inner_html (q[<!DOCTYPE HTML><title>New page</title>
1140     <h1>New page</h1>
1141 wakaba 1.41 ].$custom_edit.q[
1142 wakaba 1.1 <form action=new-page method=post accept-charset=utf-8>
1143     <p><button type=submit>Save</button>
1144     <p><label><strong>Page name(s)</strong>:<br>
1145     <textarea name=names></textarea></label>
1146     <p><label><strong>Page title</strong>:<br>
1147     <input name=title></label>
1148     <p><label for=page-body-text><strong>Page body</strong></label>:
1149     <span class=text-toolbar></span><br>
1150     <textarea name=text id=page-body-text></textarea>
1151     <p><button type=submit>Save</button>
1152     <select name=content-type></select>
1153 wakaba 1.3 [<a rel=help>Help</a> / <a rel=license>License</a>]
1154 wakaba 1.1 </form>
1155     ]);
1156     set_head_content ($doc, undef, [],
1157     [{name => 'ROBOTS', content => 'NOINDEX'}]);
1158    
1159     my $form_el = $doc->get_elements_by_tag_name ('form')->[0];
1160     set_content_type_options
1161     ($doc, $form_el->get_elements_by_tag_name ('select')->[0]);
1162    
1163     my $names = $cgi->get_parameter ('names') // '';
1164     $form_el->get_elements_by_tag_name ('textarea')->[0]
1165     ->text_content ($names);
1166    
1167     my $a_el = $form_el->get_elements_by_tag_name ('a')->[0];
1168 wakaba 1.3 our $help_page_name;
1169     $a_el->set_attribute (href => get_page_url ($help_page_name));
1170    
1171     $a_el = $form_el->get_elements_by_tag_name ('a')->[1];
1172 wakaba 1.1 our $license_name;
1173     $a_el->set_attribute (href => get_page_url ($license_name));
1174    
1175 wakaba 1.18 set_foot_content ($doc);
1176    
1177 wakaba 1.1 print $doc->inner_html;
1178     exit;
1179     }
1180     } elsif (@path == 1 and
1181     {'' => 1, 'n' => 1, 'i' => 1}->{$path[0]}) {
1182     our $homepage_name;
1183     http_redirect (303, 'See other', get_page_url ($homepage_name, undef));
1184 wakaba 1.56 exit;
1185 wakaba 1.1 } elsif (@path == 0) {
1186     my $rurl = $cgi->request_uri;
1187     $rurl =~ s!\.[^/]*$!!g;
1188     http_redirect (303, 'See other', $rurl . '/');
1189 wakaba 1.56 exit;
1190 wakaba 1.1 }
1191    
1192     http_error (404, 'Not found');
1193    
1194 wakaba 1.23 sub prepare_by_name ($$) {
1195     my ($name, $id_cand) = @_;
1196    
1197     my $ids = get_ids_by_name ($name);
1198     unless (ref $ids) {
1199     $names_lock->lock;
1200     $sw3_pages->reset;
1201    
1202     $ids = convert_sw3_page ($ids => $name);
1203     $names_lock->unlock;
1204     }
1205    
1206     my $id;
1207     if (defined $id_cand) {
1208     $id_cand += 0;
1209     for (0..$#$ids) {
1210     if ($ids->[$_] == $id_cand) {
1211     $id = $id_cand;
1212     splice @$ids, $_, 1, ();
1213     last;
1214     }
1215     }
1216     } else {
1217     $id = shift @$ids;
1218     }
1219    
1220     return ($id, $ids);
1221     } # prepare_by_name
1222    
1223 wakaba 1.1 sub get_content_type_parameter () {
1224     my $ct = $cgi->get_parameter ('content-type') // 'text/x-suikawiki';
1225    
1226     my $valid_ct;
1227 wakaba 1.38 for (@ContentMediaType) {
1228 wakaba 1.1 if ($_->{type} eq $ct) {
1229     $valid_ct = 1;
1230     last;
1231     }
1232     }
1233     unless ($valid_ct) {
1234     http_error (400, 'content-type parameter not allowed');
1235     ## TODO: 406?
1236     }
1237    
1238     return $ct;
1239     } # get_content_type_parameter
1240    
1241     sub set_content_type_options ($$;$) {
1242     my ($doc, $select_el, $ct) = @_;
1243     $ct //= 'text/x-suikawiki';
1244    
1245     my $has_ct;
1246 wakaba 1.38 for (@ContentMediaType) {
1247 wakaba 1.1 next unless defined $_->{label};
1248     my $option_el = $doc->create_element_ns (HTML_NS, 'option');
1249     $option_el->set_attribute (value => $_->{type});
1250     $option_el->text_content ($_->{label});
1251     if ($_->{type} eq $ct) {
1252     $option_el->set_attribute (selected => '');
1253     $has_ct = 1;
1254     }
1255     $select_el->append_child ($option_el);
1256     }
1257     unless ($has_ct) {
1258     my $option_el = $doc->create_element_ns (HTML_NS, 'option');
1259     $option_el->set_attribute (value => $ct);
1260     $option_el->text_content ($ct);
1261     $option_el->set_attribute (selected => '');
1262     $select_el->append_child ($option_el);
1263     }
1264     } # set_content_type_options
1265    
1266     sub http_error ($$;$) {
1267     my ($code, $text, $allowed) = @_;
1268     binmode STDOUT, ":encoding(utf-8)";
1269    
1270     our $style_url;
1271    
1272     print qq[Status: $code $text\n];
1273     print qq[Allow: $allowed\n] if defined $allowed;
1274     print qq[Content-Type: text/html; charset=utf-8\n\n];
1275     print qq[<!DOCTYPE HTML>
1276     <html lang=en><title>$code @{[htescape ($text)]}</title>
1277     <link rel=stylesheet href="@{[htescape ($style_url)]}">
1278     <h1>@{[htescape ($text)]}</h1>];
1279     exit;
1280     } # http_error
1281    
1282     sub http_redirect ($$$) {
1283     my ($code, $text, $url) = @_;
1284    
1285     my $abs_url = get_absolute_url ($url, $cgi->request_uri);
1286    
1287     binmode STDOUT, ':encoding(utf-8)';
1288     print qq[Status: $code $text
1289     Location: $abs_url
1290     Content-Type: text/html; charset=utf-8
1291    
1292     <!DOCTYPE HTML>
1293     <html lang=en>
1294     <title>$code @{[htescape ($text)]}</title>
1295     <h1>@{[htescape ($text)]}</h1>
1296     <p>See <a href="@{[htescape ($url)]}">other page</a>.];
1297     } # http_redirect
1298    
1299     sub normalize_name ($) {
1300     my $s = shift;
1301     Char::Normalize::FullwidthHalfwidth::normalize_width (\$s);
1302     $s =~ s/\s+/ /g;
1303     $s =~ s/^ //;
1304     $s =~ s/ $//;
1305     return $s;
1306     } # normalize_name
1307    
1308     sub normalize_content ($) {
1309     my $sref = shift;
1310     Char::Normalize::FullwidthHalfwidth::normalize_width ($sref);
1311     } # normalize_content
1312    
1313     ## A source anchor label in SWML -> URL
1314     sub get_page_url ($$;$) {
1315     my ($wiki_name, $base_name, $id) = @_;
1316     $wiki_name = percent_encode ($wiki_name);
1317     $wiki_name =~ s/%2F/+/g;
1318     if (defined $id) {
1319     $wiki_name .= '$' . (0 + $id);
1320     }
1321     $wiki_name = ('../' x (@path - 1)) . 'n/' . $wiki_name;
1322     return $wiki_name;
1323     } # get_page_url
1324    
1325     sub get_xml_data ($$$) {
1326     my ($id, $id_prop, $cache_prop) = @_;
1327    
1328     my $cached_hash = $cache_prop->{'cached-hash'};
1329    
1330     if ($cached_hash) {
1331     my $content_hash = $id_prop->{hash} || '';
1332    
1333     if ($cached_hash ne $content_hash) {
1334     undef $cached_hash;
1335     }
1336     }
1337    
1338     state $content_cache_db;
1339     unless (defined $content_cache_db) {
1340     require SWE::DB::IDDOM;
1341     $content_cache_db = SWE::DB::IDDOM->new;
1342     $content_cache_db->{root_directory_name} = $db_id_dir_name;
1343     $content_cache_db->{leaf_suffix} = '.domcache';
1344     }
1345    
1346     my $doc;
1347     if ($cached_hash) {
1348     $doc = $content_cache_db->get_data ($id);
1349     } else {
1350     my $textref = $content_db->get_data ($id);
1351     if ($textref) {
1352     require Whatpm::SWML::Parser;
1353     my $p = Whatpm::SWML::Parser->new;
1354    
1355     $doc = $dom->create_document;
1356     $p->parse_char_string ($$textref => $doc);
1357    
1358     $content_cache_db->set_data ($id => $doc);
1359    
1360     $cache_prop->{'cached-hash'} = get_hash ($textref);
1361     $cache_prop_db->set_data ($id => $cache_prop);
1362     } else {
1363     ## Content not found.
1364     $doc = $dom->create_document;
1365     }
1366     }
1367    
1368     return $doc;
1369     } # get_xml_data
1370    
1371     sub get_hash ($) {
1372     require Digest::MD5;
1373     return Digest::MD5::md5_hex (Encode::encode ('utf8', ${$_[0]}));
1374     } # get_hash
1375    
1376     sub get_ids_by_name ($) {
1377     my $name = shift;
1378    
1379     my $name_prop = $name_prop_db->get_data ($name);
1380    
1381     if ($name_prop->{id}) {
1382     return $name_prop->{id};
1383     } else {
1384     my $sw3id = $sw3_pages->get_data ($name);
1385    
1386     if (defined $sw3id) {
1387     return $sw3id; # not an arrayref
1388     } else {
1389     return [];
1390     }
1391     }
1392     } # get_ids_by_name
1393    
1394 wakaba 1.5 sub for_unique_words ($*) {
1395     #my ($string, $code) = @_;
1396    
1397     ## TODO: use mecab
1398    
1399     require Text::Kakasi;
1400     my $k = Text::Kakasi->new;
1401    
1402     ## TODO: support stop words
1403    
1404     my $all_terms = 0;
1405     my $terms = {};
1406     for my $term (split /\s+/, $k->set (qw/-iutf8 -outf8 -w/)->get ($_[0])) {
1407    
1408 wakaba 1.7 ## TODO: provide a way to save original representation
1409    
1410 wakaba 1.5 ## TODO: more normalization
1411     $term = lc $term;
1412    
1413     $terms->{$term}++;
1414     }
1415    
1416     for my $term (keys %$terms) {
1417     $_[1]->($term, $terms->{$term});
1418     }
1419     } # for_unique_words
1420    
1421 wakaba 1.29 ## TODO: This function is OBSOLETE!
1422 wakaba 1.5 sub update_tfidf ($$) {
1423     my ($id, $doc) = @_;
1424    
1425 wakaba 1.29 require SWE::Object::Document;
1426     my $document = SWE::Object::Document->new (db => $db, id => $id);
1427     $document->{name_prop_db} = $name_prop_db; ## TODO: ...
1428     $document->{sw3_pages} = $sw3_pages; ## TODO: ...
1429 wakaba 1.6
1430 wakaba 1.29 $document->update_tfidf ($doc);
1431 wakaba 1.5 } # update_tfidf
1432    
1433 wakaba 1.1 sub convert_sw3_page ($$) {
1434     my ($sw3key => $name) = @_;
1435    
1436     my $page_key = $sw3_pages->get_data ($name);
1437     ## NOTE: $page_key is undef if the page has been converted
1438     ## between the first (in get_ids_by_name) and the second (the
1439     ## line above) $sw3_pages->get_data calls.
1440    
1441     my $ids;
1442     if (defined $page_key) {
1443 wakaba 1.9 my $idgen = $db->id;
1444 wakaba 1.2 my $time = time;
1445 wakaba 1.1
1446     my $id = $idgen->get_next_id;
1447     my $id_lock = $id_locks->get_lock ($id);
1448     $id_lock->lock;
1449    
1450 wakaba 1.26 my $vc = $db->vc;
1451 wakaba 1.1 local $content_db->{version_control} = $vc;
1452     local $id_prop_db->{version_control} = $vc;
1453     $vc->add_file ($idgen->{file_name});
1454    
1455 wakaba 1.27 my $id_history_db = $db->id_history;
1456     local $id_history_db->{version_control} = $vc;
1457    
1458 wakaba 1.1 our $sw3_db_dir_name;
1459    
1460     state $sw3_content_db;
1461     unless (defined $sw3_content_db) {
1462     require SWE::DB::SuikaWiki3;
1463     $sw3_content_db = SWE::DB::SuikaWiki3->new;
1464     $sw3_content_db->{root_directory_name} = $sw3_db_dir_name;
1465     }
1466    
1467     state $sw3_prop_db;
1468     unless (defined $sw3_prop_db) {
1469     require SWE::DB::SuikaWiki3Props;
1470     $sw3_prop_db = SWE::DB::SuikaWiki3Props->new;
1471     $sw3_prop_db->{root_directory_name} = $sw3_db_dir_name;
1472     }
1473    
1474     state $sw3_lm_db;
1475     unless (defined $sw3_lm_db) {
1476     require SWE::DB::SuikaWiki3LastModified;
1477     $sw3_lm_db = SWE::DB::SuikaWiki3LastModified->new;
1478     $sw3_lm_db->{file_name} = $sw3_db_dir_name .
1479     'mt--6C6173745F6D6F646966696564.dat';
1480     }
1481    
1482     my $content = $sw3_content_db->get_data ($page_key);
1483     $content_db->set_data ($id => \$content);
1484    
1485     my $id_props = $sw3_prop_db->get_data ($page_key);
1486     my $lm = $sw3_lm_db->get_data ($name);
1487     $id_props->{name}->{$name} = 1;
1488     $id_props->{modified} = $lm if defined $lm;
1489     $id_props->{'converted-from-sw3'} = time;
1490     $id_props->{'sw3-key'} = $page_key;
1491     $id_props->{hash} = get_hash (\$content);
1492     $id_prop_db->set_data ($id => $id_props);
1493    
1494 wakaba 1.2 $id_history_db->append_data ($id => [$time, 't']);
1495     $id_history_db->append_data ($id => [$time, 'a', $name]);
1496    
1497 wakaba 1.1 $vc->commit_changes ("converted from SuikaWiki3 <http://suika.fam.cx/gate/cvs/suikawiki/wikidata/page/$page_key.txt>");
1498    
1499     $id_lock->unlock;
1500    
1501 wakaba 1.26 $vc = $db->vc;
1502 wakaba 1.1 local $name_prop_db->{version_control} = $vc;
1503     local $sw3_pages->{version_control} = $vc;
1504    
1505 wakaba 1.27 my $names_history_db = $db->name_history;
1506     local $names_history_db->{version_control} = $vc;
1507 wakaba 1.2
1508 wakaba 1.1 my $name_props = $name_prop_db->get_data ($name);
1509     push @{$name_props->{id} ||= []}, $id;
1510     $ids = $name_props->{id};
1511     $name_props->{name} = $name;
1512     $name_prop_db->set_data ($name => $name_props);
1513    
1514 wakaba 1.2 $names_history_db->append_data ($name => [$time, 't']);
1515     $names_history_db->append_data ($name => [$time, 'a', $id]);
1516    
1517 wakaba 1.1 $sw3_pages->delete_data ($name);
1518     $sw3_pages->save_data;
1519    
1520     $vc->commit_changes ("converted from SuikaWiki3 <http://suika.fam.cx/gate/cvs/suikawiki/wikidata/page/$page_key.txt>");
1521     } else {
1522     my $name_props = $name_prop_db->get_data ($name);
1523     $ids = $name_props->{id};
1524     }
1525    
1526     return $ids;
1527     } # convert_sw3_page
1528    
1529     sub set_head_content ($;$$$) {
1530     my ($doc, $id, $links, $metas) = @_;
1531     my $head_el = $doc->manakai_head;
1532    
1533     our $license_name;
1534     our $style_url;
1535     push @{$links ||= []}, {rel => 'stylesheet', href => $style_url},
1536     {rel => 'license', href => get_page_url ($license_name, undef)};
1537    
1538     if (defined $id) {
1539     our $cvs_archives_url;
1540     push @$links, {rel => 'archives',
1541     href => $cvs_archives_url . 'ids/' .
1542 wakaba 1.2 int ($id / 1000) . '/' . ($id % 1000) . '.txt',
1543     title => 'CVS log for the page content'};
1544 wakaba 1.1 }
1545    
1546     for my $item (@$links) {
1547     my $link_el = $doc->create_element_ns (HTML_NS, 'link');
1548     for (keys %$item) {
1549     $link_el->set_attribute ($_ => $item->{$_});
1550     }
1551     $head_el->append_child ($link_el);
1552     }
1553    
1554     for my $item (@{$metas or []}) {
1555     my $meta_el = $doc->create_element_ns (HTML_NS, 'meta');
1556     $meta_el->set_attribute (name => $item->{name} // '');
1557     $meta_el->set_attribute (content => $item->{content} // '');
1558     $head_el->append_child ($meta_el);
1559     }
1560 wakaba 1.18 } # set_head_content
1561    
1562     sub set_foot_content ($) {
1563     my $doc = shift;
1564    
1565     my $body_el = $doc->last_child->last_child;
1566 wakaba 1.1
1567     our $script_url;
1568     my $script_el = $doc->create_element_ns (HTML_NS, 'script');
1569     $script_el->set_attribute (src => $script_url);
1570 wakaba 1.18 $body_el->append_child ($script_el);
1571     } # set_foot_content
1572 wakaba 1.1
1573 wakaba 1.57 1; ## $Date: 2009/12/27 11:34:31 $

admin@suikawiki.org
ViewVC Help
Powered by ViewVC 1.1.24