summaryrefslogtreecommitdiff
path: root/IkiWiki.pm
blob: 990836f8e461226442933064d2d62defb457e27d (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use Encode;
  6. use HTML::Entities;
  7. use open qw{:utf8 :std};
  8. # Optimisation.
  9. use Memoize;
  10. memoize("abs2rel");
  11. memoize("pagespec_translate");
  12. use vars qw{%config %links %oldlinks %oldpagemtime %pagectime %pagecase
  13. %renderedfiles %pagesources %depends %hooks %forcerebuild};
  14. my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
  15. sub defaultconfig () { #{{{
  16. wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.x?html?$|\.rss$)},
  17. wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]]+)\]\]/,
  18. wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
  19. verbose => 0,
  20. syslog => 0,
  21. wikiname => "wiki",
  22. default_pageext => "mdwn",
  23. cgi => 0,
  24. rcs => 'svn',
  25. notify => 0,
  26. url => '',
  27. cgiurl => '',
  28. historyurl => '',
  29. diffurl => '',
  30. anonok => 0,
  31. rss => 0,
  32. discussion => 1,
  33. rebuild => 0,
  34. refresh => 0,
  35. getctime => 0,
  36. w3mmode => 0,
  37. wrapper => undef,
  38. wrappermode => undef,
  39. svnrepo => undef,
  40. svnpath => "trunk",
  41. srcdir => undef,
  42. destdir => undef,
  43. pingurl => [],
  44. templatedir => "$installdir/share/ikiwiki/templates",
  45. underlaydir => "$installdir/share/ikiwiki/basewiki",
  46. setup => undef,
  47. adminuser => undef,
  48. adminemail => undef,
  49. plugin => [qw{mdwn inline htmlscrubber}],
  50. timeformat => '%c',
  51. locale => undef,
  52. sslcookie => 0,
  53. } #}}}
  54. sub checkconfig () { #{{{
  55. # locale stuff; avoid LC_ALL since it overrides everything
  56. if (defined $ENV{LC_ALL}) {
  57. $ENV{LANG} = $ENV{LC_ALL};
  58. delete $ENV{LC_ALL};
  59. }
  60. if (defined $config{locale}) {
  61. eval q{use POSIX};
  62. $ENV{LANG} = $config{locale}
  63. if POSIX::setlocale(&POSIX::LC_TIME, $config{locale});
  64. }
  65. if ($config{w3mmode}) {
  66. eval q{use Cwd q{abs_path}};
  67. $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
  68. $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
  69. $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
  70. unless $config{cgiurl} =~ m!file:///!;
  71. $config{url}="file://".$config{destdir};
  72. }
  73. if ($config{cgi} && ! length $config{url}) {
  74. error("Must specify url to wiki with --url when using --cgi\n");
  75. }
  76. if ($config{rss} && ! length $config{url}) {
  77. error("Must specify url to wiki with --url when using --rss\n");
  78. }
  79. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  80. unless exists $config{wikistatedir};
  81. if ($config{rcs}) {
  82. eval qq{require IkiWiki::Rcs::$config{rcs}};
  83. if ($@) {
  84. error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@");
  85. }
  86. }
  87. else {
  88. require IkiWiki::Rcs::Stub;
  89. }
  90. run_hooks(checkconfig => sub { shift->() });
  91. } #}}}
  92. sub loadplugins () { #{{{
  93. foreach my $plugin (@{$config{plugin}}) {
  94. my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
  95. eval qq{use $mod};
  96. if ($@) {
  97. error("Failed to load plugin $mod: $@");
  98. }
  99. }
  100. run_hooks(getopt => sub { shift->() });
  101. if (grep /^-/, @ARGV) {
  102. print STDERR "Unknown option: $_\n"
  103. foreach grep /^-/, @ARGV;
  104. usage();
  105. }
  106. } #}}}
  107. sub error ($) { #{{{
  108. if ($config{cgi}) {
  109. print "Content-type: text/html\n\n";
  110. print misctemplate("Error", "<p>Error: @_</p>");
  111. }
  112. log_message(error => @_);
  113. exit(1);
  114. } #}}}
  115. sub debug ($) { #{{{
  116. return unless $config{verbose};
  117. log_message(debug => @_);
  118. } #}}}
  119. my $log_open=0;
  120. sub log_message ($$) { #{{{
  121. my $type=shift;
  122. if ($config{syslog}) {
  123. require Sys::Syslog;
  124. unless ($log_open) {
  125. Sys::Syslog::setlogsock('unix');
  126. Sys::Syslog::openlog('ikiwiki', '', 'user');
  127. $log_open=1;
  128. }
  129. eval {
  130. Sys::Syslog::syslog($type, join(" ", @_));
  131. }
  132. }
  133. elsif (! $config{cgi}) {
  134. print "@_\n";
  135. }
  136. else {
  137. print STDERR "@_\n";
  138. }
  139. } #}}}
  140. sub possibly_foolish_untaint ($) { #{{{
  141. my $tainted=shift;
  142. my ($untainted)=$tainted=~/(.*)/;
  143. return $untainted;
  144. } #}}}
  145. sub basename ($) { #{{{
  146. my $file=shift;
  147. $file=~s!.*/+!!;
  148. return $file;
  149. } #}}}
  150. sub dirname ($) { #{{{
  151. my $file=shift;
  152. $file=~s!/*[^/]+$!!;
  153. return $file;
  154. } #}}}
  155. sub pagetype ($) { #{{{
  156. my $page=shift;
  157. if ($page =~ /\.([^.]+)$/) {
  158. return $1 if exists $hooks{htmlize}{$1};
  159. }
  160. return undef;
  161. } #}}}
  162. sub pagename ($) { #{{{
  163. my $file=shift;
  164. my $type=pagetype($file);
  165. my $page=$file;
  166. $page=~s/\Q.$type\E*$// if defined $type;
  167. return $page;
  168. } #}}}
  169. sub htmlpage ($) { #{{{
  170. my $page=shift;
  171. return $page.".html";
  172. } #}}}
  173. sub srcfile ($) { #{{{
  174. my $file=shift;
  175. return "$config{srcdir}/$file" if -e "$config{srcdir}/$file";
  176. return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file";
  177. error("internal error: $file cannot be found");
  178. } #}}}
  179. sub readfile ($;$) { #{{{
  180. my $file=shift;
  181. my $binary=shift;
  182. if (-l $file) {
  183. error("cannot read a symlink ($file)");
  184. }
  185. local $/=undef;
  186. open (IN, $file) || error("failed to read $file: $!");
  187. binmode(IN) if ($binary);
  188. my $ret=<IN>;
  189. close IN;
  190. return $ret;
  191. } #}}}
  192. sub writefile ($$$;$) { #{{{
  193. my $file=shift; # can include subdirs
  194. my $destdir=shift; # directory to put file in
  195. my $content=shift;
  196. my $binary=shift;
  197. my $test=$file;
  198. while (length $test) {
  199. if (-l "$destdir/$test") {
  200. error("cannot write to a symlink ($test)");
  201. }
  202. $test=dirname($test);
  203. }
  204. my $dir=dirname("$destdir/$file");
  205. if (! -d $dir) {
  206. my $d="";
  207. foreach my $s (split(m!/+!, $dir)) {
  208. $d.="$s/";
  209. if (! -d $d) {
  210. mkdir($d) || error("failed to create directory $d: $!");
  211. }
  212. }
  213. }
  214. open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!");
  215. binmode(OUT) if ($binary);
  216. print OUT $content;
  217. close OUT;
  218. } #}}}
  219. sub bestlink ($$) { #{{{
  220. # Given a page and the text of a link on the page, determine which
  221. # existing page that link best points to. Prefers pages under a
  222. # subdirectory with the same name as the source page, failing that
  223. # goes down the directory tree to the base looking for matching
  224. # pages.
  225. my $page=shift;
  226. my $link=shift;
  227. my $cwd=$page;
  228. do {
  229. my $l=$cwd;
  230. $l.="/" if length $l;
  231. $l.=$link;
  232. if (exists $links{$l}) {
  233. return $l;
  234. }
  235. elsif (exists $pagecase{lc $l}) {
  236. return $pagecase{lc $l};
  237. }
  238. } while $cwd=~s!/?[^/]+$!!;
  239. #print STDERR "warning: page $page, broken link: $link\n";
  240. return "";
  241. } #}}}
  242. sub isinlinableimage ($) { #{{{
  243. my $file=shift;
  244. $file=~/\.(png|gif|jpg|jpeg)$/i;
  245. } #}}}
  246. sub pagetitle ($) { #{{{
  247. my $page=shift;
  248. $page=~s/__(\d+)__/&#$1;/g;
  249. $page=~y/_/ /;
  250. return $page;
  251. } #}}}
  252. sub titlepage ($) { #{{{
  253. my $title=shift;
  254. $title=~y/ /_/;
  255. $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg;
  256. return $title;
  257. } #}}}
  258. sub cgiurl (@) { #{{{
  259. my %params=@_;
  260. return $config{cgiurl}."?".join("&amp;", map "$_=$params{$_}", keys %params);
  261. } #}}}
  262. sub baseurl (;$) { #{{{
  263. my $page=shift;
  264. return "$config{url}/" if ! defined $page;
  265. $page=~s/[^\/]+$//;
  266. $page=~s/[^\/]+\//..\//g;
  267. return $page;
  268. } #}}}
  269. sub abs2rel ($$) { #{{{
  270. # Work around very innefficient behavior in File::Spec if abs2rel
  271. # is passed two relative paths. It's much faster if paths are
  272. # absolute!
  273. my $path="/".shift;
  274. my $base="/".shift;
  275. require File::Spec;
  276. my $ret=File::Spec->abs2rel($path, $base);
  277. $ret=~s/^// if defined $ret;
  278. return $ret;
  279. } #}}}
  280. sub htmllink ($$$;$$$) { #{{{
  281. my $lpage=shift; # the page doing the linking
  282. my $page=shift; # the page that will contain the link (different for inline)
  283. my $link=shift;
  284. my $noimageinline=shift; # don't turn links into inline html images
  285. my $forcesubpage=shift; # force a link to a subpage
  286. my $linktext=shift; # set to force the link text to something
  287. my $bestlink;
  288. if (! $forcesubpage) {
  289. $bestlink=bestlink($lpage, $link);
  290. }
  291. else {
  292. $bestlink="$lpage/".lc($link);
  293. }
  294. $linktext=pagetitle(basename($link)) unless defined $linktext;
  295. return "<span class=\"selflink\">$linktext</span>"
  296. if length $bestlink && $page eq $bestlink;
  297. # TODO BUG: %renderedfiles may not have it, if the linked to page
  298. # was also added and isn't yet rendered! Note that this bug is
  299. # masked by the bug that makes all new files be rendered twice.
  300. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  301. $bestlink=htmlpage($bestlink);
  302. }
  303. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  304. return "<span><a href=\"".
  305. cgiurl(do => "create", page => lc($link), from => $page).
  306. "\">?</a>$linktext</span>"
  307. }
  308. $bestlink=abs2rel($bestlink, dirname($page));
  309. if (! $noimageinline && isinlinableimage($bestlink)) {
  310. return "<img src=\"$bestlink\" alt=\"$linktext\" />";
  311. }
  312. return "<a href=\"$bestlink\">$linktext</a>";
  313. } #}}}
  314. sub indexlink () { #{{{
  315. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  316. } #}}}
  317. sub lockwiki () { #{{{
  318. # Take an exclusive lock on the wiki to prevent multiple concurrent
  319. # run issues. The lock will be dropped on program exit.
  320. if (! -d $config{wikistatedir}) {
  321. mkdir($config{wikistatedir});
  322. }
  323. open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
  324. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  325. if (! flock(WIKILOCK, 2 | 4)) {
  326. debug("wiki seems to be locked, waiting for lock");
  327. my $wait=600; # arbitrary, but don't hang forever to
  328. # prevent process pileup
  329. for (1..600) {
  330. return if flock(WIKILOCK, 2 | 4);
  331. sleep 1;
  332. }
  333. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  334. }
  335. } #}}}
  336. sub unlockwiki () { #{{{
  337. close WIKILOCK;
  338. } #}}}
  339. sub loadindex () { #{{{
  340. open (IN, "$config{wikistatedir}/index") || return;
  341. while (<IN>) {
  342. $_=possibly_foolish_untaint($_);
  343. chomp;
  344. my %items;
  345. $items{link}=[];
  346. foreach my $i (split(/ /, $_)) {
  347. my ($item, $val)=split(/=/, $i, 2);
  348. push @{$items{$item}}, decode_entities($val);
  349. }
  350. next unless exists $items{src}; # skip bad lines for now
  351. my $page=pagename($items{src}[0]);
  352. if (! $config{rebuild}) {
  353. $pagesources{$page}=$items{src}[0];
  354. $oldpagemtime{$page}=$items{mtime}[0];
  355. $oldlinks{$page}=[@{$items{link}}];
  356. $links{$page}=[@{$items{link}}];
  357. $depends{$page}=$items{depends}[0] if exists $items{depends};
  358. $renderedfiles{$page}=$items{dest}[0];
  359. $pagecase{lc $page}=$page;
  360. }
  361. $pagectime{$page}=$items{ctime}[0];
  362. }
  363. close IN;
  364. } #}}}
  365. sub saveindex () { #{{{
  366. run_hooks(savestate => sub { shift->() });
  367. if (! -d $config{wikistatedir}) {
  368. mkdir($config{wikistatedir});
  369. }
  370. open (OUT, ">$config{wikistatedir}/index") ||
  371. error("cannot write to $config{wikistatedir}/index: $!");
  372. foreach my $page (keys %oldpagemtime) {
  373. next unless $oldpagemtime{$page};
  374. my $line="mtime=$oldpagemtime{$page} ".
  375. "ctime=$pagectime{$page} ".
  376. "src=$pagesources{$page} ".
  377. "dest=$renderedfiles{$page}";
  378. $line.=" link=$_" foreach @{$links{$page}};
  379. if (exists $depends{$page}) {
  380. $line.=" depends=".encode_entities($depends{$page}, " \t\n");
  381. }
  382. print OUT $line."\n";
  383. }
  384. close OUT;
  385. } #}}}
  386. sub template_params (@) { #{{{
  387. my $filename=shift;
  388. require HTML::Template;
  389. return filter => sub {
  390. my $text_ref = shift;
  391. $$text_ref=&Encode::decode_utf8($$text_ref);
  392. },
  393. filename => "$config{templatedir}/$filename",
  394. loop_context_vars => 1,
  395. die_on_bad_params => 0,
  396. @_;
  397. } #}}}
  398. sub template ($;@) { #{{{
  399. HTML::Template->new(template_params(@_));
  400. } #}}}
  401. sub misctemplate ($$) { #{{{
  402. my $title=shift;
  403. my $pagebody=shift;
  404. my $template=template("misc.tmpl");
  405. $template->param(
  406. title => $title,
  407. indexlink => indexlink(),
  408. wikiname => $config{wikiname},
  409. pagebody => $pagebody,
  410. baseurl => baseurl(),
  411. );
  412. return $template->output;
  413. }#}}}
  414. sub hook (@) { # {{{
  415. my %param=@_;
  416. if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
  417. error "hook requires type, call, and id parameters";
  418. }
  419. $hooks{$param{type}}{$param{id}}=\%param;
  420. } # }}}
  421. sub run_hooks ($$) { # {{{
  422. # Calls the given sub for each hook of the given type,
  423. # passing it the hook function to call.
  424. my $type=shift;
  425. my $sub=shift;
  426. if (exists $hooks{$type}) {
  427. foreach my $id (keys %{$hooks{$type}}) {
  428. $sub->($hooks{$type}{$id}{call});
  429. }
  430. }
  431. } #}}}
  432. sub globlist_to_pagespec ($) { #{{{
  433. my @globlist=split(' ', shift);
  434. my (@spec, @skip);
  435. foreach my $glob (@globlist) {
  436. if ($glob=~/^!(.*)/) {
  437. push @skip, $glob;
  438. }
  439. else {
  440. push @spec, $glob;
  441. }
  442. }
  443. my $spec=join(" or ", @spec);
  444. if (@skip) {
  445. my $skip=join(" and ", @skip);
  446. if (length $spec) {
  447. $spec="$skip and ($spec)";
  448. }
  449. else {
  450. $spec=$skip;
  451. }
  452. }
  453. return $spec;
  454. } #}}}
  455. sub is_globlist ($) { #{{{
  456. my $s=shift;
  457. $s=~/[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or";
  458. } #}}}
  459. sub safequote ($) { #{{{
  460. my $s=shift;
  461. $s=~s/[{}]//g;
  462. return "q{$s}";
  463. } #}}}
  464. sub pagespec_merge ($$) { #{{{
  465. my $a=shift;
  466. my $b=shift;
  467. return $a if $a eq $b;
  468. # Support for old-style GlobLists.
  469. if (is_globlist($a)) {
  470. $a=globlist_to_pagespec($a);
  471. }
  472. if (is_globlist($b)) {
  473. $b=globlist_to_pagespec($b);
  474. }
  475. return "($a) or ($b)";
  476. } #}}}
  477. sub pagespec_translate ($) { #{{{
  478. # This assumes that $page is in scope in the function
  479. # that evalulates the translated pagespec code.
  480. my $spec=shift;
  481. # Support for old-style GlobLists.
  482. if (is_globlist($spec)) {
  483. $spec=globlist_to_pagespec($spec);
  484. }
  485. # Convert spec to perl code.
  486. my $code="";
  487. while ($spec=~m/\s*(\!|\(|\)|\w+\([^\)]+\)|[^\s()]+)\s*/ig) {
  488. my $word=$1;
  489. if (lc $word eq "and") {
  490. $code.=" &&";
  491. }
  492. elsif (lc $word eq "or") {
  493. $code.=" ||";
  494. }
  495. elsif ($word eq "(" || $word eq ")" || $word eq "!") {
  496. $code.=" ".$word;
  497. }
  498. elsif ($word =~ /^(link|backlink|created_before|created_after|creation_month|creation_year|creation_day)\((.+)\)$/) {
  499. $code.=" match_$1(\$page, ".safequote($2).")";
  500. }
  501. else {
  502. $code.=" match_glob(\$page, ".safequote($word).")";
  503. }
  504. }
  505. return $code;
  506. } #}}}
  507. sub pagespec_match ($$) { #{{{
  508. my $page=shift;
  509. my $spec=shift;
  510. return eval pagespec_translate($spec);
  511. } #}}}
  512. sub match_glob ($$) { #{{{
  513. my $page=shift;
  514. my $glob=shift;
  515. # turn glob into safe regexp
  516. $glob=quotemeta($glob);
  517. $glob=~s/\\\*/.*/g;
  518. $glob=~s/\\\?/./g;
  519. return $page=~/^$glob$/i;
  520. } #}}}
  521. sub match_link ($$) { #{{{
  522. my $page=shift;
  523. my $link=lc(shift);
  524. my $links = $links{$page} or return undef;
  525. foreach my $p (@$links) {
  526. return 1 if lc $p eq $link;
  527. }
  528. return 0;
  529. } #}}}
  530. sub match_backlink ($$) { #{{{
  531. match_link(pop, pop);
  532. } #}}}
  533. sub match_created_before ($$) { #{{{
  534. my $page=shift;
  535. my $testpage=shift;
  536. if (exists $pagectime{$testpage}) {
  537. return $pagectime{$page} < $pagectime{$testpage};
  538. }
  539. else {
  540. return 0;
  541. }
  542. } #}}}
  543. sub match_created_after ($$) { #{{{
  544. my $page=shift;
  545. my $testpage=shift;
  546. if (exists $pagectime{$testpage}) {
  547. return $pagectime{$page} > $pagectime{$testpage};
  548. }
  549. else {
  550. return 0;
  551. }
  552. } #}}}
  553. sub match_creation_day ($$) { #{{{
  554. return ((gmtime($pagectime{shift()}))[3] == shift);
  555. } #}}}
  556. sub match_creation_month ($$) { #{{{
  557. return ((gmtime($pagectime{shift()}))[4] + 1 == shift);
  558. } #}}}
  559. sub match_creation_year ($$) { #{{{
  560. return ((gmtime($pagectime{shift()}))[5] + 1900 == shift);
  561. } #}}}
  562. 1