po: Add failing test for Debian bug #911356
[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 = ("perl", "-I".getcwd, qw(./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 $goto = delete $args{goto};
116         my %defaults = (
117                 SCRIPT_NAME     => '/cgi-bin/ikiwiki.cgi',
118                 HTTP_HOST       => 'example.com',
119         );
120         if (defined $goto) {
121                 $defaults{REQUEST_METHOD} = 'GET';
122                 $defaults{QUERY_STRING} = 'do=goto&page=a/b/c';
123         }
124         elsif (defined $is_preview) {
125                 $defaults{REQUEST_METHOD} = 'POST';
126                 $in = 'do=edit&page=a/b/c&Preview';
127                 $defaults{CONTENT_LENGTH} = length $in;
128         } else {
129                 $defaults{REQUEST_METHOD} = 'GET';
130                 $defaults{QUERY_STRING} = 'do=prefs';
131         }
132         if (defined $is_https) {
133                 $defaults{SERVER_PORT} = '443';
134                 $defaults{HTTPS} = 'on';
135         } else {
136                 $defaults{SERVER_PORT} = '80';
137         }
138         my %envvars = (
139                 %defaults,
140                 %args,
141         );
142         run(["./t/tmp/ikiwiki.cgi"], \$in, \$out, init => sub {
143                 map {
144                         $ENV{$_} = $envvars{$_}
145                 } keys(%envvars);
146         });
147
148         return $out;
149 }
150
151 sub check_goto {
152         my $expected = shift;
153         my $redirect = run_cgi(goto => 1, @_);
154         ok($redirect =~ m/^Status:\s*302\s+/m);
155         ok($redirect =~ m/^Location:\s*(\S*)\r?\n/m);
156         my $location = $1;
157         like($location, $expected);
158 }
159
160 sub test_startup {
161         ok(! system("rm -rf t/tmp"));
162         ok(! system("mkdir t/tmp"));
163
164         write_old_file("a.mdwn", "A");
165         write_old_file("a/b.mdwn", "B");
166         write_old_file("a/b/c.mdwn",
167         "* A: [[a]]\n".
168         "* B: [[b]]\n".
169         "* E: [[a/d/e]]\n");
170         write_old_file("a/d.mdwn", "D");
171         write_old_file("a/d/e.mdwn", "E");
172 }
173
174 sub test_site1_perfectly_ordinary_ikiwiki {
175         diag("test_site1_perfectly_ordinary_ikiwiki");
176         write_setup_file(
177                 url     => "http://example.com/wiki/",
178                 cgiurl  => "http://example.com/cgi-bin/ikiwiki.cgi",
179         );
180         thoroughly_rebuild();
181         check_cgi_mode_bits();
182         # url and cgiurl are on the same host so the cgiurl is host-relative
183         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
184         check_goto(qr{^http://example\.com/wiki/a/b/c/$});
185         my %bits = parse_cgi_content(run_cgi());
186         like($bits{basehref}, qr{^(?:(?:http:)?//example\.com)?/wiki/$});
187         like($bits{stylehref}, qr{^(?:(?:http:)?//example.com)?/wiki/style.css$});
188         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
189         like($bits{cgihref}, qr{^(?:(?:http:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
190
191         # when accessed via HTTPS, links are secure
192         %bits = parse_cgi_content(run_cgi(is_https => 1));
193         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
194         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
195         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
196         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
197         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, is_https => 1);
198
199         # when accessed via a different hostname, links stay on that host
200         %bits = parse_cgi_content(run_cgi(HTTP_HOST => 'staging.example.net'));
201         like($bits{basehref}, qr{^(?:(?:http:)?//staging\.example\.net)?/wiki/$});
202         like($bits{stylehref}, qr{^(?:(?:http:)?//staging.example.net)?/wiki/style.css$});
203         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
204         like($bits{cgihref}, qr{^(?:(?:http:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
205         TODO: {
206         local $TODO = "hostname should be copied to redirects' Location";
207         check_goto(qr{^https://staging\.example\.net/wiki/a/b/c/$}, is_https => 1);
208         }
209
210         # previewing a page
211         %bits = parse_cgi_content(run_cgi(is_preview => 1));
212         like($bits{basehref}, qr{^(?:(?:http:)?//example\.com)?/wiki/a/b/c/$});
213         like($bits{stylehref}, qr{^(?:(?:http:)?//example.com)?/wiki/style.css$});
214         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
215         like($bits{cgihref}, qr{^(?:(?:http:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
216 }
217
218 sub test_site2_static_content_and_cgi_on_different_servers {
219         diag("test_site2_static_content_and_cgi_on_different_servers");
220         write_setup_file(
221                 url     => "http://static.example.com/",
222                 cgiurl  => "http://cgi.example.com/ikiwiki.cgi",
223         );
224         thoroughly_rebuild();
225         check_cgi_mode_bits();
226         # url and cgiurl are not on the same host so the cgiurl has to be
227         # protocol-relative or absolute
228         check_generated_content(qr{<a[^>]+href="(?:http:)?//cgi.example.com/ikiwiki.cgi\?do=prefs"});
229         check_goto(qr{^http://static\.example\.com/a/b/c/$});
230
231         my %bits = parse_cgi_content(run_cgi(SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'cgi.example.com'));
232         like($bits{basehref}, qr{^(?:(?:http:)?//static.example.com)?/$});
233         like($bits{stylehref}, qr{^(?:(?:http:)?//static.example.com)?/style.css$});
234         like($bits{tophref}, qr{^(?:http:)?//static.example.com/$});
235         like($bits{cgihref}, qr{^(?:(?:http:)?//cgi.example.com)?/ikiwiki.cgi$});
236
237         # when accessed via HTTPS, links are secure
238         %bits = parse_cgi_content(run_cgi(is_https => 1, SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'cgi.example.com'));
239         like($bits{basehref}, qr{^(?:https:)?//static\.example\.com/$});
240         like($bits{stylehref}, qr{^(?:(?:https:)?//static.example.com)?/style.css$});
241         like($bits{tophref}, qr{^(?:https:)?//static.example.com/$});
242         like($bits{cgihref}, qr{^(?:(?:https:)?//cgi.example.com)?/ikiwiki.cgi$});
243         check_goto(qr{^https://static\.example\.com/a/b/c/$}, is_https => 1,
244                 HTTP_HOST => 'cgi.example.com', SCRIPT_NAME => '/ikiwiki.cgi');
245
246         # when accessed via a different hostname, links to the CGI (only) should
247         # stay on that host?
248         %bits = parse_cgi_content(run_cgi(is_preview => 1, SCRIPT_NAME => '/ikiwiki.cgi', HTTP_HOST => 'staging.example.net'));
249         like($bits{basehref}, qr{^(?:http:)?//static\.example\.com/a/b/c/$});
250         like($bits{stylehref}, qr{^(?:(?:http:)?//static.example.com|\.\./\.\./\.\.)/style.css$});
251         like($bits{tophref}, qr{^(?:(?:http:)?//static.example.com|\.\./\.\./\.\.)/$});
252         like($bits{cgihref}, qr{^(?:(?:http:)?//(?:staging\.example\.net|cgi\.example\.com))?/ikiwiki.cgi$});
253         TODO: {
254         local $TODO = "use self-referential CGI URL?";
255         like($bits{cgihref}, qr{^(?:(?:http:)?//staging.example.net)?/ikiwiki.cgi$});
256         }
257         check_goto(qr{^https://static\.example\.com/a/b/c/$}, is_https => 1,
258                 HTTP_HOST => 'staging.example.net', SCRIPT_NAME => '/ikiwiki.cgi');
259 }
260
261 sub test_site3_we_specifically_want_everything_to_be_secure {
262         diag("test_site3_we_specifically_want_everything_to_be_secure");
263         write_setup_file(
264                 url     => "https://example.com/wiki/",
265                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
266         );
267         thoroughly_rebuild();
268         check_cgi_mode_bits();
269         # url and cgiurl are on the same host so the cgiurl is host-relative
270         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
271
272         # when accessed via HTTPS, links are secure
273         my %bits = parse_cgi_content(run_cgi(is_https => 1));
274         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
275         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
276         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
277         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
278         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, is_https => 1);
279
280         # when not accessed via HTTPS, links should still be secure
281         # (but if this happens, that's a sign of web server misconfiguration)
282         %bits = parse_cgi_content(run_cgi());
283         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
284         TODO: {
285         local $TODO = "treat https in configured url, cgiurl as required?";
286         is($bits{basehref}, "https://example.com/wiki/");
287         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
288         }
289         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
290         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, is_https => 0);
291
292         # when accessed via a different hostname, links stay on that host
293         %bits = parse_cgi_content(run_cgi(is_https => 1, HTTP_HOST => 'staging.example.net'));
294         like($bits{basehref}, qr{^(?:(?:https:)?//staging\.example\.net)?/wiki/$});
295         like($bits{stylehref}, qr{^(?:(?:https:)?//staging.example.net)?/wiki/style.css$});
296         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
297         like($bits{cgihref}, qr{^(?:(?:https:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
298         check_goto(qr{^https://staging\.example\.net/wiki/a/b/c/$}, is_https => 1,
299                 HTTP_HOST => 'staging.example.net');
300
301         # previewing a page
302         %bits = parse_cgi_content(run_cgi(is_preview => 1, is_https => 1));
303         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/a/b/c/$});
304         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
305         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
306         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
307 }
308
309 sub test_site4_cgi_is_secure_static_content_doesnt_have_to_be {
310         diag("test_site4_cgi_is_secure_static_content_doesnt_have_to_be");
311         # (NetBSD wiki)
312         write_setup_file(
313                 url     => "http://example.com/wiki/",
314                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
315         );
316         thoroughly_rebuild();
317         check_cgi_mode_bits();
318         # url and cgiurl are on the same host but different schemes
319         check_generated_content(qr{<a[^>]+href="https://example.com/cgi-bin/ikiwiki.cgi\?do=prefs"});
320
321         # when accessed via HTTPS, links are secure (to avoid mixed-content)
322         my %bits = parse_cgi_content(run_cgi(is_https => 1));
323         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
324         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
325         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
326         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
327         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, is_https => 1);
328
329         # FIXME: when not accessed via HTTPS, should the static content be
330         # forced to https anyway? For now we accept either
331         %bits = parse_cgi_content(run_cgi());
332         like($bits{basehref}, qr{^(?:(?:https?)?://example\.com)?/wiki/$});
333         like($bits{stylehref}, qr{^(?:(?:https?:)?//example.com)?/wiki/style.css$});
334         like($bits{tophref}, qr{^(?:(?:https?://example.com)?/wiki|\.)/$});
335         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
336         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, is_https => 0);
337
338         # when accessed via a different hostname, links stay on that host
339         %bits = parse_cgi_content(run_cgi(is_https => 1, HTTP_HOST => 'staging.example.net'));
340         # because the static and dynamic stuff is on the same server, we assume that
341         # both are also on the staging server
342         like($bits{basehref}, qr{^(?:(?:https:)?//staging\.example\.net)?/wiki/$});
343         like($bits{stylehref}, qr{^(?:(?:https:)?//staging.example.net)?/wiki/style.css$});
344         like($bits{tophref}, qr{^(?:(?:(?:https:)?//staging.example.net)?/wiki|\.)/$});
345         like($bits{cgihref}, qr{^(?:(?:https:)?//(?:staging\.example\.net|example\.com))?/cgi-bin/ikiwiki.cgi$});
346         TODO: {
347         local $TODO = "this should really point back to itself but currently points to example.com";
348         like($bits{cgihref}, qr{^(?:(?:https:)?//staging.example.net)?/cgi-bin/ikiwiki.cgi$});
349         }
350         check_goto(qr{^https://staging\.example\.net/wiki/a/b/c/$}, is_https => 1,
351                 HTTP_HOST => 'staging.example.net');
352
353         # previewing a page
354         %bits = parse_cgi_content(run_cgi(is_preview => 1, is_https => 1));
355         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/a/b/c/$});
356         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
357         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
358         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
359 }
360
361 sub test_site5_w3mmode {
362         diag("test_site5_w3mmode");
363         # as documented in [[w3mmode]]
364         write_setup_file(
365                 url     => undef,
366                 cgiurl  => "ikiwiki.cgi",
367                 w3mmode => 1,
368         );
369         thoroughly_rebuild();
370         check_cgi_mode_bits();
371         # FIXME: does /$LIB/ikiwiki-w3m.cgi work under w3m?
372         check_generated_content(qr{<a[^>]+href="(?:file://)?/\$LIB/ikiwiki-w3m.cgi/ikiwiki.cgi\?do=prefs"});
373
374         my %bits = parse_cgi_content(run_cgi(PATH_INFO => '/ikiwiki.cgi', SCRIPT_NAME => '/cgi-bin/ikiwiki-w3m.cgi'));
375         my $pwd = getcwd();
376         like($bits{tophref}, qr{^(?:\Q$pwd\E/t/tmp/out|\.)/$});
377         like($bits{cgihref}, qr{^(?:file://)?/\$LIB/ikiwiki-w3m.cgi/ikiwiki.cgi$});
378         like($bits{basehref}, qr{^(?:(?:file:)?//)?\Q$pwd\E/t/tmp/out/$});
379         like($bits{stylehref}, qr{^(?:(?:(?:file:)?//)?\Q$pwd\E/t/tmp/out|\.)/style.css$});
380
381         my $redirect = run_cgi(goto => 1, PATH_INFO => '/ikiwiki.cgi',
382                 SCRIPT_NAME => '/cgi-bin/ikiwiki-w3m.cgi');
383         like($redirect, qr{^Content-type: text/plain\r?\n}m);
384         like($redirect, qr{^W3m-control: GOTO (?:file://)?\Q$pwd\E/t/tmp/out/a/b/c/\r?\n}m);
385 }
386
387 sub test_site6_behind_reverse_proxy {
388         diag("test_site6_behind_reverse_proxy");
389         write_setup_file(
390                 url     => "https://example.com/wiki/",
391                 cgiurl  => "https://example.com/cgi-bin/ikiwiki.cgi",
392                 reverse_proxy => 1,
393         );
394         thoroughly_rebuild();
395         check_cgi_mode_bits();
396         # url and cgiurl are on the same host so the cgiurl is host-relative
397         check_generated_content(qr{<a[^>]+href="/cgi-bin/ikiwiki.cgi\?do=prefs"});
398
399         # because we are behind a reverse-proxy we must assume that
400         # we're being accessed by the configured cgiurl
401         my %bits = parse_cgi_content(run_cgi(HTTP_HOST => 'localhost'));
402         like($bits{tophref}, qr{^(?:/wiki|\.)/$});
403         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
404         like($bits{basehref}, qr{^(?:(?:https:)?//example\.com)?/wiki/$});
405         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
406         check_goto(qr{^https://example\.com/wiki/a/b/c/$}, HTTP_HOST => 'localhost');
407
408         # previewing a page
409         %bits = parse_cgi_content(run_cgi(is_preview => 1, HTTP_HOST => 'localhost'));
410         like($bits{tophref}, qr{^(?:/wiki|\.\./\.\./\.\.)/$});
411         like($bits{cgihref}, qr{^(?:(?:https:)?//example.com)?/cgi-bin/ikiwiki.cgi$});
412         like($bits{basehref}, qr{^(?:(?:https)?://example\.com)?/wiki/a/b/c/$});
413         like($bits{stylehref}, qr{^(?:(?:https:)?//example.com)?/wiki/style.css$});
414 }
415
416 test_startup();
417
418 test_site1_perfectly_ordinary_ikiwiki();
419 test_site2_static_content_and_cgi_on_different_servers();
420 test_site3_we_specifically_want_everything_to_be_secure();
421 test_site4_cgi_is_secure_static_content_doesnt_have_to_be();
422 test_site5_w3mmode();
423 test_site6_behind_reverse_proxy();
424
425 done_testing();