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