File Coverage

blib/lib/YATT/Lite/WebMVC0/Connection.pm
Criterion Covered Total %
statement 195 288 67.7
branch 72 164 43.9
condition 38 96 39.5
subroutine 32 45 71.1
pod 0 27 0.0
total 337 620 54.3


line stmt bran cond sub pod time code
1             package YATT::Lite::WebMVC0::Connection; sub PROP () {__PACKAGE__}
2 8     8   41 use strict;
  8         18  
  8         260  
3 8     8   40 use warnings qw(FATAL all NONFATAL misc);
  8         16  
  8         290  
4 8     8   38 use Carp;
  8         16  
  8         474  
5              
6 8     8   42 use base qw(YATT::Lite::Connection);
  8         21  
  8         4149  
7             use YATT::Lite::MFields
8 8         122 (qw/cf_cgi
9             cf_is_psgi cf_hmv
10             params_hash
11              
12             cf_site_prefix
13              
14             cf_no_nested_query
15              
16             cf_no_unicode_params
17              
18             current_user
19 8     8   56 /);
  8         16  
20 8     8   51 use YATT::Lite::Util qw(globref url_encode nonempty lexpand);
  8         26  
  8         557  
21 8     8   47 use YATT::Lite::PSGIEnv;
  8         17  
  8         58  
22              
23 8     8   6445 use YATT::Lite::Util::CGICompat;
  8         27  
  8         97  
