summaryrefslogtreecommitdiff
path: root/IkiWiki/CGI.pm
blob: 3b4228b75bbf84debebc38b55b07ed91cd25ecd7 (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 page_locked ($$;$) { #{{{
  10. my $page=shift;
  11. my $session=shift;
  12. my $nonfatal=shift;
  13. my $user=$session->param("name");
  14. return if defined $user && is_admin($user);
  15. foreach my $admin (@{$config{adminuser}}) {
  16. my $locked_pages=userinfo_get($admin, "locked_pages");
  17. if (globlist_match($page, userinfo_get($admin, "locked_pages"))) {
  18. return 1 if $nonfatal;
  19. error(htmllink("", "", $page, 1)." is locked by ".
  20. htmllink("", "", $admin, 1)." and cannot be edited.");
  21. }
  22. }
  23. return 0;
  24. } #}}}
  25. sub cgi_recentchanges ($) { #{{{
  26. my $q=shift;
  27. unlockwiki();
  28. # Optimisation: building recentchanges means calculating lots of
  29. # links. Memoizing htmllink speeds it up a lot (can't be memoized
  30. # during page builds as the return values may change, but they
  31. # won't here.)
  32. eval q{use Memoize};
  33. memoize("htmllink");
  34. my $template=template("recentchanges.tmpl");
  35. $template->param(
  36. title => "RecentChanges",
  37. indexlink => indexlink(),
  38. wikiname => $config{wikiname},
  39. changelog => [rcs_recentchanges(100)],
  40. styleurl => styleurl(),
  41. baseurl => "$config{url}/",
  42. );
  43. print $q->header(-charset=>'utf-8'), $template->output;
  44. } #}}}
  45. sub cgi_signin ($$) { #{{{
  46. my $q=shift;
  47. my $session=shift;
  48. eval q{use CGI::FormBuilder};
  49. my $form = CGI::FormBuilder->new(
  50. title => "signin",
  51. fields => [qw(do title page subpage from name password confirm_password email)],
  52. header => 1,
  53. charset => "utf-8",
  54. method => 'POST',
  55. validate => {
  56. confirm_password => {
  57. perl => q{eq $form->field("password")},
  58. },
  59. email => 'EMAIL',
  60. },
  61. required => 'NONE',
  62. javascript => 0,
  63. params => $q,
  64. action => $config{cgiurl},
  65. header => 0,
  66. template => (-e "$config{templatedir}/signin.tmpl" ?
  67. {template_params("signin.tmpl")} : ""),
  68. stylesheet => styleurl(),
  69. );
  70. $form->field(name => "name", required => 0);
  71. $form->field(name => "do", type => "hidden");
  72. $form->field(name => "page", type => "hidden");
  73. $form->field(name => "title", type => "hidden");
  74. $form->field(name => "from", type => "hidden");
  75. $form->field(name => "subpage", type => "hidden");
  76. $form->field(name => "password", type => "password", required => 0);
  77. $form->field(name => "confirm_password", type => "password", required => 0);
  78. $form->field(name => "email", required => 0);
  79. if ($q->param("do") ne "signin") {
  80. $form->text("You need to log in first.");
  81. }
  82. if ($form->submitted) {
  83. # Set required fields based on how form was submitted.
  84. my %required=(
  85. "Login" => [qw(name password)],
  86. "Register" => [qw(name password confirm_password email)],
  87. "Mail Password" => [qw(name)],
  88. );
  89. foreach my $opt (@{$required{$form->submitted}}) {
  90. $form->field(name => $opt, required => 1);
  91. }
  92. # Validate password differently depending on how
  93. # form was submitted.
  94. if ($form->submitted eq 'Login') {
  95. $form->field(
  96. name => "password",
  97. validate => sub {
  98. length $form->field("name") &&
  99. shift eq userinfo_get($form->field("name"), 'password');
  100. },
  101. );
  102. $form->field(name => "name", validate => '/^\w+$/');
  103. }
  104. else {
  105. $form->field(name => "password", validate => 'VALUE');
  106. }
  107. # And make sure the entered name exists when logging
  108. # in or sending email, and does not when registering.
  109. if ($form->submitted eq 'Register') {
  110. $form->field(
  111. name => "name",
  112. validate => sub {
  113. my $name=shift;
  114. length $name &&
  115. $name=~/$config{wiki_file_regexp}/ &&
  116. ! userinfo_get($name, "regdate");
  117. },
  118. );
  119. }
  120. else {
  121. $form->field(
  122. name => "name",
  123. validate => sub {
  124. my $name=shift;
  125. length $name &&
  126. userinfo_get($name, "regdate");
  127. },
  128. );
  129. }
  130. }
  131. else {
  132. # First time settings.
  133. $form->field(name => "name", comment => "use FirstnameLastName");
  134. $form->field(name => "confirm_password", comment => "(only needed");
  135. $form->field(name => "email", comment => "for registration)");
  136. if ($session->param("name")) {
  137. $form->field(name => "name", value => $session->param("name"));
  138. }
  139. }
  140. if ($form->submitted && $form->validate) {
  141. if ($form->submitted eq 'Login') {
  142. $session->param("name", $form->field("name"));
  143. if (defined $form->field("do") &&
  144. $form->field("do") ne 'signin') {
  145. print $q->redirect(cgiurl(
  146. do => $form->field("do"),
  147. page => $form->field("page"),
  148. title => $form->field("title"),
  149. subpage => $form->field("subpage"),
  150. from => $form->field("from"),
  151. ));
  152. }
  153. else {
  154. print $q->redirect($config{url});
  155. }
  156. }
  157. elsif ($form->submitted eq 'Register') {
  158. my $user_name=$form->field('name');
  159. if (userinfo_setall($user_name, {
  160. 'email' => $form->field('email'),
  161. 'password' => $form->field('password'),
  162. 'regdate' => time
  163. })) {
  164. $form->field(name => "confirm_password", type => "hidden");
  165. $form->field(name => "email", type => "hidden");
  166. $form->text("Registration successful. Now you can Login.");
  167. print $session->header(-charset=>'utf-8');
  168. print misctemplate($form->title, $form->render(submit => ["Login"]));
  169. }
  170. else {
  171. error("Error saving registration.");
  172. }
  173. }
  174. elsif ($form->submitted eq 'Mail Password') {
  175. my $user_name=$form->field("name");
  176. my $template=template("passwordmail.tmpl");
  177. $template->param(
  178. user_name => $user_name,
  179. user_password => userinfo_get($user_name, "password"),
  180. wikiurl => $config{url},
  181. wikiname => $config{wikiname},
  182. REMOTE_ADDR => $ENV{REMOTE_ADDR},
  183. );
  184. eval q{use Mail::Sendmail};
  185. sendmail(
  186. To => userinfo_get($user_name, "email"),
  187. From => "$config{wikiname} admin <$config{adminemail}>",
  188. Subject => "$config{wikiname} information",
  189. Message => $template->output,
  190. ) or error("Failed to send mail");
  191. $form->text("Your password has been emailed to you.");
  192. $form->field(name => "name", required => 0);
  193. print $session->header(-charset=>'utf-8');
  194. print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
  195. }
  196. }
  197. else {
  198. print $session->header(-charset=>'utf-8');
  199. print misctemplate($form->title, $form->render(submit => ["Login", "Register", "Mail Password"]));
  200. }
  201. } #}}}
  202. sub cgi_prefs ($$) { #{{{
  203. my $q=shift;
  204. my $session=shift;
  205. eval q{use CGI::FormBuilder};
  206. my $form = CGI::FormBuilder->new(
  207. title => "preferences",
  208. fields => [qw(do name password confirm_password email
  209. subscriptions locked_pages)],
  210. header => 0,
  211. charset => "utf-8",
  212. method => 'POST',
  213. validate => {
  214. confirm_password => {
  215. perl => q{eq $form->field("password")},
  216. },
  217. email => 'EMAIL',
  218. },
  219. required => 'NONE',
  220. javascript => 0,
  221. params => $q,
  222. action => $config{cgiurl},
  223. template => (-e "$config{templatedir}/prefs.tmpl" ?
  224. {template_params("prefs.tmpl")} : ""),
  225. stylesheet => styleurl(),
  226. );
  227. my @buttons=("Save Preferences", "Logout", "Cancel");
  228. my $user_name=$session->param("name");
  229. $form->field(name => "do", type => "hidden");
  230. $form->field(name => "name", disabled => 1,
  231. value => $user_name, force => 1);
  232. $form->field(name => "password", type => "password");
  233. $form->field(name => "confirm_password", type => "password");
  234. $form->field(name => "subscriptions", size => 50,
  235. comment => "(".htmllink("", "", "GlobList", 1).")");
  236. $form->field(name => "locked_pages", size => 50,
  237. comment => "(".htmllink("", "", "GlobList", 1).")");
  238. if (! is_admin($user_name)) {
  239. $form->field(name => "locked_pages", type => "hidden");
  240. }
  241. if (! $form->submitted) {
  242. $form->field(name => "email", force => 1,
  243. value => userinfo_get($user_name, "email"));
  244. $form->field(name => "subscriptions", force => 1,
  245. value => userinfo_get($user_name, "subscriptions"));
  246. $form->field(name => "locked_pages", force => 1,
  247. value => userinfo_get($user_name, "locked_pages"));
  248. }
  249. if ($form->submitted eq 'Logout') {
  250. $session->delete();
  251. print $q->redirect($config{url});
  252. return;
  253. }
  254. elsif ($form->submitted eq 'Cancel') {
  255. print $q->redirect($config{url});
  256. return;
  257. }
  258. elsif ($form->submitted eq "Save Preferences" && $form->validate) {
  259. foreach my $field (qw(password email subscriptions locked_pages)) {
  260. if (length $form->field($field)) {
  261. userinfo_set($user_name, $field, $form->field($field)) || error("failed to set $field");
  262. }
  263. }
  264. $form->text("Preferences saved.");
  265. }
  266. print $session->header(-charset=>'utf-8');
  267. print misctemplate($form->title, $form->render(submit => \@buttons));
  268. } #}}}
  269. sub cgi_editpage ($$) { #{{{
  270. my $q=shift;
  271. my $session=shift;
  272. eval q{use CGI::FormBuilder};
  273. my $form = CGI::FormBuilder->new(
  274. fields => [qw(do rcsinfo subpage from page editcontent comments)],
  275. header => 1,
  276. charset => "utf-8",
  277. method => 'POST',
  278. validate => {
  279. editcontent => '/.+/',
  280. },
  281. required => [qw{editcontent}],
  282. javascript => 0,
  283. params => $q,
  284. action => $config{cgiurl},
  285. table => 0,
  286. template => {template_params("editpage.tmpl")},
  287. );
  288. my @buttons=("Save Page", "Preview", "Cancel");
  289. # This untaint is safe because titlepage removes any problimatic
  290. # characters.
  291. my ($page)=decode_utf8($form->param('page'));
  292. $page=titlepage(possibly_foolish_untaint(lc($page)));
  293. if (! defined $page || ! length $page ||
  294. $page=~/$config{wiki_file_prune_regexp}/ || $page=~/^\//) {
  295. error("bad page name");
  296. }
  297. $page=lc($page);
  298. my $file;
  299. if (exists $pagesources{lc($page)}) {
  300. $file=$pagesources{lc($page)};
  301. }
  302. else {
  303. $file=$page.".".$config{default_pageext};
  304. }
  305. my $newfile=0;
  306. if (! -e "$config{srcdir}/$file") {
  307. $newfile=1;
  308. }
  309. $form->field(name => "do", type => 'hidden');
  310. $form->field(name => "from", type => 'hidden');
  311. $form->field(name => "rcsinfo", type => 'hidden');
  312. $form->field(name => "subpage", type => 'hidden');
  313. $form->field(name => "page", value => "$page", force => 1);
  314. $form->field(name => "comments", type => "text", size => 80);
  315. $form->field(name => "editcontent", type => "textarea", rows => 20,
  316. cols => 80);
  317. $form->tmpl_param("can_commit", $config{rcs});
  318. $form->tmpl_param("indexlink", indexlink());
  319. $form->tmpl_param("helponformattinglink",
  320. htmllink("", "", "HelpOnFormatting", 1));
  321. $form->tmpl_param("styleurl", styleurl());
  322. $form->tmpl_param("baseurl", "$config{url}/");
  323. if (! $form->submitted) {
  324. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  325. force => 1);
  326. }
  327. if ($form->submitted eq "Cancel") {
  328. print $q->redirect("$config{url}/".htmlpage($page));
  329. return;
  330. }
  331. elsif ($form->submitted eq "Preview") {
  332. require IkiWiki::Render;
  333. # Apparently FormBuilder doesn't not treat input as
  334. # utf-8, so decode from it.
  335. my $content=decode_utf8($form->field('editcontent'));
  336. my $comments=decode_utf8($form->field('comments'));
  337. $form->field(name => "editcontent",
  338. value => $content, force => 1);
  339. $form->field(name => "comments",
  340. value => $comments, force => 1);
  341. $form->tmpl_param("page_preview",
  342. htmlize(pagetype($file),
  343. linkify($page, $page, $content)));
  344. }
  345. else {
  346. $form->tmpl_param("page_preview", "");
  347. }
  348. $form->tmpl_param("page_conflict", "");
  349. if (! $form->submitted || $form->submitted eq "Preview" ||
  350. ! $form->validate) {
  351. if ($form->field("do") eq "create") {
  352. my @page_locs;
  353. my $best_loc;
  354. my ($from)=$form->param('from')=~/$config{wiki_file_regexp}/;
  355. if (! defined $from || ! length $from ||
  356. $from ne $form->param('from') ||
  357. $from=~/$config{wiki_file_prune_regexp}/ ||
  358. $from=~/^\// ||
  359. $form->submitted eq "Preview") {
  360. @page_locs=$best_loc=$page;
  361. }
  362. else {
  363. my $dir=$from."/";
  364. $dir=~s![^/]+/+$!!;
  365. if ((defined $form->param('subpage') && length $form->param('subpage')) ||
  366. $page eq 'discussion') {
  367. $best_loc="$from/$page";
  368. }
  369. else {
  370. $best_loc=$dir.$page;
  371. }
  372. push @page_locs, $dir.$page;
  373. push @page_locs, "$from/$page";
  374. while (length $dir) {
  375. $dir=~s![^/]+/+$!!;
  376. push @page_locs, $dir.$page;
  377. }
  378. }
  379. @page_locs = grep {
  380. ! exists $pagesources{lc($_)} &&
  381. ! page_locked($_, $session, 1)
  382. } @page_locs;
  383. if (! @page_locs) {
  384. # hmm, someone else made the page in the
  385. # meantime?
  386. print $q->redirect("$config{url}/".htmlpage($page));
  387. return;
  388. }
  389. $form->tmpl_param("page_select", 1);
  390. $form->field(name => "page", type => 'select',
  391. options => \@page_locs, value => $best_loc);
  392. $form->title("creating ".pagetitle($page));
  393. }
  394. elsif ($form->field("do") eq "edit") {
  395. page_locked($page, $session);
  396. if (! defined $form->field('editcontent') ||
  397. ! length $form->field('editcontent')) {
  398. my $content="";
  399. if (exists $pagesources{lc($page)}) {
  400. $content=readfile(srcfile($pagesources{lc($page)}));
  401. $content=~s/\n/\r\n/g;
  402. }
  403. $form->field(name => "editcontent", value => $content,
  404. force => 1);
  405. }
  406. $form->tmpl_param("page_select", 0);
  407. $form->field(name => "page", type => 'hidden');
  408. $form->title("editing ".pagetitle($page));
  409. }
  410. print $form->render(submit => \@buttons);
  411. }
  412. else {
  413. # save page
  414. page_locked($page, $session);
  415. # Decode utf-8 since FormBuilder does not
  416. my $content=decode_utf8($form->field('editcontent'));
  417. $content=~s/\r\n/\n/g;
  418. $content=~s/\r/\n/g;
  419. writefile($file, $config{srcdir}, $content);
  420. my $message="web commit ";
  421. if (defined $session->param("name") &&
  422. length $session->param("name")) {
  423. $message.="by ".$session->param("name");
  424. }
  425. else {
  426. $message.="from $ENV{REMOTE_ADDR}";
  427. }
  428. if (defined $form->field('comments') &&
  429. length $form->field('comments')) {
  430. $message.=": ".decode_utf8($form->field('comments'));
  431. }
  432. if ($config{rcs}) {
  433. if ($newfile) {
  434. rcs_add($file);
  435. }
  436. # prevent deadlock with post-commit hook
  437. unlockwiki();
  438. # presumably the commit will trigger an update
  439. # of the wiki
  440. my $conflict=rcs_commit($file, $message,
  441. $form->field("rcsinfo"));
  442. if (defined $conflict) {
  443. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  444. force => 1);
  445. $form->tmpl_param("page_conflict", 1);
  446. $form->field("editcontent", value => $conflict, force => 1);
  447. $form->field(name => "comments", value => decode_utf8($form->field('comments')), force => 1);
  448. $form->field("do", "edit)");
  449. $form->tmpl_param("page_select", 0);
  450. $form->field(name => "page", type => 'hidden');
  451. $form->title("editing $page");
  452. print $form->render(submit => \@buttons);
  453. return;
  454. }
  455. }
  456. else {
  457. require IkiWiki::Render;
  458. refresh();
  459. saveindex();
  460. }
  461. # The trailing question mark tries to avoid broken
  462. # caches and get the most recent version of the page.
  463. print $q->redirect("$config{url}/".htmlpage($page)."?updated");
  464. }
  465. } #}}}
  466. sub cgi () { #{{{
  467. eval q{use CGI};
  468. eval q{use CGI::Session};
  469. my $q=CGI->new;
  470. if (exists $hooks{cgi}) {
  471. foreach my $id (keys %{$hooks{cgi}}) {
  472. $hooks{cgi}{$id}{call}->($q);
  473. }
  474. }
  475. my $do=$q->param('do');
  476. if (! defined $do || ! length $do) {
  477. error("\"do\" parameter missing");
  478. }
  479. # Things that do not need a session.
  480. if ($do eq 'recentchanges') {
  481. cgi_recentchanges($q);
  482. return;
  483. }
  484. elsif ($do eq 'hyperestraier') {
  485. cgi_hyperestraier();
  486. }
  487. CGI::Session->name("ikiwiki_session_$config{wikiname}");
  488. my $oldmask=umask(077);
  489. my $session = CGI::Session->new("driver:DB_File", $q,
  490. { FileName => "$config{wikistatedir}/sessions.db" });
  491. umask($oldmask);
  492. # Everything below this point needs the user to be signed in.
  493. if ((! $config{anonok} &&
  494. (! defined $session->param("name") ||
  495. ! userinfo_get($session->param("name"), "regdate"))) || $do eq 'signin') {
  496. cgi_signin($q, $session);
  497. # Force session flush with safe umask.
  498. my $oldmask=umask(077);
  499. $session->flush;
  500. umask($oldmask);
  501. return;
  502. }
  503. if ($do eq 'create' || $do eq 'edit') {
  504. cgi_editpage($q, $session);
  505. }
  506. elsif ($do eq 'prefs') {
  507. cgi_prefs($q, $session);
  508. }
  509. elsif ($do eq 'blog') {
  510. my $page=titlepage(lc($q->param('title')));
  511. # if the page already exists, munge it to be unique
  512. my $from=$q->param('from');
  513. my $add="";
  514. while (exists $oldpagemtime{"$from/$page$add"}) {
  515. $add=1 unless length $add;
  516. $add++;
  517. }
  518. $q->param('page', $page.$add);
  519. # now run same as create
  520. $q->param('do', 'create');
  521. cgi_editpage($q, $session);
  522. }
  523. else {
  524. error("unknown do parameter");
  525. }
  526. } #}}}
  527. 1