]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WwwText.pm
581a19f34c4d9f0787e8716476d616362fc86f01
[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;
250         for my $cr_name (@$cr) {
251                 $ret[0] //= do {
252                         my $thing = $ctx->{ibx}->can('cloneurl') ?
253                                 'public inbox' : 'external index';
254                         <<EOF;
255 Code repositories for project(s) associated with this $thing
256 EOF
257                 };
258                 my $urls = $cfg->get_all("coderepo.$cr_name.cgiturl");
259                 if ($urls) {
260                         for (@$urls) {
261                                 # relative or absolute URL?, prefix relative
262                                 # "foo.git" with appropriate number of "../"
263                                 my $u = m!\A(?:[a-z\+]+:)?//!i ? $_ :
264                                         $top_url.$_;
265                                 $ret[0] .= "\n\t" . prurl($ctx->{env}, $u);
266                         }
267                 } else {
268                         $ret[0] .= qq[\n\t$cr_name.git (no URL configured)];
269                 }
270         }
271         @ret; # may be empty, this sub is called as an arg for join()
272 }
273
274 sub _add_non_http_urls ($$) {
275         my ($ctx, $txt) = @_;
276         $ctx->{ibx}->can('nntp_url') or return; # TODO extindex can have IMAP
277         my $urls = $ctx->{ibx}->imap_url($ctx);
278         if (@$urls) {
279                 $urls = join("\n  ", @$urls);
280                 $urls =~ s!://([^/@]+)/!://;AUTH=ANONYMOUS\@$1/!sg;
281                 $$txt .= <<EOM
282
283 IMAP subfolder(s) are available under:
284   $urls
285   # each subfolder (starting with `0') holds 50K messages at most
286 EOM
287         }
288         $urls = $ctx->{ibx}->nntp_url($ctx);
289         if (@$urls) {
290                 $$txt .= @$urls == 1 ? "\nNewsgroup" : "\nNewsgroups are";
291                 $$txt .= ' available over NNTP:';
292                 $$txt .= "\n  " . join("\n  ", @$urls) . "\n";
293         }
294         $urls = $ctx->{ibx}->pop3_url($ctx);
295         if (@$urls) {
296                 $urls = join("\n  ", @$urls);
297                 $$txt .= <<EOM;
298
299 POP3 access is available:
300   $urls
301
302 The POP3 password is: anonymous
303 The POP3 username is: \$(uuidgen)\@$ctx->{ibx}->{newsgroup}
304 where \$(uuidgen) in the output of the `uuidgen' command on your system.
305 The UUID in the username functions as a private cookie (don't share it).
306 Idle accounts will expire periodically.
307 EOM
308         }
309 }
310
311 sub _add_onion_note ($) {
312         my ($txt) = @_;
313         $$txt =~ m!\b[^:]+://\w+\.onion/!i and $$txt .= <<EOM
314
315 note: .onion URLs require Tor: https://www.torproject.org/
316
317 EOM
318 }
319
320 sub _mirror_help ($$) {
321         my ($ctx, $txt) = @_;
322         my $ibx = $ctx->{ibx};
323         my $base_url = $ibx->base_url($ctx->{env});
324         chop $base_url; # no trailing slash for "git clone"
325         my $dir = (split(m!/!, $base_url))[-1];
326         my %seen = ($base_url => 1);
327         my $top_url = $base_url;
328         $top_url =~ s!/[^/]+\z!/!;
329         $$txt .= "public-inbox mirroring instructions\n\n";
330         if ($ibx->can('cloneurl')) { # PublicInbox::Inbox
331                 $$txt .=
332                   "This public inbox may be cloned and mirrored by anyone:\n";
333                 my @urls;
334                 my $max = $ibx->max_git_epoch;
335                 # TODO: some of these URLs may be too long and we may need to
336                 # do something like code_footer() above, but these are local
337                 # admin-defined
338                 if (defined($max)) { # v2
339                         for my $i (0..$max) {
340                                 # old epochs my be deleted:
341                                 -d "$ibx->{inboxdir}/git/$i.git" or next;
342                                 my $url = "$base_url/$i";
343                                 $seen{$url} = 1;
344                                 push @urls, "$url $dir/git/$i.git";
345                         }
346                         my $nr = scalar(@urls);
347                         if ($nr > 1) {
348                                 chomp($$txt .= <<EOM);
349
350   # this inbox consists of $nr epochs: (no need to clone all of them)
351 EOM
352                                 $urls[0] .= " # oldest";
353                                 $urls[-1] .= " # newest";
354                         }
355                 } else { # v1
356                         push @urls, $base_url;
357                 }
358                 # FIXME: epoch splits can be different in other repositories,
359                 # use the "cloneurl" file as-is for now:
360                 for my $u (@{$ibx->cloneurl}) {
361                         next if $seen{$u}++;
362                         push @urls, $u;
363                 }
364                 $$txt .= "\n";
365                 $$txt .= join('', map { "  git clone --mirror $_\n" } @urls);
366                 my $addrs = $ibx->{address} // 'inbox@example.com';
367                 my $ng = $ibx->{newsgroup} // '';
368                 substr($ng, 0, 0, ' --ng ') if $ng;
369                 $addrs = join(' ', @$addrs) if ref($addrs) eq 'ARRAY';
370                 my $v = defined $max ? '-V2' : '-V1';
371                 $$txt .= <<EOF;
372
373   # If you have public-inbox 1.1+ installed, you may
374   # initialize and index your mirror using the following commands:
375   public-inbox-init $v$ng \\
376     $ibx->{name} ./$dir $base_url \\
377     $addrs
378   public-inbox-index ./$dir
379 EOF
380         } else { # PublicInbox::ExtSearch
381                 $$txt .= <<EOM;
382 This is an external index which is an amalgamation of several public inboxes.
383 Each public inbox needs to be mirrored individually.
384 EOM
385                 my $v = $ctx->{www}->{pi_cfg}->{lc('publicInbox.wwwListing')};
386                 if (($v // '') =~ /\A(?:all|match=domain)\z/) {
387                         $$txt .= <<EOM;
388 A list of them is available at $top_url
389 EOM
390                 }
391         }
392         my $cfg_link = "$base_url/_/text/config/raw";
393         $$txt .= <<EOF;
394
395 Example config snippet for mirrors: $cfg_link
396 EOF
397         _add_non_http_urls($ctx, $txt);
398         _add_onion_note($txt);
399
400         my $code_url = prurl($ctx->{env}, $PublicInbox::WwwStream::CODE_URL);
401         $$txt .= join("\n\n",
402                 coderepos_raw($ctx, $top_url), # may be empty
403                 "AGPL code for this site:\n  git clone $code_url");
404         1;
405 }
406
407 sub _default_text ($$$$) {
408         my ($ctx, $key, $hdr, $txt) = @_;
409         if ($key eq 'mirror') {
410                 return _mirror_help($ctx, $txt);
411         } elsif ($key eq 'color') {
412                 return _colors_help($ctx, $txt);
413         } elsif ($key eq 'config') {
414                 return $ctx->{ibx}->can('cloneurl') ?
415                         inbox_config($ctx, $hdr, $txt) :
416                         extindex_config($ctx, $hdr, $txt);
417         }
418         return if $key ne 'help'; # TODO more keys?
419
420         my $ibx = $ctx->{ibx};
421         my $base_url = $ibx->base_url($ctx->{env});
422         $$txt .= <<EOF;
423 public-inbox help for $base_url
424
425 overview
426 --------
427
428   public-inbox uses Message-ID identifiers in URLs.
429   One may look up messages by substituting Message-IDs
430   (without the leading '<' or trailing '>') into the URL.
431   Forward slash ('/') characters in the Message-IDs
432   need to be escaped as "%2F" (without quotes).
433
434   Thus, it is possible to retrieve any message by its
435   Message-ID by going to:
436
437     $base_url<Message-ID>/
438     (without the '<' or '>')
439
440   Message-IDs are described at:
441
442     $WIKI_URL/Message-ID
443
444 EOF
445
446         # n.b. we use the Xapian DB for any regeneratable,
447         # order-of-arrival-independent data.
448         if ($ibx->isrch) {
449                 $$txt .= <<EOF;
450 search
451 ------
452
453   This public-inbox has search functionality provided by Xapian.
454
455   It supports typical AND, OR, NOT, '+', '-' queries present
456   in other search engines.
457
458   We also support search prefixes to limit the scope of the
459   search to certain fields.
460
461   Prefixes supported in this installation include:
462
463 EOF
464                 _srch_prefix($ibx, $txt);
465                 $$txt .= <<EOF;
466
467   Most prefixes are probabilistic, meaning they support stemming
468   and wildcards ('*').  Ranges (such as 'd:') and boolean prefixes
469   do not support stemming or wildcards.
470   The upstream Xapian query parser documentation fully explains
471   the query syntax:
472
473     $QP_URL
474
475 EOF
476         } # $srch
477         if ($ibx->over) {
478                 $$txt .= <<EOF;
479 message threading
480 -----------------
481
482   Message threading is enabled for this public-inbox,
483   additional endpoints for message threads are available:
484
485   * $base_url<Message-ID>/T/#u
486
487     Loads the thread belonging to the given <Message-ID>
488     in flat chronological order.  The "#u" anchor
489     focuses the browser on the given <Message-ID>.
490
491   * $base_url<Message-ID>/t/#u
492
493     Loads the thread belonging to the given <Message-ID>
494     in threaded order with nesting.  For deep threads,
495     this requires a wide display or horizontal scrolling.
496
497   Both of these HTML endpoints are suitable for offline reading
498   using the thread overview at the bottom of each page.
499
500   The gzipped mbox for a thread is available for downloading and
501   importing into your favorite mail client:
502
503   * $base_url<Message-ID>/t.mbox.gz
504
505     We use the mboxrd variant of the mbox format described at:
506
507     $WIKI_URL/Mbox
508
509   Users of feed readers may follow a particular thread using:
510
511   * $base_url<Message-ID>/t.atom
512
513     Which loads the thread in Atom Syndication Standard
514     described at Wikipedia and RFC4287:
515
516     $WIKI_URL/Atom_(standard)
517     https://tools.ietf.org/html/rfc4287
518
519     Atom Threading Extensions (RFC4685) are supported:
520
521     https://tools.ietf.org/html/rfc4685
522
523 EOF
524         } # $over
525
526         _add_non_http_urls($ctx, \(my $note = ''));
527         $note and $note =~ s/^/  /gms and $$txt .= <<EOF;
528 additional protocols
529 --------------------
530 $note
531 EOF
532         $$txt .= <<EOF;
533 contact
534 -------
535
536   This help text is maintained by public-inbox developers
537   reachable via plain-text email at: meta\@public-inbox.org
538   Their inbox is archived at: https://public-inbox.org/meta/
539 EOF
540         # TODO: support admin contact info in ~/.public-inbox/config
541         1;
542 }
543
544 1;