+
+ # handle backlinks; if a page has added/removed links, update the
+ # pages it links to
+ # TODO: inefficient; pages may get rendered above and again here;
+ # problem is the backlinks could be wrong in the first pass render
+ # above
+ if (%rendered) {
+ my %linkchanged;
+ foreach my $file (keys %rendered, @del) {
+ my $page=pagename($file);
+ if (exists $links{$page}) {
+ foreach my $link (@{$links{$page}}) {
+ $link=bestlink($page, $link);
+ if (length $link &&
+ ! exists $oldlinks{$page} ||
+ ! grep { $_ eq $link } @{$oldlinks{$page}}) {
+ $linkchanged{$link}=1;
+ }
+ }
+ }
+ if (exists $oldlinks{$page}) {
+ foreach my $link (@{$oldlinks{$page}}) {
+ $link=bestlink($page, $link);
+ if (length $link &&
+ ! exists $links{$page} ||
+ ! grep { $_ eq $link } @{$links{$page}}) {
+ $linkchanged{$link}=1;
+ }
+ }
+ }
+ }
+ foreach my $link (keys %linkchanged) {
+ my $linkfile=$pagesources{$link};
+ if (defined $linkfile) {
+ debug("rendering $linkfile, to update its backlinks");
+ render($linkfile);
+ }
+ }
+ }
+} #}}}
+
+# Generates a C wrapper program for running ikiwiki in a specific way.
+# The wrapper may be safely made suid.
+sub gen_wrapper ($$) { #{{{
+ my ($svn, $rebuild)=@_;
+
+ eval q{use Cwd 'abs_path'};
+ $srcdir=abs_path($srcdir);
+ $destdir=abs_path($destdir);
+ my $this=abs_path($0);
+ if (! -x $this) {
+ error("$this doesn't seem to be executable");
+ }
+
+ my @params=($srcdir, $templatedir, $destdir, "--wikiname=$wikiname");
+ push @params, "--verbose" if $verbose;
+ push @params, "--rebuild" if $rebuild;
+ push @params, "--nosvn" if !$svn;
+ push @params, "--cgi" if $cgi;
+ push @params, "--url=$url" if $url;
+ push @params, "--cgiurl=$cgiurl" if $cgiurl;
+ push @params, "--historyurl=$historyurl" if $historyurl;
+ push @params, "--anonok" if $anonok;
+ my $params=join(" ", @params);
+ my $call='';
+ foreach my $p ($this, $this, @params) {
+ $call.=qq{"$p", };
+ }
+ $call.="NULL";
+
+ my @envsave;
+ push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
+ CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
+ HTTP_COOKIE} if $cgi;
+ my $envsave="";
+ foreach my $var (@envsave) {
+ $envsave.=<<"EOF"
+ if ((s=getenv("$var")))
+ asprintf(&newenviron[i++], "%s=%s", "$var", s);
+EOF
+ }
+
+ open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");;
+ print OUT <<"EOF";
+/* A wrapper for ikiwiki, can be safely made suid. */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern char **environ;
+
+int main (int argc, char **argv) {
+ /* Sanitize environment. */
+ char *s;
+ char *newenviron[$#envsave+3];
+ int i=0;
+$envsave
+ newenviron[i++]="HOME=$ENV{HOME}";
+ newenviron[i]=NULL;
+ environ=newenviron;
+
+ if (argc == 2 && strcmp(argv[1], "--params") == 0) {
+ printf("$params\\n");
+ exit(0);
+ }
+
+ execl($call);
+ perror("failed to run $this");
+ exit(1);
+}
+EOF
+ close OUT;
+ if (system("gcc", "ikiwiki-wrap.c", "-o", "ikiwiki-wrap") != 0) {
+ error("failed to compile ikiwiki-wrap.c");
+ }
+ unlink("ikiwiki-wrap.c");
+ print "successfully generated ikiwiki-wrap\n";
+ exit 0;
+} #}}}
+
+sub misctemplate ($$) { #{{{
+ my $title=shift;
+ my $pagebody=shift;
+
+ my $template=HTML::Template->new(
+ filename => "$templatedir/misc.tmpl"
+ );
+ $template->param(
+ title => $title,
+ indexlink => indexlink(),
+ wikiname => $wikiname,
+ pagebody => $pagebody,
+ );
+ return $template->output;
+}#}}}
+
+sub cgi_recentchanges ($) { #{{{
+ my $q=shift;
+
+ my $template=HTML::Template->new(
+ filename => "$templatedir/recentchanges.tmpl"
+ );
+ $template->param(
+ title => "RecentChanges",
+ indexlink => indexlink(),
+ wikiname => $wikiname,
+ changelog => [rcs_recentchanges(100)],
+ );
+ print $q->header, $template->output;
+} #}}}
+
+sub userinfo_get ($$) { #{{{
+ my $user=shift;
+ my $field=shift;
+
+ eval q{use Storable};
+ my $userdata=eval{ Storable::lock_retrieve("$srcdir/.ikiwiki/userdb") };
+ if (! defined $userdata || ! ref $userdata ||
+ ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
+ return "";
+ }
+ return $userdata->{$user}->{$field};
+} #}}}
+
+sub userinfo_set ($$) { #{{{
+ my $user=shift;
+ my $info=shift;
+
+ eval q{use Storable};
+ my $userdata=eval{ Storable::lock_retrieve("$srcdir/.ikiwiki/userdb") };
+ if (! defined $userdata || ! ref $userdata) {
+ $userdata={};
+ }
+ $userdata->{$user}=$info;
+ my $oldmask=umask(077);
+ my $ret=Storable::lock_store($userdata, "$srcdir/.ikiwiki/userdb");
+ umask($oldmask);
+ return $ret;
+} #}}}
+
+sub cgi_signin ($$) { #{{{
+ my $q=shift;
+ my $session=shift;
+
+ eval q{use CGI::FormBuilder};
+ my $form = CGI::FormBuilder->new(
+ title => "$wikiname signin",
+ fields => [qw(do page name password confirm_password email)],
+ header => 1,
+ method => 'POST',
+ validate => {
+ confirm_password => {
+ perl => q{eq $form->field("password")},
+ },
+ email => 'EMAIL',
+ },
+ required => 'NONE',
+ javascript => 0,
+ params => $q,
+ action => $q->request_uri,
+ header => 0,
+ template => (-e "$templatedir/signin.tmpl" ? "$templatedir/signin.tmpl" : "")
+ );
+
+ $form->field(name => "name", required => 0);
+ $form->field(name => "do", type => "hidden");
+ $form->field(name => "page", type => "hidden");
+ $form->field(name => "password", type => "password", required => 0);
+ $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.");
+ }
+
+ if ($form->submitted) {
+ # Set required fields based on how form was submitted.
+ my %required=(
+ "Login" => [qw(name password)],
+ "Register" => [qw(name password confirm_password email)],
+ "Mail Password" => [qw(name)],
+ );
+ foreach my $opt (@{$required{$form->submitted}}) {
+ $form->field(name => $opt, required => 1);
+ }
+
+ # Validate password differently depending on how
+ # form was submitted.
+ if ($form->submitted eq 'Login') {
+ $form->field(
+ name => "password",
+ validate => sub {
+ length $form->field("name") &&
+ shift eq userinfo_get($form->field("name"), 'password');
+ },
+ );
+ $form->field(name => "name", validate => '/^\w+$/');
+ }
+ else {
+ $form->field(name => "password", validate => 'VALUE');
+ }
+ # And make sure the entered name exists when logging
+ # in or sending email, and does not when registering.
+ if ($form->submitted eq 'Register') {
+ $form->field(
+ name => "name",
+ validate => sub {
+ my $name=shift;
+ length $name &&
+ ! userinfo_get($name, "regdate");
+ },
+ );
+ }
+ else {
+ $form->field(
+ name => "name",
+ validate => sub {
+ my $name=shift;
+ length $name &&
+ userinfo_get($name, "regdate");
+ },
+ );
+ }
+ }
+ else {
+ # First time settings.
+ $form->field(name => "name", comment => "use FirstnameLastName");
+ $form->field(name => "confirm_password", comment => "(only needed");
+ $form->field(name => "email", comment => "for registration)");
+ if ($session->param("name")) {
+ $form->field(name => "name", value => $session->param("name"));
+ }
+ }
+
+ if ($form->submitted && $form->validate) {
+ if ($form->submitted eq 'Login') {
+ $session->param("name", $form->field("name"));
+ if (defined $form->field("do") &&
+ $form->field("do") ne 'signin') {
+ print $q->redirect(
+ "$cgiurl?do=".$form->field("do").
+ "&page=".$form->field("page"));
+ }
+ else {
+ print $q->redirect($url);
+ }
+ }
+ elsif ($form->submitted eq 'Register') {
+ my $user_name=$form->field('name');
+ if (userinfo_set($user_name, {
+ 'email' => $form->field('email'),
+ 'password' => $form->field('password'),
+ 'regdate' => time
+ })) {
+ $form->field(name => "confirm_password", type => "hidden");
+ $form->field(name => "email", type => "hidden");
+ $form->text("Registration successful. Now you can Login.");
+ print $session->header();
+ print misctemplate($form->title, $form->render(submit => ["Login"]));
+ }
+ else {
+ error("Error saving registration.");
+ }
+ }
+ elsif ($form->submitted eq 'Mail Password') {
+ my $user_name=$form->field("name");
+ my $template=HTML::Template->new(
+ filename => "$templatedir/passwordmail.tmpl"
+ );
+ $template->param(
+ user_name => $user_name,
+ user_password => userinfo_get($user_name, "password"),
+ wikiurl => $url,
+ wikiname => $wikiname,
+ REMOTE_ADDR => $ENV{REMOTE_ADDR},
+ );
+
+ eval q{use Mail::Sendmail};
+ my ($fromhost) = $cgiurl =~ m!/([^/]+)!;
+ print STDERR "$< $> >>> $cgiurl ".(getpwuid($>))[0]."@".$fromhost."\n";
+ sendmail(
+ To => userinfo_get($user_name, "email"),
+ From => "$wikiname admin <".(getpwuid($>))[0]."@".$fromhost.">",
+ Subject => "$wikiname information",
+ Message => $template->output,
+ ) or error("Failed to send mail");
+
+ $form->text("Your password has been emailed to you.");
+ $form->field(name => "name", required => 0);
+ print $session->header();
+ print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
+ }
+ }
+ else {
+ print $session->header();
+ print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
+ }
+} #}}}
+
+sub cgi_editpage ($$) { #{{{
+ my $q=shift;
+ my $session=shift;
+
+ eval q{use CGI::FormBuilder};
+ my $form = CGI::FormBuilder->new(
+ fields => [qw(do from page content comments)],
+ header => 1,
+ method => 'POST',
+ validate => {
+ content => '/.+/',
+ },
+ required => [qw{content}],
+ javascript => 0,
+ params => $q,
+ action => $q->request_uri,
+ table => 0,
+ template => "$templatedir/editpage.tmpl"
+ );
+
+ my ($page)=$form->param('page')=~/$wiki_file_regexp/;
+ if (! defined $page || ! length $page || $page ne $q->param('page') ||
+ $page=~/$wiki_file_prune_regexp/ || $page=~/^\//) {
+ error("bad page name");
+ }
+ $page=lc($page);
+
+ $form->field(name => "do", type => 'hidden');
+ $form->field(name => "from", type => 'hidden');
+ $form->field(name => "page", value => "$page", force => 1);
+ $form->field(name => "comments", type => "text", size => 80);
+ $form->field(name => "content", type => "textarea", rows => 20,
+ cols => 80);
+
+ if ($form->submitted eq "Cancel") {
+ print $q->redirect("$url/".htmlpage($page));
+ return;
+ }
+ if (! $form->submitted || ! $form->validate) {
+ if ($form->field("do") eq "create") {
+ if (exists $pagesources{lc($page)}) {
+ # hmm, someone else made the page in the
+ # meantime?
+ print $q->redirect("$url/".htmlpage($page));
+ return;
+ }
+
+ my @page_locs;
+ my ($from)=$form->param('from')=~/$wiki_file_regexp/;
+ if (! defined $from || ! length $from ||
+ $from ne $form->param('from') ||
+ $from=~/$wiki_file_prune_regexp/ || $from=~/^\//) {
+ @page_locs=$page;
+ }
+ else {
+ my $dir=$from."/";
+ $dir=~s![^/]+/$!!;
+ push @page_locs, $dir.$page;
+ push @page_locs, "$from/$page";
+ while (length $dir) {
+ $dir=~s![^/]+/$!!;
+ push @page_locs, $dir.$page;
+ }
+ }
+
+ $form->tmpl_param("page_select", 1);
+ $form->field(name => "page", type => 'select',
+ options => \@page_locs);
+ $form->title("creating $page");
+ }
+ elsif ($form->field("do") eq "edit") {
+ my $content="";
+ if (exists $pagesources{lc($page)}) {
+ $content=readfile("$srcdir/$pagesources{lc($page)}");
+ $content=~s/\n/\r\n/g;
+ }
+ $form->tmpl_param("page_select", 0);
+ $form->field(name => "content", value => $content,
+ force => 1);
+ $form->field(name => "page", type => 'hidden');
+ $form->title("editing $page");
+ }
+
+ $form->tmpl_param("can_commit", $svn);
+ $form->tmpl_param("indexlink", indexlink());
+ print $form->render(submit => ["Save Page", "Cancel"]);
+ }
+ else {
+ # save page
+ my $file=$page.$default_pagetype;
+ my $newfile=1;
+ if (exists $pagesources{lc($page)}) {
+ $file=$pagesources{lc($page)};
+ $newfile=0;
+ }
+
+ my $content=$form->field('content');
+ $content=~s/\r\n/\n/g;
+ $content=~s/\r/\n/g;
+ writefile("$srcdir/$file", $content);
+
+ my $message="web commit ";
+ if ($session->param("name")) {
+ $message.="by ".$session->param("name");
+ }
+ else {
+ $message.="from $ENV{REMOTE_ADDR}";
+ }
+ if (defined $form->field('comments') &&
+ length $form->field('comments')) {
+ $message.=": ".$form->field('comments');
+ }
+
+ if ($svn) {
+ if ($newfile) {
+ rcs_add($file);
+ }
+ # presumably the commit will trigger an update
+ # of the wiki
+ rcs_commit($message);
+ }
+ else {
+ refresh();
+ }
+
+ print $q->redirect("$url/".htmlpage($page));
+ }
+} #}}}
+
+sub cgi () { #{{{
+ eval q{use CGI};
+ eval q{use CGI::Session};
+
+ my $q=CGI->new;
+
+ my $do=$q->param('do');
+ if (! defined $do || ! length $do) {
+ error("\"do\" parameter missing");
+ }
+
+ # This does not need a session.
+ if ($do eq 'recentchanges') {
+ cgi_recentchanges($q);
+ return;
+ }
+
+ CGI::Session->name("ikiwiki_session");
+
+ my $oldmask=umask(077);
+ my $session = CGI::Session->new("driver:db_file", $q,
+ { FileName => "$srcdir/.ikiwiki/sessions.db" });
+ umask($oldmask);
+
+ # Everything below this point needs the user to be signed in.
+ if ((! $anonok && ! defined $session->param("name") ||
+ ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
+ cgi_signin($q, $session);
+
+ # Force session flush with safe umask.
+ my $oldmask=umask(077);
+ $session->flush;
+ umask($oldmask);
+
+ return;
+ }
+
+ if ($do eq 'create' || $do eq 'edit') {
+ cgi_editpage($q, $session);
+ }
+ else {
+ error("unknown do parameter");
+ }
+} #}}}
+
+# main {{{
+my $rebuild=0;
+my $wrapper=0;
+if (grep /^-/, @ARGV) {
+ eval {use Getopt::Long};
+ GetOptions(
+ "wikiname=s" => \$wikiname,
+ "verbose|v" => \$verbose,
+ "rebuild" => \$rebuild,
+ "wrapper" => \$wrapper,
+ "svn!" => \$svn,
+ "anonok!" => \$anonok,
+ "cgi" => \$cgi,
+ "url=s" => \$url,
+ "cgiurl=s" => \$cgiurl,
+ "historyurl=s" => \$historyurl,
+ ) || usage();