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>© 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 — '); |
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 — #'); |
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 $ |