* Atom feed support based on a patch by Clint Adams.
[ikiwiki] / 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 ($config{httpauth}) {
319                 $form->field(name => "password", type => "hidden");
320                 $form->field(name => "confirm_password", type => "hidden");
321         }
322         
323         if (! $form->submitted) {
324                 $form->field(name => "email", force => 1,
325                         value => userinfo_get($user_name, "email"));
326                 $form->field(name => "subscriptions", force => 1,
327                         value => userinfo_get($user_name, "subscriptions"));
328                 $form->field(name => "locked_pages", force => 1,
329                         value => userinfo_get($user_name, "locked_pages"));
330         }
331         
332         decode_form_utf8($form);
333         
334         if ($form->submitted eq 'Logout') {
335                 $session->delete();
336                 redirect($q, $config{url});
337                 return;
338         }
339         elsif ($form->submitted eq 'Cancel') {
340                 redirect($q, $config{url});
341                 return;
342         }
343         elsif ($form->submitted eq "Save Preferences" && $form->validate) {
344                 foreach my $field (qw(password email subscriptions locked_pages)) {
345                         if (length $form->field($field)) {
346                                 userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
347                         }
348                 }
349                 $form->text("Preferences saved.");
350         }
351         
352         printheader($session);
353         print misctemplate($form->title, $form->render(submit => \@buttons));
354 } #}}}
355
356 sub cgi_editpage ($$) { #{{{
357         my $q=shift;
358         my $session=shift;
359
360         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
361         my @buttons=("Save Page", "Preview", "Cancel");
362         
363         eval q{use CGI::FormBuilder; use CGI::FormBuilder::Template::HTML};
364         my $renderer=CGI::FormBuilder::Template::HTML->new(
365                 fields => \@fields,
366                 template_params("editpage.tmpl"),
367         );
368         run_hooks(pagetemplate => sub {
369                 shift->(page => "", destpage => "", template => $renderer->engine);
370         });
371         my $form = CGI::FormBuilder->new(
372                 fields => \@fields,
373                 header => 1,
374                 charset => "utf-8",
375                 method => 'POST',
376                 validate => {
377                         editcontent => '/.+/',
378                 },
379                 required => [qw{editcontent}],
380                 javascript => 0,
381                 params => $q,
382                 action => $config{cgiurl},
383                 table => 0,
384                 template => $renderer,
385         );
386         
387         decode_form_utf8($form);
388         
389         # This untaint is safe because titlepage removes any problematic
390         # characters.
391         my ($page)=$form->field('page');
392         $page=titlepage(possibly_foolish_untaint($page));
393         if (! defined $page || ! length $page ||
394             $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
395                 error("bad page name");
396         }
397         
398         my $from;
399         if (defined $form->field('from')) {
400                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
401         }
402         
403         my $file;
404         my $type;
405         if (exists $pagesources{$page}) {
406                 $file=$pagesources{$page};
407                 $type=pagetype($file);
408         }
409         else {
410                 $type=$form->param('type');
411                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
412                         $type=possibly_foolish_untaint($type);
413                 }
414                 elsif (defined $from) {
415                         # favor the type of linking page
416                         $type=pagetype($pagesources{$from});
417                 }
418                 $type=$config{default_pageext} unless defined $type;
419                 $file=$page.".".$type;
420         }
421
422         my $newfile=0;
423         if (! -e "$config{srcdir}/$file") {
424                 $newfile=1;
425         }
426
427         $form->field(name => "do", type => 'hidden');
428         $form->field(name => "from", type => 'hidden');
429         $form->field(name => "rcsinfo", type => 'hidden');
430         $form->field(name => "subpage", type => 'hidden');
431         $form->field(name => "page", value => $page, force => 1);
432         $form->field(name => "type", value => $type, force => 1);
433         $form->field(name => "comments", type => "text", size => 80);
434         $form->field(name => "editcontent", type => "textarea", rows => 20,
435                 cols => 80);
436         $form->tmpl_param("can_commit", $config{rcs});
437         $form->tmpl_param("indexlink", indexlink());
438         $form->tmpl_param("helponformattinglink",
439                 htmllink("", "", "HelpOnFormatting", 1));
440         $form->tmpl_param("baseurl", baseurl());
441         if (! $form->submitted) {
442                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
443                         force => 1);
444         }
445         
446         if ($form->submitted eq "Cancel") {
447                 if ($newfile && defined $from) {
448                         redirect($q, "$config{url}/".htmlpage($from));
449                 }
450                 elsif ($newfile) {
451                         redirect($q, $config{url});
452                 }
453                 else {
454                         redirect($q, "$config{url}/".htmlpage($page));
455                 }
456                 return;
457         }
458         elsif ($form->submitted eq "Preview") {
459                 my $content=$form->field('editcontent');
460                 my $comments=$form->field('comments');
461                 $form->field(name => "editcontent",
462                                 value => $content, force => 1);
463                 $form->field(name => "comments",
464                                 value => $comments, force => 1);
465                 $config{rss}=$config{atom}=0; # avoid preview writing a feed!
466                 $form->tmpl_param("page_preview",
467                         htmlize($page, $type,
468                         linkify($page, "",
469                         preprocess($page, $page,
470                         filter($page, $content)))));
471         }
472         else {
473                 $form->tmpl_param("page_preview", "");
474         }
475         $form->tmpl_param("page_conflict", "");
476         
477         if (! $form->submitted || $form->submitted eq "Preview" || 
478             ! $form->validate) {
479                 if ($form->field("do") eq "create") {
480                         my @page_locs;
481                         my $best_loc;
482                         if (! defined $from || ! length $from ||
483                             $from ne $form->field('from') ||
484                             $from=~/$config{wiki_file_prune_regexp}/ ||
485                             $from=~/^\// ||
486                             $form->submitted eq "Preview") {
487                                 @page_locs=$best_loc=$page;
488                         }
489                         else {
490                                 my $dir=$from."/";
491                                 $dir=~s![^/]+/+$!!;
492                                 
493                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
494                                     $page eq 'discussion') {
495                                         $best_loc="$from/$page";
496                                 }
497                                 else {
498                                         $best_loc=$dir.$page;
499                                 }
500                                 
501                                 push @page_locs, $dir.$page;
502                                 push @page_locs, "$from/$page";
503                                 while (length $dir) {
504                                         $dir=~s![^/]+/+$!!;
505                                         push @page_locs, $dir.$page;
506                                 }
507                         }
508
509                         @page_locs = grep {
510                                 ! exists $pagecase{lc $_} &&
511                                 ! page_locked($_, $session, 1)
512                         } @page_locs;
513                         
514                         if (! @page_locs) {
515                                 # hmm, someone else made the page in the
516                                 # meantime?
517                                 redirect($q, "$config{url}/".htmlpage($page));
518                                 return;
519                         }
520                         
521                         my @page_types;
522                         if (exists $hooks{htmlize}) {
523                                 @page_types=keys %{$hooks{htmlize}};
524                         }
525                         
526                         $form->tmpl_param("page_select", 1);
527                         $form->field(name => "page", type => 'select',
528                                 options => \@page_locs, value => $best_loc);
529                         $form->field(name => "type", type => 'select',
530                                 options => \@page_types);
531                         $form->title("creating ".pagetitle($page));
532                 }
533                 elsif ($form->field("do") eq "edit") {
534                         page_locked($page, $session);
535                         if (! defined $form->field('editcontent') || 
536                             ! length $form->field('editcontent')) {
537                                 my $content="";
538                                 if (exists $pagesources{$page}) {
539                                         $content=readfile(srcfile($pagesources{$page}));
540                                         $content=~s/\n/\r\n/g;
541                                 }
542                                 $form->field(name => "editcontent", value => $content,
543                                         force => 1);
544                         }
545                         $form->tmpl_param("page_select", 0);
546                         $form->field(name => "page", type => 'hidden');
547                         $form->field(name => "type", type => 'hidden');
548                         $form->title("editing ".pagetitle($page));
549                 }
550                 
551                 print $form->render(submit => \@buttons);
552         }
553         else {
554                 # save page
555                 page_locked($page, $session);
556                 
557                 my $content=$form->field('editcontent');
558
559                 $content=~s/\r\n/\n/g;
560                 $content=~s/\r/\n/g;
561                 writefile($file, $config{srcdir}, $content);
562                 
563                 my $message="web commit ";
564                 if (defined $session->param("name") && 
565                     length $session->param("name")) {
566                         $message.="by ".$session->param("name");
567                 }
568                 else {
569                         $message.="from $ENV{REMOTE_ADDR}";
570                 }
571                 if (defined $form->field('comments') &&
572                     length $form->field('comments')) {
573                         $message.=": ".$form->field('comments');
574                 }
575                 
576                 if ($config{rcs}) {
577                         if ($newfile) {
578                                 rcs_add($file);
579                         }
580                         # prevent deadlock with post-commit hook
581                         unlockwiki();
582                         # presumably the commit will trigger an update
583                         # of the wiki
584                         my $conflict=rcs_commit($file, $message,
585                                 $form->field("rcsinfo"));
586                 
587                         if (defined $conflict) {
588                                 $form->field(name => "rcsinfo", value => rcs_prepedit($file),
589                                         force => 1);
590                                 $form->tmpl_param("page_conflict", 1);
591                                 $form->field("editcontent", value => $conflict, force => 1);
592                                 $form->field(name => "comments", value => $form->field('comments'), force => 1);
593                                 $form->field("do", "edit)");
594                                 $form->tmpl_param("page_select", 0);
595                                 $form->field(name => "page", type => 'hidden');
596                                 $form->field(name => "type", type => 'hidden');
597                                 $form->title("editing $page");
598                                 print $form->render(submit => \@buttons);
599                                 return;
600                         }
601                 }
602                 else {
603                         require IkiWiki::Render;
604                         refresh();
605                         saveindex();
606                 }
607                 
608                 # The trailing question mark tries to avoid broken
609                 # caches and get the most recent version of the page.
610                 redirect($q, "$config{url}/".htmlpage($page)."?updated");
611         }
612 } #}}}
613
614 sub cgi () { #{{{
615         eval q{use CGI};
616         eval q{use CGI::Session};
617         
618         my $q=CGI->new;
619         
620         run_hooks(cgi => sub { shift->($q) });
621         
622         my $do=$q->param('do');
623         if (! defined $do || ! length $do) {
624                 my $error = $q->cgi_error;
625                 if ($error) {
626                         error("Request not processed: $error");
627                 }
628                 else {
629                         error("\"do\" parameter missing");
630                 }
631         }
632         
633         # Things that do not need a session.
634         if ($do eq 'recentchanges') {
635                 cgi_recentchanges($q);
636                 return;
637         }
638         elsif ($do eq 'hyperestraier') {
639                 cgi_hyperestraier();
640         }
641         
642         CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
643         
644         my $oldmask=umask(077);
645         my $session = CGI::Session->new("driver:DB_File", $q,
646                 { FileName => "$config{wikistatedir}/sessions.db" });
647         umask($oldmask);
648         
649         # Everything below this point needs the user to be signed in.
650         if (((! $config{anonok} || $do eq 'prefs') &&
651              (! $config{httpauth}) &&
652              (! defined $session->param("name") ||
653              ! userinfo_get($session->param("name"), "regdate"))) || $do eq 'signin') {
654                 cgi_signin($q, $session);
655         
656                 # Force session flush with safe umask.
657                 my $oldmask=umask(077);
658                 $session->flush;
659                 umask($oldmask);
660                 
661                 return;
662         }
663
664         if ($config{httpauth} && (! defined $session->param("name"))) {
665                 if (! defined $q->remote_user()) {
666                         error("Could not determine authenticated username.");
667                 }
668                 else {
669                         $session->param("name", $q->remote_user());
670                         if (!userinfo_get($session->param("name"),"regdate")) {
671                                 userinfo_setall($session->param("name"), {
672                                         email => "",
673                                         password => "",
674                                         regdate=>time,
675                                 });
676                         }
677                 }
678         }
679         
680         if ($do eq 'create' || $do eq 'edit') {
681                 cgi_editpage($q, $session);
682         }
683         elsif ($do eq 'prefs') {
684                 cgi_prefs($q, $session);
685         }
686         elsif ($do eq 'blog') {
687                 my $page=titlepage(decode_utf8($q->param('title')));
688                 # if the page already exists, munge it to be unique
689                 my $from=$q->param('from');
690                 my $add="";
691                 while (exists $pagecase{lc "$from/$page$add"}) {
692                         $add=1 unless length $add;
693                         $add++;
694                 }
695                 $q->param('page', $page.$add);
696                 # now run same as create
697                 $q->param('do', 'create');
698                 cgi_editpage($q, $session);
699         }
700         else {
701                 error("unknown do parameter");
702         }
703 } #}}}
704
705 1