web commit by http://sabr.myopenid.com/: poll vote (Accept only OpenID for logins)
[ikiwiki.git] / IkiWiki / CGI.pm
1 #!/usr/bin/perl
2
3 use warnings;
4 use strict;
5 use IkiWiki;
6 use IkiWiki::UserInfo;
7 use open qw{:utf8 :std};
8 use Encode;
9
10 package IkiWiki;
11
12 sub printheader ($) { #{{{
13         my $session=shift;
14         
15         if ($config{sslcookie}) {
16                 print $session->header(-charset => 'utf-8',
17                         -cookie => $session->cookie(-secure => 1));
18         } else {
19                 print $session->header(-charset => 'utf-8');
20         }
21
22 } #}}}
23
24 sub showform ($$$$;@) { #{{{
25         my $form=shift;
26         my $buttons=shift;
27         my $session=shift;
28         my $cgi=shift;
29
30         if (exists $hooks{formbuilder}) {
31                 run_hooks(formbuilder => sub {
32                         shift->(form => $form, cgi => $cgi, session => $session,
33                                 buttons => $buttons);
34                 });
35         }
36
37         printheader($session);
38         print misctemplate($form->title, $form->render(submit => $buttons), @_);
39 }
40
41 sub redirect ($$) { #{{{
42         my $q=shift;
43         my $url=shift;
44         if (! $config{w3mmode}) {
45                 print $q->redirect($url);
46         }
47         else {
48                 print "Content-type: text/plain\n";
49                 print "W3m-control: GOTO $url\n\n";
50         }
51 } #}}}
52
53 sub check_canedit ($$$;$) { #{{{
54         my $page=shift;
55         my $q=shift;
56         my $session=shift;
57         my $nonfatal=shift;
58         
59         my $canedit;
60         run_hooks(canedit => sub {
61                 return if defined $canedit;
62                 my $ret=shift->($page, $q, $session);
63                 if (defined $ret) {
64                         if ($ret eq "") {
65                                 $canedit=1;
66                         }
67                         elsif (ref $ret eq 'CODE') {
68                                 $ret->() unless $nonfatal;
69                                 $canedit=0;
70                         }
71                         elsif (defined $ret) {
72                                 error($ret) unless $nonfatal;
73                                 $canedit=0;
74                         }
75                 }
76         });
77         return $canedit;
78 } #}}}
79
80 sub decode_cgi_utf8 ($) { #{{{
81         my $cgi = shift;
82         foreach my $f ($cgi->param) {
83                 $cgi->param($f, map { decode_utf8 $_ } $cgi->param($f));
84         }
85 } #}}}
86
87 # Check if the user is signed in. If not, redirect to the signin form and
88 # save their place to return to later.
89 sub needsignin ($$) { #{{{
90         my $q=shift;
91         my $session=shift;
92
93         if (! defined $session->param("name") ||
94             ! userinfo_get($session->param("name"), "regdate")) {
95                 $session->param(postsignin => $ENV{QUERY_STRING});
96                 cgi_signin($q, $session);
97                 cgi_savesession($session);
98                 exit;
99         }
100 } #}}}  
101
102 sub cgi_signin ($$) { #{{{
103         my $q=shift;
104         my $session=shift;
105
106         decode_cgi_utf8($q);
107         eval q{use CGI::FormBuilder};
108         error($@) if $@;
109         my $form = CGI::FormBuilder->new(
110                 title => "signin",
111                 name => "signin",
112                 charset => "utf-8",
113                 method => 'POST',
114                 required => 'NONE',
115                 javascript => 0,
116                 params => $q,
117                 action => $config{cgiurl},
118                 header => 0,
119                 template => {type => 'div'},
120                 stylesheet => baseurl()."style.css",
121         );
122         my $buttons=["Login"];
123         
124         if ($q->param("do") ne "signin" && !$form->submitted) {
125                 $form->text(gettext("You need to log in first."));
126         }
127         $form->field(name => "do", type => "hidden", value => "signin",
128                 force => 1);
129         
130         run_hooks(formbuilder_setup => sub {
131                 shift->(form => $form, cgi => $q, session => $session,
132                         buttons => $buttons);
133         });
134
135         if ($form->submitted) {
136                 $form->validate;
137         }
138
139         showform($form, $buttons, $session, $q);
140 } #}}}
141
142 sub cgi_postsignin ($$) { #{{{
143         my $q=shift;
144         my $session=shift;
145         
146         # Continue with whatever was being done before the signin process.
147         if (defined $session->param("postsignin")) {
148                 my $postsignin=CGI->new($session->param("postsignin"));
149                 $session->clear("postsignin");
150                 cgi($postsignin, $session);
151                 cgi_savesession($session);
152                 exit;
153         }
154         else {
155                 error(gettext("login failed, perhaps you need to turn on cookies?"));
156         }
157 } #}}}
158
159 sub cgi_prefs ($$) { #{{{
160         my $q=shift;
161         my $session=shift;
162
163         needsignin($q, $session);
164
165         decode_cgi_utf8($q);
166         eval q{use CGI::FormBuilder};
167         error($@) if $@;
168         my $form = CGI::FormBuilder->new(
169                 title => "preferences",
170                 name => "preferences",
171                 header => 0,
172                 charset => "utf-8",
173                 method => 'POST',
174                 validate => {
175                         email => 'EMAIL',
176                 },
177                 required => 'NONE',
178                 javascript => 0,
179                 params => $q,
180                 action => $config{cgiurl},
181                 template => {type => 'div'},
182                 stylesheet => baseurl()."style.css",
183                 fieldsets => [
184                         [login => gettext("Login")],
185                         [preferences => gettext("Preferences")],
186                         [admin => gettext("Admin")]
187                 ],
188         );
189         my $buttons=["Save Preferences", "Logout", "Cancel"];
190
191         run_hooks(formbuilder_setup => sub {
192                 shift->(form => $form, cgi => $q, session => $session,
193                         buttons => $buttons);
194         });
195         
196         $form->field(name => "do", type => "hidden");
197         $form->field(name => "email", size => 50, fieldset => "preferences");
198         $form->field(name => "banned_users", size => 50,
199                 fieldset => "admin");
200         
201         my $user_name=$session->param("name");
202         if (! is_admin($user_name)) {
203                 $form->field(name => "banned_users", type => "hidden");
204         }
205
206         if (! $form->submitted) {
207                 $form->field(name => "email", force => 1,
208                         value => userinfo_get($user_name, "email"));
209                 if (is_admin($user_name)) {
210                         $form->field(name => "banned_users", force => 1,
211                                 value => join(" ", get_banned_users()));
212                 }
213         }
214         
215         if ($form->submitted eq 'Logout') {
216                 $session->delete();
217                 redirect($q, $config{url});
218                 return;
219         }
220         elsif ($form->submitted eq 'Cancel') {
221                 redirect($q, $config{url});
222                 return;
223         }
224         elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
225                 if (defined $form->field('email')) {
226                         userinfo_set($user_name, 'email', $form->field('email')) ||
227                                 error("failed to set email");
228                 }
229                 if (is_admin($user_name)) {
230                         set_banned_users(grep { ! is_admin($_) }
231                                         split(' ',
232                                                 $form->field("banned_users"))) ||
233                                 error("failed saving changes");
234                 }
235                 $form->text(gettext("Preferences saved."));
236         }
237         
238         showform($form, $buttons, $session, $q);
239 } #}}}
240
241 sub cgi_editpage ($$) { #{{{
242         my $q=shift;
243         my $session=shift;
244
245         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
246         my @buttons=("Save Page", "Preview", "Cancel");
247         
248         decode_cgi_utf8($q);
249         eval q{use CGI::FormBuilder};
250         error($@) if $@;
251         my $form = CGI::FormBuilder->new(
252                 title => "editpage",
253                 fields => \@fields,
254                 charset => "utf-8",
255                 method => 'POST',
256                 required => [qw{editcontent}],
257                 javascript => 0,
258                 params => $q,
259                 action => $config{cgiurl},
260                 header => 0,
261                 table => 0,
262                 template => scalar template_params("editpage.tmpl"),
263                 wikiname => $config{wikiname},
264         );
265         
266         run_hooks(formbuilder_setup => sub {
267                 shift->(form => $form, cgi => $q, session => $session,
268                         buttons => \@buttons);
269         });
270         
271         # This untaint is safe because titlepage removes any problematic
272         # characters.
273         my ($page)=$form->field('page');
274         $page=titlepage(possibly_foolish_untaint($page));
275         if (! defined $page || ! length $page ||
276             file_pruned($page, $config{srcdir}) || $page=~/^\//) {
277                 error("bad page name");
278         }
279
280         my $baseurl=$config{url}."/".htmlpage($page);
281         
282         my $from;
283         if (defined $form->field('from')) {
284                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
285         }
286         
287         my $file;
288         my $type;
289         if (exists $pagesources{$page} && $form->field("do") ne "create") {
290                 $file=$pagesources{$page};
291                 $type=pagetype($file);
292                 if (! defined $type || $type=~/^_/) {
293                         error(sprintf(gettext("%s is not an editable page"), $page));
294                 }
295                 if (! $form->submitted) {
296                         $form->field(name => "rcsinfo",
297                                 value => rcs_prepedit($file), force => 1);
298                 }
299                 $form->field(name => "editcontent", validate => '/.*/');
300         }
301         else {
302                 $type=$form->param('type');
303                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
304                         $type=possibly_foolish_untaint($type);
305                 }
306                 elsif (defined $from && exists $pagesources{$from}) {
307                         # favor the type of linking page
308                         $type=pagetype($pagesources{$from});
309                 }
310                 $type=$config{default_pageext} unless defined $type;
311                 $file=$page.".".$type;
312                 if (! $form->submitted) {
313                         $form->field(name => "rcsinfo", value => "", force => 1);
314                 }
315                 $form->field(name => "editcontent", validate => '/.+/');
316         }
317
318         $form->field(name => "do", type => 'hidden');
319         $form->field(name => "from", type => 'hidden');
320         $form->field(name => "rcsinfo", type => 'hidden');
321         $form->field(name => "subpage", type => 'hidden');
322         $form->field(name => "page", value => pagetitle($page, 1), force => 1);
323         $form->field(name => "type", value => $type, force => 1);
324         $form->field(name => "comments", type => "text", size => 80);
325         $form->field(name => "editcontent", type => "textarea", rows => 20,
326                 cols => 80);
327         $form->tmpl_param("can_commit", $config{rcs});
328         $form->tmpl_param("indexlink", indexlink());
329         $form->tmpl_param("helponformattinglink",
330                 htmllink($page, $page, "ikiwiki/formatting",
331                         noimageinline => 1,
332                         linktext => "FormattingHelp"));
333         
334         if ($form->submitted eq "Cancel") {
335                 if ($form->field("do") eq "create" && defined $from) {
336                         redirect($q, "$config{url}/".htmlpage($from));
337                 }
338                 elsif ($form->field("do") eq "create") {
339                         redirect($q, $config{url});
340                 }
341                 else {
342                         redirect($q, "$config{url}/".htmlpage($page));
343                 }
344                 return;
345         }
346         elsif ($form->submitted eq "Preview") {
347                 my $new=not exists $pagesources{$page};
348                 if ($new) {
349                         # temporarily record its type
350                         $pagesources{$page}=$page.".".$type;
351                 }
352
353                 my $content=$form->field('editcontent');
354                 run_hooks(editcontent => sub {
355                         $content=shift->(
356                                 content => $content,
357                                 page => $page,
358                                 cgi => $q,
359                                 session => $session,
360                         );
361                 });
362                 $form->tmpl_param("page_preview",
363                         htmlize($page, $type,
364                         linkify($page, $page,
365                         preprocess($page, $page,
366                         filter($page, $page, $content), 0, 1))));
367                 
368                 if ($new) {
369                         delete $pagesources{$page};
370                 }
371                 # previewing may have created files on disk
372                 saveindex();
373         }
374         elsif ($form->submitted eq "Save Page") {
375                 $form->tmpl_param("page_preview", "");
376         }
377         $form->tmpl_param("page_conflict", "");
378         
379         if ($form->submitted ne "Save Page" || ! $form->validate) {
380                 if ($form->field("do") eq "create") {
381                         my @page_locs;
382                         my $best_loc;
383                         if (! defined $from || ! length $from ||
384                             $from ne $form->field('from') ||
385                             file_pruned($from, $config{srcdir}) ||
386                             $from=~/^\// ||
387                             $form->submitted eq "Preview") {
388                                 @page_locs=$best_loc=$page;
389                         }
390                         else {
391                                 my $dir=$from."/";
392                                 $dir=~s![^/]+/+$!!;
393                                 
394                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
395                                     $page eq gettext('discussion')) {
396                                         $best_loc="$from/$page";
397                                 }
398                                 else {
399                                         $best_loc=$dir.$page;
400                                 }
401                                 
402                                 push @page_locs, $dir.$page;
403                                 push @page_locs, "$from/$page";
404                                 while (length $dir) {
405                                         $dir=~s![^/]+/+$!!;
406                                         push @page_locs, $dir.$page;
407                                 }
408                         
409                                 push @page_locs, "$config{userdir}/$page"
410                                         if length $config{userdir};
411                         }
412
413                         @page_locs = grep {
414                                 ! exists $pagecase{lc $_}
415                         } @page_locs;
416                         if (! @page_locs) {
417                                 # hmm, someone else made the page in the
418                                 # meantime?
419                                 if ($form->submitted eq "Preview") {
420                                         # let them go ahead with the edit
421                                         # and resolve the conflict at save
422                                         # time
423                                         @page_locs=$page;
424                                 }
425                                 else {
426                                         redirect($q, "$config{url}/".htmlpage($page));
427                                         return;
428                                 }
429                         }
430
431                         my @editable_locs = grep {
432                                 check_canedit($_, $q, $session, 1)
433                         } @page_locs;
434                         if (! @editable_locs) {
435                                 # let it throw an error this time
436                                 map { check_canedit($_, $q, $session) } @page_locs;
437                         }
438                         
439                         my @page_types;
440                         if (exists $hooks{htmlize}) {
441                                 @page_types=grep { !/^_/ }
442                                         keys %{$hooks{htmlize}};
443                         }
444                         
445                         $form->tmpl_param("page_select", 1);
446                         $form->field(name => "page", type => 'select',
447                                 options => [ map { pagetitle($_, 1) } @editable_locs ],
448                                 value => pagetitle($best_loc, 1));
449                         $form->field(name => "type", type => 'select',
450                                 options => \@page_types);
451                         $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
452                         
453                 }
454                 elsif ($form->field("do") eq "edit") {
455                         check_canedit($page, $q, $session);
456                         if (! defined $form->field('editcontent') || 
457                             ! length $form->field('editcontent')) {
458                                 my $content="";
459                                 if (exists $pagesources{$page}) {
460                                         $content=readfile(srcfile($pagesources{$page}));
461                                         $content=~s/\n/\r\n/g;
462                                 }
463                                 $form->field(name => "editcontent", value => $content,
464                                         force => 1);
465                         }
466                         $form->tmpl_param("page_select", 0);
467                         $form->field(name => "page", type => 'hidden');
468                         $form->field(name => "type", type => 'hidden');
469                         $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
470                 }
471                 
472                 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
473         }
474         else {
475                 # save page
476                 check_canedit($page, $q, $session);
477
478                 my $exists=-e "$config{srcdir}/$file";
479
480                 if ($form->field("do") ne "create" && ! $exists &&
481                     ! eval { srcfile($file) }) {
482                         $form->tmpl_param("page_gone", 1);
483                         $form->field(name => "do", value => "create", force => 1);
484                         $form->tmpl_param("page_select", 0);
485                         $form->field(name => "page", type => 'hidden');
486                         $form->field(name => "type", type => 'hidden');
487                         $form->title(sprintf(gettext("editing %s"), $page));
488                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
489                         return;
490                 }
491                 elsif ($form->field("do") eq "create" && $exists) {
492                         $form->tmpl_param("creation_conflict", 1);
493                         $form->field(name => "do", value => "edit", force => 1);
494                         $form->tmpl_param("page_select", 0);
495                         $form->field(name => "page", type => 'hidden');
496                         $form->field(name => "type", type => 'hidden');
497                         $form->title(sprintf(gettext("editing %s"), $page));
498                         $form->field("editcontent", 
499                                 value => readfile("$config{srcdir}/$file").
500                                          "\n\n\n".$form->field("editcontent"),
501                                 force => 1);
502                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
503                         return;
504                 }
505                 
506                 my $content=$form->field('editcontent');
507                 run_hooks(editcontent => sub {
508                         $content=shift->(
509                                 content => $content,
510                                 page => $page,
511                                 cgi => $q,
512                                 session => $session,
513                         );
514                 });
515                 $content=~s/\r\n/\n/g;
516                 $content=~s/\r/\n/g;
517                 $content.="\n" if $content !~ /\n$/;
518
519                 $config{cgi}=0; # avoid cgi error message
520                 eval { writefile($file, $config{srcdir}, $content) };
521                 $config{cgi}=1;
522                 if ($@) {
523                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
524                                 force => 1);
525                         $form->tmpl_param("failed_save", 1);
526                         $form->tmpl_param("error_message", $@);
527                         $form->field("editcontent", value => $content, force => 1);
528                         $form->tmpl_param("page_select", 0);
529                         $form->field(name => "page", type => 'hidden');
530                         $form->field(name => "type", type => 'hidden');
531                         $form->title(sprintf(gettext("editing %s"), $page));
532                         showform($form, \@buttons, $session, $q,
533                                 forcebaseurl => $baseurl);
534                         return;
535                 }
536                 
537                 my $conflict;
538                 if ($config{rcs}) {
539                         my $message="";
540                         if (defined $form->field('comments') &&
541                             length $form->field('comments')) {
542                                 $message=$form->field('comments');
543                         }
544                         
545                         if (! $exists) {
546                                 rcs_add($file);
547                         }
548
549                         # Prevent deadlock with post-commit hook by
550                         # signaling to it that it should not try to
551                         # do anything.
552                         disable_commit_hook();
553                         $conflict=rcs_commit($file, $message,
554                                 $form->field("rcsinfo"),
555                                 $session->param("name"), $ENV{REMOTE_ADDR});
556                         enable_commit_hook();
557                         rcs_update();
558                 }
559                 
560                 # Refresh even if there was a conflict, since other changes
561                 # may have been committed while the post-commit hook was
562                 # disabled.
563                 require IkiWiki::Render;
564                 refresh();
565                 saveindex();
566
567                 if (defined $conflict) {
568                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
569                                 force => 1);
570                         $form->tmpl_param("page_conflict", 1);
571                         $form->field("editcontent", value => $conflict, force => 1);
572                         $form->field("do", "edit", force => 1);
573                         $form->tmpl_param("page_select", 0);
574                         $form->field(name => "page", type => 'hidden');
575                         $form->field(name => "type", type => 'hidden');
576                         $form->title(sprintf(gettext("editing %s"), $page));
577                         showform($form, \@buttons, $session, $q,
578                                 forcebaseurl => $baseurl);
579                         return;
580                 }
581                 else {
582                         # The trailing question mark tries to avoid broken
583                         # caches and get the most recent version of the page.
584                         redirect($q, "$config{url}/".htmlpage($page)."?updated");
585                 }
586         }
587 } #}}}
588
589 sub cgi_getsession ($) { #{{{
590         my $q=shift;
591
592         eval q{use CGI::Session};
593         CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
594         
595         my $oldmask=umask(077);
596         my $session = CGI::Session->new("driver:DB_File", $q,
597                 { FileName => "$config{wikistatedir}/sessions.db" });
598         umask($oldmask);
599
600         return $session;
601 } #}}}
602
603 sub cgi_savesession ($) { #{{{
604         my $session=shift;
605
606         # Force session flush with safe umask.
607         my $oldmask=umask(077);
608         $session->flush;
609         umask($oldmask);
610 } #}}}
611
612 sub cgi (;$$) { #{{{
613         my $q=shift;
614         my $session=shift;
615
616         if (! $q) {
617                 eval q{use CGI};
618                 error($@) if $@;
619         
620                 $q=CGI->new;
621         
622                 run_hooks(cgi => sub { shift->($q) });
623         }
624
625         my $do=$q->param('do');
626         if (! defined $do || ! length $do) {
627                 my $error = $q->cgi_error;
628                 if ($error) {
629                         error("Request not processed: $error");
630                 }
631                 else {
632                         error("\"do\" parameter missing");
633                 }
634         }
635         
636         # Need to lock the wiki before getting a session.
637         lockwiki();
638         loadindex();
639         
640         if (! $session) {
641                 $session=cgi_getsession($q);
642         }
643         
644         # Auth hooks can sign a user in.
645         if ($do ne 'signin' && ! defined $session->param("name")) {
646                 run_hooks(auth => sub {
647                         shift->($q, $session)
648                 });
649                 if (defined $session->param("name")) {
650                         # Make sure whatever user was authed is in the
651                         # userinfo db.
652                         if (! userinfo_get($session->param("name"), "regdate")) {
653                                 userinfo_setall($session->param("name"), {
654                                         email => "",
655                                         password => "",
656                                         regdate => time,
657                                 }) || error("failed adding user");
658                         }
659                 }
660         }
661         
662         if (defined $session->param("name") &&
663             userinfo_get($session->param("name"), "banned")) {
664                 print $q->header(-status => "403 Forbidden");
665                 $session->delete();
666                 print gettext("You are banned.");
667                 cgi_savesession($session);
668         }
669
670         run_hooks(sessioncgi => sub { shift->($q, $session) });
671
672         if ($do eq 'signin') {
673                 cgi_signin($q, $session);
674                 cgi_savesession($session);
675         }
676         elsif ($do eq 'prefs') {
677                 cgi_prefs($q, $session);
678         }
679         elsif ($do eq 'create' || $do eq 'edit') {
680                 cgi_editpage($q, $session);
681         }
682         elsif (defined $session->param("postsignin") || $do eq 'postsignin') {
683                 cgi_postsignin($q, $session);
684         }
685         else {
686                 error("unknown do parameter");
687         }
688 } #}}}
689
690 1