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