11 $blosxom::version="is a proper perl module too much to ask?";
12 do "/usr/bin/markdown";
15 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
16 my ($srcdir, $templatedir, $destdir, %links, %oldlinks, %oldpagemtime,
17 %renderedfiles, %pagesources);
18 my $wiki_link_regexp=qr/\[\[([^\s]+)\]\]/;
19 my $wiki_file_regexp=qr/(^[-A-Za-z0-9_.:\/+]+$)/;
20 my $wiki_file_prune_regexp=qr!((^|/).svn/|\.\.|^\.|\/\.|\.html?$)!;
23 my $default_pagetype=".mdwn";
32 die "usage: ikiwiki [options] source templates dest\n";
37 print "Content-type: text/html\n\n";
47 print "@_\n" if $verbose;
53 return (stat($page))[9];
56 sub possibly_foolish_untaint ($) { #{{{
58 my ($untainted)=$tainted=~/(.*)/;
62 sub basename ($) { #{{{
69 sub dirname ($) { #{{{
76 sub pagetype ($) { #{{{
79 if ($page =~ /\.mdwn$/) {
87 sub pagename ($) { #{{{
90 my $type=pagetype($file);
92 $page=~s/\Q$type\E*$// unless $type eq 'unknown';
96 sub htmlpage ($) { #{{{
102 sub readfile ($) { #{{{
106 open (IN, "$file") || error("failed to read $file: $!");
112 sub writefile ($$) { #{{{
116 my $dir=dirname($file);
119 foreach my $s (split(m!/+!, $dir)) {
122 mkdir($d) || error("failed to create directory $d: $!");
127 open (OUT, ">$file") || error("failed to write $file: $!");
132 sub findlinks ($) { #{{{
136 while ($content =~ /$wiki_link_regexp/g) {
142 # Given a page and the text of a link on the page, determine which existing
143 # page that link best points to. Prefers pages under a subdirectory with
144 # the same name as the source page, failing that goes down the directory tree
145 # to the base looking for matching pages.
146 sub bestlink ($$) { #{{{
153 $l.="/" if length $l;
156 if (exists $links{$l}) {
157 #debug("for $page, \"$link\", use $l");
160 } while $cwd=~s!/?[^/]+$!!;
162 #print STDERR "warning: page $page, broken link: $link\n";
166 sub isinlinableimage ($) { #{{{
169 $file=~/\.(png|gif|jpg|jpeg)$/;
175 my $noimagelink=shift;
177 my $bestlink=bestlink($page, $link);
179 return $link if $page eq $bestlink;
181 # TODO BUG: %renderedfiles may not have it, if the linked to page
182 # was also added and isn't yet rendered! Note that this bug is
183 # masked by the bug mentioned below that makes all new files
185 if (! grep { $_ eq $bestlink } values %renderedfiles) {
186 $bestlink=htmlpage($bestlink);
188 if (! grep { $_ eq $bestlink } values %renderedfiles) {
189 return "<a href=\"$cgiurl?do=create&page=$link&from=$page\">?</a>$link"
192 $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
194 if (! $noimagelink && isinlinableimage($bestlink)) {
195 return "<img src=\"$bestlink\">";
197 return "<a href=\"$bestlink\">$link</a>";
200 sub linkify ($$) { #{{{
204 $content =~ s/$wiki_link_regexp/htmllink(pagename($file), $1)/eg;
209 sub htmlize ($$) { #{{{
213 if ($type eq '.mdwn') {
214 return Markdown::Markdown($content);
217 error("htmlization of $type not supported");
221 sub backlinks ($) { #{{{
225 foreach my $p (keys %links) {
226 next if bestlink($page, $p) eq $page;
227 if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
228 my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
230 # Trim common dir prefixes from both pages.
232 my $page_trimmed=$page;
234 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
236 $p_trimmed=~s/^\Q$dir\E// &&
237 $page_trimmed=~s/^\Q$dir\E//;
239 push @links, { url => $href, page => $p_trimmed };
246 sub parentlinks ($) { #{{{
253 foreach my $dir (reverse split("/", $page)) {
255 unshift @ret, { url => "$path$dir.html", page => $dir };
265 sub indexlink () { #{{{
266 return "<a href=\"$url\">$wikiname</a>/ ";
269 sub finalize ($$) { #{{{
273 my $title=basename($page);
276 my $template=HTML::Template->new(blind_cache => 1,
277 filename => "$templatedir/page.tmpl");
279 if (length $cgiurl) {
280 $template->param(editurl => "$cgiurl?do=edit&page=$page");
281 $template->param(recentchangesurl => "$cgiurl?do=recentchanges");
284 if (length $historyurl) {
286 $u=~s/\[\[\]\]/$pagesources{$page}/g;
287 $template->param(historyurl => $u);
293 wikiname => $wikiname,
294 parentlinks => [parentlinks($page)],
296 backlinks => [backlinks($page)],
299 return $template->output;
302 sub render ($) { #{{{
305 my $type=pagetype($file);
306 my $content=readfile("$srcdir/$file");
307 if ($type ne 'unknown') {
308 my $page=pagename($file);
310 $links{$page}=[findlinks($content)];
312 $content=linkify($content, $file);
313 $content=htmlize($type, $content);
314 $content=finalize($content, $page);
316 writefile("$destdir/".htmlpage($page), $content);
317 $oldpagemtime{$page}=time;
318 $renderedfiles{$page}=htmlpage($page);
322 writefile("$destdir/$file", $content);
323 $oldpagemtime{$file}=time;
324 $renderedfiles{$file}=$file;
328 sub loadindex () { #{{{
329 open (IN, "$srcdir/.ikiwiki/index") || return;
331 $_=possibly_foolish_untaint($_);
333 my ($mtime, $file, $rendered, @links)=split(' ', $_);
334 my $page=pagename($file);
335 $pagesources{$page}=$file;
336 $oldpagemtime{$page}=$mtime;
337 $oldlinks{$page}=[@links];
338 $links{$page}=[@links];
339 $renderedfiles{$page}=$rendered;
344 sub saveindex () { #{{{
345 if (! -d "$srcdir/.ikiwiki") {
346 mkdir("$srcdir/.ikiwiki");
348 open (OUT, ">$srcdir/.ikiwiki/index") || error("cannot write to index: $!");
349 foreach my $page (keys %oldpagemtime) {
350 print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
351 join(" ", @{$links{$page}})."\n"
352 if $oldpagemtime{$page};
357 sub rcs_update () { #{{{
358 if (-d "$srcdir/.svn") {
359 if (system("svn", "update", "--quiet", $srcdir) != 0) {
360 warn("svn update failed\n");
365 sub rcs_commit ($) { #{{{
368 if (-d "$srcdir/.svn") {
369 if (system("svn", "commit", "--quiet", "-m",
370 possibly_foolish_untaint($message), $srcdir) != 0) {
371 warn("svn commit failed\n");
376 sub rcs_add ($) { #{{{
379 if (-d "$srcdir/.svn") {
380 my $parent=dirname($file);
381 while (! -d "$srcdir/$parent/.svn") {
383 $parent=dirname($file);
386 if (system("svn", "add", "--quiet", "$srcdir/$file") != 0) {
387 warn("svn add failed\n");
392 sub rcs_recentchanges ($) { #{{{
396 eval q{use Date::Parse};
397 eval q{use Time::Duration};
399 if (-d "$srcdir/.svn") {
400 my $info=`LANG=C svn info $srcdir`;
401 my ($svn_url)=$info=~/^URL: (.*)$/m;
403 # FIXME: currently assumes that the wiki is somewhere
404 # under trunk in svn, doesn't support other layouts.
405 my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!;
407 my $div=qr/^--------------------+$/;
408 my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
410 my ($rev, $user, $when, @pages, @message);
411 foreach (`LANG=C svn log -v '$svn_url'`) {
413 if ($state eq 'start' && /$div/) {
416 elsif ($state eq 'header' && /$infoline/) {
419 $when=concise(ago(time - str2time($3)));
421 elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/(.+)$/) {
422 push @pages, { link => htmllink("", pagename($1), 1) }
425 elsif ($state eq 'header' && /^$/) {
428 elsif ($state eq 'body' && /$div/) {
429 push @ret, { rev => $rev, user => $user,
430 when => $when, message => [@message],
431 pages => [@pages] } if @pages;
432 return @ret if @ret >= $num;
435 $rev=$user=$when=undef;
438 elsif ($state eq 'body') {
439 push @message, {line => $_},
451 my $dir=dirname($file);
452 while (rmdir($dir)) {
457 sub refresh () { #{{{
458 # Find existing pages.
464 if (/$wiki_file_prune_regexp/) {
465 $File::Find::prune=1;
468 my ($f)=/$wiki_file_regexp/; # untaint
470 warn("skipping bad filename $_\n");
473 $f=~s/^\Q$srcdir\E\/?//;
475 $exists{pagename($f)}=1;
483 # check for added or removed pages
485 foreach my $file (@files) {
486 my $page=pagename($file);
487 if (! $oldpagemtime{$page}) {
488 debug("new page $page");
491 $pagesources{$page}=$file;
495 foreach my $page (keys %oldpagemtime) {
496 if (! $exists{$page}) {
497 debug("removing old page $page");
498 push @del, $renderedfiles{$page};
499 prune($destdir."/".$renderedfiles{$page});
500 delete $renderedfiles{$page};
501 $oldpagemtime{$page}=0;
502 delete $pagesources{$page};
506 # render any updated files
507 foreach my $file (@files) {
508 my $page=pagename($file);
510 if (! exists $oldpagemtime{$page} ||
511 mtime("$srcdir/$file") > $oldpagemtime{$page}) {
512 debug("rendering changed file $file");
518 # if any files were added or removed, check to see if each page
519 # needs an update due to linking to them
520 # TODO: inefficient; pages may get rendered above and again here;
521 # problem is the bestlink may have changed and we won't know until
524 FILE: foreach my $file (@files) {
525 my $page=pagename($file);
526 foreach my $f (@add, @del) {
528 foreach my $link (@{$links{$page}}) {
529 if (bestlink($page, $link) eq $p) {
530 debug("rendering $file, which links to $p");
540 # handle backlinks; if a page has added/removed links, update the
542 # TODO: inefficient; pages may get rendered above and again here;
543 # problem is the backlinks could be wrong in the first pass render
547 foreach my $file (keys %rendered, @del) {
548 my $page=pagename($file);
549 if (exists $links{$page}) {
550 foreach my $link (@{$links{$page}}) {
551 $link=bestlink($page, $link);
553 ! exists $oldlinks{$page} ||
554 ! grep { $_ eq $link } @{$oldlinks{$page}}) {
555 $linkchanged{$link}=1;
559 if (exists $oldlinks{$page}) {
560 foreach my $link (@{$oldlinks{$page}}) {
561 $link=bestlink($page, $link);
563 ! exists $links{$page} ||
564 ! grep { $_ eq $link } @{$links{$page}}) {
565 $linkchanged{$link}=1;
570 foreach my $link (keys %linkchanged) {
571 my $linkfile=$pagesources{$link};
572 if (defined $linkfile) {
573 debug("rendering $linkfile, to update its backlinks");
580 # Generates a C wrapper program for running ikiwiki in a specific way.
581 # The wrapper may be safely made suid.
582 sub gen_wrapper ($$) { #{{{
583 my ($svn, $rebuild)=@_;
585 eval q{use Cwd 'abs_path'};
586 $srcdir=abs_path($srcdir);
587 $destdir=abs_path($destdir);
588 my $this=abs_path($0);
590 error("$this doesn't seem to be executable");
593 my @params=($srcdir, $templatedir, $destdir, "--wikiname=$wikiname");
594 push @params, "--verbose" if $verbose;
595 push @params, "--rebuild" if $rebuild;
596 push @params, "--nosvn" if !$svn;
597 push @params, "--cgi" if $cgi;
598 push @params, "--url=$url" if $url;
599 push @params, "--cgiurl=$cgiurl" if $cgiurl;
600 push @params, "--historyurl=$historyurl" if $historyurl;
601 push @params, "--anonok" if $anonok;
602 my $params=join(" ", @params);
604 foreach my $p ($this, $this, @params) {
610 push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
611 CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
612 HTTP_COOKIE} if $cgi;
614 foreach my $var (@envsave) {
616 if ((s=getenv("$var")))
617 asprintf(&newenviron[i++], "%s=%s", "$var", s);
621 open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");;
623 /* A wrapper for ikiwiki, can be safely made suid. */
630 extern char **environ;
632 int main (int argc, char **argv) {
633 /* Sanitize environment. */
635 char *newenviron[$#envsave+3];
638 newenviron[i++]="HOME=$ENV{HOME}";
642 if (argc == 2 && strcmp(argv[1], "--params") == 0) {
643 printf("$params\\n");
648 perror("failed to run $this");
653 if (system("gcc", "ikiwiki-wrap.c", "-o", "ikiwiki-wrap") != 0) {
654 error("failed to compile ikiwiki-wrap.c");
656 unlink("ikiwiki-wrap.c");
657 print "successfully generated ikiwiki-wrap\n";
661 sub cgi_recentchanges ($) { #{{{
664 my $template=HTML::Template->new(
665 filename => "$templatedir/recentchanges.tmpl");
667 title => "RecentChanges",
669 wikiname => $wikiname,
670 changelog => [rcs_recentchanges(100)],
672 print $q->header, $template->output;
675 sub cgi_signin ($$) { #{{{
679 eval q{use CGI::FormBuilder};
680 my $form = CGI::FormBuilder->new(
681 title => "$wikiname signin",
682 fields => [qw(do page name password confirm_password email)],
687 confirm_password => {
688 perl => q{eq $form->field("password")},
695 action => $q->request_uri,
696 template => (-e "$templatedir/signin.tmpl" ? "$templatedir/signin.tmpl" : "")
699 $form->sessionid($session->id);
700 $form->field(name => "name", required => 0);
701 $form->field(name => "do", type => "hidden");
702 $form->field(name => "page", type => "hidden");
703 $form->field(name => "password", type => "password", required => 0);
704 $form->field(name => "confirm_password", type => "password", required => 0);
705 $form->field(name => "email", required => 0);
706 if ($session->param("name")) {
707 $form->field(name => "name", value => $session->param("name"));
709 if ($q->param("do") ne "signin") {
710 $form->text("You need to log in before you can edit pages.");
713 if ($form->submitted) {
714 # Set required fields based on how form was submitted.
716 "Login" => [qw(name password)],
717 "Register" => [qw(name password confirm_password email)],
718 "Mail Password" => [qw(name)],
720 foreach my $opt (@{$required{$form->submitted}}) {
721 $form->field(name => $opt, required => 1);
724 # Validate password differently depending on how form was
726 if ($form->submitted eq 'Login') {
730 # TODO get real user password
736 $form->field(name => "password", validate => 'VALUE');
740 # Comments only shown first time.
741 $form->field(name => "name", comment => "use FirstnameLastName");
742 $form->field(name => "confirm_password", comment => "(only needed");
743 $form->field(name => "email", comment => "for registration)");
746 if ($form->submitted && $form->validate) {
747 if ($form->submitted eq 'Login') {
748 $session->param("name", $form->field("name"));
749 if (defined $form->field("do") &&
750 $form->field("do") ne 'signin') {
752 "$cgiurl?do=".$form->field("do").
753 "&page=".$form->field("page"));
756 print $q->redirect($url);
759 elsif ($form->submitted eq 'Register') {
760 # TODO: save registration info
761 $form->field(name => "confirm_password", type => "hidden");
762 $form->field(name => "email", type => "hidden");
763 $form->text("Registration successful. Now you can Login.");
764 print $form->render(submit => ["Login"]);;
766 elsif ($form->submitted eq 'Mail Password') {
768 $form->text("Your password has been emailed to you.");
769 print $form->render(submit => ["Login", "Register", "Mail Password"]);;
773 print $form->render(submit => ["Login", "Register", "Mail Password"]);;
779 eval q{use CGI::Session};
783 my $do=$q->param('do');
784 if (! defined $do || ! length $do) {
785 error("\"do\" parameter missing");
788 # This does not need a session.
789 if ($do eq 'recentchanges') {
790 cgi_recentchanges($q);
794 # session id has to be _sessionid for CGI::FormBuilder to work.
795 # TODO: stop having the formbuilder emit cookies and change session
796 # id to something else.
797 CGI::Session->name("_sessionid");
798 my $session = CGI::Session->new(undef, $q,
799 { Directory=> "$srcdir/.ikiwiki/sessions" });
801 # Everything below this point needs the user to be signed in.
802 if ((! $anonok && ! defined $session->param("name")) || $do eq 'signin') {
803 cgi_signin($q, $session);
807 my ($page)=$q->param('page')=~/$wiki_file_regexp/;
808 if (! defined $page || ! length $page || $page ne $q->param('page') ||
809 $page=~/$wiki_file_prune_regexp/ || $page=~/^\//) {
810 error("bad page name");
814 my $action=$q->request_uri;
817 if ($do eq 'create') {
818 if (exists $pagesources{lc($page)}) {
819 # hmm, someone else made the page in the meantime?
820 print $q->redirect("$url/".htmlpage($page));
824 my ($from)=$q->param('from')=~/$wiki_file_regexp/;
825 if (! defined $from || ! length $from ||
826 $from ne $q->param('from') ||
827 $from=~/$wiki_file_prune_regexp/ || $from=~/^\//) {
833 push @page_locs, $dir.$page;
834 push @page_locs, "$from/$page";
835 while (length $dir) {
837 push @page_locs, $dir.$page;
841 $q->param("do", "save");
843 $q->start_html("Creating $page"),
844 $q->h1(indexlink()." Creating $page"),
845 $q->start_form(-action => $action),
847 "Select page location:",
848 $q->popup_menu('page', \@page_locs),
849 $q->textarea(-name => 'content',
854 "Optional comment about this change:",
856 $q->textfield(-name => "comments", -size => 80),
858 $q->submit("Save Page"),
862 elsif ($do eq 'edit') {
864 if (exists $pagesources{lc($page)}) {
865 $content=readfile("$srcdir/$pagesources{lc($page)}");
866 $content=~s/\n/\r\n/g;
868 $q->param("do", "save");
870 $q->start_html("Editing $page"),
871 $q->h1(indexlink()." Editing $page"),
872 $q->start_form(-action => $action),
875 $q->textarea(-name => 'content',
876 -default => $content,
880 "Optional comment about this change:",
882 $q->textfield(-name => "comments", -size => 80),
884 $q->submit("Save Page"),
888 elsif ($do eq 'save') {
889 my $file=$page.$default_pagetype;
891 if (exists $pagesources{lc($page)}) {
892 $file=$pagesources{lc($page)};
896 my $content=$q->param('content');
897 $content=~s/\r\n/\n/g;
899 writefile("$srcdir/$file", $content);
901 my $message="web commit from $ENV{REMOTE_ADDR}";
902 if (defined $q->param('comments')) {
903 $message.=": ".$q->param('comments');
910 # presumably the commit will trigger an update
912 rcs_commit($message);
918 print $q->redirect("$url/".htmlpage($page));
921 error("unknown do parameter");
928 if (grep /^-/, @ARGV) {
929 eval {use Getopt::Long};
931 "wikiname=s" => \$wikiname,
932 "verbose|v" => \$verbose,
933 "rebuild" => \$rebuild,
934 "wrapper" => \$wrapper,
936 "anonok!" => \$anonok,
939 "cgiurl=s" => \$cgiurl,
940 "historyurl=s" => \$historyurl,
943 usage() unless @ARGV == 3;
944 ($srcdir) = possibly_foolish_untaint(shift);
945 ($templatedir) = possibly_foolish_untaint(shift);
946 ($destdir) = possibly_foolish_untaint(shift);
948 if ($cgi && ! length $url) {
949 error("Must specify url to wiki with --url when using --cgi");
952 gen_wrapper($svn, $rebuild) if $wrapper;
955 loadindex() unless $rebuild;
960 rcs_update() if $svn;