File Coverage

blib/lib/CGI/Tiny.pm
Criterion Covered Total %
statement 512 533 96.0
branch 280 358 78.2
condition 91 136 66.9
subroutine 82 84 97.6
pod 64 65 98.4
total 1029 1176 87.5


line stmt bran cond sub pod time code
1             package CGI::Tiny;
2             # ABSTRACT: Common Gateway Interface, with no frills
3              
4             # This file is part of CGI::Tiny which is released under:
5             # The Artistic License 2.0 (GPL Compatible)
6             # See the documentation for CGI::Tiny for full license details.
7              
8 7     7   9049 use strict;
  7         50  
  7         203  
9 7     7   35 use warnings;
  7         14  
  7         159  
10 7     7   29 use Carp ();
  7         14  
  7         89  
11 7     7   4023 use IO::Handle ();
  7         46472  
  7         154  
12 7     7   48 use Exporter ();
  7         14  
  7         217  
13              
14             our $VERSION = '1.002';
15              
16 7     7   35 use constant DEFAULT_REQUEST_BODY_LIMIT => 16777216;
  7         15  
  7         881  
17 7     7   43 use constant DEFAULT_REQUEST_BODY_BUFFER => 262144;
  7         14  
  7         420  
18 7     7   49 use constant DEFAULT_RESPONSE_BODY_BUFFER => 131072;
  7         14  
  7         58425  
19              
20             our @EXPORT = 'cgi';
21              
22             # List from HTTP::Status 6.29
23             # Unmarked codes are from RFC 7231 (2017-12-20)
24             my %HTTP_STATUS = (
25             100 => 'Continue',
26             101 => 'Switching Protocols',
27             102 => 'Processing', # RFC 2518: WebDAV
28             103 => 'Early Hints', # RFC 8297: Indicating Hints
29             200 => 'OK',
30             201 => 'Created',
31             202 => 'Accepted',
32             203 => 'Non-Authoritative Information',
33             204 => 'No Content',
34             205 => 'Reset Content',
35             206 => 'Partial Content', # RFC 7233: Range Requests
36             207 => 'Multi-Status', # RFC 4918: WebDAV
37             208 => 'Already Reported', # RFC 5842: WebDAV bindings
38             226 => 'IM Used', # RFC 3229: Delta encoding
39             300 => 'Multiple Choices',
40             301 => 'Moved Permanently',
41             302 => 'Found',
42             303 => 'See Other',
43             304 => 'Not Modified', # RFC 7232: Conditional Request
44             305 => 'Use Proxy',
45             307 => 'Temporary Redirect',
46             308 => 'Permanent Redirect', # RFC 7528: Permanent Redirect
47             400 => 'Bad Request',
48             401 => 'Unauthorized', # RFC 7235: Authentication
49             402 => 'Payment Required',
50             403 => 'Forbidden',
51             404 => 'Not Found',
52             405 => 'Method Not Allowed',
53             406 => 'Not Acceptable',
54             407 => 'Proxy Authentication Required', # RFC 7235: Authentication
55             408 => 'Request Timeout',
56             409 => 'Conflict',
57             410 => 'Gone',
58             411 => 'Length Required',
59             412 => 'Precondition Failed', # RFC 7232: Conditional Request
60             413 => 'Payload Too Large',
61             414 => 'URI Too Long',
62             415 => 'Unsupported Media Type',
63             416 => 'Range Not Satisfiable', # RFC 7233: Range Requests
64             417 => 'Expectation Failed',
65             418 => 'I\'m a teapot', # RFC 2324: HTCPC/1.0 1-april
66             421 => 'Misdirected Request', # RFC 7540: HTTP/2
67             422 => 'Unprocessable Entity', # RFC 4918: WebDAV
68             423 => 'Locked', # RFC 4918: WebDAV
69             424 => 'Failed Dependency', # RFC 4918: WebDAV
70             425 => 'Too Early', # RFC 8470: Using Early Data in HTTP
71             426 => 'Upgrade Required',
72             428 => 'Precondition Required', # RFC 6585: Additional Codes
73             429 => 'Too Many Requests', # RFC 6585: Additional Codes
74             431 => 'Request Header Fields Too Large', # RFC 6585: Additional Codes
75             451 => 'Unavailable For Legal Reasons', # RFC 7725: Legal Obstacles
76             500 => 'Internal Server Error',
77             501 => 'Not Implemented',
78             502 => 'Bad Gateway',
79             503 => 'Service Unavailable',
80             504 => 'Gateway Timeout',
81             505 => 'HTTP Version Not Supported',
82             506 => 'Variant Also Negotiates', # RFC 2295: Transparant Ngttn
83             507 => 'Insufficient Storage', # RFC 4918: WebDAV
84             508 => 'Loop Detected', # RFC 5842: WebDAV bindings
85             509 => 'Bandwidth Limit Exceeded', # Apache / cPanel
86             510 => 'Not Extended', # RFC 2774: Extension Framework
87             511 => 'Network Authentication Required', # RFC 6585: Additional Codes
88             );
89              
90             {
91             my $cgi;
92              
93             sub import {
94             # for cleanup in END in case of premature exit
95 8   50 8   7689993 $cgi ||= bless {pid => $$}, $_[0];
96 8         1133 goto &Exporter::import;
97             }
98              
99             sub cgi (&) {
100 75     75 0 8078357 my ($handler) = @_;
101 75   100     1079 $cgi ||= bless {pid => $$}, __PACKAGE__;
102 75 50 33     341 if (@ARGV and !defined $ENV{REQUEST_METHOD}) {
103 0         0 require CGI::Tiny::_Debug;
104 0         0 CGI::Tiny::_Debug::debug_command($cgi, [@ARGV]);
105             }
106 75         142 my ($error, $errored);
107             {
108 75         139 local $@;
  75         147  
109 75 100       162 eval { local $_ = $cgi; $handler->(); 1 } or do { $error = $@; $errored = 1 };
  75         156  
  75         248  
  65         240  
  8         152  
  8         28  
110             }
111 73 100       288 if ($errored) {
    100          
112 8         41 _handle_error($cgi, $error);
113             } elsif (!$cgi->{headers_rendered}) {
114 20         55 _handle_error($cgi, "cgi completed without rendering a response\n");
115             }
116 73         421 undef $cgi;
117 73         198 1;
118             }
119              
120             # cleanup of premature exit, more reliable than potentially doing this in global destruction
121             # ModPerl::Registry or CGI::Compile won't run END after each request,
122             # but they override exit to throw an exception which we handle already
123             END {
124 7 100   7   6653 if (defined $cgi) {
125 5 50       122 _handle_error($cgi, "cgi exited without rendering a response\n") unless $cgi->{headers_rendered};
126 5         62 undef $cgi;
127             }
128             }
129             }
130              
131             sub _handle_error {
132 33     33   136 my ($cgi, $error) = @_;
133 33 100       160 return unless $cgi->{pid} == $$; # in case of fork
134             $cgi->{response_status} = "500 $HTTP_STATUS{500}" unless $cgi->{headers_rendered}
135 32 100 100     400 or (defined $cgi->{response_status} and $cgi->{response_status} =~ m/^[45][0-9]{2} /);
      100        
136 32 100       96 if (defined(my $handler = $cgi->{on_error})) {
137 30         51 my ($error_error, $error_errored);
138             {
139 30         126 local $@;
  30         46  
140 30 50       55 eval { $handler->($cgi, $error, !!$cgi->{headers_rendered}); 1 } or do { $error_error = $@; $error_errored = 1 };
  30         101  
  30         237  
  0         0  
  0         0  
141             }
142 30 50       83 return unless $cgi->{pid} == $$; # in case of fork in error handler
143 30 50       73 if ($error_errored) {
144 0         0 warn "Exception in error handler: $error_error";
145 0         0 warn "Original error: $error";
146             }
147             } else {
148 2         83 warn $error;
149             }
150 32 100       258 $cgi->set_response_type('text/plain')->render(data => $cgi->{response_status}) unless $cgi->{headers_rendered};
151             }
152              
153 30     30 1 352 sub set_error_handler { $_[0]{on_error} = $_[1]; $_[0] }
  30         63  
