Wrapper: allocate new environment dynamically
[ikiwiki] / t / relativity.t
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4
5 use Test::More;
6 plan(skip_all => "IPC::Run not available")
7         unless eval q{
8                 use IPC::Run qw(run);
9                 1;
10         };
11
12 use IkiWiki;
13
14 use Cwd qw(getcwd);
15 use Errno qw(ENOENT);
16
17 # Black-box (ish) test for relative linking between CGI and static content
18
19 my $installed = $ENV{INSTALLED_TESTS};
20
21 my @command;
22 if ($installed) {
23         @command = qw(ikiwiki);
24 }
25 else {
26         ok(! system("make -s ikiwiki.out"));
27         @command = qw(perl -I. ./ikiwiki.out
28                 --underlaydir=underlays/basewiki
29                 --set underlaydirbase=underlays
30                 --templatedir=templates);
31 }
32
33 sub parse_cgi_content {
34         my $content = shift;
35         my %bits;
36         if ($content =~ qr{<base href="([^"]+)" */>}) {
37                 $bits{basehref} = $1;
38         }
39         if ($content =~ qr{href="([^"]+/style.css)"}) {
40                 $bits{stylehref} = $1;
41         }
42         if ($content =~ qr{class="parentlinks">\s+<a href="([^"]+)">this is the name of my wiki</a>/}s) {
43                 $bits{tophref} = $1;
44         }
45         if ($content =~ qr{<a[^>]+href="([^"]+)\?do=prefs"}) {
46                 $bits{cgihref} = $1;
47         }
48         return %bits;
49 }
50
51 sub write_old_file {
52         my $name = shift;
53         my $content = shift;
54
55         writefile($name, "t/tmp/in", $content);
56         ok(utime(333333333, 333333333, "t/tmp/in/$name"));
57 }
58
59 sub write_setup_file {
60         my (%args) = @_;
61         my $urlline = defined $args{url} ? "url: $args{url}" : "";
62         my $w3mmodeline = defined $args{w3mmode} ? "w3mmode: $args{w3mmode}" : "";
63         my $reverseproxyline = defined $args{reverse_proxy} ? "reverse_proxy: $args{reverse_proxy}" : "";
64
65         writefile("test.setup", "t/tmp", <<EOF
66 # IkiWiki::Setup::Yaml - YAML formatted setup file
67 wikiname: this is the name of my wiki
68 srcdir: t/tmp/in
69 destdir: t/tmp/out
70 $urlline
71 cgiurl: $args{cgiurl}
72 $w3mmodeline
73 cgi_wrapper: t/tmp/ikiwiki.cgi
74 cgi_wrappermode: 0754
75 # make it easier to test previewing
76 add_plugins:
77 - anonok
78 anonok_pagespec: "*"
79 $reverseproxyline
80 ENV: { 'PERL5LIB': 'blib/lib:blib/arch' }
81 EOF
82         );
83 }
84
85 sub thoroughly_rebuild {
86         ok(unlink("t/tmp/ikiwiki.cgi") || $!{ENOENT});
87         ok(! system(@command, qw(--setup t/tmp/test.setup --rebuild --wrappers)));
88 }
89
90 sub check_cgi_mode_bits {
91         my (undef, undef, $mode, undef, undef,
92                 undef, undef, undef, undef, undef,
93                 undef, undef, undef) = stat("t/tmp/ikiwiki.cgi");
94         is($mode & 07777, 0754);
95 }
96
97 sub check_generated_content {
98         my $cgiurl_regex = shift;
99         ok(-e "t/tmp/out/a/b/c/index.html");
100         my $content = readfile("t/tmp/out/a/b/c/index.html");
101         # no <base> on static HTML
102         unlike($content, qr{<base\W});
103         like($content, $cgiurl_regex);
104         # cross-links between static pages are relative
105         like($content, qr{<li>A: <a href="../../">a</a></li>});
106         like($content, qr{<li>B: <a href="../">b</a></li>});
107         like($content, qr{<li>E: <a href="../../d/e/">e</a></li>});
108 }
109
110 sub run_cgi {
111         my (%args) = @_;
112         my ($in, $out);
113         my $is_preview = delete $args{is_preview};
114         my $is_https = delete $args{is_https};
115         my %defaults = (
116                 SCRIPT_NAME     => '/cgi-bin/ikiwiki.cgi',
117                 HTTP_HOST       => 'example.com',
118         );
119         if (defined $is_preview) {
120                 $defaults{REQUEST_METHOD} = 'POST';
121                 $in = 'do=edit&page=a/b/c&Preview';
122                 $defaults{CONTENT_LENGTH} = length $in;
123         } else {
124                 $defaults{REQUEST_METHOD} = 'GET';
125                 $defaults{QUERY_STRING} = 'do=prefs';
126         }
127         if (defined $is_https) {
128                 $defaults{SERVER_PORT} = '443';
129                 $defaults{HTTPS} = 'on';
130         } else {
131                 $defaults{SERVER_PORT} = '80';
132         }
133         my %envvars = (
134                 %defaults,
135                 %args,
136         );
137         run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
138                 map {
139                         $ENV{$_} = $envvars{$_}
140                 } keys(%envvars);
141         });
142
143         return $out;
144 }
145
146 sub test_startup {
147         ok(! system("rm -rf t/tmp"));
148         ok(! system("mkdir t/tmp"));
149
150         write_old_file("a.mdwn", "A");
151         write_old_file("a/b.mdwn", "B");
152         write_old_file("a/b/c.mdwn",
153         "* A: [[a]]\n".
154         "* B: [[b]]\n".
155         "* E: [[a/d/e]]\n");
156         write_old_file("a/d.mdwn", "D");
157         write_old_file("a/d/e.mdwn", "E");
158 }
159
160 sub test_site1_perfectly_ordinary_ikiwiki {
161         write_setup_file(
162                 url     => "http://example.com/wiki/",
163                 cgiurl  => "http://example.com/cgi-bin/ikiwiki.cgi",
164         );
165         thoroughly_rebuild();
166         check_cgi_mode_bits();
167         # url and cgiurl are on the same host so the cgiurl is host-relative
168         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
169         my %bits = parse_cgi_content(run_cgi());
170         like($bits{basehref}, qr{^(?:(?:http:)?//example\.com)?/wiki/$});
171         like($bits{stylehref}, qr{^(?:(?:http:)?//example.com)?/wiki/style.css$});
172         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
173         like($bits{cgihref}, qr{^(?:(?:http:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
174
175         # when accessed via HTTPS, links are secure
176         %bits = parse_cgi_content(run_cgi(is_https => 1));
177         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
178         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
179         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
180         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
181
182         # when accessed via a different hostname, links stay on that host
183         %bits = parse_cgi_content(run_cgi(HTTP_HOST => 'staging.example.net'));
184         like($bits{basehref}, qr{^(?:(?:http:)?//staging\.example\.net)?/wiki/$});
185         like($bits{stylehref}, qr{^(?:(?:http:)?//staging.example.net)?/wiki/style.css$});
186         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
187         like($bits{cgihref}, qr{^(?:(?:http:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
188
189         # previewing a page
190         %bits = parse_cgi_content(run_cgi(is_preview => 1));
191         like($bits{basehref}, qr{^(?:(?:http:)?//example\.com)?/wiki/a/b/c/$});
192         like($bits{stylehref}, qr{^(?:(?:http:)?//example.com)?/wiki/style.css$});
193         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
194         like($bits{cgihref}, qr{^(?:(?:http:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
195 }
196
197 sub test_site2_static_content_and_cgi_on_different_servers {
198         write_setup_file(
199                 url     => "http://static.example.com/",
200                 cgiurl  => "http://cgi.example.com/ikiwiki.cgi",
201         );
202         thoroughly_rebuild();
203         check_cgi_mode_bits();
204         # url and cgiurl are not on the same host so the cgiurl has to be
205         # protocol-relative or absolute
206         check_generated_content(qr{<a[^>]+href="(?:http:)?//cgi.example.com/ikiwiki.cgi\?do=prefs"});
207
208         my %bits = parse_cgi_content(run_cgi(SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'cgi.example.com'));
209         like($bits{basehref}, qr{^(?:(?:http:)?//static.example.com)?/$});
210         like($bits{stylehref}, qr{^(?:(?:http:)?//static.example.com)?/style.css$});
211         like($bits{tophref}, qr{^(?:http:)?//static.example.com/$});
212         like($bits{cgihref}, qr{^(?:(?:http:)?//cgi.example.com)?/ikiwiki.cgi$});
213
214         # when accessed via HTTPS, links are secure
215         %bits = parse_cgi_content(run_cgi(is_https => 1, SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'cgi.example.com'));
216         like($bits{basehref}, qr{^(?:https:)?//static\.example\.com/$});
217         like($bits{stylehref}, qr{^(?:(?:https:)?//static.example.com)?/style.css$});
218         like($bits{tophref}, qr{^(?:https:)?//static.example.com/$});
219         like($bits{cgihref}, qr{^(?:(?:https:)?//cgi.example.com)?/ikiwiki.cgi$});
220
221         # when accessed via a different hostname, links to the CGI (only) should
222         # stay on that host?
223         %bits = parse_cgi_content(run_cgi(is_preview => 1, SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'staging.example.net'));
224         like($bits{basehref}, qr{^(?:http:)?//static\.example\.com/a/b/c/$});
225         like($bits{stylehref}, qr{^(?:(?:http:)?//static.example.com|\.\./\.\./\.\.)/style.css$});
226         like($bits{tophref}, qr{^(?:(?:http:)?//static.example.com|\.\./\.\./\.\.)/$});
227         like($bits{cgihref}, qr{^(?:(?:http:)?//(?:staging\.example\.net|cgi\.example\.com))?/ikiwiki.cgi$});
228         TODO: {
229         local $TODO = "use self-referential CGI URL?";
230         like($bits{cgihref}, qr{^(?:(?:http:)?//staging.example.net)?/ikiwiki.cgi$});
231         }
232 }
233
234 sub test_site3_we_specifically_want_everything_to_be_secure {
235         write_setup_file(
236                 url     => "https://example.com/wiki/",
237                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
238         );
239         thoroughly_rebuild();
240         check_cgi_mode_bits();
241         # url and cgiurl are on the same host so the cgiurl is host-relative
242         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
243
244         # when accessed via HTTPS, links are secure
245         my %bits = parse_cgi_content(run_cgi(is_https => 1));
246         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
247         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
248         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
249         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
250
251         # when not accessed via HTTPS, links should still be secure
252         # (but if this happens, that's a sign of web server misconfiguration)
253         %bits = parse_cgi_content(run_cgi());
254         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
255         TODO: {
256         local $TODO = "treat https in configured url, cgiurl as required?";
257         is($bits{basehref}, "https://example.com/wiki/");
258         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
259         }
260         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
261
262         # when accessed via a different hostname, links stay on that host
263         %bits = parse_cgi_content(run_cgi(is_https => 1, HTTP_HOST => 'staging.example.net'));
264         like($bits{basehref}, qr{^(?:(?:https:)?//staging\.example\.net)?/wiki/$});
265         like($bits{stylehref}, qr{^(?:(?:https:)?//staging.example.net)?/wiki/style.css$});
266         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
267         like($bits{cgihref}, qr{^(?:(?:https:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
268
269         # previewing a page
270         %bits = parse_cgi_content(run_cgi(is_preview => 1, is_https => 1));
271         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/a/b/c/$});
272         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
273         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
274         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
275 }
276
277 sub test_site4_cgi_is_secure_static_content_doesnt_have_to_be {
278         # (NetBSD wiki)
279         write_setup_file(
280                 url     => "http://example.com/wiki/",
281                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
282         );
283         thoroughly_rebuild();
284         check_cgi_mode_bits();
285         # url and cgiurl are on the same host but different schemes
286         check_generated_content(qr{<a[^>]+href="https://example.com/cgi-bin/ikiwiki.cgi\?do=prefs"});
287
288         # when accessed via HTTPS, links are secure (to avoid mixed-content)
289         my %bits = parse_cgi_content(run_cgi(is_https => 1));
290         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
291         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
292         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
293         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
294
295         # FIXME: when not accessed via HTTPS, should the static content be
296         # forced to https anyway? For now we accept either
297         %bits = parse_cgi_content(run_cgi());
298         like($bits{basehref}, qr{^(?:(?:https?)?://example\.com)?/wiki/$});
299         like($bits{stylehref}, qr{^(?:(?:https?:)?//example.com)?/wiki/style.css$});
300         like($bits{tophref}, qr{^(?:(?:https?://example.com)?/wiki|\.)/$});
301         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
302
303         # when accessed via a different hostname, links stay on that host
304         %bits = parse_cgi_content(run_cgi(is_https => 1, HTTP_HOST => 'staging.example.net'));
305         # because the static and dynamic stuff is on the same server, we assume that
306         # both are also on the staging server
307         like($bits{basehref}, qr{^(?:(?:https:)?//staging\.example\.net)?/wiki/$});
308         like($bits{stylehref}, qr{^(?:(?:https:)?//staging.example.net)?/wiki/style.css$});
309         like($bits{tophref}, qr{^(?:(?:(?:https:)?//staging.example.net)?/wiki|\.)/$});
310         like($bits{cgihref}, qr{^(?:(?:https:)?//(?:staging\.example\.net|example\.com))?/cgi-bin/ikiwiki.cgi$});
311         TODO: {
312         local $TODO = "this should really point back to itself but currently points to example.com";
313         like($bits{cgihref}, qr{^(?:(?:https:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
314         }
315
316         # previewing a page
317         %bits = parse_cgi_content(run_cgi(is_preview => 1, is_https => 1));
318         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/a/b/c/$});
319         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
320         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
321         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
322 }
323
324 sub test_site5_w3mmode {
325         # as documented in [[w3mmode]]
326         write_setup_file(
327                 url     => undef,
328                 cgiurl  => "ikiwiki.cgi",
329                 w3mmode => 1,
330         );
331         thoroughly_rebuild();
332         check_cgi_mode_bits();
333         # FIXME: does /$LIB/ikiwiki-w3m.cgi work under w3m?
334         check_generated_content(qr{<a[^>]+href="(?:file://)?/\$LIB/ikiwiki-w3m.cgi/ikiwiki.cgi\?do=prefs"});
335
336         my %bits = parse_cgi_content(run_cgi(PATH_INFO => '/ikiwiki.cgi', SCRIPT_NAME => '/cgi-bin/ikiwiki-w3m.cgi'));
337         my $pwd = getcwd();
338         like($bits{tophref}, qr{^(?:\Q$pwd\E/t/tmp/out|\.)/$});
339         like($bits{cgihref}, qr{^(?:file://)?/\$LIB/ikiwiki-w3m.cgi/ikiwiki.cgi$});
340         like($bits{basehref}, qr{^(?:(?:file:)?//)?\Q$pwd\E/t/tmp/out/$});
341         like($bits{stylehref}, qr{^(?:(?:(?:file:)?//)?\Q$pwd\E/t/tmp/out|\.)/style.css$});
342 }
343
344 sub test_site6_behind_reverse_proxy {
345         write_setup_file(
346                 url     => "https://example.com/wiki/",
347                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
348                 reverse_proxy => 1,
349         );
350         thoroughly_rebuild();
351         check_cgi_mode_bits();
352         # url and cgiurl are on the same host so the cgiurl is host-relative
353         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
354
355         # because we are behind a reverse-proxy we must assume that
356         # we're being accessed by the configured cgiurl
357         my %bits = parse_cgi_content(run_cgi(HTTP_HOST => 'localhost'));
358         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
359         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
360         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
361         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
362
363         # previewing a page
364         %bits = parse_cgi_content(run_cgi(is_preview => 1, HTTP_HOST => 'localhost'));
365         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
366         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
367         like($bits{basehref}, qr{^(?:(?:https)?://example\.com)?/wiki/a/b/c/$});
368         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
369 }
370
371 test_startup();
372
373 test_site1_perfectly_ordinary_ikiwiki();
374 test_site2_static_content_and_cgi_on_different_servers();
375 test_site3_we_specifically_want_everything_to_be_secure();
376 test_site4_cgi_is_secure_static_content_doesnt_have_to_be();
377 test_site5_w3mmode();
378 test_site6_behind_reverse_proxy();
379
380 done_testing();