]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WwwText.pm
No ext_urls
[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         return;
399         my ($ctx, $key, $txt) = @_;
400         if ($key eq 'mirror') {
401                 return _mirror_help($ctx, $txt);
402         } elsif ($key eq 'color') {
403                 return _colors_help($ctx, $txt);
404         } elsif ($key eq 'config') {
405                 return $ctx->{ibx}->can('cloneurl') ?
406                         inbox_config($ctx, $txt) :
407                         extindex_config($ctx, $txt);
408         }
409         return if $key ne 'help'; # TODO more keys?
410
411         my $ibx = $ctx->{ibx};
412         my $base_url = $ibx->base_url($ctx->{env});
413         $$txt .= <<EOF;
414 public-inbox help for $base_url
415
416 overview
417 --------
418
419   public-inbox uses Message-ID identifiers in URLs.
420   One may look up messages by substituting Message-IDs
421   (without the leading '<' or trailing '>') into the URL.
422   Forward slash ('/') characters in the Message-IDs
423   need to be escaped as "%2F" (without quotes).
424
425   Thus, it is possible to retrieve any message by its
426   Message-ID by going to:
427
428     $base_url<Message-ID>/
429     (without the '<' or '>')
430
431   Message-IDs are described at:
432
433     $WIKI_URL/Message-ID
434
435 EOF
436
437         # n.b. we use the Xapian DB for any regeneratable,
438         # order-of-arrival-independent data.
439         if ($ibx->isrch) {
440                 $$txt .= <<EOF;
441 search
442 ------
443
444   This public-inbox has search functionality provided by Xapian.
445
446   It supports typical AND, OR, NOT, '+', '-' queries present
447   in other search engines.
448
449   We also support search prefixes to limit the scope of the
450   search to certain fields.
451
452   Prefixes supported in this installation include:
453
454 EOF
455                 _srch_prefix($ibx, $txt);
456                 $$txt .= <<EOF;
457
458   Most prefixes are probabilistic, meaning they support stemming
459   and wildcards ('*').  Ranges (such as 'd:') and boolean prefixes
460   do not support stemming or wildcards.
461   The upstream Xapian query parser documentation fully explains
462   the query syntax:
463
464     $QP_URL
465
466 EOF
467         } # $srch
468         if ($ibx->over) {
469                 $$txt .= <<EOF;
470 message threading
471 -----------------
472
473   Message threading is enabled for this public-inbox,
474   additional endpoints for message threads are available:
475
476   * $base_url<Message-ID>/T/#u
477
478     Loads the thread belonging to the given <Message-ID>
479     in flat chronological order.  The "#u" anchor
480     focuses the browser on the given <Message-ID>.
481
482   * $base_url<Message-ID>/t/#u
483
484     Loads the thread belonging to the given <Message-ID>
485     in threaded order with nesting.  For deep threads,
486     this requires a wide display or horizontal scrolling.
487
488   Both of these HTML endpoints are suitable for offline reading
489   using the thread overview at the bottom of each page.
490
491   The gzipped mbox for a thread is available for downloading and
492   importing into your favorite mail client:
493
494   * $base_url<Message-ID>/t.mbox.gz
495
496     We use the mboxrd variant of the mbox format described at:
497
498     $WIKI_URL/Mbox
499
500   Users of feed readers may follow a particular thread using:
501
502   * $base_url<Message-ID>/t.atom
503
504     Which loads the thread in Atom Syndication Standard
505     described at Wikipedia and RFC4287:
506
507     $WIKI_URL/Atom_(standard)
508     https://tools.ietf.org/html/rfc4287
509
510     Atom Threading Extensions (RFC4685) are supported:
511
512     https://tools.ietf.org/html/rfc4685
513
514 EOF
515         } # $over
516
517         _add_non_http_urls($ctx, \(my $note = ''));
518         $note and $note =~ s/^/  /gms and $$txt .= <<EOF;
519 additional protocols
520 --------------------
521 $note
522 EOF
523         $$txt .= <<EOF;
524 contact
525 -------
526
527   This help text is maintained by public-inbox developers
528   reachable via plain-text email at: meta\@public-inbox.org
529   Their inbox is archived at: https://public-inbox.org/meta/
530 EOF
531         # TODO: support admin contact info in ~/.public-inbox/config
532         1;
533 }
534
535 1;