summaryrefslogtreecommitdiff
path: root/IkiWiki/CGI.pm
blob: 5fccfb4741d4375760777a530916fa535e0273c2 (plain)
  1. #!/usr/bin/perl
  2. use warnings;
  3. use strict;
  4. use IkiWiki;
  5. use IkiWiki::UserInfo;
  6. use open qw{:utf8 :std};
  7. use Encode;
  8. package IkiWiki;
  9. sub printheader ($) { #{{{
  10. my $session=shift;
  11. if ($config{sslcookie}) {
  12. print $session->header(-charset => 'utf-8',
  13. -cookie => $session->cookie(-secure => 1));
  14. } else {
  15. print $session->header(-charset => 'utf-8');
  16. }
  17. } #}}}
  18. sub showform ($$$$;@) { #{{{
  19. my $form=shift;
  20. my $buttons=shift;
  21. my $session=shift;
  22. my $cgi=shift;
  23. if (exists $hooks{formbuilder}) {
  24. run_hooks(formbuilder => sub {
  25. shift->(form => $form, cgi => $cgi, session => $session,
  26. buttons => $buttons);
  27. });
  28. }
  29. printheader($session);
  30. print misctemplate($form->title, $form->render(submit => $buttons), @_);
  31. }
  32. sub redirect ($$) { #{{{
  33. my $q=shift;
  34. my $url=shift;
  35. if (! $config{w3mmode}) {
  36. print $q->redirect($url);
  37. }
  38. else {
  39. print "Content-type: text/plain\n";
  40. print "W3m-control: GOTO $url\n\n";
  41. }
  42. } #}}}
  43. sub check_canedit ($$$;$) { #{{{
  44. my $page=shift;
  45. my $q=shift;
  46. my $session=shift;
  47. my $nonfatal=shift;
  48. my $canedit;
  49. run_hooks(canedit => sub {
  50. return if defined $canedit;
  51. my $ret=shift->($page, $q, $session);
  52. if (defined $ret) {
  53. if ($ret eq "") {
  54. $canedit=1;
  55. }
  56. elsif (ref $ret eq 'CODE') {
  57. $ret->() unless $nonfatal;
  58. $canedit=0;
  59. }
  60. elsif (defined $ret) {
  61. error($ret) unless $nonfatal;
  62. $canedit=0;
  63. }
  64. }
  65. });
  66. return $canedit;
  67. } #}}}
  68. sub decode_form_utf8 ($) { #{{{
  69. my $form = shift;
  70. foreach my $f ($form->field) {
  71. $form->field(name => $f,
  72. value => decode_utf8($form->field($f)),
  73. force => 1,
  74. );
  75. }
  76. } #}}}
  77. # Check if the user is signed in. If not, redirect to the signin form and
  78. # save their place to return to later.
  79. sub needsignin ($$) { #{{{
  80. my $q=shift;
  81. my $session=shift;
  82. if (! defined $session->param("name") ||
  83. ! userinfo_get($session->param("name"), "regdate")) {
  84. $session->param(postsignin => $ENV{QUERY_STRING});
  85. cgi_signin($q, $session);
  86. cgi_savesession($session);
  87. exit;
  88. }
  89. } #}}}
  90. sub cgi_signin ($$) { #{{{
  91. my $q=shift;
  92. my $session=shift;
  93. eval q{use CGI::FormBuilder};
  94. error($@) if $@;
  95. my $form = CGI::FormBuilder->new(
  96. title => "signin",
  97. name => "signin",
  98. charset => "utf-8",
  99. method => 'POST',
  100. required => 'NONE',
  101. javascript => 0,
  102. params => $q,
  103. action => $config{cgiurl},
  104. header => 0,
  105. template => {type => 'div'},
  106. stylesheet => baseurl()."style.css",
  107. );
  108. my $buttons=["Login"];
  109. if ($q->param("do") ne "signin" && !$form->submitted) {
  110. $form->text(gettext("You need to log in first."));
  111. }
  112. $form->field(name => "do", type => "hidden", value => "signin",
  113. force => 1);
  114. decode_form_utf8($form);
  115. run_hooks(formbuilder_setup => sub {
  116. shift->(form => $form, cgi => $q, session => $session,
  117. buttons => $buttons);
  118. });
  119. decode_form_utf8($form);
  120. if ($form->submitted) {
  121. $form->validate;
  122. }
  123. showform($form, $buttons, $session, $q);
  124. } #}}}
  125. sub cgi_postsignin ($$) { #{{{
  126. my $q=shift;
  127. my $session=shift;
  128. # Continue with whatever was being done before the signin process.
  129. if (defined $session->param("postsignin")) {
  130. my $postsignin=CGI->new($session->param("postsignin"));
  131. $session->clear("postsignin");
  132. cgi($postsignin, $session);
  133. cgi_savesession($session);
  134. exit;
  135. }
  136. else {
  137. error(gettext("login failed, perhaps you need to turn on cookies?"));
  138. }
  139. } #}}}
  140. sub cgi_prefs ($$) { #{{{
  141. my $q=shift;
  142. my $session=shift;
  143. needsignin($q, $session);
  144. # The session id is stored on the form and checked to
  145. # guard against CSRF.
  146. my $sid=$q->param('sid');
  147. if (! defined $sid) {
  148. $q->delete_all;
  149. }
  150. elsif ($sid ne $session->id) {
  151. error(gettext("Your login session has expired."));
  152. }
  153. eval q{use CGI::FormBuilder};
  154. error($@) if $@;
  155. my $form = CGI::FormBuilder->new(
  156. title => "preferences",
  157. name => "preferences",
  158. header => 0,
  159. charset => "utf-8",
  160. method => 'POST',
  161. validate => {
  162. email => 'EMAIL',
  163. },
  164. required => 'NONE',
  165. javascript => 0,
  166. params => $q,
  167. action => $config{cgiurl},
  168. template => {type => 'div'},
  169. stylesheet => baseurl()."style.css",
  170. fieldsets => [
  171. [login => gettext("Login")],
  172. [preferences => gettext("Preferences")],
  173. [admin => gettext("Admin")]
  174. ],
  175. );
  176. my $buttons=["Save Preferences", "Logout", "Cancel"];
  177. decode_form_utf8($form);
  178. run_hooks(formbuilder_setup => sub {
  179. shift->(form => $form, cgi => $q, session => $session,
  180. buttons => $buttons);
  181. });
  182. decode_form_utf8($form);
  183. $form->field(name => "do", type => "hidden", value => "prefs",
  184. force => 1);
  185. $form->field(name => "sid", type => "hidden", value => $session->id,
  186. force => 1);
  187. $form->field(name => "email", size => 50, fieldset => "preferences");
  188. $form->field(name => "banned_users", size => 50,
  189. fieldset => "admin");
  190. my $user_name=$session->param("name");
  191. if (! is_admin($user_name)) {
  192. $form->field(name => "banned_users", type => "hidden");
  193. }
  194. if (! $form->submitted) {
  195. $form->field(name => "email", force => 1,
  196. value => userinfo_get($user_name, "email"));
  197. if (is_admin($user_name)) {
  198. $form->field(name => "banned_users", force => 1,
  199. value => join(" ", get_banned_users()));
  200. }
  201. }
  202. if ($form->submitted eq 'Logout') {
  203. $session->delete();
  204. redirect($q, $config{url});
  205. return;
  206. }
  207. elsif ($form->submitted eq 'Cancel') {
  208. redirect($q, $config{url});
  209. return;
  210. }
  211. elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
  212. if (defined $form->field('email')) {
  213. userinfo_set($user_name, 'email', $form->field('email')) ||
  214. error("failed to set email");
  215. }
  216. if (is_admin($user_name)) {
  217. set_banned_users(grep { ! is_admin($_) }
  218. split(' ',
  219. $form->field("banned_users"))) ||
  220. error("failed saving changes");
  221. }
  222. $form->text(gettext("Preferences saved."));
  223. }
  224. showform($form, $buttons, $session, $q);
  225. } #}}}
  226. sub cgi_editpage ($$) { #{{{
  227. my $q=shift;
  228. my $session=shift;
  229. my @fields=qw(do rcsinfo subpage from page type editcontent comments);
  230. my @buttons=("Save Page", "Preview", "Cancel");
  231. eval q{use CGI::FormBuilder};
  232. error($@) if $@;
  233. my $form = CGI::FormBuilder->new(
  234. title => "editpage",
  235. fields => \@fields,
  236. charset => "utf-8",
  237. method => 'POST',
  238. required => [qw{editcontent}],
  239. javascript => 0,
  240. params => $q,
  241. action => $config{cgiurl},
  242. header => 0,
  243. table => 0,
  244. template => scalar template_params("editpage.tmpl"),
  245. wikiname => $config{wikiname},
  246. );
  247. decode_form_utf8($form);
  248. run_hooks(formbuilder_setup => sub {
  249. shift->(form => $form, cgi => $q, session => $session,
  250. buttons => \@buttons);
  251. });
  252. decode_form_utf8($form);
  253. # This untaint is safe because titlepage removes any problematic
  254. # characters.
  255. my ($page)=$form->field('page');
  256. $page=titlepage(possibly_foolish_untaint($page));
  257. if (! defined $page || ! length $page ||
  258. file_pruned($page, $config{srcdir}) || $page=~/^\//) {
  259. error("bad page name");
  260. }
  261. my $baseurl=$config{url}."/".htmlpage($page);
  262. my $from;
  263. if (defined $form->field('from')) {
  264. ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
  265. }
  266. my $file;
  267. my $type;
  268. if (exists $pagesources{$page} && $form->field("do") ne "create") {
  269. $file=$pagesources{$page};
  270. $type=pagetype($file);
  271. if (! defined $type || $type=~/^_/) {
  272. error(sprintf(gettext("%s is not an editable page"), $page));
  273. }
  274. if (! $form->submitted) {
  275. $form->field(name => "rcsinfo",
  276. value => rcs_prepedit($file), force => 1);
  277. }
  278. $form->field(name => "editcontent", validate => '/.*/');
  279. }
  280. else {
  281. $type=$form->param('type');
  282. if (defined $type && length $type && $hooks{htmlize}{$type}) {
  283. $type=possibly_foolish_untaint($type);
  284. }
  285. elsif (defined $from && exists $pagesources{$from}) {
  286. # favor the type of linking page
  287. $type=pagetype($pagesources{$from});
  288. }
  289. $type=$config{default_pageext} unless defined $type;
  290. $file=$page.".".$type;
  291. if (! $form->submitted) {
  292. $form->field(name => "rcsinfo", value => "", force => 1);
  293. }
  294. $form->field(name => "editcontent", validate => '/.+/');
  295. }
  296. $form->field(name => "do", type => 'hidden');
  297. $form->field(name => "sid", type => "hidden", value => $session->id,
  298. force => 1);
  299. $form->field(name => "from", type => 'hidden');
  300. $form->field(name => "rcsinfo", type => 'hidden');
  301. $form->field(name => "subpage", type => 'hidden');
  302. $form->field(name => "page", value => pagetitle($page, 1), force => 1);
  303. $form->field(name => "type", value => $type, force => 1);
  304. $form->field(name => "comments", type => "text", size => 80);
  305. $form->field(name => "editcontent", type => "textarea", rows => 20,
  306. cols => 80);
  307. $form->tmpl_param("can_commit", $config{rcs});
  308. $form->tmpl_param("indexlink", indexlink());
  309. $form->tmpl_param("helponformattinglink",
  310. htmllink($page, $page, "ikiwiki/formatting",
  311. noimageinline => 1,
  312. linktext => "FormattingHelp"));
  313. if ($form->submitted eq "Cancel") {
  314. if ($form->field("do") eq "create" && defined $from) {
  315. redirect($q, "$config{url}/".htmlpage($from));
  316. }
  317. elsif ($form->field("do") eq "create") {
  318. redirect($q, $config{url});
  319. }
  320. else {
  321. redirect($q, "$config{url}/".htmlpage($page));
  322. }
  323. return;
  324. }
  325. elsif ($form->submitted eq "Preview") {
  326. my $new=not exists $pagesources{$page};
  327. if ($new) {
  328. # temporarily record its type
  329. $pagesources{$page}=$page.".".$type;
  330. }
  331. my $content=$form->field('editcontent');
  332. run_hooks(editcontent => sub {
  333. $content=shift->(
  334. content => $content,
  335. page => $page,
  336. cgi => $q,
  337. session => $session,
  338. );
  339. });
  340. $form->tmpl_param("page_preview",
  341. htmlize($page, $type,
  342. linkify($page, $page,
  343. preprocess($page, $page,
  344. filter($page, $page, $content), 0, 1))));
  345. if ($new) {
  346. delete $pagesources{$page};
  347. }
  348. # previewing may have created files on disk
  349. saveindex();
  350. }
  351. elsif ($form->submitted eq "Save Page") {
  352. $form->tmpl_param("page_preview", "");
  353. }
  354. $form->tmpl_param("page_conflict", "");
  355. if ($form->submitted ne "Save Page" || ! $form->validate) {
  356. if ($form->field("do") eq "create") {
  357. my @page_locs;
  358. my $best_loc;
  359. if (! defined $from || ! length $from ||
  360. $from ne $form->field('from') ||
  361. file_pruned($from, $config{srcdir}) ||
  362. $from=~/^\// ||
  363. $form->submitted eq "Preview") {
  364. @page_locs=$best_loc=$page;
  365. }
  366. else {
  367. my $dir=$from."/";
  368. $dir=~s![^/]+/+$!!;
  369. if ((defined $form->field('subpage') && length $form->field('subpage')) ||
  370. $page eq gettext('discussion')) {
  371. $best_loc="$from/$page";
  372. }
  373. else {
  374. $best_loc=$dir.$page;
  375. }
  376. push @page_locs, $dir.$page;
  377. push @page_locs, "$from/$page";
  378. while (length $dir) {
  379. $dir=~s![^/]+/+$!!;
  380. push @page_locs, $dir.$page;
  381. }
  382. push @page_locs, "$config{userdir}/$page"
  383. if length $config{userdir};
  384. }
  385. @page_locs = grep {
  386. ! exists $pagecase{lc $_}
  387. } @page_locs;
  388. if (! @page_locs) {
  389. # hmm, someone else made the page in the
  390. # meantime?
  391. if ($form->submitted eq "Preview") {
  392. # let them go ahead with the edit
  393. # and resolve the conflict at save
  394. # time
  395. @page_locs=$page;
  396. }
  397. else {
  398. redirect($q, "$config{url}/".htmlpage($page));
  399. return;
  400. }
  401. }
  402. my @editable_locs = grep {
  403. check_canedit($_, $q, $session, 1)
  404. } @page_locs;
  405. if (! @editable_locs) {
  406. # let it throw an error this time
  407. map { check_canedit($_, $q, $session) } @page_locs;
  408. }
  409. my @page_types;
  410. if (exists $hooks{htmlize}) {
  411. @page_types=grep { !/^_/ }
  412. keys %{$hooks{htmlize}};
  413. }
  414. $form->tmpl_param("page_select", 1);
  415. $form->field(name => "page", type => 'select',
  416. options => [ map { pagetitle($_, 1) } @editable_locs ],
  417. value => pagetitle($best_loc, 1));
  418. $form->field(name => "type", type => 'select',
  419. options => \@page_types);
  420. $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
  421. }
  422. elsif ($form->field("do") eq "edit") {
  423. check_canedit($page, $q, $session);
  424. if (! defined $form->field('editcontent') ||
  425. ! length $form->field('editcontent')) {
  426. my $content="";
  427. if (exists $pagesources{$page}) {
  428. $content=readfile(srcfile($pagesources{$page}));
  429. $content=~s/\n/\r\n/g;
  430. }
  431. $form->field(name => "editcontent", value => $content,
  432. force => 1);
  433. }
  434. $form->tmpl_param("page_select", 0);
  435. $form->field(name => "page", type => 'hidden');
  436. $form->field(name => "type", type => 'hidden');
  437. $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
  438. }
  439. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  440. }
  441. else {
  442. # save page
  443. check_canedit($page, $q, $session);
  444. # The session id is stored on the form and checked to
  445. # guard against CSRF. But only if the user is logged in,
  446. # as anonok can allow anonymous edits.
  447. if (defined $session->param("name")) {
  448. my $sid=$q->param('sid');
  449. if (! defined $sid || $sid ne $session->id) {
  450. error(gettext("Your login session has expired."));
  451. }
  452. }
  453. my $exists=-e "$config{srcdir}/$file";
  454. if ($form->field("do") ne "create" && ! $exists &&
  455. ! defined srcfile($file, 1)) {
  456. $form->tmpl_param("page_gone", 1);
  457. $form->field(name => "do", value => "create", force => 1);
  458. $form->tmpl_param("page_select", 0);
  459. $form->field(name => "page", type => 'hidden');
  460. $form->field(name => "type", type => 'hidden');
  461. $form->title(sprintf(gettext("editing %s"), $page));
  462. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  463. return;
  464. }
  465. elsif ($form->field("do") eq "create" && $exists) {
  466. $form->tmpl_param("creation_conflict", 1);
  467. $form->field(name => "do", value => "edit", force => 1);
  468. $form->tmpl_param("page_select", 0);
  469. $form->field(name => "page", type => 'hidden');
  470. $form->field(name => "type", type => 'hidden');
  471. $form->title(sprintf(gettext("editing %s"), $page));
  472. $form->field("editcontent",
  473. value => readfile("$config{srcdir}/$file").
  474. "\n\n\n".$form->field("editcontent"),
  475. force => 1);
  476. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  477. return;
  478. }
  479. my $content=$form->field('editcontent');
  480. run_hooks(editcontent => sub {
  481. $content=shift->(
  482. content => $content,
  483. page => $page,
  484. cgi => $q,
  485. session => $session,
  486. );
  487. });
  488. $content=~s/\r\n/\n/g;
  489. $content=~s/\r/\n/g;
  490. $content.="\n" if $content !~ /\n$/;
  491. $config{cgi}=0; # avoid cgi error message
  492. eval { writefile($file, $config{srcdir}, $content) };
  493. $config{cgi}=1;
  494. if ($@) {
  495. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  496. force => 1);
  497. $form->tmpl_param("failed_save", 1);
  498. $form->tmpl_param("error_message", $@);
  499. $form->field("editcontent", value => $content, force => 1);
  500. $form->tmpl_param("page_select", 0);
  501. $form->field(name => "page", type => 'hidden');
  502. $form->field(name => "type", type => 'hidden');
  503. $form->title(sprintf(gettext("editing %s"), $page));
  504. showform($form, \@buttons, $session, $q,
  505. forcebaseurl => $baseurl);
  506. return;
  507. }
  508. my $conflict;
  509. if ($config{rcs}) {
  510. my $message="";
  511. if (defined $form->field('comments') &&
  512. length $form->field('comments')) {
  513. $message=$form->field('comments');
  514. }
  515. if (! $exists) {
  516. rcs_add($file);
  517. }
  518. # Prevent deadlock with post-commit hook by
  519. # signaling to it that it should not try to
  520. # do anything.
  521. disable_commit_hook();
  522. $conflict=rcs_commit($file, $message,
  523. $form->field("rcsinfo"),
  524. $session->param("name"), $ENV{REMOTE_ADDR});
  525. enable_commit_hook();
  526. rcs_update();
  527. }
  528. # Refresh even if there was a conflict, since other changes
  529. # may have been committed while the post-commit hook was
  530. # disabled.
  531. require IkiWiki::Render;
  532. refresh();
  533. saveindex();
  534. if (defined $conflict) {
  535. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  536. force => 1);
  537. $form->tmpl_param("page_conflict", 1);
  538. $form->field("editcontent", value => $conflict, force => 1);
  539. $form->field("do", "edit", force => 1);
  540. $form->tmpl_param("page_select", 0);
  541. $form->field(name => "page", type => 'hidden');
  542. $form->field(name => "type", type => 'hidden');
  543. $form->title(sprintf(gettext("editing %s"), $page));
  544. showform($form, \@buttons, $session, $q,
  545. forcebaseurl => $baseurl);
  546. return;
  547. }
  548. else {
  549. # The trailing question mark tries to avoid broken
  550. # caches and get the most recent version of the page.
  551. redirect($q, "$config{url}/".htmlpage($page)."?updated");
  552. }
  553. }
  554. } #}}}
  555. sub cgi_getsession ($) { #{{{
  556. my $q=shift;
  557. eval q{use CGI::Session};
  558. CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
  559. my $oldmask=umask(077);
  560. my $session = CGI::Session->new("driver:DB_File", $q,
  561. { FileName => "$config{wikistatedir}/sessions.db" });
  562. umask($oldmask);
  563. return $session;
  564. } #}}}
  565. sub cgi_savesession ($) { #{{{
  566. my $session=shift;
  567. # Force session flush with safe umask.
  568. my $oldmask=umask(077);
  569. $session->flush;
  570. umask($oldmask);
  571. } #}}}
  572. sub cgi (;$$) { #{{{
  573. my $q=shift;
  574. my $session=shift;
  575. if (! $q) {
  576. eval q{use CGI};
  577. error($@) if $@;
  578. binmode(STDIN);
  579. $q=CGI->new;
  580. binmode(STDIN, ":utf8");
  581. run_hooks(cgi => sub { shift->($q) });
  582. }
  583. my $do=$q->param('do');
  584. if (! defined $do || ! length $do) {
  585. my $error = $q->cgi_error;
  586. if ($error) {
  587. error("Request not processed: $error");
  588. }
  589. else {
  590. error("\"do\" parameter missing");
  591. }
  592. }
  593. # Need to lock the wiki before getting a session.
  594. lockwiki();
  595. loadindex();
  596. if (! $session) {
  597. $session=cgi_getsession($q);
  598. }
  599. # Auth hooks can sign a user in.
  600. if ($do ne 'signin' && ! defined $session->param("name")) {
  601. run_hooks(auth => sub {
  602. shift->($q, $session)
  603. });
  604. if (defined $session->param("name")) {
  605. # Make sure whatever user was authed is in the
  606. # userinfo db.
  607. if (! userinfo_get($session->param("name"), "regdate")) {
  608. userinfo_setall($session->param("name"), {
  609. email => "",
  610. password => "",
  611. regdate => time,
  612. }) || error("failed adding user");
  613. }
  614. }
  615. }
  616. if (defined $session->param("name") &&
  617. userinfo_get($session->param("name"), "banned")) {
  618. print $q->header(-status => "403 Forbidden");
  619. $session->delete();
  620. print gettext("You are banned.");
  621. cgi_savesession($session);
  622. }
  623. run_hooks(sessioncgi => sub { shift->($q, $session) });
  624. if ($do eq 'signin') {
  625. cgi_signin($q, $session);
  626. cgi_savesession($session);
  627. }
  628. elsif ($do eq 'prefs') {
  629. cgi_prefs($q, $session);
  630. }
  631. elsif ($do eq 'create' || $do eq 'edit') {
  632. cgi_editpage($q, $session);
  633. }
  634. elsif (defined $session->param("postsignin") || $do eq 'postsignin') {
  635. cgi_postsignin($q, $session);
  636. }
  637. else {
  638. error("unknown do parameter");
  639. }
  640. } #}}}
  641. 1