* Add typography (SmartyPants) plugin by Recai.
[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 redirect ($$) { #{{{
25         my $q=shift;
26         my $url=shift;
27         if (! $config{w3mmode}) {
28                 print $q->redirect($url);
29         }
30         else {
31                 print "Content-type: text/plain\n";
32                 print "W3m-control: GOTO $url\n\n";
33         }
34 } #}}}
35
36 sub page_locked ($$;$) { #{{{
37         my $page=shift;
38         my $session=shift;
39         my $nonfatal=shift;
40         
41         my $user=$session->param("name");
42         return if defined $user && is_admin($user);
43
44         foreach my $admin (@{$config{adminuser}}) {
45                 my $locked_pages=userinfo_get($admin, "locked_pages");
46                 if (pagespec_match($page, userinfo_get($admin, "locked_pages"))) {
47                         return 1 if $nonfatal;
48                         error(htmllink("", "", $page, 1)." is locked by ".
49                               htmllink("", "", $admin, 1)." and cannot be edited.");
50                 }
51         }
52
53         return 0;
54 } #}}}
55
56 sub decode_form_utf8 ($) { #{{{
57         my $form = shift;
58         foreach my $f ($form->field) {
59                 next if Encode::is_utf8(scalar $form->field($f));
60                 $form->field(name  => $f,
61                              value => decode_utf8($form->field($f)),
62                              force => 1,
63                             );
64         }
65 } #}}}
66
67 sub cgi_recentchanges ($) { #{{{
68         my $q=shift;
69         
70         unlockwiki();
71
72         # Optimisation: building recentchanges means calculating lots of
73         # links. Memoizing htmllink speeds it up a lot (can't be memoized
74         # during page builds as the return values may change, but they
75         # won't here.)
76         eval q{use Memoize};
77         memoize("htmllink");
78
79         eval q{use Time::Duration};
80         eval q{use CGI 'escapeHTML'};
81
82         my $changelog=[rcs_recentchanges(100)];
83         foreach my $change (@$changelog) {
84                 $change->{when} = concise(ago($change->{when}));
85                 $change->{user} = htmllink("", "", escapeHTML($change->{user}), 1);
86                 $change->{pages} = [
87                         map {
88                                 $_->{link} = htmllink("", "", $_->{page}, 1);
89                                 $_;
90                         } @{$change->{pages}}
91                 ];
92         }
93
94         my $template=template("recentchanges.tmpl"); 
95         $template->param(
96                 title => "RecentChanges",
97                 indexlink => indexlink(),
98                 wikiname => $config{wikiname},
99                 changelog => $changelog,
100                 baseurl => baseurl(),
101         );
102         run_hooks(pagetemplate => sub {
103                 shift->(page => "", destpage => "", template => $template);
104         });
105         print $q->header(-charset => 'utf-8'), $template->output;
106 } #}}}
107
108 sub cgi_signin ($$) { #{{{
109         my $q=shift;
110         my $session=shift;
111
112         eval q{use CGI::FormBuilder};
113         my $form = CGI::FormBuilder->new(
114                 title => "signin",
115                 fields => [qw(do title page subpage from name password confirm_password email)],
116                 header => 1,
117                 charset => "utf-8",
118                 method => 'POST',
119                 validate => {
120                         confirm_password => {
121                                 perl => q{eq $form->field("password")},
122                         },
123                         email => 'EMAIL',
124                 },
125                 required => 'NONE',
126                 javascript => 0,
127                 params => $q,
128                 action => $config{cgiurl},
129                 header => 0,
130                 template => (-e "$config{templatedir}/signin.tmpl" ?
131                              {template_params("signin.tmpl")} : ""),
132                 stylesheet => baseurl()."style.css",
133         );
134                 
135         decode_form_utf8($form);
136         
137         $form->field(name => "name", required => 0);
138         $form->field(name => "do", type => "hidden");
139         $form->field(name => "page", type => "hidden");
140         $form->field(name => "title", type => "hidden");
141         $form->field(name => "from", type => "hidden");
142         $form->field(name => "subpage", type => "hidden");
143         $form->field(name => "password", type => "password", required => 0);
144         $form->field(name => "confirm_password", type => "password", required => 0);
145         $form->field(name => "email", required => 0);
146         if ($q->param("do") ne "signin" && !$form->submitted) {
147                 $form->text("You need to log in first.");
148         }
149         
150         if ($form->submitted) {
151                 # Set required fields based on how form was submitted.
152                 my %required=(
153                         "Login" => [qw(name password)],
154                         "Register" => [qw(name password confirm_password email)],
155                         "Mail Password" => [qw(name)],
156                 );
157                 foreach my $opt (@{$required{$form->submitted}}) {
158                         $form->field(name => $opt, required => 1);
159                 }
160         
161                 # Validate password differently depending on how
162                 # form was submitted.
163                 if ($form->submitted eq 'Login') {
164                         $form->field(
165                                 name => "password",
166                                 validate => sub {
167                                         length $form->field("name") &&
168                                         shift eq userinfo_get($form->field("name"), 'password');
169                                 },
170                         );
171                         $form->field(name => "name", validate => '/^\w+$/');
172                 }
173                 else {
174                         $form->field(name => "password", validate => 'VALUE');
175                 }
176                 # And make sure the entered name exists when logging
177                 # in or sending email, and does not when registering.
178                 if ($form->submitted eq 'Register') {
179                         $form->field(
180                                 name => "name",
181                                 validate => sub {
182                                         my $name=shift;
183                                         length $name &&
184                                         $name=~/$config{wiki_file_regexp}/ &&
185                                         ! userinfo_get($name, "regdate");
186                                 },
187                         );
188                 }
189                 else {
190                         $form->field(
191                                 name => "name",
192                                 validate => sub {
193                                         my $name=shift;
194                                         length $name &&
195                                         userinfo_get($name, "regdate");
196                                 },
197                         );
198                 }
199         }
200         else {
201                 # First time settings.
202                 $form->field(name => "name", comment => "use FirstnameLastName");
203                 $form->field(name => "confirm_password", comment => "(only needed");
204                 $form->field(name => "email",            comment => "for registration)");
205                 if ($session->param("name")) {
206                         $form->field(name => "name", value => $session->param("name"));
207                 }
208         }
209
210         if ($form->submitted && $form->validate) {
211                 if ($form->submitted eq 'Login') {
212                         $session->param("name", $form->field("name"));
213                         if (defined $form->field("do") && 
214                             $form->field("do") ne 'signin') {
215                                 redirect($q, cgiurl(
216                                         do => $form->field("do"),
217                                         page => $form->field("page"),
218                                         title => $form->field("title"),
219                                         subpage => $form->field("subpage"),
220                                         from => $form->field("from"),
221                                 ));
222                         }
223                         else {
224                                 redirect($q, $config{url});
225                         }
226                 }
227                 elsif ($form->submitted eq 'Register') {
228                         my $user_name=$form->field('name');
229                         if (userinfo_setall($user_name, {
230                                            'email' => $form->field('email'),
231                                            'password' => $form->field('password'),
232                                            'regdate' => time
233                                          })) {
234                                 $form->field(name => "confirm_password", type => "hidden");
235                                 $form->field(name => "email", type => "hidden");
236                                 $form->text("Registration successful. Now you can Login.");
237                                 printheader($session);
238                                 print misctemplate($form->title, $form->render(submit => ["Login"]));
239                         }
240                         else {
241                                 error("Error saving registration.");
242                         }
243                 }
244                 elsif ($form->submitted eq 'Mail Password') {
245                         my $user_name=$form->field("name");
246                         my $template=template("passwordmail.tmpl");
247                         $template->param(
248                                 user_name => $user_name,
249                                 user_password => userinfo_get($user_name, "password"),
250                                 wikiurl => $config{url},
251                                 wikiname => $config{wikiname},
252                                 REMOTE_ADDR => $ENV{REMOTE_ADDR},
253                         );
254                         
255                         eval q{use Mail::Sendmail};
256                         sendmail(
257                                 To => userinfo_get($user_name, "email"),
258                                 From => "$config{wikiname} admin <$config{adminemail}>",
259                                 Subject => "$config{wikiname} information",
260                                 Message => $template->output,
261                         ) or error("Failed to send mail");
262                         
263                         $form->text("Your password has been emailed to you.");
264                         $form->field(name => "name", required => 0);
265                         printheader($session);
266                         print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
267                 }
268         }
269         else {
270                 printheader($session);
271                 print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
272         }
273 } #}}}
274
275 sub cgi_prefs ($$) { #{{{
276         my $q=shift;
277         my $session=shift;
278
279         eval q{use CGI::FormBuilder};
280         my $form = CGI::FormBuilder->new(
281                 title => "preferences",
282                 fields => [qw(do name password confirm_password email 
283                               subscriptions locked_pages)],
284                 header => 0,
285                 charset => "utf-8",
286                 method => 'POST',
287                 validate => {
288                         confirm_password => {
289                                 perl => q{eq $form->field("password")},
290                         },
291                         email => 'EMAIL',
292                 },
293                 required => 'NONE',
294                 javascript => 0,
295                 params => $q,
296                 action => $config{cgiurl},
297                 template => (-e "$config{templatedir}/prefs.tmpl" ?
298                              {template_params("prefs.tmpl")} : ""),
299                 stylesheet => baseurl()."style.css",
300         );
301         my @buttons=("Save Preferences", "Logout", "Cancel");
302         
303         my $user_name=$session->param("name");
304         $form->field(name => "do", type => "hidden");
305         $form->field(name => "name", disabled => 1,
306                 value => $user_name, force => 1);
307         $form->field(name => "password", type => "password");
308         $form->field(name => "confirm_password", type => "password");
309         $form->field(name => "subscriptions", size => 50,
310                 comment => "(".htmllink("", "", "PageSpec", 1).")");
311         $form->field(name => "locked_pages", size => 50,
312                 comment => "(".htmllink("", "", "PageSpec", 1).")");
313         
314         if (! is_admin($user_name)) {
315                 $form->field(name => "locked_pages", type => "hidden");
316         }
317         
318         if (! $form->submitted) {
319                 $form->field(name => "email", force => 1,
320                         value => userinfo_get($user_name, "email"));
321                 $form->field(name => "subscriptions", force => 1,
322                         value => userinfo_get($user_name, "subscriptions"));
323                 $form->field(name => "locked_pages", force => 1,
324                         value => userinfo_get($user_name, "locked_pages"));
325         }
326         
327         decode_form_utf8($form);
328         
329         if ($form->submitted eq 'Logout') {
330                 $session->delete();
331                 redirect($q, $config{url});
332                 return;
333         }
334         elsif ($form->submitted eq 'Cancel') {
335                 redirect($q, $config{url});
336                 return;
337         }
338         elsif ($form->submitted eq "Save Preferences" && $form->validate) {
339                 foreach my $field (qw(password email subscriptions locked_pages)) {
340                         if (length $form->field($field)) {
341                                 userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
342                         }
343                 }
344                 $form->text("Preferences saved.");
345         }
346         
347         printheader($session);
348         print misctemplate($form->title, $form->render(submit => \@buttons));
349 } #}}}
350
351 sub cgi_editpage ($$) { #{{{
352         my $q=shift;
353         my $session=shift;
354
355         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
356         my @buttons=("Save Page", "Preview", "Cancel");
357         
358         eval q{use CGI::FormBuilder; use CGI::FormBuilder::Template::HTML};
359         my $renderer=CGI::FormBuilder::Template::HTML->new(
360                 fields => \@fields,
361                 template_params("editpage.tmpl"),
362         );
363         run_hooks(pagetemplate => sub {
364                 shift->(page => "", destpage => "", template => $renderer->engine);
365         });
366         my $form = CGI::FormBuilder->new(
367                 fields => \@fields,
368                 header => 1,
369                 charset => "utf-8",
370                 method => 'POST',
371                 validate => {
372                         editcontent => '/.+/',
373                 },
374                 required => [qw{editcontent}],
375                 javascript => 0,
376                 params => $q,
377                 action => $config{cgiurl},
378                 table => 0,
379                 template => $renderer,
380         );
381         
382         decode_form_utf8($form);
383         
384         # This untaint is safe because titlepage removes any problematic
385         # characters.
386         my ($page)=$form->field('page');
387         $page=titlepage(possibly_foolish_untaint($page));
388         if (! defined $page || ! length $page ||
389             $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
390                 error("bad page name");
391         }
392         
393         my $from;
394         if (defined $form->field('from')) {
395                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
396         }
397         
398         my $file;
399         my $type;
400         if (exists $pagesources{$page}) {
401                 $file=$pagesources{$page};
402                 $type=pagetype($file);
403         }
404         else {
405                 $type=$form->param('type');
406                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
407                         $type=possibly_foolish_untaint($type);
408                 }
409                 elsif (defined $from) {
410                         # favor the type of linking page
411                         $type=pagetype($pagesources{$from});
412                 }
413                 $type=$config{default_pageext} unless defined $type;
414                 $file=$page.".".$type;
415         }
416
417         my $newfile=0;
418         if (! -e "$config{srcdir}/$file") {
419                 $newfile=1;
420         }
421
422         $form->field(name => "do", type => 'hidden');
423         $form->field(name => "from", type => 'hidden');
424         $form->field(name => "rcsinfo", type => 'hidden');
425         $form->field(name => "subpage", type => 'hidden');
426         $form->field(name => "page", value => $page, force => 1);
427         $form->field(name => "type", value => $type, force => 1);
428         $form->field(name => "comments", type => "text", size => 80);
429         $form->field(name => "editcontent", type => "textarea", rows => 20,
430                 cols => 80);
431         $form->tmpl_param("can_commit", $config{rcs});
432         $form->tmpl_param("indexlink", indexlink());
433         $form->tmpl_param("helponformattinglink",
434                 htmllink("", "", "HelpOnFormatting", 1));
435         $form->tmpl_param("baseurl", baseurl());
436         if (! $form->submitted) {
437                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
438                         force => 1);
439         }
440         
441         if ($form->submitted eq "Cancel") {
442                 if ($newfile && defined $from) {
443                         redirect($q, "$config{url}/".htmlpage($from));
444                 }
445                 elsif ($newfile) {
446                         redirect($q, $config{url});
447                 }
448                 else {
449                         redirect($q, "$config{url}/".htmlpage($page));
450                 }
451                 return;
452         }
453         elsif ($form->submitted eq "Preview") {
454                 my $content=$form->field('editcontent');
455                 my $comments=$form->field('comments');
456                 $form->field(name => "editcontent",
457                                 value => $content, force => 1);
458                 $form->field(name => "comments",
459                                 value => $comments, force => 1);
460                 $config{rss}=0; # avoid preview writing an rss feed!
461                 $form->tmpl_param("page_preview",
462                         htmlize($page, $type,
463                         linkify($page, "",
464                         preprocess($page, $page,
465                         filter($page, $content)))));
466         }
467         else {
468                 $form->tmpl_param("page_preview", "");
469         }
470         $form->tmpl_param("page_conflict", "");
471         
472         if (! $form->submitted || $form->submitted eq "Preview" || 
473             ! $form->validate) {
474                 if ($form->field("do") eq "create") {
475                         my @page_locs;
476                         my $best_loc;
477                         if (! defined $from || ! length $from ||
478                             $from ne $form->field('from') ||
479                             $from=~/$config{wiki_file_prune_regexp}/ ||
480                             $from=~/^\// ||
481                             $form->submitted eq "Preview") {
482                                 @page_locs=$best_loc=$page;
483                         }
484                         else {
485                                 my $dir=$from."/";
486                                 $dir=~s![^/]+/+$!!;
487                                 
488                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
489                                     $page eq 'discussion') {
490                                         $best_loc="$from/$page";
491                                 }
492                                 else {
493                                         $best_loc=$dir.$page;
494                                 }
495                                 
496                                 push @page_locs, $dir.$page;
497                                 push @page_locs, "$from/$page";
498                                 while (length $dir) {
499                                         $dir=~s![^/]+/+$!!;
500                                         push @page_locs, $dir.$page;
501                                 }
502                         }
503
504                         @page_locs = grep {
505                                 ! exists $pagecase{lc $_} &&
506                                 ! page_locked($_, $session, 1)
507                         } @page_locs;
508                         
509                         if (! @page_locs) {
510                                 # hmm, someone else made the page in the
511                                 # meantime?
512                                 redirect($q, "$config{url}/".htmlpage($page));
513                                 return;
514                         }
515                         
516                         my @page_types;
517                         if (exists $hooks{htmlize}) {
518                                 @page_types=keys %{$hooks{htmlize}};
519                         }
520                         
521                         $form->tmpl_param("page_select", 1);
522                         $form->field(name => "page", type => 'select',
523                                 options => \@page_locs, value => $best_loc);
524                         $form->field(name => "type", type => 'select',
525                                 options => \@page_types);
526                         $form->title("creating ".pagetitle($page));
527                 }
528                 elsif ($form->field("do") eq "edit") {
529                         page_locked($page, $session);
530                         if (! defined $form->field('editcontent') || 
531                             ! length $form->field('editcontent')) {
532                                 my $content="";
533                                 if (exists $pagesources{$page}) {
534                                         $content=readfile(srcfile($pagesources{$page}));
535                                         $content=~s/\n/\r\n/g;
536                                 }
537                                 $form->field(name => "editcontent", value => $content,
538                                         force => 1);
539                         }
540                         $form->tmpl_param("page_select", 0);
541                         $form->field(name => "page", type => 'hidden');
542                         $form->field(name => "type", type => 'hidden');
543                         $form->title("editing ".pagetitle($page));
544                 }
545                 
546                 print $form->render(submit => \@buttons);
547         }
548         else {
549                 # save page
550                 page_locked($page, $session);
551                 
552                 my $content=$form->field('editcontent');
553
554                 $content=~s/\r\n/\n/g;
555                 $content=~s/\r/\n/g;
556                 writefile($file, $config{srcdir}, $content);
557                 
558                 my $message="web commit ";
559                 if (defined $session->param("name") && 
560                     length $session->param("name")) {
561                         $message.="by ".$session->param("name");
562                 }
563                 else {
564                         $message.="from $ENV{REMOTE_ADDR}";
565                 }
566                 if (defined $form->field('comments') &&
567                     length $form->field('comments')) {
568                         $message.=": ".$form->field('comments');
569                 }
570                 
571                 if ($config{rcs}) {
572                         if ($newfile) {
573                                 rcs_add($file);
574                         }
575                         # prevent deadlock with post-commit hook
576                         unlockwiki();
577                         # presumably the commit will trigger an update
578                         # of the wiki
579                         my $conflict=rcs_commit($file, $message,
580                                 $form->field("rcsinfo"));
581                 
582                         if (defined $conflict) {
583                                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
584                                         force => 1);
585                                 $form->tmpl_param("page_conflict", 1);
586                                 $form->field("editcontent", value => $conflict, force => 1);
587                                 $form->field(name => "comments", value => $form->field('comments'), force => 1);
588                                 $form->field("do", "edit)");
589                                 $form->tmpl_param("page_select", 0);
590                                 $form->field(name => "page", type => 'hidden');
591                                 $form->field(name => "type", type => 'hidden');
592                                 $form->title("editing $page");
593                                 print $form->render(submit => \@buttons);
594                                 return;
595                         }
596                 }
597                 else {
598                         require IkiWiki::Render;
599                         refresh();
600                         saveindex();
601                 }
602                 
603                 # The trailing question mark tries to avoid broken
604                 # caches and get the most recent version of the page.
605                 redirect($q, "$config{url}/".htmlpage($page)."?updated");
606         }
607 } #}}}
608
609 sub cgi () { #{{{
610         eval q{use CGI};
611         eval q{use CGI::Session};
612         
613         my $q=CGI->new;
614         
615         run_hooks(cgi => sub { shift->($q) });
616         
617         my $do=$q->param('do');
618         if (! defined $do || ! length $do) {
619                 my $error = $q->cgi_error;
620                 if ($error) {
621                         error("Request not processed: $error");
622                 }
623                 else {
624                         error("\"do\" parameter missing");
625                 }
626         }
627         
628         # Things that do not need a session.
629         if ($do eq 'recentchanges') {
630                 cgi_recentchanges($q);
631                 return;
632         }
633         elsif ($do eq 'hyperestraier') {
634                 cgi_hyperestraier();
635         }
636         
637         CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
638         
639         my $oldmask=umask(077);
640         my $session = CGI::Session->new("driver:DB_File", $q,
641                 { FileName => "$config{wikistatedir}/sessions.db" });
642         umask($oldmask);
643         
644         # Everything below this point needs the user to be signed in.
645         if (((! $config{anonok} || $do eq 'prefs') &&
646              (! defined $session->param("name") ||
647              ! userinfo_get($session->param("name"), "regdate"))) || $do eq 'signin') {
648                 cgi_signin($q, $session);
649         
650                 # Force session flush with safe umask.
651                 my $oldmask=umask(077);
652                 $session->flush;
653                 umask($oldmask);
654                 
655                 return;
656         }
657         
658         if ($do eq 'create' || $do eq 'edit') {
659                 cgi_editpage($q, $session);
660         }
661         elsif ($do eq 'prefs') {
662                 cgi_prefs($q, $session);
663         }
664         elsif ($do eq 'blog') {
665                 my $page=titlepage(decode_utf8($q->param('title')));
666                 # if the page already exists, munge it to be unique
667                 my $from=$q->param('from');
668                 my $add="";
669                 while (exists $pagecase{lc "$from/$page$add"}) {
670                         $add=1 unless length $add;
671                         $add++;
672                 }
673                 $q->param('page', $page.$add);
674                 # now run same as create
675                 $q->param('do', 'create');
676                 cgi_editpage($q, $session);
677         }
678         else {
679                 error("unknown do parameter");
680         }
681 } #}}}
682
683 1