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   9979 use strict;
  7         67  
  7         223  
9 7     7   41 use warnings;
  7         14  
  7         166  
10 7     7   35 use Carp ();
  7         14  
  7         104  
11 7     7   4700 use IO::Handle ();
  7         50349  
  7         180  
12 7     7   60 use Exporter ();
  7         20  
  7         250  
13              
14             our $VERSION = '1.001';
15              
16 7     7   35 use constant DEFAULT_REQUEST_BODY_LIMIT => 16777216;
  7         20  
  7         933  
17 7     7   43 use constant DEFAULT_REQUEST_BODY_BUFFER => 262144;
  7         21  
  7         442  
18 7     7   48 use constant DEFAULT_RESPONSE_BODY_BUFFER => 131072;
  7         15  
  7         60884  
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   7584201 $cgi ||= bless {pid => $$}, $_[0];
96 8         1339 goto &Exporter::import;
97             }
98              
99             sub cgi (&) {
100 75     75 0 8033710 my ($handler) = @_;
101 75   100     1084 $cgi ||= bless {pid => $$}, __PACKAGE__;
102 75 50 33     318 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         148 my ($error, $errored);
107             {
108 75         143 local $@;
  75         151  
109 75 100       145 eval { local $_ = $cgi; $handler->(); 1 } or do { $error = $@; $errored = 1 };
  75         188  
  75         241  
  65         279  
  8         133  
  8         21  
110             }
111 73 100       283 if ($errored) {
    100          
112 8         38 _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         382 undef $cgi;
117 73         185 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   6028 if (defined $cgi) {
125 5 50       164 _handle_error($cgi, "cgi exited without rendering a response\n") unless $cgi->{headers_rendered};
126 5         43 undef $cgi;
127             }
128             }
129             }
130              
131             sub _handle_error {
132 33     33   129 my ($cgi, $error) = @_;
133 33 100       147 return unless $cgi->{pid} == $$; # in case of fork
134             $cgi->{response_status} = "500 $HTTP_STATUS{500}" unless $cgi->{headers_rendered}
135 32 100 100     338 or (defined $cgi->{response_status} and $cgi->{response_status} =~ m/^[45][0-9]{2} /);
      100        
136 32 100       97 if (defined(my $handler = $cgi->{on_error})) {
137 30         53 my ($error_error, $error_errored);
138             {
139 30         105 local $@;
  30         43  
140 30 50       56 eval { $handler->($cgi, $error, !!$cgi->{headers_rendered}); 1 } or do { $error_error = $@; $error_errored = 1 };
  30         122  
  30         208  
  0         0  
  0         0  
141             }
142 30 50       85 return unless $cgi->{pid} == $$; # in case of fork in error handler
143 30 50       66 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         91 warn $error;
149             }
150 32 100       227 $cgi->set_response_type('text/plain')->render(data => $cgi->{response_status}) unless $cgi->{headers_rendered};
151             }
152              
153 30     30 1 299 sub set_error_handler { $_[0]{on_error} = $_[1]; $_[0] }
  30         86  
154 0     0 1 0 sub set_request_body_buffer { $_[0]{request_body_buffer} = $_[1]; $_[0] }
  0         0  
155 1     1 1 16 sub set_request_body_limit { $_[0]{request_body_limit} = $_[1]; $_[0] }
  1         60  
156 5     5 1 41 sub set_multipart_form_options { $_[0]{multipart_form_options} = $_[1]; $_[0] }
  5         12  
157 5     5 1 29 sub set_multipart_form_charset { $_[0]{multipart_form_charset} = $_[1]; $_[0] }
  5         11  
158 75     75 1 485 sub set_input_handle { $_[0]{input_handle} = $_[1]; $_[0] }
  75         147  
159 75     75 1 361 sub set_output_handle { $_[0]{output_handle} = $_[1]; $_[0] }
  75         125  
