summaryrefslogtreecommitdiff
path: root/IkiWiki.pm
blob: d667e7e10fe43c9162b9dd54a0105c41c80c3afe (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 Storable;
  10. use open qw{:utf8 :std};
  11. use vars qw{%config %links %oldlinks %pagemtime %pagectime %pagecase
  12. %pagestate %wikistate %renderedfiles %oldrenderedfiles
  13. %pagesources %destsources %depends %depends_simple %hooks
  14. %forcerebuild %loaded_plugins};
  15. use Exporter q{import};
  16. our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match
  17. pagespec_match_list bestlink htmllink readfile writefile
  18. pagetype srcfile pagename displaytime will_render gettext urlto
  19. targetpage add_underlay pagetitle titlepage linkpage
  20. newpagefile inject add_link
  21. %config %links %pagestate %wikistate %renderedfiles
  22. %pagesources %destsources);
  23. our $VERSION = 3.00; # plugin interface version, next is ikiwiki version
  24. our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE
  25. our $installdir='/usr'; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE
  26. # Optimisation.
  27. use Memoize;
  28. memoize("abs2rel");
  29. memoize("pagespec_translate");
  30. memoize("template_file");
  31. sub getsetup () {
  32. wikiname => {
  33. type => "string",
  34. default => "wiki",
  35. description => "name of the wiki",
  36. safe => 1,
  37. rebuild => 1,
  38. },
  39. adminemail => {
  40. type => "string",
  41. default => undef,
  42. example => 'me@example.com',
  43. description => "contact email for wiki",
  44. safe => 1,
  45. rebuild => 0,
  46. },
  47. adminuser => {
  48. type => "string",
  49. default => [],
  50. description => "users who are wiki admins",
  51. safe => 1,
  52. rebuild => 0,
  53. },
  54. banned_users => {
  55. type => "string",
  56. default => [],
  57. description => "users who are banned from the wiki",
  58. safe => 1,
  59. rebuild => 0,
  60. },
  61. srcdir => {
  62. type => "string",
  63. default => undef,
  64. example => "$ENV{HOME}/wiki",
  65. description => "where the source of the wiki is located",
  66. safe => 0, # path
  67. rebuild => 1,
  68. },
  69. destdir => {
  70. type => "string",
  71. default => undef,
  72. example => "/var/www/wiki",
  73. description => "where to build the wiki",
  74. safe => 0, # path
  75. rebuild => 1,
  76. },
  77. url => {
  78. type => "string",
  79. default => '',
  80. example => "http://example.com/wiki",
  81. description => "base url to the wiki",
  82. safe => 1,
  83. rebuild => 1,
  84. },
  85. cgiurl => {
  86. type => "string",
  87. default => '',
  88. example => "http://example.com/wiki/ikiwiki.cgi",
  89. description => "url to the ikiwiki.cgi",
  90. safe => 1,
  91. rebuild => 1,
  92. },
  93. cgi_wrapper => {
  94. type => "string",
  95. default => '',
  96. example => "/var/www/wiki/ikiwiki.cgi",
  97. description => "filename of cgi wrapper to generate",
  98. safe => 0, # file
  99. rebuild => 0,
  100. },
  101. cgi_wrappermode => {
  102. type => "string",
  103. default => '06755',
  104. description => "mode for cgi_wrapper (can safely be made suid)",
  105. safe => 0,
  106. rebuild => 0,
  107. },
  108. rcs => {
  109. type => "string",
  110. default => '',
  111. description => "rcs backend to use",
  112. safe => 0, # don't allow overriding
  113. rebuild => 0,
  114. },
  115. default_plugins => {
  116. type => "internal",
  117. default => [qw{mdwn link inline meta htmlscrubber passwordauth
  118. openid signinedit lockedit conditional
  119. recentchanges parentlinks editpage}],
  120. description => "plugins to enable by default",
  121. safe => 0,
  122. rebuild => 1,
  123. },
  124. add_plugins => {
  125. type => "string",
  126. default => [],
  127. description => "plugins to add to the default configuration",
  128. safe => 1,
  129. rebuild => 1,
  130. },
  131. disable_plugins => {
  132. type => "string",
  133. default => [],
  134. description => "plugins to disable",
  135. safe => 1,
  136. rebuild => 1,
  137. },
  138. templatedir => {
  139. type => "string",
  140. default => "$installdir/share/ikiwiki/templates",
  141. description => "location of template files",
  142. advanced => 1,
  143. safe => 0, # path
  144. rebuild => 1,
  145. },
  146. templatedirs => {
  147. type => "internal",
  148. default => [],
  149. description => "additional directories containing template files",
  150. safe => 0,
  151. rebuild => 0,
  152. },
  153. underlaydir => {
  154. type => "string",
  155. default => "$installdir/share/ikiwiki/basewiki",
  156. description => "base wiki source location",
  157. advanced => 1,
  158. safe => 0, # path
  159. rebuild => 0,
  160. },
  161. underlaydirbase => {
  162. type => "internal",
  163. default => "$installdir/share/ikiwiki",
  164. description => "parent directory containing additional underlays",
  165. safe => 0,
  166. rebuild => 0,
  167. },
  168. wrappers => {
  169. type => "internal",
  170. default => [],
  171. description => "wrappers to generate",
  172. safe => 0,
  173. rebuild => 0,
  174. },
  175. underlaydirs => {
  176. type => "internal",
  177. default => [],
  178. description => "additional underlays to use",
  179. safe => 0,
  180. rebuild => 0,
  181. },
  182. verbose => {
  183. type => "boolean",
  184. example => 1,
  185. description => "display verbose messages?",
  186. safe => 1,
  187. rebuild => 0,
  188. },
  189. syslog => {
  190. type => "boolean",
  191. example => 1,
  192. description => "log to syslog?",
  193. safe => 1,
  194. rebuild => 0,
  195. },
  196. usedirs => {
  197. type => "boolean",
  198. default => 1,
  199. description => "create output files named page/index.html?",
  200. safe => 0, # changing requires manual transition
  201. rebuild => 1,
  202. },
  203. prefix_directives => {
  204. type => "boolean",
  205. default => 1,
  206. description => "use '!'-prefixed preprocessor directives?",
  207. safe => 0, # changing requires manual transition
  208. rebuild => 1,
  209. },
  210. indexpages => {
  211. type => "boolean",
  212. default => 0,
  213. description => "use page/index.mdwn source files",
  214. safe => 1,
  215. rebuild => 1,
  216. },
  217. discussion => {
  218. type => "boolean",
  219. default => 1,
  220. description => "enable Discussion pages?",
  221. safe => 1,
  222. rebuild => 1,
  223. },
  224. discussionpage => {
  225. type => "string",
  226. default => gettext("Discussion"),
  227. description => "name of Discussion pages",
  228. safe => 1,
  229. rebuild => 1,
  230. },
  231. sslcookie => {
  232. type => "boolean",
  233. default => 0,
  234. description => "only send cookies over SSL connections?",
  235. advanced => 1,
  236. safe => 1,
  237. rebuild => 0,
  238. },
  239. default_pageext => {
  240. type => "string",
  241. default => "mdwn",
  242. description => "extension to use for new pages",
  243. safe => 0, # not sanitized
  244. rebuild => 0,
  245. },
  246. htmlext => {
  247. type => "string",
  248. default => "html",
  249. description => "extension to use for html files",
  250. safe => 0, # not sanitized
  251. rebuild => 1,
  252. },
  253. timeformat => {
  254. type => "string",
  255. default => '%c',
  256. description => "strftime format string to display date",
  257. advanced => 1,
  258. safe => 1,
  259. rebuild => 1,
  260. },
  261. locale => {
  262. type => "string",
  263. default => undef,
  264. example => "en_US.UTF-8",
  265. description => "UTF-8 locale to use",
  266. advanced => 1,
  267. safe => 0,
  268. rebuild => 1,
  269. },
  270. userdir => {
  271. type => "string",
  272. default => "",
  273. example => "users",
  274. description => "put user pages below specified page",
  275. safe => 1,
  276. rebuild => 1,
  277. },
  278. numbacklinks => {
  279. type => "integer",
  280. default => 10,
  281. description => "how many backlinks to show before hiding excess (0 to show all)",
  282. safe => 1,
  283. rebuild => 1,
  284. },
  285. hardlink => {
  286. type => "boolean",
  287. default => 0,
  288. description => "attempt to hardlink source files? (optimisation for large files)",
  289. advanced => 1,
  290. safe => 0, # paranoia
  291. rebuild => 0,
  292. },
  293. umask => {
  294. type => "integer",
  295. example => "022",
  296. description => "force ikiwiki to use a particular umask",
  297. advanced => 1,
  298. safe => 0, # paranoia
  299. rebuild => 0,
  300. },
  301. wrappergroup => {
  302. type => "string",
  303. example => "ikiwiki",
  304. description => "group for wrappers to run in",
  305. advanced => 1,
  306. safe => 0, # paranoia
  307. rebuild => 0,
  308. },
  309. libdir => {
  310. type => "string",
  311. default => "",
  312. example => "$ENV{HOME}/.ikiwiki/",
  313. description => "extra library and plugin directory",
  314. advanced => 1,
  315. safe => 0, # directory
  316. rebuild => 0,
  317. },
  318. ENV => {
  319. type => "string",
  320. default => {},
  321. description => "environment variables",
  322. safe => 0, # paranoia
  323. rebuild => 0,
  324. },
  325. exclude => {
  326. type => "string",
  327. default => undef,
  328. example => '\.wav$',
  329. description => "regexp of source files to ignore",
  330. advanced => 1,
  331. safe => 0, # regexp
  332. rebuild => 1,
  333. },
  334. wiki_file_prune_regexps => {
  335. type => "internal",
  336. default => [qr/(^|\/)\.\.(\/|$)/, qr/^\./, qr/\/\./,
  337. qr/\.x?html?$/, qr/\.ikiwiki-new$/,
  338. qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//,
  339. qr/(^|\/)_MTN\//, qr/(^|\/)_darcs\//,
  340. qr/(^|\/)CVS\//, qr/\.dpkg-tmp$/],
  341. description => "regexps of source files to ignore",
  342. safe => 0,
  343. rebuild => 1,
  344. },
  345. wiki_file_chars => {
  346. type => "string",
  347. description => "specifies the characters that are allowed in source filenames",
  348. default => "-[:alnum:]+/.:_",
  349. safe => 0,
  350. rebuild => 1,
  351. },
  352. wiki_file_regexp => {
  353. type => "internal",
  354. description => "regexp of legal source files",
  355. safe => 0,
  356. rebuild => 1,
  357. },
  358. web_commit_regexp => {
  359. type => "internal",
  360. default => qr/^web commit (by (.*?(?=: |$))|from ([0-9a-fA-F:.]+[0-9a-fA-F])):?(.*)/,
  361. description => "regexp to parse web commits from logs",
  362. safe => 0,
  363. rebuild => 0,
  364. },
  365. cgi => {
  366. type => "internal",
  367. default => 0,
  368. description => "run as a cgi",
  369. safe => 0,
  370. rebuild => 0,
  371. },
  372. cgi_disable_uploads => {
  373. type => "internal",
  374. default => 1,
  375. description => "whether CGI should accept file uploads",
  376. safe => 0,
  377. rebuild => 0,
  378. },
  379. post_commit => {
  380. type => "internal",
  381. default => 0,
  382. description => "run as a post-commit hook",
  383. safe => 0,
  384. rebuild => 0,
  385. },
  386. rebuild => {
  387. type => "internal",
  388. default => 0,
  389. description => "running in rebuild mode",
  390. safe => 0,
  391. rebuild => 0,
  392. },
  393. setup => {
  394. type => "internal",
  395. default => undef,
  396. description => "running in setup mode",
  397. safe => 0,
  398. rebuild => 0,
  399. },
  400. refresh => {
  401. type => "internal",
  402. default => 0,
  403. description => "running in refresh mode",
  404. safe => 0,
  405. rebuild => 0,
  406. },
  407. test_receive => {
  408. type => "internal",
  409. default => 0,
  410. description => "running in receive test mode",
  411. safe => 0,
  412. rebuild => 0,
  413. },
  414. getctime => {
  415. type => "internal",
  416. default => 0,
  417. description => "running in getctime mode",
  418. safe => 0,
  419. rebuild => 0,
  420. },
  421. w3mmode => {
  422. type => "internal",
  423. default => 0,
  424. description => "running in w3mmode",
  425. safe => 0,
  426. rebuild => 0,
  427. },
  428. wikistatedir => {
  429. type => "internal",
  430. default => undef,
  431. description => "path to the .ikiwiki directory holding ikiwiki state",
  432. safe => 0,
  433. rebuild => 0,
  434. },
  435. setupfile => {
  436. type => "internal",
  437. default => undef,
  438. description => "path to setup file",
  439. safe => 0,
  440. rebuild => 0,
  441. },
  442. allow_symlinks_before_srcdir => {
  443. type => "boolean",
  444. default => 0,
  445. description => "allow symlinks in the path leading to the srcdir (potentially insecure)",
  446. safe => 0,
  447. rebuild => 0,
  448. },
  449. }
  450. sub defaultconfig () {
  451. my %s=getsetup();
  452. my @ret;
  453. foreach my $key (keys %s) {
  454. push @ret, $key, $s{$key}->{default};
  455. }
  456. use Data::Dumper;
  457. return @ret;
  458. }
  459. sub checkconfig () {
  460. # locale stuff; avoid LC_ALL since it overrides everything
  461. if (defined $ENV{LC_ALL}) {
  462. $ENV{LANG} = $ENV{LC_ALL};
  463. delete $ENV{LC_ALL};
  464. }
  465. if (defined $config{locale}) {
  466. if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) {
  467. $ENV{LANG}=$config{locale};
  468. define_gettext();
  469. }
  470. }
  471. if (! defined $config{wiki_file_regexp}) {
  472. $config{wiki_file_regexp}=qr/(^[$config{wiki_file_chars}]+$)/;
  473. }
  474. if (ref $config{ENV} eq 'HASH') {
  475. foreach my $val (keys %{$config{ENV}}) {
  476. $ENV{$val}=$config{ENV}{$val};
  477. }
  478. }
  479. if ($config{w3mmode}) {
  480. eval q{use Cwd q{abs_path}};
  481. error($@) if $@;
  482. $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir}));
  483. $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir}));
  484. $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl}
  485. unless $config{cgiurl} =~ m!file:///!;
  486. $config{url}="file://".$config{destdir};
  487. }
  488. if ($config{cgi} && ! length $config{url}) {
  489. error(gettext("Must specify url to wiki with --url when using --cgi"));
  490. }
  491. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  492. unless exists $config{wikistatedir} && defined $config{wikistatedir};
  493. if (defined $config{umask}) {
  494. umask(possibly_foolish_untaint($config{umask}));
  495. }
  496. run_hooks(checkconfig => sub { shift->() });
  497. return 1;
  498. }
  499. sub listplugins () {
  500. my %ret;
  501. foreach my $dir (@INC, $config{libdir}) {
  502. next unless defined $dir && length $dir;
  503. foreach my $file (glob("$dir/IkiWiki/Plugin/*.pm")) {
  504. my ($plugin)=$file=~/.*\/(.*)\.pm$/;
  505. $ret{$plugin}=1;
  506. }
  507. }
  508. foreach my $dir ($config{libdir}, "$installdir/lib/ikiwiki") {
  509. next unless defined $dir && length $dir;
  510. foreach my $file (glob("$dir/plugins/*")) {
  511. $ret{basename($file)}=1 if -x $file;
  512. }
  513. }
  514. return keys %ret;
  515. }
  516. sub loadplugins () {
  517. if (defined $config{libdir} && length $config{libdir}) {
  518. unshift @INC, possibly_foolish_untaint($config{libdir});
  519. }
  520. foreach my $plugin (@{$config{default_plugins}}, @{$config{add_plugins}}) {
  521. loadplugin($plugin);
  522. }
  523. if ($config{rcs}) {
  524. if (exists $hooks{rcs}) {
  525. error(gettext("cannot use multiple rcs plugins"));
  526. }
  527. loadplugin($config{rcs});
  528. }
  529. if (! exists $hooks{rcs}) {
  530. loadplugin("norcs");
  531. }
  532. run_hooks(getopt => sub { shift->() });
  533. if (grep /^-/, @ARGV) {
  534. print STDERR "Unknown option (or missing parameter): $_\n"
  535. foreach grep /^-/, @ARGV;
  536. usage();
  537. }
  538. return 1;
  539. }
  540. sub loadplugin ($) {
  541. my $plugin=shift;
  542. return if grep { $_ eq $plugin} @{$config{disable_plugins}};
  543. foreach my $dir (defined $config{libdir} ? possibly_foolish_untaint($config{libdir}) : undef,
  544. "$installdir/lib/ikiwiki") {
  545. if (defined $dir && -x "$dir/plugins/$plugin") {
  546. eval { require IkiWiki::Plugin::external };
  547. if ($@) {
  548. my $reason=$@;
  549. error(sprintf(gettext("failed to load external plugin needed for %s plugin: %s"), $plugin, $reason));
  550. }
  551. import IkiWiki::Plugin::external "$dir/plugins/$plugin";
  552. $loaded_plugins{$plugin}=1;
  553. return 1;
  554. }
  555. }
  556. my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin);
  557. eval qq{use $mod};
  558. if ($@) {
  559. error("Failed to load plugin $mod: $@");
  560. }
  561. $loaded_plugins{$plugin}=1;
  562. return 1;
  563. }
  564. sub error ($;$) {
  565. my $message=shift;
  566. my $cleaner=shift;
  567. log_message('err' => $message) if $config{syslog};
  568. if (defined $cleaner) {
  569. $cleaner->();
  570. }
  571. die $message."\n";
  572. }
  573. sub debug ($) {
  574. return unless $config{verbose};
  575. return log_message(debug => @_);
  576. }
  577. my $log_open=0;
  578. sub log_message ($$) {
  579. my $type=shift;
  580. if ($config{syslog}) {
  581. require Sys::Syslog;
  582. if (! $log_open) {
  583. Sys::Syslog::setlogsock('unix');
  584. Sys::Syslog::openlog('ikiwiki', '', 'user');
  585. $log_open=1;
  586. }
  587. return eval {
  588. Sys::Syslog::syslog($type, "[$config{wikiname}] %s", join(" ", @_));
  589. };
  590. }
  591. elsif (! $config{cgi}) {
  592. return print "@_\n";
  593. }
  594. else {
  595. return print STDERR "@_\n";
  596. }
  597. }
  598. sub possibly_foolish_untaint ($) {
  599. my $tainted=shift;
  600. my ($untainted)=$tainted=~/(.*)/s;
  601. return $untainted;
  602. }
  603. sub basename ($) {
  604. my $file=shift;
  605. $file=~s!.*/+!!;
  606. return $file;
  607. }
  608. sub dirname ($) {
  609. my $file=shift;
  610. $file=~s!/*[^/]+$!!;
  611. return $file;
  612. }
  613. sub isinternal ($) {
  614. my $page=shift;
  615. return exists $pagesources{$page} &&
  616. $pagesources{$page} =~ /\._([^.]+)$/;
  617. }
  618. sub pagetype ($) {
  619. my $file=shift;
  620. if ($file =~ /\.([^.]+)$/) {
  621. return $1 if exists $hooks{htmlize}{$1};
  622. }
  623. my $base=basename($file);
  624. if (exists $hooks{htmlize}{$base} &&
  625. $hooks{htmlize}{$base}{noextension}) {
  626. return $base;
  627. }
  628. return;
  629. }
  630. my %pagename_cache;
  631. sub pagename ($) {
  632. my $file=shift;
  633. if (exists $pagename_cache{$file}) {
  634. return $pagename_cache{$file};
  635. }
  636. my $type=pagetype($file);
  637. my $page=$file;
  638. $page=~s/\Q.$type\E*$//
  639. if defined $type && !$hooks{htmlize}{$type}{keepextension}
  640. && !$hooks{htmlize}{$type}{noextension};
  641. if ($config{indexpages} && $page=~/(.*)\/index$/) {
  642. $page=$1;
  643. }
  644. $pagename_cache{$file} = $page;
  645. return $page;
  646. }
  647. sub newpagefile ($$) {
  648. my $page=shift;
  649. my $type=shift;
  650. if (! $config{indexpages} || $page eq 'index') {
  651. return $page.".".$type;
  652. }
  653. else {
  654. return $page."/index.".$type;
  655. }
  656. }
  657. sub targetpage ($$;$) {
  658. my $page=shift;
  659. my $ext=shift;
  660. my $filename=shift;
  661. if (defined $filename) {
  662. return $page."/".$filename.".".$ext;
  663. }
  664. elsif (! $config{usedirs} || $page eq 'index') {
  665. return $page.".".$ext;
  666. }
  667. else {
  668. return $page."/index.".$ext;
  669. }
  670. }
  671. sub htmlpage ($) {
  672. my $page=shift;
  673. return targetpage($page, $config{htmlext});
  674. }
  675. sub srcfile_stat {
  676. my $file=shift;
  677. my $nothrow=shift;
  678. return "$config{srcdir}/$file", stat(_) if -e "$config{srcdir}/$file";
  679. foreach my $dir (@{$config{underlaydirs}}, $config{underlaydir}) {
  680. return "$dir/$file", stat(_) if -e "$dir/$file";
  681. }
  682. error("internal error: $file cannot be found in $config{srcdir} or underlay") unless $nothrow;
  683. return;
  684. }
  685. sub srcfile ($;$) {
  686. return (srcfile_stat(@_))[0];
  687. }
  688. sub add_underlay ($) {
  689. my $dir=shift;
  690. if ($dir !~ /^\//) {
  691. $dir="$config{underlaydirbase}/$dir";
  692. }
  693. if (! grep { $_ eq $dir } @{$config{underlaydirs}}) {
  694. unshift @{$config{underlaydirs}}, $dir;
  695. }
  696. return 1;
  697. }
  698. sub readfile ($;$$) {
  699. my $file=shift;
  700. my $binary=shift;
  701. my $wantfd=shift;
  702. if (-l $file) {
  703. error("cannot read a symlink ($file)");
  704. }
  705. local $/=undef;
  706. open (my $in, "<", $file) || error("failed to read $file: $!");
  707. binmode($in) if ($binary);
  708. return \*$in if $wantfd;
  709. my $ret=<$in>;
  710. # check for invalid utf-8, and toss it back to avoid crashes
  711. if (! utf8::valid($ret)) {
  712. $ret=encode_utf8($ret);
  713. }
  714. close $in || error("failed to read $file: $!");
  715. return $ret;
  716. }
  717. sub prep_writefile ($$) {
  718. my $file=shift;
  719. my $destdir=shift;
  720. my $test=$file;
  721. while (length $test) {
  722. if (-l "$destdir/$test") {
  723. error("cannot write to a symlink ($test)");
  724. }
  725. $test=dirname($test);
  726. }
  727. my $dir=dirname("$destdir/$file");
  728. if (! -d $dir) {
  729. my $d="";
  730. foreach my $s (split(m!/+!, $dir)) {
  731. $d.="$s/";
  732. if (! -d $d) {
  733. mkdir($d) || error("failed to create directory $d: $!");
  734. }
  735. }
  736. }
  737. return 1;
  738. }
  739. sub writefile ($$$;$$) {
  740. my $file=shift; # can include subdirs
  741. my $destdir=shift; # directory to put file in
  742. my $content=shift;
  743. my $binary=shift;
  744. my $writer=shift;
  745. prep_writefile($file, $destdir);
  746. my $newfile="$destdir/$file.ikiwiki-new";
  747. if (-l $newfile) {
  748. error("cannot write to a symlink ($newfile)");
  749. }
  750. my $cleanup = sub { unlink($newfile) };
  751. open (my $out, '>', $newfile) || error("failed to write $newfile: $!", $cleanup);
  752. binmode($out) if ($binary);
  753. if ($writer) {
  754. $writer->(\*$out, $cleanup);
  755. }
  756. else {
  757. print $out $content or error("failed writing to $newfile: $!", $cleanup);
  758. }
  759. close $out || error("failed saving $newfile: $!", $cleanup);
  760. rename($newfile, "$destdir/$file") ||
  761. error("failed renaming $newfile to $destdir/$file: $!", $cleanup);
  762. return 1;
  763. }
  764. my %cleared;
  765. sub will_render ($$;$) {
  766. my $page=shift;
  767. my $dest=shift;
  768. my $clear=shift;
  769. # Important security check.
  770. if (-e "$config{destdir}/$dest" && ! $config{rebuild} &&
  771. ! grep { $_ eq $dest } (@{$renderedfiles{$page}}, @{$oldrenderedfiles{$page}}, @{$wikistate{editpage}{previews}})) {
  772. error("$config{destdir}/$dest independently created, not overwriting with version from $page");
  773. }
  774. if (! $clear || $cleared{$page}) {
  775. $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}];
  776. }
  777. else {
  778. foreach my $old (@{$renderedfiles{$page}}) {
  779. delete $destsources{$old};
  780. }
  781. $renderedfiles{$page}=[$dest];
  782. $cleared{$page}=1;
  783. }
  784. $destsources{$dest}=$page;
  785. return 1;
  786. }
  787. sub bestlink ($$) {
  788. my $page=shift;
  789. my $link=shift;
  790. my $cwd=$page;
  791. if ($link=~s/^\/+//) {
  792. # absolute links
  793. $cwd="";
  794. }
  795. $link=~s/\/$//;
  796. do {
  797. my $l=$cwd;
  798. $l.="/" if length $l;
  799. $l.=$link;
  800. if (exists $links{$l}) {
  801. return $l;
  802. }
  803. elsif (exists $pagecase{lc $l}) {
  804. return $pagecase{lc $l};
  805. }
  806. } while $cwd=~s{/?[^/]+$}{};
  807. if (length $config{userdir}) {
  808. my $l = "$config{userdir}/".lc($link);
  809. if (exists $links{$l}) {
  810. return $l;
  811. }
  812. elsif (exists $pagecase{lc $l}) {
  813. return $pagecase{lc $l};
  814. }
  815. }
  816. #print STDERR "warning: page $page, broken link: $link\n";
  817. return "";
  818. }
  819. sub isinlinableimage ($) {
  820. my $file=shift;
  821. return $file =~ /\.(png|gif|jpg|jpeg)$/i;
  822. }
  823. sub pagetitle ($;$) {
  824. my $page=shift;
  825. my $unescaped=shift;
  826. if ($unescaped) {
  827. $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg;
  828. }
  829. else {
  830. $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg;
  831. }
  832. return $page;
  833. }
  834. sub titlepage ($) {
  835. my $title=shift;
  836. # support use w/o %config set
  837. my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
  838. $title=~s/([^$chars]|_)/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
  839. return $title;
  840. }
  841. sub linkpage ($) {
  842. my $link=shift;
  843. my $chars = defined $config{wiki_file_chars} ? $config{wiki_file_chars} : "-[:alnum:]+/.:_";
  844. $link=~s/([^$chars])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg;
  845. return $link;
  846. }
  847. sub cgiurl (@) {
  848. my %params=@_;
  849. return $config{cgiurl}."?".
  850. join("&amp;", map $_."=".uri_escape_utf8($params{$_}), keys %params);
  851. }
  852. sub baseurl (;$) {
  853. my $page=shift;
  854. return "$config{url}/" if ! defined $page;
  855. $page=htmlpage($page);
  856. $page=~s/[^\/]+$//;
  857. $page=~s/[^\/]+\//..\//g;
  858. return $page;
  859. }
  860. sub abs2rel ($$) {
  861. # Work around very innefficient behavior in File::Spec if abs2rel
  862. # is passed two relative paths. It's much faster if paths are
  863. # absolute! (Debian bug #376658; fixed in debian unstable now)
  864. my $path="/".shift;
  865. my $base="/".shift;
  866. require File::Spec;
  867. my $ret=File::Spec->abs2rel($path, $base);
  868. $ret=~s/^// if defined $ret;
  869. return $ret;
  870. }
  871. sub displaytime ($;$) {
  872. # Plugins can override this function to mark up the time to
  873. # display.
  874. return '<span class="date">'.formattime(@_).'</span>';
  875. }
  876. sub formattime ($;$) {
  877. # Plugins can override this function to format the time.
  878. my $time=shift;
  879. my $format=shift;
  880. if (! defined $format) {
  881. $format=$config{timeformat};
  882. }
  883. # strftime doesn't know about encodings, so make sure
  884. # its output is properly treated as utf8
  885. return decode_utf8(POSIX::strftime($format, localtime($time)));
  886. }
  887. sub beautify_urlpath ($) {
  888. my $url=shift;
  889. # Ensure url is not an empty link, and if necessary,
  890. # add ./ to avoid colon confusion.
  891. if ($url !~ /^\// && $url !~ /^\.\.?\//) {
  892. $url="./$url";
  893. }
  894. if ($config{usedirs}) {
  895. $url =~ s!/index.$config{htmlext}$!/!;
  896. }
  897. return $url;
  898. }
  899. sub urlto ($$;$) {
  900. my $to=shift;
  901. my $from=shift;
  902. my $absolute=shift;
  903. if (! length $to) {
  904. return beautify_urlpath(baseurl($from)."index.$config{htmlext}");
  905. }
  906. if (! $destsources{$to}) {
  907. $to=htmlpage($to);
  908. }
  909. if ($absolute) {
  910. return $config{url}.beautify_urlpath("/".$to);
  911. }
  912. my $link = abs2rel($to, dirname(htmlpage($from)));
  913. return beautify_urlpath($link);
  914. }
  915. sub htmllink ($$$;@) {
  916. my $lpage=shift; # the page doing the linking
  917. my $page=shift; # the page that will contain the link (different for inline)
  918. my $link=shift;
  919. my %opts=@_;
  920. $link=~s/\/$//;
  921. my $bestlink;
  922. if (! $opts{forcesubpage}) {
  923. $bestlink=bestlink($lpage, $link);
  924. }
  925. else {
  926. $bestlink="$lpage/".lc($link);
  927. }
  928. my $linktext;
  929. if (defined $opts{linktext}) {
  930. $linktext=$opts{linktext};
  931. }
  932. else {
  933. $linktext=pagetitle(basename($link));
  934. }
  935. return "<span class=\"selflink\">$linktext</span>"
  936. if length $bestlink && $page eq $bestlink &&
  937. ! defined $opts{anchor};
  938. if (! $destsources{$bestlink}) {
  939. $bestlink=htmlpage($bestlink);
  940. if (! $destsources{$bestlink}) {
  941. return $linktext unless length $config{cgiurl};
  942. return "<span class=\"createlink\"><a href=\"".
  943. cgiurl(
  944. do => "create",
  945. page => lc($link),
  946. from => $lpage
  947. ).
  948. "\" rel=\"nofollow\">?</a>$linktext</span>"
  949. }
  950. }
  951. $bestlink=abs2rel($bestlink, dirname(htmlpage($page)));
  952. $bestlink=beautify_urlpath($bestlink);
  953. if (! $opts{noimageinline} && isinlinableimage($bestlink)) {
  954. return "<img src=\"$bestlink\" alt=\"$linktext\" />";
  955. }
  956. if (defined $opts{anchor}) {
  957. $bestlink.="#".$opts{anchor};
  958. }
  959. my @attrs;
  960. if (defined $opts{rel}) {
  961. push @attrs, ' rel="'.$opts{rel}.'"';
  962. }
  963. if (defined $opts{class}) {
  964. push @attrs, ' class="'.$opts{class}.'"';
  965. }
  966. return "<a href=\"$bestlink\"@attrs>$linktext</a>";
  967. }
  968. sub openiduser ($) {
  969. my $user=shift;
  970. if ($user =~ m!^https?://! &&
  971. eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) {
  972. my $display;
  973. if (Net::OpenID::VerifiedIdentity->can("DisplayOfURL")) {
  974. # this works in at least 2.x
  975. $display = Net::OpenID::VerifiedIdentity::DisplayOfURL($user);
  976. }
  977. else {
  978. # this only works in 1.x
  979. my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user);
  980. $display=$oid->display;
  981. }
  982. # Convert "user.somehost.com" to "user [somehost.com]"
  983. # (also "user.somehost.co.uk")
  984. if ($display !~ /\[/) {
  985. $display=~s/^([-a-zA-Z0-9]+?)\.([-.a-zA-Z0-9]+\.[a-z]+)$/$1 [$2]/;
  986. }
  987. # Convert "http://somehost.com/user" to "user [somehost.com]".
  988. # (also "https://somehost.com/user/")
  989. if ($display !~ /\[/) {
  990. $display=~s/^https?:\/\/(.+)\/([^\/]+)\/?$/$2 [$1]/;
  991. }
  992. $display=~s!^https?://!!; # make sure this is removed
  993. eval q{use CGI 'escapeHTML'};
  994. error($@) if $@;
  995. return escapeHTML($display);
  996. }
  997. return;
  998. }
  999. sub userlink ($) {
  1000. my $user=shift;
  1001. my $oiduser=eval { openiduser($user) };
  1002. if (defined $oiduser) {
  1003. return "<a href=\"$user\">$oiduser</a>";
  1004. }
  1005. else {
  1006. eval q{use CGI 'escapeHTML'};
  1007. error($@) if $@;
  1008. return htmllink("", "", escapeHTML(
  1009. length $config{userdir} ? $config{userdir}."/".$user : $user
  1010. ), noimageinline => 1);
  1011. }
  1012. }
  1013. sub htmlize ($$$$) {
  1014. my $page=shift;
  1015. my $destpage=shift;
  1016. my $type=shift;
  1017. my $content=shift;
  1018. my $oneline = $content !~ /\n/;
  1019. if (exists $hooks{htmlize}{$type}) {
  1020. $content=$hooks{htmlize}{$type}{call}->(
  1021. page => $page,
  1022. content => $content,
  1023. );
  1024. }
  1025. else {
  1026. error("htmlization of $type not supported");
  1027. }
  1028. run_hooks(sanitize => sub {
  1029. $content=shift->(
  1030. page => $page,
  1031. destpage => $destpage,
  1032. content => $content,
  1033. );
  1034. });
  1035. if ($oneline) {
  1036. # hack to get rid of enclosing junk added by markdown
  1037. # and other htmlizers
  1038. $content=~s/^<p>//i;
  1039. $content=~s/<\/p>$//i;
  1040. chomp $content;
  1041. }
  1042. return $content;
  1043. }
  1044. sub linkify ($$$) {
  1045. my $page=shift;
  1046. my $destpage=shift;
  1047. my $content=shift;
  1048. run_hooks(linkify => sub {
  1049. $content=shift->(
  1050. page => $page,
  1051. destpage => $destpage,
  1052. content => $content,
  1053. );
  1054. });
  1055. return $content;
  1056. }
  1057. our %preprocessing;
  1058. our $preprocess_preview=0;
  1059. sub preprocess ($$$;$$) {
  1060. my $page=shift; # the page the data comes from
  1061. my $destpage=shift; # the page the data will appear in (different for inline)
  1062. my $content=shift;
  1063. my $scan=shift;
  1064. my $preview=shift;
  1065. # Using local because it needs to be set within any nested calls
  1066. # of this function.
  1067. local $preprocess_preview=$preview if defined $preview;
  1068. my $handle=sub {
  1069. my $escape=shift;
  1070. my $prefix=shift;
  1071. my $command=shift;
  1072. my $params=shift;
  1073. $params="" if ! defined $params;
  1074. if (length $escape) {
  1075. return "[[$prefix$command $params]]";
  1076. }
  1077. elsif (exists $hooks{preprocess}{$command}) {
  1078. return "" if $scan && ! $hooks{preprocess}{$command}{scan};
  1079. # Note: preserve order of params, some plugins may
  1080. # consider it significant.
  1081. my @params;
  1082. while ($params =~ m{
  1083. (?:([-\w]+)=)? # 1: named parameter key?
  1084. (?:
  1085. """(.*?)""" # 2: triple-quoted value
  1086. |
  1087. "([^"]+)" # 3: single-quoted value
  1088. |
  1089. (\S+) # 4: unquoted value
  1090. )
  1091. (?:\s+|$) # delimiter to next param
  1092. }sgx) {
  1093. my $key=$1;
  1094. my $val;
  1095. if (defined $2) {
  1096. $val=$2;
  1097. $val=~s/\r\n/\n/mg;
  1098. $val=~s/^\n+//g;
  1099. $val=~s/\n+$//g;
  1100. }
  1101. elsif (defined $3) {
  1102. $val=$3;
  1103. }
  1104. elsif (defined $4) {
  1105. $val=$4;
  1106. }
  1107. if (defined $key) {
  1108. push @params, $key, $val;
  1109. }
  1110. else {
  1111. push @params, $val, '';
  1112. }
  1113. }
  1114. if ($preprocessing{$page}++ > 3) {
  1115. # Avoid loops of preprocessed pages preprocessing
  1116. # other pages that preprocess them, etc.
  1117. return "[[!$command <span class=\"error\">".
  1118. sprintf(gettext("preprocessing loop detected on %s at depth %i"),
  1119. $page, $preprocessing{$page}).
  1120. "</span>]]";
  1121. }
  1122. my $ret;
  1123. if (! $scan) {
  1124. $ret=eval {
  1125. $hooks{preprocess}{$command}{call}->(
  1126. @params,
  1127. page => $page,
  1128. destpage => $destpage,
  1129. preview => $preprocess_preview,
  1130. );
  1131. };
  1132. if ($@) {
  1133. my $error=$@;
  1134. chomp $error;
  1135. $ret="[[!$command <span class=\"error\">".
  1136. gettext("Error").": $error"."</span>]]";
  1137. }
  1138. }
  1139. else {
  1140. # use void context during scan pass
  1141. eval {
  1142. $hooks{preprocess}{$command}{call}->(
  1143. @params,
  1144. page => $page,
  1145. destpage => $destpage,
  1146. preview => $preprocess_preview,
  1147. );
  1148. };
  1149. $ret="";
  1150. }
  1151. $preprocessing{$page}--;
  1152. return $ret;
  1153. }
  1154. else {
  1155. return "[[$prefix$command $params]]";
  1156. }
  1157. };
  1158. my $regex;
  1159. if ($config{prefix_directives}) {
  1160. $regex = qr{
  1161. (\\?) # 1: escape?
  1162. \[\[(!) # directive open; 2: prefix
  1163. ([-\w]+) # 3: command
  1164. ( # 4: the parameters..
  1165. \s+ # Must have space if parameters present
  1166. (?:
  1167. (?:[-\w]+=)? # named parameter key?
  1168. (?:
  1169. """.*?""" # triple-quoted value
  1170. |
  1171. "[^"]+" # single-quoted value
  1172. |
  1173. [^"\s\]]+ # unquoted value
  1174. )
  1175. \s* # whitespace or end
  1176. # of directive
  1177. )
  1178. *)? # 0 or more parameters
  1179. \]\] # directive closed
  1180. }sx;
  1181. }
  1182. else {
  1183. $regex = qr{
  1184. (\\?) # 1: escape?
  1185. \[\[(!?) # directive open; 2: optional prefix
  1186. ([-\w]+) # 3: command
  1187. \s+
  1188. ( # 4: the parameters..
  1189. (?:
  1190. (?:[-\w]+=)? # named parameter key?
  1191. (?:
  1192. """.*?""" # triple-quoted value
  1193. |
  1194. "[^"]+" # single-quoted value
  1195. |
  1196. [^"\s\]]+ # unquoted value
  1197. )
  1198. \s* # whitespace or end
  1199. # of directive
  1200. )
  1201. *) # 0 or more parameters
  1202. \]\] # directive closed
  1203. }sx;
  1204. }
  1205. $content =~ s{$regex}{$handle->($1, $2, $3, $4)}eg;
  1206. return $content;
  1207. }
  1208. sub filter ($$$) {
  1209. my $page=shift;
  1210. my $destpage=shift;
  1211. my $content=shift;
  1212. run_hooks(filter => sub {
  1213. $content=shift->(page => $page, destpage => $destpage,
  1214. content => $content);
  1215. });
  1216. return $content;
  1217. }
  1218. sub indexlink () {
  1219. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  1220. }
  1221. sub check_canedit ($$$;$) {
  1222. my $page=shift;
  1223. my $q=shift;
  1224. my $session=shift;
  1225. my $nonfatal=shift;
  1226. my $canedit;
  1227. run_hooks(canedit => sub {
  1228. return if defined $canedit;
  1229. my $ret=shift->($page, $q, $session);
  1230. if (defined $ret) {
  1231. if ($ret eq "") {
  1232. $canedit=1;
  1233. }
  1234. elsif (ref $ret eq 'CODE') {
  1235. $ret->() unless $nonfatal;
  1236. $canedit=0;
  1237. }
  1238. elsif (defined $ret) {
  1239. error($ret) unless $nonfatal;
  1240. $canedit=0;
  1241. }
  1242. }
  1243. });
  1244. return defined $canedit ? $canedit : 1;
  1245. }
  1246. sub check_content (@) {
  1247. my %params=@_;
  1248. return 1 if ! exists $hooks{checkcontent}; # optimisation
  1249. if (exists $pagesources{$params{page}}) {
  1250. my @diff;
  1251. my %old=map { $_ => 1 }
  1252. split("\n", readfile(srcfile($pagesources{$params{page}})));
  1253. foreach my $line (split("\n", $params{content})) {
  1254. push @diff, $line if ! exists $old{$_};
  1255. }
  1256. $params{diff}=join("\n", @diff);
  1257. }
  1258. my $ok;
  1259. run_hooks(checkcontent => sub {
  1260. return if defined $ok;
  1261. my $ret=shift->(%params);
  1262. if (defined $ret) {
  1263. if ($ret eq "") {
  1264. $ok=1;
  1265. }
  1266. elsif (ref $ret eq 'CODE') {
  1267. $ret->() unless $params{nonfatal};
  1268. $ok=0;
  1269. }
  1270. elsif (defined $ret) {
  1271. error($ret) unless $params{nonfatal};
  1272. $ok=0;
  1273. }
  1274. }
  1275. });
  1276. return defined $ok ? $ok : 1;
  1277. }
  1278. my $wikilock;
  1279. sub lockwiki () {
  1280. # Take an exclusive lock on the wiki to prevent multiple concurrent
  1281. # run issues. The lock will be dropped on program exit.
  1282. if (! -d $config{wikistatedir}) {
  1283. mkdir($config{wikistatedir});
  1284. }
  1285. open($wikilock, '>', "$config{wikistatedir}/lockfile") ||
  1286. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  1287. if (! flock($wikilock, 2)) { # LOCK_EX
  1288. error("failed to get lock");
  1289. }
  1290. return 1;
  1291. }
  1292. sub unlockwiki () {
  1293. POSIX::close($ENV{IKIWIKI_CGILOCK_FD}) if exists $ENV{IKIWIKI_CGILOCK_FD};
  1294. return close($wikilock) if $wikilock;
  1295. return;
  1296. }
  1297. my $commitlock;
  1298. sub commit_hook_enabled () {
  1299. open($commitlock, '+>', "$config{wikistatedir}/commitlock") ||
  1300. error("cannot write to $config{wikistatedir}/commitlock: $!");
  1301. if (! flock($commitlock, 1 | 4)) { # LOCK_SH | LOCK_NB to test
  1302. close($commitlock) || error("failed closing commitlock: $!");
  1303. return 0;
  1304. }
  1305. close($commitlock) || error("failed closing commitlock: $!");
  1306. return 1;
  1307. }
  1308. sub disable_commit_hook () {
  1309. open($commitlock, '>', "$config{wikistatedir}/commitlock") ||
  1310. error("cannot write to $config{wikistatedir}/commitlock: $!");
  1311. if (! flock($commitlock, 2)) { # LOCK_EX
  1312. error("failed to get commit lock");
  1313. }
  1314. return 1;
  1315. }
  1316. sub enable_commit_hook () {
  1317. return close($commitlock) if $commitlock;
  1318. return;
  1319. }
  1320. sub loadindex () {
  1321. %oldrenderedfiles=%pagectime=();
  1322. if (! $config{rebuild}) {
  1323. %pagesources=%pagemtime=%oldlinks=%links=%depends=
  1324. %destsources=%renderedfiles=%pagecase=%pagestate=
  1325. %depends_simple=();
  1326. }
  1327. my $in;
  1328. if (! open ($in, "<", "$config{wikistatedir}/indexdb")) {
  1329. if (-e "$config{wikistatedir}/index") {
  1330. system("ikiwiki-transition", "indexdb", $config{srcdir});
  1331. open ($in, "<", "$config{wikistatedir}/indexdb") || return;
  1332. }
  1333. else {
  1334. return;
  1335. }
  1336. }
  1337. my $index=Storable::fd_retrieve($in);
  1338. if (! defined $index) {
  1339. return 0;
  1340. }
  1341. my $pages;
  1342. if (exists $index->{version} && ! ref $index->{version}) {
  1343. $pages=$index->{page};
  1344. %wikistate=%{$index->{state}};
  1345. }
  1346. else {
  1347. $pages=$index;
  1348. %wikistate=();
  1349. }
  1350. foreach my $src (keys %$pages) {
  1351. my $d=$pages->{$src};
  1352. my $page=pagename($src);
  1353. $pagectime{$page}=$d->{ctime};
  1354. if (! $config{rebuild}) {
  1355. $pagesources{$page}=$src;
  1356. $pagemtime{$page}=$d->{mtime};
  1357. $renderedfiles{$page}=$d->{dest};
  1358. if (exists $d->{links} && ref $d->{links}) {
  1359. $links{$page}=$d->{links};
  1360. $oldlinks{$page}=[@{$d->{links}}];
  1361. }
  1362. if (exists $d->{depends_simple}) {
  1363. $depends_simple{$page}={
  1364. map { $_ => 1 } @{$d->{depends_simple}}
  1365. };
  1366. }
  1367. if (exists $d->{dependslist}) {
  1368. $depends{$page}={
  1369. map { $_ => 1 } @{$d->{dependslist}}
  1370. };
  1371. }
  1372. elsif (exists $d->{depends}) {
  1373. $depends{$page}={$d->{depends} => 1};
  1374. }
  1375. if (exists $d->{state}) {
  1376. $pagestate{$page}=$d->{state};
  1377. }
  1378. }
  1379. $oldrenderedfiles{$page}=[@{$d->{dest}}];
  1380. }
  1381. foreach my $page (keys %pagesources) {
  1382. $pagecase{lc $page}=$page;
  1383. }
  1384. foreach my $page (keys %renderedfiles) {
  1385. $destsources{$_}=$page foreach @{$renderedfiles{$page}};
  1386. }
  1387. return close($in);
  1388. }
  1389. sub saveindex () {
  1390. run_hooks(savestate => sub { shift->() });
  1391. my %hookids;
  1392. foreach my $type (keys %hooks) {
  1393. $hookids{$_}=1 foreach keys %{$hooks{$type}};
  1394. }
  1395. my @hookids=keys %hookids;
  1396. if (! -d $config{wikistatedir}) {
  1397. mkdir($config{wikistatedir});
  1398. }
  1399. my $newfile="$config{wikistatedir}/indexdb.new";
  1400. my $cleanup = sub { unlink($newfile) };
  1401. open (my $out, '>', $newfile) || error("cannot write to $newfile: $!", $cleanup);
  1402. my %index;
  1403. foreach my $page (keys %pagemtime) {
  1404. next unless $pagemtime{$page};
  1405. my $src=$pagesources{$page};
  1406. $index{page}{$src}={
  1407. ctime => $pagectime{$page},
  1408. mtime => $pagemtime{$page},
  1409. dest => $renderedfiles{$page},
  1410. links => $links{$page},
  1411. };
  1412. if (exists $depends{$page}) {
  1413. $index{page}{$src}{dependslist} = [ keys %{$depends{$page}} ];
  1414. }
  1415. if (exists $depends_simple{$page}) {
  1416. $index{page}{$src}{depends_simple} = [ keys %{$depends_simple{$page}} ];
  1417. }
  1418. if (exists $pagestate{$page}) {
  1419. foreach my $id (@hookids) {
  1420. foreach my $key (keys %{$pagestate{$page}{$id}}) {
  1421. $index{page}{$src}{state}{$id}{$key}=$pagestate{$page}{$id}{$key};
  1422. }
  1423. }
  1424. }
  1425. }
  1426. $index{state}={};
  1427. foreach my $id (@hookids) {
  1428. foreach my $key (keys %{$wikistate{$id}}) {
  1429. $index{state}{$id}{$key}=$wikistate{$id}{$key};
  1430. }
  1431. }
  1432. $index{version}="3";
  1433. my $ret=Storable::nstore_fd(\%index, $out);
  1434. return if ! defined $ret || ! $ret;
  1435. close $out || error("failed saving to $newfile: $!", $cleanup);
  1436. rename($newfile, "$config{wikistatedir}/indexdb") ||
  1437. error("failed renaming $newfile to $config{wikistatedir}/indexdb", $cleanup);
  1438. return 1;
  1439. }
  1440. sub template_file ($) {
  1441. my $template=shift;
  1442. foreach my $dir ($config{templatedir}, @{$config{templatedirs}},
  1443. "$installdir/share/ikiwiki/templates") {
  1444. return "$dir/$template" if -e "$dir/$template";
  1445. }
  1446. return;
  1447. }
  1448. sub template_params (@) {
  1449. my $filename=template_file(shift);
  1450. if (! defined $filename) {
  1451. return if wantarray;
  1452. return "";
  1453. }
  1454. my @ret=(
  1455. filter => sub {
  1456. my $text_ref = shift;
  1457. ${$text_ref} = decode_utf8(${$text_ref});
  1458. },
  1459. filename => $filename,
  1460. loop_context_vars => 1,
  1461. die_on_bad_params => 0,
  1462. @_
  1463. );
  1464. return wantarray ? @ret : {@ret};
  1465. }
  1466. sub template ($;@) {
  1467. require HTML::Template;
  1468. return HTML::Template->new(template_params(@_));
  1469. }
  1470. sub misctemplate ($$;@) {
  1471. my $title=shift;
  1472. my $pagebody=shift;
  1473. my $template=template("misc.tmpl");
  1474. $template->param(
  1475. title => $title,
  1476. indexlink => indexlink(),
  1477. wikiname => $config{wikiname},
  1478. pagebody => $pagebody,
  1479. baseurl => baseurl(),
  1480. @_,
  1481. );
  1482. run_hooks(pagetemplate => sub {
  1483. shift->(page => "", destpage => "", template => $template);
  1484. });
  1485. return $template->output;
  1486. }
  1487. sub hook (@) {
  1488. my %param=@_;
  1489. if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) {
  1490. error 'hook requires type, call, and id parameters';
  1491. }
  1492. return if $param{no_override} && exists $hooks{$param{type}}{$param{id}};
  1493. $hooks{$param{type}}{$param{id}}=\%param;
  1494. return 1;
  1495. }
  1496. sub run_hooks ($$) {
  1497. # Calls the given sub for each hook of the given type,
  1498. # passing it the hook function to call.
  1499. my $type=shift;
  1500. my $sub=shift;
  1501. if (exists $hooks{$type}) {
  1502. my (@first, @middle, @last);
  1503. foreach my $id (keys %{$hooks{$type}}) {
  1504. if ($hooks{$type}{$id}{first}) {
  1505. push @first, $id;
  1506. }
  1507. elsif ($hooks{$type}{$id}{last}) {
  1508. push @last, $id;
  1509. }
  1510. else {
  1511. push @middle, $id;
  1512. }
  1513. }
  1514. foreach my $id (@first, @middle, @last) {
  1515. $sub->($hooks{$type}{$id}{call});
  1516. }
  1517. }
  1518. return 1;
  1519. }
  1520. sub rcs_update () {
  1521. $hooks{rcs}{rcs_update}{call}->(@_);
  1522. }
  1523. sub rcs_prepedit ($) {
  1524. $hooks{rcs}{rcs_prepedit}{call}->(@_);
  1525. }
  1526. sub rcs_commit ($$$;$$) {
  1527. $hooks{rcs}{rcs_commit}{call}->(@_);
  1528. }
  1529. sub rcs_commit_staged ($$$) {
  1530. $hooks{rcs}{rcs_commit_staged}{call}->(@_);
  1531. }
  1532. sub rcs_add ($) {
  1533. $hooks{rcs}{rcs_add}{call}->(@_);
  1534. }
  1535. sub rcs_remove ($) {
  1536. $hooks{rcs}{rcs_remove}{call}->(@_);
  1537. }
  1538. sub rcs_rename ($$) {
  1539. $hooks{rcs}{rcs_rename}{call}->(@_);
  1540. }
  1541. sub rcs_recentchanges ($) {
  1542. $hooks{rcs}{rcs_recentchanges}{call}->(@_);
  1543. }
  1544. sub rcs_diff ($) {
  1545. $hooks{rcs}{rcs_diff}{call}->(@_);
  1546. }
  1547. sub rcs_getctime ($) {
  1548. $hooks{rcs}{rcs_getctime}{call}->(@_);
  1549. }
  1550. sub rcs_receive () {
  1551. $hooks{rcs}{rcs_receive}{call}->();
  1552. }
  1553. sub add_depends ($$) {
  1554. my $page=shift;
  1555. my $pagespec=shift;
  1556. if ($pagespec =~ /$config{wiki_file_regexp}/ &&
  1557. $pagespec !~ /[\s*?()!]/) {
  1558. # a simple dependency, which can be matched by string eq
  1559. $depends_simple{$page}{lc $pagespec} = 1;
  1560. return 1;
  1561. }
  1562. return unless pagespec_valid($pagespec);
  1563. $depends{$page}{$pagespec} = 1;
  1564. return 1;
  1565. }
  1566. sub file_pruned ($;$) {
  1567. my $file=shift;
  1568. if (@_) {
  1569. require File::Spec;
  1570. $file=File::Spec->canonpath($file);
  1571. my $base=File::Spec->canonpath(shift);
  1572. return if $file eq $base;
  1573. $file =~ s#^\Q$base\E/+##;
  1574. }
  1575. my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')';
  1576. return $file =~ m/$regexp/;
  1577. }
  1578. sub define_gettext () {
  1579. # If translation is needed, redefine the gettext function to do it.
  1580. # Otherwise, it becomes a quick no-op.
  1581. no warnings 'redefine';
  1582. if ((exists $ENV{LANG} && length $ENV{LANG}) ||
  1583. (exists $ENV{LC_ALL} && length $ENV{LC_ALL}) ||
  1584. (exists $ENV{LC_MESSAGES} && length $ENV{LC_MESSAGES})) {
  1585. *gettext=sub {
  1586. my $gettext_obj=eval q{
  1587. use Locale::gettext q{textdomain};
  1588. Locale::gettext->domain('ikiwiki')
  1589. };
  1590. if ($gettext_obj) {
  1591. $gettext_obj->get(shift);
  1592. }
  1593. else {
  1594. return shift;
  1595. }
  1596. };
  1597. }
  1598. else {
  1599. *gettext=sub { return shift };
  1600. }
  1601. }
  1602. sub gettext {
  1603. define_gettext();
  1604. gettext(@_);
  1605. }
  1606. sub yesno ($) {
  1607. my $val=shift;
  1608. return (defined $val && (lc($val) eq gettext("yes") || lc($val) eq "yes" || $val eq "1"));
  1609. }
  1610. sub inject {
  1611. # Injects a new function into the symbol table to replace an
  1612. # exported function.
  1613. my %params=@_;
  1614. # This is deep ugly perl foo, beware.
  1615. no strict;
  1616. no warnings;
  1617. if (! defined $params{parent}) {
  1618. $params{parent}='::';
  1619. $params{old}=\&{$params{name}};
  1620. $params{name}=~s/.*:://;
  1621. }
  1622. my $parent=$params{parent};
  1623. foreach my $ns (grep /^\w+::/, keys %{$parent}) {
  1624. $ns = $params{parent} . $ns;
  1625. inject(%params, parent => $ns) unless $ns eq '::main::';
  1626. *{$ns . $params{name}} = $params{call}
  1627. if exists ${$ns}{$params{name}} &&
  1628. \&{${$ns}{$params{name}}} == $params{old};
  1629. }
  1630. use strict;
  1631. use warnings;
  1632. }
  1633. sub add_link ($$) {
  1634. my $page=shift;
  1635. my $link=shift;
  1636. push @{$links{$page}}, $link
  1637. unless grep { $_ eq $link } @{$links{$page}};
  1638. }
  1639. sub pagespec_translate ($) {
  1640. my $spec=shift;
  1641. # Convert spec to perl code.
  1642. my $code="";
  1643. my @data;
  1644. while ($spec=~m{
  1645. \s* # ignore whitespace
  1646. ( # 1: match a single word
  1647. \! # !
  1648. |
  1649. \( # (
  1650. |
  1651. \) # )
  1652. |
  1653. \w+\([^\)]*\) # command(params)
  1654. |
  1655. [^\s()]+ # any other text
  1656. )
  1657. \s* # ignore whitespace
  1658. }gx) {
  1659. my $word=$1;
  1660. if (lc $word eq 'and') {
  1661. $code.=' &&';
  1662. }
  1663. elsif (lc $word eq 'or') {
  1664. $code.=' ||';
  1665. }
  1666. elsif ($word eq "(" || $word eq ")" || $word eq "!") {
  1667. $code.=' '.$word;
  1668. }
  1669. elsif ($word =~ /^(\w+)\((.*)\)$/) {
  1670. if (exists $IkiWiki::PageSpec::{"match_$1"}) {
  1671. push @data, $2;
  1672. $code.="IkiWiki::PageSpec::match_$1(\$page, \$data[$#data], \@_)";
  1673. }
  1674. else {
  1675. push @data, qq{unknown function in pagespec "$word"};
  1676. $code.="IkiWiki::ErrorReason->new(\$data[$#data])";
  1677. }
  1678. }
  1679. else {
  1680. push @data, $word;
  1681. $code.=" IkiWiki::PageSpec::match_glob(\$page, \$data[$#data], \@_)";
  1682. }
  1683. }
  1684. if (! length $code) {
  1685. $code="IkiWiki::FailReason->new('empty pagespec')";
  1686. }
  1687. no warnings;
  1688. return eval 'sub { my $page=shift; '.$code.' }';
  1689. }
  1690. sub pagespec_match ($$;@) {
  1691. my $page=shift;
  1692. my $spec=shift;
  1693. my @params=@_;
  1694. # Backwards compatability with old calling convention.
  1695. if (@params == 1) {
  1696. unshift @params, 'location';
  1697. }
  1698. my $sub=pagespec_translate($spec);
  1699. return IkiWiki::ErrorReason->new("syntax error in pagespec \"$spec\"")
  1700. if $@ || ! defined $sub;
  1701. return $sub->($page, @params);
  1702. }
  1703. sub pagespec_match_list ($$;@) {
  1704. my $pages=shift;
  1705. my $spec=shift;
  1706. my @params=@_;
  1707. my $sub=pagespec_translate($spec);
  1708. error "syntax error in pagespec \"$spec\""
  1709. if $@ || ! defined $sub;
  1710. my @ret;
  1711. my $r;
  1712. foreach my $page (@$pages) {
  1713. $r=$sub->($page, @params);
  1714. push @ret, $page if $r;
  1715. }
  1716. if (! @ret && defined $r && $r->isa("IkiWiki::ErrorReason")) {
  1717. error(sprintf(gettext("cannot match pages: %s"), $r));
  1718. }
  1719. else {
  1720. return @ret;
  1721. }
  1722. }
  1723. sub pagespec_valid ($) {
  1724. my $spec=shift;
  1725. my $sub=pagespec_translate($spec);
  1726. return ! $@;
  1727. }
  1728. sub glob2re ($) {
  1729. my $re=quotemeta(shift);
  1730. $re=~s/\\\*/.*/g;
  1731. $re=~s/\\\?/./g;
  1732. return $re;
  1733. }
  1734. package IkiWiki::FailReason;
  1735. use overload (
  1736. '""' => sub { ${$_[0]} },
  1737. '0+' => sub { 0 },
  1738. '!' => sub { bless $_[0], 'IkiWiki::SuccessReason'},
  1739. fallback => 1,
  1740. );
  1741. sub new {
  1742. my $class = shift;
  1743. my $value = shift;
  1744. return bless \$value, $class;
  1745. }
  1746. package IkiWiki::ErrorReason;
  1747. our @ISA = 'IkiWiki::FailReason';
  1748. package IkiWiki::SuccessReason;
  1749. use overload (
  1750. '""' => sub { ${$_[0]} },
  1751. '0+' => sub { 1 },
  1752. '!' => sub { bless $_[0], 'IkiWiki::FailReason'},
  1753. fallback => 1,
  1754. );
  1755. sub new {
  1756. my $class = shift;
  1757. my $value = shift;
  1758. return bless \$value, $class;
  1759. };
  1760. package IkiWiki::PageSpec;
  1761. sub derel ($$) {
  1762. my $path=shift;
  1763. my $from=shift;
  1764. if ($path =~ m!^\./!) {
  1765. $from=~s#/?[^/]+$## if defined $from;
  1766. $path=~s#^\./##;
  1767. $path="$from/$path" if length $from;
  1768. }
  1769. return $path;
  1770. }
  1771. sub match_glob ($$;@) {
  1772. my $page=shift;
  1773. my $glob=shift;
  1774. my %params=@_;
  1775. $glob=derel($glob, $params{location});
  1776. my $regexp=IkiWiki::glob2re($glob);
  1777. if ($page=~/^$regexp$/i) {
  1778. if (! IkiWiki::isinternal($page) || $params{internal}) {
  1779. return IkiWiki::SuccessReason->new("$glob matches $page");
  1780. }
  1781. else {
  1782. return IkiWiki::FailReason->new("$glob matches $page, but the page is an internal page");
  1783. }
  1784. }
  1785. else {
  1786. return IkiWiki::FailReason->new("$glob does not match $page");
  1787. }
  1788. }
  1789. sub match_internal ($$;@) {
  1790. return match_glob($_[0], $_[1], @_, internal => 1)
  1791. }
  1792. sub match_link ($$;@) {
  1793. my $page=shift;
  1794. my $link=lc(shift);
  1795. my %params=@_;
  1796. $link=derel($link, $params{location});
  1797. my $from=exists $params{location} ? $params{location} : '';
  1798. my $links = $IkiWiki::links{$page};
  1799. return IkiWiki::FailReason->new("$page has no links") unless $links && @{$links};
  1800. my $bestlink = IkiWiki::bestlink($from, $link);
  1801. foreach my $p (@{$links}) {
  1802. if (length $bestlink) {
  1803. return IkiWiki::SuccessReason->new("$page links to $link")
  1804. if $bestlink eq IkiWiki::bestlink($page, $p);
  1805. }
  1806. else {
  1807. return IkiWiki::SuccessReason->new("$page links to page $p matching $link")
  1808. if match_glob($p, $link, %params);
  1809. my ($p_rel)=$p=~/^\/?(.*)/;
  1810. $link=~s/^\///;
  1811. return IkiWiki::SuccessReason->new("$page links to page $p_rel matching $link")
  1812. if match_glob($p_rel, $link, %params);
  1813. }
  1814. }
  1815. return IkiWiki::FailReason->new("$page does not link to $link");
  1816. }
  1817. sub match_backlink ($$;@) {
  1818. return match_link($_[1], $_[0], @_);
  1819. }
  1820. sub match_created_before ($$;@) {
  1821. my $page=shift;
  1822. my $testpage=shift;
  1823. my %params=@_;
  1824. $testpage=derel($testpage, $params{location});
  1825. if (exists $IkiWiki::pagectime{$testpage}) {
  1826. if ($IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}) {
  1827. return IkiWiki::SuccessReason->new("$page created before $testpage");
  1828. }
  1829. else {
  1830. return IkiWiki::FailReason->new("$page not created before $testpage");
  1831. }
  1832. }
  1833. else {
  1834. return IkiWiki::ErrorReason->new("$testpage does not exist");
  1835. }
  1836. }
  1837. sub match_created_after ($$;@) {
  1838. my $page=shift;
  1839. my $testpage=shift;
  1840. my %params=@_;
  1841. $testpage=derel($testpage, $params{location});
  1842. if (exists $IkiWiki::pagectime{$testpage}) {
  1843. if ($IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}) {
  1844. return IkiWiki::SuccessReason->new("$page created after $testpage");
  1845. }
  1846. else {
  1847. return IkiWiki::FailReason->new("$page not created after $testpage");
  1848. }
  1849. }
  1850. else {
  1851. return IkiWiki::ErrorReason->new("$testpage does not exist");
  1852. }
  1853. }
  1854. sub match_creation_day ($$;@) {
  1855. if ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift) {
  1856. return IkiWiki::SuccessReason->new('creation_day matched');
  1857. }
  1858. else {
  1859. return IkiWiki::FailReason->new('creation_day did not match');
  1860. }
  1861. }
  1862. sub match_creation_month ($$;@) {
  1863. if ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift) {
  1864. return IkiWiki::SuccessReason->new('creation_month matched');
  1865. }
  1866. else {
  1867. return IkiWiki::FailReason->new('creation_month did not match');
  1868. }
  1869. }
  1870. sub match_creation_year ($$;@) {
  1871. if ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift) {
  1872. return IkiWiki::SuccessReason->new('creation_year matched');
  1873. }
  1874. else {
  1875. return IkiWiki::FailReason->new('creation_year did not match');
  1876. }
  1877. }
  1878. sub match_user ($$;@) {
  1879. shift;
  1880. my $user=shift;
  1881. my %params=@_;
  1882. if (! exists $params{user}) {
  1883. return IkiWiki::ErrorReason->new("no user specified");
  1884. }
  1885. if (defined $params{user} && lc $params{user} eq lc $user) {
  1886. return IkiWiki::SuccessReason->new("user is $user");
  1887. }
  1888. elsif (! defined $params{user}) {
  1889. return IkiWiki::FailReason->new("not logged in");
  1890. }
  1891. else {
  1892. return IkiWiki::FailReason->new("user is $params{user}, not $user");
  1893. }
  1894. }
  1895. sub match_admin ($$;@) {
  1896. shift;
  1897. shift;
  1898. my %params=@_;
  1899. if (! exists $params{user}) {
  1900. return IkiWiki::ErrorReason->new("no user specified");
  1901. }
  1902. if (defined $params{user} && IkiWiki::is_admin($params{user})) {
  1903. return IkiWiki::SuccessReason->new("user is an admin");
  1904. }
  1905. elsif (! defined $params{user}) {
  1906. return IkiWiki::FailReason->new("not logged in");
  1907. }
  1908. else {
  1909. return IkiWiki::FailReason->new("user is not an admin");
  1910. }
  1911. }
  1912. sub match_ip ($$;@) {
  1913. shift;
  1914. my $ip=shift;
  1915. my %params=@_;
  1916. if (! exists $params{ip}) {
  1917. return IkiWiki::ErrorReason->new("no IP specified");
  1918. }
  1919. if (defined $params{ip} && lc $params{ip} eq lc $ip) {
  1920. return IkiWiki::SuccessReason->new("IP is $ip");
  1921. }
  1922. else {
  1923. return IkiWiki::FailReason->new("IP is $params{ip}, not $ip");
  1924. }
  1925. }
  1926. 1