]>
Commit | Line | Data |
---|---|---|
70789617 TC |
1 | package BSE::Request::Base; |
2 | use strict; | |
3 | use CGI (); | |
4 | use BSE::Cfg; | |
5 | use DevHelp::HTML; | |
61138170 | 6 | use Carp qw(cluck confess); |
70789617 TC |
7 | |
8 | sub new { | |
9 | my ($class, %opts) = @_; | |
10 | ||
d2473dc2 TC |
11 | $opts{cfg} ||= BSE::Cfg->new; |
12 | ||
b3dbf74b | 13 | unless ($opts{nodatabase}) { |
f811b4b8 | 14 | require BSE::DB; |
b3dbf74b TC |
15 | BSE::DB->init($opts{cfg}); |
16 | BSE::DB->startup(); | |
17 | } | |
5ac2ad24 | 18 | |
a93c4cb8 TC |
19 | my $self = bless \%opts, $class; |
20 | ||
21 | $opts{cgi} ||= $self->_make_cgi; | |
8062fbd7 | 22 | $opts{fastcgi} ||= 0; |
70789617 | 23 | |
70789617 TC |
24 | if ($self->cfg->entry('html', 'utf8decodeall')) { |
25 | $self->_encode_utf8(); | |
26 | } | |
27 | elsif ($self->cfg->entry('html', 'ajaxcharset', 0) | |
61138170 | 28 | && $self->is_ajax) { |
70789617 TC |
29 | # convert the values of each parameter from UTF8 to iso-8859-1 |
30 | $self->_convert_utf8_cgi_to_charset(); | |
31 | } | |
32 | ||
33 | $self; | |
34 | } | |
35 | ||
a93c4cb8 TC |
36 | sub _tracking_uploads { |
37 | my ($self) = @_; | |
38 | unless (defined $self->{_tracking_uploads}) { | |
39 | my $want_track = $self->cfg->entry("basic", "track_uploads", 0); | |
40 | my $will_track = $self->_cache_available && $want_track; | |
41 | if ($want_track && !$will_track) { | |
42 | print STDERR "** Upload tracking requested but no cache found\n"; | |
43 | } | |
44 | $self->{_tracking_uploads} = $will_track; | |
45 | } | |
46 | ||
47 | return $self->{_tracking_uploads}; | |
48 | } | |
49 | ||
50 | sub _cache_available { | |
51 | my ($self) = @_; | |
52 | ||
53 | unless (defined $self->{_cache_available}) { | |
54 | my $cache_class = $self->cfg->entry("cache", "class"); | |
55 | $self->{_cache_available} = defined $cache_class; | |
56 | } | |
57 | ||
58 | return $self->{_cache_available}; | |
59 | } | |
60 | ||
61 | sub _cache_object { | |
62 | my ($self) = @_; | |
63 | ||
64 | $self->_cache_available or return; | |
65 | $self->{_cache} and return $self->{_cache}; | |
66 | ||
ebc63b18 | 67 | require BSE::Cache; |
a93c4cb8 | 68 | |
ebc63b18 | 69 | $self->{_cache} = BSE::Cache->load($self->cfg); |
a93c4cb8 TC |
70 | |
71 | return $self->{_cache}; | |
72 | } | |
73 | ||
74 | sub cache_set { | |
75 | my ($self, $key, $value) = @_; | |
76 | ||
77 | my $cache = $self->_cache_object | |
78 | or return; | |
79 | ||
46a55e1e | 80 | $cache->set($key, $value); |
a93c4cb8 TC |
81 | } |
82 | ||
83 | sub cache_get { | |
84 | my ($self, $key) = @_; | |
85 | ||
86 | my $cache = $self->_cache_object | |
87 | or return; | |
88 | ||
46a55e1e | 89 | return $cache->get($key); |
a93c4cb8 TC |
90 | } |
91 | ||
70789617 | 92 | sub _make_cgi { |
a93c4cb8 TC |
93 | my ($self) = @_; |
94 | ||
46a55e1e | 95 | my $cache; |
a93c4cb8 TC |
96 | if ($self->_tracking_uploads |
97 | && $ENV{REQUEST_METHOD} eq 'POST' | |
78d982ee | 98 | && $ENV{CONTENT_TYPE} |
46a55e1e | 99 | && $ENV{CONTENT_TYPE} =~ m(^multipart/form-data) |
15f30f31 | 100 | && $ENV{CONTENT_LENGTH} |
46a55e1e | 101 | && defined ($cache = $self->_cache_object)) { |
a93c4cb8 TC |
102 | # very hacky |
103 | my $q; | |
15f30f31 | 104 | my $done = 0; |
ec269d0f | 105 | my $last_set = time(); |
15f30f31 TC |
106 | my $upload_key; |
107 | if ($ENV{QUERY_STRING} | |
108 | && $ENV{QUERY_STRING} =~ /^_upload=([a-zA-Z0-9_]+)$/) { | |
109 | $upload_key = $1; | |
110 | } | |
111 | my $complete = 0; | |
112 | eval { | |
113 | $q = CGI->new | |
114 | ( | |
115 | sub { | |
116 | my ($filename, $data, $size_so_far) = @_; | |
117 | ||
118 | $upload_key ||= $q->param("_upload"); | |
119 | $upload_key or return; | |
120 | my $fullkey = "upload-$upload_key"; | |
121 | $done += length $data; | |
122 | my $now = time; | |
123 | if ($last_set + 1 <= $now) { # just in case we end up loading Time::HiRes | |
124 | $cache->set($fullkey, | |
125 | { | |
126 | done => $done, | |
127 | total => $ENV{CONTENT_LENGTH}, | |
128 | filename => $filename, | |
129 | complete => 0 | |
130 | }); | |
131 | $last_set = $now; | |
132 | } | |
133 | }, | |
134 | 0, # data for upload hook | |
135 | 1, # continue to use temp files | |
136 | {} # start out empty and don't read STDIN | |
137 | ); | |
138 | ||
139 | $q->init(); # initialize for real cgi | |
140 | $complete = 1; | |
141 | }; | |
142 | ||
143 | if ($upload_key) { | |
144 | my $fullkey = "upload-$upload_key"; | |
145 | ||
146 | if ($complete) { | |
147 | $cache->set($fullkey, | |
148 | { | |
149 | done => $ENV{CONTENT_LENGTH}, | |
150 | total => $ENV{CONTENT_LENGTH}, | |
151 | complete => 1, | |
152 | }); | |
153 | } | |
154 | else { | |
155 | $cache->set($fullkey, | |
156 | { | |
157 | failed => 1, | |
158 | }); | |
159 | die; | |
160 | } | |
161 | } | |
162 | ||
a93c4cb8 TC |
163 | |
164 | return $q; | |
165 | } | |
166 | ||
6361fafb TC |
167 | my $q = CGI->new; |
168 | my $error = $q->cgi_error; | |
169 | if ($error) { | |
170 | print STDERR "CGI ERROR: $error\n"; | |
171 | } | |
172 | ||
173 | return $q; | |
70789617 TC |
174 | } |
175 | ||
176 | sub cgi { | |
177 | return $_[0]{cgi}; | |
178 | } | |
179 | ||
180 | sub cfg { | |
181 | return $_[0]{cfg}; | |
182 | } | |
183 | ||
184 | sub session { | |
185 | $_[0]{session} or die "Session has been deleted already\n"; | |
186 | ||
187 | return $_[0]{session}; | |
188 | } | |
189 | ||
8062fbd7 TC |
190 | sub is_fastcgi { |
191 | $_[0]{fastcgi}; | |
192 | } | |
193 | ||
70789617 TC |
194 | sub end_request { |
195 | delete $_[0]{session}; | |
196 | } | |
197 | ||
70789617 TC |
198 | sub user { |
199 | return $_[0]{adminuser}; | |
200 | } | |
201 | ||
202 | sub setuser { | |
203 | $_[0]{adminuser} = $_[1]; | |
204 | } | |
205 | ||
206 | sub getuser { | |
207 | $_[0]{adminuser}; | |
208 | } | |
209 | ||
210 | # this needs to become non-admin specific | |
211 | sub url { | |
212 | my ($self, $action, $params, $name) = @_; | |
213 | ||
214 | require BSE::CfgInfo; | |
215 | my $url = BSE::CfgInfo::admin_base_url($self->{cfg}); | |
216 | $url .= "/cgi-bin/admin/$action.pl"; | |
217 | if ($params && keys %$params) { | |
218 | $url .= "?" . join("&", map { "$_=".escape_uri($params->{$_}) } keys %$params); | |
219 | } | |
220 | $url .= "#$name" if $name; | |
221 | ||
222 | $url; | |
223 | } | |
224 | ||
225 | sub check_admin_logon { | |
226 | my ($self) = @_; | |
227 | ||
228 | require BSE::Permissions; | |
229 | return BSE::Permissions->check_logon($self); | |
230 | } | |
231 | ||
232 | sub template_sets { | |
233 | my ($self) = @_; | |
234 | ||
235 | return () unless $self->access_control; | |
236 | ||
237 | my $user = $self->user | |
238 | or return; | |
239 | ||
240 | return grep $_ ne '', map $_->{template_set}, $user->groups; | |
241 | } | |
242 | ||
243 | my $site_article = | |
244 | { | |
245 | id => -1, | |
246 | title => "unknown", | |
247 | parentid => 0, | |
248 | generator => 'Generate::Article', | |
249 | level => 0, | |
250 | }; | |
251 | ||
252 | sub user_can { | |
253 | my ($self, $perm, $object, $rmsg) = @_; | |
254 | ||
255 | require BSE::Permissions; | |
256 | $object ||= $site_article; | |
257 | $self->{perms} ||= BSE::Permissions->new($self->cfg); | |
258 | if ($self->cfg->entry('basic', 'access_control', 0)) { | |
259 | unless (ref $object) { | |
260 | require Articles; | |
261 | my $art = $object == -1 ? $site_article : Articles->getByPkey($object); | |
262 | if ($art) { | |
263 | $object = $art; | |
264 | } | |
265 | else { | |
266 | print STDERR "** Cannot find article id $object\n"; | |
267 | require Carp; | |
268 | Carp::cluck "Cannot find article id $object"; | |
269 | return 0; | |
270 | } | |
271 | } | |
272 | return $self->{perms}->user_has_perm($self->user, $object, $perm, $rmsg); | |
273 | } | |
274 | else { | |
275 | # some checks need to happen even if we don't want logons | |
276 | return $self->{perms}->user_has_perm({ id=>-1 }, $object, $perm, $rmsg); | |
277 | } | |
278 | } | |
279 | ||
280 | # a stub for now | |
281 | sub get_object { | |
282 | return; | |
283 | } | |
284 | ||
285 | sub access_control { | |
286 | $_[0]->{cfg}->entry('basic', 'access_control', 0); | |
287 | } | |
288 | ||
d2473dc2 TC |
289 | sub get_refresh { |
290 | my ($req, $url) = @_; | |
291 | ||
292 | require BSE::Template; | |
293 | BSE::Template->get_refresh($url, $req->cfg); | |
294 | } | |
295 | ||
70789617 TC |
296 | sub output_result { |
297 | my ($req, $result) = @_; | |
298 | ||
299 | require BSE::Template; | |
300 | BSE::Template->output_result($req, $result); | |
301 | } | |
302 | ||
e63c3728 TC |
303 | sub flash { |
304 | my ($self, @msg) = @_; | |
305 | ||
f83119bf TC |
306 | my $msg; |
307 | if ($msg[0] =~ /^msg:/) { | |
308 | $msg = $self->catmsg(@msg); | |
309 | } | |
310 | else { | |
311 | $msg = "@msg"; | |
312 | } | |
313 | ||
e63c3728 TC |
314 | my @flash; |
315 | @flash = @{$self->session->{flash}} if $self->session->{flash}; | |
316 | push @flash, $msg; | |
317 | $self->session->{flash} = \@flash; | |
318 | } | |
319 | ||
70789617 TC |
320 | sub message { |
321 | my ($req, $errors) = @_; | |
322 | ||
323 | my $msg = ''; | |
e63c3728 | 324 | my @lines; |
70789617 | 325 | if ($errors and keys %$errors) { |
ebc63b18 TC |
326 | # do any translation needed |
327 | for my $key (keys %$errors) { | |
328 | my @msgs = ref $errors->{$key} ? @{$errors->{$key}} : $errors->{$key}; | |
329 | ||
330 | for my $msg (@msgs) { | |
f83119bf | 331 | if ($msg =~ /^(msg:[\w-]+(?:\/[\w-]+)+)(?::(.*))?$/) { |
ebc63b18 TC |
332 | my $id = $1; |
333 | my $params = $2; | |
334 | my @params = defined $params ? split(/:/, $params) : (); | |
335 | $msg = $req->catmsg($id, \@params); | |
336 | } | |
337 | } | |
338 | $errors->{$key} = ref $errors->{$key} ? \@msgs : $msgs[0]; | |
339 | } | |
340 | ||
70789617 TC |
341 | my @fields = $req->cgi->param; |
342 | my %work = %$errors; | |
70789617 TC |
343 | for my $field (@fields) { |
344 | if (my $entry = delete $work{$field}) { | |
345 | push @lines, ref($entry) ? grep $_, @$entry : $entry; | |
346 | } | |
347 | } | |
348 | for my $entry (values %work) { | |
349 | if (ref $entry) { | |
350 | push @lines, grep $_, @$entry; | |
351 | } | |
352 | else { | |
353 | push @lines, $entry; | |
354 | } | |
355 | } | |
356 | my %seen; | |
357 | @lines = grep !$seen{$_}++, @lines; # don't need duplicates | |
70789617 | 358 | } |
b0090c10 | 359 | if (!$req->{nosession} && $req->session->{flash}) { |
e63c3728 TC |
360 | push @lines, @{$req->session->{flash}}; |
361 | delete $req->session->{flash}; | |
362 | } | |
363 | $msg = join "<br />", map escape_html($_), @lines; | |
70789617 TC |
364 | if (!$msg && $req->cgi->param('m')) { |
365 | $msg = join(' ', $req->cgi->param('m')); | |
366 | $msg = escape_html($msg); | |
367 | } | |
368 | ||
369 | return $msg; | |
370 | } | |
371 | ||
372 | sub dyn_response { | |
4c4d3c3f | 373 | my ($req, $template, $acts, $modifier) = @_; |
70789617 TC |
374 | |
375 | my @search = $template; | |
376 | my $base_template = $template; | |
377 | my $t = $req->cgi->param('t'); | |
378 | $t or $t = $req->cgi->param('_t'); | |
4c4d3c3f | 379 | $t or $t = $modifier; |
70789617 TC |
380 | if ($t && $t =~ /^\w+$/) { |
381 | $template .= "_$t"; | |
382 | unshift @search, $template; | |
383 | } | |
384 | ||
385 | require BSE::Template; | |
386 | my @sets; | |
387 | if ($template =~ m!^admin/!) { | |
388 | @sets = $req->template_sets; | |
389 | } | |
390 | ||
391 | return BSE::Template->get_response($template, $req->cfg, $acts, | |
392 | $base_template, \@sets); | |
393 | } | |
394 | ||
395 | sub response { | |
396 | my ($req, $template, $acts) = @_; | |
397 | ||
398 | require BSE::Template; | |
399 | my @sets; | |
400 | if ($template =~ m!^admin/!) { | |
401 | @sets = $req->template_sets; | |
402 | } | |
403 | ||
404 | return BSE::Template->get_response($template, $req->cfg, $acts, | |
405 | $template, \@sets); | |
406 | } | |
407 | ||
408 | # get the current site user if one is logged on | |
409 | sub siteuser { | |
410 | my ($req) = @_; | |
411 | ||
412 | ++$req->{siteuser_calls}; | |
413 | if (exists $req->{_siteuser}) { | |
414 | ++$req->{siteuser_cached}; | |
415 | return $req->{_siteuser}; | |
416 | } | |
417 | ||
418 | my $cfg = $req->cfg; | |
419 | my $session = $req->session; | |
420 | require SiteUsers; | |
421 | if ($cfg->entryBool('custom', 'user_auth')) { | |
422 | require BSE::CfgInfo; | |
423 | my $custom = BSE::CfgInfo::custom_class($cfg); | |
424 | ||
425 | return $custom->siteuser_auth($session, $req->cgi, $cfg); | |
426 | } | |
427 | else { | |
428 | $req->{_siteuser} = undef; | |
429 | ||
430 | my $userid = $session->{userid} | |
431 | or return; | |
432 | my $user = SiteUsers->getBy(userId=>$userid) | |
433 | or return; | |
434 | $user->{disabled} | |
435 | and return; | |
436 | ||
437 | $req->{_siteuser} = $user; | |
438 | ||
439 | return $user; | |
440 | } | |
441 | } | |
442 | ||
443 | sub validate { | |
444 | my ($req, %options) = @_; | |
445 | ||
446 | $options{rules} ||= {}; | |
447 | ||
448 | require BSE::Validate; | |
9b3a5df0 TC |
449 | my %opts = |
450 | ( | |
451 | fields => $options{fields}, | |
452 | rules => $options{rules}, | |
453 | ); | |
454 | exists $options{optional} and $opts{optional} = $options{optional}; | |
455 | BSE::Validate::bse_validate | |
456 | ( | |
457 | $req->cgi, | |
458 | $options{errors}, | |
459 | \%opts, | |
460 | $req->cfg, | |
461 | $options{section} | |
462 | ); | |
70789617 TC |
463 | } |
464 | ||
465 | sub validate_hash { | |
466 | my ($req, %options) = @_; | |
467 | ||
468 | $options{rules} ||= {}; | |
469 | ||
9b3a5df0 TC |
470 | my %opts = |
471 | ( | |
472 | fields => $options{fields}, | |
473 | rules => $options{rules}, | |
474 | ); | |
475 | exists $options{optional} and $opts{optional} = $options{optional}; | |
70789617 | 476 | require BSE::Validate; |
9b3a5df0 TC |
477 | BSE::Validate::bse_validate_hash |
478 | ( | |
479 | $options{data}, | |
480 | $options{errors}, | |
481 | \%opts, | |
482 | $req->cfg, | |
483 | $options{section} | |
484 | ); | |
70789617 TC |
485 | } |
486 | ||
487 | sub configure_fields { | |
488 | my ($self, $fields, $section) = @_; | |
489 | ||
490 | my $cfg = $self->cfg; | |
491 | require BSE::Validate; | |
492 | my $cfg_fields = BSE::Validate::bse_configure_fields($fields, $cfg, $section); | |
493 | ||
494 | for my $name (keys %$fields) { | |
495 | for my $cfg_name (qw/htmltype type width height size maxlength/) { | |
496 | my $value = $cfg->entry($section, "${name}_${cfg_name}"); | |
497 | defined $value and $cfg_fields->{$name}{$cfg_name} = $value; | |
498 | } | |
499 | } | |
500 | ||
501 | $cfg_fields; | |
502 | } | |
503 | ||
504 | sub _have_group_access { | |
505 | my ($req, $user, $group_ids, $membership) = @_; | |
506 | ||
507 | if (grep $_ > 0, @$group_ids) { | |
508 | $membership->{filled} | |
509 | or %$membership = map { $_ => 1 } 'filled', $user->group_ids; | |
510 | return 1 | |
511 | if grep $membership->{$_}, @$group_ids; | |
512 | } | |
513 | for my $query_id (grep $_ < 0, @$group_ids) { | |
514 | require BSE::TB::SiteUserGroups; | |
515 | my $group = BSE::TB::SiteUserGroups->getQueryGroup($req->cfg, $query_id) | |
516 | or next; | |
517 | my $rows = BSE::DB->single->dbh->selectall_arrayref($group->{sql}, { MaxRows=>1 }, $user->{id}); | |
518 | $rows && @$rows | |
519 | and return 1; | |
520 | } | |
521 | ||
522 | return 0; | |
523 | } | |
524 | ||
525 | sub _siteuser_has_access { | |
526 | my ($req, $article, $user, $default, $membership) = @_; | |
527 | ||
528 | defined $default or $default = 1; | |
529 | defined $membership or $membership = {}; | |
530 | ||
1b73ea7e TC |
531 | unless ($article) { |
532 | # this shouldn't happen | |
533 | cluck("_siteuser_has_access() called without an article parameter!"); | |
534 | return 0; | |
535 | } | |
536 | ||
70789617 TC |
537 | my @group_ids = $article->group_ids; |
538 | if ($article->{inherit_siteuser_rights} | |
539 | && $article->{parentid} != -1) { | |
540 | if (@group_ids) { | |
541 | $user ||= $req->siteuser | |
542 | or return 0; | |
543 | if ($req->_have_group_access($user, \@group_ids, $membership)) { | |
544 | return 1; | |
545 | } | |
546 | else { | |
547 | return $req->siteuser_has_access($article->parent, $user, 0); | |
548 | } | |
549 | } | |
550 | else { | |
551 | # ask parent | |
552 | return $req->siteuser_has_access($article->parent, $user, $default); | |
553 | } | |
554 | } | |
555 | else { | |
556 | if (@group_ids) { | |
557 | $user ||= $req->siteuser | |
558 | or return 0; | |
559 | if ($req->_have_group_access($user, \@group_ids, $membership)) { | |
560 | return 1; | |
561 | } | |
562 | else { | |
563 | return 0; | |
564 | } | |
565 | } | |
566 | else { | |
567 | return $default; | |
568 | } | |
569 | } | |
570 | } | |
571 | ||
572 | sub siteuser_has_access { | |
573 | my ($req, $article, $user, $default, $membership) = @_; | |
574 | ||
575 | $user ||= $req->siteuser; | |
576 | ||
577 | ++$req->{has_access_total}; | |
578 | if ($req->{_siteuser} && $user && $user->{id} == $req->{_siteuser}{id} | |
579 | && exists $req->{_access_cache}{$article->{id}}) { | |
580 | ++$req->{has_access_cached}; | |
581 | return $req->{_access_cache}{$article->{id}}; | |
582 | } | |
583 | ||
584 | my $result = $req->_siteuser_has_access($article, $user, $default, $membership); | |
585 | ||
586 | if ($user && $req->{_siteuser} && $user->{id} == $req->{_siteuser}{id}) { | |
587 | $req->{_access_cache}{$article->{id}} = $result; | |
588 | } | |
589 | ||
590 | return $result; | |
591 | } | |
592 | ||
593 | sub dyn_user_tags { | |
594 | my ($self) = @_; | |
595 | ||
596 | require BSE::Util::DynamicTags; | |
597 | return BSE::Util::DynamicTags->new($self)->tags; | |
598 | } | |
599 | ||
600 | sub DESTROY { | |
601 | my ($self) = @_; | |
602 | ||
603 | if ($self->{cache_stats}) { | |
604 | print STDERR "Siteuser cache: $self->{siteuser_calls} Total, $self->{siteuser_cached} Cached\n" | |
605 | if $self->{siteuser_calls}; | |
606 | print STDERR "Access cache: $self->{has_access_total} Total, $self->{has_access_cached} Cached\n" | |
607 | if $self->{has_access_total}; | |
608 | } | |
609 | ||
610 | if ($self->{session}) { | |
611 | undef $self->{session}; | |
612 | } | |
613 | } | |
614 | ||
615 | sub set_article { | |
616 | my ($self, $name, $article) = @_; | |
617 | ||
618 | if ($article) { | |
619 | $self->{articles}{$name} = $article; | |
620 | } | |
621 | else { | |
622 | delete $self->{articles}{$name}; | |
623 | } | |
624 | } | |
625 | ||
626 | sub get_article { | |
627 | my ($self, $name) = @_; | |
628 | ||
629 | exists $self->{articles}{$name} | |
630 | or return; | |
631 | ||
632 | my $article = $self->{articles}{$name}; | |
633 | if (ref $article eq 'SCALAR') { | |
634 | $article = $$article; | |
635 | } | |
636 | $article | |
637 | or return; | |
638 | ||
639 | $article; | |
640 | } | |
641 | ||
642 | sub text { | |
643 | my ($self, $id, $default) = @_; | |
644 | ||
56f87a80 | 645 | return $self->cfg->entry('messages', $id, $default); |
70789617 TC |
646 | } |
647 | ||
648 | sub _convert_utf8_cgi_to_charset { | |
649 | my ($self) = @_; | |
650 | ||
651 | require Encode; | |
652 | my $cgi = $self->cgi; | |
653 | my $workset = $self->cfg->entry('html', 'charset', 'iso-8859-1'); | |
654 | my $decoded = $self->cfg->entry('html', 'cgi_decoded', 1); | |
655 | ||
656 | # avoids param decoding the data | |
657 | $cgi->charset($workset); | |
658 | ||
659 | print STDERR "Converting parameters from UTF8 to $workset\n" | |
660 | if $self->cfg->entry('debug', 'convert_charset'); | |
661 | ||
662 | if ($decoded) { | |
663 | # CGI.pm has already converted it from utf8 to perl's internal encoding | |
664 | # so we just need to encode to the working encoding | |
665 | # I don't see a reliable way to detect this without configuring it | |
666 | for my $name ($cgi->param) { | |
667 | my @values = map Encode::encode($workset, $_), $cgi->param($name); | |
668 | ||
669 | $cgi->param($name => @values); | |
670 | } | |
671 | } | |
672 | else { | |
673 | for my $name ($cgi->param) { | |
674 | my @values = $cgi->param($name); | |
675 | Encode::from_to($_, $workset, 'utf8') for @values; | |
676 | $cgi->param($name => @values); | |
677 | } | |
678 | } | |
679 | } | |
680 | ||
681 | sub _encode_utf8 { | |
682 | my ($self) = @_; | |
683 | ||
684 | my $cgi = $self->cgi; | |
685 | ||
686 | require Encode; | |
687 | for my $name ($cgi->param) { | |
688 | my @values = map Encode::encode('utf8', $_), $cgi->param($name); | |
689 | $cgi->param($name => @values); | |
690 | } | |
691 | } | |
692 | ||
f5505b76 TC |
693 | sub user_url { |
694 | my ($req, $script, $target, @options) = @_; | |
695 | ||
696 | my $cfg = $req->cfg; | |
697 | my $base = $script eq 'shop' ? $cfg->entryVar('site', 'secureurl') : ''; | |
698 | my $template; | |
796809d1 TC |
699 | if ($target) { |
700 | if ($script eq 'nuser') { | |
701 | $template = "/cgi-bin/nuser.pl/user/TARGET"; | |
702 | } | |
703 | else { | |
704 | $template = "$base/cgi-bin/$script.pl?a_TARGET=1"; | |
705 | } | |
706 | $template = $cfg->entry('targets', $script, $template); | |
707 | $template =~ s/TARGET/$target/; | |
f5505b76 TC |
708 | } |
709 | else { | |
796809d1 TC |
710 | if ($script eq 'nuser') { |
711 | $template = "/cgi-bin/nuser.pl/user"; | |
712 | } | |
713 | else { | |
714 | $template = "$base/cgi-bin/$script.pl"; | |
715 | } | |
716 | $template = $cfg->entry('targets', $script.'_n', $template); | |
f5505b76 | 717 | } |
f5505b76 TC |
718 | if (@options) { |
719 | $template .= $template =~ /\?/ ? '&' : '?'; | |
720 | my @entries; | |
721 | while (my ($key, $value) = splice(@options, 0, 2)) { | |
722 | push @entries, "$key=" . escape_uri($value); | |
723 | } | |
724 | $template .= join '&', @entries; | |
725 | } | |
726 | ||
727 | return $template; | |
728 | } | |
729 | ||
61138170 TC |
730 | sub admin_tags { |
731 | my ($req) = @_; | |
732 | ||
733 | require BSE::Util::Tags; | |
734 | return | |
735 | ( | |
736 | BSE::Util::Tags->common($req), | |
737 | BSE::Util::Tags->admin(undef, $req->cfg), | |
738 | BSE::Util::Tags->secure($req), | |
6bc5006a | 739 | $req->custom_admin_tags, |
61138170 TC |
740 | ); |
741 | } | |
742 | ||
6bc5006a TC |
743 | sub custom_admin_tags { |
744 | my ($req) = @_; | |
745 | ||
746 | $req->cfg->entry("custom", "admin_tags") | |
747 | or return; | |
748 | ||
749 | require BSE::CfgInfo; | |
750 | ||
751 | return BSE::CfgInfo::custom_class($req->cfg)->admin_tags($req); | |
752 | } | |
753 | ||
61138170 TC |
754 | =item is_ajax |
755 | ||
8f42c1c2 TC |
756 | Return true if the current request is an Ajax request. |
757 | ||
758 | Warning: changing this code has security concerns, it should only | |
759 | match where the request can only be an Ajax request - if the request | |
760 | can be produced by a normal form/link POST or GET this method must NOT | |
761 | return true. | |
61138170 TC |
762 | |
763 | =cut | |
764 | ||
765 | sub is_ajax { | |
766 | my ($self) = @_; | |
767 | ||
768 | defined $ENV{HTTP_X_REQUESTED_WITH} | |
769 | && $ENV{HTTP_X_REQUESTED_WITH} =~ /XMLHttpRequest/ | |
770 | and return 1; | |
771 | ||
61138170 TC |
772 | return; |
773 | } | |
774 | ||
5273694f TC |
775 | =item want_json_response |
776 | ||
777 | Return true if the caller has indicated they want a JSON response. | |
778 | ||
779 | In practice, returns true if is_ajax() is true or a _ parameter was | |
780 | supplied. | |
781 | ||
782 | =cut | |
783 | ||
784 | sub want_json_response { | |
785 | my ($self) = @_; | |
786 | ||
787 | $self->is_ajax and return 1; | |
788 | ||
789 | $self->cgi->param("_") and return 1; | |
790 | ||
791 | return; | |
792 | } | |
793 | ||
61138170 TC |
794 | =item send_email |
795 | ||
796 | Send a simple email. | |
797 | ||
798 | =cut | |
799 | ||
800 | sub send_email { | |
801 | my ($self, %opts) = @_; | |
802 | ||
803 | require BSE::ComposeMail; | |
804 | my $mailer = BSE::ComposeMail->new(cfg => $self->cfg); | |
805 | ||
806 | my $id = $opts{id} | |
807 | or confess "No mail id provided"; | |
808 | ||
809 | my $section = "email $id"; | |
810 | ||
811 | for my $key (qw/subject template html_template allow_html from from_name/) { | |
812 | my $value = $self->{cfg}->entry($section, $key); | |
813 | defined $value and $opts{$key} = $value; | |
814 | } | |
815 | unless (defined $opts{acts}) { | |
816 | require BSE::Util::Tags; | |
817 | BSE::Util::Tags->import(qw/tag_hash_plain/); | |
818 | my %acts = | |
819 | ( | |
820 | $self->dyn_user_tags | |
821 | ); | |
6c83a514 TC |
822 | if ($opts{extraacts}) { |
823 | %acts = ( %acts, %{$opts{extraacts}} ); | |
824 | } | |
825 | $opts{acts} = \%acts; | |
61138170 TC |
826 | } |
827 | ||
828 | $mailer->send(%opts) | |
829 | or print STDERR "Error sending mail $id: ", $mailer->errstr, "\n"; | |
830 | ||
831 | return 1; | |
832 | } | |
833 | ||
c6fc339f TC |
834 | =item is_ssl |
835 | ||
836 | Return true if the current request is an SSL request. | |
837 | ||
838 | =cut | |
839 | ||
840 | sub is_ssl { | |
841 | exists $ENV{HTTPS} || exists $ENV{SSL_CIPHER}; | |
842 | } | |
843 | ||
844 | my %recaptcha_errors = | |
845 | ( | |
846 | 'incorrect-captcha-sol' => 'Incorrect CAPTCHA solution', | |
847 | 'recaptcha-not-reachable' => "CAPTCHA server not reachable, please wait a moment and try again", | |
848 | ); | |
849 | ||
850 | =item test_recaptcha | |
851 | ||
852 | Test if a valid reCAPTCHA response was received. | |
853 | ||
854 | =cut | |
855 | ||
856 | sub test_recaptcha { | |
857 | my ($self, %opts) = @_; | |
858 | ||
859 | require Captcha::reCAPTCHA; | |
860 | my $apiprivkey = $self->cfg->entry('recaptcha', 'api_private_key'); | |
861 | unless (defined $apiprivkey) { | |
862 | print STDERR "** No recaptcha api_private_key defined **\n"; | |
863 | return; | |
864 | } | |
865 | my $msg; | |
866 | my $error = $opts{error} || \$msg; | |
867 | my $c = Captcha::reCAPTCHA->new; | |
868 | my $cgi = $self->cgi; | |
869 | my $challenge = $cgi->param('recaptcha_challenge_field'); | |
870 | my $response = $cgi->param('recaptcha_response_field'); | |
871 | delete $self->{recaptcha_error}; | |
872 | if (!defined $challenge || $challenge !~ /\S/) { | |
873 | $$error = "No reCAPTCHA challenge found"; | |
874 | return; | |
875 | } | |
876 | if (!defined $response || $response !~ /\S/) { | |
877 | $$error = "No reCAPTCHA response entered"; | |
878 | return; | |
879 | } | |
880 | ||
881 | my $result = $c->check_answer($apiprivkey, $ENV{REMOTE_ADDR}, | |
882 | $challenge, $response); | |
883 | unless ($result->{is_valid}) { | |
884 | my $key = 'error_'.$result->{error}; | |
885 | $key =~ tr/-/_/; | |
886 | $$error = $self->cfg->entry('recaptcha', $key) | |
887 | || $recaptcha_errors{$result->{error}} | |
888 | || $result->{error}; | |
889 | } | |
890 | $self->{recaptcha_result} = $result; | |
891 | ||
892 | return !!$result->{is_valid}; | |
893 | } | |
894 | ||
895 | sub recaptcha_result { | |
896 | $_[0]{recaptcha_result}; | |
897 | } | |
898 | ||
58baa27b TC |
899 | =item json_content |
900 | ||
901 | Generate a hash suitable for output_result() as JSON. | |
902 | ||
903 | =cut | |
904 | ||
905 | sub json_content { | |
906 | my ($self, @values) = @_; | |
907 | ||
908 | require JSON; | |
909 | ||
910 | my $json = JSON->new; | |
911 | ||
912 | my $value = @values > 1 ? +{ @values } : $values[0]; | |
7350b200 TC |
913 | my ($context) = $self->cgi->param("_context"); |
914 | if (defined $context) { | |
915 | $value->{context} = $context; | |
916 | } | |
58baa27b | 917 | |
f6c1d890 | 918 | my $json_result = |
58baa27b TC |
919 | +{ |
920 | type => "application/json", | |
921 | content => $json->encode($value), | |
922 | }; | |
f6c1d890 TC |
923 | |
924 | if (!exists $ENV{HTTP_X_REQUESTED_WITH} | |
925 | || $ENV{HTTP_X_REQUESTED_WITH} !~ /XMLHttpRequest/) { | |
926 | $json_result->{type} = "text/plain"; | |
927 | } | |
928 | ||
929 | return $json_result; | |
58baa27b TC |
930 | } |
931 | ||
932 | =item get_csrf_token($name) | |
933 | ||
934 | Generate a csrf token for the given name. | |
935 | ||
936 | =cut | |
937 | ||
938 | my $sequence = 0; | |
939 | ||
940 | sub get_csrf_token { | |
941 | my ($req, $name) = @_; | |
942 | ||
943 | my $cache = $req->session->{csrfp}; | |
944 | my $max_age = $req->cfg->entry('basic', 'csrfp_max_age', 3600); | |
945 | my $now = time; | |
946 | ||
947 | my $entry = $cache->{$name}; | |
948 | if (!$entry || $entry->{time} + $max_age < $now) { | |
949 | if ($entry) { | |
950 | $entry->{oldtoken} = $entry->{token}; | |
951 | $entry->{oldtime} = $entry->{time}; | |
952 | } | |
953 | else { | |
954 | $entry = {}; | |
955 | } | |
956 | ||
957 | # this doesn't need to be so perfectly secure that we drain the | |
958 | # entropy pool and it'll be called fairly often | |
959 | require Digest::MD5; | |
960 | $entry->{token} = | |
961 | Digest::MD5::md5_hex($now . $$ . rand() . $sequence++ . $name); | |
962 | $entry->{time} = $now; | |
963 | } | |
964 | $cache->{$name} = $entry; | |
965 | $req->session->{csrfp} = $cache; | |
966 | ||
967 | return $entry->{token}; | |
968 | } | |
969 | ||
970 | =item check_csrf($name) | |
971 | ||
972 | Check if the CSRF token supplied by the form is valid. | |
973 | ||
974 | $name should be the name supplied to the csrfp token. | |
975 | ||
976 | =cut | |
977 | ||
978 | sub check_csrf { | |
979 | my ($self, $name) = @_; | |
980 | ||
981 | defined $name | |
982 | or confess "No CSRF token name supplied"; | |
983 | ||
8f42c1c2 TC |
984 | $self->is_ajax |
985 | and return 1; | |
986 | ||
58baa27b TC |
987 | my $debug = $self->cfg->entry('debug', 'csrf', 0); |
988 | ||
989 | # the form might have multiple submit buttons, each initiating a | |
990 | # different function, so the the form should supply tokens for every | |
991 | # function for the form | |
992 | my @tokens = $self->cgi->param('_csrfp'); | |
993 | unless (@tokens) { | |
994 | $self->_csrf_error("No _csrfp token supplied"); | |
995 | return; | |
996 | } | |
997 | ||
998 | my $entry = $self->session->{csrfp}{$name}; | |
999 | unless ($entry) { | |
1000 | $self->_csrf_error("No token entry found for $name"); | |
1001 | return; | |
1002 | } | |
1003 | ||
1004 | my $max_age = $self->cfg->entry('basic', 'csrfp_max_age', 3600); | |
1005 | my $now = time; | |
1006 | for my $token (@tokens) { | |
1007 | if ($entry->{token} | |
1008 | && $entry->{token} eq $token | |
1009 | && $entry->{time} + 2*$max_age >= $now) { | |
1010 | $debug | |
1011 | and print STDERR "CSRF: match current token\n"; | |
1012 | return 1; | |
1013 | } | |
1014 | ||
1015 | if ($entry->{oldtoken} | |
1016 | && $entry->{oldtoken} eq $token | |
1017 | && $entry->{oldtime} + 2*$max_age >= $now) { | |
1018 | return 1; | |
1019 | } | |
1020 | } | |
1021 | ||
1022 | $self->_csrf_error("No tokens matched the $name entry"); | |
1023 | return; | |
1024 | } | |
1025 | ||
1026 | sub _csrf_error { | |
1027 | my ($self, $message) = @_; | |
1028 | ||
1029 | $self->cfg->entry('debug', 'csrf', 0) | |
1030 | and print STDERR "csrf error: $message\n"; | |
1031 | $self->{csrf_error} = $message; | |
1032 | ||
1033 | return; | |
1034 | } | |
1035 | ||
1036 | sub csrf_error { | |
1037 | $_[0]{csrf_error}; | |
1038 | } | |
1039 | ||
a0edb02e TC |
1040 | =item audit(object => $object, action => $action) |
1041 | ||
1042 | Simple audit logging. | |
1043 | ||
1044 | We record: | |
1045 | ||
1046 | object id, object describe result, action, siteuserid, ip address, date/time | |
1047 | ||
1048 | object and action are required. | |
1049 | ||
1050 | =cut | |
1051 | ||
1052 | sub audit { | |
1053 | my ($self, %opts) = @_; | |
1054 | ||
1055 | my $object = delete $opts{object} | |
1056 | or confess "Missing object parameter"; | |
1057 | ||
1058 | my $action = delete $opts{action} | |
1059 | or confess "Missing action parameter"; | |
1060 | ||
1061 | # check all of these are callable | |
1062 | my $id = $object->id; | |
1063 | my $desc = $object->describe; | |
1064 | ||
1065 | $self->cfg->entry("basic", "auditlog", 0) | |
1066 | or return; # no audit logging | |
1067 | ||
1068 | # assumed that check_admin_logon() has been done | |
1069 | my $admin = $self->user; | |
1070 | ||
1071 | require BSE::Util::SQL; | |
1072 | require BSE::TB::AuditLog; | |
1073 | my %entry = | |
1074 | ( | |
1075 | object_id => $id, | |
1076 | object_desc => $desc, | |
1077 | action => $action, | |
1078 | admin_id => $admin ? $admin->id : undef, | |
1079 | ip_address => $ENV{REMOTE_ADDR}, | |
1080 | when_at => BSE::Util::SQL::now_datetime(), | |
1081 | ); | |
1082 | BSE::TB::AuditLog->make(%entry); | |
1083 | } | |
1084 | ||
ebc63b18 TC |
1085 | =item message_catalog |
1086 | ||
1087 | Retrieve the message catalog. | |
1088 | ||
1089 | =cut | |
1090 | ||
1091 | sub message_catalog { | |
1092 | my ($self) = @_; | |
1093 | ||
1094 | unless ($self->{message_catalog}) { | |
1095 | require BSE::Message; | |
1096 | my %opts; | |
1097 | $self->_cache_available and $opts{cache} = $self->_cache_object; | |
1098 | $self->{message_catalog} = BSE::Message->new(%opts); | |
1099 | } | |
1100 | ||
1101 | return $self->{message_catalog}; | |
1102 | } | |
1103 | ||
1104 | =item catmsg($id) | |
1105 | ||
1106 | =item catmsg($id, \@params) | |
1107 | ||
1108 | =item catmsg($id, \@params, $default) | |
1109 | ||
1110 | =item catmsg($id, \@params, $default, $lang) | |
1111 | ||
1112 | Retrieve a message from the message catalog, performing substitution. | |
1113 | ||
1114 | This retrieves the text version of the message only. | |
1115 | ||
1116 | =cut | |
1117 | ||
1118 | sub catmsg { | |
1119 | my ($self, $id, $params, $default, $lang) = @_; | |
1120 | ||
1121 | defined $lang or $lang = $self->language; | |
1122 | defined $params or $params = []; | |
1123 | ||
1124 | $id =~ s/^msg:// | |
1125 | or return "* bad message id - missing leading msg: *"; | |
1126 | ||
1127 | my $result = $self->message_catalog->text($lang, $id, $params, $default); | |
1128 | unless ($result) { | |
1129 | $result = "Unknown message id $id"; | |
1130 | } | |
1131 | ||
1132 | return $result; | |
1133 | } | |
1134 | ||
1135 | =item language | |
1136 | ||
1137 | Fetch the language for the current system/user. | |
1138 | ||
1139 | Warning: this currently fetches a system configured default, in the | |
1140 | future it will use a user default and/or a browser set default. | |
1141 | ||
1142 | =cut | |
1143 | ||
1144 | sub language { | |
1145 | my ($self) = @_; | |
1146 | ||
1147 | return $self->cfg->entry("basic", "language_code", "en"); | |
1148 | } | |
1149 | ||
a74330a2 TC |
1150 | sub ip_address { |
1151 | return $ENV{REMOTE_ADDR}; | |
1152 | } | |
1153 | ||
70789617 | 1154 | 1; |