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.
master
joey 2006-03-23 06:51:15 +00:00
parent 7b0346bf82
commit 6c8cf5dd57
10 changed files with 1195 additions and 1093 deletions

509
IkiWiki/CGI.pm 100644
View File

@ -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

169
IkiWiki/RCS/SVN.pm 100644
View File

@ -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

View File

@ -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

316
IkiWiki/Render.pm 100644
View File

@ -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 =~ /(?<!\\)$config{wiki_link_regexp}/g) {
push @links, lc($1);
}
# Discussion links are a special case since they're not in the text
# of the page, but on its template.
return @links, "$page/discussion";
} #}}}
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 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);
}
}
}
} #}}}
1

22
IkiWiki/Setup.pm 100644
View File

@ -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=<IN>;
($code)=$code=~/(.*)/s;
close IN;
eval $code;
error($@) if $@;
exit;
} #}}}
1

View File

@ -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

96
IkiWiki/Wrapper.pm 100644
View File

@ -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 <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", 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

View File

@ -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'],
);

View File

@ -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

1101
ikiwiki

File diff suppressed because it is too large Load Diff