summaryrefslogtreecommitdiff
path: root/IkiWiki
diff options
context:
space:
mode:
authorjoey <joey@0fa5a96a-9a0e-0410-b3b2-a0fd24251071>2006-03-23 06:51:15 +0000
committerjoey <joey@0fa5a96a-9a0e-0410-b3b2-a0fd24251071>2006-03-23 06:51:15 +0000
commit6c8cf5dd571662f981227489f7c4652a1a1f10cd (patch)
tree778453ccf0d85720f37579f6e3c95624fdb5f960 /IkiWiki
parent7b0346bf8234100f608aa7f24684becc952e8956 (diff)
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.
Diffstat (limited to 'IkiWiki')
-rw-r--r--IkiWiki/CGI.pm509
-rw-r--r--IkiWiki/RCS/SVN.pm169
-rw-r--r--IkiWiki/RCS/Stub.pm26
-rw-r--r--IkiWiki/Render.pm316
-rw-r--r--IkiWiki/Setup.pm22
-rw-r--r--IkiWiki/Setup/Standard.pm37
-rw-r--r--IkiWiki/Wrapper.pm96
7 files changed, 1160 insertions, 15 deletions
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 =~ /(?<!\\)$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
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=<IN>;
+ ($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 <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