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