160              
161 1 50   1 1 48 sub auth_type { defined $ENV{AUTH_TYPE} ? $ENV{AUTH_TYPE} : '' }
162 1 50   1 1 10 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 9 sub gateway_interface { defined $ENV{GATEWAY_INTERFACE} ? $ENV{GATEWAY_INTERFACE} : '' }
165 2 50   2 1 32 sub path_info { defined $ENV{PATH_INFO} ? $ENV{PATH_INFO} : '' }
166             *path = \&path_info;
167 1 50   1 1 7 sub path_translated { defined $ENV{PATH_TRANSLATED} ? $ENV{PATH_TRANSLATED} : '' }
168 5 50   5 1 58 sub query_string { defined $ENV{QUERY_STRING} ? $ENV{QUERY_STRING} : '' }
169             *query = \&query_string;
170 1 50   1 1 9 sub remote_addr { defined $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : '' }
171 1 50   1 1 16 sub remote_host { defined $ENV{REMOTE_HOST} ? $ENV{REMOTE_HOST} : '' }
172 1 50   1 1 10 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 19 sub script_name { defined $ENV{SCRIPT_NAME} ? $ENV{SCRIPT_NAME} : '' }
177 1 50   1 1 8 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 9 sub server_software { defined $ENV{SERVER_SOFTWARE} ? $ENV{SERVER_SOFTWARE} : '' }
181              
182             sub headers {
183 1     1 1 9 my ($self) = @_;
184 1 50       6 unless (exists $self->{request_headers}) {
185 1         3 my %headers;
186 1         29 foreach my $key (keys %ENV) {
187 54         67 my $name = $key;
188 54 100       104 next unless $name =~ s/^HTTP_//;
189 3         8 $name =~ tr/_/-/;
190 3         16 $headers{lc $name} = $ENV{$key};
191             }
192 1         12 $self->{request_headers} = \%headers;
193             }
194 1         4 return {%{$self->{request_headers}}};
  1         8  
195             }
196              
197 2     2 1 12 sub header { (my $name = $_[1]) =~ tr/-/_/; $ENV{"HTTP_\U$name"} }
  2         10  
198              
199 1     1 1 7 sub cookies { [map { [@$_] } @{$_[0]->_cookies->{ordered}}] }
  4         14  
  1         7  
200 1     1 1 8 sub cookie_names { [@{$_[0]->_cookies->{names}}] }
  1         5  
201 2 100   2 1 10 sub cookie { my $c = $_[0]->_cookies->{keyed}; exists $c->{$_[1]} ? $c->{$_[1]}[-1] : undef }
  2         9  
202 1 50   1 1 7 sub cookie_array { my $c = $_[0]->_cookies->{keyed}; exists $c->{$_[1]} ? [@{$c->{$_[1]}}] : [] }
  1         4  
  1         6  