24              
25             #----------------------------------------
26              
27             BEGIN {
28             # print STDERR join("\n", sort(keys our %FIELDS)), "\n";
29              
30 8     8   30 foreach my $name (qw(raw_body uploads upload)) {
31 24         86 *{globref(PROP, $name)} = sub {
32 4     4   2380 my PROP $prop = (my $glob = shift)->prop;
33 4 50       23 unless ($prop->{cf_is_psgi}) {
34 0         0 croak "Connection method $name is PSGI mode only!"
35             }
36 4         31 $prop->{cf_cgi}->$name(@_);
37 24         94 };
38             }
39              
40 8         25 foreach my $name (qw(url_param)) {
41 8         41 *{globref(PROP, $name)} = sub {
42 0     0   0 my PROP $prop = (my $glob = shift)->prop;
43 0         0 $prop->{cf_cgi}->$name(@_);
44 8         34 };
45             }
46              
47 8         130 foreach my $item ([referer => 'HTTP_REFERER']
48             , map([lc($_) => uc($_)]
49             , qw/REMOTE_ADDR
50             REQUEST_METHOD
51             SCRIPT_NAME
52             PATH_INFO
53             QUERY_STRING
54             SERVER_NAME
55             SERVER_PORT
56             SERVER_PROTOCOL
57             CONTENT_LENGTH
58             CONTENT_TYPE
59             /)
60             ) {
61 88         168 my ($method, $env) = @$item;
62 88         243 *{globref(PROP, $method)} = sub {
63 3     3   22 my PROP $prop = (my $glob = shift)->prop;
64 3         8 my ($default) = @_;
65 3 50 0     13 if ($prop->{cf_env}) {
    0          
66 3   33     21 $prop->{cf_env}->{$env} // $default;
67             } elsif ($prop->{cf_cgi} and my $sub = $prop->{cf_cgi}->can($method)) {
68 0   0     0 $sub->($prop->{cf_cgi}) // $default;
69             } else {
70 0         0 $default;
71             }
72 88         375 };
73             }
74              
75 8         39 foreach my $name (qw(file subpath)) {
76 16         39 my $cf = "cf_$name";
77 16         54 *{globref(PROP, $name)} = sub {
78 0     0   0 my PROP $prop = (my $glob = shift)->prop;
79 0         0 $prop->{$cf};
80 16         53 };
81             }
82             }
83              
84             #========================================
85              
86             sub param {
87 67     67 0 4078 my PROP $prop = (my $glob = shift)->prop;
88 67 50       264 if (my $ixh = $prop->{params_hash}) {
    0          
    0          
89 67 100       354 return keys %$ixh unless @_;
90 12 50       39 defined (my $key = shift)
91             or croak "undefined key!";
92 12 50       40 if (@_) {
93 0 0       0 if (@_ >= 2) {
94 0         0 $ixh->{$key} = [@_]
95             } else {
96 0         0 $ixh->{$key} = shift;
97             }
98             } else {
99             # If params_hash is enabled, value is returned AS-IS.
100 12         66 $ixh->{$key};
101             }
102             } elsif (my $hmv = $prop->{cf_hmv}) {
103 0 0       0 return $hmv->keys unless @_;
104 0 0       0 if (@_ == 1) {
105 0 0       0 return wantarray ? $hmv->get_all($_[0]) : $hmv->get($_[0]);
106             } else {
107 0         0 $hmv->add(@_);
108 0         0 return $glob;
109             }
110             } elsif (my $cgi = $prop->{cf_cgi}) {
111 0         0 return $cgi->param(@_);
112             } else {
113 0         0 croak "Neither Hash::MultiValue nor CGI is found in connection!";
114             }
115             }
116              
117             # Annoying multi_param support.
118             sub multi_param {
119 1     1 0 4 my PROP $prop = (my $glob = shift)->prop;
120 1 50 0     6 if (my $ixh = $prop->{params_hash}) {
    0          
    0          
121 1 50       4 return keys %$ixh unless @_;
122 1 50       4 defined (my $key = shift)
123             or croak "undefined key!";
124             # If params_hash is enabled, value is returned AS-IS.
125 1         5 $ixh->{$key};
126              
127             } elsif (my $hmv = ($prop->{cf_hmv} // do {
128             $prop->{cf_is_psgi} && $prop->{cf_cgi}->parameters
129 0 0       0 })) {
130 0 0       0 return $hmv->keys unless @_;
131 0 0       0 return wantarray ? $hmv->get_all($_[0]) : $hmv->get($_[0]);
132             } elsif (my $cgi = $prop->{cf_cgi}) {
133 0         0 return $cgi->multi_param(@_);
134             } else {
135 0         0 croak "Neither Hash::MultiValue nor CGI is found in connection!";
136             }
137             }
138              
139             sub queryobj {
140 2     2 0 8 my PROP $prop = (my $glob = shift)->prop;
141 2 50 33     15 $prop->{params_hash} || $prop->{cf_hmv} || $prop->{cf_cgi};
142             }
143              
144             #========================================
145              
146             sub configure_cgi {
147 45     45 0 81501 my PROP $prop = (my $glob = shift)->prop;
148 45         134 $prop->{cf_cgi} = my $cgi = shift;
149 45 50       217 return unless $glob->is_form_content_type($cgi->content_type);
150 45 50       201 unless ($prop->{cf_no_nested_query}) {
151 45 100       126 if ($prop->{cf_is_psgi}) {
152 29         90 $glob->convert_array_param_psgi($cgi);
153             } else {
154 16         61 $glob->convert_array_param_cgi($cgi);
155             }
156             }
157             }
158              
159             sub is_form_content_type {
160 45     45 0 354 my ($self, $real_ct) = @_;
161 45 100 100     343 return 1 if ($real_ct // '') eq '';
162 9         33 foreach my $check_ct ($self->form_content_types) {
163 16 100       111 return 1 if $real_ct =~ $check_ct;
164             }
165 0         0 return 0;
166             }
167              
168             sub form_content_types {
169 9     9 0 69 (qr(^multipart/form-data\s*(?:;|$))i
170             , qr(^application/x-www-form-urlencoded$)i);
171             }
172              
173             sub parse_nested_query {
174 54     54 0 8216 my PROP $prop = (my $glob = shift)->prop;
175 54         124 my ($obj_or_string) = @_;
176             YATT::Lite::Util::parse_nested_query
177             ($obj_or_string
178             , (!$prop->{cf_no_unicode_params} && $prop->{cf_encoding})
179 54   33     340 );
180             }
181              
182             sub convert_array_param_psgi {
183 29     29 0 95 my PROP $prop = (my $glob = shift)->prop;
184 29         53 my ($req) = @_;
185 29         61 my Env $env = $prop->{cf_env};
186 29         40 $prop->{params_hash} = do {
187 29 100 66     121 if ($env->{CONTENT_TYPE} and defined $env->{CONTENT_LENGTH}) {
188 9         40 my $body = $glob->parse_nested_query([$req->body_parameters->flatten]);
189 9         44 my $qs = $glob->parse_nested_query($env->{QUERY_STRING});
190 9         40 foreach my $key (keys %$qs) {
191 5 50       88 if (exists $body->{$key}) {
192 0         0 die $glob->error("Attempt to overwrite post param '%s' by qs"
193             , $key);
194             }
195 5         59 $body->{$key} = $qs->{$key};
196             }
197 9         160 $body;
198             } else {
199 20         72 $glob->parse_nested_query($env->{QUERY_STRING});
200             }
201             };
202             }
203              
204             sub convert_array_param_cgi {
205 16     16 0 62 my PROP $prop = (my $glob = shift)->prop;
206 16         34 my ($cgi) = @_;
207 16 50 50     56 return if ($cgi->content_type // "") eq "application/json";
208             $prop->{params_hash}
209 16         186 = $glob->parse_nested_query($cgi->query_string);
210             }
211              
212             # Location(path part of url) of overall SiteApp.
213             sub site_location {
214 2     2 0 7 my PROP $prop = (my $glob = shift)->prop;
215 2         11 $prop->{cf_site_prefix} . '/';
216             }
217             *site_loc = *site_location; *site_loc = *site_location;
218             sub site_prefix {
219 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
220 0         0 $prop->{cf_site_prefix};
221             }
222              
223             # Location of DirApp
224             sub location {
225 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
226 0   0     0 (my $loc = ($prop->{cf_location} // '')) =~ s,/*$,/,;
227 0         0 $loc;
228             }
229              
230             sub _invoke_or {
231 16     16   30 my ($default, $obj, $method, @args) = @_;
232 16 50 33     91 if (defined $obj and my $sub = $obj->can($method)) {
233 16         42 $sub->($obj, @args)
234             } else {
235 0         0 $default;
236             }
237             }
238              
239             # XXX: parameter の加減算も?
240             # XXX: 絶対 path/相対 path の選択?
241             # scheme
242             # authority
243             # path
244             # query
245             # fragment
246             sub mkurl {
247 17     17 0 64 my PROP $prop = (my $glob = shift)->prop;
248 17         35 my ($file, $param, %opts) = @_;
249              
250 17         22 my $req = do {
251 17 50       38 if ($opts{mapped_path}) {
252 0         0 $glob->mapped_path;
253             } else {
254 17         38 $glob->request_path;
255             }
256             };
257              
258 17         25 my $path = do {
259 17 50 66     67 if (defined $file and $file =~ m!^/!) {
260 0         0 $prop->{cf_site_prefix}.$file;
261             } else {
262 17         27 my ($orig, $dir) = ('');
263 17 100       82 if (($dir = $req) =~ s{([^/]+)$}{}) {
264 13         29 $orig = $1;
265             }
266 17 100 66     73 if (not defined $file or $file eq '') {
    100          
267 11         24 $dir . $orig;
268             } elsif ($file eq '.') {
269 2         6 $dir
270             } else {
271 4         11 $dir . $file;
272             }
273             }
274             };
275              
276             # XXX: /../ truncation
277             # XXX: If sep is '&', scalar ref quoting is required.
278             # XXX: connection should have default separator.
279 17         25 my $url = '';
280 17 100       54 $url .= $glob->mkprefix unless $opts{local};
281 17         60 $url .= $path . $glob->mkquery($param, $opts{separator});
282 17         103 $url;
283             }
284              
285             sub mkprefix {
286 16     16 0 50 my PROP $prop = (my $glob = shift)->prop;
287 16   66     68 my $scheme = $prop->{cf_env}{'psgi.url_scheme'} || $prop->{cf_cgi}->protocol;
288 16         268 my $host = $glob->mkhost($scheme);
289 16         69 $scheme . '://' . $host . join("", @_);
290             }
291              
292             sub mkhost {
293 18     18 0 62 my PROP $prop = (my $glob = shift)->prop;
294 18         35 my ($scheme) = @_;
295 18   100     44 $scheme ||= 'http';
296 18         30 my $env = $prop->{cf_env};
297              
298             # XXX? Is this secure?
299 18 100       61 return $env->{HTTP_HOST} if nonempty($env->{HTTP_HOST});
300              
301             my $base = $env->{SERVER_NAME}
302 8   33     37 // _invoke_or('localhost', $prop->{cf_cgi}, 'server_name');
303 8 50 33     121 if (my $port = $env->{SERVER_PORT}
304             || _invoke_or(80, $prop->{cf_cgi}, 'server_port')) {
305 8 0 33     80 $base .= ":$port" unless ($scheme eq 'http' and $port == 80
      0        
      33        
306             or $scheme eq 'https' and $port == 443);
307             }
308 8         18 $base;
309             }
310              
311             sub mkquery {
312 18     18 0 47 my ($self, $param, $sep) = @_;
313 18   50     67 $sep //= '&';
314              
315 18         23 my @enc_param;
316 18         23 my ($fkeys, $fgetall);
317 18 100 66     117 if (not defined $param or not ref $param) {
318 14 50       54 return wantarray ? () : '';
319             # nop
320             }
321              
322 4 100       18 if (UNIVERSAL::isa($param, ref $self)) {
323             # $CON->mkquery($CON) == $CON->mkquery($CON->queryobj)
324 2         7 $param = $param->queryobj;
325             }
326              
327 4 100 33     29 if (ref $param eq 'HASH') {
    50 33        
    0          
328             push @enc_param, $self->url_encode($_).'='.$self->url_encode($param->{$_})
329 2         15 for sort keys %$param;
330             } elsif ($fkeys = UNIVERSAL::can($param, 'keys')
331             and $fgetall = UNIVERSAL::can($param, 'get_all')
332             or ($fkeys = $fgetall = UNIVERSAL::can($param, 'param'))) {
333 2         7 foreach my $key (YATT::Lite::Util::unique($fkeys->($param))) {
334 4         30 my $enc = $self->url_encode($key);
335             push @enc_param, "$enc=".$self->url_encode($_)
336 4         11 for $fgetall->($param, $key);
337             }
338             } elsif (ref $param eq 'ARRAY') {
339 0         0 my @list = @$param;
340 0         0 while (my ($key, $value) = splice @list, 0, 2) {
341 0         0 push @enc_param, $self->url_encode($key).'='.$self->url_encode($value);
342             }
343             }
344              
345 4 50       13 unless (@enc_param) {
346 0 0       0 wantarray ? () : '';
347             } else {
348 4 50       24 wantarray ? @enc_param : '?'.join($sep, @enc_param);
349             }
350             }
351              
352             sub mapped_path {
353 8     8 0 459 my PROP $prop = (my $glob = shift)->prop;
354 8         12 my @path = do {
355 8   100     30 my $loc = $prop->{cf_location} // "/";
356             $loc .= $prop->{cf_file} if defined $prop->{cf_file}
357 8 100 100     47 and not $prop->{cf_is_index};
358 8         24 ($loc);
359             };
360 8 100       25 if (defined (my $sp = $prop->{cf_subpath})) {
361 6         23 $sp =~ s!^/*!/!;
362 6         15 push @path, $sp;
363             }
364 8 100       19 if (wantarray) {
365 4         30 @path;
366             } else {
367 4         10 my $res = join "", @path;
368 4         13 $res =~ s!^/+!/!;
369 4         28 $res;
370             }
371             }
372              
373             sub request_path {
374 18   100 18 0 160 (my $uri = shift->request_uri // '') =~ s/\?.*//;
375 18         43 $uri;
376             }
377              
378             sub request_uri {
379 18     18 0 77 my PROP $prop = (my $glob = shift)->prop;
380 18 100 33     52 if ($prop->{cf_env}) {
    50          
381 17         76 $prop->{cf_env}{REQUEST_URI};
382             } elsif ($prop->{cf_cgi}
383             and my $sub = $prop->{cf_cgi}->can('request_uri')) {
384 0         0 $sub->($prop->{cf_cgi});
385             } else {
386 1         9 $ENV{REQUEST_URI};
387             }
388             }
389              
390             #========================================
391              
392             sub redirect {
393 3     3 0 28 my PROP $prop = (my $glob = shift)->prop;
394 3 50 33     26 croak "undefined url" unless @_ and defined $_[0];
395 3         5 my $url = do {
396 3 100 0     14 if (ref $_[0]) {
    50 33        
397             # To do external redirect, $url should pass as SCALAR REF.
398 2         4 my $arg = shift;
399             # die "redirect url is not a scalar ref: $arg";
400 2         4 $$arg;
401             } elsif ($_[0] =~ m{^(?:\w+:)?//([^/]+)}
402             and $1 ne ($glob->mkhost // '')) {
403 0         0 die $glob->error("External redirect is not allowed: %s", $_[0]);
404             } else {
405             # taint check
406 1         2 shift;
407             }
408             };
409 3 50       15 if ($prop->{header_was_sent}++) {
410 0         0 die "Can't redirect multiple times!";
411             }
412              
413             # Make sure session is flushed before redirection.
414 3         13 $glob->finalize_headers;
415              
416 3         5 ${$prop->{cf_buffer}} = '';
  3         8  
417              
418 3         20 die [302, [Location => $url, $glob->list_header], []];
419             }
420              
421             #========================================
422             # Session support is delegated to 'system'.
423             # 'system' must implement session_{start,resume,flush,destroy}
424              
425             # To avoid confusion against $system->session_$verb,
426             # connection side interface is named ${verb}_session.
427              
428             sub get_session {
429 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
430             # To avoid repeative false session tests.
431 0 0       0 if (exists $prop->{session}) {
432 0         0 $prop->{session};
433             } else {
434 0         0 $prop->{cf_system}->session_resume($glob);
435             }
436             }
437              
438             sub start_session {
439 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
440 0 0       0 if (defined (my $sess = $prop->{session})) {
441 0         0 die $glob->error("load_session is called twice! sid=%s", $sess->id);
442             }
443 0         0 $prop->{cf_system}->session_start($glob, @_);
444             }
445              
446             sub delete_session {
447 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
448 0         0 $prop->{cf_system}->session_delete($glob);
449             }
450              
451             sub flush_session {
452 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
453 0         0 $prop->{cf_system}->session_flush($glob);
454             }
455              
456             #========================================
457              
458             sub current_user {
459 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
460 0         0 my $cu = do {
461 0 0       0 if (exists $prop->{current_user}) {
    0          
462             $prop->{current_user}
463 0         0 } elsif (defined $prop->{cf_system}) {
464 0         0 $prop->{current_user} = $prop->{cf_system}->load_current_user($glob);
465             } else {
466 0         0 $prop->{current_user} = undef;
467             }
468             };
469              
470 0 0       0 return $cu unless @_;
471 0 0       0 die $glob->error("current_user is empty") unless defined $cu;
472 0         0 my $method = shift;
473              
474 0         0 $cu->$method(@_);
475             }
476              
477             #========================================
478              
479 8     8   2454 use YATT::Lite::RegexpNames; # For re_name, re_integer, ...
  8         22  
  8         7209  
480              
481             sub param_type {
482 0     0 0 0 my PROP $prop = (my $glob = shift)->prop;
483 0   0     0 my $name = shift // croak "Undefined name!";
484 0   0     0 my $type = shift // croak "Undefined type!";
485 0         0 my $diag = shift;
486 0         0 my $opts = shift;
487 0 0       0 my $pat = ref $type eq 'Regexp' ? $type : do {
488 0 0       0 my $pat_sub = $glob->can("re_$type")
489             or croak "Unknown type: $type";
490 0         0 $pat_sub->();
491             };
492              
493 0         0 my $value = $glob->param($name);
494              
495 0 0 0     0 if (defined $value && $value =~ $pat) {
    0          
    0          
496 0         0 return $&; # Also for taint check.
497             } elsif ($diag) {
498 0 0       0 die $glob->error_with_status
499             (400, (ref $diag eq 'CODE' ? $diag->($value) : $diag)
500             , $name, $value);
501             } elsif (not defined $value) {
502 0 0       0 return undef if $opts->{allow_undef};
503 0         0 die $glob->error_with_status
504             (400, "Parameter '%s' is missing!", $name);
505             } else {
506             # Just for default message. Production code should provide $diag.
507 0         0 die $glob->error_with_status
508             (400, "Parameter '%s' must match %s!: '%s'"
509             , $name, $type, $value);
510             }
511             }
512              
513             #========================================
514              
515             sub accept_language {
516 4     4 0 19 my PROP $prop = (my $glob = shift)->prop;
517 4         12 my (%opts) = @_;
518 4         7 my $filter = delete $opts{filter};
519 4         8 my $detail = delete $opts{detail};
520 4         7 my $long = delete $opts{long};
521 4 50       15 if (keys %opts) {
522 0         0 die $glob->error("Unknown option for accept_language: %s"
523             , join ", ", keys %opts);
524             }
525              
526 4         8 my Env $env = $prop->{cf_env};
527             my $langlist = $env->{HTTP_ACCEPT_LANGUAGE}
528 4 50       15 or return;
529             my @langlist = sort {
530 12         37 $$b[-1] <=> $$a[-1]
531             } map {
532 4         27 my ($lang, $qual) = split /\s*;\s*q=/;
  12         42  
533 12   100     72 [$lang, $qual // 1]
534             } split /\s*,\s*/, $langlist;
535              
536 4 50       14 if ($filter) {
537 0         0 my $filtsub = do {
538 0 0       0 if (ref $filter eq 'CODE') {
    0          
    0          
    0          
539 0         0 $filter
540             } elsif (ref $filter eq 'Regexp') {
541 0     0   0 sub { grep {$$_[0] =~ $filter} @_ }
  0         0  
542 0         0 } elsif (ref $filter eq 'HASH') {
543 0     0   0 sub { grep {$filter->{$$_[0]}} @_ }
  0         0  
544 0         0 } elsif (ref $filter eq 'ARRAY') {
545 0         0 my $hash = +{map {$_ => 1} lexpand($filter)};
  0         0  
546 0     0   0 sub { grep {$hash->{$$_[0]}} @_ }
  0         0  
547 0         0 } else {
548 0         0 die $glob->error("Unknown filter type for accept_language");
549             }
550             };
551 0         0 @langlist = $filtsub->(@langlist);
552             }
553              
554 4 100       10 if ($detail) {
555             @langlist
556 1         12 } else {
557 3 100       11 if ($long) {
558             # en-US => en_US
559 1         12 $$_[0] =~ s/-/_/g for @langlist;
560             } else {
561             # en-US => en
562 2         16 $$_[0] =~ s/-.*// for @langlist;
563             }
564 3         4 my %dup;
565 3 100       16 wantarray ? (map {$dup{$$_[0]}++ ? () : $$_[0]} @langlist)
  6 100       46  
566             : $langlist[0][0];
567             }
568             }
569              
570             1;