]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WwwText.pm
177d55e46aa1b6a2d269e29b512a9bc06306b0e4
[public-inbox.git] / lib / PublicInbox / WwwText.pm
1 # Copyright (C) 2016-2021 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 URI::Escape qw(uri_escape_utf8);
12 use PublicInbox::GzipFilter qw(gzf_maybe);
13 our $QP_URL = 'https://xapian.org/docs/queryparser.html';
14 our $WIKI_URL = 'https://en.wikipedia.org/wiki';
15 my $hl = eval {
16         require PublicInbox::HlMod;
17         PublicInbox::HlMod->new
18 };
19
20 # /$INBOX/_/text/$KEY/ # KEY may contain slashes
21 # For now, "help" is the only supported $KEY
22 sub get_text {
23         my ($ctx, $key) = @_;
24         my $code = 200;
25
26         $key //= 'help'; # this 302s to _/text/help/
27
28         # get the raw text the same way we get mboxrds
29         my $raw = ($key =~ s!/raw\z!!);
30         my $have_tslash = ($key =~ s!/\z!!) if !$raw;
31
32         my $txt = '';
33         my $hdr = [ 'Content-Type', 'text/plain', 'Content-Length', undef ];
34         if (!_default_text($ctx, $key, $hdr, \$txt)) {
35                 $code = 404;
36                 $txt = "404 Not Found ($key)\n";
37         }
38         my $env = $ctx->{env};
39         if ($raw) {
40                 if ($code == 200) {
41                         my $gzf = gzf_maybe($hdr, $env);
42                         $txt = $gzf->translate($txt);
43                         $txt .= $gzf->zflush;
44                 }
45                 $hdr->[3] = length($txt);
46                 return [ $code, $hdr, [ $txt ] ]
47         }
48
49         # enforce trailing slash for "wget -r" compatibility
50         if (!$have_tslash && $code == 200) {
51                 my $url = $ctx->{ibx}->base_url($env);
52                 $url .= "_/text/$key/";
53
54                 return [ 302, [ 'Content-Type', 'text/plain',
55                                 'Location', $url ],
56                         [ "Redirecting to $url\n" ] ];
57         }
58
59         # Follow git commit message conventions,
60         # first line is the Subject/title
61         my ($title) = ($txt =~ /\A([^\n]*)/s);
62         $ctx->{-title_html} = ascii_html($title);
63         my $nslash = ($key =~ tr!/!/!);
64         $ctx->{-upfx} = '../../../' . ('../' x $nslash);
65         my $l = PublicInbox::Linkify->new;
66         $l->linkify_1($txt);
67         if ($hl) {
68                 $hl->do_hl_text(\$txt);
69         } else {
70                 $txt = ascii_html($txt);
71         }
72         $txt = '<pre>' . $l->linkify_2($txt) . '</pre>';
73         PublicInbox::WwwStream::html_oneshot($ctx, $code, \$txt);
74 }
75
76 sub _srch_prefix ($$) {
77         my ($srch, $txt) = @_;
78         my $pad = 0;
79         my $htxt = '';
80         my $help = $srch->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 + 8);
92         $htxt =~ s/^/$padding/gms;
93         $htxt =~ s/^$padding(\S+)\0/"        $1".
94                                 (' ' 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 $name = dq_escape($ibx->{name});
175         my $inboxdir = '/path/to/top-level-inbox';
176         my $base_url = $ibx->base_url($ctx->{env});
177         $$txt .= <<EOS;
178 ; Example public-inbox config snippet for a mirror of
179 ; $base_url
180 ; See public-inbox-config(5) manpage for more details:
181 ; https://public-inbox.org/public-inbox-config.html
182 [publicinbox "$name"]
183         inboxdir = $inboxdir
184         ; note: public-inbox before v1.2.0 used `mainrepo' instead of
185         ; `inboxdir', both remain supported after 1.2
186         mainrepo = $inboxdir
187         url = https://example.com/$name/
188         url = http://example.onion/$name/
189 EOS
190         for my $k (qw(address listid infourl watchheader)) {
191                 defined(my $v = $ibx->{$k}) or next;
192                 $$txt .= "\t$k = $_\n" for @$v;
193         }
194         if (my $altid = $ibx->{altid}) {
195                 my $altid_map = $ibx->altid_map;
196                 $$txt .= <<EOF;
197         ; altid DBs may be used to provide numeric article ID lookup from
198         ; old, pre-existing sources.  You can recreate them via curl(1),
199         ; gzip(1), and sqlite3(1) as documented:
200 EOF
201                 for (sort keys %$altid_map) {
202                         $$txt .= "\t;\tcurl -d '' $base_url$_.sql.gz | \\\n" .
203                                 "\t;\tgzip -dc | \\\n" .
204                                 "\t;\tsqlite3 $inboxdir/$_.sqlite3\n";
205                         $$txt .= "\taltid = serial:$_:file=$_.sqlite3\n";
206                 }
207         }
208
209         for my $k (qw(filter newsgroup obfuscate replyto)) {
210                 defined(my $v = $ibx->{$k}) or next;
211                 $$txt .= "\t$k = $v\n";
212         }
213         $$txt .= "\tnntpmirror = $_\n" for (@{$ibx->nntp_url($ctx)});
214         _coderepo_config($ctx, $txt);
215         1;
216 }
217
218 # n.b. this is a perfect candidate for memoization
219 sub extindex_config ($$$) {
220         my ($ctx, $hdr, $txt) = @_;
221         my $ibx = $ctx->{ibx};
222         push @$hdr, 'Content-Disposition', 'inline; filename=extindex.config';
223         my $name = dq_escape($ibx->{name});
224         my $base_url = $ibx->base_url($ctx->{env});
225         $$txt .= <<EOS;
226 ; Example public-inbox config snippet for the external index (extindex) at:
227 ; $base_url
228 ; See public-inbox-config(5)manpage for more details:
229 ; https://public-inbox.org/public-inbox-config.html
230 [extindex "$name"]
231         topdir = /path/to/extindex-topdir
232         url = https://example.com/$name/
233         url = http://example.onion/$name/
234 EOS
235         for my $k (qw(infourl)) {
236                 defined(my $v = $ibx->{$k}) or next;
237                 $$txt .= "\t$k = $v\n";
238         }
239         _coderepo_config($ctx, $txt);
240         1;
241 }
242
243 sub coderepos_raw ($$) {
244         my ($ctx, $top_url) = @_;
245         my $cr = $ctx->{ibx}->{coderepo} // return ();
246         my $cfg = $ctx->{www}->{pi_cfg};
247         my @ret;
248         for my $cr_name (@$cr) {
249                 $ret[0] //= do {
250                         my $thing = $ctx->{ibx}->can('cloneurl') ?
251                                 'public inbox' : 'external index';
252                         <<EOF;
253 Code repositories for project(s) associated with this $thing
254 EOF
255                 };
256                 my $urls = $cfg->get_all("coderepo.$cr_name.cgiturl");
257                 if ($urls) {
258                         for (@$urls) {
259                                 # relative or absolute URL?, prefix relative
260                                 # "foo.git" with appropriate number of "../"
261                                 my $u = m!\A(?:[a-z\+]+:)?//!i ? $_ :
262                                         $top_url.$_;
263                                 $ret[0] .= "\n\t" . prurl($ctx->{env}, $u);
264                         }
265                 } else {
266                         $ret[0] .= qq[\n\t$cr_name.git (no URL configured)];
267                 }
268         }
269         @ret; # may be empty, this sub is called as an arg for join()
270 }
271
272 sub _mirror_help ($$) {
273         my ($ctx, $txt) = @_;
274         my $ibx = $ctx->{ibx};
275         my $base_url = $ibx->base_url($ctx->{env});
276         chop $base_url; # no trailing slash for "git clone"
277         my $dir = (split(m!/!, $base_url))[-1];
278         my %seen = ($base_url => 1);
279         my $top_url = $base_url;
280         $top_url =~ s!/[^/]+\z!/!;
281         $$txt .= "public-inbox mirroring instructions\n\n";
282         if ($ibx->can('cloneurl')) { # PublicInbox::Inbox
283                 $$txt .=
284                   "This public inbox may be cloned and mirrored by anyone:\n";
285                 my @urls;
286                 my $max = $ibx->max_git_epoch;
287                 # TODO: some of these URLs may be too long and we may need to
288                 # do something like code_footer() above, but these are local
289                 # admin-defined
290                 if (defined($max)) { # v2
291                         for my $i (0..$max) {
292                                 # old epochs my be deleted:
293                                 -d "$ibx->{inboxdir}/git/$i.git" or next;
294                                 my $url = "$base_url/$i";
295                                 $seen{$url} = 1;
296                                 push @urls, "$url $dir/git/$i.git";
297                         }
298                         my $nr = scalar(@urls);
299                         if ($nr > 1) {
300                                 $$txt .= "\n\t";
301                                 $$txt .= "# this inbox consists of $nr epochs:";
302                                 $urls[0] .= " # oldest";
303                                 $urls[-1] .= " # newest";
304                         }
305                 } else { # v1
306                         push @urls, $base_url;
307                 }
308                 # FIXME: epoch splits can be different in other repositories,
309                 # use the "cloneurl" file as-is for now:
310                 for my $u (@{$ibx->cloneurl}) {
311                         next if $seen{$u}++;
312                         push @urls, $u;
313                 }
314                 $$txt .= "\n";
315                 $$txt .= join('', map { "\tgit clone --mirror $_\n" } @urls);
316                 if (my $addrs = $ibx->{address}) {
317                         $addrs = join(' ', @$addrs) if ref($addrs) eq 'ARRAY';
318                         my $v = defined $max ? '-V2' : '-V1';
319                         $$txt .= <<EOF;
320
321         # If you have public-inbox 1.1+ installed, you may
322         # initialize and index your mirror using the following commands:
323         public-inbox-init $v $ibx->{name} $dir/ $base_url \\
324                 $addrs
325         public-inbox-index $dir
326 EOF
327                 }
328         } else { # PublicInbox::ExtSearch
329                 $$txt .= <<EOM;
330 This is an external index which is an amalgamation of several public inboxes.
331 Each public inbox needs to be mirrored individually.
332 EOM
333                 my $v = $ctx->{www}->{pi_cfg}->{lc('publicInbox.wwwListing')};
334                 if (($v // '') =~ /\A(?:all|match=domain)\z/) {
335                         $$txt .= <<EOM;
336 A list of them is available at $top_url
337 EOM
338                 }
339         }
340         my $cfg_link = "$base_url/_/text/config/raw";
341         $$txt .= <<EOF;
342
343 Example config snippet for mirrors: $cfg_link
344 EOF
345         if ($ibx->can('nntp_url')) {
346                 my $nntp = $ibx->nntp_url($ctx);
347                 if (scalar @$nntp) {
348                         $$txt .= "\n";
349                         $$txt .= @$nntp == 1 ? 'Newsgroup' : 'Newsgroups are';
350                         $$txt .= ' available over NNTP:';
351                         $$txt .= "\n\t" . join("\n\t", @$nntp) . "\n";
352                 }
353         }
354         if ($$txt =~ m!\b[^:]+://\w+\.onion/!) {
355                 $$txt .= <<EOM
356
357 note: .onion URLs require Tor: https://www.torproject.org/
358
359 EOM
360         }
361         my $code_url = prurl($ctx->{env}, $PublicInbox::WwwStream::CODE_URL);
362         $$txt .= join("\n\n",
363                 coderepos_raw($ctx, $top_url), # may be empty
364                 "AGPL code for this site:\n\tgit clone $code_url");
365         1;
366 }
367
368 sub _default_text ($$$$) {
369         my ($ctx, $key, $hdr, $txt) = @_;
370         if ($key eq 'mirror') {
371                 return _mirror_help($ctx, $txt);
372         } elsif ($key eq 'color') {
373                 return _colors_help($ctx, $txt);
374         } elsif ($key eq 'config') {
375                 return $ctx->{ibx}->can('cloneurl') ?
376                         inbox_config($ctx, $hdr, $txt) :
377                         extindex_config($ctx, $hdr, $txt);
378         }
379
380         return if $key ne 'help'; # TODO more keys?
381
382         my $ibx = $ctx->{ibx};
383         my $base_url = $ibx->base_url($ctx->{env});
384         $$txt .= "public-inbox help for $base_url\n";
385         $$txt .= <<EOF;
386
387 overview
388 --------
389
390     public-inbox uses Message-ID identifiers in URLs.
391     One may look up messages by substituting Message-IDs
392     (without the leading '<' or trailing '>') into the URL.
393     Forward slash ('/') characters in the Message-IDs
394     need to be escaped as "%2F" (without quotes).
395
396     Thus, it is possible to retrieve any message by its
397     Message-ID by going to:
398
399         $base_url<Message-ID>/
400
401         (without the '<' or '>')
402
403     Message-IDs are described at:
404
405         $WIKI_URL/Message-ID
406
407 EOF
408
409         # n.b. we use the Xapian DB for any regeneratable,
410         # order-of-arrival-independent data.
411         my $srch = $ibx->isrch;
412         if ($srch) {
413                 $$txt .= <<EOF;
414 search
415 ------
416
417     This public-inbox has search functionality provided by Xapian.
418
419     It supports typical AND, OR, NOT, '+', '-' queries present
420     in other search engines.
421
422     We also support search prefixes to limit the scope of the
423     search to certain fields.
424
425     Prefixes supported in this installation include:
426
427 EOF
428                 _srch_prefix($srch, $txt);
429
430                 $$txt .= <<EOF;
431
432     Most prefixes are probabilistic, meaning they support stemming
433     and wildcards ('*').  Ranges (such as 'd:') and boolean prefixes
434     do not support stemming or wildcards.
435     The upstream Xapian query parser documentation fully explains
436     the query syntax:
437
438         $QP_URL
439
440 EOF
441         } # $srch
442         my $over = $ibx->over;
443         if ($over) {
444                 $$txt .= <<EOF;
445 message threading
446 -----------------
447
448     Message threading is enabled for this public-inbox,
449     additional endpoints for message threads are available:
450
451     * $base_url<Message-ID>/T/#u
452
453       Loads the thread belonging to the given <Message-ID>
454       in flat chronological order.  The "#u" anchor
455       focuses the browser on the given <Message-ID>.
456
457     * $base_url<Message-ID>/t/#u
458
459       Loads the thread belonging to the given <Message-ID>
460       in threaded order with nesting.  For deep threads,
461       this requires a wide display or horizontal scrolling.
462
463     Both of these HTML endpoints are suitable for offline reading
464     using the thread overview at the bottom of each page.
465
466     Users of feed readers may follow a particular thread using:
467
468     * $base_url<Message-ID>/t.atom
469
470       Which loads the thread in Atom Syndication Standard
471       described at Wikipedia and RFC4287:
472
473         $WIKI_URL/Atom_(standard)
474         https://tools.ietf.org/html/rfc4287
475
476       Atom Threading Extensions (RFC4685) is supported:
477
478         https://tools.ietf.org/html/rfc4685
479
480     Finally, the gzipped mbox for a thread is available for
481     downloading and importing into your favorite mail client:
482
483     * $base_url<Message-ID>/t.mbox.gz
484
485     We use the mboxrd variant of the mbox format described
486     at:
487
488         $WIKI_URL/Mbox
489
490 EOF
491         } # $over
492
493         $$txt .= <<EOF;
494 contact
495 -------
496
497     This help text is maintained by public-inbox developers
498     reachable via plain-text email at: meta\@public-inbox.org
499     Their inbox is archived at: https://public-inbox.org/meta/
500
501 EOF
502         # TODO: support admin contact info in ~/.public-inbox/config
503         1;
504 }
505
506 1;