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