gopher-server.pl 28 KB

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