factored out an urlabs from aggregate and cgi
[ikiwiki] / IkiWiki / CGI.pm
1 #!/usr/bin/perl
2
3 package IkiWiki;
4
5 use warnings;
6 use strict;
7 use IkiWiki;
8 use IkiWiki::UserInfo;
9 use open qw{:utf8 :std};
10 use Encode;
11
12 sub printheader ($) {
13         my $session=shift;
14         
15         if ($ENV{HTTPS} || $config{sslcookie}) {
16                 print $session->header(-charset => 'utf-8',
17                         -cookie => $session->cookie(-httponly => 1, -secure => 1));
18         }
19         else {
20                 print $session->header(-charset => 'utf-8',
21                         -cookie => $session->cookie(-httponly => 1));
22         }
23 }
24
25 sub prepform {
26         my $form=shift;
27         my $buttons=shift;
28         my $session=shift;
29         my $cgi=shift;
30
31         if (exists $hooks{formbuilder}) {
32                 run_hooks(formbuilder => sub {
33                         shift->(form => $form, cgi => $cgi, session => $session,
34                                 buttons => $buttons);
35                 });
36         }
37
38         return $form;
39 }
40
41 sub showform ($$$$;@) {
42         my $form=prepform(@_);
43         shift;
44         my $buttons=shift;
45         my $session=shift;
46         my $cgi=shift;
47
48         printheader($session);
49         print misctemplate($form->title, $form->render(submit => $buttons), @_);
50 }
51
52 # Like showform, but the base url will be set to allow edit previews
53 # that use links relative to the specified page.
54 sub showform_preview ($$$$;@) {
55         my $form=shift;
56         my $buttons=shift;
57         my $session=shift;
58         my $cgi=shift;
59         my %params=@_;
60
61         # The base url needs to be a full URL, and urlto may return a path.
62         my $baseurl = urlabs(urlto($params{page}), $cgi->url);
63
64         showform($form, $buttons, $session, $cgi, @_,
65                 forcebaseurl => $baseurl);
66 }
67
68 sub redirect ($$) {
69         my $q=shift;
70         eval q{use URI};
71         my $url=URI->new(urlabs(shift, $q->url));
72         if (! $config{w3mmode}) {
73                 print $q->redirect($url);
74         }
75         else {
76                 print "Content-type: text/plain\n";
77                 print "W3m-control: GOTO $url\n\n";
78         }
79 }
80
81 sub decode_cgi_utf8 ($) {
82         # decode_form_utf8 method is needed for 5.01
83         if ($] < 5.01) {
84                 my $cgi = shift;
85                 foreach my $f ($cgi->param) {
86                         $cgi->param($f, map { decode_utf8 $_ } $cgi->param($f));
87                 }
88         }
89 }
90
91 sub decode_form_utf8 ($) {
92         if ($] >= 5.01) {
93                 my $form = shift;
94                 foreach my $f ($form->field) {
95                         my @value=map { decode_utf8($_) } $form->field($f);
96                         $form->field(name  => $f,
97                                      value => \@value,
98                                      force => 1,
99                         );
100                 }
101         }
102 }
103
104 # Check if the user is signed in. If not, redirect to the signin form and
105 # save their place to return to later.
106 sub needsignin ($$) {
107         my $q=shift;
108         my $session=shift;
109
110         if (! defined $session->param("name") ||
111             ! userinfo_get($session->param("name"), "regdate")) {
112                 $session->param(postsignin => $ENV{QUERY_STRING});
113                 cgi_signin($q, $session);
114                 cgi_savesession($session);
115                 exit;
116         }
117 }
118
119 sub cgi_signin ($$;$) {
120         my $q=shift;
121         my $session=shift;
122         my $returnhtml=shift;
123
124         decode_cgi_utf8($q);
125         eval q{use CGI::FormBuilder};
126         error($@) if $@;
127         my $form = CGI::FormBuilder->new(
128                 title => "signin",
129                 name => "signin",
130                 charset => "utf-8",
131                 method => 'POST',
132                 required => 'NONE',
133                 javascript => 0,
134                 params => $q,
135                 action => cgiurl(),
136                 header => 0,
137                 template => {type => 'div'},
138                 stylesheet => 1,
139         );
140         my $buttons=["Login"];
141         
142         $form->field(name => "do", type => "hidden", value => "signin",
143                 force => 1);
144         
145         decode_form_utf8($form);
146         run_hooks(formbuilder_setup => sub {
147                 shift->(form => $form, cgi => $q, session => $session,
148                         buttons => $buttons);
149         });
150         decode_form_utf8($form);
151
152         if ($form->submitted) {
153                 $form->validate;
154         }
155
156         if ($returnhtml) {
157                 $form=prepform($form, $buttons, $session, $q);
158                 return $form->render(submit => $buttons);
159         }
160
161         showform($form, $buttons, $session, $q);
162 }
163
164 sub cgi_postsignin ($$) {
165         my $q=shift;
166         my $session=shift;
167         
168         # Continue with whatever was being done before the signin process.
169         if (defined $session->param("postsignin")) {
170                 my $postsignin=CGI->new($session->param("postsignin"));
171                 $session->clear("postsignin");
172                 cgi($postsignin, $session);
173                 cgi_savesession($session);
174                 exit;
175         }
176         else {
177                 if ($config{sslcookie} && ! $q->https()) {
178                         error(gettext("probable misconfiguration: sslcookie is set, but you are attempting to login via http, not https"));
179                 }
180                 else {
181                         error(gettext("login failed, perhaps you need to turn on cookies?"));
182                 }
183         }
184 }
185
186 sub cgi_prefs ($$) {
187         my $q=shift;
188         my $session=shift;
189
190         needsignin($q, $session);
191         decode_cgi_utf8($q);
192         
193         # The session id is stored on the form and checked to
194         # guard against CSRF.
195         my $sid=$q->param('sid');
196         if (! defined $sid) {
197                 $q->delete_all;
198         }
199         elsif ($sid ne $session->id) {
200                 error(gettext("Your login session has expired."));
201         }
202
203         eval q{use CGI::FormBuilder};
204         error($@) if $@;
205         my $form = CGI::FormBuilder->new(
206                 title => "preferences",
207                 name => "preferences",
208                 header => 0,
209                 charset => "utf-8",
210                 method => 'POST',
211                 validate => {
212                         email => 'EMAIL',
213                 },
214                 required => 'NONE',
215                 javascript => 0,
216                 params => $q,
217                 action => cgiurl(),
218                 template => {type => 'div'},
219                 stylesheet => 1,
220                 fieldsets => [
221                         [login => gettext("Login")],
222                         [preferences => gettext("Preferences")],
223                         [admin => gettext("Admin")]
224                 ],
225         );
226         my $buttons=["Save Preferences", "Logout", "Cancel"];
227         
228         decode_form_utf8($form);
229         run_hooks(formbuilder_setup => sub {
230                 shift->(form => $form, cgi => $q, session => $session,
231                         buttons => $buttons);
232         });
233         decode_form_utf8($form);
234         
235         $form->field(name => "do", type => "hidden", value => "prefs",
236                 force => 1);
237         $form->field(name => "sid", type => "hidden", value => $session->id,
238                 force => 1);
239         $form->field(name => "email", size => 50, fieldset => "preferences");
240         
241         my $user_name=$session->param("name");
242
243         if (! $form->submitted) {
244                 $form->field(name => "email", force => 1,
245                         value => userinfo_get($user_name, "email"));
246         }
247         
248         if ($form->submitted eq 'Logout') {
249                 $session->delete();
250                 redirect($q, baseurl(undef));
251                 return;
252         }
253         elsif ($form->submitted eq 'Cancel') {
254                 redirect($q, baseurl(undef));
255                 return;
256         }
257         elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
258                 if (defined $form->field('email')) {
259                         userinfo_set($user_name, 'email', $form->field('email')) ||
260                                 error("failed to set email");
261                 }
262
263                 $form->text(gettext("Preferences saved."));
264         }
265         
266         showform($form, $buttons, $session, $q,
267                 prefsurl => "", # avoid showing the preferences link
268         );
269 }
270
271 sub cgi_custom_failure ($$$) {
272         my $q=shift;
273         my $httpstatus=shift;
274         my $message=shift;
275
276         print $q->header(
277                 -status => $httpstatus,
278                 -charset => 'utf-8',
279         );
280         print $message;
281
282         # Internet Explod^Hrer won't show custom 404 responses
283         # unless they're >= 512 bytes
284         print ' ' x 512;
285
286         exit;
287 }
288
289 sub check_banned ($$) {
290         my $q=shift;
291         my $session=shift;
292
293         my $banned=0;
294         my $name=$session->param("name");
295         if (defined $name && 
296             grep { $name eq $_ } @{$config{banned_users}}) {
297                 $banned=1;
298         }
299
300         foreach my $b (@{$config{banned_users}}) {
301                 if (pagespec_match("", $b,
302                         ip => $session->remote_addr(),
303                         name => defined $name ? $name : "",
304                 )) {
305                         $banned=1;
306                         last;
307                 }
308         }
309
310         if ($banned) {
311                 $session->delete();
312                 cgi_savesession($session);
313                 cgi_custom_failure(
314                         $q, "403 Forbidden",
315                         gettext("You are banned."));
316         }
317 }
318
319 sub cgi_getsession ($) {
320         my $q=shift;
321
322         eval q{use CGI::Session; use HTML::Entities};
323         error($@) if $@;
324         CGI::Session->name("ikiwiki_session_".encode_entities($config{wikiname}));
325         
326         my $oldmask=umask(077);
327         my $session = eval {
328                 CGI::Session->new("driver:DB_File", $q,
329                         { FileName => "$config{wikistatedir}/sessions.db" })
330         };
331         if (! $session || $@) {
332                 error($@." ".CGI::Session->errstr());
333         }
334         
335         umask($oldmask);
336
337         return $session;
338 }
339
340 # To guard against CSRF, the user's session id (sid)
341 # can be stored on a form. This function will check
342 # (for logged in users) that the sid on the form matches
343 # the session id in the cookie.
344 sub checksessionexpiry ($$) {
345         my $q=shift;
346         my $session = shift;
347
348         if (defined $session->param("name")) {
349                 my $sid=$q->param('sid');
350                 if (! defined $sid || $sid ne $session->id) {
351                         error(gettext("Your login session has expired."));
352                 }
353         }
354 }
355
356 sub cgi_savesession ($) {
357         my $session=shift;
358
359         # Force session flush with safe umask.
360         my $oldmask=umask(077);
361         $session->flush;
362         umask($oldmask);
363 }
364
365 sub cgi (;$$) {
366         my $q=shift;
367         my $session=shift;
368
369         eval q{use CGI};
370         error($@) if $@;
371         $CGI::DISABLE_UPLOADS=$config{cgi_disable_uploads};
372
373         if (! $q) {
374                 binmode(STDIN);
375                 $q=CGI->new;
376                 binmode(STDIN, ":utf8");
377         
378                 run_hooks(cgi => sub { shift->($q) });
379         }
380
381         my $do=$q->param('do');
382         if (! defined $do || ! length $do) {
383                 my $error = $q->cgi_error;
384                 if ($error) {
385                         error("Request not processed: $error");
386                 }
387                 else {
388                         error("\"do\" parameter missing");
389                 }
390         }
391
392         # Need to lock the wiki before getting a session.
393         lockwiki();
394         loadindex();
395         
396         if (! $session) {
397                 $session=cgi_getsession($q);
398         }
399         
400         # Auth hooks can sign a user in.
401         if ($do ne 'signin' && ! defined $session->param("name")) {
402                 run_hooks(auth => sub {
403                         shift->($q, $session)
404                 });
405                 if (defined $session->param("name")) {
406                         # Make sure whatever user was authed is in the
407                         # userinfo db.
408                         if (! userinfo_get($session->param("name"), "regdate")) {
409                                 userinfo_setall($session->param("name"), {
410                                         email => "",
411                                         password => "",
412                                         regdate => time,
413                                 }) || error("failed adding user");
414                         }
415                 }
416         }
417         
418         check_banned($q, $session);
419         
420         run_hooks(sessioncgi => sub { shift->($q, $session) });
421
422         if ($do eq 'signin') {
423                 cgi_signin($q, $session);
424                 cgi_savesession($session);
425         }
426         elsif ($do eq 'prefs') {
427                 cgi_prefs($q, $session);
428         }
429         elsif (defined $session->param("postsignin") || $do eq 'postsignin') {
430                 cgi_postsignin($q, $session);
431         }
432         else {
433                 error("unknown do parameter");
434         }
435 }
436
437 # Does not need to be called directly; all errors will go through here.
438 sub cgierror ($) {
439         my $message=shift;
440
441         print "Content-type: text/html\n\n";
442         print misctemplate(gettext("Error"),
443                 "<p class=\"error\">".gettext("Error").": $message</p>");
444         die $@;
445 }
446
447 1