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