summaryrefslogtreecommitdiff
path: root/ikiwiki
blob: 2dbb42bc483a042d3231685fc57c9ede74bea9d0 (plain)
  1. #!/usr/bin/perl -T
  2. $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
  3. package IkiWiki;
  4. use warnings;
  5. use strict;
  6. use File::Spec;
  7. use HTML::Template;
  8. use lib '.'; # For use without installation, removed by Makefile.
  9. use vars qw{%config %links %oldlinks %oldpagemtime %pagectime
  10. %renderedfiles %pagesources %inlinepages};
  11. sub usage () { #{{{
  12. die "usage: ikiwiki [options] source dest\n";
  13. } #}}}
  14. sub getconfig () { #{{{
  15. if (! exists $ENV{WRAPPED_OPTIONS}) {
  16. %config=(
  17. wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$|\.rss$)},
  18. wiki_link_regexp => qr/\[\[(?:([^\s\]\|]+)\|)?([^\s\]]+)\]\]/,
  19. wiki_processor_regexp => qr/\[\[(\w+)\s+([^\]]+)\]\]/,
  20. wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/,
  21. verbose => 0,
  22. wikiname => "wiki",
  23. default_pageext => ".mdwn",
  24. cgi => 0,
  25. svn => 1,
  26. notify => 0,
  27. url => '',
  28. cgiurl => '',
  29. historyurl => '',
  30. diffurl => '',
  31. anonok => 0,
  32. rss => 0,
  33. rebuild => 0,
  34. refresh => 0,
  35. getctime => 0,
  36. hyperestraier => 0,
  37. wrapper => undef,
  38. wrappermode => undef,
  39. svnrepo => undef,
  40. svnpath => "trunk",
  41. srcdir => undef,
  42. destdir => undef,
  43. templatedir => "/usr/share/ikiwiki/templates",
  44. underlaydir => "/usr/share/ikiwiki/basewiki",
  45. setup => undef,
  46. adminuser => undef,
  47. adminemail => undef,
  48. );
  49. eval q{use Getopt::Long};
  50. GetOptions(
  51. "setup|s=s" => \$config{setup},
  52. "wikiname=s" => \$config{wikiname},
  53. "verbose|v!" => \$config{verbose},
  54. "rebuild!" => \$config{rebuild},
  55. "refresh!" => \$config{refresh},
  56. "getctime" => \$config{getctime},
  57. "wrappermode=i" => \$config{wrappermode},
  58. "svn!" => \$config{svn},
  59. "anonok!" => \$config{anonok},
  60. "hyperestraier" => \$config{hyperestraier},
  61. "rss!" => \$config{rss},
  62. "cgi!" => \$config{cgi},
  63. "notify!" => \$config{notify},
  64. "url=s" => \$config{url},
  65. "cgiurl=s" => \$config{cgiurl},
  66. "historyurl=s" => \$config{historyurl},
  67. "diffurl=s" => \$config{diffurl},
  68. "svnrepo" => \$config{svnrepo},
  69. "svnpath" => \$config{svnpath},
  70. "adminemail=s" => \$config{adminemail},
  71. "exclude=s@" => sub {
  72. $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
  73. },
  74. "adminuser=s@" => sub {
  75. push @{$config{adminuser}}, $_[1]
  76. },
  77. "templatedir=s" => sub {
  78. $config{templatedir}=possibly_foolish_untaint($_[1])
  79. },
  80. "underlaydir=s" => sub {
  81. $config{underlaydir}=possibly_foolish_untaint($_[1])
  82. },
  83. "wrapper:s" => sub {
  84. $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap"
  85. },
  86. ) || usage();
  87. if (! $config{setup}) {
  88. usage() unless @ARGV == 2;
  89. $config{srcdir} = possibly_foolish_untaint(shift @ARGV);
  90. $config{destdir} = possibly_foolish_untaint(shift @ARGV);
  91. checkconfig();
  92. }
  93. }
  94. else {
  95. # wrapper passes a full config structure in the environment
  96. # variable
  97. eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS});
  98. checkconfig();
  99. }
  100. } #}}}
  101. sub checkconfig () { #{{{
  102. if ($config{cgi} && ! length $config{url}) {
  103. error("Must specify url to wiki with --url when using --cgi\n");
  104. }
  105. if ($config{rss} && ! length $config{url}) {
  106. error("Must specify url to wiki with --url when using --rss\n");
  107. }
  108. if ($config{hyperestraier} && ! length $config{url}) {
  109. error("Must specify --url when using --hyperestraier\n");
  110. }
  111. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  112. unless exists $config{wikistatedir};
  113. if ($config{svn}) {
  114. require IkiWiki::Rcs::SVN;
  115. $config{rcs}=1;
  116. }
  117. else {
  118. require IkiWiki::Rcs::Stub;
  119. $config{rcs}=0;
  120. }
  121. } #}}}
  122. sub error ($) { #{{{
  123. if ($config{cgi}) {
  124. print "Content-type: text/html\n\n";
  125. print misctemplate("Error", "<p>Error: @_</p>");
  126. }
  127. die @_;
  128. } #}}}
  129. sub possibly_foolish_untaint ($) { #{{{
  130. my $tainted=shift;
  131. my ($untainted)=$tainted=~/(.*)/;
  132. return $untainted;
  133. } #}}}
  134. sub debug ($) { #{{{
  135. return unless $config{verbose};
  136. if (! $config{cgi}) {
  137. print "@_\n";
  138. }
  139. else {
  140. print STDERR "@_\n";
  141. }
  142. } #}}}
  143. sub basename ($) { #{{{
  144. my $file=shift;
  145. $file=~s!.*/+!!;
  146. return $file;
  147. } #}}}
  148. sub dirname ($) { #{{{
  149. my $file=shift;
  150. $file=~s!/*[^/]+$!!;
  151. return $file;
  152. } #}}}
  153. sub pagetype ($) { #{{{
  154. my $page=shift;
  155. if ($page =~ /\.mdwn$/) {
  156. return ".mdwn";
  157. }
  158. else {
  159. return "unknown";
  160. }
  161. } #}}}
  162. sub pagename ($) { #{{{
  163. my $file=shift;
  164. my $type=pagetype($file);
  165. my $page=$file;
  166. $page=~s/\Q$type\E*$// unless $type eq 'unknown';
  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=lc(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. #debug("for $page, \"$link\", use $l");
  234. return $l;
  235. }
  236. } while $cwd=~s!/?[^/]+$!!;
  237. #print STDERR "warning: page $page, broken link: $link\n";
  238. return "";
  239. } #}}}
  240. sub isinlinableimage ($) { #{{{
  241. my $file=shift;
  242. $file=~/\.(png|gif|jpg|jpeg)$/i;
  243. } #}}}
  244. sub pagetitle ($) { #{{{
  245. my $page=shift;
  246. $page=~s/__(\d+)__/&#$1;/g;
  247. $page=~y/_/ /;
  248. return $page;
  249. } #}}}
  250. sub titlepage ($) { #{{{
  251. my $title=shift;
  252. $title=~y/ /_/;
  253. $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg;
  254. return $title;
  255. } #}}}
  256. sub cgiurl (@) { #{{{
  257. my %params=@_;
  258. return $config{cgiurl}."?".join("&amp;", map "$_=$params{$_}", keys %params);
  259. } #}}}
  260. sub styleurl (;$) { #{{{
  261. my $page=shift;
  262. return "$config{url}/style.css" if ! defined $page;
  263. $page=~s/[^\/]+$//;
  264. $page=~s/[^\/]+\//..\//g;
  265. return $page."style.css";
  266. } #}}}
  267. sub htmllink ($$;$$$) { #{{{
  268. my $page=shift;
  269. my $link=shift;
  270. my $noimageinline=shift; # don't turn links into inline html images
  271. my $forcesubpage=shift; # force a link to a subpage
  272. my $linktext=shift; # set to force the link text to something
  273. my $bestlink;
  274. if (! $forcesubpage) {
  275. $bestlink=bestlink($page, $link);
  276. }
  277. else {
  278. $bestlink="$page/".lc($link);
  279. }
  280. $linktext=pagetitle(basename($link)) unless defined $linktext;
  281. return $linktext if length $bestlink && $page eq $bestlink;
  282. # TODO BUG: %renderedfiles may not have it, if the linked to page
  283. # was also added and isn't yet rendered! Note that this bug is
  284. # masked by the bug mentioned below that makes all new files
  285. # be rendered twice.
  286. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  287. $bestlink=htmlpage($bestlink);
  288. }
  289. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  290. return "<span><a href=\"".
  291. cgiurl(do => "create", page => $link, from =>$page).
  292. "\">?</a>$linktext</span>"
  293. }
  294. $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
  295. if (! $noimageinline && isinlinableimage($bestlink)) {
  296. return "<img src=\"$bestlink\" alt=\"$linktext\" />";
  297. }
  298. return "<a href=\"$bestlink\">$linktext</a>";
  299. } #}}}
  300. sub indexlink () { #{{{
  301. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  302. } #}}}
  303. sub lockwiki () { #{{{
  304. # Take an exclusive lock on the wiki to prevent multiple concurrent
  305. # run issues. The lock will be dropped on program exit.
  306. if (! -d $config{wikistatedir}) {
  307. mkdir($config{wikistatedir});
  308. }
  309. open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
  310. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  311. if (! flock(WIKILOCK, 2 | 4)) {
  312. debug("wiki seems to be locked, waiting for lock");
  313. my $wait=600; # arbitrary, but don't hang forever to
  314. # prevent process pileup
  315. for (1..600) {
  316. return if flock(WIKILOCK, 2 | 4);
  317. sleep 1;
  318. }
  319. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  320. }
  321. } #}}}
  322. sub unlockwiki () { #{{{
  323. close WIKILOCK;
  324. } #}}}
  325. sub loadindex () { #{{{
  326. open (IN, "$config{wikistatedir}/index") || return;
  327. while (<IN>) {
  328. $_=possibly_foolish_untaint($_);
  329. chomp;
  330. my %items;
  331. $items{link}=[];
  332. foreach my $i (split(/ /, $_)) {
  333. my ($item, $val)=split(/=/, $i, 2);
  334. push @{$items{$item}}, $val;
  335. }
  336. next unless exists $items{src}; # skip bad lines for now
  337. my $page=pagename($items{src}[0]);
  338. if (! $config{rebuild}) {
  339. $pagesources{$page}=$items{src}[0];
  340. $oldpagemtime{$page}=$items{mtime}[0];
  341. $oldlinks{$page}=[@{$items{link}}];
  342. $links{$page}=[@{$items{link}}];
  343. $inlinepages{$page}=join(" ", @{$items{inlinepage}})
  344. if exists $items{inlinepage};
  345. $renderedfiles{$page}=$items{dest}[0];
  346. }
  347. $pagectime{$page}=$items{ctime}[0];
  348. }
  349. close IN;
  350. } #}}}
  351. sub saveindex () { #{{{
  352. if (! -d $config{wikistatedir}) {
  353. mkdir($config{wikistatedir});
  354. }
  355. open (OUT, ">$config{wikistatedir}/index") ||
  356. error("cannot write to $config{wikistatedir}/index: $!");
  357. foreach my $page (keys %oldpagemtime) {
  358. next unless $oldpagemtime{$page};
  359. my $line="mtime=$oldpagemtime{$page} ".
  360. "ctime=$pagectime{$page} ".
  361. "src=$pagesources{$page} ".
  362. "dest=$renderedfiles{$page}";
  363. $line.=" link=$_" foreach @{$links{$page}};
  364. if (exists $inlinepages{$page}) {
  365. $line.=" inlinepage=$_" foreach split " ", $inlinepages{$page};
  366. }
  367. print OUT $line."\n";
  368. }
  369. close OUT;
  370. } #}}}
  371. sub misctemplate ($$) { #{{{
  372. my $title=shift;
  373. my $pagebody=shift;
  374. my $template=HTML::Template->new(
  375. filename => "$config{templatedir}/misc.tmpl"
  376. );
  377. $template->param(
  378. title => $title,
  379. indexlink => indexlink(),
  380. wikiname => $config{wikiname},
  381. pagebody => $pagebody,
  382. styleurl => styleurl(),
  383. baseurl => "$config{url}/",
  384. );
  385. return $template->output;
  386. }#}}}
  387. sub glob_match ($$) { #{{{
  388. my $page=shift;
  389. my $glob=shift;
  390. # turn glob into safe regexp
  391. $glob=quotemeta($glob);
  392. $glob=~s/\\\*/.*/g;
  393. $glob=~s/\\\?/./g;
  394. $glob=~s!\\/!/!g;
  395. $page=~/^$glob$/i;
  396. } #}}}
  397. sub globlist_match ($$) { #{{{
  398. my $page=shift;
  399. my @globlist=split(" ", shift);
  400. # check any negated globs first
  401. foreach my $glob (@globlist) {
  402. return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
  403. }
  404. foreach my $glob (@globlist) {
  405. return 1 if glob_match($page, $glob);
  406. }
  407. return 0;
  408. } #}}}
  409. sub main () { #{{{
  410. getconfig();
  411. if ($config{cgi}) {
  412. lockwiki();
  413. loadindex();
  414. require IkiWiki::CGI;
  415. cgi();
  416. }
  417. elsif ($config{setup}) {
  418. require IkiWiki::Setup;
  419. setup();
  420. }
  421. elsif ($config{wrapper}) {
  422. lockwiki();
  423. require IkiWiki::Wrapper;
  424. gen_wrapper();
  425. }
  426. else {
  427. lockwiki();
  428. loadindex();
  429. require IkiWiki::Render;
  430. rcs_update();
  431. rcs_notify() if $config{notify};
  432. rcs_getctime() if $config{getctime};
  433. refresh();
  434. saveindex();
  435. }
  436. } #}}}
  437. main;