From b73debadba7994ecb882b3405376e3d7ece124ef Mon Sep 17 00:00:00 2001 From: www-data Date: Thu, 23 Mar 2006 01:48:01 +0000 Subject: [PATCH 01/16] web commit by joey --- doc/todo.mdwn | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/todo.mdwn b/doc/todo.mdwn index 1d4f4759a..fa48f1aec 100644 --- a/doc/todo.mdwn +++ b/doc/todo.mdwn @@ -31,6 +31,9 @@ is built. (As long as all changes to all pages is ok.) page that lets them tune it, and probably choose literal or glob by default. + I think that the new globlist() function should do everything you need. + Adding a field to the prefs page will be trivial --[[Joey]] + The first cut, I suppose, could use one sendmail process to batch-mail all subscribers for a given page. However, in the long run, I can see users demanding a bit of feature creep: -- 2.39.5 From def02747934ebb9bbcc09de0824667666448881e Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 01:49:55 +0000 Subject: [PATCH 02/16] add another example --- basewiki/globlist.mdwn | 4 ++++ doc/globlist.mdwn | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/basewiki/globlist.mdwn b/basewiki/globlist.mdwn index 94f18127c..99ea73e73 100644 --- a/basewiki/globlist.mdwn +++ b/basewiki/globlist.mdwn @@ -14,3 +14,7 @@ pages that match it. So if you want to specify all pages except for Discussion pages: !*/Discussion + +Here's how to specify all pages except discussion pages and the SandBox: + + * !SandBox !*/Discussion diff --git a/doc/globlist.mdwn b/doc/globlist.mdwn index 94f18127c..99ea73e73 100644 --- a/doc/globlist.mdwn +++ b/doc/globlist.mdwn @@ -14,3 +14,7 @@ pages that match it. So if you want to specify all pages except for Discussion pages: !*/Discussion + +Here's how to specify all pages except discussion pages and the SandBox: + + * !SandBox !*/Discussion -- 2.39.5 From bb04b36cc932f6f5c992da11cb8ffc08ea412c53 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 02:03:48 +0000 Subject: [PATCH 03/16] Fix a bad use of implicit return for admin-less wikis. I got lucky.. --- ikiwiki | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ikiwiki b/ikiwiki index aec52ca86..80ad72526 100755 --- a/ikiwiki +++ b/ikiwiki @@ -1137,6 +1137,8 @@ sub page_locked ($$;$) { #{{{ htmllink("", $admin, 1)." and cannot be edited."); } } + + return 0; } #}}} sub cgi_prefs ($$) { #{{{ -- 2.39.5 From 509479f236eaa130965f768421cf978dc9e6b8f2 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 02:30:51 +0000 Subject: [PATCH 04/16] improve --- basewiki/globlist.mdwn | 6 +----- doc/globlist.mdwn | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/basewiki/globlist.mdwn b/basewiki/globlist.mdwn index 99ea73e73..a93a15b06 100644 --- a/basewiki/globlist.mdwn +++ b/basewiki/globlist.mdwn @@ -11,10 +11,6 @@ name. So if you wanted to list all the pages about tea, and any You can also prefix an item in the list with "!" to skip matching any pages that match it. So if you want to specify all pages except for -Discussion pages: - - !*/Discussion - -Here's how to specify all pages except discussion pages and the SandBox: +Discussion pages and the SandBox: * !SandBox !*/Discussion diff --git a/doc/globlist.mdwn b/doc/globlist.mdwn index 99ea73e73..a93a15b06 100644 --- a/doc/globlist.mdwn +++ b/doc/globlist.mdwn @@ -11,10 +11,6 @@ name. So if you wanted to list all the pages about tea, and any You can also prefix an item in the list with "!" to skip matching any pages that match it. So if you want to specify all pages except for -Discussion pages: - - !*/Discussion - -Here's how to specify all pages except discussion pages and the SandBox: +Discussion pages and the SandBox: * !SandBox !*/Discussion -- 2.39.5 From 483f61d228a547627512ec14cc16345bdd4dc159 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 02:31:51 +0000 Subject: [PATCH 05/16] improve --- basewiki/globlist.mdwn | 2 +- doc/globlist.mdwn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/basewiki/globlist.mdwn b/basewiki/globlist.mdwn index a93a15b06..30bc837ac 100644 --- a/basewiki/globlist.mdwn +++ b/basewiki/globlist.mdwn @@ -9,7 +9,7 @@ name. So if you wanted to list all the pages about tea, and any *tea* SandBox/* -You can also prefix an item in the list with "!" to skip matching any +You can also prefix an item in the list with "`!`" to skip matching any pages that match it. So if you want to specify all pages except for Discussion pages and the SandBox: diff --git a/doc/globlist.mdwn b/doc/globlist.mdwn index a93a15b06..30bc837ac 100644 --- a/doc/globlist.mdwn +++ b/doc/globlist.mdwn @@ -9,7 +9,7 @@ name. So if you wanted to list all the pages about tea, and any *tea* SandBox/* -You can also prefix an item in the list with "!" to skip matching any +You can also prefix an item in the list with "`!`" to skip matching any pages that match it. So if you want to specify all pages except for Discussion pages and the SandBox: -- 2.39.5 From 0b1828f694dde648c63a192a132308348438379a Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 02:53:03 +0000 Subject: [PATCH 06/16] *warning* any wrappers built with a previous version of ikiwiki need to be rebuilt This changes ikiwiki's syntax to require only 2 parameters (source and dest) and not three. The templatedir parameter is now an optional --templatedir. --- Makefile.PL | 4 ++-- doc/ikiwiki.setup | 4 ++-- doc/setup.mdwn | 11 +++++------ doc/usage.mdwn | 10 +++++++--- ikiwiki | 13 +++++++------ 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Makefile.PL b/Makefile.PL index e5d05dea4..70d81b806 100755 --- a/Makefile.PL +++ b/Makefile.PL @@ -12,8 +12,8 @@ install:: extra_install pure_install:: extra_install extra_build: - ./ikiwiki doc templates html --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: diff --git a/doc/ikiwiki.setup b/doc/ikiwiki.setup index 7a561434e..374093a5e 100644 --- a/doc/ikiwiki.setup +++ b/doc/ikiwiki.setup @@ -12,12 +12,12 @@ use IkiWiki::Setup::Standard { # Be sure to customise these.. srcdir => "/path/to/source", destdir => "/var/www/wiki", - templatedir => "/usr/share/ikiwiki/templates", - + url => "http://myhost/wiki", cgiurl => "http://myhost/ikiwiki.cgi", #historyurl => "http://svn.myhost/trunk/[[file]]", #diffurl => "http://svn.someurl/trunk/[[file]]?root=wiki&r1=[[r1]]&r2=[[r2]]", + #templatedir => "/usr/share/ikiwiki/templates", # Whether to integrate with svn. svn => 1, diff --git a/doc/setup.mdwn b/doc/setup.mdwn index 44e60ae97..69972ca52 100644 --- a/doc/setup.mdwn +++ b/doc/setup.mdwn @@ -29,11 +29,10 @@ optional support for commits from the web. 5. Build your wiki for the first time. - ikiwiki --verbose ~/wikiwc/ \ - /usr/share/ikiwiki/templates ~/public_html/wiki/ \ - --url=http://host/~you/wiki/ + ikiwiki --verbose ~/wikiwc/ ~/public_html/wiki/ \ + --url=http://host/~you/wiki/ - Replace the url with the right url to your wiki. You should now + Replace the url with the real url to your wiki. You should now be able to visit the url and see your page that you created earlier. 6. Repeat steps 4 and 5 as desired, editing or adding pages and rebuilding @@ -50,8 +49,8 @@ optional support for commits from the web. `doc/ikiwiki.setup` in the ikiwiki sources), and edit it. Most of the options, like `wikiname` in the setup file are the same as - ikiwiki's command line options (documented in [[usage]]. `srcdir`, - `templatedir` and `destdir` are the three directories you specify when + ikiwiki's command line options (documented in [[usage]]. `srcdir` + and `destdir` are the two directories you specify when running ikiwiki by hand. `svnrepo` is the path to your subversion repository. Make sure that all of these are pointing to the right directories, and read through and configure the rest of the file to your diff --git a/doc/usage.mdwn b/doc/usage.mdwn index 7d7acf16a..83866c1a8 100644 --- a/doc/usage.mdwn +++ b/doc/usage.mdwn @@ -4,15 +4,14 @@ ikiwiki - a wiki compiler # SYNOPSIS -ikiwiki [options] source templates destination +ikiwiki [options] source destination ikiwiki --setup configfile # DESCRIPTION `ikiwiki` is a wiki compiler. It builds static html pages for a wiki, from -`source` in the [[MarkDown]] language, using the specified html `templates` -and writes it out to `destination`. +`source` in the [[MarkDown]] language, and writes it out to `destination`. # OPTIONS @@ -31,6 +30,11 @@ flags such as --verbose can be negated with --no-verbose. Force a rebuild of all pages. +* --templatedir + + Specify the directory that the page [[templates]] are stored in. + Default is `/usr/share/ikiwiki/templates`. + * --wrapper [file] Generate a [[wrapper]] binary that is hardcoded to do action specified by diff --git a/ikiwiki b/ikiwiki index 80ad72526..358123543 100755 --- a/ikiwiki +++ b/ikiwiki @@ -33,7 +33,7 @@ our %config=( #{{{ wrappermode => undef, srcdir => undef, destdir => undef, - templatedir => undef, + templatedir => "/usr/share/ikiwiki/templates", setup => undef, adminuser => undef, ); #}}} @@ -56,12 +56,12 @@ GetOptions( #{{{ $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/; }, "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] }, + "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) }, ) || usage(); if (! $config{setup}) { - usage() unless @ARGV == 3; + usage() unless @ARGV == 2; $config{srcdir} = possibly_foolish_untaint(shift); - $config{templatedir} = possibly_foolish_untaint(shift); $config{destdir} = possibly_foolish_untaint(shift); if ($config{cgi} && ! length $config{url}) { error("Must specify url to wiki with --url when using --cgi"); @@ -70,7 +70,7 @@ if (! $config{setup}) { #}}} sub usage { #{{{ - die "usage: ikiwiki [options] source templates dest\n"; + die "usage: ikiwiki [options] source dest\n"; } #}}} sub error { #{{{ @@ -772,8 +772,9 @@ sub gen_wrapper (@) { #{{{ error("cannot create a wrapper that uses a setup file"); } - my @params=($config{srcdir}, $config{templatedir}, $config{destdir}, - "--wikiname=$config{wikiname}"); + my @params=($config{srcdir}, $config{destdir}, + "--wikiname=$config{wikiname}", + "--templatedir=$config{templatedir}"); push @params, "--verbose" if $config{verbose}; push @params, "--rebuild" if $config{rebuild}; push @params, "--nosvn" if !$config{svn}; -- 2.39.5 From e4d9da55d923cdd78cd07959de44edf17a9a5fe5 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:01:02 +0000 Subject: [PATCH 07/16] At Branden's request, clean up the hardcoded ".ikiwiki" everywhere, and add checkoptions() that can be used to set defaults for this and other options based on existing options. Also involved some cleanups to how gen_wrapper is used. --- IkiWiki/Setup/Standard.pm | 5 ++++- ikiwiki | 43 +++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/IkiWiki/Setup/Standard.pm b/IkiWiki/Setup/Standard.pm index 68f43b408..76213b11a 100644 --- a/IkiWiki/Setup/Standard.pm +++ b/IkiWiki/Setup/Standard.pm @@ -14,7 +14,9 @@ sub import { ::debug("generating wrappers.."); foreach my $wrapper (@{$setup{wrappers}}) { - ::gen_wrapper(%::config, verbose => 0, %setup, %{$wrapper}); + %::config=(%::config, verbose => 0, %setup, %{$wrapper}); + ::checkoptions(); + ::gen_wrapper(); } ::debug("rebuilding wiki.."); @@ -23,6 +25,7 @@ sub import { if defined $setup{$c} && ! ref $setup{$c}; } $::config{rebuild}=1; + ::checkoptions(); ::refresh(); ::debug("done"); diff --git a/ikiwiki b/ikiwiki index 358123543..51f324d13 100755 --- a/ikiwiki +++ b/ikiwiki @@ -63,11 +63,17 @@ if (! $config{setup}) { usage() unless @ARGV == 2; $config{srcdir} = possibly_foolish_untaint(shift); $config{destdir} = possibly_foolish_untaint(shift); + checkoptions(); +} +#}}} + +sub checkoptions { #{{{ if ($config{cgi} && ! length $config{url}) { error("Must specify url to wiki with --url when using --cgi"); } -} -#}}} + $config{wikistatedir}="$config{srcdir}/.ikiwiki" + unless exists $config{wikistatedir}; +} #}}} sub usage { #{{{ die "usage: ikiwiki [options] source dest\n"; @@ -415,10 +421,11 @@ sub render ($) { #{{{ sub lockwiki () { #{{{ # Take an exclusive lock on the wiki to prevent multiple concurrent # run issues. The lock will be dropped on program exit. - if (! -d "$config{srcdir}/.ikiwiki") { - mkdir("$config{srcdir}/.ikiwiki"); + if (! -d $config{wikistatedir}) { + mkdir($config{wikistatedir}); } - open(WIKILOCK, ">$config{srcdir}/.ikiwiki/lockfile") || error ("cannot write to lockfile: $!"); + open(WIKILOCK, ">$config{wikistatedir}/lockfile") || + error ("cannot write to $config{wikistatedir}/lockfile: $!"); if (! flock(WIKILOCK, 2 | 4)) { debug("wiki seems to be locked, waiting for lock"); my $wait=600; # arbitrary, but don't hang forever to @@ -436,7 +443,7 @@ sub unlockwiki () { #{{{ } #}}} sub loadindex () { #{{{ - open (IN, "$config{srcdir}/.ikiwiki/index") || return; + open (IN, "$config{wikistatedir}/index") || return; while () { $_=possibly_foolish_untaint($_); chomp; @@ -452,10 +459,11 @@ sub loadindex () { #{{{ } #}}} sub saveindex () { #{{{ - if (! -d "$config{srcdir}/.ikiwiki") { - mkdir("$config{srcdir}/.ikiwiki"); + if (! -d $config{wikistatedir}) { + mkdir($config{wikistatedir}); } - open (OUT, ">$config{srcdir}/.ikiwiki/index") || error("cannot write to index: $!"); + open (OUT, ">$config{wikistatedir}/index") || + error("cannot write to $config{wikistatedir}/index: $!"); foreach my $page (keys %oldpagemtime) { print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ". join(" ", @{$links{$page}})."\n" @@ -758,8 +766,7 @@ FILE: foreach my $file (@files) { } } #}}} -sub gen_wrapper (@) { #{{{ - my %config=(@_); +sub gen_wrapper () { #{{{ eval q{use Cwd 'abs_path'}; $config{srcdir}=abs_path($config{srcdir}); $config{destdir}=abs_path($config{destdir}); @@ -883,7 +890,7 @@ sub userinfo_get ($$) { #{{{ my $field=shift; eval q{use Storable}; - my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") }; + my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") }; if (! defined $userdata || ! ref $userdata || ! exists $userdata->{$user} || ! ref $userdata->{$user} || ! exists $userdata->{$user}->{$field}) { @@ -898,7 +905,7 @@ sub userinfo_set ($$$) { #{{{ my $value=shift; eval q{use Storable}; - my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") }; + my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") }; if (! defined $userdata || ! ref $userdata || ! exists $userdata->{$user} || ! ref $userdata->{$user}) { return ""; @@ -906,7 +913,7 @@ sub userinfo_set ($$$) { #{{{ $userdata->{$user}->{$field}=$value; my $oldmask=umask(077); - my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb"); + my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb"); umask($oldmask); return $ret; } #}}} @@ -916,13 +923,13 @@ sub userinfo_setall ($$) { #{{{ my $info=shift; eval q{use Storable}; - my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") }; + my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") }; if (! defined $userdata || ! ref $userdata) { $userdata={}; } $userdata->{$user}=$info; my $oldmask=umask(077); - my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb"); + my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb"); umask($oldmask); return $ret; } #}}} @@ -1418,7 +1425,7 @@ sub cgi () { #{{{ my $oldmask=umask(077); my $session = CGI::Session->new("driver:db_file", $q, - { FileName => "$config{srcdir}/.ikiwiki/sessions.db" }); + { FileName => "$config{wikistatedir}/sessions.db" }); umask($oldmask); # Everything below this point needs the user to be signed in. @@ -1464,7 +1471,7 @@ sub setup () { # {{{ setup() if $config{setup}; lockwiki(); if ($config{wrapper}) { - gen_wrapper(%config); + gen_wrapper(); exit; } memoize('pagename'); -- 2.39.5 From 8978c1d959bd016fade8d3db39da50680a7314a5 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:02:19 +0000 Subject: [PATCH 08/16] fix oops in %config handling --- IkiWiki/Setup/Standard.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IkiWiki/Setup/Standard.pm b/IkiWiki/Setup/Standard.pm index 76213b11a..da712e94a 100644 --- a/IkiWiki/Setup/Standard.pm +++ b/IkiWiki/Setup/Standard.pm @@ -13,8 +13,9 @@ sub import { my %setup=%{$_[1]}; ::debug("generating wrappers.."); + my %startconfig=(%::config); foreach my $wrapper (@{$setup{wrappers}}) { - %::config=(%::config, verbose => 0, %setup, %{$wrapper}); + %::config=(%startconfig, verbose => 0, %setup, %{$wrapper}); ::checkoptions(); ::gen_wrapper(); } -- 2.39.5 From c0b1bfd0cd40830b85c4d0e573a32ff12dbbc927 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:05:26 +0000 Subject: [PATCH 09/16] -s == --setup --- ikiwiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ikiwiki b/ikiwiki index 51f324d13..78aa65ce2 100755 --- a/ikiwiki +++ b/ikiwiki @@ -39,7 +39,7 @@ our %config=( #{{{ ); #}}} GetOptions( #{{{ - "setup=s" => \$config{setup}, + "setup|s=s" => \$config{setup}, "wikiname=s" => \$config{wikiname}, "verbose|v!" => \$config{verbose}, "rebuild!" => \$config{rebuild}, -- 2.39.5 From 2eb5893ce7095475cadc07f9f3b0c50eb6efc68d Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:07:58 +0000 Subject: [PATCH 10/16] fix config restoration after wrapper gen --- IkiWiki/Setup/Standard.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IkiWiki/Setup/Standard.pm b/IkiWiki/Setup/Standard.pm index da712e94a..4d1118f30 100644 --- a/IkiWiki/Setup/Standard.pm +++ b/IkiWiki/Setup/Standard.pm @@ -19,7 +19,8 @@ sub import { ::checkoptions(); ::gen_wrapper(); } - + %::config=(%startconfig); + ::debug("rebuilding wiki.."); foreach my $c (keys %setup) { $::config{$c}=::possibly_foolish_untaint($setup{$c}) -- 2.39.5 From 62f1f9732b746a84a1fd3ee67b70f7a297fcdb42 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:33:35 +0000 Subject: [PATCH 11/16] found & fixed another symlink attack --- doc/security.mdwn | 20 ++++++++++++++++++-- ikiwiki | 10 +++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/doc/security.mdwn b/doc/security.mdwn index 63d140ec5..c7a6fcd69 100644 --- a/doc/security.mdwn +++ b/doc/security.mdwn @@ -141,6 +141,22 @@ into the repo. ikiwiki uses File::Find to traverse the repo, and does not tell it to follow symlinks, but it might be possible to race replacing a directory with a symlink and trick it into following the link. -Also, if someone checks in a symlink to /etc/passwd, ikiwiki would read and publish that, which could be used to expose files a committer otherwise wouldn't see. +Also, if someone checks in a symlink to /etc/passwd, ikiwiki would read and +publish that, which could be used to expose files a committer otherwise +wouldn't see. -To avoid this, ikiwiki will avoid reading files that are symlinks, and uses locking to prevent more than one instance running at a time. The lock prevents one ikiwiki from running a svn up at the wrong time to race another ikiwiki. So only attackers who can write to the working copy on their own can race it. +To avoid this, ikiwiki will skip over symlinks when scanning for pages, and +uses locking to prevent more than one instance running at a time. The lock +prevents one ikiwiki from running a svn up at the wrong time to race +another ikiwiki. So only attackers who can write to the working copy on +their own can race it. + +## symlink + cgi attacks + +Similarly, a svn commit of a symlink could be made, ikiwiki ignores it +because of the above, but the symlink is still there, and then you edit the +page from the web, which follows the symlink when reading the page, and +again when saving the changed page. + +This was fixed by making ikiwiki refuse to read or write to files that are +symlinks, combined with the above locking. diff --git a/ikiwiki b/ikiwiki index 78aa65ce2..6b8a51535 100755 --- a/ikiwiki +++ b/ikiwiki @@ -152,6 +152,10 @@ sub htmlpage ($) { #{{{ sub readfile ($) { #{{{ my $file=shift; + if (-l $file) { + error("cannot read a symlink ($file)"); + } + local $/=undef; open (IN, "$file") || error("failed to read $file: $!"); my $ret=; @@ -162,6 +166,10 @@ sub readfile ($) { #{{{ sub writefile ($$) { #{{{ my $file=shift; my $content=shift; + + if (-l $file) { + error("cannot write to a symlink ($file)"); + } my $dir=dirname($file); if (! -d $dir) { @@ -1334,7 +1342,7 @@ sub cgi_editpage ($$) { #{{{ ! length $form->field('content')) { my $content=""; if (exists $pagesources{lc($page)}) { - $content=readfile("$config{srcdir}/$pagesources{lc($page)}"); + $content=readfile("$config{srcdir}/$pagesources{lc($page)}"); $content=~s/\n/\r\n/g; } $form->field(name => "content", value => $content, -- 2.39.5 From 03781baeb096a752753eae09bc775499f171b498 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 04:44:32 +0000 Subject: [PATCH 12/16] underlays are *hard*. feh --- doc/todo.mdwn | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/doc/todo.mdwn b/doc/todo.mdwn index fa48f1aec..bdebfb88c 100644 --- a/doc/todo.mdwn +++ b/doc/todo.mdwn @@ -7,7 +7,7 @@ * Should support RSS for notification of new and changed pages. - This can be a static rss file that is generated when the moo + This can be a static rss file that is generated when the wiki is built. (As long as all changes to all pages is ok.) * Should support mail notification of new and changed pages. @@ -105,19 +105,29 @@ you need that data.. ## page indexes -Might be nice to support automatically generating an index based on headers in a page, for long pages. The question is, how to turn on such an index? +Might be nice to support automatically generating an index based on headers +in a page, for long pages. The question is, how to turn on such an index? ## basewiki underlay -Rather than copy the basewii around everywhere, it should be configured to +Rather than copy the basewiki around everywhere, it should be configured to underlay the main srcdir, and pages be rendered from there if not in the srcdir. This would allow upgrades to add/edit pages in the basewiki. Impementaion will be slightly tricky since currently ikiwiki is hardcoded -in many places to look in srcdir for pages. +in many places to look in srcdir for pages. Also, there are possible +security attacks in the vein of providing a file ikiwiki would normally +skip in the srcdir, and tricking it to processing this file instead of the +one from the underlaydir. + +There are also difficulties related to removing files from the srcdir, and +exposing ones from the underlaydir. Will need to make sure that the mtime +for the source file is zeroed when the page is removed, and that it then +finds the underlay file and treats it as newer. ## Logo -ikiwiki needs a logo. I'm thinking something simple like the word "ikiwiki" with the first "k" backwards; drawn to show that it's "wiki" reflected. +ikiwiki needs a logo. I'm thinking something simple like the word "ikiwiki" +with the first "k" backwards; drawn to show that it's "wiki" reflected. ## [[Bugs]] -- 2.39.5 From 7b0346bf8234100f608aa7f24684becc952e8956 Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 05:21:17 +0000 Subject: [PATCH 13/16] remove accidental makemaker cruft --- ikiwiki | 2 -- 1 file changed, 2 deletions(-) diff --git a/ikiwiki b/ikiwiki index 6b8a51535..d9dddbe4e 100755 --- a/ikiwiki +++ b/ikiwiki @@ -1,7 +1,5 @@ #!/usr/bin/perl -T -eval 'exec /usr/bin/perl -T -S $0 ${1+"$@"}' - if 0; # not running under some shell $ENV{PATH}="/usr/local/bin:/usr/bin:/bin"; use warnings; -- 2.39.5 From 6c8cf5dd571662f981227489f7c4652a1a1f10cd Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 06:51:15 +0000 Subject: [PATCH 14/16] 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. --- IkiWiki/CGI.pm | 509 +++++++++++++++++ IkiWiki/RCS/SVN.pm | 169 ++++++ IkiWiki/RCS/Stub.pm | 26 + IkiWiki/Render.pm | 316 +++++++++++ IkiWiki/Setup.pm | 22 + IkiWiki/Setup/Standard.pm | 37 +- IkiWiki/Wrapper.pm | 96 ++++ Makefile.PL | 5 +- doc/todo.mdwn | 7 + ikiwiki | 1101 +------------------------------------ 10 files changed, 1195 insertions(+), 1093 deletions(-) create mode 100644 IkiWiki/CGI.pm create mode 100644 IkiWiki/RCS/SVN.pm create mode 100644 IkiWiki/RCS/Stub.pm create mode 100644 IkiWiki/Render.pm create mode 100644 IkiWiki/Setup.pm create mode 100644 IkiWiki/Wrapper.pm 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(); } -- 2.39.5 From b645dc5a4118feabd37d95fabbc6aaa803e3c45f Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 07:37:16 +0000 Subject: [PATCH 15/16] Getopt::Long is a huge, heavy perl module. So why use it? This {gross,amazing} hack makes all wrapped uses of ikiwiki forgo any option parsing at all. Options come in preparses via an env var from the wrapper. As a bonus, Wrapper.pm no longer needs to be updated when command line options are added to the program. Load time is sped up by around 10%. ikiwikiwrap --params is no longer supported by this change. You will need to rebuild your wrappers to take advantage of it. --- IkiWiki/Wrapper.pm | 48 ++++++++++------------------- doc/usage.mdwn | 4 +-- doc/wrapperparamstrick.mdwn | 20 ------------ ikiwiki | 61 +++++++++++++++++++++---------------- 4 files changed, 52 insertions(+), 81 deletions(-) delete mode 100644 doc/wrapperparamstrick.mdwn diff --git a/IkiWiki/Wrapper.pm b/IkiWiki/Wrapper.pm index 8e513c1f6..62d284eb6 100644 --- a/IkiWiki/Wrapper.pm +++ b/IkiWiki/Wrapper.pm @@ -2,11 +2,12 @@ use warnings; use strict; +use Cwd q{abs_path}; +use Data::Dumper; 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); @@ -17,26 +18,8 @@ sub gen_wrapper () { #{{{ 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 $wrapper=possibly_foolish_untaint($config{wrapper}); + delete $config{wrapper}; my @envsave; push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI @@ -50,6 +33,11 @@ sub gen_wrapper () { #{{{ EOF } + $Data::Dumper::Indent=0; + my $configstring=Data::Dumper->Dump([\%config], ['*config']); + $configstring=~s/\\/\\\\/g; + $configstring=~s/"/\\"/g; + open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");; print OUT <<"EOF"; /* A wrapper for ikiwiki, can be safely made suid. */ @@ -64,33 +52,29 @@ extern char **environ; int main (int argc, char **argv) { /* Sanitize environment. */ char *s; - char *newenviron[$#envsave+3]; + char *newenviron[$#envsave+4]; int i=0; $envsave newenviron[i++]="HOME=$ENV{HOME}"; + newenviron[i++]="WRAPPED_OPTIONS=$configstring"; newenviron[i]=NULL; environ=newenviron; - if (argc == 2 && strcmp(argv[1], "--params") == 0) { - printf("$params\\n"); - exit(0); - } - - execl($call); + execl("$this", "$this", NULL); perror("failed to run $this"); exit(1); } EOF close OUT; - if (system("gcc", "ikiwiki-wrap.c", "-o", possibly_foolish_untaint($config{wrapper})) != 0) { + if (system("gcc", "ikiwiki-wrap.c", "-o", $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}: $!"); + ! chmod(oct($config{wrappermode}), $wrapper)) { + error("chmod $wrapper: $!"); } - print "successfully generated $config{wrapper}\n"; + print "successfully generated $wrapper\n"; } #}}} 1 diff --git a/doc/usage.mdwn b/doc/usage.mdwn index 83866c1a8..b9744438b 100644 --- a/doc/usage.mdwn +++ b/doc/usage.mdwn @@ -44,9 +44,7 @@ flags such as --verbose can be negated with --no-verbose. The wrapper is designed to be safely made suid and be run by untrusted users, as a [[Subversion]] [[post-commit]] hook, or as a [[CGI]]. - Note that the generated wrapper will ignore all command line parameters - except for --params, which will make it print out the parameters it would - run ikiwiki with. + Note that the generated wrapper will ignore all command line parameters. * --wrappermode mode diff --git a/doc/wrapperparamstrick.mdwn b/doc/wrapperparamstrick.mdwn deleted file mode 100644 index d55629a79..000000000 --- a/doc/wrapperparamstrick.mdwn +++ /dev/null @@ -1,20 +0,0 @@ -ikiwiki --wrapper can be used to generate a wrapper -program that runs ikiwiki with the specified parameters. This is used for -[[post-commit]] hooks, [[CGI]], etc, both for convenience and because these -things often need suid wrapper scripts to make ikiwiki run as the right -user. - -The generated wrapper is a binary program. What if you want to regenerate -it with different parameters, or just run ikiwiki like it would but with -some parameter changed? To easily accomomplish this, the wrappers all -support being run with --params, which causes them to print out the -parameters they run ikiwiki with. - -You can use this trick to regenerate a wrapper, adding or changing a -parameter: - - ikiwiki $(./ikiwiki-wrap --params) --wikiname="newname" --wrapper - -Or just to run ikiwiki like the wrapper would, and add a parameter: - - ikiwiki $(./ikiwiki-wrap --params) --rebuild diff --git a/ikiwiki b/ikiwiki index cdc5c8ca4..b59aa8c8f 100755 --- a/ikiwiki +++ b/ikiwiki @@ -10,7 +10,6 @@ use strict; use Memoize; use File::Spec; use HTML::Template; -use Getopt::Long; use vars qw{%config %links %oldlinks %oldpagemtime %renderedfiles %pagesources}; @@ -39,31 +38,41 @@ our %config=( #{{{ adminuser => undef, ); #}}} -GetOptions( #{{{ - "setup|s=s" => \$config{setup}, - "wikiname=s" => \$config{wikiname}, - "verbose|v!" => \$config{verbose}, - "rebuild!" => \$config{rebuild}, - "wrapper:s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" }, - "wrappermode=i" => \$config{wrappermode}, - "svn!" => \$config{svn}, - "anonok!" => \$config{anonok}, - "cgi!" => \$config{cgi}, - "url=s" => \$config{url}, - "cgiurl=s" => \$config{cgiurl}, - "historyurl=s" => \$config{historyurl}, - "diffurl=s" => \$config{diffurl}, - "exclude=s@" => sub { - $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/; - }, - "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] }, - "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) }, -) || usage(); - -if (! $config{setup}) { - usage() unless @ARGV == 2; - $config{srcdir} = possibly_foolish_untaint(shift); - $config{destdir} = possibly_foolish_untaint(shift); +# option parsing #{{{ +if (! exists $ENV{WRAPPED_OPTIONS}) { + eval q{use Getopt::Long}; + GetOptions( + "setup|s=s" => \$config{setup}, + "wikiname=s" => \$config{wikiname}, + "verbose|v!" => \$config{verbose}, + "rebuild!" => \$config{rebuild}, + "wrapper:s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" }, + "wrappermode=i" => \$config{wrappermode}, + "svn!" => \$config{svn}, + "anonok!" => \$config{anonok}, + "cgi!" => \$config{cgi}, + "url=s" => \$config{url}, + "cgiurl=s" => \$config{cgiurl}, + "historyurl=s" => \$config{historyurl}, + "diffurl=s" => \$config{diffurl}, + "exclude=s@" => sub { + $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/; + }, + "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] }, + "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) }, + ) || usage(); + + if (! $config{setup}) { + usage() unless @ARGV == 2; + $config{srcdir} = possibly_foolish_untaint(shift); + $config{destdir} = possibly_foolish_untaint(shift); + checkoptions(); + } +} +else { + # wrapper passes a full config structure in the environment + # variable + eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS}); checkoptions(); } #}}} -- 2.39.5 From bb0193aff3e044e93006ee7ad0aaef1a82adaeee Mon Sep 17 00:00:00 2001 From: joey Date: Thu, 23 Mar 2006 07:42:24 +0000 Subject: [PATCH 16/16] benchmarking suggests that meoization doesn't help even when doing a --rebuild, so remove it --- IkiWiki/Wrapper.pm | 2 +- ikiwiki | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/IkiWiki/Wrapper.pm b/IkiWiki/Wrapper.pm index 62d284eb6..4966c453a 100644 --- a/IkiWiki/Wrapper.pm +++ b/IkiWiki/Wrapper.pm @@ -33,7 +33,7 @@ sub gen_wrapper () { #{{{ EOF } - $Data::Dumper::Indent=0; + $Data::Dumper::Indent=0; # no newlines my $configstring=Data::Dumper->Dump([\%config], ['*config']); $configstring=~s/\\/\\\\/g; $configstring=~s/"/\\"/g; diff --git a/ikiwiki b/ikiwiki index b59aa8c8f..7f6480e0c 100755 --- a/ikiwiki +++ b/ikiwiki @@ -7,7 +7,6 @@ use lib '.'; # For use without installation, removed by Makefile. package IkiWiki; use warnings; use strict; -use Memoize; use File::Spec; use HTML::Template; @@ -426,8 +425,6 @@ sub globlist_match ($$) { #{{{ } #}}} # main {{{ -memoize('pagename'); -memoize('bestlink'); if ($config{setup}) { require IkiWiki::Setup; setup(); -- 2.39.5