gopher-server.pl 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. #!/usr/bin/env perl
  2. # Copyright (C) 2017–2018 Alex Schroeder <alex@gnu.org>
  3. # This program is free software: you can redistribute it and/or modify it under
  4. # the terms of the GNU General Public License as published by the Free Software
  5. # Foundation, either version 3 of the License, or (at your option) any later
  6. # version.
  7. #
  8. # This program is distributed in the hope that it will be useful, but WITHOUT
  9. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  10. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License along with
  13. # this program. If not, see <http://www.gnu.org/licenses/>.
  14. package OddMuse;
  15. use strict;
  16. use 5.10.0;
  17. use base qw(Net::Server::Fork); # any personality will do
  18. use MIME::Base64;
  19. use Text::Wrap;
  20. use List::Util qw(first);
  21. our($RunCGI, $DataDir, %IndexHash, @IndexList, $IndexFile, $TagFile, $q,
  22. %Page, $OpenPageName, $MaxPost, $ShowEdits, %Locks, $CommentsPattern,
  23. $CommentsPrefix, $EditAllowed, $NoEditFile, $SiteName);
  24. # Sadly, we need this information before doing anything else
  25. my %args = (proto => 'ssl');
  26. for (grep(/--wiki_(key|cert)_file=/, @ARGV)) {
  27. $args{SSL_cert_file} = $1 if /--wiki_cert_file=(.*)/;
  28. $args{SSL_key_file} = $1 if /--wiki_key_file=(.*)/;
  29. }
  30. if ($args{SSL_cert_file} and not $args{SSL_key_file}
  31. or not $args{SSL_cert_file} and $args{SSL_key_file}) {
  32. die "I must have both --wiki_key_file and --wiki_cert_file\n";
  33. } elsif ($args{SSL_cert_file} and $args{SSL_key_file}) {
  34. OddMuse->run(%args);
  35. } else {
  36. OddMuse->run;
  37. }
  38. sub options {
  39. my $self = shift;
  40. my $prop = $self->{'server'};
  41. my $template = shift;
  42. # setup options in the parent classes
  43. $self->SUPER::options($template);
  44. # add a single value option
  45. $prop->{wiki} ||= undef;
  46. $template->{wiki} = \$prop->{wiki};
  47. $prop->{wiki_dir} ||= undef;
  48. $template->{wiki_dir} = \$prop->{wiki_dir};
  49. $prop->{wiki_pages} ||= [];
  50. $template->{wiki_pages} = $prop->{wiki_pages};
  51. $prop->{menu} ||= [];
  52. $template->{menu} = $prop->{menu};
  53. $prop->{menu_file} ||= [];
  54. $template->{menu_file} = $prop->{menu_file};
  55. # $prop->{wiki_pem_file} ||= undef;
  56. # $template->{wiki_pem_file} = $prop->{wiki_pem_file};
  57. }
  58. sub post_configure_hook {
  59. my $self = shift;
  60. $self->write_help if $ARGV[0] eq '--help';
  61. $DataDir = $self->{server}->{wiki_dir} || $ENV{WikiDataDir} || '/tmp/oddmuse';
  62. $self->log(3, "PID $$");
  63. $self->log(3, "Host " . ("@{$self->{server}->{host}}" || "*"));
  64. $self->log(3, "Port @{$self->{server}->{port}}");
  65. $self->log(3, "Wiki data dir is $DataDir\n");
  66. $RunCGI = 0;
  67. my $wiki = $self->{server}->{wiki} || "./wiki.pl";
  68. $self->log(1, "Running $wiki\n");
  69. unless (my $return = do $wiki) {
  70. $self->log(1, "couldn't parse wiki library $wiki: $@") if $@;
  71. $self->log(1, "couldn't do wiki library $wiki: $!") unless defined $return;
  72. $self->log(1, "couldn't run wiki library $wiki") unless $return;
  73. }
  74. # make sure search is sorted newest first because NewTagFiltered resorts
  75. *OldGopherFiltered = \&Filtered;
  76. *Filtered = \&NewGopherFiltered;
  77. }
  78. my $usage = << 'EOT';
  79. This server serves a wiki as a gopher site.
  80. It implements Net::Server and thus all the options available to
  81. Net::Server are also available here. Additional options are available:
  82. wiki - this is the path to the Oddmuse script
  83. wiki_dir - this is the path to the Oddmuse data directory
  84. wiki_pages - this is a page to show on the entry menu
  85. menu - this is the description of a gopher menu to prepend
  86. menu_file - this is the filename of the gopher menu to prepend
  87. wiki_cert_file - the filename containing a certificate in PEM format
  88. wiki_key_file - the filename containing a private key in PEM format
  89. For many of the options, more information can be had in the Net::Server
  90. documentation. This is important if you want to daemonize the server. You'll
  91. need to use --pid_file so that you can stop it using a script, --setsid to
  92. daemonize it, --log_file to write keep logs, and you'll net to set the user or
  93. group using --user or --group such that the server has write access to the data
  94. directory.
  95. For testing purposes, you can start with the following:
  96. --port=7070
  97. The port to listen to, defaults to a random port.
  98. --log_level=4
  99. The log level to use, defaults to 2.
  100. --wiki_dir=/var/oddmuse
  101. The wiki directory, defaults to the value of the "WikiDataDir" environment
  102. variable or "/tmp/oddmuse".
  103. --wiki_lib=/home/alex/src/oddmuse/wiki.pl
  104. The Oddmuse main script, defaults to "./wiki.pl".
  105. --wiki_pages=SiteMap
  106. This adds a page to the main index. Can be used multiple times.
  107. --help
  108. Prints this message.
  109. Example invocation:
  110. /home/alex/src/oddmuse/stuff/gopher-server.pl \
  111. --port=7070 \
  112. --wiki=/home/alex/src/oddmuse/wiki.pl \
  113. --pid_file=/tmp/oddmuse/gopher.pid \
  114. --wiki_dir=/tmp/oddmuse \
  115. --wiki_pages=Homepage \
  116. --wiki_pages=Gopher
  117. Run the script and test it:
  118. echo | nc localhost 7070
  119. lynx gopher://localhost:7070
  120. If you want to use SSL, you need to provide PEM files containing certificate and
  121. private key. To create self-signed files, for example:
  122. openssl req -new -x509 -days 365 -nodes -out \
  123. gopher-server-cert.pem -keyout gopher-server-key.pem
  124. Make sure the common name you provide matches your domain name!
  125. Note that parameters should not contain spaces. Thus:
  126. /home/alex/src/oddmuse/stuff/gopher-server.pl \
  127. --port=7070 \
  128. --log_level=3 \
  129. --wiki=/home/alex/src/oddmuse/wiki.pl \
  130. --wiki_dir=/home/alex/alexschroeder \
  131. --menu=Moku_Pona_Updates \
  132. --menu_file=~/.moku-pona/updates.txt \
  133. --menu=Moku_Pona_Sites \
  134. --menu_file=~/.moku-pona/sites.txt
  135. EOT
  136. run();
  137. sub NewGopherFiltered {
  138. my @pages = OldGopherFiltered(@_);
  139. @pages = sort newest_first @pages;
  140. return @pages;
  141. }
  142. sub print_text {
  143. my $self = shift;
  144. my $text = shift;
  145. print($text); # bytes
  146. }
  147. sub print_menu {
  148. my $self = shift;
  149. my $display = shift;
  150. my $selector = shift;
  151. my $host = shift
  152. || $self->{server}->{host}->[0]
  153. || $self->{server}->{sockaddr};
  154. my $port = shift
  155. || $self->{server}->{port}->[0]
  156. || $self->{server}->{sockport};
  157. my $encoded = shift;
  158. $selector = join('/', map { UrlEncode($_) } split(/\//, $selector)) unless $encoded;
  159. $self->print_text(join("\t", $display, $selector, $host, $port)
  160. . "\r\n");
  161. }
  162. sub print_info {
  163. my $self = shift;
  164. my $info = shift;
  165. $self->print_menu("i$info", "");
  166. }
  167. sub print_error {
  168. my $self = shift;
  169. my $error = shift;
  170. $self->print_menu("3$error", "");
  171. }
  172. sub serve_main_menu {
  173. my $self = shift;
  174. my $more = shift;
  175. $self->log(3, "Serving main menu");
  176. $self->print_info("Welcome to the Gopher version of this wiki.");
  177. $self->print_info("");
  178. $self->print_info("Phlog:");
  179. my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
  180. for my $id (@pages[0..9]) {
  181. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  182. }
  183. $self->print_menu("1" . "More...", "do/more");
  184. $self->print_info("");
  185. for my $id (@{$self->{server}->{wiki_pages}}) {
  186. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  187. }
  188. for my $id (@{$self->{server}->{menu}}) {
  189. $self->print_menu("1" . NormalToFree($id), "map/$id");
  190. }
  191. $self->print_menu("1" . "Recent Changes", "do/rc");
  192. $self->print_menu("7" . "Find matching page titles", "do/match");
  193. $self->print_menu("7" . "Full text search", "do/search");
  194. $self->print_menu("1" . "Index of all pages", "do/index");
  195. if ($TagFile) {
  196. $self->print_menu("1" . "Index of all tags", "do/tags");
  197. }
  198. if ($EditAllowed and not IsFile($NoEditFile)) {
  199. $self->print_menu("w" . "New page", "do/new");
  200. }
  201. }
  202. sub serve_phlog_archive {
  203. my $self = shift;
  204. $self->log(3, "Serving phlog archive");
  205. my @pages = sort { $b cmp $a } grep(/^\d\d\d\d-\d\d-\d\d/, @IndexList);
  206. for my $id (@pages) {
  207. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  208. }
  209. }
  210. sub serve_index {
  211. my $self = shift;
  212. $self->log(3, "Serving index of all pages");
  213. for my $id (sort newest_first @IndexList) {
  214. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  215. }
  216. }
  217. sub serve_match {
  218. my $self = shift;
  219. my $match = shift;
  220. $self->log(3, "Serving pages matching " . UrlEncode($match));
  221. $self->print_info("Use a regular expression to match page titles.");
  222. $self->print_info("Spaces in page titles are underlines, '_'.");
  223. for my $id (sort newest_first grep(/$match/i, @IndexList)) {
  224. $self->print_menu( "1" . NormalToFree($id), "$id/menu");
  225. }
  226. }
  227. sub serve_search {
  228. my $self = shift;
  229. my $str = shift;
  230. $self->log(3, "Serving search result for " . UrlEncode($str));
  231. $self->print_info("Use regular expressions separated by spaces.");
  232. SearchTitleAndBody($str, sub {
  233. my $id = shift;
  234. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  235. });
  236. }
  237. sub serve_tags {
  238. my $self = shift;
  239. $self->log(3, "Serving tag cloud");
  240. # open the DB file
  241. my %h = TagReadHash();
  242. my %count = ();
  243. foreach my $tag (grep !/^_/, keys %h) {
  244. $count{$tag} = @{$h{$tag}};
  245. }
  246. foreach my $id (sort { $count{$b} <=> $count{$a} } keys %count) {
  247. $self->print_menu("1" . NormalToFree($id), "$id/tag");
  248. }
  249. }
  250. sub serve_rc {
  251. my $self = shift;
  252. my $showedit = $ShowEdits = shift;
  253. $self->log(3, "Serving recent changes"
  254. . ($showedit ? " including minor changes" : ""));
  255. $self->print_info("Recent Changes");
  256. if ($showedit) {
  257. $self->print_menu("1" . "Skip minor edits", "do/rc");
  258. } else {
  259. $self->print_menu("1" . "Show minor edits", "do/rc/showedits");
  260. }
  261. ProcessRcLines(
  262. sub {
  263. my $date = shift;
  264. $self->print_info("");
  265. $self->print_info("$date");
  266. $self->print_info("");
  267. },
  268. sub {
  269. my($id, $ts, $author_host, $username, $summary, $minor, $revision,
  270. $languages, $cluster, $last) = @_;
  271. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  272. for my $line (split(/\n/, wrap(' ', ' ', $summary))) {
  273. $self->print_info($line);
  274. }
  275. });
  276. }
  277. sub serve_map {
  278. my $self = shift;
  279. my $id = shift;
  280. $self->log(3, "Serving map " . UrlEncode($id));
  281. my @menu = @{$self->{server}->{menu}};
  282. my $i = first { $id eq $menu[$_] } 0..$#menu;
  283. my $file = $self->{server}->{menu_file}->[$i];
  284. if (-f $file and open(my $fh, '<:encoding(UTF-8)', $file)) {
  285. local $/ = undef;
  286. my $text = <$fh>;
  287. $self->log(4, "Map has " . length($text) . " characters");
  288. $self->print_text($text);
  289. } else {
  290. $self->log(1, "Error reading $file");
  291. }
  292. }
  293. sub serve_page_comment_link {
  294. my $self = shift;
  295. my $id = shift;
  296. my $revision = shift;
  297. if (not $revision and $CommentsPattern) {
  298. if ($id =~ /$CommentsPattern/) {
  299. my $original = $1;
  300. # sometimes we are on a comment page and cannot derive the original
  301. $self->print_menu("1" . "Back to the original page",
  302. "$original/menu") if $original;
  303. $self->print_menu("w" . "Add a comment", "$id/append/text");
  304. } else {
  305. my $comments = $CommentsPrefix . $id;
  306. $self->print_menu("1" . "Comments on this page", "$comments/menu");
  307. }
  308. }
  309. }
  310. sub serve_page_history_link {
  311. my $self = shift;
  312. my $id = shift;
  313. my $revision = shift;
  314. if (not $revision) {
  315. $self->print_menu("1" . "Page History", "$id/history");
  316. }
  317. }
  318. sub serve_file_page_menu {
  319. my $self = shift;
  320. my $id = shift;
  321. my $type = shift;
  322. my $revision = shift;
  323. my $code = substr($type, 0, 6) eq 'image/' ? 'I' : '9';
  324. $self->log(3, "Serving file page menu for " . UrlEncode($id));
  325. $self->print_menu($code . NormalToFree($id)
  326. . ($revision ? "/$revision" : ""), $id);
  327. $self->serve_page_comment_link($id, $revision);
  328. $self->serve_page_history_link($id, $revision);
  329. }
  330. sub serve_text_page_menu {
  331. my $self = shift;
  332. my $id = shift;
  333. my $page = shift;
  334. my $revision = shift;
  335. $self->log(3, "Serving text page menu for " . UrlEncode($id)
  336. . ($revision ? "/$revision" : ""));
  337. $self->print_info("The text of this page:");
  338. $self->print_menu("0" . NormalToFree($id),
  339. $id . ($revision ? "/$revision" : ""));
  340. $self->print_menu("h" . NormalToFree($id),
  341. $id . ($revision ? "/$revision" : "") . "/html");
  342. $self->print_menu("w" . "Replace " . NormalToFree($id),
  343. $id . "/write/text");
  344. $self->serve_page_comment_link($id, $revision);
  345. $self->serve_page_history_link($id, $revision);
  346. my $first = 1;
  347. while ($page->{text} =~ /\[\[([^\]|]*)(?:\|([^\]]*))?\]\]|\[(https?:\/\/\S+)\s+([^\]]*)\]|\[gopher:\/\/([^:\/]*)(?::(\d+))?\/(\d)(\S+)\s+([^\]]+)\]/g) {
  348. my ($title, $text, $url, $hostname, $port, $type, $selector)
  349. = ($1, $2||$4||$9, $3, $5, $6, $7, $8);
  350. if ($first) {
  351. $self->print_info("");
  352. $self->print_info("Links leaving " . NormalToFree($id) . ":");
  353. $first = 0;
  354. }
  355. if ($hostname) {
  356. $self->print_text(join("\t", $type . $text, $selector, $hostname, $port) . "\r\n");
  357. } elsif ($url) {
  358. $self->print_menu("h$text", "URL:" . $url, undef, undef, 1);
  359. } elsif ($title and substr($title, 0, 4) eq 'tag:') {
  360. $self->print_menu("1" . ($text||substr($title, 4)),
  361. substr($title, 4) . "/tag");
  362. } elsif ($title) {
  363. if (substr($title, 0, 6) eq 'image:') {
  364. $title = substr($title, 6);
  365. }
  366. $self->print_menu("1" . $text||$title, $title . "/menu");
  367. }
  368. }
  369. $first = 1;
  370. while ($page->{text} =~ /\[https?:\/\/gopher\.floodgap\.com\/gopher\/gw\?a=gopher%3a%2f%2f(.*?)(?:%3a(\d+))?%2f(.)(\S+)\s+([^\]]+)\]/gi) {
  371. my ($hostname, $port, $type, $selector, $text) = ($1, $2||"70", $3, $4, $5);
  372. if ($first) {
  373. $self->print_info("");
  374. $self->print_info("Gopher links (via Floodgap):");
  375. $first = 0;
  376. }
  377. $selector =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eig; # url unescape
  378. $self->print_text(join("\t", $type . $text, $selector, $hostname, $port)
  379. . "\r\n");
  380. }
  381. if ($page->{text} =~ m/<journal search tag:(\S+)>\s*/) {
  382. my $tag = $1;
  383. $self->print_info("");
  384. $self->serve_tag_list($tag);
  385. }
  386. }
  387. sub serve_page_history {
  388. my $self = shift;
  389. my $id = shift;
  390. $self->log(3, "Serving history of " . UrlEncode($id));
  391. OpenPage($id);
  392. $self->print_menu("1" . NormalToFree($id) . " (current)", "$id/menu");
  393. $self->print_info(CalcTime($Page{ts})
  394. . " by " . GetAuthor($Page{host}, $Page{username})
  395. . ($Page{summary} ? ": $Page{summary}" : "")
  396. . ($Page{minor} ? " (minor)" : ""));
  397. foreach my $revision (GetKeepRevisions($OpenPageName)) {
  398. my $keep = GetKeptRevision($revision);
  399. $self->print_menu("1" . NormalToFree($id) . " ($keep->{revision})",
  400. "$id/$keep->{revision}/menu");
  401. $self->print_info(CalcTime($keep->{ts})
  402. . " by " . GetAuthor($keep->{host}, $keep->{username})
  403. . ($keep->{summary} ? ": $keep->{summary}" : "")
  404. . ($keep->{minor} ? " (minor)" : ""));
  405. }
  406. }
  407. sub get_page {
  408. my $id = shift;
  409. my $revision = shift;
  410. my $page;
  411. if ($revision) {
  412. $OpenPageName = $id;
  413. $page = GetKeptRevision($revision);
  414. } else {
  415. OpenPage($id);
  416. $page = \%Page;
  417. }
  418. return $page;
  419. }
  420. sub serve_page_menu {
  421. my $self = shift;
  422. my $id = shift;
  423. my $revision = shift;
  424. my $page = get_page($id, $revision);
  425. if (my ($type) = TextIsFile($page->{text})) {
  426. $self->serve_file_page_menu($id, $type, $revision);
  427. } else {
  428. $self->serve_text_page_menu($id, $page, $revision);
  429. }
  430. }
  431. sub serve_file_page {
  432. my $self = shift;
  433. my $id = shift;
  434. my $page = shift;
  435. $self->log(3, "Serving " . UrlEncode($id) . " as file");
  436. my ($encoded) = $page->{text} =~ /^[^\n]*\n(.*)/s;
  437. $self->log(4, UrlEncode($id) . " has " . length($encoded)
  438. . " bytes of MIME encoded data");
  439. my $data = decode_base64($encoded);
  440. $self->log(4, UrlEncode($id) . " has " . length($data)
  441. . " bytes of binary data");
  442. binmode(STDOUT, ":raw");
  443. print($data);
  444. # do not append a dot, just close the connection
  445. goto EXIT_NO_DOT;
  446. }
  447. sub serve_text_page {
  448. my $self = shift;
  449. my $id = shift;
  450. my $page = shift;
  451. my $text = $page->{text};
  452. $self->log(3, "Serving " . UrlEncode($id) . " as " . length($text)
  453. . " bytes of text");
  454. $text =~ s/^\./../mg;
  455. $self->print_text($text);
  456. }
  457. sub serve_page {
  458. my $self = shift;
  459. my $id = shift;
  460. my $revision = shift;
  461. my $page = get_page($id, $revision);
  462. if (my ($type) = TextIsFile($page->{text})) {
  463. $self->serve_file_page($id, $page);
  464. } else {
  465. $self->serve_text_page($id, $page);
  466. }
  467. }
  468. sub serve_page_html {
  469. my $self = shift;
  470. my $id = shift;
  471. my $revision = shift;
  472. my $page = get_page($id, $revision);
  473. $self->log(3, "Serving " . UrlEncode($id) . " as HTML");
  474. my $title = NormalToFree($id);
  475. print GetHtmlHeader(Ts('%s:', $SiteName) . ' ' . UnWiki($title), $id);
  476. print GetHeaderDiv($id, $title);
  477. print $q->start_div({-class=>'wrapper'});
  478. if ($revision) {
  479. # no locking of the file, no updating of the cache
  480. PrintWikiToHTML($page->{text});
  481. } else {
  482. PrintPageHtml();
  483. }
  484. PrintFooter($id, $revision);
  485. # do not append a dot, just close the connection
  486. goto EXIT_NO_DOT;
  487. }
  488. sub serve_redirect {
  489. my $self = shift;
  490. my $url = shift;
  491. print qq{<!DOCTYPE HTML>
  492. <html lang="en-US">
  493. <head>
  494. <meta http-equiv="refresh" content="0; url=$url">
  495. <title>Redirection</title>
  496. </head>
  497. <body>
  498. If you are not redirected automatically, follow this <a href='$url'>link</a>.
  499. </body>
  500. </html>
  501. };
  502. # do not append a dot, just close the connection
  503. goto EXIT_NO_DOT;
  504. }
  505. sub newest_first {
  506. my ($A, $B) = ($a, $b);
  507. if ($A =~ /^\d\d\d\d-\d\d-\d\d/ and $B =~ /^\d\d\d\d-\d\d-\d\d/) {
  508. return $B cmp $A;
  509. }
  510. $A cmp $B;
  511. }
  512. sub serve_tag_list {
  513. my $self = shift;
  514. my $tag = shift;
  515. $self->print_info("Search result for tag $tag:");
  516. for my $id (sort newest_first TagFind($tag)) {
  517. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  518. }
  519. }
  520. sub serve_tag {
  521. my $self = shift;
  522. my $tag = shift;
  523. $self->log(3, "Serving tag " . UrlEncode($tag));
  524. if ($IndexHash{$tag}) {
  525. $self->print_info("This page is about the tag $tag.");
  526. $self->print_menu("1" . NormalToFree($tag), "$tag/menu");
  527. $self->print_info("");
  528. }
  529. $self->serve_tag_list($tag);
  530. }
  531. sub serve_error {
  532. my $self = shift;
  533. my $id = shift;
  534. my $error = shift;
  535. $self->log(3, "Error ('" . UrlEncode($id) . "'): $error");
  536. $self->print_error("Error ('" . UrlEncode($id) . "'): $error");
  537. }
  538. sub write_help {
  539. my $self = shift;
  540. my @lines = split(/\n/, <<"EOF");
  541. This is how your document should start:
  542. ```
  543. username: Alex Schroeder
  544. summary: typo fixed
  545. ```
  546. This is the text of your document.
  547. Just write whatever.
  548. Note the space after the colon for metadata fields.
  549. More metadata fields are allowed:
  550. `minor` is 1 if this is a minor edit. The default is 0.
  551. EOF
  552. for my $line (@lines) {
  553. $self->print_info($line);
  554. }
  555. }
  556. sub write_page_ok {
  557. my $self = shift;
  558. my $id = shift;
  559. $self->print_info("Page was saved.");
  560. $self->print_menu("1" . NormalToFree($id), "$id/menu");
  561. }
  562. sub write_page_error {
  563. my $self = shift;
  564. my $error = shift;
  565. $self->log(4, "Not saved: $error");
  566. $self->print_error("Page was not saved: $error");
  567. map { ReleaseLockDir($_); } keys %Locks;
  568. goto EXIT;
  569. }
  570. sub write_data {
  571. my $self = shift;
  572. my $id = shift;
  573. my $data = shift;
  574. my $param = shift||'text';
  575. SetParam($param, $data);
  576. my $error;
  577. eval {
  578. local *ReBrowsePage = sub {};
  579. local *ReportError = sub { $error = shift };
  580. DoPost($id);
  581. };
  582. if ($error) {
  583. $self->write_page_error($error);
  584. } else {
  585. $self->write_page_ok($id);
  586. }
  587. }
  588. sub write_file_page {
  589. my $self = shift;
  590. my $id = shift;
  591. my $data = shift;
  592. my $type = shift || 'application/octet-stream';
  593. $self->write_page_error("page title is missing") unless $id;
  594. $self->log(3, "Posting " . length($data) . " bytes of $type to page "
  595. . UrlEncode($id));
  596. # no metadata
  597. $self->write_data($id, "#FILE $type\n" . encode_base64($data));
  598. }
  599. sub write_text {
  600. my $self = shift;
  601. my $id = shift;
  602. my $data = shift;
  603. my $param = shift;
  604. utf8::decode($data);
  605. my ($lead, $meta, $text) = split(/^```\s*(?:meta)?\n/m, $data, 3);
  606. if (not $lead and $meta) {
  607. while ($meta =~ /^([a-z-]+): (.*)/mg) {
  608. if ($1 eq 'minor' and $2) {
  609. SetParam('recent_edit', 'on'); # legacy UseMod parameter name
  610. } else {
  611. SetParam($1, $2);
  612. if ($1 eq "title") {
  613. $id = $2;
  614. }
  615. }
  616. }
  617. $self->log(3, ($param eq 'text' ? "Posting" : "Appending")
  618. . " " . length($text) . " characters (with metadata) to page $id");
  619. $self->write_data($id, $text, $param);
  620. } else {
  621. # no meta data
  622. $self->log(3, ($param eq 'text' ? "Posting" : "Appending")
  623. . " " . length($data) . " characters to page $id") if $id;
  624. $self->write_data($id, $data, $param);
  625. }
  626. }
  627. sub write_text_page {
  628. my $self = shift;
  629. $self->write_text(@_, 'text');
  630. }
  631. sub append_text_page {
  632. my $self = shift;
  633. $self->write_text(@_, 'aftertext');
  634. }
  635. sub read_file {
  636. my $self = shift;
  637. my $length = shift;
  638. $length = $MaxPost if $length > $MaxPost;
  639. local $/ = \$length;
  640. my $buf .= <STDIN>;
  641. $self->log(4, "Received " . length($buf) . " bytes (max is $MaxPost)");
  642. return $buf;
  643. }
  644. sub read_text {
  645. my $self = shift;
  646. my $buf;
  647. while (1) {
  648. my $line = <STDIN>;
  649. if (length($line) == 0) {
  650. sleep(1); # wait for input
  651. next;
  652. }
  653. last if $line =~ /^.\r?\n/m;
  654. $buf .= $line;
  655. if (length($buf) > $MaxPost) {
  656. $buf = substr($buf, 0, $MaxPost);
  657. last;
  658. }
  659. }
  660. $self->log(4, "Received " . length($buf) . " bytes (max is $MaxPost)");
  661. utf8::decode($buf);
  662. $self->log(4, "Received " . length($buf) . " characters");
  663. return $buf;
  664. }
  665. sub process_request {
  666. my $self = shift;
  667. # clear cookie and all that
  668. $q = undef;
  669. Init();
  670. # refresh list of pages
  671. if (IsFile($IndexFile) and ReadIndex()) {
  672. # we're good
  673. } else {
  674. RefreshIndex();
  675. }
  676. eval {
  677. local $SIG{'ALRM'} = sub {
  678. $self->log(1, "Timeout!");
  679. die "Timed Out!\n";
  680. };
  681. alarm(10); # timeout
  682. my $selector = <STDIN>; # no loop
  683. $selector = UrlDecode($selector); # assuming URL-encoded UTF-8
  684. $selector =~ s/\s+$//g; # no trailing whitespace
  685. if (not $selector) {
  686. $self->serve_main_menu();
  687. } elsif ($selector eq "do/more") {
  688. $self->serve_phlog_archive();
  689. } elsif ($selector eq "do/index") {
  690. $self->serve_index();
  691. } elsif (substr($selector, 0, 9) eq "do/match\t") {
  692. $self->serve_match(substr($selector, 9));
  693. } elsif (substr($selector, 0, 10) eq "do/search\t") {
  694. $self->serve_search(substr($selector, 10));
  695. } elsif ($selector eq "do/tags") {
  696. $self->serve_tags();
  697. } elsif ($selector eq "do/rc") {
  698. $self->serve_rc(0);
  699. } elsif ($selector eq "do/rc/showedits") {
  700. $self->serve_rc(1);
  701. } elsif ($selector eq "do/new") {
  702. my $data = $self->read_text();
  703. $self->write_text_page(undef, $data);
  704. } elsif ($selector =~ m!^([^/]*)/(\d+)/menu$!) {
  705. $self->serve_page_menu($1, $2);
  706. } elsif ($selector =~ m!^map/(.*)!) {
  707. $self->serve_map($1);
  708. } elsif (substr($selector, -5) eq '/menu') {
  709. $self->serve_page_menu(substr($selector, 0, -5));
  710. } elsif ($selector =~ m!^([^/]*)/tag$!) {
  711. $self->serve_tag($1);
  712. } elsif ($selector =~ m!^([^/]*)(?:/(\d+))?/html!) {
  713. $self->serve_page_html($1, $2);
  714. } elsif ($selector =~ m!^([^/]*)/history$!) {
  715. $self->serve_page_history($1);
  716. } elsif ($selector =~ m!^([^/]*)/write/text$!) {
  717. my $data = $self->read_text();
  718. $self->write_text_page($1, $data);
  719. } elsif ($selector =~ m!^([^/]*)/append/text$!) {
  720. my $data = $self->read_text();
  721. $self->append_text_page($1, $data);
  722. } elsif ($selector =~ m!^([^/]*)(?:/([a-z]+/[-a-z]+))?/write/file(?:\t(\d+))?$!) {
  723. my $data = $self->read_file($3);
  724. $self->write_file_page($1, $data, $2);
  725. } elsif ($selector =~ m!^([^/]*)(?:/(\d+))?(?:/text)?$!) {
  726. $self->serve_page($1, $2);
  727. } elsif ($selector =~ m!^URL:(.*)!i) {
  728. $self->serve_redirect(UrlDecode($1));
  729. } else {
  730. $self->serve_error($selector, ValidId($selector)||'Cause unknown');
  731. }
  732. EXIT:
  733. # write final dot for almost everything
  734. $self->print_text(".\r\n");
  735. EXIT_NO_DOT:
  736. # except when sending a binary file
  737. $self->log(4, "Done");
  738. }
  739. }