154 0     0 1 0 sub set_request_body_buffer { $_[0]{request_body_buffer} = $_[1]; $_[0] }
  0         0  
155 1     1 1 14 sub set_request_body_limit { $_[0]{request_body_limit} = $_[1]; $_[0] }
  1         86  
156 5     5 1 58 sub set_multipart_form_options { $_[0]{multipart_form_options} = $_[1]; $_[0] }
  5         14  
157 5     5 1 39 sub set_multipart_form_charset { $_[0]{multipart_form_charset} = $_[1]; $_[0] }
  5         18  
158 75     75 1 464 sub set_input_handle { $_[0]{input_handle} = $_[1]; $_[0] }
  75         137  
159 75     75 1 382 sub set_output_handle { $_[0]{output_handle} = $_[1]; $_[0] }
  75         139  
160              
161 1 50   1 1 55 sub auth_type { defined $ENV{AUTH_TYPE} ? $ENV{AUTH_TYPE} : '' }
162 1 50   1 1 17 sub content_length { defined $ENV{CONTENT_LENGTH} ? $ENV{CONTENT_LENGTH} : '' }
163 1 50   1 1 9 sub content_type { defined $ENV{CONTENT_TYPE} ? $ENV{CONTENT_TYPE} : '' }
164 1 50   1 1 7 sub gateway_interface { defined $ENV{GATEWAY_INTERFACE} ? $ENV{GATEWAY_INTERFACE} : '' }
165 2 50   2 1 26 sub path_info { defined $ENV{PATH_INFO} ? $ENV{PATH_INFO} : '' }
166             *path = \&path_info;
167 1 50   1 1 8 sub path_translated { defined $ENV{PATH_TRANSLATED} ? $ENV{PATH_TRANSLATED} : '' }
168 5 50   5 1 64 sub query_string { defined $ENV{QUERY_STRING} ? $ENV{QUERY_STRING} : '' }
169             *query = \&query_string;
170 1 50   1 1 17 sub remote_addr { defined $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : '' }
171 1 50   1 1 10 sub remote_host { defined $ENV{REMOTE_HOST} ? $ENV{REMOTE_HOST} : '' }
172 1 50   1 1 9 sub remote_ident { defined $ENV{REMOTE_IDENT} ? $ENV{REMOTE_IDENT} : '' }
173 1 50   1 1 8 sub remote_user { defined $ENV{REMOTE_USER} ? $ENV{REMOTE_USER} : '' }
174 2 50   2 1 27 sub request_method { defined $ENV{REQUEST_METHOD} ? $ENV{REQUEST_METHOD} : '' }
175             *method = \&request_method;
176 1 50   1 1 16 sub script_name { defined $ENV{SCRIPT_NAME} ? $ENV{SCRIPT_NAME} : '' }
177 1 50   1 1 17 sub server_name { defined $ENV{SERVER_NAME} ? $ENV{SERVER_NAME} : '' }
178 1 50   1 1 8 sub server_port { defined $ENV{SERVER_PORT} ? $ENV{SERVER_PORT} : '' }
179 1 50   1 1 9 sub server_protocol { defined $ENV{SERVER_PROTOCOL} ? $ENV{SERVER_PROTOCOL} : '' }
180 1 50   1 1 8 sub server_software { defined $ENV{SERVER_SOFTWARE} ? $ENV{SERVER_SOFTWARE} : '' }
181              
182             sub headers {
183 1     1 1 11 my ($self) = @_;
184 1 50       5 unless (exists $self->{request_headers}) {
185 1         2 my %headers;
186 1         34 foreach my $key (keys %ENV) {
187 54         65 my $name = $key;
188 54 100       105 next unless $name =~ s/^HTTP_//;
189 3         7 $name =~ tr/_/-/;
190 3         17 $headers{lc $name} = $ENV{$key};
191             }
192 1         12 $self->{request_headers} = \%headers;
193             }
194 1         3 return {%{$self->{request_headers}}};
  1         8  
195             }
196              
197 2     2 1 14 sub header { (my $name = $_[1]) =~ tr/-/_/; $ENV{"HTTP_\U$name"} }
  2         8  
