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