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