summaryrefslogtreecommitdiff
path: root/ikiwiki
blob: b59aa8c8fa6c1cdfbe3504f01dd2b18c15f5b863 (plain)
  1. #!/usr/bin/perl -T
  2. $ENV{PATH}="/usr/local/bin:/usr/bin:/bin";
  3. use lib '.'; # For use without installation, removed by Makefile.
  4. package IkiWiki;
  5. use warnings;
  6. use strict;
  7. use Memoize;
  8. use File::Spec;
  9. use HTML::Template;
  10. use vars qw{%config %links %oldlinks %oldpagemtime %renderedfiles %pagesources};
  11. # Holds global config settings, also used by some modules.
  12. our %config=( #{{{
  13. wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.html?$)},
  14. wiki_link_regexp => qr/\[\[([^\s\]]+)\]\]/,
  15. wiki_file_regexp => qr/(^[-A-Za-z0-9_.:\/+]+$)/,
  16. verbose => 0,
  17. wikiname => "wiki",
  18. default_pageext => ".mdwn",
  19. cgi => 0,
  20. svn => 1,
  21. url => '',
  22. cgiurl => '',
  23. historyurl => '',
  24. diffurl => '',
  25. anonok => 0,
  26. rebuild => 0,
  27. wrapper => undef,
  28. wrappermode => undef,
  29. srcdir => undef,
  30. destdir => undef,
  31. templatedir => "/usr/share/ikiwiki/templates",
  32. setup => undef,
  33. adminuser => undef,
  34. ); #}}}
  35. # option parsing #{{{
  36. if (! exists $ENV{WRAPPED_OPTIONS}) {
  37. eval q{use Getopt::Long};
  38. GetOptions(
  39. "setup|s=s" => \$config{setup},
  40. "wikiname=s" => \$config{wikiname},
  41. "verbose|v!" => \$config{verbose},
  42. "rebuild!" => \$config{rebuild},
  43. "wrapper:s" => sub { $config{wrapper}=$_[1] ? $_[1] : "ikiwiki-wrap" },
  44. "wrappermode=i" => \$config{wrappermode},
  45. "svn!" => \$config{svn},
  46. "anonok!" => \$config{anonok},
  47. "cgi!" => \$config{cgi},
  48. "url=s" => \$config{url},
  49. "cgiurl=s" => \$config{cgiurl},
  50. "historyurl=s" => \$config{historyurl},
  51. "diffurl=s" => \$config{diffurl},
  52. "exclude=s@" => sub {
  53. $config{wiki_file_prune_regexp}=qr/$config{wiki_file_prune_regexp}|$_[1]/;
  54. },
  55. "adminuser=s@" => sub { push @{$config{adminuser}}, $_[1] },
  56. "templatedir=s" => sub { $config{templatedir}=possibly_foolish_untaint($_[1]) },
  57. ) || usage();
  58. if (! $config{setup}) {
  59. usage() unless @ARGV == 2;
  60. $config{srcdir} = possibly_foolish_untaint(shift);
  61. $config{destdir} = possibly_foolish_untaint(shift);
  62. checkoptions();
  63. }
  64. }
  65. else {
  66. # wrapper passes a full config structure in the environment
  67. # variable
  68. eval possibly_foolish_untaint($ENV{WRAPPED_OPTIONS});
  69. checkoptions();
  70. }
  71. #}}}
  72. sub checkoptions { #{{{
  73. if ($config{cgi} && ! length $config{url}) {
  74. error("Must specify url to wiki with --url when using --cgi");
  75. }
  76. $config{wikistatedir}="$config{srcdir}/.ikiwiki"
  77. unless exists $config{wikistatedir};
  78. if ($config{svn}) {
  79. require IkiWiki::RCS::SVN;
  80. $config{rcs}=1;
  81. }
  82. else {
  83. require IkiWiki::RCS::Stub;
  84. $config{rcs}=0;
  85. }
  86. } #}}}
  87. sub usage { #{{{
  88. die "usage: ikiwiki [options] source dest\n";
  89. } #}}}
  90. sub error { #{{{
  91. if ($config{cgi}) {
  92. print "Content-type: text/html\n\n";
  93. print misctemplate("Error", "<p>Error: @_</p>");
  94. }
  95. die @_;
  96. } #}}}
  97. sub debug ($) { #{{{
  98. return unless $config{verbose};
  99. if (! $config{cgi}) {
  100. print "@_\n";
  101. }
  102. else {
  103. print STDERR "@_\n";
  104. }
  105. } #}}}
  106. sub possibly_foolish_untaint { #{{{
  107. my $tainted=shift;
  108. my ($untainted)=$tainted=~/(.*)/;
  109. return $untainted;
  110. } #}}}
  111. sub basename ($) { #{{{
  112. my $file=shift;
  113. $file=~s!.*/!!;
  114. return $file;
  115. } #}}}
  116. sub dirname ($) { #{{{
  117. my $file=shift;
  118. $file=~s!/?[^/]+$!!;
  119. return $file;
  120. } #}}}
  121. sub pagetype ($) { #{{{
  122. my $page=shift;
  123. if ($page =~ /\.mdwn$/) {
  124. return ".mdwn";
  125. }
  126. else {
  127. return "unknown";
  128. }
  129. } #}}}
  130. sub pagename ($) { #{{{
  131. my $file=shift;
  132. my $type=pagetype($file);
  133. my $page=$file;
  134. $page=~s/\Q$type\E*$// unless $type eq 'unknown';
  135. return $page;
  136. } #}}}
  137. sub htmlpage ($) { #{{{
  138. my $page=shift;
  139. return $page.".html";
  140. } #}}}
  141. sub readfile ($) { #{{{
  142. my $file=shift;
  143. if (-l $file) {
  144. error("cannot read a symlink ($file)");
  145. }
  146. local $/=undef;
  147. open (IN, "$file") || error("failed to read $file: $!");
  148. my $ret=<IN>;
  149. close IN;
  150. return $ret;
  151. } #}}}
  152. sub writefile ($$) { #{{{
  153. my $file=shift;
  154. my $content=shift;
  155. if (-l $file) {
  156. error("cannot write to a symlink ($file)");
  157. }
  158. my $dir=dirname($file);
  159. if (! -d $dir) {
  160. my $d="";
  161. foreach my $s (split(m!/+!, $dir)) {
  162. $d.="$s/";
  163. if (! -d $d) {
  164. mkdir($d) || error("failed to create directory $d: $!");
  165. }
  166. }
  167. }
  168. open (OUT, ">$file") || error("failed to write $file: $!");
  169. print OUT $content;
  170. close OUT;
  171. } #}}}
  172. sub bestlink ($$) { #{{{
  173. # Given a page and the text of a link on the page, determine which
  174. # existing page that link best points to. Prefers pages under a
  175. # subdirectory with the same name as the source page, failing that
  176. # goes down the directory tree to the base looking for matching
  177. # pages.
  178. my $page=shift;
  179. my $link=lc(shift);
  180. my $cwd=$page;
  181. do {
  182. my $l=$cwd;
  183. $l.="/" if length $l;
  184. $l.=$link;
  185. if (exists $links{$l}) {
  186. #debug("for $page, \"$link\", use $l");
  187. return $l;
  188. }
  189. } while $cwd=~s!/?[^/]+$!!;
  190. #print STDERR "warning: page $page, broken link: $link\n";
  191. return "";
  192. } #}}}
  193. sub isinlinableimage ($) { #{{{
  194. my $file=shift;
  195. $file=~/\.(png|gif|jpg|jpeg)$/;
  196. } #}}}
  197. sub htmllink { #{{{
  198. my $page=shift;
  199. my $link=shift;
  200. my $noimageinline=shift; # don't turn links into inline html images
  201. my $forcesubpage=shift; # force a link to a subpage
  202. my $bestlink;
  203. if (! $forcesubpage) {
  204. $bestlink=bestlink($page, $link);
  205. }
  206. else {
  207. $bestlink="$page/".lc($link);
  208. }
  209. return $link if length $bestlink && $page eq $bestlink;
  210. # TODO BUG: %renderedfiles may not have it, if the linked to page
  211. # was also added and isn't yet rendered! Note that this bug is
  212. # masked by the bug mentioned below that makes all new files
  213. # be rendered twice.
  214. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  215. $bestlink=htmlpage($bestlink);
  216. }
  217. if (! grep { $_ eq $bestlink } values %renderedfiles) {
  218. return "<a href=\"$config{cgiurl}?do=create&page=$link&from=$page\">?</a>$link"
  219. }
  220. $bestlink=File::Spec->abs2rel($bestlink, dirname($page));
  221. if (! $noimageinline && isinlinableimage($bestlink)) {
  222. return "<img src=\"$bestlink\">";
  223. }
  224. return "<a href=\"$bestlink\">$link</a>";
  225. } #}}}
  226. sub indexlink () { #{{{
  227. return "<a href=\"$config{url}\">$config{wikiname}</a>";
  228. } #}}}
  229. sub lockwiki () { #{{{
  230. # Take an exclusive lock on the wiki to prevent multiple concurrent
  231. # run issues. The lock will be dropped on program exit.
  232. if (! -d $config{wikistatedir}) {
  233. mkdir($config{wikistatedir});
  234. }
  235. open(WIKILOCK, ">$config{wikistatedir}/lockfile") ||
  236. error ("cannot write to $config{wikistatedir}/lockfile: $!");
  237. if (! flock(WIKILOCK, 2 | 4)) {
  238. debug("wiki seems to be locked, waiting for lock");
  239. my $wait=600; # arbitrary, but don't hang forever to
  240. # prevent process pileup
  241. for (1..600) {
  242. return if flock(WIKILOCK, 2 | 4);
  243. sleep 1;
  244. }
  245. error("wiki is locked; waited $wait seconds without lock being freed (possible stuck process or stale lock?)");
  246. }
  247. } #}}}
  248. sub unlockwiki () { #{{{
  249. close WIKILOCK;
  250. } #}}}
  251. sub loadindex () { #{{{
  252. open (IN, "$config{wikistatedir}/index") || return;
  253. while (<IN>) {
  254. $_=possibly_foolish_untaint($_);
  255. chomp;
  256. my ($mtime, $file, $rendered, @links)=split(' ', $_);
  257. my $page=pagename($file);
  258. $pagesources{$page}=$file;
  259. $oldpagemtime{$page}=$mtime;
  260. $oldlinks{$page}=[@links];
  261. $links{$page}=[@links];
  262. $renderedfiles{$page}=$rendered;
  263. }
  264. close IN;
  265. } #}}}
  266. sub saveindex () { #{{{
  267. if (! -d $config{wikistatedir}) {
  268. mkdir($config{wikistatedir});
  269. }
  270. open (OUT, ">$config{wikistatedir}/index") ||
  271. error("cannot write to $config{wikistatedir}/index: $!");
  272. foreach my $page (keys %oldpagemtime) {
  273. print OUT "$oldpagemtime{$page} $pagesources{$page} $renderedfiles{$page} ".
  274. join(" ", @{$links{$page}})."\n"
  275. if $oldpagemtime{$page};
  276. }
  277. close OUT;
  278. } #}}}
  279. sub misctemplate ($$) { #{{{
  280. my $title=shift;
  281. my $pagebody=shift;
  282. my $template=HTML::Template->new(
  283. filename => "$config{templatedir}/misc.tmpl"
  284. );
  285. $template->param(
  286. title => $title,
  287. indexlink => indexlink(),
  288. wikiname => $config{wikiname},
  289. pagebody => $pagebody,
  290. );
  291. return $template->output;
  292. }#}}}
  293. sub userinfo_get ($$) { #{{{
  294. my $user=shift;
  295. my $field=shift;
  296. eval q{use Storable};
  297. my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
  298. if (! defined $userdata || ! ref $userdata ||
  299. ! exists $userdata->{$user} || ! ref $userdata->{$user} ||
  300. ! exists $userdata->{$user}->{$field}) {
  301. return "";
  302. }
  303. return $userdata->{$user}->{$field};
  304. } #}}}
  305. sub userinfo_set ($$$) { #{{{
  306. my $user=shift;
  307. my $field=shift;
  308. my $value=shift;
  309. eval q{use Storable};
  310. my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
  311. if (! defined $userdata || ! ref $userdata ||
  312. ! exists $userdata->{$user} || ! ref $userdata->{$user}) {
  313. return "";
  314. }
  315. $userdata->{$user}->{$field}=$value;
  316. my $oldmask=umask(077);
  317. my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
  318. umask($oldmask);
  319. return $ret;
  320. } #}}}
  321. sub userinfo_setall ($$) { #{{{
  322. my $user=shift;
  323. my $info=shift;
  324. eval q{use Storable};
  325. my $userdata=eval{ Storable::lock_retrieve("$config{wikistatedir}/userdb") };
  326. if (! defined $userdata || ! ref $userdata) {
  327. $userdata={};
  328. }
  329. $userdata->{$user}=$info;
  330. my $oldmask=umask(077);
  331. my $ret=Storable::lock_store($userdata, "$config{wikistatedir}/userdb");
  332. umask($oldmask);
  333. return $ret;
  334. } #}}}
  335. sub is_admin ($) { #{{{
  336. my $user_name=shift;
  337. return grep { $_ eq $user_name } @{$config{adminuser}};
  338. } #}}}
  339. sub glob_match ($$) { #{{{
  340. my $page=shift;
  341. my $glob=shift;
  342. # turn glob into safe regexp
  343. $glob=quotemeta($glob);
  344. $glob=~s/\\\*/.*/g;
  345. $glob=~s/\\\?/./g;
  346. $glob=~s!\\/!/!g;
  347. $page=~/^$glob$/i;
  348. } #}}}
  349. sub globlist_match ($$) { #{{{
  350. my $page=shift;
  351. my @globlist=split(" ", shift);
  352. # check any negated globs first
  353. foreach my $glob (@globlist) {
  354. return 0 if $glob=~/^!(.*)/ && glob_match($page, $1);
  355. }
  356. foreach my $glob (@globlist) {
  357. return 1 if glob_match($page, $glob);
  358. }
  359. return 0;
  360. } #}}}
  361. # main {{{
  362. memoize('pagename');
  363. memoize('bestlink');
  364. if ($config{setup}) {
  365. require IkiWiki::Setup;
  366. setup();
  367. }
  368. lockwiki();
  369. if ($config{wrapper}) {
  370. require IkiWiki::Wrapper;
  371. gen_wrapper();
  372. exit;
  373. }
  374. loadindex() unless $config{rebuild};
  375. if ($config{cgi}) {
  376. require IkiWiki::CGI;
  377. cgi();
  378. }
  379. else {
  380. require IkiWiki::Render;
  381. rcs_update();
  382. refresh();
  383. saveindex();
  384. }
  385. #}}}