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