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