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