]> sipb.mit.edu Git - ikiwiki.git/blobdiff - ikiwiki
At Branden's request, clean up the hardcoded ".ikiwiki" everywhere, and add
[ikiwiki.git] / ikiwiki
diff --git a/ikiwiki b/ikiwiki
index 14c50a9afa3bb05ecdba44d307ccc3d740b10671..51f324d13abd56ed573ac3861683af89dbe9f19a 100755 (executable)
--- a/ikiwiki
+++ b/ikiwiki
@@ -1,4 +1,7 @@
 #!/usr/bin/perl -T
+
+eval 'exec /usr/bin/perl -T -S $0 ${1+"$@"}'
+    if 0; # not running under some shell
 $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
 
 use warnings;
@@ -13,7 +16,7 @@ my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources);
 # Holds global config settings, also used by some modules.
 our %config=( #{{{
        wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
-       wiki_link_regexp => qr/\[\[([^\s]+)\]\]/,
+       wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
        wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
        verbose => 0,
        wikiname => "wiki",
@@ -23,14 +26,16 @@ our %config=( #{{{
        url => '',
        cgiurl => '',
        historyurl => '',
+       diffurl => '',
        anonok => 0,
        rebuild => 0,
        wrapper => undef,
        wrappermode => undef,
        srcdir => undef,
        destdir => undef,
-       templatedir => undef,
+       templatedir => "/usr/share/ikiwiki/templates",
        setup => undef,
+       adminuser => undef,
 ); #}}}
 
 GetOptions( #{{{
@@ -46,24 +51,32 @@ GetOptions( #{{{
        "url=s" => \$config{url},
        "cgiurl=s" => \$config{cgiurl},
        "historyurl=s" => \$config{historyurl},
+       "diffurl=s" => \$config{diffurl},
        "exclude=s@" => sub {
                $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
        },
+       "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] },
+       "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) },
 ) || usage();
 
 if (! $config{setup}) {
-       usage() unless @ARGV == 3;
+       usage() unless @ARGV == 2;
        $config{srcdir} = possibly_foolish_untaint(shift);
-       $config{templatedir} = possibly_foolish_untaint(shift);
        $config{destdir} = possibly_foolish_untaint(shift);
+       checkoptions();
+}
+#}}}
+
+sub checkoptions { #{{{
        if ($config{cgi} && ! length $config{url}) {
                error("Must specify url to wiki with --url when using --cgi");
        }
-}
-#}}}
+       $config{wikistatedir}="$config{srcdir}/.ikiwiki"
+               unless exists $config{wikistatedir};
+} #}}}
 
 sub usage { #{{{
-       die "usage: ikiwiki [options] source templates dest\n";
+       die "usage: ikiwiki [options] source dest\n";
 } #}}}
 
 sub error { #{{{
@@ -324,9 +337,10 @@ sub indexlink () { #{{{
        return "<a href=\"$config{url}\">$config{wikiname}</a>";
 } #}}}
 
-sub finalize ($$) { #{{{
+sub finalize ($$$) { #{{{
        my $content=shift;
        my $page=shift;
+       my $mtime=shift;
 
        my $title=basename($page);
        $title=~s/_/ /g;
@@ -336,6 +350,7 @@ sub finalize ($$) { #{{{
        
        if (length $config{cgiurl}) {
                $template->param(editurl => "$config{cgiurl}?do=edit&page=$page");
+               $template->param(prefsurl => "$config{cgiurl}?do=prefs");
                if ($config{svn}) {
                        $template->param(recentchangesurl => "$config{cgiurl}?do=recentchanges");
                }
@@ -343,7 +358,7 @@ sub finalize ($$) { #{{{
 
        if (length $config{historyurl}) {
                my $u=$config{historyurl};
-               $u=~s/\[\[\]\]/$pagesources{$page}/g;
+               $u=~s/\[\[file\]\]/$pagesources{$page}/g;
                $template->param(historyurl => $u);
        }
        
@@ -354,6 +369,7 @@ sub finalize ($$) { #{{{
                content => $content,
                backlinks => [backlinks($page)],
                discussionlink => htmllink($page, "Discussion", 1, 1),
+               mtime => scalar(gmtime($mtime)),
        );
        
        return $template->output;
@@ -385,7 +401,8 @@ sub render ($) { #{{{
                
                $content=linkify($content, $page);
                $content=htmlize($type, $content);
-               $content=finalize($content, $page);
+               $content=finalize($content, $page,
+                       mtime("$config{srcdir}/$file"));
                
                check_overwrite("$config{destdir}/".htmlpage($page), $page);
                writefile("$config{destdir}/".htmlpage($page), $content);
@@ -404,10 +421,11 @@ sub render ($) { #{{{
 sub lockwiki () { #{{{
        # Take an exclusive lock on the wiki to prevent multiple concurrent
        # run issues. The lock will be dropped on program exit.
-       if (! -d "$config{srcdir}/.ikiwiki") {
-               mkdir("$config{srcdir}/.ikiwiki");
+       if (! -d $config{wikistatedir}) {
+               mkdir($config{wikistatedir});
        }
-       open(WIKILOCK, ">$config{srcdir}/.ikiwiki/lockfile") || error ("cannot write to lockfile: $!");
+       open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
+               error ("cannot write to $config{wikistatedir}/lockfile: $!");
        if (! flock(WIKILOCK, 2 | 4)) {
                debug("wiki seems to be locked, waiting for lock");
                my $wait=600; # arbitrary, but don't hang forever to 
@@ -425,7 +443,7 @@ sub unlockwiki () { #{{{
 } #}}}
 
 sub loadindex () { #{{{
-       open (IN, "$config{srcdir}/.ikiwiki/index") || return;
+       open (IN, "$config{wikistatedir}/index") || return;
        while (<IN>) {
                $_=possibly_foolish_untaint($_);
                chomp;
@@ -441,10 +459,11 @@ sub loadindex () { #{{{
 } #}}}
 
 sub saveindex () { #{{{
-       if (! -d "$config{srcdir}/.ikiwiki") {
-               mkdir("$config{srcdir}/.ikiwiki");
+       if (! -d $config{wikistatedir}) {
+               mkdir($config{wikistatedir});
        }
-       open (OUT, ">$config{srcdir}/.ikiwiki/index") || error("cannot write to index: $!");
+       open (OUT, ">$config{wikistatedir}/index") || 
+               error("cannot write to $config{wikistatedir}/index: $!");
        foreach my $page (keys %oldpagemtime) {
                print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
                        join(" ", @{$links{$page}})."\n"
@@ -489,7 +508,7 @@ sub rcs_commit ($$$) { #{{{
                # else since rcs_prepedit was called.
                my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
                my $rev=svn_info("Revision", "$config{srcdir}/$file");
-               if ($rev != $oldrev) {
+               if (defined $rev && defined $oldrev && $rev != $oldrev) {
                        # Merge their changes into the file that we've
                        # changed.
                        chdir($config{srcdir}); # svn merge wants to be here
@@ -501,7 +520,7 @@ sub rcs_commit ($$$) { #{{{
 
                if (system("svn", "commit", "--quiet", "-m",
                           possibly_foolish_untaint($message),
-                          "$config{srcdir}/$file") != 0) {
+                          "$config{srcdir}") != 0) {
                        my $conflict=readfile("$config{srcdir}/$file");
                        if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) {
                                warn("svn revert failed\n");
@@ -567,9 +586,16 @@ sub rcs_recentchanges ($) { #{{{
                                $user=$2;
                                $when=concise(ago(time - str2time($3)));
                        }
-                       elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/(.+)$/) {
-                               push @pages, { link => htmllink("", pagename($1), 1) }
-                                       if length $1;
+                       elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/([^ ]+)(?:$|\s)/) {
+                               my $file=$1;
+                               my $diffurl=$config{diffurl};
+                               $diffurl=~s/\[\[file\]\]/$file/g;
+                               $diffurl=~s/\[\[r1\]\]/$rev - 1/eg;
+                               $diffurl=~s/\[\[r2\]\]/$rev/g;
+                               push @pages, {
+                                       link => htmllink("", pagename($file), 1),
+                                       diffurl => $diffurl,
+                               } if length $file;
                        }
                        elsif ($state eq 'header' && /^$/) {
                                $state='body';
@@ -589,7 +615,8 @@ sub rcs_recentchanges ($) { #{{{
                                        user => htmllink("", $user, 1),
                                        committype => $committype,
                                        when => $when, message => [@message],
-                                       pages => [@pages] } if @pages;
+                                       pages => [@pages],
+                               } if @pages;
                                return @ret if @ret >= $num;
                                
                                $state='header';
@@ -739,8 +766,7 @@ FILE:               foreach my $file (@files) {
        }
 } #}}}
 
-sub gen_wrapper (@) { #{{{
-       my %config=(@_);
+sub gen_wrapper () { #{{{
        eval q{use Cwd 'abs_path'};
        $config{srcdir}=abs_path($config{srcdir});
        $config{destdir}=abs_path($config{destdir});
@@ -753,8 +779,9 @@ sub gen_wrapper (@) { #{{{
                error("cannot create a wrapper that uses a setup file");
        }
        
-       my @params=($config{srcdir}, $config{templatedir}, $config{destdir},
-               "--wikiname=$config{wikiname}");
+       my @params=($config{srcdir}, $config{destdir},
+               "--wikiname=$config{wikiname}",
+               "--templatedir=$config{templatedir}");
        push @params, "--verbose" if $config{verbose};
        push @params, "--rebuild" if $config{rebuild};
        push @params, "--nosvn" if !$config{svn};
@@ -762,7 +789,9 @@ sub gen_wrapper (@) { #{{{
        push @params, "--url=$config{url}" if length $config{url};
        push @params, "--cgiurl=$config{cgiurl}" if length $config{cgiurl};
        push @params, "--historyurl=$config{historyurl}" if length $config{historyurl};
+       push @params, "--diffurl=$config{diffurl}" if length $config{diffurl};
        push @params, "--anonok" if $config{anonok};
+       push @params, "--adminuser=$_" foreach @{$config{adminuser}};
        my $params=join(" ", @params);
        my $call='';
        foreach my $p ($this, $this, @params) {
@@ -861,26 +890,46 @@ sub userinfo_get ($$) { #{{{
        my $field=shift;
 
        eval q{use Storable};
-       my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
+       my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
        if (! defined $userdata || ! ref $userdata || 
-           ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
+           ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
+            ! exists $userdata->{$user}->{$field}) {
                return "";
        }
        return $userdata->{$user}->{$field};
 } #}}}
 
-sub userinfo_set ($$) { #{{{
+sub userinfo_set ($$$) { #{{{
+       my $user=shift;
+       my $field=shift;
+       my $value=shift;
+       
+       eval q{use Storable};
+       my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
+       if (! defined $userdata || ! ref $userdata || 
+           ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
+               return "";
+       }
+       
+       $userdata->{$user}->{$field}=$value;
+       my $oldmask=umask(077);
+       my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
+       umask($oldmask);
+       return $ret;
+} #}}}
+
+sub userinfo_setall ($$) { #{{{
        my $user=shift;
        my $info=shift;
        
        eval q{use Storable};
-       my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
+       my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
        if (! defined $userdata || ! ref $userdata) {
                $userdata={};
        }
        $userdata->{$user}=$info;
        my $oldmask=umask(077);
-       my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb");
+       my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
        umask($oldmask);
        return $ret;
 } #}}}
@@ -891,7 +940,7 @@ sub cgi_signin ($$) { #{{{
 
        eval q{use CGI::FormBuilder};
        my $form = CGI::FormBuilder->new(
-               title => "$config{wikiname} signin",
+               title => "signin",
                fields => [qw(do page from name password confirm_password email)],
                header => 1,
                method => 'POST',
@@ -918,7 +967,7 @@ sub cgi_signin ($$) { #{{{
        $form->field(name => "confirm_password", type => "password", required => 0);
        $form->field(name => "email", required => 0);
        if ($q->param("do") ne "signin") {
-               $form->text("You need to log in before you can edit pages.");
+               $form->text("You need to log in first.");
        }
        
        if ($form->submitted) {
@@ -996,7 +1045,7 @@ sub cgi_signin ($$) { #{{{
                }
                elsif ($form->submitted eq 'Register') {
                        my $user_name=$form->field('name');
-                       if (userinfo_set($user_name, {
+                       if (userinfo_setall($user_name, {
                                           'email' => $form->field('email'),
                                           'password' => $form->field('password'),
                                           'regdate' => time
@@ -1045,6 +1094,128 @@ sub cgi_signin ($$) { #{{{
        }
 } #}}}
 
+sub is_admin ($) { #{{{
+       my $user_name=shift;
+
+       return grep { $_ eq $user_name } @{$config{adminuser}};
+} #}}}
+
+sub glob_match ($$) { #{{{
+       my $page=shift;
+       my $glob=shift;
+
+       # turn glob into safe regexp
+       $glob=quotemeta($glob);
+       $glob=~s/\\\*/.*/g;
+       $glob=~s/\\\?/./g;
+       $glob=~s!\\/!/!g;
+       
+       $page=~/^$glob$/i;
+} #}}}
+
+sub globlist_match ($$) { #{{{
+       my $page=shift;
+       my @globlist=split(" ", shift);
+
+       # check any negated globs first
+       foreach my $glob (@globlist) {
+               return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
+       }
+
+       foreach my $glob (@globlist) {
+               return 1 if glob_match($page, $glob);
+       }
+       
+       return 0;
+} #}}}
+
+sub page_locked ($$;$) { #{{{
+       my $page=shift;
+       my $session=shift;
+       my $nonfatal=shift;
+       
+       my $user=$session->param("name");
+       return if length $user && is_admin($user);
+
+       foreach my $admin (@{$config{adminuser}}) {
+               my $locked_pages=userinfo_get($admin, "locked_pages");
+               if (globlist_match($page, userinfo_get($admin, "locked_pages"))) {
+                       return 1 if $nonfatal;
+                       error(htmllink("", $page, 1)." is locked by ".
+                             htmllink("", $admin, 1)." and cannot be edited.");
+               }
+       }
+
+       return 0;
+} #}}}
+
+sub cgi_prefs ($$) { #{{{
+       my $q=shift;
+       my $session=shift;
+
+       eval q{use CGI::FormBuilder};
+       my $form = CGI::FormBuilder->new(
+               title => "preferences",
+               fields => [qw(do name password confirm_password email locked_pages)],
+               header => 0,
+               method => 'POST',
+               validate => {
+                       confirm_password => {
+                               perl => q{eq $form->field("password")},
+                       },
+                       email => 'EMAIL',
+               },
+               required => 'NONE',
+               javascript => 0,
+               params => $q,
+               action => $q->request_uri,
+               template => (-e "$config{templatedir}/prefs.tmpl" ?
+                             "$config{templatedir}/prefs.tmpl" : "")
+       );
+       my @buttons=("Save Preferences", "Logout", "Cancel");
+       
+       my $user_name=$session->param("name");
+       $form->field(name => "do", type => "hidden");
+       $form->field(name => "name", disabled => 1,
+               value => $user_name, force => 1);
+       $form->field(name => "password", type => "password");
+       $form->field(name => "confirm_password", type => "password");
+       $form->field(name => "locked_pages", size => 50,
+               comment => "(".htmllink("", "GlobList", 1).")");
+       
+       if (! is_admin($user_name)) {
+               $form->field(name => "locked_pages", type => "hidden");
+       }
+       
+       if (! $form->submitted) {
+               $form->field(name => "email", force => 1,
+                       value => userinfo_get($user_name, "email"));
+               $form->field(name => "locked_pages", force => 1,
+                       value => userinfo_get($user_name, "locked_pages"));
+       }
+       
+       if ($form->submitted eq 'Logout') {
+               $session->delete();
+               print $q->redirect($config{url});
+               return;
+       }
+       elsif ($form->submitted eq 'Cancel') {
+               print $q->redirect($config{url});
+               return;
+       }
+       elsif ($form->submitted eq "Save Preferences" && $form->validate) {
+               foreach my $field (qw(password email locked_pages)) {
+                       if (length $form->field($field)) {
+                               userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
+                       }
+               }
+               $form->text("Preferences saved.");
+       }
+       
+       print $session->header();
+       print misctemplate($form->title, $form->render(submit => \@buttons));
+} #}}}
+
 sub cgi_editpage ($$) { #{{{
        my $q=shift;
        my $session=shift;
@@ -1089,6 +1260,8 @@ sub cgi_editpage ($$) { #{{{
                cols => 80);
        $form->tmpl_param("can_commit", $config{svn});
        $form->tmpl_param("indexlink", indexlink());
+       $form->tmpl_param("helponformattinglink",
+               htmllink("", "HelpOnFormatting", 1));
        if (! $form->submitted) {
                $form->field(name => "rcsinfo", value => rcs_prepedit($file),
                        force => 1);
@@ -1129,16 +1302,25 @@ sub cgi_editpage ($$) { #{{{
                        else {
                                my $dir=$from."/";
                                $dir=~s![^/]+/$!!;
+                               
+                               if ($page eq 'discussion') {
+                                       $best_loc="$from/$page";
+                               }
+                               else {
+                                       $best_loc=$dir.$page;
+                               }
+                               
                                push @page_locs, $dir.$page;
                                push @page_locs, "$from/$page";
-                               $best_loc="$from/$page";
                                while (length $dir) {
                                        $dir=~s![^/]+/$!!;
                                        push @page_locs, $dir.$page;
                                }
 
-                               @page_locs = grep { ! exists
-                                       $pagesources{lc($_)} } @page_locs;
+                               @page_locs = grep {
+                                       ! exists $pagesources{lc($_)} &&
+                                       ! page_locked($_, $session, 1)
+                               } @page_locs;
                        }
 
                        $form->tmpl_param("page_select", 1);
@@ -1147,6 +1329,7 @@ sub cgi_editpage ($$) { #{{{
                        $form->title("creating $page");
                }
                elsif ($form->field("do") eq "edit") {
+                       page_locked($page, $session);
                        if (! defined $form->field('content') || 
                            ! length $form->field('content')) {
                                my $content="";
@@ -1166,13 +1349,15 @@ sub cgi_editpage ($$) { #{{{
        }
        else {
                # save page
+               page_locked($page, $session);
+               
                my $content=$form->field('content');
                $content=~s/\r\n/\n/g;
                $content=~s/\r/\n/g;
                writefile("$config{srcdir}/$file", $content);
                
                my $message="web commit ";
-               if ($session->param("name")) {
+               if (length $session->param("name")) {
                        $message.="by ".$session->param("name");
                }
                else {
@@ -1240,12 +1425,13 @@ sub cgi () { #{{{
 
        my $oldmask=umask(077);
        my $session = CGI::Session->new("driver:db_file", $q,
-               { FileName => "$config{srcdir}/.ikiwiki/sessions.db" });
+               { FileName => "$config{wikistatedir}/sessions.db" });
        umask($oldmask);
        
        # Everything below this point needs the user to be signed in.
        if ((! $config{anonok} && ! defined $session->param("name") ||
-               ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
+            ! defined $session->param("name") ||
+            ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
                cgi_signin($q, $session);
        
                # Force session flush with safe umask.
@@ -1259,6 +1445,9 @@ sub cgi () { #{{{
        if ($do eq 'create' || $do eq 'edit') {
                cgi_editpage($q, $session);
        }
+       elsif ($do eq 'prefs') {
+               cgi_prefs($q, $session);
+       }
        else {
                error("unknown do parameter");
        }
@@ -1279,10 +1468,10 @@ sub setup () { # {{{
 } #}}}
 
 # main {{{
-lockwiki();
 setup() if $config{setup};
+lockwiki();
 if ($config{wrapper}) {
-       gen_wrapper(%config);
+       gen_wrapper();
        exit;
 }
 memoize('pagename');