]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WwwText.pm
spawn_pp: use `which()' properly for pure-Perl spawn
[public-inbox.git] / lib / PublicInbox / WwwText.pm
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # used for displaying help texts and other non-mail content
5 package PublicInbox::WwwText;
6 use strict;
7 use v5.10.1;
8 use PublicInbox::Linkify;
9 use PublicInbox::WwwStream;
10 use PublicInbox::Hval qw(ascii_html prurl);
11 use HTTP::Date qw(time2str);
12 use URI::Escape qw(uri_escape_utf8);
13 use PublicInbox::GzipFilter qw(gzf_maybe);
14 our $QP_URL = 'https://xapian.org/docs/queryparser.html';
15 our $WIKI_URL = 'https://en.wikipedia.org/wiki';
16 my $hl = eval {
17         require PublicInbox::HlMod;
18         PublicInbox::HlMod->new
19 };
20
21 # /$INBOX/_/text/$KEY/ # KEY may contain slashes
22 # For now, "help" is the only supported $KEY
23 sub get_text {
24         my ($ctx, $key) = @_;
25         my $code = 200;
26
27         $key //= 'help'; # this 302s to _/text/help/
28
29         # get the raw text the same way we get mboxrds
30         my $raw = ($key =~ s!/raw\z!!);
31         my $have_tslash = ($key =~ s!/\z!!) if !$raw;
32
33         my $txt = '';
34         if (!_default_text($ctx, $key, \$txt)) {
35                 $code = 404;
36                 $txt = "404 Not Found ($key)\n";
37         }
38         my $env = $ctx->{env};
39         if ($raw) {
40                 my $h = delete $ctx->{-res_hdr};
41                 $txt = gzf_maybe($h, $env)->zflush($txt) if $code == 200;
42                 push @$h, 'Content-Type', 'text/plain',
43                         'Content-Length', length($txt);
44                 return [ $code, $h, [ $txt ] ]
45         }
46
47         # enforce trailing slash for "wget -r" compatibility
48         if (!$have_tslash && $code == 200) {
49                 my $url = $ctx->{ibx}->base_url($env);
50                 $url .= "_/text/$key/";
51
52                 return [ 302, [ 'Content-Type', 'text/plain',
53                                 'Location', $url ],
54                         [ "Redirecting to $url\n" ] ];
55         }
56
57         # Follow git commit message conventions,
58         # first line is the Subject/title
59         my ($title) = ($txt =~ /\A([^\n]*)/s);
60         $ctx->{-title_html} = ascii_html($title);
61         my $nslash = ($key =~ tr!/!/!);
62         $ctx->{-upfx} = '../../../' . ('../' x $nslash);
63         my $l = PublicInbox::Linkify->new;
64         $l->linkify_1($txt);
65         if ($hl) {
66                 $hl->do_hl_text(\$txt);
67         } else {
68                 $txt = ascii_html($txt);
69         }
70         $txt = '<pre>' . $l->linkify_2($txt) . '</pre>';
71         $txt =~ s!^search$!<a\nid=search>search</a>!sm;
72         $txt =~ s!\bPOP3\b!<a\nid=pop3>POP3</a>!;
73         $txt =~ s!\b(Newsgroups?)\b!<a\nid=nntp>$1</a>!;
74         $txt =~ s!\bIMAP\b!<a\nid=imap>IMAP</a>!;
75         PublicInbox::WwwStream::html_oneshot($ctx, $code, $txt);
76 }
77
78 sub _srch_prefix ($$) {
79         my ($ibx, $txt) = @_;
80         my $pad = 0;
81         my $htxt = '';
82         my $help = $ibx->isrch->help;
83         my $i;
84         for ($i = 0; $i < @$help; $i += 2) {
85                 my $pfx = $help->[$i];
86                 my $n = length($pfx);
87                 $pad = $n if $n > $pad;
88                 $htxt .= $pfx . "\0";
89                 $htxt .= $help->[$i + 1];
90                 $htxt .= "\f\n";
91         }
92         $pad += 2;
93         my $padding = ' ' x ($pad + 4);
94         $htxt =~ s/^/$padding/gms;
95         $htxt =~ s/^$padding(\S+)\0/"    $1".(' ' x ($pad - length($1)))/egms;
96         $htxt =~ s/\f\n/\n/gs;
97         $$txt .= $htxt;
98         1;
99 }
100
101 sub _colors_help ($$) {
102         my ($ctx, $txt) = @_;
103         my $ibx = $ctx->{ibx};
104         my $env = $ctx->{env};
105         my $base_url = $ibx->base_url($env);
106         $$txt .= "color customization for $base_url\n";
107         $$txt .= <<EOF;
108
109 public-inbox provides a stable set of CSS classes for users to
110 customize colors for highlighting diffs and code.
111
112 Users of browsers such as dillo, Firefox, or some browser
113 extensions may start by downloading the following sample CSS file
114 to control the colors they see:
115
116   ${base_url}userContent.css
117
118 CSS sample
119 ----------
120 ```css
121 EOF
122         $$txt .= PublicInbox::UserContent::sample($ibx, $env) . "```\n";
123 }
124
125 # git-config section names are quoted in the config file, so escape them
126 sub dq_escape ($) {
127         my ($name) = @_;
128         $name =~ s/\\/\\\\/g;
129         $name =~ s/"/\\"/g;
130         $name;
131 }
132
133 sub _coderepo_config ($$) {
134         my ($ctx, $txt) = @_;
135         my $cr = $ctx->{ibx}->{coderepo} // return;
136         # note: this doesn't preserve cgitrc layout, since we parse cgitrc
137         # and drop the original structure
138         $$txt .= "\tcoderepo = $_\n" for @$cr;
139         $$txt .= <<'EOF';
140
141 ; `coderepo' entries allows blob reconstruction via patch emails if
142 ; the inbox is indexed with Xapian.  `@@ <from-range> <to-range> @@'
143 ; line number ranges in `[PATCH]' emails link to /$INBOX_NAME/$OID/s/,
144 ; an HTTP endpoint which reconstructs git blobs via git-apply(1).
145 EOF
146         my $pi_cfg = $ctx->{www}->{pi_cfg};
147         for my $cr_name (@$cr) {
148                 my $urls = $pi_cfg->get_all("coderepo.$cr_name.cgiturl");
149                 my $path = "/path/to/$cr_name";
150                 $cr_name = dq_escape($cr_name);
151
152                 $$txt .= qq([coderepo "$cr_name"]\n);
153                 if ($urls && scalar(@$urls)) {
154                         $$txt .= "\t; ";
155                         $$txt .= join(" ||\n\t;\t", map {;
156                                 my $dst = $path;
157                                 if ($path !~ m![a-z0-9_/\.\-]!i) {
158                                         $dst = '"'.dq_escape($dst).'"';
159                                 }
160                                 qq(git clone $_ $dst);
161                         } @$urls);
162                         $$txt .= "\n";
163                 }
164                 $$txt .= "\tdir = $path\n";
165                 $$txt .= "\tcgiturl = https://example.com/";
166                 $$txt .= uri_escape_utf8($cr_name, '^A-Za-z0-9\-\._~/')."\n";
167         }
168 }
169
170 # n.b. this is a perfect candidate for memoization
171 sub inbox_config ($$) {
172         my ($ctx, $txt) = @_;
173         my $ibx = $ctx->{ibx};
174         push @{$ctx->{-res_hdr}},
175                 'Content-Disposition', 'inline; filename=inbox.config';
176         my $t = eval { $ibx->mm->created_at };
177         push(@{$ctx->{-res_hdr}}, 'Last-Modified', time2str($t)) if $t;
178         my $name = dq_escape($ibx->{name});
179         my $inboxdir = '/path/to/top-level-inbox';
180         my $base_url = $ibx->base_url($ctx->{env});
181         $$txt .= <<EOS;
182 ; Example public-inbox config snippet for a mirror of
183 ; $base_url
184 ; See public-inbox-config(5) manpage for more details:
185 ; https://public-inbox.org/public-inbox-config.html
186 [publicinbox "$name"]
187         inboxdir = $inboxdir
188         ; note: public-inbox before v1.2.0 used `mainrepo' instead of
189         ; `inboxdir', both remain supported after 1.2
190         mainrepo = $inboxdir
191         url = https://example.com/$name/
192         url = http://example.onion/$name/
193 EOS
194         for my $k (qw(address listid infourl watchheader)) {
195                 defined(my $v = $ibx->{$k}) or next;
196                 $$txt .= "\t$k = $_\n" for @$v;
197         }
198         if (my $altid = $ibx->{altid}) {
199                 my $altid_map = $ibx->altid_map;
200                 $$txt .= <<EOF;
201         ; altid DBs may be used to provide numeric article ID lookup from
202         ; old, pre-existing sources.  You can recreate them via curl(1),
203         ; gzip(1), and sqlite3(1) as documented:
204 EOF
205                 for (sort keys %$altid_map) {
206                         $$txt .= "\t;\tcurl -d '' $base_url$_.sql.gz | \\\n" .
207                                 "\t;\tgzip -dc | \\\n" .
208                                 "\t;\tsqlite3 $inboxdir/$_.sqlite3\n";
209                         $$txt .= "\taltid = serial:$_:file=$_.sqlite3\n";
210                 }
211         }
212
213         for my $k (qw(filter newsgroup obfuscate replyto)) {
214                 defined(my $v = $ibx->{$k}) or next;
215                 $$txt .= "\t$k = $v\n";
216         }
217         $$txt .= "\timapmirror = $_\n" for (@{$ibx->imap_url($ctx)});
218         $$txt .= "\tnntpmirror = $_\n" for (@{$ibx->nntp_url($ctx)});
219         _coderepo_config($ctx, $txt);
220         1;
221 }
222
223 # n.b. this is a perfect candidate for memoization
224 sub extindex_config ($$) {
225         my ($ctx, $txt) = @_;
226         my $ibx = $ctx->{ibx};
227         push @{$ctx->{-res_hdr}},
228                 'Content-Disposition', 'inline; filename=extindex.config';
229         my $name = dq_escape($ibx->{name});
230         my $base_url = $ibx->base_url($ctx->{env});
231         $$txt .= <<EOS;
232 ; Example public-inbox config snippet for the external index (extindex) at:
233 ; $base_url
234 ; See public-inbox-config(5)manpage for more details:
235 ; https://public-inbox.org/public-inbox-config.html
236 [extindex "$name"]
237         topdir = /path/to/extindex-topdir
238         url = https://example.com/$name/
239         url = http://example.onion/$name/
240 EOS
241         for my $k (qw(infourl)) {
242                 defined(my $v = $ibx->{$k}) or next;
243                 $$txt .= "\t$k = $v\n";
244         }
245         _coderepo_config($ctx, $txt);
246         1;
247 }
248
249 sub coderepos_raw ($$) {
250         my ($ctx, $top_url) = @_;
251         my $cr = $ctx->{ibx}->{coderepo} // return ();
252         my $cfg = $ctx->{www}->{pi_cfg};
253         my $buf = 'Code repositories for project(s) associated with this '.
254                 $ctx->{ibx}->thing_type . "\n";
255         for my $git (@{$ctx->{www}->{pi_cfg}->repo_objs($ctx->{ibx})}) {
256                 for ($git->pub_urls($ctx->{env})) {
257                         my $u = m!\A(?:[a-z\+]+:)?//!i ? $_ : $top_url.$_;
258                         $buf .= "\n\t" . prurl($ctx->{env}, $u);
259                 }
260         }
261         ($buf);
262 }
263
264 sub _add_non_http_urls ($$) {
265         my ($ctx, $txt) = @_;
266         $ctx->{ibx}->can('nntp_url') or return; # TODO extindex can have IMAP
267         my $urls = $ctx->{ibx}->imap_url($ctx);
268         if (@$urls) {
269                 $urls = join("\n  ", @$urls);
270                 $urls =~ s!://([^/@]+)/!://;AUTH=ANONYMOUS\@$1/!sg;
271                 $$txt .= <<EOM
272
273 IMAP subfolder(s) are available under:
274   $urls
275   # each subfolder (starting with `0') holds 50K messages at most
276 EOM
277         }
278         $urls = $ctx->{ibx}->nntp_url($ctx);
279         if (@$urls) {
280                 $$txt .= @$urls == 1 ? "\nNewsgroup" : "\nNewsgroups are";
281                 $$txt .= ' available over NNTP:';
282                 $$txt .= "\n  " . join("\n  ", @$urls) . "\n";
283         }
284         $urls = $ctx->{ibx}->pop3_url($ctx);
285         if (@$urls) {
286                 $urls = join("\n  ", @$urls);
287                 $$txt .= <<EOM;
288
289 POP3 access is available:
290   $urls
291
292 The POP3 password is: anonymous
293 The POP3 username is: \$(uuidgen)\@$ctx->{ibx}->{newsgroup}
294 where \$(uuidgen) in the output of the `uuidgen' command on your system.
295 The UUID in the username functions as a private cookie (don't share it).
296 Idle accounts will expire periodically.
297 EOM
298         }
299 }
300
301 sub _add_onion_note ($) {
302         my ($txt) = @_;
303         $$txt =~ m!\b[^:]+://\w+\.onion/!i and $$txt .= <<EOM
304
305 note: .onion URLs require Tor: https://www.torproject.org/
306
307 EOM
308 }
309
310 sub _mirror_help ($$) {
311         my ($ctx, $txt) = @_;
312         my $ibx = $ctx->{ibx};
313         my $base_url = $ibx->base_url($ctx->{env});
314         chop $base_url; # no trailing slash for "git clone"
315         my $dir = (split(m!/!, $base_url))[-1];
316         my %seen = ($base_url => 1);
317         my $top_url = $base_url;
318         $top_url =~ s!/[^/]+\z!/!;
319         $$txt .= "public-inbox mirroring instructions\n\n";
320         if ($ibx->can('cloneurl')) { # PublicInbox::Inbox
321                 $$txt .=
322                   "This public inbox may be cloned and mirrored by anyone:\n";
323                 my @urls;
324                 my $max = $ibx->max_git_epoch;
325                 # TODO: some of these URLs may be too long and we may need to
326                 # do something like code_footer() above, but these are local
327                 # admin-defined
328                 if (defined($max)) { # v2
329                         for my $i (0..$max) {
330                                 # old epochs my be deleted:
331                                 -d "$ibx->{inboxdir}/git/$i.git" or next;
332                                 my $url = "$base_url/$i";
333                                 $seen{$url} = 1;
334                                 push @urls, "$url $dir/git/$i.git";
335                         }
336                         my $nr = scalar(@urls);
337                         if ($nr > 1) {
338                                 chomp($$txt .= <<EOM);
339
340   # this inbox consists of $nr epochs: (no need to clone all of them)
341 EOM
342                                 $urls[0] .= " # oldest";
343                                 $urls[-1] .= " # newest";
344                         }
345                 } else { # v1
346                         push @urls, $base_url;
347                 }
348                 # FIXME: epoch splits can be different in other repositories,
349                 # use the "cloneurl" file as-is for now:
350                 for my $u (@{$ibx->cloneurl}) {
351                         next if $seen{$u}++;
352                         push @urls, $u;
353                 }
354                 $$txt .= "\n";
355                 $$txt .= join('', map { "  git clone --mirror $_\n" } @urls);
356                 my $addrs = $ibx->{address} // 'inbox@example.com';
357                 my $ng = $ibx->{newsgroup} // '';
358                 substr($ng, 0, 0, ' --ng ') if $ng;
359                 $addrs = join(' ', @$addrs) if ref($addrs) eq 'ARRAY';
360                 my $v = defined $max ? '-V2' : '-V1';
361                 $$txt .= <<EOF;
362
363   # If you have public-inbox 1.1+ installed, you may
364   # initialize and index your mirror using the following commands:
365   public-inbox-init $v$ng \\
366     $ibx->{name} ./$dir $base_url \\
367     $addrs
368   public-inbox-index ./$dir
369 EOF
370         } else { # PublicInbox::ExtSearch
371                 $$txt .= <<EOM;
372 This is an external index which is an amalgamation of several public inboxes.
373 Each public inbox needs to be mirrored individually.
374 EOM
375                 my $v = $ctx->{www}->{pi_cfg}->{lc('publicInbox.wwwListing')};
376                 if (($v // '') =~ /\A(?:all|match=domain)\z/) {
377                         $$txt .= <<EOM;
378 A list of them is available at $top_url
379 EOM
380                 }
381         }
382         my $cfg_link = "$base_url/_/text/config/raw";
383         $$txt .= <<EOF;
384
385 Example config snippet for mirrors: $cfg_link
386 EOF
387         _add_non_http_urls($ctx, $txt);
388         _add_onion_note($txt);
389
390         my $code_url = prurl($ctx->{env}, $PublicInbox::WwwStream::CODE_URL);
391         $$txt .= join("\n\n",
392                 coderepos_raw($ctx, $top_url), # may be empty
393                 "AGPL code for this site:\n  git clone $code_url");
394         1;
395 }
396
397 sub _default_text ($$$) {
398         my ($ctx, $key, $txt) = @_;
399         if ($key eq 'mirror') {
400                 return _mirror_help($ctx, $txt);
401         } elsif ($key eq 'color') {
402                 return _colors_help($ctx, $txt);
403         } elsif ($key eq 'config') {
404                 return $ctx->{ibx}->can('cloneurl') ?
405                         inbox_config($ctx, $txt) :
406                         extindex_config($ctx, $txt);
407         }
408         return if $key ne 'help'; # TODO more keys?
409
410         my $ibx = $ctx->{ibx};
411         my $base_url = $ibx->base_url($ctx->{env});
412         $$txt .= <<EOF;
413 public-inbox help for $base_url
414
415 overview
416 --------
417
418   public-inbox uses Message-ID identifiers in URLs.
419   One may look up messages by substituting Message-IDs
420   (without the leading '<' or trailing '>') into the URL.
421   Forward slash ('/') characters in the Message-IDs
422   need to be escaped as "%2F" (without quotes).
423
424   Thus, it is possible to retrieve any message by its
425   Message-ID by going to:
426
427     $base_url<Message-ID>/
428     (without the '<' or '>')
429
430   Message-IDs are described at:
431
432     $WIKI_URL/Message-ID
433
434 EOF
435
436         # n.b. we use the Xapian DB for any regeneratable,
437         # order-of-arrival-independent data.
438         if ($ibx->isrch) {
439                 $$txt .= <<EOF;
440 search
441 ------
442
443   This public-inbox has search functionality provided by Xapian.
444
445   It supports typical AND, OR, NOT, '+', '-' queries present
446   in other search engines.
447
448   We also support search prefixes to limit the scope of the
449   search to certain fields.
450
451   Prefixes supported in this installation include:
452
453 EOF
454                 _srch_prefix($ibx, $txt);
455                 $$txt .= <<EOF;
456
457   Most prefixes are probabilistic, meaning they support stemming
458   and wildcards ('*').  Ranges (such as 'd:') and boolean prefixes
459   do not support stemming or wildcards.
460   The upstream Xapian query parser documentation fully explains
461   the query syntax:
462
463     $QP_URL
464
465 EOF
466         } # $srch
467         if ($ibx->over) {
468                 $$txt .= <<EOF;
469 message threading
470 -----------------
471
472   Message threading is enabled for this public-inbox,
473   additional endpoints for message threads are available:
474
475   * $base_url<Message-ID>/T/#u
476
477     Loads the thread belonging to the given <Message-ID>
478     in flat chronological order.  The "#u" anchor
479     focuses the browser on the given <Message-ID>.
480
481   * $base_url<Message-ID>/t/#u
482
483     Loads the thread belonging to the given <Message-ID>
484     in threaded order with nesting.  For deep threads,
485     this requires a wide display or horizontal scrolling.
486
487   Both of these HTML endpoints are suitable for offline reading
488   using the thread overview at the bottom of each page.
489
490   The gzipped mbox for a thread is available for downloading and
491   importing into your favorite mail client:
492
493   * $base_url<Message-ID>/t.mbox.gz
494
495     We use the mboxrd variant of the mbox format described at:
496
497     $WIKI_URL/Mbox
498
499   Users of feed readers may follow a particular thread using:
500
501   * $base_url<Message-ID>/t.atom
502
503     Which loads the thread in Atom Syndication Standard
504     described at Wikipedia and RFC4287:
505
506     $WIKI_URL/Atom_(standard)
507     https://tools.ietf.org/html/rfc4287
508
509     Atom Threading Extensions (RFC4685) are supported:
510
511     https://tools.ietf.org/html/rfc4685
512
513 EOF
514         } # $over
515
516         _add_non_http_urls($ctx, \(my $note = ''));
517         $note and $note =~ s/^/  /gms and $$txt .= <<EOF;
518 additional protocols
519 --------------------
520 $note
521 EOF
522         $$txt .= <<EOF;
523 contact
524 -------
525
526   This help text is maintained by public-inbox developers
527   reachable via plain-text email at: meta\@public-inbox.org
528   Their inbox is archived at: https://public-inbox.org/meta/
529 EOF
530         # TODO: support admin contact info in ~/.public-inbox/config
531         1;
532 }
533
534 1;