summaryrefslogtreecommitdiff
path: root/ikiwiki
blob: 358123543dfa8d9d27f68c7fb1a80d0806315658 (plain)
  1. #!/usr/bin/perl -T
  2. eval 'exec /usr/bin/perl -T -S $0 ${1+"$@"}'
  3. if 0; # not running under some shell
  4. $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
  5. use warnings;
  6. use strict;
  7. use Memoize;
  8. use File::Spec;
  9. use HTML::Template;
  10. use Getopt::Long;
  11. my (%links, %oldlinks, %oldpagemtime, %renderedfiles, %pagesources);
  12. # Holds global config settings, also used by some modules.
  13. our %config=( #{{{
  14. wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
  15. wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
  16. wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
  17. verbose => 0,
  18. wikiname => "wiki",
  19. default_pageext => ".mdwn",
  20. cgi => 0,
  21. svn => 1,
  22. url => '',
  23. cgiurl => '',
  24. historyurl => '',
  25. diffurl => '',
  26. anonok => 0,
  27. rebuild => 0,
  28. wrapper => undef,
  29. wrappermode => undef,
  30. srcdir => undef,
  31. destdir => undef,
  32. templatedir => "/usr/share/ikiwiki/templates",
  33. setup => undef,
  34. adminuser => undef,
  35. ); #}}}
  36. GetOptions( #{{{
  37. "setup=s" => \$config{setup},
  38. "wikiname=s" => \$config{wikiname},
  39. "verbose|v!" => \$config{verbose},
  40. "rebuild!" => \$config{rebuild},
  41. "wrapper=s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" },
  42. "wrappermode=i" => \$config{wrappermode},
  43. "svn!" => \$config{svn},
  44. "anonok!" => \$config{anonok},
  45. "cgi!" => \$config{cgi},
  46. "url=s" => \$config{url},
  47. "cgiurl=s" => \$config{cgiurl},
  48. "historyurl=s" => \$config{historyurl},
  49. "diffurl=s" => \$config{diffurl},
  50. "exclude=s@" => sub {
  51. $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
  52. },
  53. "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] },
  54. "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) },
  55. ) || usage();
  56. if (! $config{setup}) {
  57. usage() unless @ARGV == 2;
  58. $config{srcdir} = possibly_foolish_untaint(shift);
  59. $config{destdir} = possibly_foolish_untaint(shift);
  60. if ($config{cgi} && ! length $config{url}) {
  61. error("Must specify url to wiki with --url when using --cgi");
  62. }
  63. }
  64. #}}}
  65. sub usage { #{{{
  66. die "usage: ikiwiki [options] source dest\n";
  67. } #}}}
  68. sub error { #{{{
  69. if ($config{cgi}) {
  70. print "Content-type: text/html\n\n";
  71. print misctemplate("Error", "<p>Error: @_</p>");
  72. }
  73. die @_;
  74. } #}}}
  75. sub debug ($) { #{{{
  76. return unless $config{verbose};
  77. if (! $config{cgi}) {
  78. print "@_\n";
  79. }
  80. else {
  81. print STDERR "@_\n";
  82. }
  83. } #}}}
  84. sub mtime ($) { #{{{
  85. my $page=shift;
  86. return (stat($page))[9];
  87. } #}}}
  88. sub possibly_foolish_untaint { #{{{
  89. my $tainted=shift;
  90. my ($untainted)=$tainted=~/(.*)/;
  91. return $untainted;
  92. } #}}}
  93. sub basename ($) { #{{{
  94. my $file=shift;
  95. $file=~s!.*/!!;
  96. return $file;
  97. } #}}}
  98. sub dirname ($) { #{{{
  99. my $file=shift;
  100. $file=~s!/?[^/]+$!!;
  101. return $file;
  102. } #}}}
  103. sub pagetype ($) { #{{{
  104. my $page=shift;
  105. if ($page =~ /\.mdwn$/) {
  106. return ".mdwn";
  107. }
  108. else {
  109. return "unknown";
  110. }
  111. } #}}}
  112. sub pagename ($) { #{{{
  113. my $file=shift;
  114. my $type=pagetype($file);
  115. my $page=$file;
  116. $page=~s/\Q$type\E*$// unless $type eq 'unknown';
  117. return $page;
  118. } #}}}
  119. sub htmlpage ($) { #{{{
  120. my $page=shift;
  121. return $page.".html";
  122. } #}}}
  123. sub readfile ($) { #{{{
  124. my $file=shift;
  125. local $/=undef;
  126. open (IN, "$file") || error("failed to read $file: $!");
  127. my $ret=<IN>;
  128. close IN;
  129. return $ret;
  130. } #}}}
  131. sub writefile ($$) { #{{{
  132. my $file=shift;
  133. my $content=shift;
  134. my $dir=dirname($file);
  135. if (! -d $dir) {
  136. my $d="";
  137. foreach my $s (split(m!/+!, $dir)) {
  138. $d.="$s/";
  139. if (! -d $d) {
  140. mkdir($d) || error("failed to create directory $d: $!");
  141. }
  142. }
  143. }
  144. open (OUT, ">$file") || error("failed to write $file: $!");
  145. print OUT $content;
  146. close OUT;
  147. } #}}}
  148. sub findlinks ($$) { #{{{
  149. my $content=shift;
  150. my $page=shift;
  151. my @links;
  152. while ($content =~ /(?<!\\)$config{wiki_link_regexp}/g) {
  153. push @links, lc($1);
  154. }
  155. # Discussion links are a special case since they're not in the text
  156. # of the page, but on its template.
  157. return @links, "$page/discussion";
  158. } #}}}
  159. sub bestlink ($$) { #{{{
  160. # Given a page and the text of a link on the page, determine which
  161. # existing page that link best points to. Prefers pages under a
  162. # subdirectory with the same name as the source page, failing that
  163. # goes down the directory tree to the base looking for matching
  164. # pages.
  165. my $page=shift;
  166. my $link=lc(shift);
  167. my $cwd=$page;
  168. do {
  169. my $l=$cwd;
  170. $l.="/" if length $l;
  171. $l.=$link;
  172. if (exists $links{$l}) {
  173. #debug("for $page, \"$link\", use $l");
  174. return $l;
  175. }
  176. } while $cwd=~s!/?[^/]+$!!;
  177. #print STDERR "warning: page $page, broken link: $link\n";
  178. return "";
  179. } #}}}
  180. sub isinlinableimage ($) { #{{{
  181. my $file=shift;
  182. $file=~/\.(png|gif|jpg|jpeg)$/;
  183. } #}}}
  184. sub htmllink { #{{{
  185. my $page=shift;
  186. my $link=shift;
  187. my $noimageinline=shift; # don't turn links into inline html images
  188. my $forcesubpage=shift; # force a link to a subpage
  189. my $bestlink;
  190. if (! $forcesubpage) {
  191. $bestlink=bestlink($page, $link);
  192. }
  193. else {
  194. $bestlink="$page/".lc($link);
  195. }
  196. return $link if length $bestlink && $page eq $bestlink;
  197. # TODO BUG: %renderedfiles may not have it, if the linked to page
  198. # was also added and isn't yet rendered! Note that this bug is
  199. # masked by the bug mentioned below that makes all new files
  200. # be rendered twice.
  201. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  202. $bestlink=htmlpage($bestlink);
  203. }
  204. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  205. return "<a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$link"
  206. }
  207. $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
  208. if (! $noimageinline && isinlinableimage($bestlink)) {
  209. return "<img src=\"$bestlink\">";
  210. }
  211. return "<a href=\"$bestlink\">$link</a>";
  212. } #}}}
  213. sub linkify ($$) { #{{{
  214. my $content=shift;
  215. my $page=shift;
  216. $content =~ s{(\\?)$config{wiki_link_regexp}}{
  217. $1 ? "[[$2]]" : htmllink($page, $2)
  218. }eg;
  219. return $content;
  220. } #}}}
  221. sub htmlize ($$) { #{{{
  222. my $type=shift;
  223. my $content=shift;
  224. if (! $INC{"/usr/bin/markdown"}) {
  225. no warnings 'once';
  226. $blosxom::version="is a proper perl module too much to ask?";
  227. use warnings 'all';
  228. do "/usr/bin/markdown";
  229. }
  230. if ($type eq '.mdwn') {
  231. return Markdown::Markdown($content);
  232. }
  233. else {
  234. error("htmlization of $type not supported");
  235. }
  236. } #}}}
  237. sub backlinks ($) { #{{{
  238. my $page=shift;
  239. my @links;
  240. foreach my $p (keys %links) {
  241. next if bestlink($page, $p) eq $page;
  242. if (grep { length $_ && bestlink($p, $_) eq $page } @{$links{$p}}) {
  243. my $href=File::Spec->abs2rel(htmlpage($p), dirname($page));
  244. # Trim common dir prefixes from both pages.
  245. my $p_trimmed=$p;
  246. my $page_trimmed=$page;
  247. my $dir;
  248. 1 while (($dir)=$page_trimmed=~m!^([^/]+/)!) &&
  249. defined $dir &&
  250. $p_trimmed=~s/^\Q$dir\E// &&
  251. $page_trimmed=~s/^\Q$dir\E//;
  252. push @links, { url => $href, page => $p_trimmed };
  253. }
  254. }
  255. return sort { $a->{page} cmp $b->{page} } @links;
  256. } #}}}
  257. sub parentlinks ($) { #{{{
  258. my $page=shift;
  259. my @ret;
  260. my $pagelink="";
  261. my $path="";
  262. my $skip=1;
  263. foreach my $dir (reverse split("/", $page)) {
  264. if (! $skip) {
  265. $path.="../";
  266. unshift @ret, { url => "$path$dir.html", page => $dir };
  267. }
  268. else {
  269. $skip=0;
  270. }
  271. }
  272. unshift @ret, { url => length $path ? $path : ".", page => $config{wikiname} };
  273. return @ret;
  274. } #}}}
  275. sub indexlink () { #{{{
  276. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  277. } #}}}
  278. sub finalize ($$$) { #{{{
  279. my $content=shift;
  280. my $page=shift;
  281. my $mtime=shift;
  282. my $title=basename($page);
  283. $title=~s/_/ /g;
  284. my $template=HTML::Template->new(blind_cache => 1,
  285. filename => "$config{templatedir}/page.tmpl");
  286. if (length $config{cgiurl}) {
  287. $template->param(editurl => "$config{cgiurl}?do=edit&page=$page");
  288. $template->param(prefsurl => "$config{cgiurl}?do=prefs");
  289. if ($config{svn}) {
  290. $template->param(recentchangesurl => "$config{cgiurl}?do=recentchanges");
  291. }
  292. }
  293. if (length $config{historyurl}) {
  294. my $u=$config{historyurl};
  295. $u=~s/\[\[file\]\]/$pagesources{$page}/g;
  296. $template->param(historyurl => $u);
  297. }
  298. $template->param(
  299. title => $title,
  300. wikiname => $config{wikiname},
  301. parentlinks => [parentlinks($page)],
  302. content => $content,
  303. backlinks => [backlinks($page)],
  304. discussionlink => htmllink($page, "Discussion", 1, 1),
  305. mtime => scalar(gmtime($mtime)),
  306. );
  307. return $template->output;
  308. } #}}}
  309. sub check_overwrite ($$) { #{{{
  310. # Important security check. Make sure to call this before saving
  311. # any files to the source directory.
  312. my $dest=shift;
  313. my $src=shift;
  314. if (! exists $renderedfiles{$src} && -e $dest && ! $config{rebuild}) {
  315. error("$dest already exists and was rendered from ".
  316. join(" ",(grep { $renderedfiles{$_} eq $dest } keys
  317. %renderedfiles)).
  318. ", before, so not rendering from $src");
  319. }
  320. } #}}}
  321. sub render ($) { #{{{
  322. my $file=shift;
  323. my $type=pagetype($file);
  324. my $content=readfile("$config{srcdir}/$file");
  325. if ($type ne 'unknown') {
  326. my $page=pagename($file);
  327. $links{$page}=[findlinks($content, $page)];
  328. $content=linkify($content, $page);
  329. $content=htmlize($type, $content);
  330. $content=finalize($content, $page,
  331. mtime("$config{srcdir}/$file"));
  332. check_overwrite("$config{destdir}/".htmlpage($page), $page);
  333. writefile("$config{destdir}/".htmlpage($page), $content);
  334. $oldpagemtime{$page}=time;
  335. $renderedfiles{$page}=htmlpage($page);
  336. }
  337. else {
  338. $links{$file}=[];
  339. check_overwrite("$config{destdir}/$file", $file);
  340. writefile("$config{destdir}/$file", $content);
  341. $oldpagemtime{$file}=time;
  342. $renderedfiles{$file}=$file;
  343. }
  344. } #}}}
  345. sub lockwiki () { #{{{
  346. # Take an exclusive lock on the wiki to prevent multiple concurrent
  347. # run issues. The lock will be dropped on program exit.
  348. if (! -d "$config{srcdir}/.ikiwiki") {
  349. mkdir("$config{srcdir}/.ikiwiki");
  350. }
  351. open(WIKILOCK, ">$config{srcdir}/.ikiwiki/lockfile") || error ("cannot write to lockfile: $!");
  352. if (! flock(WIKILOCK, 2 | 4)) {
  353. debug("wiki seems to be locked, waiting for lock");
  354. my $wait=600; # arbitrary, but don't hang forever to
  355. # prevent process pileup
  356. for (1..600) {
  357. return if flock(WIKILOCK, 2 | 4);
  358. sleep 1;
  359. }
  360. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  361. }
  362. } #}}}
  363. sub unlockwiki () { #{{{
  364. close WIKILOCK;
  365. } #}}}
  366. sub loadindex () { #{{{
  367. open (IN, "$config{srcdir}/.ikiwiki/index") || return;
  368. while (<IN>) {
  369. $_=possibly_foolish_untaint($_);
  370. chomp;
  371. my ($mtime, $file, $rendered, @links)=split(' ', $_);
  372. my $page=pagename($file);
  373. $pagesources{$page}=$file;
  374. $oldpagemtime{$page}=$mtime;
  375. $oldlinks{$page}=[@links];
  376. $links{$page}=[@links];
  377. $renderedfiles{$page}=$rendered;
  378. }
  379. close IN;
  380. } #}}}
  381. sub saveindex () { #{{{
  382. if (! -d "$config{srcdir}/.ikiwiki") {
  383. mkdir("$config{srcdir}/.ikiwiki");
  384. }
  385. open (OUT, ">$config{srcdir}/.ikiwiki/index") || error("cannot write to index: $!");
  386. foreach my $page (keys %oldpagemtime) {
  387. print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
  388. join(" ", @{$links{$page}})."\n"
  389. if $oldpagemtime{$page};
  390. }
  391. close OUT;
  392. } #}}}
  393. sub rcs_update () { #{{{
  394. if (-d "$config{srcdir}/.svn") {
  395. if (system("svn", "update", "--quiet", $config{srcdir}) != 0) {
  396. warn("svn update failed\n");
  397. }
  398. }
  399. } #}}}
  400. sub rcs_prepedit ($) { #{{{
  401. # Prepares to edit a file under revision control. Returns a token
  402. # that must be passed into rcs_commit when the file is ready
  403. # for committing.
  404. # The file is relative to the srcdir.
  405. my $file=shift;
  406. if (-d "$config{srcdir}/.svn") {
  407. # For subversion, return the revision of the file when
  408. # editing begins.
  409. my $rev=svn_info("Revision", "$config{srcdir}/$file");
  410. return defined $rev ? $rev : "";
  411. }
  412. } #}}}
  413. sub rcs_commit ($$$) { #{{{
  414. # Tries to commit the page; returns undef on _success_ and
  415. # a version of the page with the rcs's conflict markers on failure.
  416. # The file is relative to the srcdir.
  417. my $file=shift;
  418. my $message=shift;
  419. my $rcstoken=shift;
  420. if (-d "$config{srcdir}/.svn") {
  421. # Check to see if the page has been changed by someone
  422. # else since rcs_prepedit was called.
  423. my ($oldrev)=$rcstoken=~/^([0-9]+)$/; # untaint
  424. my $rev=svn_info("Revision", "$config{srcdir}/$file");
  425. if (defined $rev && defined $oldrev && $rev != $oldrev) {
  426. # Merge their changes into the file that we've
  427. # changed.
  428. chdir($config{srcdir}); # svn merge wants to be here
  429. if (system("svn", "merge", "--quiet", "-r$oldrev:$rev",
  430. "$config{srcdir}/$file") != 0) {
  431. warn("svn merge -r$oldrev:$rev failed\n");
  432. }
  433. }
  434. if (system("svn", "commit", "--quiet", "-m",
  435. possibly_foolish_untaint($message),
  436. "$config{srcdir}") != 0) {
  437. my $conflict=readfile("$config{srcdir}/$file");
  438. if (system("svn", "revert", "--quiet", "$config{srcdir}/$file") != 0) {
  439. warn("svn revert failed\n");
  440. }
  441. return $conflict;
  442. }
  443. }
  444. return undef # success
  445. } #}}}
  446. sub rcs_add ($) { #{{{
  447. # filename is relative to the root of the srcdir
  448. my $file=shift;
  449. if (-d "$config{srcdir}/.svn") {
  450. my $parent=dirname($file);
  451. while (! -d "$config{srcdir}/$parent/.svn") {
  452. $file=$parent;
  453. $parent=dirname($file);
  454. }
  455. if (system("svn", "add", "--quiet", "$config{srcdir}/$file") != 0) {
  456. warn("svn add failed\n");
  457. }
  458. }
  459. } #}}}
  460. sub svn_info ($$) { #{{{
  461. my $field=shift;
  462. my $file=shift;
  463. my $info=`LANG=C svn info $file`;
  464. my ($ret)=$info=~/^$field: (.*)$/m;
  465. return $ret;
  466. } #}}}
  467. sub rcs_recentchanges ($) { #{{{
  468. my $num=shift;
  469. my @ret;
  470. eval q{use CGI 'escapeHTML'};
  471. eval q{use Date::Parse};
  472. eval q{use Time::Duration};
  473. if (-d "$config{srcdir}/.svn") {
  474. my $svn_url=svn_info("URL", $config{srcdir});
  475. # FIXME: currently assumes that the wiki is somewhere
  476. # under trunk in svn, doesn't support other layouts.
  477. my ($svn_base)=$svn_url=~m!(/trunk(?:/.*)?)$!;
  478. my $div=qr/^--------------------+$/;
  479. my $infoline=qr/^r(\d+)\s+\|\s+([^\s]+)\s+\|\s+(\d+-\d+-\d+\s+\d+:\d+:\d+\s+[-+]?\d+).*/;
  480. my $state='start';
  481. my ($rev, $user, $when, @pages, @message);
  482. foreach (`LANG=C svn log --limit $num -v '$svn_url'`) {
  483. chomp;
  484. if ($state eq 'start' && /$div/) {
  485. $state='header';
  486. }
  487. elsif ($state eq 'header' && /$infoline/) {
  488. $rev=$1;
  489. $user=$2;
  490. $when=concise(ago(time - str2time($3)));
  491. }
  492. elsif ($state eq 'header' && /^\s+[A-Z]\s+\Q$svn_base\E\/([^ ]+)(?:$|\s)/) {
  493. my $file=$1;
  494. my $diffurl=$config{diffurl};
  495. $diffurl=~s/\[\[file\]\]/$file/g;
  496. $diffurl=~s/\[\[r1\]\]/$rev - 1/eg;
  497. $diffurl=~s/\[\[r2\]\]/$rev/g;
  498. push @pages, {
  499. link => htmllink("", pagename($file), 1),
  500. diffurl => $diffurl,
  501. } if length $file;
  502. }
  503. elsif ($state eq 'header' && /^$/) {
  504. $state='body';
  505. }
  506. elsif ($state eq 'body' && /$div/) {
  507. my $committype="web";
  508. if (defined $message[0] &&
  509. $message[0]->{line}=~/^web commit by (\w+):?(.*)/) {
  510. $user="$1";
  511. $message[0]->{line}=$2;
  512. }
  513. else {
  514. $committype="svn";
  515. }
  516. push @ret, { rev => $rev,
  517. user => htmllink("", $user, 1),
  518. committype => $committype,
  519. when => $when, message => [@message],
  520. pages => [@pages],
  521. } if @pages;
  522. return @ret if @ret >= $num;
  523. $state='header';
  524. $rev=$user=$when=undef;
  525. @pages=@message=();
  526. }
  527. elsif ($state eq 'body') {
  528. push @message, {line => escapeHTML($_)},
  529. }
  530. }
  531. }
  532. return @ret;
  533. } #}}}
  534. sub prune ($) { #{{{
  535. my $file=shift;
  536. unlink($file);
  537. my $dir=dirname($file);
  538. while (rmdir($dir)) {
  539. $dir=dirname($dir);
  540. }
  541. } #}}}
  542. sub refresh () { #{{{
  543. # find existing pages
  544. my %exists;
  545. my @files;
  546. eval q{use File::Find};
  547. find({
  548. no_chdir => 1,
  549. wanted => sub {
  550. if (/$config{wiki_file_prune_regexp}/) {
  551. no warnings 'once';
  552. $File::Find::prune=1;
  553. use warnings "all";
  554. }
  555. elsif (! -d $_ && ! -l $_) {
  556. my ($f)=/$config{wiki_file_regexp}/; # untaint
  557. if (! defined $f) {
  558. warn("skipping bad filename $_\n");
  559. }
  560. else {
  561. $f=~s/^\Q$config{srcdir}\E\/?//;
  562. push @files, $f;
  563. $exists{pagename($f)}=1;
  564. }
  565. }
  566. },
  567. }, $config{srcdir});
  568. my %rendered;
  569. # check for added or removed pages
  570. my @add;
  571. foreach my $file (@files) {
  572. my $page=pagename($file);
  573. if (! $oldpagemtime{$page}) {
  574. debug("new page $page");
  575. push @add, $file;
  576. $links{$page}=[];
  577. $pagesources{$page}=$file;
  578. }
  579. }
  580. my @del;
  581. foreach my $page (keys %oldpagemtime) {
  582. if (! $exists{$page}) {
  583. debug("removing old page $page");
  584. push @del, $pagesources{$page};
  585. prune($config{destdir}."/".$renderedfiles{$page});
  586. delete $renderedfiles{$page};
  587. $oldpagemtime{$page}=0;
  588. delete $pagesources{$page};
  589. }
  590. }
  591. # render any updated files
  592. foreach my $file (@files) {
  593. my $page=pagename($file);
  594. if (! exists $oldpagemtime{$page} ||
  595. mtime("$config{srcdir}/$file") > $oldpagemtime{$page}) {
  596. debug("rendering changed file $file");
  597. render($file);
  598. $rendered{$file}=1;
  599. }
  600. }
  601. # if any files were added or removed, check to see if each page
  602. # needs an update due to linking to them
  603. # TODO: inefficient; pages may get rendered above and again here;
  604. # problem is the bestlink may have changed and we won't know until
  605. # now
  606. if (@add || @del) {
  607. FILE: foreach my $file (@files) {
  608. my $page=pagename($file);
  609. foreach my $f (@add, @del) {
  610. my $p=pagename($f);
  611. foreach my $link (@{$links{$page}}) {
  612. if (bestlink($page, $link) eq $p) {
  613. debug("rendering $file, which links to $p");
  614. render($file);
  615. $rendered{$file}=1;
  616. next FILE;
  617. }
  618. }
  619. }
  620. }
  621. }
  622. # handle backlinks; if a page has added/removed links, update the
  623. # pages it links to
  624. # TODO: inefficient; pages may get rendered above and again here;
  625. # problem is the backlinks could be wrong in the first pass render
  626. # above
  627. if (%rendered) {
  628. my %linkchanged;
  629. foreach my $file (keys %rendered, @del) {
  630. my $page=pagename($file);
  631. if (exists $links{$page}) {
  632. foreach my $link (map { bestlink($page, $_) } @{$links{$page}}) {
  633. if (length $link &&
  634. ! exists $oldlinks{$page} ||
  635. ! grep { $_ eq $link } @{$oldlinks{$page}}) {
  636. $linkchanged{$link}=1;
  637. }
  638. }
  639. }
  640. if (exists $oldlinks{$page}) {
  641. foreach my $link (map { bestlink($page, $_) } @{$oldlinks{$page}}) {
  642. if (length $link &&
  643. ! exists $links{$page} ||
  644. ! grep { $_ eq $link } @{$links{$page}}) {
  645. $linkchanged{$link}=1;
  646. }
  647. }
  648. }
  649. }
  650. foreach my $link (keys %linkchanged) {
  651. my $linkfile=$pagesources{$link};
  652. if (defined $linkfile) {
  653. debug("rendering $linkfile, to update its backlinks");
  654. render($linkfile);
  655. }
  656. }
  657. }
  658. } #}}}
  659. sub gen_wrapper (@) { #{{{
  660. my %config=(@_);
  661. eval q{use Cwd 'abs_path'};
  662. $config{srcdir}=abs_path($config{srcdir});
  663. $config{destdir}=abs_path($config{destdir});
  664. my $this=abs_path($0);
  665. if (! -x $this) {
  666. error("$this doesn't seem to be executable");
  667. }
  668. if ($config{setup}) {
  669. error("cannot create a wrapper that uses a setup file");
  670. }
  671. my @params=($config{srcdir}, $config{destdir},
  672. "--wikiname=$config{wikiname}",
  673. "--templatedir=$config{templatedir}");
  674. push @params, "--verbose" if $config{verbose};
  675. push @params, "--rebuild" if $config{rebuild};
  676. push @params, "--nosvn" if !$config{svn};
  677. push @params, "--cgi" if $config{cgi};
  678. push @params, "--url=$config{url}" if length $config{url};
  679. push @params, "--cgiurl=$config{cgiurl}" if length $config{cgiurl};
  680. push @params, "--historyurl=$config{historyurl}" if length $config{historyurl};
  681. push @params, "--diffurl=$config{diffurl}" if length $config{diffurl};
  682. push @params, "--anonok" if $config{anonok};
  683. push @params, "--adminuser=$_" foreach @{$config{adminuser}};
  684. my $params=join(" ", @params);
  685. my $call='';
  686. foreach my $p ($this, $this, @params) {
  687. $call.=qq{"$p", };
  688. }
  689. $call.="NULL";
  690. my @envsave;
  691. push @envsave, qw{REMOTE_ADDR QUERY_STRING REQUEST_METHOD REQUEST_URI
  692. CONTENT_TYPE CONTENT_LENGTH GATEWAY_INTERFACE
  693. HTTP_COOKIE} if $config{cgi};
  694. my $envsave="";
  695. foreach my $var (@envsave) {
  696. $envsave.=<<"EOF"
  697. if ((s=getenv("$var")))
  698. asprintf(&newenviron[i++], "%s=%s", "$var", s);
  699. EOF
  700. }
  701. open(OUT, ">ikiwiki-wrap.c") || error("failed to write ikiwiki-wrap.c: $!");;
  702. print OUT <<"EOF";
  703. /* A wrapper for ikiwiki, can be safely made suid. */
  704. #define _GNU_SOURCE
  705. #include <stdio.h>
  706. #include <unistd.h>
  707. #include <stdlib.h>
  708. #include <string.h>
  709. extern char **environ;
  710. int main (int argc, char **argv) {
  711. /* Sanitize environment. */
  712. char *s;
  713. char *newenviron[$#envsave+3];
  714. int i=0;
  715. $envsave
  716. newenviron[i++]="HOME=$ENV{HOME}";
  717. newenviron[i]=NULL;
  718. environ=newenviron;
  719. if (argc == 2 && strcmp(argv[1], "--params") == 0) {
  720. printf("$params\\n");
  721. exit(0);
  722. }
  723. execl($call);
  724. perror("failed to run $this");
  725. exit(1);
  726. }
  727. EOF
  728. close OUT;
  729. if (system("gcc", "ikiwiki-wrap.c", "-o", possibly_foolish_untaint($config{wrapper})) != 0) {
  730. error("failed to compile ikiwiki-wrap.c");
  731. }
  732. unlink("ikiwiki-wrap.c");
  733. if (defined $config{wrappermode} &&
  734. ! chmod(oct($config{wrappermode}), possibly_foolish_untaint($config{wrapper}))) {
  735. error("chmod $config{wrapper}: $!");
  736. }
  737. print "successfully generated $config{wrapper}\n";
  738. } #}}}
  739. sub misctemplate ($$) { #{{{
  740. my $title=shift;
  741. my $pagebody=shift;
  742. my $template=HTML::Template->new(
  743. filename => "$config{templatedir}/misc.tmpl"
  744. );
  745. $template->param(
  746. title => $title,
  747. indexlink => indexlink(),
  748. wikiname => $config{wikiname},
  749. pagebody => $pagebody,
  750. );
  751. return $template->output;
  752. }#}}}
  753. sub cgi_recentchanges ($) { #{{{
  754. my $q=shift;
  755. my $template=HTML::Template->new(
  756. filename => "$config{templatedir}/recentchanges.tmpl"
  757. );
  758. $template->param(
  759. title => "RecentChanges",
  760. indexlink => indexlink(),
  761. wikiname => $config{wikiname},
  762. changelog => [rcs_recentchanges(100)],
  763. );
  764. print $q->header, $template->output;
  765. } #}}}
  766. sub userinfo_get ($$) { #{{{
  767. my $user=shift;
  768. my $field=shift;
  769. eval q{use Storable};
  770. my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
  771. if (! defined $userdata || ! ref $userdata ||
  772. ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
  773. ! exists $userdata->{$user}->{$field}) {
  774. return "";
  775. }
  776. return $userdata->{$user}->{$field};
  777. } #}}}
  778. sub userinfo_set ($$$) { #{{{
  779. my $user=shift;
  780. my $field=shift;
  781. my $value=shift;
  782. eval q{use Storable};
  783. my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
  784. if (! defined $userdata || ! ref $userdata ||
  785. ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
  786. return "";
  787. }
  788. $userdata->{$user}->{$field}=$value;
  789. my $oldmask=umask(077);
  790. my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb");
  791. umask($oldmask);
  792. return $ret;
  793. } #}}}
  794. sub userinfo_setall ($$) { #{{{
  795. my $user=shift;
  796. my $info=shift;
  797. eval q{use Storable};
  798. my $userdata=eval{ Storable::lock_retrieve("$config{srcdir}/.ikiwiki/userdb") };
  799. if (! defined $userdata || ! ref $userdata) {
  800. $userdata={};
  801. }
  802. $userdata->{$user}=$info;
  803. my $oldmask=umask(077);
  804. my $ret=Storable::lock_store($userdata, "$config{srcdir}/.ikiwiki/userdb");
  805. umask($oldmask);
  806. return $ret;
  807. } #}}}
  808. sub cgi_signin ($$) { #{{{
  809. my $q=shift;
  810. my $session=shift;
  811. eval q{use CGI::FormBuilder};
  812. my $form = CGI::FormBuilder->new(
  813. title => "signin",
  814. fields => [qw(do page from name password confirm_password email)],
  815. header => 1,
  816. method => 'POST',
  817. validate => {
  818. confirm_password => {
  819. perl => q{eq $form->field("password")},
  820. },
  821. email => 'EMAIL',
  822. },
  823. required => 'NONE',
  824. javascript => 0,
  825. params => $q,
  826. action => $q->request_uri,
  827. header => 0,
  828. template => (-e "$config{templatedir}/signin.tmpl" ?
  829. "$config{templatedir}/signin.tmpl" : "")
  830. );
  831. $form->field(name => "name", required => 0);
  832. $form->field(name => "do", type => "hidden");
  833. $form->field(name => "page", type => "hidden");
  834. $form->field(name => "from", type => "hidden");
  835. $form->field(name => "password", type => "password", required => 0);
  836. $form->field(name => "confirm_password", type => "password", required => 0);
  837. $form->field(name => "email", required => 0);
  838. if ($q->param("do") ne "signin") {
  839. $form->text("You need to log in first.");
  840. }
  841. if ($form->submitted) {
  842. # Set required fields based on how form was submitted.
  843. my %required=(
  844. "Login" => [qw(name password)],
  845. "Register" => [qw(name password confirm_password email)],
  846. "Mail Password" => [qw(name)],
  847. );
  848. foreach my $opt (@{$required{$form->submitted}}) {
  849. $form->field(name => $opt, required => 1);
  850. }
  851. # Validate password differently depending on how
  852. # form was submitted.
  853. if ($form->submitted eq 'Login') {
  854. $form->field(
  855. name => "password",
  856. validate => sub {
  857. length $form->field("name") &&
  858. shift eq userinfo_get($form->field("name"), 'password');
  859. },
  860. );
  861. $form->field(name => "name", validate => '/^\w+$/');
  862. }
  863. else {
  864. $form->field(name => "password", validate => 'VALUE');
  865. }
  866. # And make sure the entered name exists when logging
  867. # in or sending email, and does not when registering.
  868. if ($form->submitted eq 'Register') {
  869. $form->field(
  870. name => "name",
  871. validate => sub {
  872. my $name=shift;
  873. length $name &&
  874. ! userinfo_get($name, "regdate");
  875. },
  876. );
  877. }
  878. else {
  879. $form->field(
  880. name => "name",
  881. validate => sub {
  882. my $name=shift;
  883. length $name &&
  884. userinfo_get($name, "regdate");
  885. },
  886. );
  887. }
  888. }
  889. else {
  890. # First time settings.
  891. $form->field(name => "name", comment => "use FirstnameLastName");
  892. $form->field(name => "confirm_password", comment => "(only needed");
  893. $form->field(name => "email", comment => "for registration)");
  894. if ($session->param("name")) {
  895. $form->field(name => "name", value => $session->param("name"));
  896. }
  897. }
  898. if ($form->submitted && $form->validate) {
  899. if ($form->submitted eq 'Login') {
  900. $session->param("name", $form->field("name"));
  901. if (defined $form->field("do") &&
  902. $form->field("do") ne 'signin') {
  903. print $q->redirect(
  904. "$config{cgiurl}?do=".$form->field("do").
  905. "&page=".$form->field("page").
  906. "&from=".$form->field("from"));;
  907. }
  908. else {
  909. print $q->redirect($config{url});
  910. }
  911. }
  912. elsif ($form->submitted eq 'Register') {
  913. my $user_name=$form->field('name');
  914. if (userinfo_setall($user_name, {
  915. 'email' => $form->field('email'),
  916. 'password' => $form->field('password'),
  917. 'regdate' => time
  918. })) {
  919. $form->field(name => "confirm_password", type => "hidden");
  920. $form->field(name => "email", type => "hidden");
  921. $form->text("Registration successful. Now you can Login.");
  922. print $session->header();
  923. print misctemplate($form->title, $form->render(submit => ["Login"]));
  924. }
  925. else {
  926. error("Error saving registration.");
  927. }
  928. }
  929. elsif ($form->submitted eq 'Mail Password') {
  930. my $user_name=$form->field("name");
  931. my $template=HTML::Template->new(
  932. filename => "$config{templatedir}/passwordmail.tmpl"
  933. );
  934. $template->param(
  935. user_name => $user_name,
  936. user_password => userinfo_get($user_name, "password"),
  937. wikiurl => $config{url},
  938. wikiname => $config{wikiname},
  939. REMOTE_ADDR => $ENV{REMOTE_ADDR},
  940. );
  941. eval q{use Mail::Sendmail};
  942. my ($fromhost) = $config{cgiurl} =~ m!/([^/]+)!;
  943. sendmail(
  944. To => userinfo_get($user_name, "email"),
  945. From => "$config{wikiname} admin <".(getpwuid($>))[0]."@".$fromhost.">",
  946. Subject => "$config{wikiname} information",
  947. Message => $template->output,
  948. ) or error("Failed to send mail");
  949. $form->text("Your password has been emailed to you.");
  950. $form->field(name => "name", required => 0);
  951. print $session->header();
  952. print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
  953. }
  954. }
  955. else {
  956. print $session->header();
  957. print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
  958. }
  959. } #}}}
  960. sub is_admin ($) { #{{{
  961. my $user_name=shift;
  962. return grep { $_ eq $user_name } @{$config{adminuser}};
  963. } #}}}
  964. sub glob_match ($$) { #{{{
  965. my $page=shift;
  966. my $glob=shift;
  967. # turn glob into safe regexp
  968. $glob=quotemeta($glob);
  969. $glob=~s/\\\*/.*/g;
  970. $glob=~s/\\\?/./g;
  971. $glob=~s!\\/!/!g;
  972. $page=~/^$glob$/i;
  973. } #}}}
  974. sub globlist_match ($$) { #{{{
  975. my $page=shift;
  976. my @globlist=split(" ", shift);
  977. # check any negated globs first
  978. foreach my $glob (@globlist) {
  979. return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
  980. }
  981. foreach my $glob (@globlist) {
  982. return 1 if glob_match($page, $glob);
  983. }
  984. return 0;
  985. } #}}}
  986. sub page_locked ($$;$) { #{{{
  987. my $page=shift;
  988. my $session=shift;
  989. my $nonfatal=shift;
  990. my $user=$session->param("name");
  991. return if length $user && is_admin($user);
  992. foreach my $admin (@{$config{adminuser}}) {
  993. my $locked_pages=userinfo_get($admin, "locked_pages");
  994. if (globlist_match($page, userinfo_get($admin, "locked_pages"))) {
  995. return 1 if $nonfatal;
  996. error(htmllink("", $page, 1)." is locked by ".
  997. htmllink("", $admin, 1)." and cannot be edited.");
  998. }
  999. }
  1000. return 0;
  1001. } #}}}
  1002. sub cgi_prefs ($$) { #{{{
  1003. my $q=shift;
  1004. my $session=shift;
  1005. eval q{use CGI::FormBuilder};
  1006. my $form = CGI::FormBuilder->new(
  1007. title => "preferences",
  1008. fields => [qw(do name password confirm_password email locked_pages)],
  1009. header => 0,
  1010. method => 'POST',
  1011. validate => {
  1012. confirm_password => {
  1013. perl => q{eq $form->field("password")},
  1014. },
  1015. email => 'EMAIL',
  1016. },
  1017. required => 'NONE',
  1018. javascript => 0,
  1019. params => $q,
  1020. action => $q->request_uri,
  1021. template => (-e "$config{templatedir}/prefs.tmpl" ?
  1022. "$config{templatedir}/prefs.tmpl" : "")
  1023. );
  1024. my @buttons=("Save Preferences", "Logout", "Cancel");
  1025. my $user_name=$session->param("name");
  1026. $form->field(name => "do", type => "hidden");
  1027. $form->field(name => "name", disabled => 1,
  1028. value => $user_name, force => 1);
  1029. $form->field(name => "password", type => "password");
  1030. $form->field(name => "confirm_password", type => "password");
  1031. $form->field(name => "locked_pages", size => 50,
  1032. comment => "(".htmllink("", "GlobList", 1).")");
  1033. if (! is_admin($user_name)) {
  1034. $form->field(name => "locked_pages", type => "hidden");
  1035. }
  1036. if (! $form->submitted) {
  1037. $form->field(name => "email", force => 1,
  1038. value => userinfo_get($user_name, "email"));
  1039. $form->field(name => "locked_pages", force => 1,
  1040. value => userinfo_get($user_name, "locked_pages"));
  1041. }
  1042. if ($form->submitted eq 'Logout') {
  1043. $session->delete();
  1044. print $q->redirect($config{url});
  1045. return;
  1046. }
  1047. elsif ($form->submitted eq 'Cancel') {
  1048. print $q->redirect($config{url});
  1049. return;
  1050. }
  1051. elsif ($form->submitted eq "Save Preferences" && $form->validate) {
  1052. foreach my $field (qw(password email locked_pages)) {
  1053. if (length $form->field($field)) {
  1054. userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
  1055. }
  1056. }
  1057. $form->text("Preferences saved.");
  1058. }
  1059. print $session->header();
  1060. print misctemplate($form->title, $form->render(submit => \@buttons));
  1061. } #}}}
  1062. sub cgi_editpage ($$) { #{{{
  1063. my $q=shift;
  1064. my $session=shift;
  1065. eval q{use CGI::FormBuilder};
  1066. my $form = CGI::FormBuilder->new(
  1067. fields => [qw(do rcsinfo from page content comments)],
  1068. header => 1,
  1069. method => 'POST',
  1070. validate => {
  1071. content => '/.+/',
  1072. },
  1073. required => [qw{content}],
  1074. javascript => 0,
  1075. params => $q,
  1076. action => $q->request_uri,
  1077. table => 0,
  1078. template => "$config{templatedir}/editpage.tmpl"
  1079. );
  1080. my @buttons=("Save Page", "Preview", "Cancel");
  1081. my ($page)=$form->param('page')=~/$config{wiki_file_regexp}/;
  1082. if (! defined $page || ! length $page || $page ne $q->param('page') ||
  1083. $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
  1084. error("bad page name");
  1085. }
  1086. $page=lc($page);
  1087. my $file=$page.$config{default_pageext};
  1088. my $newfile=1;
  1089. if (exists $pagesources{lc($page)}) {
  1090. $file=$pagesources{lc($page)};
  1091. $newfile=0;
  1092. }
  1093. $form->field(name => "do", type => 'hidden');
  1094. $form->field(name => "from", type => 'hidden');
  1095. $form->field(name => "rcsinfo", type => 'hidden');
  1096. $form->field(name => "page", value => "$page", force => 1);
  1097. $form->field(name => "comments", type => "text", size => 80);
  1098. $form->field(name => "content", type => "textarea", rows => 20,
  1099. cols => 80);
  1100. $form->tmpl_param("can_commit", $config{svn});
  1101. $form->tmpl_param("indexlink", indexlink());
  1102. $form->tmpl_param("helponformattinglink",
  1103. htmllink("", "HelpOnFormatting", 1));
  1104. if (! $form->submitted) {
  1105. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  1106. force => 1);
  1107. }
  1108. if ($form->submitted eq "Cancel") {
  1109. print $q->redirect("$config{url}/".htmlpage($page));
  1110. return;
  1111. }
  1112. elsif ($form->submitted eq "Preview") {
  1113. $form->tmpl_param("page_preview",
  1114. htmlize($config{default_pageext},
  1115. linkify($form->field('content'), $page)));
  1116. }
  1117. else {
  1118. $form->tmpl_param("page_preview", "");
  1119. }
  1120. $form->tmpl_param("page_conflict", "");
  1121. if (! $form->submitted || $form->submitted eq "Preview" ||
  1122. ! $form->validate) {
  1123. if ($form->field("do") eq "create") {
  1124. if (exists $pagesources{lc($page)}) {
  1125. # hmm, someone else made the page in the
  1126. # meantime?
  1127. print $q->redirect("$config{url}/".htmlpage($page));
  1128. return;
  1129. }
  1130. my @page_locs;
  1131. my $best_loc;
  1132. my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/;
  1133. if (! defined $from || ! length $from ||
  1134. $from ne $form->param('from') ||
  1135. $from=~/$config{wiki_file_prune_regexp}/ || $from=~/^\//) {
  1136. @page_locs=$best_loc=$page;
  1137. }
  1138. else {
  1139. my $dir=$from."/";
  1140. $dir=~s![^/]+/$!!;
  1141. if ($page eq 'discussion') {
  1142. $best_loc="$from/$page";
  1143. }
  1144. else {
  1145. $best_loc=$dir.$page;
  1146. }
  1147. push @page_locs, $dir.$page;
  1148. push @page_locs, "$from/$page";
  1149. while (length $dir) {
  1150. $dir=~s![^/]+/$!!;
  1151. push @page_locs, $dir.$page;
  1152. }
  1153. @page_locs = grep {
  1154. ! exists $pagesources{lc($_)} &&
  1155. ! page_locked($_, $session, 1)
  1156. } @page_locs;
  1157. }
  1158. $form->tmpl_param("page_select", 1);
  1159. $form->field(name => "page", type => 'select',
  1160. options => \@page_locs, value => $best_loc);
  1161. $form->title("creating $page");
  1162. }
  1163. elsif ($form->field("do") eq "edit") {
  1164. page_locked($page, $session);
  1165. if (! defined $form->field('content') ||
  1166. ! length $form->field('content')) {
  1167. my $content="";
  1168. if (exists $pagesources{lc($page)}) {
  1169. $content=readfile("$config{srcdir}/$pagesources{lc($page)}");
  1170. $content=~s/\n/\r\n/g;
  1171. }
  1172. $form->field(name => "content", value => $content,
  1173. force => 1);
  1174. }
  1175. $form->tmpl_param("page_select", 0);
  1176. $form->field(name => "page", type => 'hidden');
  1177. $form->title("editing $page");
  1178. }
  1179. print $form->render(submit => \@buttons);
  1180. }
  1181. else {
  1182. # save page
  1183. page_locked($page, $session);
  1184. my $content=$form->field('content');
  1185. $content=~s/\r\n/\n/g;
  1186. $content=~s/\r/\n/g;
  1187. writefile("$config{srcdir}/$file", $content);
  1188. my $message="web commit ";
  1189. if (length $session->param("name")) {
  1190. $message.="by ".$session->param("name");
  1191. }
  1192. else {
  1193. $message.="from $ENV{REMOTE_ADDR}";
  1194. }
  1195. if (defined $form->field('comments') &&
  1196. length $form->field('comments')) {
  1197. $message.=": ".$form->field('comments');
  1198. }
  1199. if ($config{svn}) {
  1200. if ($newfile) {
  1201. rcs_add($file);
  1202. }
  1203. # prevent deadlock with post-commit hook
  1204. unlockwiki();
  1205. # presumably the commit will trigger an update
  1206. # of the wiki
  1207. my $conflict=rcs_commit($file, $message,
  1208. $form->field("rcsinfo"));
  1209. if (defined $conflict) {
  1210. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  1211. force => 1);
  1212. $form->tmpl_param("page_conflict", 1);
  1213. $form->field("content", value => $conflict, force => 1);
  1214. $form->field("do", "edit)");
  1215. $form->tmpl_param("page_select", 0);
  1216. $form->field(name => "page", type => 'hidden');
  1217. $form->title("editing $page");
  1218. print $form->render(submit => \@buttons);
  1219. return;
  1220. }
  1221. }
  1222. else {
  1223. loadindex();
  1224. refresh();
  1225. saveindex();
  1226. }
  1227. # The trailing question mark tries to avoid broken
  1228. # caches and get the most recent version of the page.
  1229. print $q->redirect("$config{url}/".htmlpage($page)."?updated");
  1230. }
  1231. } #}}}
  1232. sub cgi () { #{{{
  1233. eval q{use CGI};
  1234. eval q{use CGI::Session};
  1235. my $q=CGI->new;
  1236. my $do=$q->param('do');
  1237. if (! defined $do || ! length $do) {
  1238. error("\"do\" parameter missing");
  1239. }
  1240. # This does not need a session.
  1241. if ($do eq 'recentchanges') {
  1242. cgi_recentchanges($q);
  1243. return;
  1244. }
  1245. CGI::Session->name("ikiwiki_session");
  1246. my $oldmask=umask(077);
  1247. my $session = CGI::Session->new("driver:db_file", $q,
  1248. { FileName => "$config{srcdir}/.ikiwiki/sessions.db" });
  1249. umask($oldmask);
  1250. # Everything below this point needs the user to be signed in.
  1251. if ((! $config{anonok} && ! defined $session->param("name") ||
  1252. ! defined $session->param("name") ||
  1253. ! userinfo_get($session->param("name"), "regdate")) || $do eq 'signin') {
  1254. cgi_signin($q, $session);
  1255. # Force session flush with safe umask.
  1256. my $oldmask=umask(077);
  1257. $session->flush;
  1258. umask($oldmask);
  1259. return;
  1260. }
  1261. if ($do eq 'create' || $do eq 'edit') {
  1262. cgi_editpage($q, $session);
  1263. }
  1264. elsif ($do eq 'prefs') {
  1265. cgi_prefs($q, $session);
  1266. }
  1267. else {
  1268. error("unknown do parameter");
  1269. }
  1270. } #}}}
  1271. sub setup () { # {{{
  1272. my $setup=possibly_foolish_untaint($config{setup});
  1273. delete $config{setup};
  1274. open (IN, $setup) || error("read $setup: $!\n");
  1275. local $/=undef;
  1276. my $code=<IN>;
  1277. ($code)=$code=~/(.*)/s;
  1278. close IN;
  1279. eval $code;
  1280. error($@) if $@;
  1281. exit;
  1282. } #}}}
  1283. # main {{{
  1284. setup() if $config{setup};
  1285. lockwiki();
  1286. if ($config{wrapper}) {
  1287. gen_wrapper(%config);
  1288. exit;
  1289. }
  1290. memoize('pagename');
  1291. memoize('bestlink');
  1292. loadindex() unless $config{rebuild};
  1293. if ($config{cgi}) {
  1294. cgi();
  1295. }
  1296. else {
  1297. rcs_update() if $config{svn};
  1298. refresh();
  1299. saveindex();
  1300. }
  1301. #}}}