summaryrefslogtreecommitdiff
path: root/IkiWiki/CGI.pm
blob: f8617bfc63ca63f51a8f51c135c96641947faf7d (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki;
  3. use warnings;
  4. use strict;
  5. use IkiWiki;
  6. use IkiWiki::UserInfo;
  7. use open qw{:utf8 :std};
  8. use Encode;
  9. sub printheader ($) {
  10. my $session=shift;
  11. if ($ENV{HTTPS} || $config{sslcookie}) {
  12. print $session->header(-charset => 'utf-8',
  13. -cookie => $session->cookie(-httponly => 1, -secure => 1));
  14. }
  15. else {
  16. print $session->header(-charset => 'utf-8',
  17. -cookie => $session->cookie(-httponly => 1));
  18. }
  19. }
  20. sub prepform {
  21. my $form=shift;
  22. my $buttons=shift;
  23. my $session=shift;
  24. my $cgi=shift;
  25. if (exists $hooks{formbuilder}) {
  26. run_hooks(formbuilder => sub {
  27. shift->(form => $form, cgi => $cgi, session => $session,
  28. buttons => $buttons);
  29. });
  30. }
  31. return $form;
  32. }
  33. sub showform ($$$$;@) {
  34. my $form=prepform(@_);
  35. shift;
  36. my $buttons=shift;
  37. my $session=shift;
  38. my $cgi=shift;
  39. printheader($session);
  40. print cgitemplate($cgi, $form->title,
  41. $form->render(submit => $buttons), @_);
  42. }
  43. sub cgitemplate ($$$;@) {
  44. my $cgi=shift;
  45. my $title=shift;
  46. my $content=shift;
  47. my %params=@_;
  48. my $template=template("page.tmpl");
  49. my $topurl = defined $cgi ? $cgi->url : $config{url};
  50. my $page="";
  51. if (exists $params{page}) {
  52. $page=delete $params{page};
  53. $params{forcebaseurl}=urlabs(urlto($page), $topurl);
  54. }
  55. run_hooks(pagetemplate => sub {
  56. shift->(
  57. page => $page,
  58. destpage => $page,
  59. template => $template,
  60. );
  61. });
  62. templateactions($template, "");
  63. $template->param(
  64. dynamic => 1,
  65. title => $title,
  66. wikiname => $config{wikiname},
  67. content => $content,
  68. baseurl => urlabs(urlto(""), $topurl),
  69. html5 => $config{html5},
  70. %params,
  71. );
  72. return $template->output;
  73. }
  74. sub redirect ($$) {
  75. my $q=shift;
  76. eval q{use URI};
  77. my $url=URI->new(urlabs(shift, $q->url));
  78. if (! $config{w3mmode}) {
  79. print $q->redirect($url);
  80. }
  81. else {
  82. print "Content-type: text/plain\n";
  83. print "W3m-control: GOTO $url\n\n";
  84. }
  85. }
  86. sub decode_cgi_utf8 ($) {
  87. # decode_form_utf8 method is needed for 5.01
  88. if ($] < 5.01) {
  89. my $cgi = shift;
  90. foreach my $f ($cgi->param) {
  91. $cgi->param($f, map { decode_utf8 $_ } $cgi->param($f));
  92. }
  93. }
  94. }
  95. sub decode_form_utf8 ($) {
  96. if ($] >= 5.01) {
  97. my $form = shift;
  98. foreach my $f ($form->field) {
  99. my @value=map { decode_utf8($_) } $form->field($f);
  100. $form->field(name => $f,
  101. value => \@value,
  102. force => 1,
  103. );
  104. }
  105. }
  106. }
  107. # Check if the user is signed in. If not, redirect to the signin form and
  108. # save their place to return to later.
  109. sub needsignin ($$) {
  110. my $q=shift;
  111. my $session=shift;
  112. if (! defined $session->param("name") ||
  113. ! userinfo_get($session->param("name"), "regdate")) {
  114. $session->param(postsignin => $ENV{QUERY_STRING});
  115. cgi_signin($q, $session);
  116. cgi_savesession($session);
  117. exit;
  118. }
  119. }
  120. sub cgi_signin ($$;$) {
  121. my $q=shift;
  122. my $session=shift;
  123. my $returnhtml=shift;
  124. decode_cgi_utf8($q);
  125. eval q{use CGI::FormBuilder};
  126. error($@) if $@;
  127. my $form = CGI::FormBuilder->new(
  128. title => "signin",
  129. name => "signin",
  130. charset => "utf-8",
  131. method => 'POST',
  132. required => 'NONE',
  133. javascript => 0,
  134. params => $q,
  135. action => cgiurl(),
  136. header => 0,
  137. template => {type => 'div'},
  138. stylesheet => 1,
  139. );
  140. my $buttons=["Login"];
  141. $form->field(name => "do", type => "hidden", value => "signin",
  142. force => 1);
  143. decode_form_utf8($form);
  144. run_hooks(formbuilder_setup => sub {
  145. shift->(form => $form, cgi => $q, session => $session,
  146. buttons => $buttons);
  147. });
  148. decode_form_utf8($form);
  149. if ($form->submitted) {
  150. $form->validate;
  151. }
  152. if ($returnhtml) {
  153. $form=prepform($form, $buttons, $session, $q);
  154. return $form->render(submit => $buttons);
  155. }
  156. showform($form, $buttons, $session, $q);
  157. }
  158. sub cgi_postsignin ($$) {
  159. my $q=shift;
  160. my $session=shift;
  161. # Continue with whatever was being done before the signin process.
  162. if (defined $session->param("postsignin")) {
  163. my $postsignin=CGI->new($session->param("postsignin"));
  164. $session->clear("postsignin");
  165. cgi($postsignin, $session);
  166. cgi_savesession($session);
  167. exit;
  168. }
  169. else {
  170. if ($config{sslcookie} && ! $q->https()) {
  171. error(gettext("probable misconfiguration: sslcookie is set, but you are attempting to login via http, not https"));
  172. }
  173. else {
  174. error(gettext("login failed, perhaps you need to turn on cookies?"));
  175. }
  176. }
  177. }
  178. sub cgi_prefs ($$) {
  179. my $q=shift;
  180. my $session=shift;
  181. needsignin($q, $session);
  182. decode_cgi_utf8($q);
  183. # The session id is stored on the form and checked to
  184. # guard against CSRF.
  185. my $sid=$q->param('sid');
  186. if (! defined $sid) {
  187. $q->delete_all;
  188. }
  189. elsif ($sid ne $session->id) {
  190. error(gettext("Your login session has expired."));
  191. }
  192. eval q{use CGI::FormBuilder};
  193. error($@) if $@;
  194. my $form = CGI::FormBuilder->new(
  195. title => "preferences",
  196. name => "preferences",
  197. header => 0,
  198. charset => "utf-8",
  199. method => 'POST',
  200. validate => {
  201. email => 'EMAIL',
  202. },
  203. required => 'NONE',
  204. javascript => 0,
  205. params => $q,
  206. action => cgiurl(),
  207. template => {type => 'div'},
  208. stylesheet => 1,
  209. fieldsets => [
  210. [login => gettext("Login")],
  211. [preferences => gettext("Preferences")],
  212. [admin => gettext("Admin")]
  213. ],
  214. );
  215. my $buttons=["Save Preferences", "Logout", "Cancel"];
  216. decode_form_utf8($form);
  217. run_hooks(formbuilder_setup => sub {
  218. shift->(form => $form, cgi => $q, session => $session,
  219. buttons => $buttons);
  220. });
  221. decode_form_utf8($form);
  222. $form->field(name => "do", type => "hidden", value => "prefs",
  223. force => 1);
  224. $form->field(name => "sid", type => "hidden", value => $session->id,
  225. force => 1);
  226. $form->field(name => "email", size => 50, fieldset => "preferences");
  227. my $user_name=$session->param("name");
  228. if (! $form->submitted) {
  229. $form->field(name => "email", force => 1,
  230. value => userinfo_get($user_name, "email"));
  231. }
  232. if ($form->submitted eq 'Logout') {
  233. $session->delete();
  234. redirect($q, baseurl(undef));
  235. return;
  236. }
  237. elsif ($form->submitted eq 'Cancel') {
  238. redirect($q, baseurl(undef));
  239. return;
  240. }
  241. elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
  242. if (defined $form->field('email')) {
  243. userinfo_set($user_name, 'email', $form->field('email')) ||
  244. error("failed to set email");
  245. }
  246. $form->text(gettext("Preferences saved."));
  247. }
  248. showform($form, $buttons, $session, $q,
  249. prefsurl => "", # avoid showing the preferences link
  250. );
  251. }
  252. sub cgi_custom_failure ($$$) {
  253. my $q=shift;
  254. my $httpstatus=shift;
  255. my $message=shift;
  256. print $q->header(
  257. -status => $httpstatus,
  258. -charset => 'utf-8',
  259. );
  260. print $message;
  261. # Internet Explod^Hrer won't show custom 404 responses
  262. # unless they're >= 512 bytes
  263. print ' ' x 512;
  264. exit;
  265. }
  266. sub check_banned ($$) {
  267. my $q=shift;
  268. my $session=shift;
  269. my $banned=0;
  270. my $name=$session->param("name");
  271. if (defined $name &&
  272. grep { $name eq $_ } @{$config{banned_users}}) {
  273. $banned=1;
  274. }
  275. foreach my $b (@{$config{banned_users}}) {
  276. if (pagespec_match("", $b,
  277. ip => $session->remote_addr(),
  278. name => defined $name ? $name : "",
  279. )) {
  280. $banned=1;
  281. last;
  282. }
  283. }
  284. if ($banned) {
  285. $session->delete();
  286. cgi_savesession($session);
  287. cgi_custom_failure(
  288. $q, "403 Forbidden",
  289. gettext("You are banned."));
  290. }
  291. }
  292. sub cgi_getsession ($) {
  293. my $q=shift;
  294. eval q{use CGI::Session; use HTML::Entities};
  295. error($@) if $@;
  296. CGI::Session->name("ikiwiki_session_".encode_entities($config{wikiname}));
  297. my $oldmask=umask(077);
  298. my $session = eval {
  299. CGI::Session->new("driver:DB_File", $q,
  300. { FileName => "$config{wikistatedir}/sessions.db" })
  301. };
  302. if (! $session || $@) {
  303. error($@." ".CGI::Session->errstr());
  304. }
  305. umask($oldmask);
  306. return $session;
  307. }
  308. # To guard against CSRF, the user's session id (sid)
  309. # can be stored on a form. This function will check
  310. # (for logged in users) that the sid on the form matches
  311. # the session id in the cookie.
  312. sub checksessionexpiry ($$) {
  313. my $q=shift;
  314. my $session = shift;
  315. if (defined $session->param("name")) {
  316. my $sid=$q->param('sid');
  317. if (! defined $sid || $sid ne $session->id) {
  318. error(gettext("Your login session has expired."));
  319. }
  320. }
  321. }
  322. sub cgi_savesession ($) {
  323. my $session=shift;
  324. # Force session flush with safe umask.
  325. my $oldmask=umask(077);
  326. $session->flush;
  327. umask($oldmask);
  328. }
  329. sub cgi (;$$) {
  330. my $q=shift;
  331. my $session=shift;
  332. eval q{use CGI};
  333. error($@) if $@;
  334. $CGI::DISABLE_UPLOADS=$config{cgi_disable_uploads};
  335. if (! $q) {
  336. binmode(STDIN);
  337. $q=CGI->new;
  338. binmode(STDIN, ":utf8");
  339. run_hooks(cgi => sub { shift->($q) });
  340. }
  341. my $do=$q->param('do');
  342. if (! defined $do || ! length $do) {
  343. my $error = $q->cgi_error;
  344. if ($error) {
  345. error("Request not processed: $error");
  346. }
  347. else {
  348. error("\"do\" parameter missing");
  349. }
  350. }
  351. # Need to lock the wiki before getting a session.
  352. lockwiki();
  353. loadindex();
  354. if (! $session) {
  355. $session=cgi_getsession($q);
  356. }
  357. # Auth hooks can sign a user in.
  358. if ($do ne 'signin' && ! defined $session->param("name")) {
  359. run_hooks(auth => sub {
  360. shift->($q, $session)
  361. });
  362. if (defined $session->param("name")) {
  363. # Make sure whatever user was authed is in the
  364. # userinfo db.
  365. if (! userinfo_get($session->param("name"), "regdate")) {
  366. userinfo_setall($session->param("name"), {
  367. email => "",
  368. password => "",
  369. regdate => time,
  370. }) || error("failed adding user");
  371. }
  372. }
  373. }
  374. check_banned($q, $session);
  375. run_hooks(sessioncgi => sub { shift->($q, $session) });
  376. if ($do eq 'signin') {
  377. cgi_signin($q, $session);
  378. cgi_savesession($session);
  379. }
  380. elsif ($do eq 'prefs') {
  381. cgi_prefs($q, $session);
  382. }
  383. elsif (defined $session->param("postsignin") || $do eq 'postsignin') {
  384. cgi_postsignin($q, $session);
  385. }
  386. else {
  387. error("unknown do parameter");
  388. }
  389. }
  390. # Does not need to be called directly; all errors will go through here.
  391. sub cgierror ($) {
  392. my $message=shift;
  393. print "Content-type: text/html\n\n";
  394. print cgitemplate(undef, gettext("Error"),
  395. "<p class=\"error\">".gettext("Error").": $message</p>");
  396. die $@;
  397. }
  398. 1