203              
204             sub _cookies {
205 5     5   8 my ($self) = @_;
206 5 100       13 unless (exists $self->{request_cookies}) {
207 1         17 $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       12 next unless length $pair;
211 4         12 my ($name, $value) = split /=/, $pair, 2;
212 4 50       11 next unless defined $value;
213 4 100       12 push @names, $name unless exists $keyed{$name};
214 4         9 push @ordered, [$name, $value];
215 4         6 push @{$keyed{$name}}, $value;
  4         19  
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         20  
  1         5  
  1         4  
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         18  
  1         4  
224             sub param {
225 3     3 1 19 my ($self, $name) = @_;
226 3         6 my $p = $self->_body_params->{keyed};
227 3 100       11 return $p->{$name}[-1] if exists $p->{$name};
228 2         11 my $q = $self->_query_params->{keyed};
229 2 100       9 return exists $q->{$name} ? $q->{$name}[-1] : undef;
230             }
231 3 100   3 1 14 sub param_array { [map { exists $_->{$_[1]} ? @{$_->{$_[1]}} : () } $_[0]->_query_params->{keyed}, $_[0]->_body_params->{keyed}] }
  6         16  
  4         16  
232              
233 1     1 1 13 sub query_params { [map { [@$_] } @{$_[0]->_query_params->{ordered}}] }
  4         13  
  1         5  
234 1     1 1 5 sub query_param_names { [@{$_[0]->_query_params->{names}}] }
  1         4  
235 2 100   2 1 9 sub query_param { my $p = $_[0]->_query_params->{keyed}; exists $p->{$_[1]} ? $p->{$_[1]}[-1] : undef }
  2         10  
236 2 100   2 1 9 sub query_param_array { my $p = $_[0]->_query_params->{keyed}; exists $p->{$_[1]} ? [@{$p->{$_[1]}}] : [] }
  2         6  
  1         4  
237              
238             sub _query_params {
239 13     13   28 my ($self) = @_;
240 13 100       30 unless (exists $self->{query_params}) {
241 3         82 $self->{query_params} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
242 3         15 foreach my $pair (split /[&;]/, $self->query) {
243 10         30 my ($name, $value) = split /=/, $pair, 2;
244 10 50       24 $value = '' unless defined $value;
245 10         19 do { tr/+/ /; s/%([0-9a-fA-F]{2})/chr hex $1/ge; utf8::decode $_ } for $name, $value;
  20         26  
  20         50  
  8         29  
  20         42  
246 10 100       29 push @names, $name unless exists $keyed{$name};
247 10         31 push @ordered, [$name, $value];
248 10         16 push @{$keyed{$name}}, $value;
  10         47  
249             }
250             }
251 13         33 return $self->{query_params};
252             }
253              
254             sub body {
255 6     6 1 28 my ($self) = @_;
256 6 50 33     48 unless (exists $self->{body_content} or exists $self->{body_parts}) {
257 6         24 $self->{body_content} = '';
258 6         28 my $length = $self->_body_length;
259 5 50       20 my $in_fh = defined $self->{input_handle} ? $self->{input_handle} : *STDIN;
260 5         16 binmode $in_fh;
261 5   50     36 my $buffer_size = 0 + ($self->{request_body_buffer} || $ENV{CGI_TINY_REQUEST_BODY_BUFFER} || DEFAULT_REQUEST_BODY_BUFFER);
262 5         16 while ($length > 0) {
263 5 50       15 my $chunk = $length < $buffer_size ? $length : $buffer_size;
264 5 50       32 last unless my $read = read $in_fh, $self->{body_content}, $chunk, length $self->{body_content};
265 5         19 $length -= $read;
266             }
267             }
268 5         37 return $self->{body_content};
269             }
270              
271             sub body_json {
272 4     4 1 26 my ($self) = @_;
273 4 100       23 unless (exists $self->{body_json}) {
274 3         15 $self->{body_json} = undef;
275 3 100 66     40 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^application\/json\b/i) {
276 2         12 $self->{body_json} = $self->_json->decode($self->body);
277             }
278             }
279 4         19 return $self->{body_json};
280             }
281              
282 8     8 1 34 sub body_params { [map { [@$_] } @{$_[0]->_body_params->{ordered}}] }
  14         44  
  8         26  
283 7     7 1 24 sub body_param_names { [@{$_[0]->_body_params->{names}}] }
  7         15  
284 8 100   8 1 34 sub body_param { my $p = $_[0]->_body_params->{keyed}; exists $p->{$_[1]} ? $p->{$_[1]}[-1] : undef }
  8         34  
285 6 100   6 1 26 sub body_param_array { my $p = $_[0]->_body_params->{keyed}; exists $p->{$_[1]} ? [@{$p->{$_[1]}}] : [] }
  6         50  
  5         16  
286              
287             sub _body_params {
288 37     37   63 my ($self) = @_;
289 37 100       74 unless (exists $self->{body_params}) {
290 9         38 $self->{body_params} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
291 9 100 100     106 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^application\/x-www-form-urlencoded\b/i) {
    100 66        
292 2         8 foreach my $pair (split /&/, $self->body) {
293 8         22 my ($name, $value) = split /=/, $pair, 2;
294 8 50       16 $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         22  
  16         45  
  10         30  
  16         38  
296 8 100       23 push @names, $name unless exists $keyed{$name};
297 8         18 push @ordered, [$name, $value];
298 8         10 push @{$keyed{$name}}, $value;
  8         23  
299             }
300             } elsif ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i) {
301 6         12 my $default_charset = $self->{multipart_form_charset};
302 6 100       15 $default_charset = 'UTF-8' unless defined $default_charset;
303 6         7 foreach my $part (@{$self->_body_multipart}) {
  6         13  
304 18 100       61 next if defined $part->{filename};
305 10         32 my ($name, $headers, $content, $file) = @$part{'name','headers','content','file'};
306 10 50       33 if (length $default_charset) {
307 10         50 require Encode;
308 10         36 $name = Encode::decode($default_charset, "$name");
309             }
310 10         524 my $value = '';
311 10 100       24 if (defined $content) {
    50          
312 9         23 $value = $content;
313             } elsif (defined $file) {
314 1         4 binmode $file;
315 1         10 seek $file, 0, 0;
316 1         3 $value = do { local $/; readline $file };
  1         5  
  1         26  
317 1         10 seek $file, 0, 0;
318             }
319 10         12 my $value_charset;
320 10 100       27 if (defined $headers->{'content-type'}) {
321 5 50       38 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       14 $value_charset = defined $charset_quoted ? $charset_quoted : $charset_unquoted;
324             }
325             }
326 10 50 66     39 if (defined $value_charset or !defined $headers->{'content-type'} or $headers->{'content-type'} =~ m/^text\/plain\b/i) {
      33        
327 10         39 require Encode;
328 10 100       22 if (defined $value_charset) {
    50          
329 5         26 $value = Encode::decode($value_charset, "$value");
330             } elsif (length $default_charset) {
331 5         13 $value = Encode::decode($default_charset, "$value");
332             }
333             }
334 10 100       447 push @names, $name unless exists $keyed{$name};
335 10         21 push @ordered, [$name, $value];
336 10         15 push @{$keyed{$name}}, $value;
  10         41  
337             }
338             }
339             }
340 37         93 return $self->{body_params};
341             }
342              
343             sub body_parts {
344 9     9 1 46 my ($self) = @_;
345 9 100 66     80 return [] unless $ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i;
346 8         16 return [map { +{%$_} } @{$self->_body_multipart}];
  18         90  
  8         24  
347             }
348              
349 6     6 1 22 sub uploads { [map { [@$_] } @{$_[0]->_body_uploads->{ordered}}] }
  8         28  
  6         14  
