File: | blib/lib/CGI/Info.pm |
Coverage: | 79.3% |
line | stmt | bran | cond | sub | time | code |
---|---|---|---|---|---|---|
1 | package CGI::Info; | |||||
2 | ||||||
3 | # TODO: remove the expect argument | |||||
4 | # TODO: look into params::check or params::validate | |||||
5 | ||||||
6 | 27 27 27 | 1915792 22 608 | use warnings; | |||
7 | 27 27 27 | 43 19 234 | use strict; | |||
8 | ||||||
9 | 27 27 27 | 4228 11734 43 | use boolean; | |||
10 | 27 27 27 | 771 31 568 | use Carp; | |||
11 | 27 27 27 | 5360 1207960 426 | use Object::Configure 0.12; | |||
12 | 27 27 27 | 82 18 330 | use File::Spec; | |||
13 | 27 27 27 | 61 151 254 | use Log::Abstraction 0.10; | |||
14 | 27 27 27 | 46 123 383 | use Params::Get 0.13; | |||
15 | 27 27 27 | 35 113 275 | use Params::Validate::Strict 0.11; | |||
16 | 27 27 27 | 6011 68673 682 | use Net::CIDR; | |||
17 | 27 27 27 | 76 20 334 | use Return::Set; | |||
18 | 27 27 27 | 41 14 264 | use Scalar::Util; | |||
19 | 27 27 27 | 31 19 4379 | use Socket; # For AF_INET | |||
20 | 27 27 | 208 54 | use 5.008; | |||
21 | 27 27 27 | 5376 94382 62 | use Log::Any qw($log); | |||
22 | # use Cwd; | |||||
23 | # use JSON::Parse; | |||||
24 | 27 27 27 | 23930 21 192 | use List::Util (); # Can go when expect goes | |||
25 | # use Sub::Private; | |||||
26 | 27 27 27 | 4892 335325 375 | use Sys::Path; | |||
27 | ||||||
28 | 27 27 27 | 5093 155922 77 | use namespace::clean; | |||
29 | ||||||
30 | sub _sanitise_input($); | |||||
31 | ||||||
32 - 40 | =head1 NAME CGI::Info - Information about the CGI environment =head1 VERSION Version 1.07 =cut | |||||
41 | ||||||
42 | our $VERSION = '1.07'; | |||||
43 | ||||||
44 - 145 | =head1 SYNOPSIS The C<CGI::Info> module is a Perl library designed to provide information about the environment in which a CGI script operates. It aims to eliminate hard-coded script details, enhancing code readability and portability. Additionally, it offers a simple web application firewall to add a layer of security. All too often, Perl programs have information such as the script's name hard-coded into their source. Generally speaking, hard-coding is a bad style since it can make programs difficult to read and reduces readability and portability. CGI::Info attempts to remove that. Furthermore, to aid script debugging, CGI::Info attempts to do sensible things when you're not running the program in a CGI environment. Whilst you shouldn't rely on it alone to provide security to your website, it is another layer and every little helps. use CGI::Info; my $info = CGI::Info->new(allow => { id => qr/^\d+$/ }); my $params = $info->params(); if($info->is_mobile()) { print "Mobile view\n"; } else { print "Desktop view\n"; } my $id = $info->param('id'); # Validated against allow schema =head1 SUBROUTINES/METHODS =head2 new Creates a CGI::Info object. It takes four optional arguments: allow, logger, expect and upload_dir, which are documented in the params() method. It takes other optional parameters: =over 4 =item * C<auto_load> Enable/disable the AUTOLOAD feature. The default is to have it enabled. =item * C<config_dirs> Where to look for C<config_file> =item * C<config_file> Points to a configuration file which contains the parameters to C<new()>. The file can be in any common format, including C<YAML>, C<XML>, and C<INI>. This allows the parameters to be set at run time. On non-Windows system, the class can be configured using environment variables starting with "CGI::Info::". For example: export CGI::Info::max_upload_size=65536 It doesn't work on Windows because of the case-insensitive nature of that system. If the configuration file has a section called C<CGI::Info>, only that section, and the C<global> section, if any exists, is used. =item * C<syslog> Takes an optional parameter syslog, to log messages to L<Sys::Syslog>. It can be a boolean to enable/disable logging to syslog, or a reference to a hash to be given to Sys::Syslog::setlogsock. =item * C<cache> An object that is used to cache IP lookups. This cache object is an object that understands get() and set() messages, such as a L<CHI> object. =item * C<max_upload_size> The maximum file size you can upload (-1 for no limit), the default is 512MB. =back The class can be configured at runtime using environments and configuration files, for example, setting C<$ENV{'CGI__INFO__carp_on_warn'}> causes warnings to use L<Carp>. For more information about configuring object constructors at runtime, see L<Object::Configure>. =cut | |||||
146 | ||||||
147 | our $stdin_data; # Class variable storing STDIN in case the class | |||||
148 | # is instantiated more than once | |||||
149 | ||||||
150 | sub new | |||||
151 | { | |||||
152 | 237 | 2062650 | my $class = shift; | |||
153 | ||||||
154 | # Handle hash or hashref arguments | |||||
155 | 237 | 372 | my $params = Params::Get::get_params(undef, @_) || {}; | |||
156 | ||||||
157 | 236 | 2709 | if(!defined($class)) { | |||
158 | 1 1 | 1 1 | if((scalar keys %{$params}) > 0) { | |||
159 | # Using CGI::Info:new(), not CGI::Info->new() | |||||
160 | 0 | 0 | croak(__PACKAGE__, ' use ->new() not ::new() to instantiate'); | |||
161 | } | |||||
162 | ||||||
163 | # FIXME: this only works when no arguments are given | |||||
164 | 1 | 2 | $class = __PACKAGE__; | |||
165 | } elsif(Scalar::Util::blessed($class)) { | |||||
166 | # If $class is an object, clone it with new arguments | |||||
167 | 5 5 5 | 5 6 20 | return bless { %{$class}, %{$params} }, ref($class); | |||
168 | } | |||||
169 | ||||||
170 | # Load the configuration from a config file, if provided | |||||
171 | 231 | 345 | $params = Object::Configure::configure($class, $params); | |||
172 | ||||||
173 | # Validate logger object has required methods | |||||
174 | 230 | 858180 | if(defined $params->{'logger'}) { | |||
175 | 230 | 1512 | unless(Scalar::Util::blessed($params->{'logger'}) && $params->{'logger'}->can('warn') && $params->{'logger'}->can('info') && $params->{'logger'}->can('error')) { | |||
176 | 0 | 0 | Carp::croak("Logger must be an object with info() and error() methods"); | |||
177 | } | |||||
178 | } | |||||
179 | ||||||
180 | 230 | 298 | if(defined($params->{'expect'})) { | |||
181 | # if(ref($params->{expect}) ne 'ARRAY') { | |||||
182 | # Carp::croak(__PACKAGE__, ': expect must be a reference to an array'); | |||||
183 | # } | |||||
184 | # # warn __PACKAGE__, ': expect is deprecated, use allow instead'; | |||||
185 | 2 | 3 | if(my $logger = $params->{'logger'}) { | |||
186 | 2 | 6 | $logger->error("$class: expect has been deprecated, use allow instead"); | |||
187 | } | |||||
188 | 2 | 1278 | Carp::croak("$class: expect has been deprecated, use allow instead"); | |||
189 | } | |||||
190 | ||||||
191 | # Return the blessed object | |||||
192 | return bless { | |||||
193 | max_upload_size => 512 * 1024, | |||||
194 | allow => undef, | |||||
195 | upload_dir => undef, | |||||
196 | 228 228 | 180 620 | %{$params} # Overwrite defaults with given arguments | |||
197 | }, $class; | |||||
198 | } | |||||
199 | ||||||
200 - 213 | =head2 script_name Retrieves the name of the executing CGI script. This is useful for POSTing, thus avoiding hard-coded paths into forms. use CGI::Info; my $info = CGI::Info->new(); my $script_name = $info->script_name(); # ... print "<form method=\"POST\" action=$script_name name=\"my_form\">\n"; =cut | |||||
214 | ||||||
215 | sub script_name | |||||
216 | { | |||||
217 | 22 | 814 | my $self = shift; | |||
218 | ||||||
219 | 22 | 30 | unless($self->{script_name}) { | |||
220 | 15 | 19 | $self->_find_paths(); | |||
221 | } | |||||
222 | 22 | 50 | return $self->{script_name}; | |||
223 | } | |||||
224 | ||||||
225 | sub _find_paths { | |||||
226 | 23 | 16 | my $self = shift; | |||
227 | ||||||
228 | 23 | 34 | if(!UNIVERSAL::isa((caller)[0], __PACKAGE__)) { | |||
229 | 0 | 0 | Carp::croak('Illegal Operation: This method can only be called by a subclass or ourself'); | |||
230 | } | |||||
231 | ||||||
232 | 23 | 207 | $self->_trace(__PACKAGE__ . ': entering _find_paths'); | |||
233 | ||||||
234 | 23 | 470 | require File::Basename && File::Basename->import() unless File::Basename->can('basename'); | |||
235 | ||||||
236 | # Determine script name | |||||
237 | 23 | 29 | my $script_name = $self->_get_env('SCRIPT_NAME') // $0; | |||
238 | 23 | 374 | $self->{script_name} = $self->_untaint_filename({ | |||
239 | filename => File::Basename::basename($script_name) | |||||
240 | }); | |||||
241 | ||||||
242 | # Determine script path | |||||
243 | 23 | 28 | if(my $script_path = $self->_get_env('SCRIPT_FILENAME')) { | |||
244 | 2 | 2 | $self->{script_path} = $script_path; | |||
245 | } elsif($script_name = $self->_get_env('SCRIPT_NAME')) { | |||||
246 | 12 | 11 | if(my $document_root = $self->_get_env('DOCUMENT_ROOT')) { | |||
247 | 6 | 6 | $script_name = $self->_get_env('SCRIPT_NAME'); | |||
248 | ||||||
249 | # It's usually the case, e.g. /cgi-bin/foo.pl | |||||
250 | 6 | 5 | $script_name =~ s{^/}{}; | |||
251 | ||||||
252 | 6 | 27 | $self->{script_path} = File::Spec->catfile($document_root, $script_name); | |||
253 | } else { | |||||
254 | 6 | 35 | if(File::Spec->file_name_is_absolute($script_name) && (-r $script_name)) { | |||
255 | # Called from a command line with a full path | |||||
256 | 1 | 2 | $self->{script_path} = $script_name; | |||
257 | } else { | |||||
258 | 5 | 24 | require Cwd unless Cwd->can('abs_path'); | |||
259 | ||||||
260 | 5 | 9 | if($script_name =~ /^\/(.+)/) { | |||
261 | # It's usually the case, e.g. /cgi-bin/foo.pl | |||||
262 | 2 | 2 | $script_name = $1; | |||
263 | } | |||||
264 | ||||||
265 | 5 | 37 | $self->{script_path} = File::Spec->catfile(Cwd::abs_path(), $script_name); | |||
266 | } | |||||
267 | } | |||||
268 | } elsif(File::Spec->file_name_is_absolute($0)) { | |||||
269 | # Called from a command line with a full path | |||||
270 | 0 | 0 | $self->{script_path} = $0; | |||
271 | } else { | |||||
272 | 9 | 104 | $self->{script_path} = File::Spec->rel2abs($0); | |||
273 | } | |||||
274 | ||||||
275 | # Untaint and finalize script path | |||||
276 | $self->{script_path} = $self->_untaint_filename({ | |||||
277 | filename => $self->{script_path} | |||||
278 | 23 | 39 | }); | |||
279 | } | |||||
280 | ||||||
281 - 298 | =head2 script_path Finds the full path name of the script. use CGI::Info; my $info = CGI::Info->new(); my $fullname = $info->script_path(); my @statb = stat($fullname); if(@statb) { my $mtime = localtime $statb[9]; print "Last-Modified: $mtime\n"; # TODO: only for HTTP/1.1 connections # $etag = Digest::MD5::md5_hex($html); printf "ETag: \"%x\"\n", $statb[9]; } =cut | |||||
299 | ||||||
300 | sub script_path { | |||||
301 | 26 | 3957 | my $self = shift; | |||
302 | ||||||
303 | 26 | 32 | unless($self->{script_path}) { | |||
304 | 6 | 6 | $self->_find_paths(); | |||
305 | } | |||||
306 | 26 | 92 | return $self->{script_path}; | |||
307 | } | |||||
308 | ||||||
309 - 323 | =head2 script_dir Returns the file system directory containing the script. use CGI::Info; use File::Spec; my $info = CGI::Info->new(); print 'HTML files are normally stored in ', $info->script_dir(), '/', File::Spec->updir(), "\n"; # or use lib CGI::Info::script_dir() . '../lib'; =cut | |||||
324 | ||||||
325 | sub script_dir | |||||
326 | { | |||||
327 | 14 | 11 | my $self = shift; | |||
328 | ||||||
329 | # Ensure $self is an object | |||||
330 | 14 | 17 | $self = __PACKAGE__->new() unless ref $self; | |||
331 | ||||||
332 | # Set script path if it is not already defined | |||||
333 | 14 | 30 | $self->_find_paths() unless $self->{script_path}; | |||
334 | ||||||
335 | # Extract directory from script path based on OS | |||||
336 | # Don't use File::Spec->splitpath() since that can leave the trailing slash | |||||
337 | 14 | 25 | my $dir_regex = $^O eq 'MSWin32' ? qr{(.+)\\.+?$} : qr{(.+)/.+?$}; | |||
338 | ||||||
339 | 14 | 105 | return $self->{script_path} =~ $dir_regex ? $1 : $self->{script_path}; | |||
340 | } | |||||
341 | ||||||
342 - 361 | =head2 host_name Return the host-name of the current web server, according to CGI. If the name can't be determined from the web server, the system's host-name is used as a fall back. This may not be the same as the machine that the CGI script is running on, some ISPs and other sites run scripts on different machines from those delivering static content. There is a good chance that this will be domain_name() prepended with either 'www' or 'cgi'. use CGI::Info; my $info = CGI::Info->new(); my $host_name = $info->host_name(); my $protocol = $info->protocol(); # ... print "Thank you for visiting our <A HREF=\"$protocol://$host_name\">Website!</A>"; =cut | |||||
362 | ||||||
363 | sub host_name { | |||||
364 | 10 | 747 | my $self = shift; | |||
365 | ||||||
366 | 10 | 15 | unless($self->{site}) { | |||
367 | 3 | 7 | $self->_find_site_details(); | |||
368 | } | |||||
369 | ||||||
370 | 10 | 63 | return $self->{site}; | |||
371 | } | |||||
372 | ||||||
373 | sub _find_site_details | |||||
374 | { | |||||
375 | 10 | 9 | my $self = shift; | |||
376 | ||||||
377 | # Log entry to the routine | |||||
378 | 10 | 15 | $self->_trace('Entering _find_site_details'); | |||
379 | ||||||
380 | 10 | 142 | return if $self->{site} && $self->{cgi_site}; | |||
381 | ||||||
382 | # Determine cgi_site using environment variables or hostname | |||||
383 | 8 | 31 | if (my $host = ($ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'} || $ENV{'SSL_TLS_SNI'})) { | |||
384 | # Import necessary module | |||||
385 | 5 | 209 | require URI::Heuristic unless URI::Heuristic->can('uf_uristr'); | |||
386 | ||||||
387 | 5 | 924 | $self->{cgi_site} = URI::Heuristic::uf_uristr($host); | |||
388 | # Remove trailing dots from the name. They are legal in URLs | |||||
389 | # and some sites link using them to avoid spoofing (nice) | |||||
390 | 5 | 51 | $self->{cgi_site} =~ s/(.*)\.+$/$1/; # Trim trailing dots | |||
391 | ||||||
392 | 5 | 13 | if($ENV{'SERVER_NAME'} && ($host eq $ENV{'SERVER_NAME'}) && (my $protocol = $self->protocol()) && $self->protocol() ne 'http') { | |||
393 | 1 | 2 | $self->{cgi_site} =~ s/^http/$protocol/; | |||
394 | } | |||||
395 | } else { | |||||
396 | # Import necessary module | |||||
397 | 3 | 15 | require Sys::Hostname unless Sys::Hostname->can('hostname'); | |||
398 | ||||||
399 | 3 | 7 | $self->_debug('Falling back to using hostname'); | |||
400 | 3 | 31 | $self->{cgi_site} = Sys::Hostname::hostname(); | |||
401 | } | |||||
402 | ||||||
403 | # Set site details if not already defined | |||||
404 | 8 | 31 | $self->{site} ||= $self->{cgi_site}; | |||
405 | 8 | 17 | $self->{site} =~ s/^https?:\/\/(.+)/$1/; | |||
406 | $self->{cgi_site} = ($self->protocol() || 'http') . '://' . $self->{cgi_site} | |||||
407 | 8 | 20 | unless $self->{cgi_site} =~ /^https?:\/\//; | |||
408 | ||||||
409 | # Warn if site details could not be determined | |||||
410 | 8 | 20 | $self->_warn('Could not determine site name') unless($self->{site} && $self->{cgi_site}); | |||
411 | ||||||
412 | # Log exit | |||||
413 | 8 | 9 | $self->_trace('Leaving _find_site_details'); | |||
414 | } | |||||
415 | ||||||
416 - 423 | =head2 domain_name Domain_name is the name of the controlling domain for this website. Usually it will be similar to host_name, but will lack the http:// or www prefixes. Can be called as a class method. =cut | |||||
424 | ||||||
425 | sub domain_name { | |||||
426 | 7 | 154 | my $self = shift; | |||
427 | ||||||
428 | 7 | 9 | if(!ref($self)) { | |||
429 | 1 | 2 | $self = __PACKAGE__->new(); | |||
430 | } | |||||
431 | 7 | 13 | return $self->{domain} if $self->{domain}; | |||
432 | ||||||
433 | 4 | 8 | $self->_find_site_details(); | |||
434 | ||||||
435 | 4 | 28 | if(my $site = $self->{site}) { | |||
436 | 4 | 8 | $self->{domain} = ($site =~ /^www\.(.+)/) ? $1 : $site; | |||
437 | } | |||||
438 | ||||||
439 | 4 | 9 | return $self->{domain}; | |||
440 | } | |||||
441 | ||||||
442 - 446 | =head2 cgi_host_url Return the URL of the machine running the CGI script. =cut | |||||
447 | ||||||
448 | sub cgi_host_url { | |||||
449 | 7 | 24 | my $self = shift; | |||
450 | ||||||
451 | 7 | 8 | unless($self->{cgi_site}) { | |||
452 | 3 | 4 | $self->_find_site_details(); | |||
453 | } | |||||
454 | ||||||
455 | 7 | 50 | return $self->{cgi_site}; | |||
456 | } | |||||
457 | ||||||
458 - 613 | =head2 params Returns a reference to a hash list of the CGI arguments. CGI::Info helps you to test your script prior to deployment on a website: if it is not in a CGI environment (e.g. the script is being tested from the command line), the program's command line arguments (a list of key=value pairs) are used, if there are no command line arguments then they are read from stdin as a list of key=value lines. Also you can give one of --tablet, --search-engine, --mobile and --robot to mimic those agents. For example: ./script.cgi --mobile name=Nigel Returns undef if the parameters can't be determined or if none were given. If an argument is given twice or more, then the values are put in a comma separated string. The returned hash value can be passed into L<CGI::Untaint>. Takes four optional parameters: allow, logger and upload_dir. The parameters are passed in a hash, or a reference to a hash. The latter is more efficient since it puts less on the stack. Allow is a reference to a hash list of CGI parameters that you will allow. The value for each entry is either a permitted value, a regular expression of permitted values for the key, a code reference, or a hash of L<Params::Validate::Strict> rules. Subroutine exceptions propagate normally, allowing custom error handling. This works alongside existing regex and Params::Validate::Strict patterns. A undef value means that any value will be allowed. Arguments not in the list are silently ignored. This is useful to help to block attacks on your site. Upload_dir is a string containing a directory where files being uploaded are to be stored. It must be a writeable directory in the temporary area. Takes an optional parameter logger, which is used for warnings and traces. It can be an object that understands warn() and trace() messages, such as a L<Log::Log4perl> or L<Log::Any> object, a reference to code, a reference to an array, or a filename. The allow, logger and upload_dir arguments can also be passed to the constructor. use CGI::Info; use CGI::Untaint; # ... my $info = CGI::Info->new(); my %params; if($info->params()) { %params = %{$info->params()}; } # ... foreach(keys %params) { print "$_ => $params{$_}\n"; } my $u = CGI::Untaint->new(%params); use CGI::Info; use CGI::IDS; # ... my $info = CGI::Info->new(); my $allowed = { foo => qr/^\d*$/, # foo must be a number, or empty bar => undef, # bar can be given and be any value xyzzy => qr/^[\w\s-]+$/, # must be alphanumeric # to prevent XSS, and non-empty # as a sanity check }; # or $allowed = { email => { type => 'string', matches => qr/^[^@]+@[^@]+\.[^@]+$/ }, # String, basic email format check age => { type => 'integer', min => 0, max => 150 }, # Integer between 0 and 150 bio => { type => 'string', optional => 1 }, # String, optional ip_address => { type => 'string', matches => qr/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/ }, #Basic IPv4 validation }; my $paramsref = $info->params(allow => $allowed); if(defined($paramsref)) { my $ids = CGI::IDS->new(); $ids->set_scan_keys(scan_keys => 1); if($ids->detect_attacks(request => $paramsref) > 0) { die 'horribly'; } } If the request is an XML request (i.e. the content type of the POST is text/xml), CGI::Info will put the request into the params element 'XML', thus: use CGI::Info; # ... my $info = CGI::Info->new(); my $paramsref = $info->params(); # See BUGS below my $xml = $$paramsref{'XML'}; # ... parse and process the XML request in $xml Carp if logger is not set and we detect something serious. Blocks some attacks, such as SQL and XSS injections, mustleak and directory traversals, thus creating a primitive web application firewall (WAF). Warning - this is an extra layer, not a replacement for your other security layers. =head3 Validation Subroutine Support The C<allow> parameter accepts subroutine references for dynamic validation, enabling complex parameter checks beyond static regex patterns. These callbacks: =over 4 =item * Receive three arguments: the parameter key, value and the C<CGI::Info> instance =item * Must return a true value to allow the parameter, false to reject =item * Can access other parameters through the instance for contextual validation =back Basic usage: CGI::Info->new( allow => { # Simple value check even_number => sub { ($_[1] % 2) == 0 }, # Context-aware validation child_age => sub { my ($key, $value, $info) = @_; $info->param('is_parent') ? $value <= 18 : 0 } } ); Advanced features: # Combine with regex validation mixed_validation => { email => qr/@/, # Regex check promo_code => \&validate_promo_code # Subroutine check } # Throw custom exceptions dangerous_param => sub { die 'Hacking attempt!' if $_[1] =~ /DROP TABLE/; return 1; } =cut | |||||
614 | ||||||
615 | sub params { | |||||
616 | 165 | 3587 | my $self = shift; | |||
617 | ||||||
618 | 165 | 191 | my $params = Params::Get::get_params(undef, @_); | |||
619 | ||||||
620 | 165 | 1539 | if((defined($self->{paramref})) && ((!defined($params->{'allow'})) || defined($self->{allow}) && ($params->{'allow'} eq $self->{allow}))) { | |||
621 | 47 | 58 | return $self->{paramref}; | |||
622 | } | |||||
623 | ||||||
624 | 118 | 124 | if(defined($params->{allow})) { | |||
625 | 11 | 9 | $self->{allow} = $params->{allow}; | |||
626 | } | |||||
627 | # if(defined($params->{expect})) { | |||||
628 | # if(ref($params->{expect}) eq 'ARRAY') { | |||||
629 | # $self->{expect} = $params->{expect}; | |||||
630 | # $self->_warn('expect is deprecated, use allow instead'); | |||||
631 | # } else { | |||||
632 | # $self->_warn('expect must be a reference to an array'); | |||||
633 | # } | |||||
634 | # } | |||||
635 | 118 | 116 | if(defined($params->{upload_dir})) { | |||
636 | 4 | 4 | $self->{upload_dir} = $params->{upload_dir}; | |||
637 | } | |||||
638 | 118 | 97 | if(defined($params->{'logger'})) { | |||
639 | 2 | 3 | $self->set_logger($params->{'logger'}); | |||
640 | } | |||||
641 | 118 | 162 | $self->_trace('Entering params'); | |||
642 | ||||||
643 | 118 | 1727 | my @pairs; | |||
644 | 118 | 103 | my $content_type = $ENV{'CONTENT_TYPE'}; | |||
645 | 118 | 83 | my %FORM; | |||
646 | ||||||
647 | 118 | 322 | if((!$ENV{'GATEWAY_INTERFACE'}) || (!$ENV{'REQUEST_METHOD'})) { | |||
648 | 9 | 623 | require IO::Interactive; | |||
649 | 9 | 1299 | IO::Interactive->import(); | |||
650 | ||||||
651 | 9 | 84 | if(@ARGV) { | |||
652 | 9 | 8 | @pairs = @ARGV; | |||
653 | 9 | 15 | if(defined($pairs[0])) { | |||
654 | 9 | 20 | if($pairs[0] eq '--robot') { | |||
655 | 1 | 2 | $self->{is_robot} = 1; | |||
656 | 1 | 1 | shift @pairs; | |||
657 | } elsif($pairs[0] eq '--mobile') { | |||||
658 | 2 | 4 | $self->{is_mobile} = 1; | |||
659 | 2 | 2 | shift @pairs; | |||
660 | } elsif($pairs[0] eq '--search-engine') { | |||||
661 | 1 | 1 | $self->{is_search_engine} = 1; | |||
662 | 1 | 1 | shift @pairs; | |||
663 | } elsif($pairs[0] eq '--tablet') { | |||||
664 | 1 | 1 | $self->{is_tablet} = 1; | |||
665 | 1 | 1 | shift @pairs; | |||
666 | } | |||||
667 | } | |||||
668 | } elsif($stdin_data) { | |||||
669 | 0 | 0 | @pairs = split(/\n/, $stdin_data); | |||
670 | } elsif(IO::Interactive::is_interactive() && !$self->{args_read}) { | |||||
671 | 0 | 0 | my $oldfh = select(STDOUT); | |||
672 | 0 | 0 | print "Entering debug mode\n", | |||
673 | "Enter key=value pairs - end with quit\n"; | |||||
674 | 0 | 0 | select($oldfh); | |||
675 | ||||||
676 | # Avoid prompting for the arguments more than once | |||||
677 | # if just 'quit' is entered | |||||
678 | 0 | 0 | $self->{args_read} = 1; | |||
679 | ||||||
680 | 0 | 0 | while(<STDIN>) { | |||
681 | 0 | 0 | chop(my $line = $_); | |||
682 | 0 | 0 | $line =~ s/[\r\n]//g; | |||
683 | 0 | 0 | last if $line eq 'quit'; | |||
684 | 0 | 0 | push(@pairs, $line); | |||
685 | 0 | 0 | $stdin_data .= "$line\n"; | |||
686 | } | |||||
687 | } | |||||
688 | } elsif(($ENV{'REQUEST_METHOD'} eq 'GET') || ($ENV{'REQUEST_METHOD'} eq 'HEAD')) { | |||||
689 | 80 | 93 | if(my $query = $ENV{'QUERY_STRING'}) { | |||
690 | 75 | 71 | if((defined($content_type)) && ($content_type =~ /multipart\/form-data/i)) { | |||
691 | 1 | 1 | $self->_warn('Multipart/form-data not supported for GET'); | |||
692 | } | |||||
693 | 74 | 72 | $query =~ s/\\u0026/\&/g; | |||
694 | 74 | 101 | @pairs = split(/&/, $query); | |||
695 | } else { | |||||
696 | 5 | 16 | return; | |||
697 | } | |||||
698 | } elsif($ENV{'REQUEST_METHOD'} eq 'POST') { | |||||
699 | 26 | 35 | my $content_length = $self->_get_env('CONTENT_LENGTH'); | |||
700 | 26 | 61 | if((!defined($content_length)) || ($content_length =~ /\D/)) { | |||
701 | 2 | 2 | $self->{status} = 411; | |||
702 | 2 | 5 | return; | |||
703 | } | |||||
704 | 24 | 60 | if(($self->{max_upload_size} >= 0) && ($content_length > $self->{max_upload_size})) { # Set maximum posts | |||
705 | # TODO: Design a way to tell the caller to send HTTP | |||||
706 | # status 413 | |||||
707 | 2 | 2 | $self->{status} = 413; | |||
708 | 2 | 4 | $self->_warn('Large upload prohibited'); | |||
709 | 2 | 3 | return; | |||
710 | } | |||||
711 | ||||||
712 | 22 | 77 | if((!defined($content_type)) || ($content_type =~ /application\/x-www-form-urlencoded/)) { | |||
713 | 4 | 3 | my $buffer; | |||
714 | 4 | 5 | if($stdin_data) { | |||
715 | 1 | 1 | $buffer = $stdin_data; | |||
716 | } else { | |||||
717 | 3 | 10 | if(read(STDIN, $buffer, $content_length) != $content_length) { | |||
718 | 1 | 2 | $self->_warn('POST failed: something else may have read STDIN'); | |||
719 | } | |||||
720 | 3 | 2 | $stdin_data = $buffer; | |||
721 | } | |||||
722 | 4 | 7 | @pairs = split(/&/, $buffer); | |||
723 | ||||||
724 | # if($ENV{'QUERY_STRING'}) { | |||||
725 | # my @getpairs = split(/&/, $ENV{'QUERY_STRING'}); | |||||
726 | # push(@pairs, @getpairs); | |||||
727 | # } | |||||
728 | } elsif($content_type =~ /multipart\/form-data/i) { | |||||
729 | 15 | 19 | if(!defined($self->{upload_dir})) { | |||
730 | 1 | 1 | $self->_warn({ | |||
731 | warning => 'Attempt to upload a file when upload_dir has not been set' | |||||
732 | }); | |||||
733 | 0 | 0 | return; | |||
734 | } | |||||
735 | ||||||
736 | # Validate 'upload_dir' | |||||
737 | # Ensure the upload directory is safe and accessible | |||||
738 | # - Check permissions | |||||
739 | # - Validate path to prevent directory traversal attacks | |||||
740 | # TODO: Consider using a temporary directory for uploads and moving them later | |||||
741 | 14 | 48 | if(!File::Spec->file_name_is_absolute($self->{upload_dir})) { | |||
742 | 3 | 25 | $self->_warn({ | |||
743 | warning => "upload_dir $self->{upload_dir} isn't a full pathname" | |||||
744 | }); | |||||
745 | 2 | 3 | $self->status(500); | |||
746 | 2 | 2 | delete $self->{upload_dir}; | |||
747 | 2 | 4 | return; | |||
748 | } | |||||
749 | 11 | 70 | if(!-d $self->{upload_dir}) { | |||
750 | 3 | 16 | $self->_warn({ | |||
751 | warning => "upload_dir $self->{upload_dir} isn't a directory" | |||||
752 | }); | |||||
753 | 1 | 3 | $self->status(500); | |||
754 | 1 | 2 | delete $self->{upload_dir}; | |||
755 | 1 | 2 | return; | |||
756 | } | |||||
757 | 8 | 36 | if(!-w $self->{upload_dir}) { | |||
758 | 2 | 2 | delete $self->{paramref}; | |||
759 | 2 | 9 | $self->_warn({ | |||
760 | warning => "upload_dir $self->{upload_dir} isn't writeable" | |||||
761 | }); | |||||
762 | 1 | 4 | $self->status(500); | |||
763 | 1 | 2 | delete $self->{upload_dir}; | |||
764 | 1 | 2 | return; | |||
765 | } | |||||
766 | 6 | 11 | my $tmpdir = $self->tmpdir(); | |||
767 | 6 | 32 | if($self->{'upload_dir'} !~ /^\Q$tmpdir\E/) { | |||
768 | $self->_warn({ | |||||
769 | 0 | 0 | warning => 'upload_dir ' . $self->{'upload_dir'} . " isn't somewhere in the temporary area $tmpdir" | |||
770 | }); | |||||
771 | 0 | 0 | $self->status(500); | |||
772 | 0 | 0 | delete $self->{upload_dir}; | |||
773 | 0 | 0 | return; | |||
774 | } | |||||
775 | 6 | 14 | if($content_type =~ /boundary=(\S+)$/) { | |||
776 | 6 | 16 | @pairs = $self->_multipart_data({ | |||
777 | length => $content_length, | |||||
778 | boundary => $1 | |||||
779 | }); | |||||
780 | } | |||||
781 | } elsif($content_type =~ /text\/xml/i) { | |||||
782 | 1 | 1 | my $buffer; | |||
783 | 1 | 1 | if($stdin_data) { | |||
784 | 0 | 0 | $buffer = $stdin_data; | |||
785 | } else { | |||||
786 | 1 | 3 | if(read(STDIN, $buffer, $content_length) != $content_length) { | |||
787 | 0 | 0 | $self->_warn({ | |||
788 | warning => 'XML failed: something else may have read STDIN' | |||||
789 | }); | |||||
790 | } | |||||
791 | 1 | 1 | $stdin_data = $buffer; | |||
792 | } | |||||
793 | ||||||
794 | 1 | 1 | $FORM{XML} = $buffer; | |||
795 | ||||||
796 | 1 | 1 | $self->{paramref} = \%FORM; | |||
797 | ||||||
798 | 1 | 3 | return \%FORM; | |||
799 | } elsif($content_type =~ /application\/json/i) { | |||||
800 | 1 | 21 | require JSON::MaybeXS && JSON::MaybeXS->import() unless JSON::MaybeXS->can('parse_json'); | |||
801 | # require JSON::MaybeXS; | |||||
802 | # JSON::MaybeXS->import(); | |||||
803 | ||||||
804 | 1 | 1 | my $buffer; | |||
805 | ||||||
806 | 1 | 1 | if($stdin_data) { | |||
807 | 0 | 0 | $buffer = $stdin_data; | |||
808 | } else { | |||||
809 | 1 | 3 | if(read(STDIN, $buffer, $content_length) != $content_length) { | |||
810 | 0 | 0 | $self->_warn({ | |||
811 | warning => 'read failed: something else may have read STDIN' | |||||
812 | }); | |||||
813 | } | |||||
814 | 1 | 1 | $stdin_data = $buffer; | |||
815 | } | |||||
816 | # JSON::Parse::assert_valid_json($buffer); | |||||
817 | # my $paramref = JSON::Parse::parse_json($buffer); | |||||
818 | 1 | 6 | my $paramref = decode_json($buffer); | |||
819 | 1 1 | 1 1 | foreach my $key(keys(%{$paramref})) { | |||
820 | 2 | 4 | push @pairs, "$key=" . $paramref->{$key}; | |||
821 | } | |||||
822 | } else { | |||||
823 | 1 | 2 | my $buffer; | |||
824 | 1 | 1 | if($stdin_data) { | |||
825 | 0 | 0 | $buffer = $stdin_data; | |||
826 | } else { | |||||
827 | 1 | 2 | if(read(STDIN, $buffer, $content_length) != $content_length) { | |||
828 | 0 | 0 | $self->_warn({ | |||
829 | warning => 'read failed: something else may have read STDIN' | |||||
830 | }); | |||||
831 | } | |||||
832 | 1 | 1 | $stdin_data = $buffer; | |||
833 | } | |||||
834 | ||||||
835 | 1 | 3 | $self->_warn({ | |||
836 | warning => "POST: Invalid or unsupported content type: $content_type: $buffer", | |||||
837 | }); | |||||
838 | } | |||||
839 | } elsif($ENV{'REQUEST_METHOD'} eq 'OPTIONS') { | |||||
840 | 0 | 0 | $self->{status} = 405; | |||
841 | 0 | 0 | return; | |||
842 | } elsif($ENV{'REQUEST_METHOD'} eq 'DELETE') { | |||||
843 | 1 | 1 | $self->{status} = 405; | |||
844 | 1 | 2 | return; | |||
845 | } else { | |||||
846 | # TODO: Design a way to tell the caller to send HTTP | |||||
847 | # status 501 | |||||
848 | 2 | 3 | $self->{status} = 501; | |||
849 | 2 | 4 | $self->_warn({ | |||
850 | warning => 'Use POST, GET or HEAD' | |||||
851 | }); | |||||
852 | } | |||||
853 | ||||||
854 | 93 | 122 | unless(scalar @pairs) { | |||
855 | 1 | 2 | return; | |||
856 | } | |||||
857 | ||||||
858 | 92 | 1939 | require String::Clean::XSS; | |||
859 | 92 | 47664 | String::Clean::XSS->import(); | |||
860 | # require String::EscapeCage; | |||||
861 | # String::EscapeCage->import(); | |||||
862 | ||||||
863 | 92 | 107 | foreach my $arg (@pairs) { | |||
864 | 192 | 238 | my($key, $value) = split(/=/, $arg, 2); | |||
865 | ||||||
866 | 192 | 165 | next unless($key); | |||
867 | ||||||
868 | 188 | 134 | $key =~ s/\0//g; # Strip encoded NUL byte poison | |||
869 | 188 | 114 | $key =~ s/%00//g; # Strip NUL byte poison | |||
870 | 188 1 | 118 3 | $key =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg; | |||
871 | 188 | 148 | $key =~ tr/+/ /; | |||
872 | 188 | 176 | if(defined($value)) { | |||
873 | 188 | 120 | $value =~ s/\0//g; # Strip NUL byte poison | |||
874 | 188 | 102 | $value =~ s/%00//g; # Strip encoded NUL byte poison | |||
875 | 188 83 | 128 105 | $value =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg; | |||
876 | 188 | 114 | $value =~ tr/+/ /; | |||
877 | } else { | |||||
878 | 0 | 0 | $value = ''; | |||
879 | } | |||||
880 | ||||||
881 | 188 | 155 | $key = _sanitise_input($key); | |||
882 | ||||||
883 | 188 | 14126 | if($self->{allow}) { | |||
884 | # Is this a permitted argument? | |||||
885 | 78 | 81 | if(!exists($self->{allow}->{$key})) { | |||
886 | 17 | 23 | $self->_info("Discard unallowed argument '$key'"); | |||
887 | 17 | 251 | $self->status(422); | |||
888 | 17 | 17 | next; # Skip to the next parameter | |||
889 | } | |||||
890 | ||||||
891 | # Do we allow any value, or must it be validated? | |||||
892 | 61 | 65 | if(defined(my $schema = $self->{allow}->{$key})) { # Get the schema for this key | |||
893 | 54 | 74 | if(!ref($schema)) { | |||
894 | # Can only contain one value | |||||
895 | 3 | 4 | if($value ne $schema) { | |||
896 | 2 | 2 | $self->_info("Block $key = $value"); | |||
897 | 2 | 23 | $self->status(422); | |||
898 | 2 | 3 | next; # Skip to the next parameter | |||
899 | } | |||||
900 | } elsif(ref($schema) eq 'Regexp') { | |||||
901 | 12 | 31 | if($value !~ $schema) { | |||
902 | # Simple regex | |||||
903 | 8 | 13 | $self->_info("Block $key = $value"); | |||
904 | 8 | 102 | $self->status(422); | |||
905 | 8 | 11 | next; # Skip to the next parameter | |||
906 | } | |||||
907 | } elsif(ref($schema) eq 'CODE') { | |||||
908 | 9 | 9 | unless($schema->($key, $value, $self)) { | |||
909 | 2 | 8 | $self->_info("Block $key = $value"); | |||
910 | 2 | 24 | next; | |||
911 | } | |||||
912 | } else { | |||||
913 | # Set of rules | |||||
914 | 30 | 19 | eval { | |||
915 | 30 | 53 | $value = Params::Validate::Strict::validate_strict({ | |||
916 | schema => { $key => $schema }, | |||||
917 | args => { $key => $value }, | |||||
918 | unknown_parameter_handler => 'warn', | |||||
919 | }); | |||||
920 | }; | |||||
921 | 30 | 6883 | if($@) { | |||
922 | 6 | 14 | $self->_info("Block $key = $value: $@"); | |||
923 | 6 | 106 | $self->status(422); | |||
924 | 6 | 7 | next; # Skip to the next parameter | |||
925 | } | |||||
926 | 24 | 17 | $value = $value->{$key}; | |||
927 | } | |||||
928 | } | |||||
929 | } | |||||
930 | ||||||
931 | # if($self->{expect} && (List::Util::none { $_ eq $key } @{$self->{expect}})) { | |||||
932 | # next; | |||||
933 | # } | |||||
934 | 152 | 187 | my $orig_value = $value; | |||
935 | 152 | 108 | $value = _sanitise_input($value); | |||
936 | ||||||
937 | 152 | 9323 | if((!defined($ENV{'REQUEST_METHOD'})) || ($ENV{'REQUEST_METHOD'} eq 'GET')) { | |||
938 | # From http://www.symantec.com/connect/articles/detection-sql-injection-and-cross-site-scripting-attacks | |||||
939 | # Facebook FBCLID can have "--" | |||||
940 | # if(($value =~ /(\%27)|(\')|(\-\-)|(\%23)|(\#)/ix) || | |||||
941 | 137 | 1183 | if(($value =~ /(\%27)|(\')|(\%23)|(\#)/ix) || | |||
942 | ($value =~ /((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))/i) || | |||||
943 | ($value =~ /\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))\s*(OR|AND|UNION|SELECT|--)/ix) || | |||||
944 | ($value =~ /((\%27)|(\'))union/ix) || | |||||
945 | ($value =~ /select[[a-z]\s\*]from/ix) || | |||||
946 | ($value =~ /\sAND\s1=1/ix) || | |||||
947 | ($value =~ /\sOR\s.+\sAND\s/) || | |||||
948 | ($value =~ /\/\*\*\/ORDER\/\*\*\/BY\/\*\*/ix) || | |||||
949 | ($value =~ /\/AND\/.+\(SELECT\//) || # United/**/States)/**/AND/**/(SELECT/**/6734/**/FROM/**/(SELECT(SLEEP(5)))lRNi)/**/AND/**/(8984=8984 | |||||
950 | ($value =~ /exec(\s|\+)+(s|x)p\w+/ix)) { | |||||
951 | 11 | 18 | $self->status(403); | |||
952 | 11 | 14 | if($ENV{'REMOTE_ADDR'}) { | |||
953 | 1 | 2 | $self->_warn($ENV{'REMOTE_ADDR'} . ": SQL injection attempt blocked for '$key=$value'"); | |||
954 | } else { | |||||
955 | 10 | 16 | $self->_warn("SQL injection attempt blocked for '$key=$value'"); | |||
956 | } | |||||
957 | 11 | 28 | return; | |||
958 | } | |||||
959 | 126 | 115 | if(my $agent = $ENV{'HTTP_USER_AGENT'}) { | |||
960 | 0 | 0 | if(($agent =~ /SELECT.+AND.+/) || ($agent =~ /ORDER BY /) || ($agent =~ / OR NOT /) || ($agent =~ / AND \d+=\d+/) || ($agent =~ /THEN.+ELSE.+END/) || ($agent =~ /.+AND.+SELECT.+/) || ($agent =~ /\sAND\s.+\sAND\s/)) { | |||
961 | 0 | 0 | $self->status(403); | |||
962 | 0 | 0 | if($ENV{'REMOTE_ADDR'}) { | |||
963 | 0 | 0 | $self->_warn($ENV{'REMOTE_ADDR'} . ": SQL injection attempt blocked for '$agent'"); | |||
964 | } else { | |||||
965 | 0 | 0 | $self->_warn("SQL injection attempt blocked for '$agent'"); | |||
966 | } | |||||
967 | 0 | 0 | return; | |||
968 | } | |||||
969 | } | |||||
970 | 126 | 482 | if(($value =~ /((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/ix) || | |||
971 | ($value =~ /((\%3C)|<)[^\n]+((\%3E)|>)/i) || | |||||
972 | ($orig_value =~ /((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/ix) || | |||||
973 | ($orig_value =~ /((\%3C)|<)[^\n]+((\%3E)|>)/i)) { | |||||
974 | 5 | 11 | $self->status(403); | |||
975 | 5 | 10 | $self->_warn("XSS injection attempt blocked for '$value'"); | |||
976 | 5 | 11 | return; | |||
977 | } | |||||
978 | 121 | 84 | if($value =~ /mustleak\.com\//) { | |||
979 | 0 | 0 | $self->status(403); | |||
980 | 0 | 0 | $self->_warn("Blocked mustleak attack for '$key'"); | |||
981 | 0 | 0 | return; | |||
982 | } | |||||
983 | 121 | 111 | if($value =~ /\.\.\//) { | |||
984 | 3 | 6 | $self->status(403); | |||
985 | 3 | 6 | $self->_warn("Blocked directory traversal attack for '$key'"); | |||
986 | 2 | 4 | return; | |||
987 | } | |||||
988 | } | |||||
989 | 133 | 129 | if(length($value) > 0) { | |||
990 | # Don't add if it's already there | |||||
991 | 128 | 130 | if($FORM{$key} && ($FORM{$key} ne $value)) { | |||
992 | 3 | 4 | $FORM{$key} .= ",$value"; | |||
993 | } else { | |||||
994 | 125 | 137 | $FORM{$key} = $value; | |||
995 | } | |||||
996 | } | |||||
997 | } | |||||
998 | ||||||
999 | 72 | 74 | unless(%FORM) { | |||
1000 | 11 | 23 | return; | |||
1001 | } | |||||
1002 | ||||||
1003 | 61 | 59 | if($self->{'logger'}) { | |||
1004 | 61 | 88 | while(my ($key,$value) = each %FORM) { | |||
1005 | 115 | 779 | $self->_debug("$key=$value"); | |||
1006 | } | |||||
1007 | } | |||||
1008 | ||||||
1009 | 61 | 730 | $self->{paramref} = \%FORM; | |||
1010 | ||||||
1011 | 61 | 124 | return Return::Set::set_return(\%FORM, { type => 'hashref', min => 1 }); | |||
1012 | } | |||||
1013 | ||||||
1014 - 1037 | =head2 param Get a single parameter from the query string. Takes an optional single string parameter which is the argument to return. If that parameter is not given param() is a wrapper to params() with no arguments. use CGI::Info; # ... my $info = CGI::Info->new(); my $bar = $info->param('foo'); If the requested parameter isn't in the allowed list, an error message will be thrown: use CGI::Info; my $allowed = { foo => qr/\d+/ }; my $xyzzy = $info->params(allow => $allowed); my $bar = $info->param('bar'); # Gives an error message Returns undef if the requested parameter was not given =cut | |||||
1038 | ||||||
1039 | sub param { | |||||
1040 | 40 | 4119 | my ($self, $field) = @_; | |||
1041 | ||||||
1042 | 40 | 40 | if(!defined($field)) { | |||
1043 | 2 | 4 | return $self->params(); | |||
1044 | } | |||||
1045 | # Is this a permitted argument? | |||||
1046 | 38 | 60 | if($self->{allow} && !exists($self->{allow}->{$field})) { | |||
1047 | 5 | 9 | $self->_warn({ | |||
1048 | warning => "param: $field isn't in the allow list" | |||||
1049 | }); | |||||
1050 | 1 | 2 | return; | |||
1051 | } | |||||
1052 | ||||||
1053 | # Prevent deep recursion which can happen when a validation routine calls param() | |||||
1054 | 33 | 21 | my $allow; | |||
1055 | 33 | 41 | if($self->{in_param} && $self->{allow}) { | |||
1056 | 1 | 1 | $allow = delete $self->{allow}; | |||
1057 | } | |||||
1058 | 33 | 26 | $self->{in_param} = 1; | |||
1059 | ||||||
1060 | 33 | 38 | my $params = $self->params(); | |||
1061 | ||||||
1062 | 33 | 510 | $self->{in_param} = 0; | |||
1063 | 33 | 33 | $self->{allow} = $allow if($allow); | |||
1064 | ||||||
1065 | 33 | 43 | if($params) { | |||
1066 | 27 | 41 | return Return::Set::set_return($params->{$field}, { type => 'string' }); | |||
1067 | } | |||||
1068 | } | |||||
1069 | ||||||
1070 | sub _sanitise_input($) { | |||||
1071 | 340 | 213 | my $arg = shift; | |||
1072 | ||||||
1073 | # Remove hacking attempts and spaces | |||||
1074 | 340 | 245 | $arg =~ s/[\r\n]//g; | |||
1075 | 340 | 264 | $arg =~ s/\s+$//; | |||
1076 | 340 | 261 | $arg =~ s/^\s//; | |||
1077 | ||||||
1078 | 340 | 170 | $arg =~ s/<!--.*-->//g; | |||
1079 | # Allow : | |||||
1080 | # $arg =~ s/[;<>\*|`&\$!?#\(\)\[\]\{\}'"\\\r]//g; | |||||
1081 | ||||||
1082 | # return $arg; | |||||
1083 | # return String::EscapeCage->new(convert_XSS($arg))->escapecstring(); | |||||
1084 | 340 | 287 | return convert_XSS($arg); | |||
1085 | } | |||||
1086 | ||||||
1087 | sub _multipart_data { | |||||
1088 | 6 | 6 | my ($self, $args) = @_; | |||
1089 | ||||||
1090 | 6 | 7 | $self->_trace('Entering _multipart_data'); | |||
1091 | ||||||
1092 | 6 | 70 | my $total_bytes = $$args{length}; | |||
1093 | ||||||
1094 | 6 | 11 | $self->_debug("_multipart_data: total_bytes = $total_bytes"); | |||
1095 | ||||||
1096 | 6 | 61 | if($total_bytes == 0) { | |||
1097 | 0 | 0 | return; | |||
1098 | } | |||||
1099 | ||||||
1100 | 6 | 6 | unless($stdin_data) { | |||
1101 | 6 | 24 | while(<STDIN>) { | |||
1102 | 54 | 35 | chop(my $line = $_); | |||
1103 | 54 | 25 | $line =~ s/[\r\n]//g; | |||
1104 | 54 | 53 | $stdin_data .= "$line\n"; | |||
1105 | } | |||||
1106 | 6 | 5 | if(!$stdin_data) { | |||
1107 | 0 | 0 | return; | |||
1108 | } | |||||
1109 | } | |||||
1110 | ||||||
1111 | 6 | 6 | my $boundary = $$args{boundary}; | |||
1112 | ||||||
1113 | 6 | 3 | my @pairs; | |||
1114 | 6 | 5 | my $writing_file = 0; | |||
1115 | 6 | 2 | my $key; | |||
1116 | my $value; | |||||
1117 | 6 | 5 | my $in_header = 0; | |||
1118 | 6 | 2 | my $fout; | |||
1119 | ||||||
1120 | 6 | 15 | foreach my $line(split(/\n/, $stdin_data)) { | |||
1121 | 44 | 66 | if($line =~ /^--\Q$boundary\E--$/) { | |||
1122 | 2 | 1 | last; | |||
1123 | } | |||||
1124 | 42 | 56 | if($line =~ /^--\Q$boundary\E$/) { | |||
1125 | 10 | 12 | if($writing_file) { | |||
1126 | 0 | 0 | close $fout; | |||
1127 | 0 | 0 | $writing_file = 0; | |||
1128 | } elsif(defined($key)) { | |||||
1129 | 4 | 2 | push(@pairs, "$key=$value"); | |||
1130 | 4 | 3 | $value = undef; | |||
1131 | } | |||||
1132 | 10 | 11 | $in_header = 1; | |||
1133 | } elsif($in_header) { | |||||
1134 | 20 | 30 | if(length($line) == 0) { | |||
1135 | 8 | 5 | $in_header = 0; | |||
1136 | } elsif($line =~ /^Content-Disposition: (.+)/i) { | |||||
1137 | 10 | 21 | my $field = $1; | |||
1138 | 10 | 17 | if($field =~ /name="(.+?)"/) { | |||
1139 | 10 | 7 | $key = $1; | |||
1140 | } | |||||
1141 | 10 | 19 | if($field =~ /filename="(.+)?"/) { | |||
1142 | 6 | 4 | my $filename = $1; | |||
1143 | 6 | 11 | unless(defined($filename)) { | |||
1144 | 0 | 0 | $self->_warn('No upload filename given'); | |||
1145 | 0 | 0 | } elsif($filename =~ /[\\\/\|]/) { | |||
1146 | 2 | 3 | $self->_warn("Disallowing invalid filename: $filename"); | |||
1147 | } else { | |||||
1148 | 4 | 24 | $filename = $self->_create_file_name({ | |||
1149 | filename => $filename | |||||
1150 | }); | |||||
1151 | ||||||
1152 | # Don't do this since it taints the string and I can't work out how to untaint it | |||||
1153 | # my $full_path = Cwd::realpath(File::Spec->catfile($self->{upload_dir}, $filename)); | |||||
1154 | # $full_path =~ m/^(\/[\w\.]+)$/; | |||||
1155 | 4 | 21 | my $full_path = File::Spec->catfile($self->{upload_dir}, $filename); | |||
1156 | 4 | 169 | unless(open($fout, '>', $full_path)) { | |||
1157 | 0 | 0 | $self->_warn("Can't open $full_path"); | |||
1158 | } | |||||
1159 | 4 | 19 | $writing_file = 1; | |||
1160 | 4 | 11 | push(@pairs, "$key=$filename"); | |||
1161 | } | |||||
1162 | } | |||||
1163 | } | |||||
1164 | # TODO: handle Content-Type: text/plain, etc. | |||||
1165 | } else { | |||||
1166 | 12 | 9 | if($writing_file) { | |||
1167 | 8 | 36 | print $fout "$line\n"; | |||
1168 | } else { | |||||
1169 | 4 | 4 | $value .= $line; | |||
1170 | } | |||||
1171 | } | |||||
1172 | } | |||||
1173 | ||||||
1174 | 4 | 8 | if($writing_file) { | |||
1175 | 4 | 75 | close $fout; | |||
1176 | } | |||||
1177 | ||||||
1178 | 4 | 9 | $self->_trace('Leaving _multipart_data'); | |||
1179 | ||||||
1180 | 4 | 82 | return @pairs; | |||
1181 | } | |||||
1182 | ||||||
1183 | # Robust filename generation (preventing overwriting) | |||||
1184 | sub _create_file_name { | |||||
1185 | 4 | 4 | my ($self, $args) = @_; | |||
1186 | 4 | 7 | my $filename = $$args{filename} . '_' . time; | |||
1187 | ||||||
1188 | 4 | 3 | my $counter = 0; | |||
1189 | 4 | 4 | my $rc; | |||
1190 | ||||||
1191 | 4 | 2 | do { | |||
1192 | 4 | 6 | $rc = $filename . ($counter ? "_$counter" : ''); | |||
1193 | 4 | 37 | $counter++; | |||
1194 | } until(! -e $rc); # Check if file exists | |||||
1195 | ||||||
1196 | 4 | 5 | return $rc; | |||
1197 | } | |||||
1198 | ||||||
1199 | # Untaint a filename. Regex from CGI::Untaint::Filenames | |||||
1200 | sub _untaint_filename { | |||||
1201 | 51 | 43 | my ($self, $args) = @_; | |||
1202 | ||||||
1203 | 51 | 105 | if($$args{filename} =~ /(^[\w\+_\040\#\(\)\{\}\[\]\/\-\^,\.:;&%@\\~]+\$?$)/) { | |||
1204 | 51 | 82 | return $1; | |||
1205 | } | |||||
1206 | # return undef; | |||||
1207 | } | |||||
1208 | ||||||
1209 - 1217 | =head2 is_mobile Returns a boolean if the website is being viewed on a mobile device such as a smartphone. All tablets are mobile, but not all mobile devices are tablets. Can be overriden by the IS_MOBILE environment setting =cut | |||||
1218 | ||||||
1219 | sub is_mobile { | |||||
1220 | 43 | 1164 | my $self = shift; | |||
1221 | ||||||
1222 | 43 | 55 | if(defined($self->{is_mobile})) { | |||
1223 | 12 | 17 | return $self->{is_mobile}; | |||
1224 | } | |||||
1225 | ||||||
1226 | 31 | 38 | if($ENV{'IS_MOBILE'}) { | |||
1227 | 1 | 2 | return $ENV{'IS_MOBILE'} | |||
1228 | } | |||||
1229 | ||||||
1230 | # Support Sec-CH-UA-Mobile | |||||
1231 | 30 | 35 | if(my $ch_ua_mobile = $ENV{'HTTP_SEC_CH_UA_MOBILE'}) { | |||
1232 | 3 | 3 | if($ch_ua_mobile eq '?1') { | |||
1233 | 1 | 1 | $self->{is_mobile} = 1; | |||
1234 | 1 | 2 | return 1; | |||
1235 | } | |||||
1236 | } | |||||
1237 | ||||||
1238 | 29 | 34 | if($ENV{'HTTP_X_WAP_PROFILE'}) { | |||
1239 | # E.g. Blackberry | |||||
1240 | # TODO: Check the sanity of this variable | |||||
1241 | 1 | 1 | $self->{is_mobile} = 1; | |||
1242 | 1 | 3 | return 1; | |||
1243 | } | |||||
1244 | ||||||
1245 | 28 | 36 | if(my $agent = $ENV{'HTTP_USER_AGENT'}) { | |||
1246 | 18 | 779 | if($agent =~ /.+(Android|iPhone).+/) { | |||
1247 | 3 | 3 | $self->{is_mobile} = 1; | |||
1248 | 3 | 6 | return 1; | |||
1249 | } | |||||
1250 | ||||||
1251 | # From http://detectmobilebrowsers.com/ | |||||
1252 | 15 | 364 | if($agent =~ m/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i || substr($ENV{'HTTP_USER_AGENT'}, 0, 4) =~ m/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i) { | |||
1253 | 1 | 2 | $self->{is_mobile} = 1; | |||
1254 | 1 | 2 | return 1; | |||
1255 | } | |||||
1256 | ||||||
1257 | # Save loading and calling HTTP::BrowserDetect | |||||
1258 | 14 | 23 | my $remote = $ENV{'REMOTE_ADDR'}; | |||
1259 | 14 | 24 | if(defined($remote) && $self->{cache}) { | |||
1260 | 0 | 0 | if(my $type = $self->{cache}->get("$remote/$agent")) { | |||
1261 | 0 | 0 | return $self->{is_mobile} = ($type eq 'mobile'); | |||
1262 | } | |||||
1263 | } | |||||
1264 | ||||||
1265 | 14 | 29 | unless($self->{browser_detect}) { | |||
1266 | 8 8 | 6 1576 | if(eval { require HTTP::BrowserDetect; }) { | |||
1267 | 8 | 31167 | HTTP::BrowserDetect->import(); | |||
1268 | 8 | 15 | $self->{browser_detect} = HTTP::BrowserDetect->new($agent); | |||
1269 | } | |||||
1270 | } | |||||
1271 | ||||||
1272 | 14 | 590 | if($self->{browser_detect}) { | |||
1273 | 14 | 21 | my $device = $self->{browser_detect}->device(); | |||
1274 | # Without the ?1:0 it will set to the empty string not 0 | |||||
1275 | 14 | 59 | my $is_mobile = (defined($device) && ($device =~ /blackberry|webos|iphone|ipod|ipad|android/i)) ? 1 : 0; | |||
1276 | 14 | 21 | if($is_mobile && $self->{cache} && defined($remote)) { | |||
1277 | 0 | 0 | $self->{cache}->set("$remote/$agent", 'mobile', '1 day'); | |||
1278 | } | |||||
1279 | 14 | 31 | return $self->{is_mobile} = $is_mobile; | |||
1280 | } | |||||
1281 | } | |||||
1282 | ||||||
1283 | 10 | 18 | return 0; | |||
1284 | } | |||||
1285 | ||||||
1286 - 1290 | =head2 is_tablet Returns a boolean if the website is being viewed on a tablet such as an iPad. =cut | |||||
1291 | ||||||
1292 | sub is_tablet { | |||||
1293 | 6 | 24 | my $self = shift; | |||
1294 | ||||||
1295 | 6 | 7 | if(defined($self->{is_tablet})) { | |||
1296 | 1 | 2 | return $self->{is_tablet}; | |||
1297 | } | |||||
1298 | ||||||
1299 | 5 | 159 | if($ENV{'HTTP_USER_AGENT'} && ($ENV{'HTTP_USER_AGENT'} =~ /.+(iPad|TabletPC).+/)) { | |||
1300 | # TODO: add others when I see some nice user_agents | |||||
1301 | 1 | 1 | $self->{is_tablet} = 1; | |||
1302 | } else { | |||||
1303 | 4 | 3 | $self->{is_tablet} = 0; | |||
1304 | } | |||||
1305 | ||||||
1306 | 5 | 11 | return $self->{is_tablet}; | |||
1307 | } | |||||
1308 | ||||||
1309 - 1317 | =head2 as_string Converts CGI parameters into a formatted string representation with optional raw mode (no escaping of special characters). Useful for debugging or generating keys for a cache. my $string_representation = $info->as_string(); my $raw_string = $info->as_string({ raw => 1 }); =cut | |||||
1318 | ||||||
1319 | sub as_string | |||||
1320 | { | |||||
1321 | 40 | 7696 | my $self = shift; | |||
1322 | ||||||
1323 | # Retrieve object parameters | |||||
1324 | 40 | 46 | my $params = $self->params() || return ''; | |||
1325 | 30 | 169 | my $args = Params::Get::get_params(undef, @_); | |||
1326 | 30 | 195 | my $rc; | |||
1327 | ||||||
1328 | 30 | 33 | if($args->{'raw'}) { | |||
1329 | # Raw mode: return key=value pairs without escaping | |||||
1330 | $rc = join '; ', map { | |||||
1331 | 4 | 7 | "$_=" . $params->{$_} | |||
1332 | 2 2 | 2 4 | } sort keys %{$params}; | |||
1333 | } else { | |||||
1334 | # Escaped mode: escape special characters | |||||
1335 | $rc = join '; ', map { | |||||
1336 | 42 | 37 | my $value = $params->{$_}; | |||
1337 | ||||||
1338 | 42 | 37 | $value =~ s/\\/\\\\/g; # Escape backslashes | |||
1339 | 42 | 59 | $value =~ s/(;|=)/\\$1/g; # Escape semicolons and equals signs | |||
1340 | 42 | 73 | "$_=$value" | |||
1341 | 28 28 | 18 40 | } sort keys %{$params}; | |||
1342 | } | |||||
1343 | ||||||
1344 | 30 | 71 | $self->_trace("as_string: returning '$rc'") if($rc); | |||
1345 | ||||||
1346 | 30 | 420 | return $rc; | |||
1347 | } | |||||
1348 | ||||||
1349 - 1354 | =head2 protocol Returns the connection protocol, presumably 'http' or 'https', or undef if it can't be determined. =cut | |||||
1355 | ||||||
1356 | sub protocol { | |||||
1357 | 22 | 627 | my $self = shift; | |||
1358 | ||||||
1359 | 22 | 51 | if($ENV{'SCRIPT_URI'} && ($ENV{'SCRIPT_URI'} =~ /^(.+):\/\/.+/)) { | |||
1360 | 2 | 6 | return $1; | |||
1361 | } | |||||
1362 | 20 | 32 | if($ENV{'SERVER_PROTOCOL'} && ($ENV{'SERVER_PROTOCOL'} =~ /^HTTP\//)) { | |||
1363 | 2 | 7 | return 'http'; | |||
1364 | } | |||||
1365 | ||||||
1366 | 18 | 27 | if(my $port = $ENV{'SERVER_PORT'}) { | |||
1367 | 13 | 655 | if(defined(my $name = getservbyport($port, 'tcp'))) { | |||
1368 | 13 | 33 | if($name =~ /https?/) { | |||
1369 | 11 | 28 | return $name; | |||
1370 | } elsif($name eq 'www') { | |||||
1371 | # e.g. NetBSD and OpenBSD | |||||
1372 | 0 | 0 | return 'http'; | |||
1373 | } | |||||
1374 | # Return an error, maybe missing something | |||||
1375 | } elsif($port == 80) { | |||||
1376 | # e.g. Solaris | |||||
1377 | 0 | 0 | return 'http'; | |||
1378 | } elsif($port == 443) { | |||||
1379 | 0 | 0 | return 'https'; | |||
1380 | } | |||||
1381 | } | |||||
1382 | ||||||
1383 | 7 | 16 | if($ENV{'REMOTE_ADDR'}) { | |||
1384 | 0 | 0 | $self->_warn("Can't determine the calling protocol"); | |||
1385 | } | |||||
1386 | 7 | 18 | return; | |||
1387 | } | |||||
1388 | ||||||
1389 - 1414 | =head2 tmpdir Returns the name of a directory that you can use to create temporary files in. The routine is preferable to L<File::Spec/tmpdir> since CGI programs are often running on shared servers. Having said that, tmpdir will fall back to File::Spec->tmpdir() if it can't find somewhere better. If the parameter 'default' is given, then use that directory as a fall-back rather than the value in File::Spec->tmpdir(). No sanity tests are done, so if you give the default value of '/non-existant', that will be returned. Tmpdir allows a reference of the options to be passed. use CGI::Info; my $info = CGI::Info->new(); my $dir = $info->tmpdir(default => '/var/tmp'); $dir = $info->tmpdir({ default => '/var/tmp' }); # or my $dir = CGI::Info->tmpdir(); =cut | |||||
1415 | ||||||
1416 | sub tmpdir { | |||||
1417 | 23 | 1600 | my $self = shift; | |||
1418 | ||||||
1419 | 23 | 22 | my $name = 'tmp'; | |||
1420 | 23 | 34 | if($^O eq 'MSWin32') { | |||
1421 | 0 | 0 | $name = 'temp'; | |||
1422 | } | |||||
1423 | ||||||
1424 | 23 | 10 | my $dir; | |||
1425 | ||||||
1426 | 23 | 25 | if(!ref($self)) { | |||
1427 | 3 | 3 | $self = __PACKAGE__->new(); | |||
1428 | } | |||||
1429 | 23 | 39 | my $params = Params::Get::get_params(undef, @_); | |||
1430 | ||||||
1431 | 23 | 738 | if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) { | |||
1432 | 5 | 16 | $dir = File::Spec->catdir($ENV{'C_DOCUMENT_ROOT'}, $name); | |||
1433 | 5 | 32 | if((-d $dir) && (-w $dir)) { | |||
1434 | 2 | 3 | return $self->_untaint_filename({ filename => $dir }); | |||
1435 | } | |||||
1436 | 3 | 2 | $dir = $ENV{'C_DOCUMENT_ROOT'}; | |||
1437 | 3 | 24 | if((-d $dir) && (-w $dir)) { | |||
1438 | 3 | 8 | return $self->_untaint_filename({ filename => $dir }); | |||
1439 | } | |||||
1440 | } | |||||
1441 | 18 | 35 | if($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) { | |||
1442 | 1 | 7 | $dir = File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, File::Spec->updir(), $name); | |||
1443 | 1 | 3 | if((-d $dir) && (-w $dir)) { | |||
1444 | 0 | 0 | return $self->_untaint_filename({ filename => $dir }); | |||
1445 | } | |||||
1446 | } | |||||
1447 | 18 | 216 | return $params->{default} ? $params->{default} : File::Spec->tmpdir(); | |||
1448 | } | |||||
1449 | ||||||
1450 - 1462 | =head2 rootdir Returns the document root. This is preferable to looking at DOCUMENT_ROOT in the environment because it will also work when we're not running as a CGI script, which is useful for script debugging. This can be run as a class or object method. use CGI::Info; print CGI::Info->rootdir(); =cut | |||||
1463 | ||||||
1464 | sub rootdir { | |||||
1465 | 14 | 955 | if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) { | |||
1466 | 1 | 2 | return $ENV{'C_DOCUMENT_ROOT'}; | |||
1467 | } elsif($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) { | |||||
1468 | 2 | 4 | return $ENV{'DOCUMENT_ROOT'}; | |||
1469 | } | |||||
1470 | 11 | 11 | my $script_name = $0; | |||
1471 | ||||||
1472 | 11 | 25 | unless(File::Spec->file_name_is_absolute($script_name)) { | |||
1473 | 11 | 69 | $script_name = File::Spec->rel2abs($script_name); | |||
1474 | } | |||||
1475 | 11 | 13 | if($script_name =~ /.cgi\-bin.*/) { # kludge for outside CGI environment | |||
1476 | 0 | 0 | $script_name =~ s/.cgi\-bin.*//; | |||
1477 | } | |||||
1478 | 11 | 50 | if(-f $script_name) { # More kludge | |||
1479 | 11 | 14 | if($^O eq 'MSWin32') { | |||
1480 | 0 | 0 | if($script_name =~ /(.+)\\.+?$/) { | |||
1481 | 0 | 0 | return $1; | |||
1482 | } | |||||
1483 | } else { | |||||
1484 | 11 | 33 | if($script_name =~ /(.+)\/.+?$/) { | |||
1485 | 11 | 19 | return $1; | |||
1486 | } | |||||
1487 | } | |||||
1488 | } | |||||
1489 | 0 | 0 | return $script_name; | |||
1490 | } | |||||
1491 | ||||||
1492 - 1496 | =head2 root_dir Synonym of rootdir(), for compatibility with L<CHI>. =cut | |||||
1497 | ||||||
1498 | sub root_dir | |||||
1499 | { | |||||
1500 | 4 | 469 | if($_[0] && ref($_[0])) { | |||
1501 | 2 | 2 | my $self = shift; | |||
1502 | ||||||
1503 | 2 | 4 | return $self->rootdir(@_); | |||
1504 | } | |||||
1505 | 2 | 3 | return __PACKAGE__->rootdir(@_); | |||
1506 | } | |||||
1507 | ||||||
1508 - 1512 | =head2 documentroot Synonym of rootdir(), for compatibility with Apache. =cut | |||||
1513 | ||||||
1514 | sub documentroot | |||||
1515 | { | |||||
1516 | 3 | 10 | if($_[0] && ref($_[0])) { | |||
1517 | 1 | 1 | my $self = shift; | |||
1518 | ||||||
1519 | 1 | 1 | return $self->rootdir(@_); | |||
1520 | } | |||||
1521 | 2 | 2 | return __PACKAGE__->rootdir(@_); | |||
1522 | } | |||||
1523 | ||||||
1524 - 1528 | =head2 logdir Gets and sets the name of a directory that you can use to store logs in. =cut | |||||
1529 | ||||||
1530 | sub logdir { | |||||
1531 | 5 | 1337 | my $self = shift; | |||
1532 | 5 | 6 | my $dir = shift; | |||
1533 | ||||||
1534 | 5 | 8 | if(!ref($self)) { | |||
1535 | 1 | 2 | $self = __PACKAGE__->new(); | |||
1536 | } | |||||
1537 | ||||||
1538 | 5 | 7 | if($dir) { | |||
1539 | 2 | 24 | if(length($dir) && (-d $dir) && (-w $dir)) { | |||
1540 | 1 | 4 | return $self->{'logdir'} = $dir; | |||
1541 | } | |||||
1542 | 1 | 3 | $self->_warn("Invalid logdir: $dir"); | |||
1543 | 1 | 16 | Carp::croak("Invalid logdir: $dir"); | |||
1544 | } | |||||
1545 | ||||||
1546 | 3 | 13 | foreach my $rc($self->{logdir}, $ENV{'LOGDIR'}, Sys::Path->logdir(), $self->tmpdir()) { | |||
1547 | 9 | 38 | if(defined($rc) && length($rc) && (-d $rc) && (-w $rc)) { | |||
1548 | 3 | 3 | $dir = $rc; | |||
1549 | 3 | 2 | last; | |||
1550 | } | |||||
1551 | } | |||||
1552 | 3 | 9 | $self->_warn("Can't determine logdir") if((!defined($dir)) || (length($dir) == 0)); | |||
1553 | 3 | 5 | $self->{logdir} ||= $dir; | |||
1554 | ||||||
1555 | 3 | 8 | return $dir; | |||
1556 | } | |||||
1557 | ||||||
1558 - 1573 | =head2 is_robot Is the visitor a real person or a robot? use CGI::Info; my $info = CGI::Info->new(); unless($info->is_robot()) { # update site visitor statistics } If the client is seen to be attempting an SQL injection, set the HTTP status to 403, and return 1. =cut | |||||
1574 | ||||||
1575 | sub is_robot { | |||||
1576 | 21 | 371 | my $self = shift; | |||
1577 | ||||||
1578 | 21 | 25 | if(defined($self->{is_robot})) { | |||
1579 | 3 | 3 | return $self->{is_robot}; | |||
1580 | } | |||||
1581 | ||||||
1582 | 18 | 27 | my $agent = $ENV{'HTTP_USER_AGENT'}; | |||
1583 | 18 | 14 | my $remote = $ENV{'REMOTE_ADDR'}; | |||
1584 | ||||||
1585 | 18 | 26 | unless($remote && $agent) { | |||
1586 | # Probably not running in CGI - assume real person | |||||
1587 | 8 | 11 | return 0; | |||
1588 | } | |||||
1589 | ||||||
1590 | # See also params() | |||||
1591 | 10 | 74 | if(($agent =~ /SELECT.+AND.+/) || ($agent =~ /ORDER BY /) || ($agent =~ / OR NOT /) || ($agent =~ / AND \d+=\d+/) || ($agent =~ /THEN.+ELSE.+END/) || ($agent =~ /.+AND.+SELECT.+/) || ($agent =~ /\sAND\s.+\sAND\s/)) { | |||
1592 | 1 | 2 | $self->status(403); | |||
1593 | 1 | 1 | $self->{is_robot} = 1; | |||
1594 | 1 | 2 | if($ENV{'REMOTE_ADDR'}) { | |||
1595 | 1 | 3 | $self->_warn($ENV{'REMOTE_ADDR'} . ": SQL injection attempt blocked for '$agent'"); | |||
1596 | } else { | |||||
1597 | 0 | 0 | $self->_warn("SQL injection attempt blocked for '$agent'"); | |||
1598 | } | |||||
1599 | 1 | 1 | return 1; | |||
1600 | } | |||||
1601 | 9 | 249 | if($agent =~ /.+bot|axios\/1\.6\.7|bidswitchbot|bytespider|ClaudeBot|Clickagy.Intelligence.Bot|msnptc|CriteoBot|is_archiver|backstreet|fuzz faster|linkfluence\.com|spider|scoutjet|gingersoftware|heritrix|dodnetdotcom|yandex|nutch|ezooms|plukkie|nova\.6scan\.com|Twitterbot|adscanner|Go-http-client|python-requests|Mediatoolkitbot|NetcraftSurveyAgent|Expanse|serpstatbot|DreamHost SiteMonitor|techiaith.cymru|trendictionbot|ias_crawler|WPsec|Yak\/1\.0|ZoominfoBot/i) { | |||
1602 | 3 | 3 | $self->{is_robot} = 1; | |||
1603 | 3 | 6 | return 1; | |||
1604 | } | |||||
1605 | ||||||
1606 | # TODO: | |||||
1607 | # Download and use list from | |||||
1608 | # https://raw.githubusercontent.com/mitchellkrogza/apache-ultimate-bad-bot-blocker/refs/heads/master/_generator_lists/bad-user-agents.list | |||||
1609 | ||||||
1610 | 6 | 6 | my $key = "$remote/$agent"; | |||
1611 | ||||||
1612 | 6 | 10 | if(my $referrer = $ENV{'HTTP_REFERER'}) { | |||
1613 | # https://agency.ohow.co/google-analytics-implementation-audit/google-analytics-historical-spam-list/ | |||||
1614 | 2 | 5 | my @crawler_lists = ( | |||
1615 | 'http://fix-website-errors.com', | |||||
1616 | 'http://keywords-monitoring-your-success.com', | |||||
1617 | 'http://free-video-tool.com', | |||||
1618 | 'http://magnet-to-torrent.com', | |||||
1619 | 'http://torrent-to-magnet.com', | |||||
1620 | 'http://dogsrun.net', | |||||
1621 | 'http://###.responsive-test.net', | |||||
1622 | 'http://uptime.com', | |||||
1623 | 'http://uptimechecker.com', | |||||
1624 | 'http://top1-seo-service.com', | |||||
1625 | 'http://fast-wordpress-start.com', | |||||
1626 | 'http://wordpress-crew.net', | |||||
1627 | 'http://dbutton.net', | |||||
1628 | 'http://justprofit.xyz', | |||||
1629 | 'http://video--production.com', | |||||
1630 | 'http://buttons-for-website.com', | |||||
1631 | 'http://buttons-for-your-website.com', | |||||
1632 | 'http://success-seo.com', | |||||
1633 | 'http://videos-for-your-business.com', | |||||
1634 | 'http://semaltmedia.com', | |||||
1635 | 'http://dailyrank.net', | |||||
1636 | 'http://uptimebot.net', | |||||
1637 | 'http://sitevaluation.org', | |||||
1638 | 'http://100dollars-seo.com', | |||||
1639 | 'http://forum69.info', | |||||
1640 | 'http://partner.semalt.com', | |||||
1641 | 'http://best-seo-offer.com', | |||||
1642 | 'http://best-seo-solution.com', | |||||
1643 | 'http://semalt.semalt.com', | |||||
1644 | 'http://semalt.com', | |||||
1645 | 'http://7makemoneyonline.com', | |||||
1646 | 'http://anticrawler.org', | |||||
1647 | 'http://baixar-musicas-gratis.com', | |||||
1648 | 'http://descargar-musica-gratis.net', | |||||
1649 | ||||||
1650 | # Mine | |||||
1651 | 'http://www.seokicks.de/robot.html', | |||||
1652 | ); | |||||
1653 | 2 | 2 | $referrer =~ s/\\/_/g; | |||
1654 | 2 3 | 6 12 | if(($referrer =~ /\)/) || (List::Util::any { $_ =~ /^$referrer/ } @crawler_lists)) { | |||
1655 | 2 | 3 | $self->_debug("is_robot: blocked trawler $referrer"); | |||
1656 | ||||||
1657 | 2 | 6 | if($self->{cache}) { | |||
1658 | 0 | 0 | $self->{cache}->set($key, 'robot', '1 day'); | |||
1659 | } | |||||
1660 | 2 | 1 | $self->{is_robot} = 1; | |||
1661 | 2 | 7 | return 1; | |||
1662 | } | |||||
1663 | } | |||||
1664 | ||||||
1665 | 4 | 9 | if(defined($remote) && $self->{cache}) { | |||
1666 | 0 | 0 | if(my $type = $self->{cache}->get("$remote/$agent")) { | |||
1667 | 0 | 0 | return $self->{is_robot} = ($type eq 'robot'); | |||
1668 | } | |||||
1669 | } | |||||
1670 | ||||||
1671 | # Don't use HTTP_USER_AGENT to detect more than we really have to since | |||||
1672 | # that is easily spoofed | |||||
1673 | 4 | 12 | if($agent =~ /www\.majestic12\.co\.uk|facebookexternal/) { | |||
1674 | # Mark Facebook as a search engine, not a robot | |||||
1675 | 0 | 0 | if($self->{cache}) { | |||
1676 | 0 | 0 | $self->{cache}->set($key, 'search', '1 day'); | |||
1677 | } | |||||
1678 | 0 | 0 | return 0; | |||
1679 | } | |||||
1680 | ||||||
1681 | 4 | 4 | unless($self->{browser_detect}) { | |||
1682 | 3 3 | 1 10 | if(eval { require HTTP::BrowserDetect; }) { | |||
1683 | 3 | 6 | HTTP::BrowserDetect->import(); | |||
1684 | 3 | 6 | $self->{browser_detect} = HTTP::BrowserDetect->new($agent); | |||
1685 | } | |||||
1686 | } | |||||
1687 | 4 | 288 | if($self->{browser_detect}) { | |||
1688 | 4 | 4 | my $is_robot = $self->{browser_detect}->robot(); | |||
1689 | 4 | 371 | if(defined($is_robot)) { | |||
1690 | 2 | 5 | $self->_debug("HTTP::BrowserDetect '$ENV{HTTP_USER_AGENT}' returns $is_robot"); | |||
1691 | } | |||||
1692 | 4 | 49 | $is_robot = (defined($is_robot) && ($is_robot)) ? 1 : 0; | |||
1693 | 4 | 7 | $self->_debug("is_robot: $is_robot"); | |||
1694 | ||||||
1695 | 4 | 51 | if($is_robot) { | |||
1696 | 2 | 2 | if($self->{cache}) { | |||
1697 | 0 | 0 | $self->{cache}->set($key, 'robot', '1 day'); | |||
1698 | } | |||||
1699 | 2 | 4 | $self->{is_robot} = $is_robot; | |||
1700 | 2 | 4 | return $is_robot; | |||
1701 | } | |||||
1702 | } | |||||
1703 | ||||||
1704 | 2 | 3 | if($self->{cache}) { | |||
1705 | 0 | 0 | $self->{cache}->set($key, 'unknown', '1 day'); | |||
1706 | } | |||||
1707 | 2 | 3 | $self->{is_robot} = 0; | |||
1708 | 2 | 9 | return 0; | |||
1709 | } | |||||
1710 | ||||||
1711 - 1723 | =head2 is_search_engine Is the visitor a search engine? if(CGI::Info->new()->is_search_engine()) { # display generic information about yourself } else { # allow the user to pick and choose something to display } Can be overriden by the IS_SEARCH_ENGINE environment setting =cut | |||||
1724 | ||||||
1725 | sub is_search_engine | |||||
1726 | { | |||||
1727 | 28 | 550 | my $self = shift; | |||
1728 | ||||||
1729 | 28 | 35 | if(defined($self->{is_search_engine})) { | |||
1730 | 6 | 7 | return $self->{is_search_engine}; | |||
1731 | } | |||||
1732 | ||||||
1733 | 22 | 27 | if($ENV{'IS_SEARCH_ENGINE'}) { | |||
1734 | 1 | 3 | return $ENV{'IS_SEARCH_ENGINE'} | |||
1735 | } | |||||
1736 | ||||||
1737 | 21 | 18 | my $remote = $ENV{'REMOTE_ADDR'}; | |||
1738 | 21 | 20 | my $agent = $ENV{'HTTP_USER_AGENT'}; | |||
1739 | ||||||
1740 | 21 | 32 | unless($remote && $agent) { | |||
1741 | # Probably not running in CGI - assume not a search engine | |||||
1742 | 9 | 12 | return 0; | |||
1743 | } | |||||
1744 | ||||||
1745 | 12 | 8 | my $key; | |||
1746 | ||||||
1747 | 12 | 10 | if($self->{cache}) { | |||
1748 | 0 | 0 | $key = "$remote/$agent"; | |||
1749 | 0 | 0 | if(defined($remote) && $self->{cache}) { | |||
1750 | 0 | 0 | if(my $type = $self->{cache}->get("$remote/$agent")) { | |||
1751 | 0 | 0 | return $self->{is_search} = ($type eq 'search'); | |||
1752 | } | |||||
1753 | } | |||||
1754 | } | |||||
1755 | ||||||
1756 | # Don't use HTTP_USER_AGENT to detect more than we really have to since | |||||
1757 | # that is easily spoofed | |||||
1758 | 12 | 39 | if($agent =~ /www\.majestic12\.co\.uk|facebookexternal/) { | |||
1759 | # Mark Facebook as a search engine, not a robot | |||||
1760 | 0 | 0 | if($self->{cache}) { | |||
1761 | 0 | 0 | $self->{cache}->set($key, 'search', '1 day'); | |||
1762 | } | |||||
1763 | 0 | 0 | return 1; | |||
1764 | } | |||||
1765 | ||||||
1766 | 12 | 14 | unless($self->{browser_detect}) { | |||
1767 | 8 8 | 5 425 | if(eval { require HTTP::BrowserDetect; }) { | |||
1768 | 8 | 7821 | HTTP::BrowserDetect->import(); | |||
1769 | 8 | 12 | $self->{browser_detect} = HTTP::BrowserDetect->new($agent); | |||
1770 | } | |||||
1771 | } | |||||
1772 | 12 | 602 | if(my $browser = $self->{browser_detect}) { | |||
1773 | 12 | 13 | my $is_search = ($browser->google() || $browser->msn() || $browser->baidu() || $browser->altavista() || $browser->yahoo() || $browser->bingbot()); | |||
1774 | 12 | 1828 | if(!$is_search) { | |||
1775 | 6 | 17 | if(($agent =~ /SeznamBot\//) || | |||
1776 | ($agent =~ /Google-InspectionTool\//) || | |||||
1777 | ($agent =~ /Googlebot\//)) { | |||||
1778 | 1 | 1 | $is_search = 1; | |||
1779 | } | |||||
1780 | } | |||||
1781 | 12 | 20 | if($is_search && $self->{cache}) { | |||
1782 | 0 | 0 | $self->{cache}->set($key, 'search', '1 day'); | |||
1783 | } | |||||
1784 | 12 | 29 | return $self->{is_search_engine} = $is_search; | |||
1785 | } | |||||
1786 | ||||||
1787 | # TODO: DNS lookup, not gethostbyaddr - though that will be slow | |||||
1788 | 0 | 0 | my $hostname = gethostbyaddr(inet_aton($remote), AF_INET) || $remote; | |||
1789 | ||||||
1790 | 0 | 0 | my @cidr_blocks = ('47.235.0.0/12'); # Alibaba | |||
1791 | ||||||
1792 | 0 | 0 | if((defined($hostname) && ($hostname =~ /google|msnbot|bingbot|amazonbot|GPTBot/) && ($hostname !~ /^google-proxy/)) || | |||
1793 | (Net::CIDR::cidrlookup($remote, @cidr_blocks))) { | |||||
1794 | 0 | 0 | if($self->{cache}) { | |||
1795 | 0 | 0 | $self->{cache}->set($key, 'search', '1 day'); | |||
1796 | } | |||||
1797 | 0 | 0 | $self->{is_search_engine} = 1; | |||
1798 | 0 | 0 | return 1; | |||
1799 | } | |||||
1800 | ||||||
1801 | 0 | 0 | $self->{is_search_engine} = 0; | |||
1802 | 0 | 0 | return 0; | |||
1803 | } | |||||
1804 | ||||||
1805 - 1827 | =head2 browser_type Returns one of 'web', 'search', 'robot' and 'mobile'. # Code to display a different web page for a browser, search engine and # smartphone use Template; use CGI::Info; my $info = CGI::Info->new(); my $dir = $info->rootdir() . '/templates/' . $info->browser_type(); my $filename = ref($self); $filename =~ s/::/\//g; $filename = "$dir/$filename.tmpl"; if((!-f $filename) || (!-r $filename)) { die "Can't open $filename"; } my $template = Template->new(); $template->process($filename, {}) || die $template->error(); =cut | |||||
1828 | ||||||
1829 | sub browser_type { | |||||
1830 | 21 | 21 | my $self = shift; | |||
1831 | ||||||
1832 | 21 | 26 | if($self->is_mobile()) { | |||
1833 | 8 | 24 | return 'mobile'; | |||
1834 | } | |||||
1835 | 13 | 19 | if($self->is_search_engine()) { | |||
1836 | 6 | 15 | return 'search'; | |||
1837 | } | |||||
1838 | 7 | 11 | if($self->is_robot()) { | |||
1839 | 3 | 7 | return 'robot'; | |||
1840 | } | |||||
1841 | 4 | 8 | return 'web'; | |||
1842 | } | |||||
1843 | ||||||
1844 - 1859 | =head2 get_cookie Returns a cookie's value, or undef if no name is given, or the requested cookie isn't in the jar. Deprecated - use cookie() instead. use CGI::Info; my $i = CGI::Info->new(); my $name = $i->get_cookie(cookie_name => 'name'); print "Your name is $name\n"; my $address = $i->get_cookie('address'); print "Your address is $address\n"; =cut | |||||
1860 | ||||||
1861 | sub get_cookie { | |||||
1862 | 13 | 336 | my $self = shift; | |||
1863 | 13 | 16 | my $params = Params::Get::get_params('cookie_name', @_); | |||
1864 | ||||||
1865 | # Validate field argument | |||||
1866 | 12 | 130 | if(!defined($params->{'cookie_name'})) { | |||
1867 | 2 | 4 | $self->_warn('cookie_name argument not given'); | |||
1868 | 2 | 4 | return; | |||
1869 | } | |||||
1870 | ||||||
1871 | # Load cookies if not already loaded | |||||
1872 | 10 | 11 | unless($self->{jar}) { | |||
1873 | 4 | 4 | if(defined $ENV{'HTTP_COOKIE'}) { | |||
1874 | 3 11 | 6 14 | $self->{jar} = { map { split(/=/, $_, 2) } split(/; /, $ENV{'HTTP_COOKIE'}) }; | |||
1875 | } | |||||
1876 | } | |||||
1877 | ||||||
1878 | # Return the cookie value if it exists, otherwise return undef | |||||
1879 | 10 | 24 | return $self->{jar}->{$params->{'cookie_name'}}; | |||
1880 | } | |||||
1881 | ||||||
1882 - 1894 | =head2 cookie Returns a cookie's value, or undef if no name is given, or the requested cookie isn't in the jar. API is the same as "param", it will replace the "get_cookie" method in the future. use CGI::Info; my $name = CGI::Info->new()->cookie('name'); print "Your name is $name\n"; =cut | |||||
1895 | ||||||
1896 | sub cookie { | |||||
1897 | 9 | 972 | my ($self, $field) = @_; | |||
1898 | ||||||
1899 | # Validate field argument | |||||
1900 | 9 | 10 | if(!defined($field)) { | |||
1901 | 1 | 2 | $self->_warn('what cookie do you want?'); | |||
1902 | 1 | 2 | return; | |||
1903 | } | |||||
1904 | ||||||
1905 | # Load cookies if not already loaded | |||||
1906 | 8 | 8 | unless($self->{jar}) { | |||
1907 | 4 | 4 | if(defined $ENV{'HTTP_COOKIE'}) { | |||
1908 | 4 6 | 6 9 | $self->{jar} = { map { split(/=/, $_, 2) } split(/; /, $ENV{'HTTP_COOKIE'}) }; | |||
1909 | } | |||||
1910 | } | |||||
1911 | ||||||
1912 | # Return the cookie value if it exists, otherwise return undef | |||||
1913 | 8 | 18 | return $self->{jar}{$field}; | |||
1914 | } | |||||
1915 | ||||||
1916 - 1922 | =head2 status Sets or returns the status of the object, 200 for OK, otherwise an HTTP error code =cut | |||||
1923 | ||||||
1924 | sub status | |||||
1925 | { | |||||
1926 | 91 | 3754 | my $self = shift; | |||
1927 | 91 | 62 | my $status = shift; | |||
1928 | ||||||
1929 | # Set status if provided | |||||
1930 | 91 | 113 | return $self->{status} = $status if(defined($status)); | |||
1931 | ||||||
1932 | # Determine status based on request method if status is not set | |||||
1933 | 32 | 44 | unless (defined $self->{status}) { | |||
1934 | 13 | 15 | my $method = $ENV{'REQUEST_METHOD'}; | |||
1935 | ||||||
1936 | 13 | 29 | return 405 if $method && ($method eq 'OPTIONS' || $method eq 'DELETE'); | |||
1937 | 9 | 22 | return 411 if $method && ($method eq 'POST' && !defined $ENV{'CONTENT_LENGTH'}); | |||
1938 | ||||||
1939 | 7 | 22 | return 200; | |||
1940 | } | |||||
1941 | ||||||
1942 | # Return current status or 200 by default | |||||
1943 | 19 | 50 | return $self->{status} || 200; | |||
1944 | } | |||||
1945 | ||||||
1946 - 1958 | =head2 messages Returns the messages that the object has generated as a ref to an array of hashes. my @messages; if(my $w = $info->messages()) { @messages = map { $_->{'message'} } @{$w}; } else { @messages = (); } print STDERR join(';', @messages), "\n"; =cut | |||||
1959 | ||||||
1960 | sub messages | |||||
1961 | { | |||||
1962 | 7 | 2857 | my $self = shift; | |||
1963 | ||||||
1964 | 7 | 19 | return $self->{'messages'}; | |||
1965 | } | |||||
1966 | ||||||
1967 - 1971 | =head2 messages_as_string Returns the messages of that the object has generated as a string. =cut | |||||
1972 | ||||||
1973 | sub messages_as_string | |||||
1974 | { | |||||
1975 | 2 | 2 | my $self = shift; | |||
1976 | ||||||
1977 | 2 | 4 | if(scalar($self->{'messages'})) { | |||
1978 | 1 2 1 | 1 2 2 | my @messages = map { $_->{'message'} } @{$self->{'messages'}}; | |||
1979 | 1 | 4 | return join('; ', @messages); | |||
1980 | } | |||||
1981 | 1 | 2 | return ''; | |||
1982 | } | |||||
1983 | ||||||
1984 - 1993 | =head2 cache Get/set the internal cache system. Use this rather than pass the cache argument to C<new()> if you see these error messages, "(in cleanup) Failed to get MD5_CTX pointer". It's some obscure problem that I can't work out, but calling this after C<new()> works. =cut | |||||
1994 | ||||||
1995 | sub cache | |||||
1996 | { | |||||
1997 | 4 | 22 | my $self = shift; | |||
1998 | 4 | 2 | my $cache = shift; | |||
1999 | ||||||
2000 | 4 | 6 | if($cache) { | |||
2001 | 0 | 0 | $self->{'cache'} = $cache; | |||
2002 | } | |||||
2003 | 4 | 5 | return $self->{'cache'}; | |||
2004 | } | |||||
2005 | ||||||
2006 - 2013 | =head2 set_logger Sets the class, array, code reference, or file that will be used for logging. Sometimes you don't know what the logger is until you've instantiated the class. This function fixes the catch-22 situation. =cut | |||||
2014 | ||||||
2015 | sub set_logger | |||||
2016 | { | |||||
2017 | 6 | 25 | my $self = shift; | |||
2018 | 6 | 12 | my $params = Params::Get::get_params('logger', @_); | |||
2019 | ||||||
2020 | 6 | 73 | if(my $logger = $params->{'logger'}) { | |||
2021 | 6 | 10 | if(Scalar::Util::blessed($logger)) { | |||
2022 | 4 | 4 | $self->{'logger'} = $logger; | |||
2023 | } else { | |||||
2024 | 2 | 4 | $self->{'logger'} = Log::Abstraction->new($logger); | |||
2025 | } | |||||
2026 | } else { | |||||
2027 | 0 | 0 | $self->{'logger'} = Log::Abstraction->new(); | |||
2028 | } | |||||
2029 | 6 | 42 | return $self; | |||
2030 | } | |||||
2031 | ||||||
2032 | # Log and remember a message | |||||
2033 | sub _log | |||||
2034 | { | |||||
2035 | 413 | 408 | my ($self, $level, @messages) = @_; | |||
2036 | ||||||
2037 | # FIXME: add caller's function | |||||
2038 | # if(($level eq 'warn') || ($level eq 'info')) { | |||||
2039 | 413 413 | 219 891 | push @{$self->{'messages'}}, { level => $level, message => join(' ', grep defined, @messages) }; | |||
2040 | # } | |||||
2041 | ||||||
2042 | 413 | 729 | if(scalar(@messages) && (my $logger = $self->{'logger'})) { | |||
2043 | 413 | 751 | $self->{'logger'}->$level(join('', grep defined, @messages)); | |||
2044 | } | |||||
2045 | } | |||||
2046 | ||||||
2047 | sub _debug { | |||||
2048 | 132 | 90 | my $self = shift; | |||
2049 | 132 | 106 | $self->_log('debug', @_); | |||
2050 | } | |||||
2051 | ||||||
2052 | sub _info { | |||||
2053 | 35 | 26 | my $self = shift; | |||
2054 | 35 | 31 | $self->_log('info', @_); | |||
2055 | } | |||||
2056 | ||||||
2057 | sub _notice { | |||||
2058 | 0 | 0 | my $self = shift; | |||
2059 | 0 | 0 | $self->_log('notice', @_); | |||
2060 | } | |||||
2061 | ||||||
2062 | sub _trace { | |||||
2063 | 199 | 115 | my $self = shift; | |||
2064 | 199 | 235 | $self->_log('trace', @_); | |||
2065 | } | |||||
2066 | ||||||
2067 | # Emit a warning message somewhere | |||||
2068 | sub _warn { | |||||
2069 | 47 | 40 | my $self = shift; | |||
2070 | 47 | 70 | my $params = Params::Get::get_params('warning', @_); | |||
2071 | ||||||
2072 | 47 | 527 | $self->_log('warn', $params->{'warning'}); | |||
2073 | 32 | 2927 | if(!defined($self->{'logger'})) { | |||
2074 | 0 | 0 | Carp::carp($params->{'warning'}); | |||
2075 | } | |||||
2076 | } | |||||
2077 | ||||||
2078 | # Ensure all environment variables are sanitized and validated before use. | |||||
2079 | # Use regular expressions to enforce strict input formats. | |||||
2080 | sub _get_env | |||||
2081 | { | |||||
2082 | 111 | 79 | my ($self, $var) = @_; | |||
2083 | ||||||
2084 | 111 | 197 | return unless defined $ENV{$var}; | |||
2085 | ||||||
2086 | # Strict sanitization: allow alphanumeric and limited special characters | |||||
2087 | 63 | 106 | if($ENV{$var} =~ /^[\w\.\-\/:\\]+$/) { | |||
2088 | 63 | 79 | return $ENV{$var}; | |||
2089 | } | |||||
2090 | 0 | 0 | $self->_warn("Invalid value in environment variable: $var"); | |||
2091 | ||||||
2092 | 0 | 0 | return undef; | |||
2093 | } | |||||
2094 | ||||||
2095 - 2101 | =head2 reset Class method to reset the class. You should do this in an FCGI environment before instantiating, but nowhere else. =cut | |||||
2102 | ||||||
2103 | sub reset { | |||||
2104 | 13 | 7667 | my $class = shift; | |||
2105 | ||||||
2106 | 13 | 17 | unless($class eq __PACKAGE__) { | |||
2107 | 1 | 10 | carp('Reset is a class method'); | |||
2108 | 0 | 0 | return; | |||
2109 | } | |||||
2110 | ||||||
2111 | 12 | 12 | $stdin_data = undef; | |||
2112 | } | |||||
2113 | ||||||
2114 | sub AUTOLOAD | |||||
2115 | { | |||||
2116 | 241 | 50223 | our $AUTOLOAD; | |||
2117 | ||||||
2118 | 241 | 297 | my $self = shift or return; | |||
2119 | ||||||
2120 | # Extract the method name from the AUTOLOAD variable | |||||
2121 | 241 | 745 | my ($method) = $AUTOLOAD =~ /::(\w+)$/; | |||
2122 | ||||||
2123 | # Skip if called on destruction | |||||
2124 | 241 | 603 | return if($method eq 'DESTROY'); | |||
2125 | ||||||
2126 | 8 | 14 | Carp::croak(__PACKAGE__, ": Unknown method $method") if(!ref($self)); | |||
2127 | ||||||
2128 | # Allow the AUTOLOAD feature to be disabled | |||||
2129 | 8 | 513 | Carp::croak(__PACKAGE__, ": Unknown method $method") if(exists($self->{'auto_load'}) && boolean($self->{'auto_load'})->isFalse()); | |||
2130 | ||||||
2131 | # Ensure the method is called on the correct package object or a subclass | |||||
2132 | 7 | 15 | return unless((ref($self) eq __PACKAGE__) || (UNIVERSAL::isa((caller)[0], __PACKAGE__))); | |||
2133 | ||||||
2134 | # Validate method name - only allow safe parameter names | |||||
2135 | 7 | 18 | Carp::croak(__PACKAGE__, ": Invalid method name: $method") unless $method =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/; | |||
2136 | ||||||
2137 | # Delegate to the param method | |||||
2138 | 7 | 12 | return $self->param($method); | |||
2139 | } | |||||
2140 | ||||||
2141 - 2227 | =head1 AUTHOR Nigel Horne, C<< <njh at nigelhorne.com> >> =head1 BUGS is_tablet() only currently detects the iPad and Windows PCs. Android strings don't differ between tablets and smartphones. params() returns a ref which means that calling routines can change the hash for other routines. Take a local copy before making amendments to the table if you don't want unexpected things to happen. =head1 SEE ALSO =over 4 =item * Test coverage report: L<https://nigelhorne.github.io/CGI-Info/coverage/> =item * L<Object::Configure> =item * L<HTTP::BrowserDetect> =item * L<https://github.com/mitchellkrogza/apache-ultimate-bad-bot-blocker> =back =head1 REPOSITORY L<https://github.com/nigelhorne/CGI-Info> =head1 SUPPORT This module is provided as-is without any warranty. Please report any bugs or feature requests to C<bug-cgi-info at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CGI-Info>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. You can find documentation for this module with the perldoc command. perldoc CGI::Info You can also look for information at: =over 4 =item * MetaCPAN L<https://metacpan.org/dist/CGI-Info> =item * RT: CPAN's request tracker L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=CGI-Info> =item * CPAN Testers' Matrix L<http://matrix.cpantesters.org/?dist=CGI-Info> =item * CPAN Testers Dependencies L<http://deps.cpantesters.org/?module=CGI::Info> =back =head1 LICENCE AND COPYRIGHT Copyright 2010-2025 Nigel Horne. Usage is subject to licence terms. The licence terms of this software are as follows: =over 4 =item * Personal single user, single computer use: GPL2 =item * All other users (including Commercial, Charity, Educational, Government) must apply in writing for a licence for use from Nigel Horne at the above e-mail. =back =cut | |||||
2228 | ||||||
2229 | 1; |