Fix ugly display when editing a page that has vanished.
[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         decode_cgi_utf8($q);
165         
166         # The session id is stored on the form and checked to
167         # guard against CSRF.
168         my $sid=$q->param('sid');
169         if (! defined $sid) {
170                 $q->delete_all;
171         }
172         elsif ($sid ne $session->id) {
173                 error(gettext("Your login session has expired."));
174         }
175
176         eval q{use CGI::FormBuilder};
177         error($@) if $@;
178         my $form = CGI::FormBuilder->new(
179                 title => "preferences",
180                 name => "preferences",
181                 header => 0,
182                 charset => "utf-8",
183                 method => 'POST',
184                 validate => {
185                         email => 'EMAIL',
186                 },
187                 required => 'NONE',
188                 javascript => 0,
189                 params => $q,
190                 action => $config{cgiurl},
191                 template => {type => 'div'},
192                 stylesheet => baseurl()."style.css",
193                 fieldsets => [
194                         [login => gettext("Login")],
195                         [preferences => gettext("Preferences")],
196                         [admin => gettext("Admin")]
197                 ],
198         );
199         my $buttons=["Save Preferences", "Logout", "Cancel"];
200
201         run_hooks(formbuilder_setup => sub {
202                 shift->(form => $form, cgi => $q, session => $session,
203                         buttons => $buttons);
204         });
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         $form->field(name => "banned_users", size => 50,
212                 fieldset => "admin");
213         
214         my $user_name=$session->param("name");
215         if (! is_admin($user_name)) {
216                 $form->field(name => "banned_users", type => "hidden");
217         }
218
219         if (! $form->submitted) {
220                 $form->field(name => "email", force => 1,
221                         value => userinfo_get($user_name, "email"));
222                 if (is_admin($user_name)) {
223                         $form->field(name => "banned_users", force => 1,
224                                 value => join(" ", get_banned_users()));
225                 }
226         }
227         
228         if ($form->submitted eq 'Logout') {
229                 $session->delete();
230                 redirect($q, $config{url});
231                 return;
232         }
233         elsif ($form->submitted eq 'Cancel') {
234                 redirect($q, $config{url});
235                 return;
236         }
237         elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
238                 if (defined $form->field('email')) {
239                         userinfo_set($user_name, 'email', $form->field('email')) ||
240                                 error("failed to set email");
241                 }
242                 if (is_admin($user_name)) {
243                         set_banned_users(grep { ! is_admin($_) }
244                                         split(' ',
245                                                 $form->field("banned_users"))) ||
246                                 error("failed saving changes");
247                 }
248                 $form->text(gettext("Preferences saved."));
249         }
250         
251         showform($form, $buttons, $session, $q);
252 } #}}}
253
254 sub cgi_editpage ($$) { #{{{
255         my $q=shift;
256         my $session=shift;
257         
258         decode_cgi_utf8($q);
259         
260         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
261         my @buttons=("Save Page", "Preview", "Cancel");
262         eval q{use CGI::FormBuilder};
263         error($@) if $@;
264         my $form = CGI::FormBuilder->new(
265                 title => "editpage",
266                 fields => \@fields,
267                 charset => "utf-8",
268                 method => 'POST',
269                 required => [qw{editcontent}],
270                 javascript => 0,
271                 params => $q,
272                 action => $config{cgiurl},
273                 header => 0,
274                 table => 0,
275                 template => scalar template_params("editpage.tmpl"),
276                 wikiname => $config{wikiname},
277         );
278         
279         run_hooks(formbuilder_setup => sub {
280                 shift->(form => $form, cgi => $q, session => $session,
281                         buttons => \@buttons);
282         });
283         
284         # This untaint is safe because titlepage removes any problematic
285         # characters.
286         my ($page)=$form->field('page');
287         $page=titlepage(possibly_foolish_untaint($page));
288         if (! defined $page || ! length $page ||
289             file_pruned($page, $config{srcdir}) || $page=~/^\//) {
290                 error("bad page name");
291         }
292
293         my $baseurl=$config{url}."/".htmlpage($page);
294         
295         my $from;
296         if (defined $form->field('from')) {
297                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
298         }
299         
300         my $file;
301         my $type;
302         if (exists $pagesources{$page} && $form->field("do") ne "create") {
303                 $file=$pagesources{$page};
304                 $type=pagetype($file);
305                 if (! defined $type || $type=~/^_/) {
306                         error(sprintf(gettext("%s is not an editable page"), $page));
307                 }
308                 if (! $form->submitted) {
309                         $form->field(name => "rcsinfo",
310                                 value => rcs_prepedit($file), force => 1);
311                 }
312                 $form->field(name => "editcontent", validate => '/.*/');
313         }
314         else {
315                 $type=$form->param('type');
316                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
317                         $type=possibly_foolish_untaint($type);
318                 }
319                 elsif (defined $from && exists $pagesources{$from}) {
320                         # favor the type of linking page
321                         $type=pagetype($pagesources{$from});
322                 }
323                 $type=$config{default_pageext} unless defined $type;
324                 $file=$page.".".$type;
325                 if (! $form->submitted) {
326                         $form->field(name => "rcsinfo", value => "", force => 1);
327                 }
328                 $form->field(name => "editcontent", validate => '/.+/');
329         }
330
331         $form->field(name => "do", type => 'hidden');
332         $form->field(name => "sid", type => "hidden", value => $session->id,
333                 force => 1);
334         $form->field(name => "from", type => 'hidden');
335         $form->field(name => "rcsinfo", type => 'hidden');
336         $form->field(name => "subpage", type => 'hidden');
337         $form->field(name => "page", value => pagetitle($page, 1), force => 1);
338         $form->field(name => "type", value => $type, force => 1);
339         $form->field(name => "comments", type => "text", size => 80);
340         $form->field(name => "editcontent", type => "textarea", rows => 20,
341                 cols => 80);
342         $form->tmpl_param("can_commit", $config{rcs});
343         $form->tmpl_param("indexlink", indexlink());
344         $form->tmpl_param("helponformattinglink",
345                 htmllink($page, $page, "ikiwiki/formatting",
346                         noimageinline => 1,
347                         linktext => "FormattingHelp"));
348         
349         if ($form->submitted eq "Cancel") {
350                 if ($form->field("do") eq "create" && defined $from) {
351                         redirect($q, "$config{url}/".htmlpage($from));
352                 }
353                 elsif ($form->field("do") eq "create") {
354                         redirect($q, $config{url});
355                 }
356                 else {
357                         redirect($q, "$config{url}/".htmlpage($page));
358                 }
359                 return;
360         }
361         elsif ($form->submitted eq "Preview") {
362                 my $new=not exists $pagesources{$page};
363                 if ($new) {
364                         # temporarily record its type
365                         $pagesources{$page}=$page.".".$type;
366                 }
367
368                 my $content=$form->field('editcontent');
369                 run_hooks(editcontent => sub {
370                         $content=shift->(
371                                 content => $content,
372                                 page => $page,
373                                 cgi => $q,
374                                 session => $session,
375                         );
376                 });
377                 $form->tmpl_param("page_preview",
378                         htmlize($page, $type,
379                         linkify($page, $page,
380                         preprocess($page, $page,
381                         filter($page, $page, $content), 0, 1))));
382                 
383                 if ($new) {
384                         delete $pagesources{$page};
385                 }
386                 # previewing may have created files on disk
387                 saveindex();
388         }
389         elsif ($form->submitted eq "Save Page") {
390                 $form->tmpl_param("page_preview", "");
391         }
392         $form->tmpl_param("page_conflict", "");
393         
394         if ($form->submitted ne "Save Page" || ! $form->validate) {
395                 if ($form->field("do") eq "create") {
396                         my @page_locs;
397                         my $best_loc;
398                         if (! defined $from || ! length $from ||
399                             $from ne $form->field('from') ||
400                             file_pruned($from, $config{srcdir}) ||
401                             $from=~/^\// ||
402                             $form->submitted eq "Preview") {
403                                 @page_locs=$best_loc=$page;
404                         }
405                         else {
406                                 my $dir=$from."/";
407                                 $dir=~s![^/]+/+$!!;
408                                 
409                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
410                                     $page eq gettext('discussion')) {
411                                         $best_loc="$from/$page";
412                                 }
413                                 else {
414                                         $best_loc=$dir.$page;
415                                 }
416                                 
417                                 push @page_locs, $dir.$page;
418                                 push @page_locs, "$from/$page";
419                                 while (length $dir) {
420                                         $dir=~s![^/]+/+$!!;
421                                         push @page_locs, $dir.$page;
422                                 }
423                         
424                                 push @page_locs, "$config{userdir}/$page"
425                                         if length $config{userdir};
426                         }
427
428                         @page_locs = grep {
429                                 ! exists $pagecase{lc $_}
430                         } @page_locs;
431                         if (! @page_locs) {
432                                 # hmm, someone else made the page in the
433                                 # meantime?
434                                 if ($form->submitted eq "Preview") {
435                                         # let them go ahead with the edit
436                                         # and resolve the conflict at save
437                                         # time
438                                         @page_locs=$page;
439                                 }
440                                 else {
441                                         redirect($q, "$config{url}/".htmlpage($page));
442                                         return;
443                                 }
444                         }
445
446                         my @editable_locs = grep {
447                                 check_canedit($_, $q, $session, 1)
448                         } @page_locs;
449                         if (! @editable_locs) {
450                                 # let it throw an error this time
451                                 map { check_canedit($_, $q, $session) } @page_locs;
452                         }
453                         
454                         my @page_types;
455                         if (exists $hooks{htmlize}) {
456                                 @page_types=grep { !/^_/ }
457                                         keys %{$hooks{htmlize}};
458                         }
459                         
460                         $form->tmpl_param("page_select", 1);
461                         $form->field(name => "page", type => 'select',
462                                 options => [ map { pagetitle($_, 1) } @editable_locs ],
463                                 value => pagetitle($best_loc, 1));
464                         $form->field(name => "type", type => 'select',
465                                 options => \@page_types);
466                         $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
467                         
468                 }
469                 elsif ($form->field("do") eq "edit") {
470                         check_canedit($page, $q, $session);
471                         if (! defined $form->field('editcontent') || 
472                             ! length $form->field('editcontent')) {
473                                 my $content="";
474                                 if (exists $pagesources{$page}) {
475                                         $content=readfile(srcfile($pagesources{$page}));
476                                         $content=~s/\n/\r\n/g;
477                                 }
478                                 $form->field(name => "editcontent", value => $content,
479                                         force => 1);
480                         }
481                         $form->tmpl_param("page_select", 0);
482                         $form->field(name => "page", type => 'hidden');
483                         $form->field(name => "type", type => 'hidden');
484                         $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
485                 }
486                 
487                 showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
488         }
489         else {
490                 # save page
491                 check_canedit($page, $q, $session);
492         
493                 # The session id is stored on the form and checked to
494                 # guard against CSRF. But only if the user is logged in,
495                 # as anonok can allow anonymous edits.
496                 if (defined $session->param("name")) {
497                         my $sid=$q->param('sid');
498                         if (! defined $sid || $sid ne $session->id) {
499                                 error(gettext("Your login session has expired."));
500                         }
501                 }
502
503                 my $exists=-e "$config{srcdir}/$file";
504
505                 if ($form->field("do") ne "create" && ! $exists &&
506                     ! defined srcfile($file, 1)) {
507                         $form->tmpl_param("page_gone", 1);
508                         $form->field(name => "do", value => "create", force => 1);
509                         $form->tmpl_param("page_select", 0);
510                         $form->field(name => "page", type => 'hidden');
511                         $form->field(name => "type", type => 'hidden');
512                         $form->title(sprintf(gettext("editing %s"), $page));
513                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
514                         return;
515                 }
516                 elsif ($form->field("do") eq "create" && $exists) {
517                         $form->tmpl_param("creation_conflict", 1);
518                         $form->field(name => "do", value => "edit", force => 1);
519                         $form->tmpl_param("page_select", 0);
520                         $form->field(name => "page", type => 'hidden');
521                         $form->field(name => "type", type => 'hidden');
522                         $form->title(sprintf(gettext("editing %s"), $page));
523                         $form->field("editcontent", 
524                                 value => readfile("$config{srcdir}/$file").
525                                          "\n\n\n".$form->field("editcontent"),
526                                 force => 1);
527                         showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
528                         return;
529                 }
530                 
531                 my $content=$form->field('editcontent');
532                 run_hooks(editcontent => sub {
533                         $content=shift->(
534                                 content => $content,
535                                 page => $page,
536                                 cgi => $q,
537                                 session => $session,
538                         );
539                 });
540                 $content=~s/\r\n/\n/g;
541                 $content=~s/\r/\n/g;
542                 $content.="\n" if $content !~ /\n$/;
543
544                 $config{cgi}=0; # avoid cgi error message
545                 eval { writefile($file, $config{srcdir}, $content) };
546                 $config{cgi}=1;
547                 if ($@) {
548                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
549                                 force => 1);
550                         $form->tmpl_param("failed_save", 1);
551                         $form->tmpl_param("error_message", $@);
552                         $form->field("editcontent", value => $content, force => 1);
553                         $form->tmpl_param("page_select", 0);
554                         $form->field(name => "page", type => 'hidden');
555                         $form->field(name => "type", type => 'hidden');
556                         $form->title(sprintf(gettext("editing %s"), $page));
557                         showform($form, \@buttons, $session, $q,
558                                 forcebaseurl => $baseurl);
559                         return;
560                 }
561                 
562                 my $conflict;
563                 if ($config{rcs}) {
564                         my $message="";
565                         if (defined $form->field('comments') &&
566                             length $form->field('comments')) {
567                                 $message=$form->field('comments');
568                         }
569                         
570                         if (! $exists) {
571                                 rcs_add($file);
572                         }
573
574                         # Prevent deadlock with post-commit hook by
575                         # signaling to it that it should not try to
576                         # do anything.
577                         disable_commit_hook();
578                         $conflict=rcs_commit($file, $message,
579                                 $form->field("rcsinfo"),
580                                 $session->param("name"), $ENV{REMOTE_ADDR});
581                         enable_commit_hook();
582                         rcs_update();
583                 }
584                 
585                 # Refresh even if there was a conflict, since other changes
586                 # may have been committed while the post-commit hook was
587                 # disabled.
588                 require IkiWiki::Render;
589                 refresh();
590                 saveindex();
591
592                 if (defined $conflict) {
593                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
594                                 force => 1);
595                         $form->tmpl_param("page_conflict", 1);
596                         $form->field("editcontent", value => $conflict, force => 1);
597                         $form->field("do", "edit", force => 1);
598                         $form->tmpl_param("page_select", 0);
599                         $form->field(name => "page", type => 'hidden');
600                         $form->field(name => "type", type => 'hidden');
601                         $form->title(sprintf(gettext("editing %s"), $page));
602                         showform($form, \@buttons, $session, $q,
603                                 forcebaseurl => $baseurl);
604                         return;
605                 }
606                 else {
607                         # The trailing question mark tries to avoid broken
608                         # caches and get the most recent version of the page.
609                         redirect($q, "$config{url}/".htmlpage($page)."?updated");
610                 }
611         }
612 } #}}}
613
614 sub cgi_getsession ($) { #{{{
615         my $q=shift;
616
617         eval q{use CGI::Session};
618         CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
619         
620         my $oldmask=umask(077);
621         my $session = CGI::Session->new("driver:DB_File", $q,
622                 { FileName => "$config{wikistatedir}/sessions.db" });
623         umask($oldmask);
624
625         return $session;
626 } #}}}
627
628 sub cgi_savesession ($) { #{{{
629         my $session=shift;
630
631         # Force session flush with safe umask.
632         my $oldmask=umask(077);
633         $session->flush;
634         umask($oldmask);
635 } #}}}
636
637 sub cgi (;$$) { #{{{
638         my $q=shift;
639         my $session=shift;
640
641         if (! $q) {
642                 eval q{use CGI};
643                 error($@) if $@;
644         
645                 $q=CGI->new;
646         
647                 run_hooks(cgi => sub { shift->($q) });
648         }
649
650         my $do=$q->param('do');
651         if (! defined $do || ! length $do) {
652                 my $error = $q->cgi_error;
653                 if ($error) {
654                         error("Request not processed: $error");
655                 }
656                 else {
657                         error("\"do\" parameter missing");
658                 }
659         }
660         
661         # Need to lock the wiki before getting a session.
662         lockwiki();
663         loadindex();
664         
665         if (! $session) {
666                 $session=cgi_getsession($q);
667         }
668         
669         # Auth hooks can sign a user in.
670         if ($do ne 'signin' && ! defined $session->param("name")) {
671                 run_hooks(auth => sub {
672                         shift->($q, $session)
673                 });
674                 if (defined $session->param("name")) {
675                         # Make sure whatever user was authed is in the
676                         # userinfo db.
677                         if (! userinfo_get($session->param("name"), "regdate")) {
678                                 userinfo_setall($session->param("name"), {
679                                         email => "",
680                                         password => "",
681                                         regdate => time,
682                                 }) || error("failed adding user");
683                         }
684                 }
685         }
686         
687         if (defined $session->param("name") &&
688             userinfo_get($session->param("name"), "banned")) {
689                 print $q->header(-status => "403 Forbidden");
690                 $session->delete();
691                 print gettext("You are banned.");
692                 cgi_savesession($session);
693         }
694
695         run_hooks(sessioncgi => sub { shift->($q, $session) });
696
697         if ($do eq 'signin') {
698                 cgi_signin($q, $session);
699                 cgi_savesession($session);
700         }
701         elsif ($do eq 'prefs') {
702                 cgi_prefs($q, $session);
703         }
704         elsif ($do eq 'create' || $do eq 'edit') {
705                 cgi_editpage($q, $session);
706         }
707         elsif (defined $session->param("postsignin") || $do eq 'postsignin') {
708                 cgi_postsignin($q, $session);
709         }
710         else {
711                 error("unknown do parameter");
712         }
713 } #}}}
714
715 1