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