350 6     6 1 21 sub upload_names { [@{$_[0]->_body_uploads->{names}}] }
  6         13  
351 7 100   7 1 45 sub upload { my $u = $_[0]->_body_uploads->{keyed}; exists $u->{$_[1]} ? $u->{$_[1]}[-1] : undef }
  7         28  
352 6 100   6 1 21 sub upload_array { my $u = $_[0]->_body_uploads->{keyed}; exists $u->{$_[1]} ? [@{$u->{$_[1]}}] : [] }
  6         12  
  5         16  
353              
354             sub _body_uploads {
355 25     25   39 my ($self) = @_;
356 25 100       42 unless (exists $self->{body_uploads}) {
357 6         31 $self->{body_uploads} = {names => \my @names, ordered => \my @ordered, keyed => \my %keyed};
358 6 50 33     54 if ($ENV{CONTENT_TYPE} and $ENV{CONTENT_TYPE} =~ m/^multipart\/form-data\b/i) {
359 6         51 my $default_charset = $self->{multipart_form_charset};
360 6 100       17 $default_charset = 'UTF-8' unless defined $default_charset;
361 6         10 foreach my $part (@{$self->_body_multipart}) {
  6         13  
362 18 100       36 next unless defined $part->{filename};
363 8         28 my ($name, $filename, $size, $headers, $file, $content) = @$part{'name','filename','size','headers','file','content'};
364 8 50       17 if (length $default_charset) {
365 8         35 require Encode;
366 8         24 $name = Encode::decode($default_charset, "$name");
367 8         420 $filename = Encode::decode($default_charset, "$filename");
368             }
369             my $upload = {
370             filename => $filename,
371             size => $size,
372 8         329 content_type => $headers->{'content-type'},
373             };
374 8 100       21 $upload->{file} = $file if defined $file;
375 8 100       16 $upload->{content} = $content if defined $content;
376 8 100       19 push @names, $name unless exists $keyed{$name};
377 8         28 push @ordered, [$name, $upload];
378 8         11 push @{$keyed{$name}}, $upload;
  8         33  
379             }
380             }
381             }
382 25         58 return $self->{body_uploads};
383             }
384              
385             sub _body_length {
386 12     12   31 my ($self) = @_;
387 12         26 my $limit = $self->{request_body_limit};
388 12 100       33 $limit = $ENV{CGI_TINY_REQUEST_BODY_LIMIT} unless defined $limit;
389 12 100       33 $limit = DEFAULT_REQUEST_BODY_LIMIT unless defined $limit;
390 12   50     38 my $length = $ENV{CONTENT_LENGTH} || 0;
391 12 100 66     70 if ($limit and $length > $limit) {
392 1 50       9 $self->{response_status} = "413 $HTTP_STATUS{413}" unless $self->{headers_rendered};
393 1         8 die "Request body limit exceeded\n";
394             }
395 11         25 return 0 + $length;
396             }
397              
398             sub _body_multipart {
399 20     20   34 my ($self) = @_;
400 20 100       41 unless (exists $self->{body_parts}) {
401 8         18 $self->{body_parts} = [];
402 8         838 require CGI::Tiny::Multipart;
403 8         32 my $boundary = CGI::Tiny::Multipart::extract_multipart_boundary($ENV{CONTENT_TYPE});
404 8 100       23 unless (defined $boundary) {
405 1 50       8 $self->{response_status} = "400 $HTTP_STATUS{400}" unless $self->{headers_rendered};
406 1         7 die "Malformed multipart/form-data request\n";
407             }
408              
409 7         13 my ($input, $length);
410 7 100       19 if (exists $self->{body_content}) {
411 1         2 $length = length $self->{body_content};
412 1         2 $input = \$self->{body_content};
413             } else {
414 6         17 $length = $self->_body_length;
415 6 50       20 $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     36 %{$self->{multipart_form_options} || {}},
  7         47  
421             });
422 7 100       77 unless (defined $parts) {
423 1 50       7 $self->{response_status} = "400 $HTTP_STATUS{400}" unless $self->{headers_rendered};
424 1         10 die "Malformed multipart/form-data request\n";
425             }
426              
427 6         17 $self->{body_parts} = $parts;
428             }
429 18         51 return $self->{body_parts};
430             }
431              
432             sub set_nph {
433 2     2 1 17 my ($self, $value) = @_;
434 2 50       10 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       18 $self->{nph} = @_ < 2 ? 1 : $value;
438             }
439 2         8 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 125 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     150 if (defined $status and $status =~ m/\A[0-9]+ [^\r\n]*\z/) {
    50          
450 1         574 $self->{response_status} = $status;
451             } elsif (defined $status) {
452 14 100       407 Carp::croak "Attempted to set unknown HTTP response status $status" unless exists $HTTP_STATUS{$status};
453 13         71 $self->{response_status} = "$status $HTTP_STATUS{$status}";
454             } else {
455 0         0 delete $self->{response_status};
456             }
457             }
458 14         45 return $self;
459             }
460              
461             {
462             my %DISPOSITIONS = (attachment => 1, inline => 1);
463             sub set_response_disposition {
464 4     4 1 39 my ($self, $disposition, $filename) = @_;
465 4 50       20 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       22 Carp::croak "Attempted to set unknown Content-Disposition value '$disposition'" unless exists $DISPOSITIONS{lc $disposition};
469 4         17 $self->{response_disposition} = $disposition;
470             # filename will be quoted/escaped later
471 4         18 $self->{response_filename} = $filename;
472             }
473 4         14 return $self;
474             }
475             }
476              
477             sub set_response_type {
478 36     36 1 132 my ($self, $content_type) = @_;
479 36 50       95 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     274 Carp::croak "Newline characters not allowed in HTTP response content type" if defined $content_type and $content_type =~ tr/\r\n//;
483 36         94 $self->{response_type} = $content_type;
484             }
485 36         121 return $self;
486             }
487              
488             sub set_response_charset {
489 2     2 1 25 my ($self, $charset) = @_;
490 2 50 33     27 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 31 my ($self, $name, $value) = @_;
497 4 50       15 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       15 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         21  
502             }
503 4         11 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 29 my ($self, $name, $value, @attrs) = @_;
510 3 50       10 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         7 my $i = 0;
515 3         12 while ($i <= $#attrs) {
516 10         21 my ($key, $val) = @attrs[$i, $i+1];
517 10         18 my $has_value = $COOKIE_ATTR_VALUE{lc $key};
518 10 50       23 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       18 $cookie_str .= "; $key=$val" if defined $val;
522             } else {
523 4 100       10 $cookie_str .= "; $key" if $val;
524             }
525             } continue {
526 10         23 $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         6 push @{$self->{response_headers}}, ['Set-Cookie', $cookie_str];
  3         17  
530             }
531 3         10 return $self;
532             }
533             }
534              
535 1     1 1 8 sub reset_response_headers { delete $_[0]{response_headers}; $_[0] }
  1         3  
