From: joey Date: Thu, 23 Mar 2006 06:51:15 +0000 (+0000) Subject: Major code reoganisation, splitting up the single big file. The two goals X-Git-Tag: 1.0~266 X-Git-Url: http://git.tremily.us/?p=ikiwiki.git;a=commitdiff_plain;h=6c8cf5dd571662f981227489f7c4652a1a1f10cd Major code reoganisation, splitting up the single big file. The two goals kept in mind during this are a) to reduce load time for common cases like cgi and post-commit and b) make the code easier to navigate. This also modularises RCS support to the extent that it should be possible to drop in a module for some RCS other than svn, add a switch for it, and it pretty much just work. High chance I missed an edge case that breaks something, this is only barely tested at this point. --- diff --git a/IkiWiki/CGI.pm b/IkiWiki/CGI.pm new file mode 100644 index 000000000..3ac984d30 --- /dev/null +++ b/IkiWiki/CGI.pm @@ -0,0 +1,509 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +package IkiWiki; + +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_recentchanges ($) { #{{{ + my $q=shift; + + my $template=HTML::Template->new( + filename => "$config{templatedir}/recentchanges.tmpl" + ); + $template->param( + title => "RecentChanges", + indexlink => indexlink(), + wikiname => $config{wikiname}, + changelog => [rcs_recentchanges(100)], + ); + print $q->header, $template->output; +} #}}} + +sub cgi_signin ($$) { #{{{ + my $q=shift; + my $session=shift; + + eval q{use CGI::FormBuilder}; + my $form = CGI::FormBuilder->new( + title => "signin", + fields => [qw(do page from 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 "$config{templatedir}/signin.tmpl" ? + "$config{templatedir}/signin.tmpl" : "") + ); + + $form->field(name => "name", required => 0); + $form->field(name => "do", type => "hidden"); + $form->field(name => "page", type => "hidden"); + $form->field(name => "from", 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 first."); + } + + 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( + "$config{cgiurl}?do=".$form->field("do"). + "&page=".$form->field("page"). + "&from=".$form->field("from"));; + } + else { + print $q->redirect($config{url}); + } + } + elsif ($form->submitted eq 'Register') { + my $user_name=$form->field('name'); + if (userinfo_setall($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 => "$config{templatedir}/passwordmail.tmpl" + ); + $template->param( + user_name => $user_name, + user_password => userinfo_get($user_name, "password"), + wikiurl => $config{url}, + wikiname => $config{wikiname}, + REMOTE_ADDR => $ENV{REMOTE_ADDR}, + ); + + eval q{use Mail::Sendmail}; + my ($fromhost) = $config{cgiurl} =~ m!/([^/]+)!; + sendmail( + To => userinfo_get($user_name, "email"), + From => "$config{wikiname} admin <".(getpwuid($>))[0]."@".$fromhost.">", + Subject => "$config{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_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; + + eval q{use CGI::FormBuilder}; + my $form = CGI::FormBuilder->new( + fields => [qw(do rcsinfo from page content comments)], + header => 1, + method => 'POST', + validate => { + content => '/.+/', + }, + required => [qw{content}], + javascript => 0, + params => $q, + action => $q->request_uri, + table => 0, + template => "$config{templatedir}/editpage.tmpl" + ); + my @buttons=("Save Page", "Preview", "Cancel"); + + my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/; + if (! defined $page || ! length $page || $page ne $q->param('page') || + $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) { + error("bad page name"); + } + $page=lc($page); + + my $file=$page.$config{default_pageext}; + my $newfile=1; + if (exists $pagesources{lc($page)}) { + $file=$pagesources{lc($page)}; + $newfile=0; + } + + $form->field(name => "do", type => 'hidden'); + $form->field(name => "from", type => 'hidden'); + $form->field(name => "rcsinfo", 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); + $form->tmpl_param("can_commit", $config{rcs}); + $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); + } + + if ($form->submitted eq "Cancel") { + print $q->redirect("$config{url}/".htmlpage($page)); + return; + } + elsif ($form->submitted eq "Preview") { + require IkiWiki::Render; + $form->tmpl_param("page_preview", + htmlize($config{default_pageext}, + linkify($form->field('content'), $page))); + } + else { + $form->tmpl_param("page_preview", ""); + } + $form->tmpl_param("page_conflict", ""); + + if (! $form->submitted || $form->submitted eq "Preview" || + ! $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("$config{url}/".htmlpage($page)); + return; + } + + my @page_locs; + my $best_loc; + my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/; + if (! defined $from || ! length $from || + $from ne $form->param('from') || + $from=~/$config{wiki_file_prune_regexp}/ || $from=~/^\//) { + @page_locs=$best_loc=$page; + } + 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"; + while (length $dir) { + $dir=~s![^/]+/$!!; + push @page_locs, $dir.$page; + } + + @page_locs = grep { + ! exists $pagesources{lc($_)} && + ! page_locked($_, $session, 1) + } @page_locs; + } + + $form->tmpl_param("page_select", 1); + $form->field(name => "page", type => 'select', + options => \@page_locs, value => $best_loc); + $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=""; + if (exists $pagesources{lc($page)}) { + $content=readfile("$config{srcdir}/$pagesources{lc($page)}"); + $content=~s/\n/\r\n/g; + } + $form->field(name => "content", value => $content, + force => 1); + } + $form->tmpl_param("page_select", 0); + $form->field(name => "page", type => 'hidden'); + $form->title("editing $page"); + } + + print $form->render(submit => \@buttons); + } + 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 (length $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 ($config{rcs}) { + if ($newfile) { + rcs_add($file); + } + # prevent deadlock with post-commit hook + unlockwiki(); + # presumably the commit will trigger an update + # of the wiki + my $conflict=rcs_commit($file, $message, + $form->field("rcsinfo")); + + if (defined $conflict) { + $form->field(name => "rcsinfo", value => rcs_prepedit($file), + force => 1); + $form->tmpl_param("page_conflict", 1); + $form->field("content", value => $conflict, force => 1); + $form->field("do", "edit)"); + $form->tmpl_param("page_select", 0); + $form->field(name => "page", type => 'hidden'); + $form->title("editing $page"); + print $form->render(submit => \@buttons); + return; + } + } + else { + require IkiWiki::Render; + loadindex(); + refresh(); + saveindex(); + } + + # The trailing question mark tries to avoid broken + # caches and get the most recent version of the page. + print $q->redirect("$config{url}/".htmlpage($page)."?updated"); + } +} #}}} + +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 => "$config{wikistatedir}/sessions.db" }); + umask($oldmask); + + # Everything below this point needs the user to be signed in. + if ((! $config{anonok} && ! defined $session->param("name") || + ! 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); + } + elsif ($do eq 'prefs') { + cgi_prefs($q, $session); + } + else { + error("unknown do parameter"); + } +} #}}} + +1 diff --git a/IkiWiki/RCS/SVN.pm b/IkiWiki/RCS/SVN.pm new file mode 100644 index 000000000..946412320 --- /dev/null +++ b/IkiWiki/RCS/SVN.pm @@ -0,0 +1,169 @@ +#!/usr/bin/perl -T +# For subversion support. + +use warnings; +use strict; + +package IkiWiki; + +sub svn_info ($$) { #{{{ + my $field=shift; + my $file=shift; + + my $info=`LANG=C svn info $file`; + my ($ret)=$info=~/^$field: (.*)$/m; + return $ret; +} #}}} + +sub rcs_update () { #{{{ + if (-d "$config{srcdir}/.svn") { + if (system("svn", "update", "--quiet", $config{srcdir}) != 0) { + warn("svn update failed\n"); + } + } +} #}}} + +sub rcs_prepedit ($) { #{{{ + # Prepares to edit a file under revision control. Returns a token + # that must be passed into rcs_commit when the file is ready + # for committing. + # The file is relative to the srcdir. + my $file=shift; + + if (-d "$config{srcdir}/.svn") { + # For subversion, return the revision of the file when + # editing begins. + my $rev=svn_info("Revision", "$config{srcdir}/$file"); + return defined $rev ? $rev : ""; + } +} #}}} + +sub rcs_commit ($$$) { #{{{ + # Tries to commit the page; returns undef on _success_ and + # a version of the page with the rcs's conflict markers on failure. + # The file is relative to the srcdir. + my $file=shift; + my $message=shift; + my $rcstoken=shift; + + if (-d "$config{srcdir}/.svn") { + # Check to see if the page has been changed by someone + # else since rcs_prepedit was called. + my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint + my $rev=svn_info("Revision", "$config{srcdir}/$file"); + 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 + if (system("svn", "merge", "--quiet", "-r$oldrev:$rev", + "$config{srcdir}/$file") != 0) { + warn("svn merge -r$oldrev:$rev failed\n"); + } + } + + if (system("svn", "commit", "--quiet", "-m", + possibly_foolish_untaint($message), + "$config{srcdir}") != 0) { + my $conflict=readfile("$config{srcdir}/$file"); + if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) { + warn("svn revert failed\n"); + } + return $conflict; + } + } + return undef # success +} #}}} + +sub rcs_add ($) { #{{{ + # filename is relative to the root of the srcdir + my $file=shift; + + if (-d "$config{srcdir}/.svn") { + my $parent=dirname($file); + while (! -d "$config{srcdir}/$parent/.svn") { + $file=$parent; + $parent=dirname($file); + } + + if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) { + warn("svn add failed\n"); + } + } +} #}}} + +sub rcs_recentchanges ($) { #{{{ + my $num=shift; + my @ret; + + eval q{use CGI 'escapeHTML'}; + eval q{use Date::Parse}; + eval q{use Time::Duration}; + + if (-d "$config{srcdir}/.svn") { + my $svn_url=svn_info("URL", $config{srcdir}); + + # FIXME: currently assumes that the wiki is somewhere + # under trunk in svn, doesn't support other layouts. + my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!; + + my $div=qr/^--------------------+$/; + my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/; + my $state='start'; + my ($rev, $user, $when, @pages, @message); + foreach (`LANG=C svn log --limit $num -v '$svn_url'`) { + chomp; + if ($state eq 'start' && /$div/) { + $state='header'; + } + elsif ($state eq 'header' && /$infoline/) { + $rev=$1; + $user=$2; + $when=concise(ago(time - str2time($3))); + } + 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'; + } + elsif ($state eq 'body' && /$div/) { + my $committype="web"; + if (defined $message[0] && + $message[0]->{line}=~/^web commit by (\w+):?(.*)/) { + $user="$1"; + $message[0]->{line}=$2; + } + else { + $committype="svn"; + } + + push @ret, { rev => $rev, + user => htmllink("", $user, 1), + committype => $committype, + when => $when, message => [@message], + pages => [@pages], + } if @pages; + return @ret if @ret >= $num; + + $state='header'; + $rev=$user=$when=undef; + @pages=@message=(); + } + elsif ($state eq 'body') { + push @message, {line => escapeHTML($_)}, + } + } + } + + return @ret; +} #}}} + +1 diff --git a/IkiWiki/RCS/Stub.pm b/IkiWiki/RCS/Stub.pm new file mode 100644 index 000000000..d3b72b5ea --- /dev/null +++ b/IkiWiki/RCS/Stub.pm @@ -0,0 +1,26 @@ +#!/usr/bin/perl -T +# Stubs for no revision control. + +use warnings; +use strict; + +package IkiWiki; + +sub rcs_update () { +} + +sub rcs_prepedit ($) { + return "" +} + +sub rcs_commit ($$$) { + return undef # success +} + +sub rcs_add ($) { +} + +sub rcs_recentchanges ($) { +} + +1 diff --git a/IkiWiki/Render.pm b/IkiWiki/Render.pm new file mode 100644 index 000000000..98c86bac8 --- /dev/null +++ b/IkiWiki/Render.pm @@ -0,0 +1,316 @@ +package IkiWiki; + +use warnings; +use strict; +use File::Spec; + +sub linkify ($$) { #{{{ + my $content=shift; + my $page=shift; + + $content =~ s{(\\?)$config{wiki_link_regexp}}{ + $1 ? "[[$2]]" : htmllink($page, $2) + }eg; + + return $content; +} #}}} + +sub htmlize ($$) { #{{{ + my $type=shift; + my $content=shift; + + if (! $INC{"/usr/bin/markdown"}) { + no warnings 'once'; + $blosxom::version="is a proper perl module too much to ask?"; + use warnings 'all'; + do "/usr/bin/markdown"; + } + + if ($type eq '.mdwn') { + return Markdown::Markdown($content); + } + else { + error("htmlization of $type not supported"); + } +} #}}} + +sub backlinks ($) { #{{{ + my $page=shift; + + my @links; + foreach my $p (keys %links) { + next if bestlink($page, $p) eq $page; + if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) { + my $href=File::Spec->abs2rel(htmlpage($p), dirname($page)); + + # Trim common dir prefixes from both pages. + my $p_trimmed=$p; + my $page_trimmed=$page; + my $dir; + 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) && + defined $dir && + $p_trimmed=~s/^\Q$dir\E// && + $page_trimmed=~s/^\Q$dir\E//; + + push @links, { url => $href, page => $p_trimmed }; + } + } + + return sort { $a->{page} cmp $b->{page} } @links; +} #}}} + +sub parentlinks ($) { #{{{ + my $page=shift; + + my @ret; + my $pagelink=""; + my $path=""; + my $skip=1; + foreach my $dir (reverse split("/", $page)) { + if (! $skip) { + $path.="../"; + unshift @ret, { url => "$path$dir.html", page => $dir }; + } + else { + $skip=0; + } + } + unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} }; + return @ret; +} #}}} + +sub finalize ($$$) { #{{{ + my $content=shift; + my $page=shift; + my $mtime=shift; + + my $title=basename($page); + $title=~s/_/ /g; + + my $template=HTML::Template->new(blind_cache => 1, + filename => "$config{templatedir}/page.tmpl"); + + if (length $config{cgiurl}) { + $template->param(editurl => "$config{cgiurl}?do=edit&page=$page"); + $template->param(prefsurl => "$config{cgiurl}?do=prefs"); + if ($config{rcs}) { + $template->param(recentchangesurl => "$config{cgiurl}?do=recentchanges"); + } + } + + if (length $config{historyurl}) { + my $u=$config{historyurl}; + $u=~s/\[\[file\]\]/$pagesources{$page}/g; + $template->param(historyurl => $u); + } + + $template->param( + title => $title, + wikiname => $config{wikiname}, + parentlinks => [parentlinks($page)], + content => $content, + backlinks => [backlinks($page)], + discussionlink => htmllink($page, "Discussion", 1, 1), + mtime => scalar(gmtime($mtime)), + ); + + return $template->output; +} #}}} + +sub check_overwrite ($$) { #{{{ + # Important security check. Make sure to call this before saving + # any files to the source directory. + my $dest=shift; + my $src=shift; + + if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) { + error("$dest already exists and was rendered from ". + join(" ",(grep { $renderedfiles{$_} eq $dest } keys + %renderedfiles)). + ", before, so not rendering from $src"); + } +} #}}} + +sub mtime ($) { #{{{ + my $page=shift; + + return (stat($page))[9]; +} #}}} + +sub findlinks ($$) { #{{{ + my $content=shift; + my $page=shift; + + my @links; + while ($content =~ /(? 1, + wanted => sub { + if (/$config{wiki_file_prune_regexp}/) { + no warnings 'once'; + $File::Find::prune=1; + use warnings "all"; + } + elsif (! -d $_ && ! -l $_) { + my ($f)=/$config{wiki_file_regexp}/; # untaint + if (! defined $f) { + warn("skipping bad filename $_\n"); + } + else { + $f=~s/^\Q$config{srcdir}\E\/?//; + push @files, $f; + $exists{pagename($f)}=1; + } + } + }, + }, $config{srcdir}); + + my %rendered; + + # check for added or removed pages + my @add; + foreach my $file (@files) { + my $page=pagename($file); + if (! $oldpagemtime{$page}) { + debug("new page $page"); + push @add, $file; + $links{$page}=[]; + $pagesources{$page}=$file; + } + } + my @del; + foreach my $page (keys %oldpagemtime) { + if (! $exists{$page}) { + debug("removing old page $page"); + push @del, $pagesources{$page}; + prune($config{destdir}."/".$renderedfiles{$page}); + delete $renderedfiles{$page}; + $oldpagemtime{$page}=0; + delete $pagesources{$page}; + } + } + + # render any updated files + foreach my $file (@files) { + my $page=pagename($file); + + if (! exists $oldpagemtime{$page} || + mtime("$config{srcdir}/$file") > $oldpagemtime{$page}) { + debug("rendering changed file $file"); + render($file); + $rendered{$file}=1; + } + } + + # if any files were added or removed, check to see if each page + # needs an update due to linking to them + # TODO: inefficient; pages may get rendered above and again here; + # problem is the bestlink may have changed and we won't know until + # now + if (@add || @del) { +FILE: foreach my $file (@files) { + my $page=pagename($file); + foreach my $f (@add, @del) { + my $p=pagename($f); + foreach my $link (@{$links{$page}}) { + if (bestlink($page, $link) eq $p) { + debug("rendering $file, which links to $p"); + render($file); + $rendered{$file}=1; + next FILE; + } + } + } + } + } + + # 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 (map { bestlink($page, $_) } @{$links{$page}}) { + if (length $link && + ! exists $oldlinks{$page} || + ! grep { $_ eq $link } @{$oldlinks{$page}}) { + $linkchanged{$link}=1; + } + } + } + if (exists $oldlinks{$page}) { + foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) { + 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); + } + } + } +} #}}} + +1 diff --git a/IkiWiki/Setup.pm b/IkiWiki/Setup.pm new file mode 100644 index 000000000..63659ce2e --- /dev/null +++ b/IkiWiki/Setup.pm @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +package IkiWiki; + +sub setup () { # {{{ + my $setup=possibly_foolish_untaint($config{setup}); + delete $config{setup}; + open (IN, $setup) || error("read $setup: $!\n"); + local $/=undef; + my $code=; + ($code)=$code=~/(.*)/s; + close IN; + + eval $code; + error($@) if $@; + exit; +} #}}} + +1 diff --git a/IkiWiki/Setup/Standard.pm b/IkiWiki/Setup/Standard.pm index 4d1118f30..4a49895da 100644 --- a/IkiWiki/Setup/Standard.pm +++ b/IkiWiki/Setup/Standard.pm @@ -4,34 +4,41 @@ # plus hashes for cgiwrapper and svnwrapper, which specify any differing # config stuff for them and cause the wrappers to be made. -package IkiWiki::Setup::Standard; - use warnings; use strict; +use IkiWiki::Wrapper; + +package IkiWiki::Setup::Standard; sub import { + IkiWiki::setup_standard(@_); +} + +package IkiWiki; + +sub setup_standard { my %setup=%{$_[1]}; - ::debug("generating wrappers.."); - my %startconfig=(%::config); + debug("generating wrappers.."); + my %startconfig=(%config); foreach my $wrapper (@{$setup{wrappers}}) { - %::config=(%startconfig, verbose => 0, %setup, %{$wrapper}); - ::checkoptions(); - ::gen_wrapper(); + %config=(%startconfig, verbose => 0, %setup, %{$wrapper}); + checkoptions(); + gen_wrapper(); } - %::config=(%startconfig); + %config=(%startconfig); - ::debug("rebuilding wiki.."); + debug("rebuilding wiki.."); foreach my $c (keys %setup) { - $::config{$c}=::possibly_foolish_untaint($setup{$c}) + $config{$c}=possibly_foolish_untaint($setup{$c}) if defined $setup{$c} && ! ref $setup{$c}; } - $::config{rebuild}=1; - ::checkoptions(); - ::refresh(); + $config{rebuild}=1; + checkoptions(); + refresh(); - ::debug("done"); - ::saveindex(); + debug("done"); + saveindex(); } 1 diff --git a/IkiWiki/Wrapper.pm b/IkiWiki/Wrapper.pm new file mode 100644 index 000000000..8e513c1f6 --- /dev/null +++ b/IkiWiki/Wrapper.pm @@ -0,0 +1,96 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +package IkiWiki; + +sub gen_wrapper () { #{{{ + eval q{use Cwd 'abs_path'}; + $config{srcdir}=abs_path($config{srcdir}); + $config{destdir}=abs_path($config{destdir}); + my $this=abs_path($0); + if (! -x $this) { + error("$this doesn't seem to be executable"); + } + + if ($config{setup}) { + error("cannot create a wrapper that uses a setup file"); + } + + 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}; + push @params, "--cgi" if $config{cgi}; + 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) { + $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 $config{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 +#include +#include +#include + +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", possibly_foolish_untaint($config{wrapper})) != 0) { + error("failed to compile ikiwiki-wrap.c"); + } + unlink("ikiwiki-wrap.c"); + if (defined $config{wrappermode} && + ! chmod(oct($config{wrappermode}), possibly_foolish_untaint($config{wrapper}))) { + error("chmod $config{wrapper}: $!"); + } + print "successfully generated $config{wrapper}\n"; +} #}}} + +1 diff --git a/Makefile.PL b/Makefile.PL index 70d81b806..10015c47c 100755 --- a/Makefile.PL +++ b/Makefile.PL @@ -12,8 +12,8 @@ install:: extra_install pure_install:: extra_install extra_build: - ./ikiwiki doc html --templatedir=templates --wikiname="ikiwiki" \ - --verbose --nosvn --exclude=/discussion + ./ikiwiki doc html --templatedir=templates \ + --wikiname="ikiwiki" --verbose --nosvn --exclude=/discussion ./mdwn2man doc/usage.mdwn > ikiwiki.man extra_clean: @@ -31,5 +31,6 @@ extra_install: WriteMakefile( 'NAME' => 'IkiWiki', + 'PM_FILTER' => 'grep -v "removed by Makefile"', 'EXE_FILES' => ['ikiwiki'], ); diff --git a/doc/todo.mdwn b/doc/todo.mdwn index bdebfb88c..a377c6340 100644 --- a/doc/todo.mdwn +++ b/doc/todo.mdwn @@ -99,10 +99,17 @@ you need that data.. ## search +* page name substring search * full text (use third-party tools?) + +## lists + * list of all missing pages * list of all pages or some kind of page map +These could be their own static pages updated when other pages are updated. +Perhaps this ties in with the pluggable renderers stuff. + ## page indexes Might be nice to support automatically generating an index based on headers diff --git a/ikiwiki b/ikiwiki index d9dddbe4e..cdc5c8ca4 100755 --- a/ikiwiki +++ b/ikiwiki @@ -2,6 +2,9 @@ $ENV{PATH}="/usr/local/bin:/usr/bin:/bin"; +use lib '.'; # For use without installation, removed by Makefile. + +package IkiWiki; use warnings; use strict; use Memoize; @@ -9,7 +12,7 @@ use File::Spec; use HTML::Template; use Getopt::Long; -my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources); +use vars qw{%config %links %oldlinks %oldpagemtime %renderedfiles %pagesources}; # Holds global config settings, also used by some modules. our %config=( #{{{ @@ -41,7 +44,7 @@ GetOptions( #{{{ "wikiname=s" => \$config{wikiname}, "verbose|v!" => \$config{verbose}, "rebuild!" => \$config{rebuild}, - "wrapper=s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" }, + "wrapper:s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" }, "wrappermode=i" => \$config{wrappermode}, "svn!" => \$config{svn}, "anonok!" => \$config{anonok}, @@ -69,8 +72,18 @@ 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}; + + if ($config{svn}) { + require IkiWiki::RCS::SVN; + $config{rcs}=1; + } + else { + require IkiWiki::RCS::Stub; + $config{rcs}=0; + } } #}}} sub usage { #{{{ @@ -95,12 +108,6 @@ sub debug ($) { #{{{ } } #}}} -sub mtime ($) { #{{{ - my $page=shift; - - return (stat($page))[9]; -} #}}} - sub possibly_foolish_untaint { #{{{ my $tainted=shift; my ($untainted)=$tainted=~/(.*)/; @@ -185,19 +192,6 @@ sub writefile ($$) { #{{{ close OUT; } #}}} -sub findlinks ($$) { #{{{ - my $content=shift; - my $page=shift; - - my @links; - while ($content =~ /(?$link"; } #}}} -sub linkify ($$) { #{{{ - my $content=shift; - my $page=shift; - - $content =~ s{(\\?)$config{wiki_link_regexp}}{ - $1 ? "[[$2]]" : htmllink($page, $2) - }eg; - - return $content; -} #}}} - -sub htmlize ($$) { #{{{ - my $type=shift; - my $content=shift; - - if (! $INC{"/usr/bin/markdown"}) { - no warnings 'once'; - $blosxom::version="is a proper perl module too much to ask?"; - use warnings 'all'; - do "/usr/bin/markdown"; - } - - if ($type eq '.mdwn') { - return Markdown::Markdown($content); - } - else { - error("htmlization of $type not supported"); - } -} #}}} - -sub backlinks ($) { #{{{ - my $page=shift; - - my @links; - foreach my $p (keys %links) { - next if bestlink($page, $p) eq $page; - if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) { - my $href=File::Spec->abs2rel(htmlpage($p), dirname($page)); - - # Trim common dir prefixes from both pages. - my $p_trimmed=$p; - my $page_trimmed=$page; - my $dir; - 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) && - defined $dir && - $p_trimmed=~s/^\Q$dir\E// && - $page_trimmed=~s/^\Q$dir\E//; - - push @links, { url => $href, page => $p_trimmed }; - } - } - - return sort { $a->{page} cmp $b->{page} } @links; -} #}}} - -sub parentlinks ($) { #{{{ - my $page=shift; - - my @ret; - my $pagelink=""; - my $path=""; - my $skip=1; - foreach my $dir (reverse split("/", $page)) { - if (! $skip) { - $path.="../"; - unshift @ret, { url => "$path$dir.html", page => $dir }; - } - else { - $skip=0; - } - } - unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} }; - return @ret; -} #}}} - sub indexlink () { #{{{ return "$config{wikiname}"; } #}}} -sub finalize ($$$) { #{{{ - my $content=shift; - my $page=shift; - my $mtime=shift; - - my $title=basename($page); - $title=~s/_/ /g; - - my $template=HTML::Template->new(blind_cache => 1, - filename => "$config{templatedir}/page.tmpl"); - - 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"); - } - } - - if (length $config{historyurl}) { - my $u=$config{historyurl}; - $u=~s/\[\[file\]\]/$pagesources{$page}/g; - $template->param(historyurl => $u); - } - - $template->param( - title => $title, - wikiname => $config{wikiname}, - parentlinks => [parentlinks($page)], - content => $content, - backlinks => [backlinks($page)], - discussionlink => htmllink($page, "Discussion", 1, 1), - mtime => scalar(gmtime($mtime)), - ); - - return $template->output; -} #}}} - -sub check_overwrite ($$) { #{{{ - # Important security check. Make sure to call this before saving - # any files to the source directory. - my $dest=shift; - my $src=shift; - - if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) { - error("$dest already exists and was rendered from ". - join(" ",(grep { $renderedfiles{$_} eq $dest } keys - %renderedfiles)). - ", before, so not rendering from $src"); - } -} #}}} - -sub render ($) { #{{{ - my $file=shift; - - my $type=pagetype($file); - my $content=readfile("$config{srcdir}/$file"); - if ($type ne 'unknown') { - my $page=pagename($file); - - $links{$page}=[findlinks($content, $page)]; - - $content=linkify($content, $page); - $content=htmlize($type, $content); - $content=finalize($content, $page, - mtime("$config{srcdir}/$file")); - - check_overwrite("$config{destdir}/".htmlpage($page), $page); - writefile("$config{destdir}/".htmlpage($page), $content); - $oldpagemtime{$page}=time; - $renderedfiles{$page}=htmlpage($page); - } - else { - $links{$file}=[]; - check_overwrite("$config{destdir}/$file", $file); - writefile("$config{destdir}/$file", $content); - $oldpagemtime{$file}=time; - $renderedfiles{$file}=$file; - } -} #}}} - sub lockwiki () { #{{{ # Take an exclusive lock on the wiki to prevent multiple concurrent # run issues. The lock will be dropped on program exit. @@ -478,388 +316,6 @@ sub saveindex () { #{{{ close OUT; } #}}} -sub rcs_update () { #{{{ - if (-d "$config{srcdir}/.svn") { - if (system("svn", "update", "--quiet", $config{srcdir}) != 0) { - warn("svn update failed\n"); - } - } -} #}}} - -sub rcs_prepedit ($) { #{{{ - # Prepares to edit a file under revision control. Returns a token - # that must be passed into rcs_commit when the file is ready - # for committing. - # The file is relative to the srcdir. - my $file=shift; - - if (-d "$config{srcdir}/.svn") { - # For subversion, return the revision of the file when - # editing begins. - my $rev=svn_info("Revision", "$config{srcdir}/$file"); - return defined $rev ? $rev : ""; - } -} #}}} - -sub rcs_commit ($$$) { #{{{ - # Tries to commit the page; returns undef on _success_ and - # a version of the page with the rcs's conflict markers on failure. - # The file is relative to the srcdir. - my $file=shift; - my $message=shift; - my $rcstoken=shift; - - if (-d "$config{srcdir}/.svn") { - # Check to see if the page has been changed by someone - # else since rcs_prepedit was called. - my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint - my $rev=svn_info("Revision", "$config{srcdir}/$file"); - 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 - if (system("svn", "merge", "--quiet", "-r$oldrev:$rev", - "$config{srcdir}/$file") != 0) { - warn("svn merge -r$oldrev:$rev failed\n"); - } - } - - if (system("svn", "commit", "--quiet", "-m", - possibly_foolish_untaint($message), - "$config{srcdir}") != 0) { - my $conflict=readfile("$config{srcdir}/$file"); - if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) { - warn("svn revert failed\n"); - } - return $conflict; - } - } - return undef # success -} #}}} - -sub rcs_add ($) { #{{{ - # filename is relative to the root of the srcdir - my $file=shift; - - if (-d "$config{srcdir}/.svn") { - my $parent=dirname($file); - while (! -d "$config{srcdir}/$parent/.svn") { - $file=$parent; - $parent=dirname($file); - } - - if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) { - warn("svn add failed\n"); - } - } -} #}}} - -sub svn_info ($$) { #{{{ - my $field=shift; - my $file=shift; - - my $info=`LANG=C svn info $file`; - my ($ret)=$info=~/^$field: (.*)$/m; - return $ret; -} #}}} - -sub rcs_recentchanges ($) { #{{{ - my $num=shift; - my @ret; - - eval q{use CGI 'escapeHTML'}; - eval q{use Date::Parse}; - eval q{use Time::Duration}; - - if (-d "$config{srcdir}/.svn") { - my $svn_url=svn_info("URL", $config{srcdir}); - - # FIXME: currently assumes that the wiki is somewhere - # under trunk in svn, doesn't support other layouts. - my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!; - - my $div=qr/^--------------------+$/; - my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/; - my $state='start'; - my ($rev, $user, $when, @pages, @message); - foreach (`LANG=C svn log --limit $num -v '$svn_url'`) { - chomp; - if ($state eq 'start' && /$div/) { - $state='header'; - } - elsif ($state eq 'header' && /$infoline/) { - $rev=$1; - $user=$2; - $when=concise(ago(time - str2time($3))); - } - 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'; - } - elsif ($state eq 'body' && /$div/) { - my $committype="web"; - if (defined $message[0] && - $message[0]->{line}=~/^web commit by (\w+):?(.*)/) { - $user="$1"; - $message[0]->{line}=$2; - } - else { - $committype="svn"; - } - - push @ret, { rev => $rev, - user => htmllink("", $user, 1), - committype => $committype, - when => $when, message => [@message], - pages => [@pages], - } if @pages; - return @ret if @ret >= $num; - - $state='header'; - $rev=$user=$when=undef; - @pages=@message=(); - } - elsif ($state eq 'body') { - push @message, {line => escapeHTML($_)}, - } - } - } - - return @ret; -} #}}} - -sub prune ($) { #{{{ - my $file=shift; - - unlink($file); - my $dir=dirname($file); - while (rmdir($dir)) { - $dir=dirname($dir); - } -} #}}} - -sub refresh () { #{{{ - # find existing pages - my %exists; - my @files; - eval q{use File::Find}; - find({ - no_chdir => 1, - wanted => sub { - if (/$config{wiki_file_prune_regexp}/) { - no warnings 'once'; - $File::Find::prune=1; - use warnings "all"; - } - elsif (! -d $_ && ! -l $_) { - my ($f)=/$config{wiki_file_regexp}/; # untaint - if (! defined $f) { - warn("skipping bad filename $_\n"); - } - else { - $f=~s/^\Q$config{srcdir}\E\/?//; - push @files, $f; - $exists{pagename($f)}=1; - } - } - }, - }, $config{srcdir}); - - my %rendered; - - # check for added or removed pages - my @add; - foreach my $file (@files) { - my $page=pagename($file); - if (! $oldpagemtime{$page}) { - debug("new page $page"); - push @add, $file; - $links{$page}=[]; - $pagesources{$page}=$file; - } - } - my @del; - foreach my $page (keys %oldpagemtime) { - if (! $exists{$page}) { - debug("removing old page $page"); - push @del, $pagesources{$page}; - prune($config{destdir}."/".$renderedfiles{$page}); - delete $renderedfiles{$page}; - $oldpagemtime{$page}=0; - delete $pagesources{$page}; - } - } - - # render any updated files - foreach my $file (@files) { - my $page=pagename($file); - - if (! exists $oldpagemtime{$page} || - mtime("$config{srcdir}/$file") > $oldpagemtime{$page}) { - debug("rendering changed file $file"); - render($file); - $rendered{$file}=1; - } - } - - # if any files were added or removed, check to see if each page - # needs an update due to linking to them - # TODO: inefficient; pages may get rendered above and again here; - # problem is the bestlink may have changed and we won't know until - # now - if (@add || @del) { -FILE: foreach my $file (@files) { - my $page=pagename($file); - foreach my $f (@add, @del) { - my $p=pagename($f); - foreach my $link (@{$links{$page}}) { - if (bestlink($page, $link) eq $p) { - debug("rendering $file, which links to $p"); - render($file); - $rendered{$file}=1; - next FILE; - } - } - } - } - } - - # 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 (map { bestlink($page, $_) } @{$links{$page}}) { - if (length $link && - ! exists $oldlinks{$page} || - ! grep { $_ eq $link } @{$oldlinks{$page}}) { - $linkchanged{$link}=1; - } - } - } - if (exists $oldlinks{$page}) { - foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) { - 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); - } - } - } -} #}}} - -sub gen_wrapper () { #{{{ - eval q{use Cwd 'abs_path'}; - $config{srcdir}=abs_path($config{srcdir}); - $config{destdir}=abs_path($config{destdir}); - my $this=abs_path($0); - if (! -x $this) { - error("$this doesn't seem to be executable"); - } - - if ($config{setup}) { - error("cannot create a wrapper that uses a setup file"); - } - - 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}; - push @params, "--cgi" if $config{cgi}; - 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) { - $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 $config{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 -#include -#include -#include - -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", possibly_foolish_untaint($config{wrapper})) != 0) { - error("failed to compile ikiwiki-wrap.c"); - } - unlink("ikiwiki-wrap.c"); - if (defined $config{wrappermode} && - ! chmod(oct($config{wrappermode}), possibly_foolish_untaint($config{wrapper}))) { - error("chmod $config{wrapper}: $!"); - } - print "successfully generated $config{wrapper}\n"; -} #}}} - sub misctemplate ($$) { #{{{ my $title=shift; my $pagebody=shift; @@ -876,21 +332,6 @@ sub misctemplate ($$) { #{{{ return $template->output; }#}}} -sub cgi_recentchanges ($) { #{{{ - my $q=shift; - - my $template=HTML::Template->new( - filename => "$config{templatedir}/recentchanges.tmpl" - ); - $template->param( - title => "RecentChanges", - indexlink => indexlink(), - wikiname => $config{wikiname}, - changelog => [rcs_recentchanges(100)], - ); - print $q->header, $template->output; -} #}}} - sub userinfo_get ($$) { #{{{ my $user=shift; my $field=shift; @@ -940,166 +381,6 @@ sub userinfo_setall ($$) { #{{{ return $ret; } #}}} -sub cgi_signin ($$) { #{{{ - my $q=shift; - my $session=shift; - - eval q{use CGI::FormBuilder}; - my $form = CGI::FormBuilder->new( - title => "signin", - fields => [qw(do page from 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 "$config{templatedir}/signin.tmpl" ? - "$config{templatedir}/signin.tmpl" : "") - ); - - $form->field(name => "name", required => 0); - $form->field(name => "do", type => "hidden"); - $form->field(name => "page", type => "hidden"); - $form->field(name => "from", 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 first."); - } - - 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( - "$config{cgiurl}?do=".$form->field("do"). - "&page=".$form->field("page"). - "&from=".$form->field("from"));; - } - else { - print $q->redirect($config{url}); - } - } - elsif ($form->submitted eq 'Register') { - my $user_name=$form->field('name'); - if (userinfo_setall($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 => "$config{templatedir}/passwordmail.tmpl" - ); - $template->param( - user_name => $user_name, - user_password => userinfo_get($user_name, "password"), - wikiurl => $config{url}, - wikiname => $config{wikiname}, - REMOTE_ADDR => $ENV{REMOTE_ADDR}, - ); - - eval q{use Mail::Sendmail}; - my ($fromhost) = $config{cgiurl} =~ m!/([^/]+)!; - sendmail( - To => userinfo_get($user_name, "email"), - From => "$config{wikiname} admin <".(getpwuid($>))[0]."@".$fromhost.">", - Subject => "$config{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 is_admin ($) { #{{{ my $user_name=shift; @@ -1135,359 +416,27 @@ sub globlist_match ($$) { #{{{ 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; - - eval q{use CGI::FormBuilder}; - my $form = CGI::FormBuilder->new( - fields => [qw(do rcsinfo from page content comments)], - header => 1, - method => 'POST', - validate => { - content => '/.+/', - }, - required => [qw{content}], - javascript => 0, - params => $q, - action => $q->request_uri, - table => 0, - template => "$config{templatedir}/editpage.tmpl" - ); - my @buttons=("Save Page", "Preview", "Cancel"); - - my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/; - if (! defined $page || ! length $page || $page ne $q->param('page') || - $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) { - error("bad page name"); - } - $page=lc($page); - - my $file=$page.$config{default_pageext}; - my $newfile=1; - if (exists $pagesources{lc($page)}) { - $file=$pagesources{lc($page)}; - $newfile=0; - } - - $form->field(name => "do", type => 'hidden'); - $form->field(name => "from", type => 'hidden'); - $form->field(name => "rcsinfo", 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); - $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); - } - - if ($form->submitted eq "Cancel") { - print $q->redirect("$config{url}/".htmlpage($page)); - return; - } - elsif ($form->submitted eq "Preview") { - $form->tmpl_param("page_preview", - htmlize($config{default_pageext}, - linkify($form->field('content'), $page))); - } - else { - $form->tmpl_param("page_preview", ""); - } - $form->tmpl_param("page_conflict", ""); - - if (! $form->submitted || $form->submitted eq "Preview" || - ! $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("$config{url}/".htmlpage($page)); - return; - } - - my @page_locs; - my $best_loc; - my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/; - if (! defined $from || ! length $from || - $from ne $form->param('from') || - $from=~/$config{wiki_file_prune_regexp}/ || $from=~/^\//) { - @page_locs=$best_loc=$page; - } - 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"; - while (length $dir) { - $dir=~s![^/]+/$!!; - push @page_locs, $dir.$page; - } - - @page_locs = grep { - ! exists $pagesources{lc($_)} && - ! page_locked($_, $session, 1) - } @page_locs; - } - - $form->tmpl_param("page_select", 1); - $form->field(name => "page", type => 'select', - options => \@page_locs, value => $best_loc); - $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=""; - if (exists $pagesources{lc($page)}) { - $content=readfile("$config{srcdir}/$pagesources{lc($page)}"); - $content=~s/\n/\r\n/g; - } - $form->field(name => "content", value => $content, - force => 1); - } - $form->tmpl_param("page_select", 0); - $form->field(name => "page", type => 'hidden'); - $form->title("editing $page"); - } - - print $form->render(submit => \@buttons); - } - 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 (length $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 ($config{svn}) { - if ($newfile) { - rcs_add($file); - } - # prevent deadlock with post-commit hook - unlockwiki(); - # presumably the commit will trigger an update - # of the wiki - my $conflict=rcs_commit($file, $message, - $form->field("rcsinfo")); - - if (defined $conflict) { - $form->field(name => "rcsinfo", value => rcs_prepedit($file), - force => 1); - $form->tmpl_param("page_conflict", 1); - $form->field("content", value => $conflict, force => 1); - $form->field("do", "edit)"); - $form->tmpl_param("page_select", 0); - $form->field(name => "page", type => 'hidden'); - $form->title("editing $page"); - print $form->render(submit => \@buttons); - return; - } - } - else { - loadindex(); - refresh(); - saveindex(); - } - - # The trailing question mark tries to avoid broken - # caches and get the most recent version of the page. - print $q->redirect("$config{url}/".htmlpage($page)."?updated"); - } -} #}}} - -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 => "$config{wikistatedir}/sessions.db" }); - umask($oldmask); - - # Everything below this point needs the user to be signed in. - if ((! $config{anonok} && ! defined $session->param("name") || - ! 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); - } - elsif ($do eq 'prefs') { - cgi_prefs($q, $session); - } - else { - error("unknown do parameter"); - } -} #}}} - -sub setup () { # {{{ - my $setup=possibly_foolish_untaint($config{setup}); - delete $config{setup}; - open (IN, $setup) || error("read $setup: $!\n"); - local $/=undef; - my $code=; - ($code)=$code=~/(.*)/s; - close IN; - - eval $code; - error($@) if $@; - exit; -} #}}} - # main {{{ -setup() if $config{setup}; +memoize('pagename'); +memoize('bestlink'); +if ($config{setup}) { + require IkiWiki::Setup; + setup(); +} lockwiki(); if ($config{wrapper}) { + require IkiWiki::Wrapper; gen_wrapper(); exit; } -memoize('pagename'); -memoize('bestlink'); loadindex() unless $config{rebuild}; if ($config{cgi}) { + require IkiWiki::CGI; cgi(); } else { - rcs_update() if $config{svn}; + require IkiWiki::Render; + rcs_update(); refresh(); saveindex(); }