198              
199 1     1 1 7 sub cookies { [map { [@$_] } @{$_[0]->_cookies->{ordered}}] }
  4         13  
  1         5  
200 1     1 1 7 sub cookie_names { [@{$_[0]->_cookies->{names}}] }
  1         4  
201 2 100   2 1 11 sub cookie { my $c = $_[0]->_cookies->{keyed}; exists $c->{$_[1]} ? $c->{$_[1]}[-1] : undef }
  2         9  
202 1 50   1 1 6 sub cookie_array { my $c = $_[0]->_cookies->{keyed}; exists $c->{$_[1]} ? [@{$c->{$_[1]}}] : [] }
  1         4  
  1         4  
203              
204             sub _cookies {
205 5     5   9 my ($self) = @_;
206 5 100       13 unless (exists $self->{request_cookies}) {
207 1         16 $self->{request_cookies} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
208 1 50       7 if (defined $ENV{HTTP_COOKIE}) {
209 1         24 foreach my $pair (split /\s*;\s*/, $ENV{HTTP_COOKIE}) {
210 4 50       14 next unless length $pair;
211 4         13 my ($name, $value) = split /=/, $pair, 2;
212 4 50       10 next unless defined $value;
213 4 100       11 push @names, $name unless exists $keyed{$name};
214 4         9 push @ordered, [$name, $value];
215 4         7 push @{$keyed{$name}}, $value;
  4         22  
216             }
217             }
218             }
219 5         14 return $self->{request_cookies};
220             }
221              
222 1     1 1 13 sub params { [map { [@$_] } @{$_[0]->_query_params->{ordered}}, @{$_[0]->_body_params->{ordered}}] }
  8         23  
  1         6  
  1         8  
223 1     1 1 8 sub param_names { my $q = $_[0]->_query_params; [@{$q->{names}}, grep { !exists $q->{keyed}{$_} } @{$_[0]->_body_params->{names}}] }
  1         3  
  1         3  
  3         19  
  1         4  
224             sub param {
225 3     3 1 23 my ($self, $name) = @_;
226 3         15 my $p = $self->_body_params->{keyed};
227 3 100       13 return $p->{$name}[-1] if exists $p->{$name};
228 2         14 my $q = $self->_query_params->{keyed};
229 2 100       11 return exists $q->{$name} ? $q->{$name}[-1] : undef;
230             }
231 3 100   3 1 26 sub param_array { [map { exists $_->{$_[1]} ? @{$_->{$_[1]}} : () } $_[0]->_query_params->{keyed}, $_[0]->_body_params->{keyed}] }
  6         21  
  4         17  
232              
233 1     1 1 12 sub query_params { [map { [@$_] } @{$_[0]->_query_params->{ordered}}] }
  4         12  
  1         4  
234 1     1 1 6 sub query_param_names { [@{$_[0]->_query_params->{names}}] }
  1         3  
235 2 100   2 1 9 sub query_param { my $p = $_[0]->_query_params->{keyed}; exists $p->{$_[1]} ? $p->{$_[1]}[-1] : undef }
  2         27  
236 2 100   2 1 11 sub query_param_array { my $p = $_[0]->_query_params->{keyed}; exists $p->{$_[1]} ? [@{$p->{$_[1]}}] : [] }
  2         8  
  1         4  
237              
238             sub _query_params {
239 13     13   22 my ($self) = @_;
240 13 100       56 unless (exists $self->{query_params}) {
241 3         43 $self->{query_params} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
242 3         20 foreach my $pair (split /[&;]/, $self->query) {
243 10         37 my ($name, $value) = split /=/, $pair, 2;
244 10 50       26 $value = '' unless defined $value;
245 10         21 do { tr/+/ /; s/%([0-9a-fA-F]{2})/chr hex $1/ge; utf8::decode $_ } for $name, $value;
  20         34  
  20         49  
  8         32  
  20         48  
246 10 100       33 push @names, $name unless exists $keyed{$name};
247 10         32 push @ordered, [$name, $value];
248 10         23 push @{$keyed{$name}}, $value;
  10         51  
249             }
250             }
251 13         38 return $self->{query_params};
252             }
253              
254             sub body {
255 6     6 1 32 my ($self) = @_;
256 6 50 33     53 unless (exists $self->{body_content} or exists $self->{body_parts}) {
257 6         26 $self->{body_content} = '';
258 6         38 my $length = $self->_body_length;
259 5 50       21 my $in_fh = defined $self->{input_handle} ? $self->{input_handle} : *STDIN;
260 5         23 binmode $in_fh;
261 5   50     43 my $buffer_size = 0 + ($self->{request_body_buffer} || $ENV{CGI_TINY_REQUEST_BODY_BUFFER} || DEFAULT_REQUEST_BODY_BUFFER);
262 5         19 while ($length > 0) {
263 5 50       17 my $chunk = $length < $buffer_size ? $length : $buffer_size;
264 5 50       69 last unless my $read = read $in_fh, $self->{body_content}, $chunk, length $self->{body_content};
265 5         22 $length -= $read;
266             }
267             }
268 5         42 return $self->{body_content};
269             }
270              
271             sub body_json {
272 4     4 1 24 my ($self) = @_;
273 4 100       16 unless (exists $self->{body_json}) {
274 3         15 $self->{body_json} = undef;
275 3 100 66     37 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^application\/json\b/i) {
276 2         11 $self->{body_json} = $self->_json->decode($self->body);
277             }
278             }
279 4         14 return $self->{body_json};
280             }
281              
282 8     8 1 49 sub body_params { [map { [@$_] } @{$_[0]->_body_params->{ordered}}] }
  14         52  
  8         40  