536              
537             sub response_status_code {
538 15     15 1 108 my ($self) = @_;
539 15 50 33     119 if (defined $self->{response_status} and $self->{response_status} =~ m/\A([0-9]+)/) {
540 15         78 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 302 my ($self, $type, $data) = @_;
550 65 50       194 Carp::croak "Cannot render additional data with ->render; use ->render_chunk" if $self->{headers_rendered};
551 65 100       214 $type = '' unless defined $type;
552 65 50 66     443 Carp::croak "Don't know how to render '$type'" if length $type and !exists $RENDER_TYPES{$type};
553 65 50       179 Carp::croak "Cannot render from an open filehandle with ->render; use ->render_chunk" if $type eq 'handle';
554              
555 65         131 my ($response_body, $response_length, $redirect_url);
556 65 100 100     875 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         9 $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         15 my $charset = $self->{response_charset};
563 5 100       19 $charset = 'UTF-8' unless defined $charset;
564 5 100 66     35 if (uc $charset eq 'UTF-8' and do { local $@; eval { require Unicode::UTF8; 1 } }) {
  4         10  
  4         9  
  4         766  
  4         714  
565 4         28 $response_body = Unicode::UTF8::encode_utf8($data);
566             } else {
567 1         12 require Encode;
568 1         8 $response_body = Encode::encode($charset, "$data");
569             }
570 5         103 $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         60 $response_body = $data;
576 32         52 $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       168 $response_length = 0 unless defined $response_length;
582              
583 65         300 my $headers_str = $self->_response_headers($type, $response_length, $redirect_url);
584 65 100       211 my $out_fh = defined $self->{output_handle} ? $self->{output_handle} : *STDOUT;
585 65         274 binmode $out_fh;
586 65         501 $out_fh->printflush($headers_str);
587 65         4337 $self->{headers_rendered} = 1;
588 65         239 $self->{response_fixed_length} = 1;
589 65 100       199 return $self unless $response_length;
590              
591 38 100       112 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         6 binmode $in_fh;
594 1   50     20 my $buffer_size = 0 + ($self->{response_body_buffer} || $ENV{CGI_TINY_RESPONSE_BODY_BUFFER} || DEFAULT_RESPONSE_BODY_BUFFER);
595 1         53 while (read $in_fh, my $buffer, $buffer_size) {
596 1         30 $out_fh->print($buffer);
597             }
598 1         35 $out_fh->flush;
599             } else {
600 37         100 $out_fh->printflush($response_body);
601             }
602 38         1390 return $self;
603             }
604              
605             sub render_chunk {
606 17     17 1 180 my ($self, $type, $data) = @_;
607 17 50       65 Carp::croak "Cannot render additional data after ->render" if $self->{response_fixed_length};
608 17 100       51 $type = '' unless defined $type;
609 17 50 66     116 Carp::croak "Don't know how to render '$type'" if length $type and !exists $RENDER_TYPES{$type};
610 17 50       64 Carp::croak "Cannot render a chunked redirect" if $type eq 'redirect';
611              
612 17 50       60 my $out_fh = defined $self->{output_handle} ? $self->{output_handle} : *STDOUT;
613 17 100       59 unless ($self->{headers_rendered}) {
614 12         53 my $headers_str = $self->_response_headers($type);
615 12         98 binmode $out_fh;
616 12         141 $out_fh->printflush($headers_str);
617 12         850 $self->{headers_rendered} = 1;
618             }
619              
620 17 100 50     395 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         10 my $charset = $self->{response_charset};
624 4 100       13 $charset = 'UTF-8' unless defined $charset;
625 4         8 my $response_body;
626 4 100 66     16 if (uc $charset eq 'UTF-8' and do { local $@; eval { require Unicode::UTF8; 1 } }) {
  3         5  
  3         7  
  3         17  
  3         13  
627 3         16 $response_body = Unicode::UTF8::encode_utf8($data);
628             } else {
629 1         10 require Encode;
630 1         8 $response_body = Encode::encode($charset, "$data");
631             }
632 4         84 $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         8 $out_fh->printflush($data);
638             } elsif ($type eq 'file' or $type eq 'handle') {
639 2         6 my $in_fh;
640 2 100       8 if ($type eq 'file') {
641 1 50       44 open $in_fh, '<', $data or Carp::croak "Failed to open file '$data' for rendering: $!";
642             } else {
643 1         4 $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         39 while (read $in_fh, my $buffer, $buffer_size) {
648 2         15 $out_fh->print($buffer);
649             }
650 2         45 $out_fh->flush;
651             }
652 17         323 return $self;
653             }
654             }
655              
656             sub _response_headers {
657 77     77   255 my ($self, $type, $content_length, $location) = @_;
658 77         191 my $headers_str = '';
659 77 50 33     280 return $headers_str if defined $self->{debug_method} and !$self->{debug_verbose};
660 77         227 my %headers_set;
661 77 100       143 foreach my $header (@{$self->{response_headers} || []}) {
  77         508  
662 4         6 my ($name, $value) = @$header;
663 4         11 $headers_str .= "$name: $value\r\n";
664 4         11 $headers_set{lc $name} = 1;
665             }
666 77 100 66     419 if (!$headers_set{'content-length'} and defined $content_length) {
667 65         218 $headers_str = "Content-Length: $content_length\r\n$headers_str";
668             }
669 77 100 66     556 if (!$headers_set{'content-disposition'} and (defined $self->{response_disposition} or defined $self->{response_filename})) {
      33        
670 3 50       14 my $value = defined $self->{response_disposition} ? $self->{response_disposition} : 'inline';
671 3 100       16 if (defined(my $filename = $self->{response_filename})) {
672 2         22 require Encode;
673 2         17 my $quoted_filename = Encode::encode('ISO-8859-1', "$filename");
674 2         188 $quoted_filename =~ tr/\r\n/ /;
675 2         24 $quoted_filename =~ s/([\\"])/\\$1/g;
676 2         10 $value .= "; filename=\"$quoted_filename\"";
677 2         8 my $ext_filename = Encode::encode('UTF-8', "$filename");
678 2         113 $ext_filename =~ s/([^a-zA-Z0-9!#\$&+\-.^_`|~])/sprintf '%%%02X', ord $1/ge;
  5         25  
679 2         8 $value .= "; filename*=UTF-8''$ext_filename";
680             }
681 3 100       22 $headers_str = "Content-Disposition: $value\r\n$headers_str" unless lc $value eq 'inline';
682             }
683 77 100 66     354 if (!$headers_set{location} and $type eq 'redirect') {
684 3         12 $headers_str = "Location: $location\r\n$headers_str";
685             }
686 77 100 66     446 if (!$headers_set{'content-type'} and $type ne 'redirect') {
687 74         192 my $content_type = $self->{response_type};
688 74         122 my $charset = $self->{response_charset};
689 74 100       310 $charset = 'UTF-8' unless defined $charset;
690 74 100 100     410 $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       290 $headers_str = "Content-Type: $content_type\r\n$headers_str" if defined $content_type;
698             }
699 77 50       195 if (!$headers_set{date}) {
700 77         263 my $date_str = epoch_to_date(time);
701 77         244 $headers_str = "Date: $date_str\r\n$headers_str";
702             }
703 77         152 my $status = $self->{response_status};
704 77 100 100     248 $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     487 if ($self->{nph}) {
    100          
707 2 100       9 $status = "200 $HTTP_STATUS{200}" unless defined $status;
708 2         6 my $protocol = $ENV{SERVER_PROTOCOL};
709 2 50 33     12 $protocol = 'HTTP/1.0' unless defined $protocol and length $protocol;
710 2         9 $headers_str = "$protocol $status\r\n$headers_str";
711 2         4 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         111 $headers_str = "Status: $status\r\n$headers_str";
715             }
716 77         317 return "$headers_str\r\n";
717             }
718              
719             sub _json {
720 5     5   13 my ($self) = @_;
721 5 100       17 unless (exists $self->{json}) {
722 3 50       6 if (do { local $@; eval { require Cpanel::JSON::XS; Cpanel::JSON::XS->VERSION('4.09'); 1 } }) {
  3         5  
  3         7  
  3         33  
  3         90  
  3         20  
723 3         38 $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         40 $self->{json}->utf8->canonical->allow_nonref->allow_unknown->allow_blessed->convert_blessed->escape_slash;
729             }
730 5         43 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 923 my ($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime $_[0];
741 79         817 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 41456 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       97 ($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       61 ($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       53 return undef unless defined $mday;
762              
763 18         2787 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     8478 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 667 sub escape_html { (my $escaped = $_[0]) =~ s/([&<>"'])/$ESCAPES{$1}/ge; $escaped }
  10         32  
  3         29  
774             }
775              
776             1;