summaryrefslogtreecommitdiff
path: root/IkiWiki.pm
blob: 331300db067e41fde2f5a1547587bad8dea92a12 (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use Encode;
  6. use HTML::Entities;
  7. use URI::Escape q{uri_escape_utf8};
  8. use POSIX;
  9. use open qw{:utf8 :std};
  10. use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
  11. %renderedfiles %oldrenderedfiles %pagesources %destsources
  12. %depends %hooks %forcerebuild $gettext_obj};
  13. use Exporter q{import};
  14. our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
  15. bestlink htmllink readfile writefile pagetype srcfile pagename
  16. displaytime will_render gettext urlto targetpage
  17. %config %links %renderedfiles %pagesources);
  18. our $VERSION = 1.02; # plugin interface version, next is ikiwiki version
  19. our $version="1.45";my $installdir="/usr";
  20. # Optimisation.
  21. use Memoize;
  22. memoize("abs2rel");
  23. memoize("pagespec_translate");
  24. memoize("file_pruned");
  25. sub defaultconfig () { #{{{
  26. wiki_file_prune_regexps => [qr/\.\./, qr/^\./, qr/\/\./,
  27. qr/\.x?html?$/, qr/\.ikiwiki-new$/,
  28. qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//],
  29. wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]#]+)(?:#([^\s\]]+))?\]\]/,
  30. wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
  31. web_commit_regexp => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/,
  32. verbose => 0,
  33. syslog => 0,
  34. wikiname => "wiki",
  35. default_pageext => "mdwn",
  36. cgi => 0,
  37. post_commit => 0,
  38. rcs => '',
  39. notify => 0,
  40. url => '',
  41. cgiurl => '',
  42. historyurl => '',
  43. diffurl => '',
  44. rss => 0,
  45. atom => 0,
  46. discussion => 1,
  47. rebuild => 0,
  48. refresh => 0,
  49. getctime => 0,
  50. w3mmode => 0,
  51. wrapper => undef,
  52. wrappermode => undef,
  53. svnrepo => undef,
  54. svnpath => "trunk",
  55. gitorigin_branch => "origin",
  56. gitmaster_branch => "master",
  57. srcdir => undef,
  58. destdir => undef,
  59. pingurl => [],
  60. templatedir => "$installdir/share/ikiwiki/templates",
  61. underlaydir => "$installdir/share/ikiwiki/basewiki",
  62. setup => undef,
  63. adminuser => undef,
  64. adminemail => undef,
  65. plugin => [qw{mdwn inline htmlscrubber passwordauth signinedit
  66. lockedit conditional}],
  67. timeformat => '%c',
  68. locale => undef,
  69. sslcookie => 0,
  70. httpauth => 0,
  71. userdir => "",
  72. usedirs => 0,
  73. numbacklinks => 10,
  74. } #}}}
  75. sub checkconfig () { #{{{
  76. # locale stuff; avoid LC_ALL since it overrides everything
  77. if (defined $ENV{LC_ALL}) {
  78. $ENV{LANG} = $ENV{LC_ALL};
  79. delete $ENV{LC_ALL};
  80. }
  81. if (defined $config{locale}) {
  82. if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
  83. $ENV{LANG}=$config{locale};
  84. $gettext_obj=undef;
  85. }
  86. }
  87. if ($config{w3mmode}) {
  88. eval q{use Cwd q{abs_path}};
  89. error($@) if $@;
  90. $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
  91. $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
  92. $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
  93. unless $config{cgiurl} =~ m!file:///!;
  94. $config{url}="file://".$config{destdir};
  95. }
  96. if ($config{cgi} && ! length $config{url}) {
  97. error(gettext("Must specify url to wiki with --url when using --cgi"));
  98. }
  99. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  100. unless exists $config{wikistatedir};
  101. if ($config{rcs}) {
  102. eval qq{require IkiWiki::Rcs::$config{rcs}};
  103. if ($@) {
  104. error("Failed to load RCS module IkiWiki::Rcs::$config{rcs}: $@");
  105. }
  106. }
  107. else {
  108. require IkiWiki::Rcs::Stub;
  109. }
  110. run_hooks(checkconfig => sub { shift->() });
  111. } #}}}
  112. sub loadplugins () { #{{{
  113. loadplugin($_) foreach @{$config{plugin}};
  114. run_hooks(getopt => sub { shift->() });
  115. if (grep /^-/, @ARGV) {
  116. print STDERR "Unknown option: $_\n"
  117. foreach grep /^-/, @ARGV;
  118. usage();
  119. }
  120. } #}}}
  121. sub loadplugin ($) { #{{{
  122. my $plugin=shift;
  123. return if grep { $_ eq $plugin} @{$config{disable_plugins}};
  124. my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
  125. eval qq{use $mod};
  126. if ($@) {
  127. error("Failed to load plugin $mod: $@");
  128. }
  129. } #}}}
  130. sub error ($;$) { #{{{
  131. my $message=shift;
  132. my $cleaner=shift;
  133. if ($config{cgi}) {
  134. print "Content-type: text/html\n\n";
  135. print misctemplate(gettext("Error"),
  136. "<p>".gettext("Error").": $message</p>");
  137. }
  138. log_message('err' => $message) if $config{syslog};
  139. if (defined $cleaner) {
  140. $cleaner->();
  141. }
  142. die $message."\n";
  143. } #}}}
  144. sub debug ($) { #{{{
  145. return unless $config{verbose};
  146. log_message(debug => @_);
  147. } #}}}
  148. my $log_open=0;
  149. sub log_message ($$) { #{{{
  150. my $type=shift;
  151. if ($config{syslog}) {
  152. require Sys::Syslog;
  153. unless ($log_open) {
  154. Sys::Syslog::setlogsock('unix');
  155. Sys::Syslog::openlog('ikiwiki', '', 'user');
  156. $log_open=1;
  157. }
  158. eval {
  159. Sys::Syslog::syslog($type, "%s", join(" ", @_));
  160. };
  161. }
  162. elsif (! $config{cgi}) {
  163. print "@_\n";
  164. }
  165. else {
  166. print STDERR "@_\n";
  167. }
  168. } #}}}
  169. sub possibly_foolish_untaint ($) { #{{{
  170. my $tainted=shift;
  171. my ($untainted)=$tainted=~/(.*)/;
  172. return $untainted;
  173. } #}}}
  174. sub basename ($) { #{{{
  175. my $file=shift;
  176. $file=~s!.*/+!!;
  177. return $file;
  178. } #}}}
  179. sub dirname ($) { #{{{
  180. my $file=shift;
  181. $file=~s!/*[^/]+$!!;
  182. return $file;
  183. } #}}}
  184. sub pagetype ($) { #{{{
  185. my $page=shift;
  186. if ($page =~ /\.([^.]+)$/) {
  187. return $1 if exists $hooks{htmlize}{$1};
  188. }
  189. return undef;
  190. } #}}}
  191. sub pagename ($) { #{{{
  192. my $file=shift;
  193. my $type=pagetype($file);
  194. my $page=$file;
  195. $page=~s/\Q.$type\E*$// if defined $type;
  196. return $page;
  197. } #}}}
  198. sub targetpage ($$) { #{{{
  199. my $page=shift;
  200. my $ext=shift;
  201. if (! $config{usedirs} || $page =~ /^index$/ ) {
  202. return $page.".".$ext;
  203. } else {
  204. return $page."/index.".$ext;
  205. }
  206. } #}}}
  207. sub htmlpage ($) { #{{{
  208. my $page=shift;
  209. return targetpage($page, "html");
  210. } #}}}
  211. sub srcfile ($) { #{{{
  212. my $file=shift;
  213. return "$config{srcdir}/$file" if -e "$config{srcdir}/$file";
  214. return "$config{underlaydir}/$file" if -e "$config{underlaydir}/$file";
  215. error("internal error: $file cannot be found");
  216. } #}}}
  217. sub readfile ($;$$) { #{{{
  218. my $file=shift;
  219. my $binary=shift;
  220. my $wantfd=shift;
  221. if (-l $file) {
  222. error("cannot read a symlink ($file)");
  223. }
  224. local $/=undef;
  225. open (IN, $file) || error("failed to read $file: $!");
  226. binmode(IN) if ($binary);
  227. return \*IN if $wantfd;
  228. my $ret=<IN>;
  229. close IN || error("failed to read $file: $!");
  230. return $ret;
  231. } #}}}
  232. sub writefile ($$$;$$) { #{{{
  233. my $file=shift; # can include subdirs
  234. my $destdir=shift; # directory to put file in
  235. my $content=shift;
  236. my $binary=shift;
  237. my $writer=shift;
  238. my $test=$file;
  239. while (length $test) {
  240. if (-l "$destdir/$test") {
  241. error("cannot write to a symlink ($test)");
  242. }
  243. $test=dirname($test);
  244. }
  245. my $newfile="$destdir/$file.ikiwiki-new";
  246. if (-l $newfile) {
  247. error("cannot write to a symlink ($newfile)");
  248. }
  249. my $dir=dirname($newfile);
  250. if (! -d $dir) {
  251. my $d="";
  252. foreach my $s (split(m!/+!, $dir)) {
  253. $d.="$s/";
  254. if (! -d $d) {
  255. mkdir($d) || error("failed to create directory $d: $!");
  256. }
  257. }
  258. }
  259. my $cleanup = sub { unlink($newfile) };
  260. open (OUT, ">$newfile") || error("failed to write $newfile: $!", $cleanup);
  261. binmode(OUT) if ($binary);
  262. if ($writer) {
  263. $writer->(\*OUT, $cleanup);
  264. }
  265. else {
  266. print OUT $content or error("failed writing to $newfile: $!", $cleanup);
  267. }
  268. close OUT || error("failed saving $newfile: $!", $cleanup);
  269. rename($newfile, "$destdir/$file") ||
  270. error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
  271. } #}}}
  272. my %cleared;
  273. sub will_render ($$;$) { #{{{
  274. my $page=shift;
  275. my $dest=shift;
  276. my $clear=shift;
  277. # Important security check.
  278. if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
  279. ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}})) {
  280. error("$config{destdir}/$dest independently created, not overwriting with version from $page");
  281. }
  282. if (! $clear || $cleared{$page}) {
  283. $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
  284. }
  285. else {
  286. foreach my $old (@{$renderedfiles{$page}}) {
  287. delete $destsources{$old};
  288. }
  289. $renderedfiles{$page}=[$dest];
  290. $cleared{$page}=1;
  291. }
  292. $destsources{$dest}=$page;
  293. } #}}}
  294. sub bestlink ($$) { #{{{
  295. my $page=shift;
  296. my $link=shift;
  297. my $cwd=$page;
  298. if ($link=~s/^\/+//) {
  299. # absolute links
  300. $cwd="";
  301. }
  302. do {
  303. my $l=$cwd;
  304. $l.="/" if length $l;
  305. $l.=$link;
  306. if (exists $links{$l}) {
  307. return $l;
  308. }
  309. elsif (exists $pagecase{lc $l}) {
  310. return $pagecase{lc $l};
  311. }
  312. } while $cwd=~s!/?[^/]+$!!;
  313. if (length $config{userdir} && exists $links{"$config{userdir}/".lc($link)}) {
  314. return "$config{userdir}/".lc($link);
  315. }
  316. #print STDERR "warning: page $page, broken link: $link\n";
  317. return "";
  318. } #}}}
  319. sub isinlinableimage ($) { #{{{
  320. my $file=shift;
  321. $file=~/\.(png|gif|jpg|jpeg)$/i;
  322. } #}}}
  323. sub pagetitle ($;$) { #{{{
  324. my $page=shift;
  325. my $unescaped=shift;
  326. if ($unescaped) {
  327. $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
  328. }
  329. else {
  330. $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
  331. }
  332. return $page;
  333. } #}}}
  334. sub titlepage ($) { #{{{
  335. my $title=shift;
  336. $title=~s/([^-[:alnum:]:+\/.])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
  337. return $title;
  338. } #}}}
  339. sub linkpage ($) { #{{{
  340. my $link=shift;
  341. $link=~s/([^-[:alnum:]:+\/._])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
  342. return $link;
  343. } #}}}
  344. sub cgiurl (@) { #{{{
  345. my %params=@_;
  346. return $config{cgiurl}."?".
  347. join("&amp;", map $_."=".uri_escape_utf8($params{$_}), keys %params);
  348. } #}}}
  349. sub baseurl (;$) { #{{{
  350. my $page=shift;
  351. return "$config{url}/" if ! defined $page;
  352. $page=htmlpage($page);
  353. $page=~s/[^\/]+$//;
  354. $page=~s/[^\/]+\//..\//g;
  355. return $page;
  356. } #}}}
  357. sub abs2rel ($$) { #{{{
  358. # Work around very innefficient behavior in File::Spec if abs2rel
  359. # is passed two relative paths. It's much faster if paths are
  360. # absolute! (Debian bug #376658; fixed in debian unstable now)
  361. my $path="/".shift;
  362. my $base="/".shift;
  363. require File::Spec;
  364. my $ret=File::Spec->abs2rel($path, $base);
  365. $ret=~s/^// if defined $ret;
  366. return $ret;
  367. } #}}}
  368. sub displaytime ($) { #{{{
  369. my $time=shift;
  370. # strftime doesn't know about encodings, so make sure
  371. # its output is properly treated as utf8
  372. return decode_utf8(POSIX::strftime(
  373. $config{timeformat}, localtime($time)));
  374. } #}}}
  375. sub beautify_url ($) { #{{{
  376. my $url=shift;
  377. $url =~ s!/index.html$!/!;
  378. $url =~ s!^$!./!; # Browsers don't like empty links...
  379. return $url;
  380. } #}}}
  381. sub urlto ($$) { #{{{
  382. my $to=shift;
  383. my $from=shift;
  384. if (! length $to) {
  385. return beautify_url(baseurl($from));
  386. }
  387. if (! $destsources{$to}) {
  388. $to=htmlpage($to);
  389. }
  390. my $link = abs2rel($to, dirname(htmlpage($from)));
  391. return beautify_url($link);
  392. } #}}}
  393. sub htmllink ($$$;@) { #{{{
  394. my $lpage=shift; # the page doing the linking
  395. my $page=shift; # the page that will contain the link (different for inline)
  396. my $link=shift;
  397. my %opts=@_;
  398. my $bestlink;
  399. if (! $opts{forcesubpage}) {
  400. $bestlink=bestlink($lpage, $link);
  401. }
  402. else {
  403. $bestlink="$lpage/".lc($link);
  404. }
  405. my $linktext;
  406. if (defined $opts{linktext}) {
  407. $linktext=$opts{linktext};
  408. }
  409. else {
  410. $linktext=pagetitle(basename($link));
  411. }
  412. return "<span class=\"selflink\">$linktext</span>"
  413. if length $bestlink && $page eq $bestlink;
  414. if (! $destsources{$bestlink}) {
  415. $bestlink=htmlpage($bestlink);
  416. if (! $destsources{$bestlink}) {
  417. return $linktext unless length $config{cgiurl};
  418. return "<span><a href=\"".
  419. cgiurl(
  420. do => "create",
  421. page => pagetitle(lc($link), 1),
  422. from => $lpage
  423. ).
  424. "\">?</a>$linktext</span>"
  425. }
  426. }
  427. $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
  428. $bestlink=beautify_url($bestlink);
  429. if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
  430. return "<img src=\"$bestlink\" alt=\"$linktext\" />";
  431. }
  432. if (defined $opts{anchor}) {
  433. $bestlink.="#".$opts{anchor};
  434. }
  435. return "<a href=\"$bestlink\">$linktext</a>";
  436. } #}}}
  437. sub htmlize ($$$) { #{{{
  438. my $page=shift;
  439. my $type=shift;
  440. my $content=shift;
  441. if (exists $hooks{htmlize}{$type}) {
  442. $content=$hooks{htmlize}{$type}{call}->(
  443. page => $page,
  444. content => $content,
  445. );
  446. }
  447. else {
  448. error("htmlization of $type not supported");
  449. }
  450. run_hooks(sanitize => sub {
  451. $content=shift->(
  452. page => $page,
  453. content => $content,
  454. );
  455. });
  456. return $content;
  457. } #}}}
  458. sub linkify ($$$) { #{{{
  459. my $lpage=shift; # the page containing the links
  460. my $page=shift; # the page the link will end up on (different for inline)
  461. my $content=shift;
  462. $content =~ s{(\\?)$config{wiki_link_regexp}}{
  463. defined $2
  464. ? ( $1
  465. ? "[[$2|$3".($4 ? "#$4" : "")."]]"
  466. : htmllink($lpage, $page, linkpage($3),
  467. anchor => $4, linktext => pagetitle($2)))
  468. : ( $1
  469. ? "[[$3".($4 ? "#$4" : "")."]]"
  470. : htmllink($lpage, $page, linkpage($3),
  471. anchor => $4))
  472. }eg;
  473. return $content;
  474. } #}}}
  475. my %preprocessing;
  476. our $preprocess_preview=0;
  477. sub preprocess ($$$;$$) { #{{{
  478. my $page=shift; # the page the data comes from
  479. my $destpage=shift; # the page the data will appear in (different for inline)
  480. my $content=shift;
  481. my $scan=shift;
  482. my $preview=shift;
  483. # Using local because it needs to be set within any nested calls
  484. # of this function.
  485. local $preprocess_preview=$preview if defined $preview;
  486. my $handle=sub {
  487. my $escape=shift;
  488. my $command=shift;
  489. my $params=shift;
  490. if (length $escape) {
  491. return "[[$command $params]]";
  492. }
  493. elsif (exists $hooks{preprocess}{$command}) {
  494. return "" if $scan && ! $hooks{preprocess}{$command}{scan};
  495. # Note: preserve order of params, some plugins may
  496. # consider it significant.
  497. my @params;
  498. while ($params =~ /(?:(\w+)=)?(?:"""(.*?)"""|"([^"]+)"|(\S+))(?:\s+|$)/sg) {
  499. my $key=$1;
  500. my $val;
  501. if (defined $2) {
  502. $val=$2;
  503. $val=~s/\r\n/\n/mg;
  504. $val=~s/^\n+//g;
  505. $val=~s/\n+$//g;
  506. }
  507. elsif (defined $3) {
  508. $val=$3;
  509. }
  510. elsif (defined $4) {
  511. $val=$4;
  512. }
  513. if (defined $key) {
  514. push @params, $key, $val;
  515. }
  516. else {
  517. push @params, $val, '';
  518. }
  519. }
  520. if ($preprocessing{$page}++ > 3) {
  521. # Avoid loops of preprocessed pages preprocessing
  522. # other pages that preprocess them, etc.
  523. #translators: The first parameter is a
  524. #translators: preprocessor directive name,
  525. #translators: the second a page name, the
  526. #translators: third a number.
  527. return "[[".sprintf(gettext("%s preprocessing loop detected on %s at depth %i"),
  528. $command, $page, $preprocessing{$page}).
  529. "]]";
  530. }
  531. my $ret=$hooks{preprocess}{$command}{call}->(
  532. @params,
  533. page => $page,
  534. destpage => $destpage,
  535. preview => $preprocess_preview,
  536. );
  537. $preprocessing{$page}--;
  538. return $ret;
  539. }
  540. else {
  541. return "[[$command $params]]";
  542. }
  543. };
  544. $content =~ s{(\\?)\[\[(\w+)\s+((?:(?:\w+=)?(?:""".*?"""|"[^"]+"|[^\s\]]+)\s*)*)\]\]}{$handle->($1, $2, $3)}seg;
  545. return $content;
  546. } #}}}
  547. sub filter ($$) { #{{{
  548. my $page=shift;
  549. my $content=shift;
  550. run_hooks(filter => sub {
  551. $content=shift->(page => $page, content => $content);
  552. });
  553. return $content;
  554. } #}}}
  555. sub indexlink () { #{{{
  556. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  557. } #}}}
  558. sub lockwiki () { #{{{
  559. # Take an exclusive lock on the wiki to prevent multiple concurrent
  560. # run issues. The lock will be dropped on program exit.
  561. if (! -d $config{wikistatedir}) {
  562. mkdir($config{wikistatedir});
  563. }
  564. open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
  565. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  566. if (! flock(WIKILOCK, 2 | 4)) { # LOCK_EX | LOCK_NB
  567. debug("wiki seems to be locked, waiting for lock");
  568. my $wait=600; # arbitrary, but don't hang forever to
  569. # prevent process pileup
  570. for (1..$wait) {
  571. return if flock(WIKILOCK, 2 | 4);
  572. sleep 1;
  573. }
  574. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  575. }
  576. } #}}}
  577. sub unlockwiki () { #{{{
  578. close WIKILOCK;
  579. } #}}}
  580. sub commit_hook_enabled () { #{{{
  581. open(COMMITLOCK, "+>$config{wikistatedir}/commitlock") ||
  582. error ("cannot write to $config{wikistatedir}/commitlock: $!");
  583. if (! flock(COMMITLOCK, 1 | 4)) { # LOCK_SH | LOCK_NB to test
  584. close COMMITLOCK;
  585. return 0;
  586. }
  587. close COMMITLOCK;
  588. return 1;
  589. } #}}}
  590. sub disable_commit_hook () { #{{{
  591. open(COMMITLOCK, ">$config{wikistatedir}/commitlock") ||
  592. error ("cannot write to $config{wikistatedir}/commitlock: $!");
  593. if (! flock(COMMITLOCK, 2)) { # LOCK_EX
  594. error("failed to get commit lock");
  595. }
  596. } #}}}
  597. sub enable_commit_hook () { #{{{
  598. close COMMITLOCK;
  599. } #}}}
  600. sub loadindex () { #{{{
  601. open (IN, "$config{wikistatedir}/index") || return;
  602. while (<IN>) {
  603. $_=possibly_foolish_untaint($_);
  604. chomp;
  605. my %items;
  606. $items{link}=[];
  607. $items{dest}=[];
  608. foreach my $i (split(/ /, $_)) {
  609. my ($item, $val)=split(/=/, $i, 2);
  610. push @{$items{$item}}, decode_entities($val);
  611. }
  612. next unless exists $items{src}; # skip bad lines for now
  613. my $page=pagename($items{src}[0]);
  614. if (! $config{rebuild}) {
  615. $pagesources{$page}=$items{src}[0];
  616. $pagemtime{$page}=$items{mtime}[0];
  617. $oldlinks{$page}=[@{$items{link}}];
  618. $links{$page}=[@{$items{link}}];
  619. $depends{$page}=$items{depends}[0] if exists $items{depends};
  620. $destsources{$_}=$page foreach @{$items{dest}};
  621. $renderedfiles{$page}=[@{$items{dest}}];
  622. $oldrenderedfiles{$page}=[@{$items{dest}}];
  623. $pagecase{lc $page}=$page;
  624. }
  625. $pagectime{$page}=$items{ctime}[0];
  626. }
  627. close IN;
  628. } #}}}
  629. sub saveindex () { #{{{
  630. run_hooks(savestate => sub { shift->() });
  631. if (! -d $config{wikistatedir}) {
  632. mkdir($config{wikistatedir});
  633. }
  634. my $newfile="$config{wikistatedir}/index.new";
  635. my $cleanup = sub { unlink($newfile) };
  636. open (OUT, ">$newfile") || error("cannot write to $newfile: $!", $cleanup);
  637. foreach my $page (keys %pagemtime) {
  638. next unless $pagemtime{$page};
  639. my $line="mtime=$pagemtime{$page} ".
  640. "ctime=$pagectime{$page} ".
  641. "src=$pagesources{$page}";
  642. $line.=" dest=$_" foreach @{$renderedfiles{$page}};
  643. my %count;
  644. $line.=" link=$_" foreach grep { ++$count{$_} == 1 } @{$links{$page}};
  645. if (exists $depends{$page}) {
  646. $line.=" depends=".encode_entities($depends{$page}, " \t\n");
  647. }
  648. print OUT $line."\n" || error("failed writing to $newfile: $!", $cleanup);
  649. }
  650. close OUT || error("failed saving to $newfile: $!", $cleanup);
  651. rename($newfile, "$config{wikistatedir}/index") ||
  652. error("failed renaming $newfile to $config{wikistatedir}/index", $cleanup);
  653. } #}}}
  654. sub template_file ($) { #{{{
  655. my $template=shift;
  656. foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") {
  657. return "$dir/$template" if -e "$dir/$template";
  658. }
  659. return undef;
  660. } #}}}
  661. sub template_params (@) { #{{{
  662. my $filename=template_file(shift);
  663. if (! defined $filename) {
  664. return if wantarray;
  665. return "";
  666. }
  667. require HTML::Template;
  668. my @ret=(
  669. filter => sub {
  670. my $text_ref = shift;
  671. $$text_ref=&Encode::decode_utf8($$text_ref);
  672. },
  673. filename => $filename,
  674. loop_context_vars => 1,
  675. die_on_bad_params => 0,
  676. @_
  677. );
  678. return wantarray ? @ret : {@ret};
  679. } #}}}
  680. sub template ($;@) { #{{{
  681. HTML::Template->new(template_params(@_));
  682. } #}}}
  683. sub misctemplate ($$;@) { #{{{
  684. my $title=shift;
  685. my $pagebody=shift;
  686. my $template=template("misc.tmpl");
  687. $template->param(
  688. title => $title,
  689. indexlink => indexlink(),
  690. wikiname => $config{wikiname},
  691. pagebody => $pagebody,
  692. baseurl => baseurl(),
  693. @_,
  694. );
  695. run_hooks(pagetemplate => sub {
  696. shift->(page => "", destpage => "", template => $template);
  697. });
  698. return $template->output;
  699. }#}}}
  700. sub hook (@) { # {{{
  701. my %param=@_;
  702. if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
  703. error "hook requires type, call, and id parameters";
  704. }
  705. return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
  706. $hooks{$param{type}}{$param{id}}=\%param;
  707. } # }}}
  708. sub run_hooks ($$) { # {{{
  709. # Calls the given sub for each hook of the given type,
  710. # passing it the hook function to call.
  711. my $type=shift;
  712. my $sub=shift;
  713. if (exists $hooks{$type}) {
  714. my @deferred;
  715. foreach my $id (keys %{$hooks{$type}}) {
  716. if ($hooks{$type}{$id}{last}) {
  717. push @deferred, $id;
  718. next;
  719. }
  720. $sub->($hooks{$type}{$id}{call});
  721. }
  722. foreach my $id (@deferred) {
  723. $sub->($hooks{$type}{$id}{call});
  724. }
  725. }
  726. } #}}}
  727. sub globlist_to_pagespec ($) { #{{{
  728. my @globlist=split(' ', shift);
  729. my (@spec, @skip);
  730. foreach my $glob (@globlist) {
  731. if ($glob=~/^!(.*)/) {
  732. push @skip, $glob;
  733. }
  734. else {
  735. push @spec, $glob;
  736. }
  737. }
  738. my $spec=join(" or ", @spec);
  739. if (@skip) {
  740. my $skip=join(" and ", @skip);
  741. if (length $spec) {
  742. $spec="$skip and ($spec)";
  743. }
  744. else {
  745. $spec=$skip;
  746. }
  747. }
  748. return $spec;
  749. } #}}}
  750. sub is_globlist ($) { #{{{
  751. my $s=shift;
  752. $s=~/[^\s]+\s+([^\s]+)/ && $1 ne "and" && $1 ne "or";
  753. } #}}}
  754. sub safequote ($) { #{{{
  755. my $s=shift;
  756. $s=~s/[{}]//g;
  757. return "q{$s}";
  758. } #}}}
  759. sub add_depends ($$) { #{{{
  760. my $page=shift;
  761. my $pagespec=shift;
  762. if (! exists $depends{$page}) {
  763. $depends{$page}=$pagespec;
  764. }
  765. else {
  766. $depends{$page}=pagespec_merge($depends{$page}, $pagespec);
  767. }
  768. } # }}}
  769. sub file_pruned ($$) { #{{{
  770. require File::Spec;
  771. my $file=File::Spec->canonpath(shift);
  772. my $base=File::Spec->canonpath(shift);
  773. $file=~s#^\Q$base\E/*##;
  774. my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
  775. $file =~ m/$regexp/;
  776. } #}}}
  777. sub gettext { #{{{
  778. # Only use gettext in the rare cases it's needed.
  779. if (exists $ENV{LANG} || exists $ENV{LC_ALL} || exists $ENV{LC_MESSAGES}) {
  780. if (! $gettext_obj) {
  781. $gettext_obj=eval q{
  782. use Locale::gettext q{textdomain};
  783. Locale::gettext->domain('ikiwiki')
  784. };
  785. if ($@) {
  786. print STDERR "$@";
  787. $gettext_obj=undef;
  788. return shift;
  789. }
  790. }
  791. return $gettext_obj->get(shift);
  792. }
  793. else {
  794. return shift;
  795. }
  796. } #}}}
  797. sub pagespec_merge ($$) { #{{{
  798. my $a=shift;
  799. my $b=shift;
  800. return $a if $a eq $b;
  801. # Support for old-style GlobLists.
  802. if (is_globlist($a)) {
  803. $a=globlist_to_pagespec($a);
  804. }
  805. if (is_globlist($b)) {
  806. $b=globlist_to_pagespec($b);
  807. }
  808. return "($a) or ($b)";
  809. } #}}}
  810. sub pagespec_translate ($) { #{{{
  811. # This assumes that $page is in scope in the function
  812. # that evalulates the translated pagespec code.
  813. my $spec=shift;
  814. # Support for old-style GlobLists.
  815. if (is_globlist($spec)) {
  816. $spec=globlist_to_pagespec($spec);
  817. }
  818. # Convert spec to perl code.
  819. my $code="";
  820. while ($spec=~m/\s*(\!|\(|\)|\w+\([^\)]+\)|[^\s()]+)\s*/ig) {
  821. my $word=$1;
  822. if (lc $word eq "and") {
  823. $code.=" &&";
  824. }
  825. elsif (lc $word eq "or") {
  826. $code.=" ||";
  827. }
  828. elsif ($word eq "(" || $word eq ")" || $word eq "!") {
  829. $code.=" ".$word;
  830. }
  831. elsif ($word =~ /^(\w+)\((.*)\)$/) {
  832. if (exists $IkiWiki::PageSpec::{"match_$1"}) {
  833. $code.="IkiWiki::PageSpec::match_$1(\$page, ".safequote($2).", \$from)";
  834. }
  835. else {
  836. $code.=" 0";
  837. }
  838. }
  839. else {
  840. $code.=" IkiWiki::PageSpec::match_glob(\$page, ".safequote($word).", \$from)";
  841. }
  842. }
  843. return $code;
  844. } #}}}
  845. sub pagespec_match ($$;$) { #{{{
  846. my $page=shift;
  847. my $spec=shift;
  848. my $from=shift;
  849. return eval pagespec_translate($spec);
  850. } #}}}
  851. package IkiWiki::PageSpec;
  852. sub match_glob ($$$) { #{{{
  853. my $page=shift;
  854. my $glob=shift;
  855. my $from=shift;
  856. if (! defined $from){
  857. $from = "";
  858. }
  859. # relative matching
  860. if ($glob =~ m!^\./!) {
  861. $from=~s!/?[^/]+$!!;
  862. $glob=~s!^\./!!;
  863. $glob="$from/$glob" if length $from;
  864. }
  865. # turn glob into safe regexp
  866. $glob=quotemeta($glob);
  867. $glob=~s/\\\*/.*/g;
  868. $glob=~s/\\\?/./g;
  869. return $page=~/^$glob$/i;
  870. } #}}}
  871. sub match_link ($$$) { #{{{
  872. my $page=shift;
  873. my $link=lc(shift);
  874. my $from=shift;
  875. if (! defined $from){
  876. $from = "";
  877. }
  878. # relative matching
  879. if ($link =~ m!^\.! && defined $from) {
  880. $from=~s!/?[^/]+$!!;
  881. $link=~s!^\./!!;
  882. $link="$from/$link" if length $from;
  883. }
  884. my $links = $IkiWiki::links{$page} or return undef;
  885. return 0 unless @$links;
  886. my $bestlink = IkiWiki::bestlink($from, $link);
  887. return 0 unless length $bestlink;
  888. foreach my $p (@$links) {
  889. return 1 if $bestlink eq IkiWiki::bestlink($page, $p);
  890. }
  891. return 0;
  892. } #}}}
  893. sub match_backlink ($$$) { #{{{
  894. match_link($_[1], $_[0], $_[3]);
  895. } #}}}
  896. sub match_created_before ($$$) { #{{{
  897. my $page=shift;
  898. my $testpage=shift;
  899. if (exists $IkiWiki::pagectime{$testpage}) {
  900. return $IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage};
  901. }
  902. else {
  903. return 0;
  904. }
  905. } #}}}
  906. sub match_created_after ($$$) { #{{{
  907. my $page=shift;
  908. my $testpage=shift;
  909. if (exists $IkiWiki::pagectime{$testpage}) {
  910. return $IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage};
  911. }
  912. else {
  913. return 0;
  914. }
  915. } #}}}
  916. sub match_creation_day ($$$) { #{{{
  917. return ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift);
  918. } #}}}
  919. sub match_creation_month ($$$) { #{{{
  920. return ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift);
  921. } #}}}
  922. sub match_creation_year ($$$) { #{{{
  923. return ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift);
  924. } #}}}
  925. 1