283 7     7 1 35 sub body_param_names { [@{$_[0]->_body_params->{names}}] }
  7         20  
284 8 100   8 1 36 sub body_param { my $p = $_[0]->_body_params->{keyed}; exists $p->{$_[1]} ? $p->{$_[1]}[-1] : undef }
  8         42  
285 6 100   6 1 29 sub body_param_array { my $p = $_[0]->_body_params->{keyed}; exists $p->{$_[1]} ? [@{$p->{$_[1]}}] : [] }
  6         25  
  5         18  
286              
287             sub _body_params {
288 37     37   62 my ($self) = @_;
289 37 100       75 unless (exists $self->{body_params}) {
290 9         69 $self->{body_params} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
291 9 100 100     147 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^application\/x-www-form-urlencoded\b/i) {
    100 66        
292 2         12 foreach my $pair (split /&/, $self->body) {
293 8         27 my ($name, $value) = split /=/, $pair, 2;
294 8 50       19 $value = '' unless defined $value;
295 8         14 do { tr/+/ /; s/%([0-9a-fA-F]{2})/chr hex $1/ge; utf8::decode $_ } for $name, $value;
  16         24  
  16         53  
  10         47  
  16         34  
296 8 100       25 push @names, $name unless exists $keyed{$name};
297 8         22 push @ordered, [$name, $value];
298 8         10 push @{$keyed{$name}}, $value;
  8         28  
299             }
300             } elsif ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i) {
301 6         18 my $default_charset = $self->{multipart_form_charset};
302 6 100       16 $default_charset = 'UTF-8' unless defined $default_charset;
303 6         10 foreach my $part (@{$self->_body_multipart}) {
  6         16  
304 18 100       65 next if defined $part->{filename};
305 10         38 my ($name, $headers, $content, $file) = @$part{'name','headers','content','file'};
306 10 50       38 if (length $default_charset) {
307 10         57 require Encode;
308 10         54 $name = Encode::decode($default_charset, "$name");
309             }
310 10         710 my $value = '';
311 10 100       29 if (defined $content) {
    50          
312 9         22 $value = $content;
313             } elsif (defined $file) {
314 1         6 binmode $file;
315 1         12 seek $file, 0, 0;
316 1         4 $value = do { local $/; readline $file };
  1         7  
  1         30  
317 1         10 seek $file, 0, 0;
318             }
319 10         17 my $value_charset;
320 10 100       27 if (defined $headers->{'content-type'}) {
321 5 50       50 if (my ($charset_quoted, $charset_unquoted) = $headers->{'content-type'} =~ m/;\s*charset=(?:"((?:\\[\\"]|[^"])+)"|([^";]+))/i) {
322 5 50       12 $charset_quoted =~ s/\\([\\"])/$1/g if defined $charset_quoted;
323 5 50       16 $value_charset = defined $charset_quoted ? $charset_quoted : $charset_unquoted;
324             }
325             }
326 10 50 66     89 if (defined $value_charset or !defined $headers->{'content-type'} or $headers->{'content-type'} =~ m/^text\/plain\b/i) {
      33        
327 10         46 require Encode;
328 10 100       32 if (defined $value_charset) {
    50          
329 5         33 $value = Encode::decode($value_charset, "$value");
330             } elsif (length $default_charset) {
331 5         15 $value = Encode::decode($default_charset, "$value");
332             }
333             }
334 10 100       499 push @names, $name unless exists $keyed{$name};
335 10         27 push @ordered, [$name, $value];
336 10         15 push @{$keyed{$name}}, $value;
  10         42  
337             }
338             }
339             }
340 37         109 return $self->{body_params};
341             }
342              
343             sub body_parts {
344 9     9 1 68 my ($self) = @_;
345 9 100 66     111 return [] unless $ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i;
346 8         25 return [map { +{%$_} } @{$self->_body_multipart}];
  18         149  
  8         42  
347             }
348              
349 6     6 1 30 sub uploads { [map { [@$_] } @{$_[0]->_body_uploads->{ordered}}] }
  8         29  
  6         20  
350 6     6 1 28 sub upload_names { [@{$_[0]->_body_uploads->{names}}] }
  6         14  
351 7 100   7 1 55 sub upload { my $u = $_[0]->_body_uploads->{keyed}; exists $u->{$_[1]} ? $u->{$_[1]}[-1] : undef }
  7         32  
352 6 100   6 1 28 sub upload_array { my $u = $_[0]->_body_uploads->{keyed}; exists $u->{$_[1]} ? [@{$u->{$_[1]}}] : [] }
  6         15  
  5         16  
353              
354             sub _body_uploads {
355 25     25   41 my ($self) = @_;
356 25 100       54 unless (exists $self->{body_uploads}) {
357 6         44 $self->{body_uploads} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
358 6 50 33     62 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i) {
359 6         53 my $default_charset = $self->{multipart_form_charset};
360 6 100       18 $default_charset = 'UTF-8' unless defined $default_charset;
361 6         17 foreach my $part (@{$self->_body_multipart}) {
  6         17  
362 18 100       48 next unless defined $part->{filename};
363 8         40 my ($name, $filename, $size, $headers, $file, $content) = @$part{'name','filename','size','headers','file','content'};
364 8 50       20 if (length $default_charset) {
365 8         35 require Encode;
366 8         28 $name = Encode::decode($default_charset, "$name");
367 8         374 $filename = Encode::decode($default_charset, "$filename");
368             }
369             my $upload = {
370             filename => $filename,
371             size => $size,
372 8         334 content_type => $headers->{'content-type'},
373             };
374 8 100       26 $upload->{file} = $file if defined $file;
375 8 100       19 $upload->{content} = $content if defined $content;
376 8 100       24 push @names, $name unless exists $keyed{$name};
377 8         25 push @ordered, [$name, $upload];
378 8         16 push @{$keyed{$name}}, $upload;
  8         33  
379             }
380             }
381             }
382 25         69 return $self->{body_uploads};
383             }
384              
385             sub _body_length {
386 12     12   31 my ($self) = @_;
387 12         28 my $limit = $self->{request_body_limit};
388 12 100       53 $limit = $ENV{CGI_TINY_REQUEST_BODY_LIMIT} unless defined $limit;
389 12 100       48 $limit = DEFAULT_REQUEST_BODY_LIMIT unless defined $limit;
390 12   50     48 my $length = $ENV{CONTENT_LENGTH} || 0;
391 12 100 66     98 if ($limit and $length > $limit) {
392 1 50       10 $self->{response_status} = "413 $HTTP_STATUS{413}" unless $self->{headers_rendered};
393 1         16 die "Request body limit exceeded\n";
394             }
395 11         35 return 0 + $length;
396             }
397              
398             sub _body_multipart {
399 20     20   39 my ($self) = @_;
400 20 100       51 unless (exists $self->{body_parts}) {
401 8         25 $self->{body_parts} = [];
402 8         942 require CGI::Tiny::Multipart;
403 8         41 my $boundary = CGI::Tiny::Multipart::extract_multipart_boundary($ENV{CONTENT_TYPE});
404 8 100       31 unless (defined $boundary) {
405 1 50       10 $self->{response_status} = "400 $HTTP_STATUS{400}" unless $self->{headers_rendered};
406 1         9 die "Malformed multipart/form-data request\n";
407             }
408              
409 7         18 my ($input, $length);
410 7 100       22 if (exists $self->{body_content}) {
411 1         3 $length = length $self->{body_content};
412 1         3 $input = \$self->{body_content};
413             } else {
414 6         29 $length = $self->_body_length;
415 6 50       27 $input = defined $self->{input_handle} ? $self->{input_handle} : *STDIN;
416             }
417              
418             my $parts = CGI::Tiny::Multipart::parse_multipart_form_data($input, $length, $boundary, {
419             buffer_size => $self->{request_body_buffer} || $ENV{CGI_TINY_REQUEST_BODY_BUFFER},
420 7 100 33     45 %{$self->{multipart_form_options} || {}},
  7         60  
421             });
422 7 100       98 unless (defined $parts) {
423 1 50       7 $self->{response_status} = "400 $HTTP_STATUS{400}" unless $self->{headers_rendered};
424 1         8 die "Malformed multipart/form-data request\n";
425             }
426              
427 6         21 $self->{body_parts} = $parts;
428             }
429 18         66 return $self->{body_parts};
430             }
431              
432             sub set_nph {
433 2     2 1 11 my ($self, $value) = @_;
434 2 50       8 if ($self->{headers_rendered}) {
435 0         0 Carp::carp "Attempted to set NPH response mode but headers have already been rendered";
436             } else {
437 2 100       13 $self->{nph} = @_ < 2 ? 1 : $value;
438             }
439 2         7 return $self;
440             }
441              
442 0     0 1 0 sub set_response_body_buffer { $_[0]{response_body_buffer} = $_[1]; $_[0] }
  0         0  
443              
444             sub set_response_status {
445 15     15 1 127 my ($self, $status) = @_;
446 15 50       55 if ($self->{headers_rendered}) {
447 0         0 Carp::carp "Attempted to set HTTP response status but headers have already been rendered";
448             } else {
449 15 100 66     132 if (defined $status and $status =~ m/\A[0-9]+ [^\r\n]*\z/) {
    50          
450 1         11 $self->{response_status} = $status;
451             } elsif (defined $status) {
452 14 100       415 Carp::croak "Attempted to set unknown HTTP response status $status" unless exists $HTTP_STATUS{$status};
453 13         62 $self->{response_status} = "$status $HTTP_STATUS{$status}";
454             } else {
455 0         0 delete $self->{response_status};
456             }
457             }
458 14         47 return $self;
459             }
460              
461             {
462             my %DISPOSITIONS = (attachment => 1, inline => 1);
463             sub set_response_disposition {
464 4     4 1 34 my ($self, $disposition, $filename) = @_;
465 4 50       15 if ($self->{headers_rendered}) {
466 0         0 Carp::carp "Attempted to set HTTP response content disposition but headers have already been rendered";
467             } else {
468 4 50       21 Carp::croak "Attempted to set unknown Content-Disposition value '$disposition'" unless exists $DISPOSITIONS{lc $disposition};
469 4         16 $self->{response_disposition} = $disposition;
470             # filename will be quoted/escaped later
471 4         17 $self->{response_filename} = $filename;
472             }
473 4         12 return $self;
474             }
475             }
476              
477             sub set_response_type {
478 36     36 1 129 my ($self, $content_type) = @_;
479 36 50       104 if ($self->{headers_rendered}) {
480 0         0 Carp::carp "Attempted to set HTTP response content type but headers have already been rendered";
481             } else {
482 36 50 66     272 Carp::croak "Newline characters not allowed in HTTP response content type" if defined $content_type and $content_type =~ tr/\r\n//;
483 36         145 $self->{response_type} = $content_type;
484             }
485 36         134 return $self;
486             }
487              
488             sub set_response_charset {
489 2     2 1 23 my ($self, $charset) = @_;
490 2 50 33     26 Carp::croak "Invalid characters in HTTP response charset" if defined $charset and $charset =~ m/[^a-zA-Z0-9!#\$%&'*+\-.^_`|~]/;
491 2         7 $self->{response_charset} = $charset;
492 2         6 return $self;
493             }
494              
495             sub add_response_header {
496 4     4 1 25 my ($self, $name, $value) = @_;
497 4 50       12 if ($self->{headers_rendered}) {
498 0         0 Carp::carp "Attempted to add HTTP response header '$name' but headers have already been rendered";
499             } else {
500 4 50       14 Carp::croak "Newline characters not allowed in HTTP response header '$name'" if $value =~ tr/\r\n//;
501 4         9 push @{$self->{response_headers}}, [$name, $value];
  4         18  
502             }
503 4         12 return $self;
504             }
505              
506             {
507             my %COOKIE_ATTR_VALUE = (expires => 1, domain => 1, path => 1, secure => 0, httponly => 0, samesite => 1, 'max-age' => 1);
508             sub add_response_cookie {
509 3     3 1 25 my ($self, $name, $value, @attrs) = @_;
510 3 50       9 if ($self->{headers_rendered}) {
511 0         0 Carp::carp "Attempted to add HTTP response cookie '$name' but headers have already been rendered";
512             } else {
513 3         9 my $cookie_str = "$name=$value";
514 3         8 my $i = 0;
515 3         43 while ($i <= $#attrs) {
516 10         21 my ($key, $val) = @attrs[$i, $i+1];
517 10         21 my $has_value = $COOKIE_ATTR_VALUE{lc $key};
518 10 50       20 if (!defined $has_value) {
    100          
519 0         0 Carp::croak "Attempted to set unknown cookie attribute '$key' for HTTP response cookie '$name'";
520             } elsif ($has_value) {
521 6 50       22 $cookie_str .= "; $key=$val" if defined $val;
522             } else {
523 4 100       12 $cookie_str .= "; $key" if $val;
524             }
525             } continue {
526 10         18 $i += 2;
527             }
528 3 50       10 Carp::croak "Newline characters not allowed in HTTP response cookie '$name'" if $cookie_str =~ tr/\r\n//;
529 3         5 push @{$self->{response_headers}}, ['Set-Cookie', $cookie_str];
  3         17  
530             }
531 3         11 return $self;
532             }
533             }
534              
535 1     1 1 6 sub reset_response_headers { delete $_[0]{response_headers}; $_[0] }
  1         3  
536              
537             sub response_status_code {
538 15     15 1 129 my ($self) = @_;
539 15 50 33     124 if (defined $self->{response_status} and $self->{response_status} =~ m/\A([0-9]+)/) {
540 15         81 return 0+$1;
541             }
542 0         0 return 200;
543             }
544              
545             {
546             my %RENDER_TYPES = (text => 1, html => 1, xml => 1, json => 1, data => 1, file => 1, handle => 1, redirect => 1);
547              
548             sub render {
549 65     65 1 306 my ($self, $type, $data) = @_;
550 65 50       233 Carp::croak "Cannot render additional data with ->render; use ->render_chunk" if $self->{headers_rendered};
551 65 100       198 $type = '' unless defined $type;
552 65 50 66     345 Carp::croak "Don't know how to render '$type'" if length $type and !exists $RENDER_TYPES{$type};
553 65 50       182 Carp::croak "Cannot render from an open filehandle with ->render; use ->render_chunk" if $type eq 'handle';
554              
555 65         124 my ($response_body, $response_length, $redirect_url);
556 65 100 100     993 if ($type eq 'redirect') {
    100 100        
    100 100        
    100          
    100          
    100          
557 3 50       14 Carp::croak "Newline characters not allowed in HTTP redirect" if $data =~ tr/\r\n//;
558 3         7 $redirect_url = $data;
559             } elsif (uc($ENV{REQUEST_METHOD} || '') eq 'HEAD') {
560             # no response content
561             } elsif ($type eq 'text' or $type eq 'html' or $type eq 'xml') {
562 5         16 my $charset = $self->{response_charset};
563 5 100       20 $charset = 'UTF-8' unless defined $charset;
564 5 100 66     28 if (uc $charset eq 'UTF-8' and do { local $@; eval { require Unicode::UTF8; 1 } }) {
  4         9  
  4         9  
  4         763  
  4         698  
565 4         25 $response_body = Unicode::UTF8::encode_utf8($data);
566             } else {
567 1         7 require Encode;
568 1         6 $response_body = Encode::encode($charset, "$data");
569             }
570 5         73 $response_length = length $response_body;
571             } elsif ($type eq 'json') {
572 1         6 $response_body = $self->_json->encode($data);
573 1         4 $response_length = length $response_body;
574             } elsif ($type eq 'data') {
575 32         64 $response_body = $data;
576 32         62 $response_length = length $response_body;
577             } elsif ($type eq 'file') {
578 1         19 $response_length = -s $data;
579 1 50       7 Carp::croak "Failed to retrieve size of file '$data': $!" unless defined $response_length;
580             }
581 65 100       193 $response_length = 0 unless defined $response_length;
582              
583 65         400 my $headers_str = $self->_response_headers($type, $response_length, $redirect_url);
584 65 100       246 my $out_fh = defined $self->{output_handle} ? $self->{output_handle} : *STDOUT;
585 65         317 binmode $out_fh;
586 65         570 $out_fh->printflush($headers_str);
587 65         4356 $self->{headers_rendered} = 1;
588 65         558 $self->{response_fixed_length} = 1;
589 65 100       190 return $self unless $response_length;
590              
591 38 100       116 if ($type eq 'file') {
592 1 50       42 open my $in_fh, '<', $data or Carp::croak "Failed to open file '$data' for rendering: $!";
593 1         5 binmode $in_fh;
594 1   50     21 my $buffer_size = 0 + ($self->{response_body_buffer} || $ENV{CGI_TINY_RESPONSE_BODY_BUFFER} || DEFAULT_RESPONSE_BODY_BUFFER);
595 1         52 while (read $in_fh, my $buffer, $buffer_size) {
596 1         37 $out_fh->print($buffer);
597             }
598 1         36 $out_fh->flush;
599             } else {
600 37         121 $out_fh->printflush($response_body);
601             }
602 38         1431 return $self;
603             }
604              
605             sub render_chunk {
606 17     17 1 182 my ($self, $type, $data) = @_;
607 17 50       73 Carp::croak "Cannot render additional data after ->render" if $self->{response_fixed_length};
608 17 100       64 $type = '' unless defined $type;
609 17 50 66     115 Carp::croak "Don't know how to render '$type'" if length $type and !exists $RENDER_TYPES{$type};
610 17 50       54 Carp::croak "Cannot render a chunked redirect" if $type eq 'redirect';
611              
612 17 50       69 my $out_fh = defined $self->{output_handle} ? $self->{output_handle} : *STDOUT;
613 17 100       49 unless ($self->{headers_rendered}) {
614 12         54 my $headers_str = $self->_response_headers($type);
615 12         47 binmode $out_fh;
616 12         136 $out_fh->printflush($headers_str);
617 12         852 $self->{headers_rendered} = 1;
618             }
619              
620 17 100 50     326 if (uc($ENV{REQUEST_METHOD} || '') eq 'HEAD') {
    100 100        
    100 66        
    100 100        
    100          
621             # no response content
622             } elsif ($type eq 'text' or $type eq 'html' or $type eq 'xml') {
623 4         11 my $charset = $self->{response_charset};
624 4 100       13 $charset = 'UTF-8' unless defined $charset;
625 4         6 my $response_body;
626 4 100 66     18 if (uc $charset eq 'UTF-8' and do { local $@; eval { require Unicode::UTF8; 1 } }) {
  3         4  
  3         7  
  3         16  
  3         12  
627 3         16 $response_body = Unicode::UTF8::encode_utf8($data);
628             } else {
629 1         11 require Encode;
630 1         7 $response_body = Encode::encode($charset, "$data");
631             }
632 4         71 $out_fh->printflush($response_body);
633             } elsif ($type eq 'json') {
634 2         8 my $response_body = $self->_json->encode($data);
635 2         8 $out_fh->printflush($response_body);
636             } elsif ($type eq 'data') {
637 2         11 $out_fh->printflush($data);
638             } elsif ($type eq 'file' or $type eq 'handle') {
639 2         6 my $in_fh;
640 2 100       7 if ($type eq 'file') {
641 1 50       46 open $in_fh, '<', $data or Carp::croak "Failed to open file '$data' for rendering: $!";
642             } else {
643 1         3 $in_fh = $data;
644             }
645 2         8 binmode $in_fh;
646 2   50     27 my $buffer_size = 0 + ($self->{response_body_buffer} || $ENV{CGI_TINY_RESPONSE_BODY_BUFFER} || DEFAULT_RESPONSE_BODY_BUFFER);
647 2         57 while (read $in_fh, my $buffer, $buffer_size) {
648 2         12 $out_fh->print($buffer);
649             }
650 2         44 $out_fh->flush;
651             }
652 17         323 return $self;
653             }
654             }
655              
656             sub _response_headers {
657 77     77   264 my ($self, $type, $content_length, $location) = @_;
658 77         186 my $headers_str = '';
659 77 50 33     320 return $headers_str if defined $self->{debug_method} and !$self->{debug_verbose};
660 77         139 my %headers_set;
661 77 100       178 foreach my $header (@{$self->{response_headers} || []}) {
  77         574  
662 4         10 my ($name, $value) = @$header;
663 4         8 $headers_str .= "$name: $value\r\n";
664 4         10 $headers_set{lc $name} = 1;
665             }
666 77 100 66     431 if (!$headers_set{'content-length'} and defined $content_length) {
667 65         242 $headers_str = "Content-Length: $content_length\r\n$headers_str";
668             }
669 77 100 66     573 if (!$headers_set{'content-disposition'} and (defined $self->{response_disposition} or defined $self->{response_filename})) {
      33        
670 3 50       15 my $value = defined $self->{response_disposition} ? $self->{response_disposition} : 'inline';
671 3 100       10 if (defined(my $filename = $self->{response_filename})) {
672 2         19 require Encode;
673 2         16 my $quoted_filename = Encode::encode('ISO-8859-1', "$filename");
674 2         127 $quoted_filename =~ tr/\r\n/ /;
675 2         23 $quoted_filename =~ s/([\\"])/\\$1/g;
676 2         8 $value .= "; filename=\"$quoted_filename\"";
677 2         10 my $ext_filename = Encode::encode('UTF-8', "$filename");
678 2         111 $ext_filename =~ s/([^a-zA-Z0-9!#\$&+\-.^_`|~])/sprintf '%%%02X', ord $1/ge;
  5         22  
679 2         8 $value .= "; filename*=UTF-8''$ext_filename";
680             }
681 3 100       17 $headers_str = "Content-Disposition: $value\r\n$headers_str" unless lc $value eq 'inline';
682             }
683 77 100 66     337 if (!$headers_set{location} and $type eq 'redirect') {
684 3         7 $headers_str = "Location: $location\r\n$headers_str";
685             }
686 77 100 66     402 if (!$headers_set{'content-type'} and $type ne 'redirect') {
687 74         255 my $content_type = $self->{response_type};
688 74         135 my $charset = $self->{response_charset};
689 74 100       295 $charset = 'UTF-8' unless defined $charset;
690 74 100 100     420 $content_type =
    100 100        
    100          
    100          
    100          
691             $type eq 'text' ? "text/plain;charset=$charset"
692             : $type eq 'html' ? "text/html;charset=$charset"
693             : $type eq 'xml' ? "application/xml;charset=$charset"
694             : $type eq 'json' ? 'application/json;charset=UTF-8'
695             : 'application/octet-stream'
696             unless defined $content_type or (defined $content_length and $content_length == 0);
697 74 100       266 $headers_str = "Content-Type: $content_type\r\n$headers_str" if defined $content_type;
698             }
699 77 50       183 if (!$headers_set{date}) {
700 77         299 my $date_str = epoch_to_date(time);
701 77         251 $headers_str = "Date: $date_str\r\n$headers_str";
702             }
703 77         159 my $status = $self->{response_status};
704 77 100 100     264 $status = $self->{response_status} = "302 $HTTP_STATUS{302}" if $type eq 'redirect'
      100        
705             and !(defined $status and $status =~ m/^3[0-9]{2} /);
706 77 100 66     475 if ($self->{nph}) {
    100          
707 2 100       9 $status = "200 $HTTP_STATUS{200}" unless defined $status;
708 2         7 my $protocol = $ENV{SERVER_PROTOCOL};
709 2 50 33     12 $protocol = 'HTTP/1.0' unless defined $protocol and length $protocol;
710 2         7 $headers_str = "$protocol $status\r\n$headers_str";
711 2         5 my $server = $ENV{SERVER_SOFTWARE};
712 2 100 66     14 $headers_str .= "Server: $server\r\n" if defined $server and length $server;
713             } elsif (!$headers_set{status} and defined $status) {
714 37         101 $headers_str = "Status: $status\r\n$headers_str";
715             }
716 77         322 return "$headers_str\r\n";
717             }
718              
719             sub _json {
720 5     5   10 my ($self) = @_;
721 5 100       17 unless (exists $self->{json}) {
722 3 50       7 if (do { local $@; eval { require Cpanel::JSON::XS; Cpanel::JSON::XS->VERSION('4.09'); 1 } }) {
  3         5  
  3         8  
  3         25  
  3         88  
  3         18  
723 3         37 $self->{json} = Cpanel::JSON::XS->new->allow_dupkeys->stringify_infnan;
724             } else {
725 0         0 require JSON::PP;
726 0         0 $self->{json} = JSON::PP->new;
727             }
728 3         42 $self->{json}->utf8->canonical->allow_nonref->allow_unknown->allow_blessed->convert_blessed->escape_slash;
729             }
730 5         41 return $self->{json};
731             }
732              
733             {
734             my @DAYS_OF_WEEK = qw(Sun Mon Tue Wed Thu Fri Sat);
735             my @MONTH_NAMES = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
736             my %MONTH_NUMS;
737             @MONTH_NUMS{@MONTH_NAMES} = 0..11;
738              
739             sub epoch_to_date {
740 79     79 1 1052 my ($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime $_[0];
741 79         813 return sprintf '%s, %02d %s %04d %02d:%02d:%02d GMT',
742             $DAYS_OF_WEEK[$wday], $mday, $MONTH_NAMES[$mon], $year + 1900, $hour, $min, $sec;
743             }
744              
745             sub date_to_epoch {
746             # RFC 1123 (Sun, 06 Nov 1994 08:49:37 GMT)
747 19     19 1 41578 my ($mday,$mon,$year,$hour,$min,$sec) = $_[0] =~ m/^ (?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),
748             [ ] ([0-9]{2}) [ ] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ ] ([0-9]{4})
749             [ ] ([0-9]{2}) : ([0-9]{2}) : ([0-9]{2}) [ ] GMT $/x;
750              
751             # RFC 850 (Sunday, 06-Nov-94 08:49:37 GMT)
752 19 100       100 ($mday,$mon,$year,$hour,$min,$sec) = $_[0] =~ m/^ (?:Sun|Mon|Tues|Wednes|Thurs|Fri|Satur)day,
753             [ ] ([0-9]{2}) - (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) - ([0-9]{2})
754             [ ] ([0-9]{2}) : ([0-9]{2}) : ([0-9]{2}) [ ] GMT $/x unless defined $mday;
755              
756             # asctime (Sun Nov 6 08:49:37 1994)
757 19 100       67 ($mon,$mday,$hour,$min,$sec,$year) = $_[0] =~ m/^ (?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)
758             [ ] (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ ]{1,2} ([0-9]{1,2})
759             [ ] ([0-9]{2}) : ([0-9]{2}) : ([0-9]{2}) [ ] ([0-9]{4}) $/x unless defined $mday;
760              
761 19 100       49 return undef unless defined $mday;
762              
763 18         3032 require Time::Local;
764             # 4 digit years interpreted literally, but may have leading zeroes
765             # 2 digit years interpreted with best effort heuristic
766 18 50 66     8596 return scalar Time::Local::timegm($sec, $min, $hour, $mday, $MONTH_NUMS{$mon},
767             (length($year) == 4 && $year < 1900) ? $year - 1900 : $year);
768             }
769             }
770              
771             {
772             my %ESCAPES = ('&' => '&', '<' => '<', '>' => '>', '"' => '"', '\'' => ''');
773 3     3 1 599 sub escape_html { (my $escaped = $_[0]) =~ s/([&<>"'])/$ESCAPES{$1}/ge; $escaped }
  10         30  
  3         14  
774             }
775              
776             1;