]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/WWW.pm
split out WWW package and CGI/PSGI-specific parts
[public-inbox.git] / lib / PublicInbox / WWW.pm
1 # Copyright (C) 2014, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
3 #
4 # We focus on the lowest common denominators here:
5 # - targeted at text-only console browsers (lynx, w3m, etc..)
6 # - Only basic HTML, CSS only for line-wrapping <pre> text content for GUIs
7 # - No JavaScript, graphics or icons allowed.
8 # - Must not rely on static content
9 # - UTF-8 is only for user-content, 7-bit US-ASCII for us
10 package PublicInbox::WWW;
11 use 5.008;
12 use strict;
13 use warnings;
14 use PublicInbox::Config;
15 use URI::Escape qw(uri_escape_utf8 uri_unescape);
16 our $LISTNAME_RE = qr!\A/([\w\.\-]+)!;
17 our $pi_config;
18 BEGIN {
19         $pi_config = PublicInbox::Config->new;
20 }
21
22 sub run {
23         my ($cgi, $method) = @_;
24         my %ctx;
25         if ($method !~ /\AGET|HEAD\z/) {
26                 return r(405, 'Method Not Allowed');
27         }
28         my $path_info = $cgi->path_info;
29
30         # top-level indices and feeds
31         if ($path_info eq '/') {
32                 r404();
33         } elsif ($path_info =~ m!$LISTNAME_RE\z!o) {
34                 invalid_list(\%ctx, $1) || redirect_list_index(\%ctx, $cgi);
35         } elsif ($path_info =~ m!$LISTNAME_RE(?:/|/index\.html)?\z!o) {
36                 invalid_list(\%ctx, $1) || get_index(\%ctx, $cgi, 0);
37         } elsif ($path_info =~ m!$LISTNAME_RE/atom\.xml\z!o) {
38                 invalid_list(\%ctx, $1) || get_atom(\%ctx, $cgi, 0);
39
40         # single-message pages
41         } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)\.txt\z!o) {
42                 invalid_list_mid(\%ctx, $1, $2) || get_mid_txt(\%ctx, $cgi);
43         } elsif ($path_info =~ m!$LISTNAME_RE/m/(\S+)\.html\z!o) {
44                 invalid_list_mid(\%ctx, $1, $2) || get_mid_html(\%ctx, $cgi);
45
46         # full-message page
47         } elsif ($path_info =~ m!$LISTNAME_RE/f/(\S+)\.html\z!o) {
48                 invalid_list_mid(\%ctx, $1, $2) || get_full_html(\%ctx, $cgi);
49
50         # convenience redirects, order matters
51         } elsif ($path_info =~ m!$LISTNAME_RE/(?:m|f)/(\S+)\z!o) {
52                 invalid_list_mid(\%ctx, $1, $2) || redirect_mid(\%ctx, $cgi);
53
54         } else {
55                 r404();
56         }
57 }
58
59 # for CoW-friendliness, MOOOOO!
60 sub preload {
61         require PublicInbox::Feed;
62         require PublicInbox::View;
63         require Mail::Thread;
64         require Email::MIME;
65         require Digest::SHA;
66         require POSIX;
67         require XML::Atom::SimpleFeed;
68 }
69
70 # private functions below
71
72 sub r404 { r(404, 'Not Found') }
73
74 # simple response for errors
75 sub r { [ $_[0], ['Content-Type' => 'text/plain'], [ join(' ', @_, "\n") ] ] }
76
77 # returns undef if valid, array ref response if invalid
78 sub invalid_list {
79         my ($ctx, $listname) = @_;
80         my $git_dir = $pi_config->get($listname, "mainrepo");
81         if (defined $git_dir) {
82                 $ctx->{git_dir} = $git_dir;
83                 $ctx->{listname} = $listname;
84                 return;
85         }
86         r404();
87 }
88
89 # returns undef if valid, array ref response if invalid
90 sub invalid_list_mid {
91         my ($ctx, $listname, $mid) = @_;
92         my $ret = invalid_list($ctx, $listname, $mid);
93         $ctx->{mid} = uri_unescape($mid) unless $ret;
94         $ret;
95 }
96
97 # /$LISTNAME/atom.xml                       -> Atom feed, includes replies
98 sub get_atom {
99         my ($ctx, $cgi, $top) = @_;
100         require PublicInbox::Feed;
101         [ 200, [ 'Content-Type' => 'application/xml' ],
102           [ PublicInbox::Feed->generate({
103                         git_dir => $ctx->{git_dir},
104                         listname => $ctx->{listname},
105                         pi_config => $pi_config,
106                         cgi => $cgi,
107                         top => $top,
108                 }) ]
109         ];
110 }
111
112 # /$LISTNAME/?r=$GIT_COMMIT                 -> HTML only
113 sub get_index {
114         my ($ctx, $cgi, $top) = @_;
115         require PublicInbox::Feed;
116         [ 200, [ 'Content-Type' => 'text/html; charset=UTF-8' ],
117           [ PublicInbox::Feed->generate_html_index({
118                         git_dir => $ctx->{git_dir},
119                         listname => $ctx->{listname},
120                         pi_config => $pi_config,
121                         cgi => $cgi,
122                         top => $top,
123                 }) ]
124         ];
125 }
126
127 # just returns a string ref for the blob in the current ctx
128 sub mid2blob {
129         my ($ctx) = @_;
130         require Digest::SHA;
131         my $hex = Digest::SHA::sha1_hex($ctx->{mid});
132         $hex =~ /\A([a-f0-9]{2})([a-f0-9]{38})\z/i or
133                         die "BUG: not a SHA-1 hex: $hex";
134
135         my @cmd = ('git', "--git-dir=$ctx->{git_dir}",
136                         qw(cat-file blob), "HEAD:$1/$2");
137         my $cmd = join(' ', @cmd);
138         my $pid = open my $fh, '-|';
139         defined $pid or die "fork failed: $!\n";
140         if ($pid == 0) {
141                 open STDERR, '>', '/dev/null'; # ignore errors
142                 exec @cmd or die "exec failed: $!\n";
143         } else {
144                 my $blob = eval { local $/; <$fh> };
145                 close $fh;
146                 $? == 0 ? \$blob : undef;
147         }
148 }
149
150 # /$LISTNAME/m/$MESSAGE_ID.txt                    -> raw original
151 sub get_mid_txt {
152         my ($ctx, $cgi) = @_;
153         my $x = mid2blob($ctx);
154         $x ? [ 200, [ 'Content-Type' => 'text/plain' ], [ $$x ] ] : r404();
155 }
156
157 # /$LISTNAME/m/$MESSAGE_ID.html                   -> HTML content (short quotes)
158 sub get_mid_html {
159         my ($ctx, $cgi) = @_;
160         my $x = mid2blob($ctx);
161         return r404() unless $x;
162
163         require PublicInbox::View;
164         my $mid_href = PublicInbox::Hval::ascii_html(
165                                                 uri_escape_utf8($ctx->{mid}));
166         my $pfx = "../f/$mid_href.html";
167         require Email::MIME;
168         [ 200, [ 'Content-Type' => 'text/html; charset=UTF-8' ],
169                 [ PublicInbox::View->as_html(Email::MIME->new($$x), $pfx) ] ];
170 }
171
172 # /$LISTNAME/f/$MESSAGE_ID.html                   -> HTML content (fullquotes)
173 sub get_full_html {
174         my ($ctx, $cgi) = @_;
175         my $x = mid2blob($ctx);
176         return r404() unless $x;
177         require PublicInbox::View;
178         require Email::MIME;
179         [ 200, [ 'Content-Type' => 'text/html' ],
180                 [ PublicInbox::View->as_html(Email::MIME->new($$x))] ];
181 }
182
183 sub self_url {
184         my ($cgi) = @_;
185         ref($cgi) eq 'CGI' ? $cgi->self_url : $cgi->uri->as_string;
186 }
187
188 sub redirect_list_index {
189         my ($ctx, $cgi) = @_;
190         do_redirect(self_url($cgi) . "/");
191 }
192
193 sub redirect_mid {
194         my ($ctx, $cgi) = @_;
195         my $url = self_url($cgi);
196         $url =~ s!/f/!/m/!;
197         do_redirect($url . '.html');
198 }
199
200 sub do_redirect {
201         my ($url) = @_;
202         [ 301,
203           [ Location => $url, 'Content-Type' => 'text/plain' ],
204           [ "Redirecting to $url\n" ]
205         ]
206 }
207
208 1;