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