File Coverage

blib/lib/CGI/Info.pm
Criterion Covered Total %
statement 579 686 84.4
branch 358 474 75.5
condition 120 185 64.8
subroutine 44 44 100.0
pod 24 24 100.0
total 1125 1413 79.6


line stmt bran cond sub pod time code
1             package CGI::Info;
2              
3             # TODO: remove the expect argument
4              
5 22     22   3520074 use warnings;
  22         288  
  22         762  
6 22     22   133 use strict;
  22         41  
  22         496  
7 22     22   109 use Carp;
  22         57  
  22         1201  
8 22     22   155 use File::Spec;
  22         53  
  22         757  
9 22     22   13594 use Socket; # For AF_INET
  22         85337  
  22         9287  
10 22     22   512 use 5.008;
  22         89  
11 22     22   10639 use Log::Any qw($log);
  22         191890  
  22         121  
12             # use Cwd;
13             # use JSON::Parse;
14 22     22   62089 use List::MoreUtils; # Can go when expect goes
  22         320389  
  22         159  
15             # use Sub::Private;
16 22     22   34206 use Sys::Path;
  22         622632  
  22         876  
17              
18 22     22   10960 use namespace::clean;
  22         364833  
  22         180  
19              
20             sub _sanitise_input($);
21              
22             =head1 NAME
23              
24             CGI::Info - Information about the CGI environment
25              
26             =head1 VERSION
27              
28             Version 0.77
29              
30             =cut
31              
32             our $VERSION = '0.77';
33              
34             =head1 SYNOPSIS
35              
36             All too often Perl programs have information such as the script's name
37             hard-coded into their source.
38             Generally speaking, hard-coding is bad style since it can make programs
39             difficult to read and it reduces readability and portability.
40             CGI::Info attempts to remove that.
41              
42             Furthermore, to aid script debugging, CGI::Info attempts to do sensible
43             things when you're not running the program in a CGI environment.
44              
45             use CGI::Info;
46             my $info = CGI::Info->new();
47             # ...
48              
49             =head1 SUBROUTINES/METHODS
50              
51             =head2 new
52              
53             Creates a CGI::Info object.
54              
55             It takes four optional arguments allow, logger, expect and upload_dir,
56             which are documented in the params() method.
57              
58             Takes an optional parameter syslog, to log messages to
59             L.
60             It can be a boolean to enable/disable logging to syslog, or a reference
61             to a hash to be given to Sys::Syslog::setlogsock.
62              
63             Takes optional parameter logger, an object which is used for warnings
64              
65             Takes optional parameter cache, an object which is used to cache IP lookups.
66             This cache object is an object that understands get() and set() messages,
67             such as a L object.
68              
69             Takes optional parameter max_upload, which is the maximum file size you can upload
70             (-1 for no limit), the default is 512MB.
71              
72             =cut
73              
74             our $stdin_data; # Class variable storing STDIN in case the class
75             # is instantiated more than once
76              
77             sub new {
78 154     154 1 109505 my $class = $_[0];
79              
80 154         276 shift;
81 154 100       572 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  3         12  
82              
83 154 100 100     1097 if($args{expect} && (ref($args{expect}) ne 'ARRAY')) {
84 1         14 warn __PACKAGE__, ': expect must be a reference to an array';
85 1         71 return;
86             }
87              
88 153 100       628 if(!defined($class)) {
    100          
89             # Using CGI::Info->new(), not CGI::Info::new()
90             # carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
91             # return;
92              
93             # FIXME: this only works when no arguments are given
94 1         2 $class = __PACKAGE__;
95             } elsif(ref($class)) {
96             # clone the given object
97 4         13 return bless { %{$class}, %args }, ref($class);
  4         53  
98             }
99              
100 149         561 my %defaults = (
101             max_upload_size => 512 * 1024,
102             allow => undef,
103             expect => undef,
104             upload_dir => undef
105             );
106              
107 149         894 return bless { %defaults, %args }, $class;
108             }
109              
110             =head2 script_name
111              
112             Returns the name of the CGI script.
113             This is useful for POSTing, thus avoiding putting hardcoded paths into forms
114              
115             use CGI::Info;
116              
117             my $info = CGI::Info->new();
118             my $script_name = $info->script_name();
119             # ...
120             print "
\n";
121              
122             =cut
123              
124             sub script_name {
125 15     15 1 349 my $self = shift;
126              
127 15 100       39 unless($self->{script_name}) {
128 9         22 $self->_find_paths();
129             }
130 15         136 return $self->{script_name};
131             }
132              
133             sub _find_paths {
134 15     15   38 my $self = shift;
135              
136 15         84 require File::Basename;
137 15         460 File::Basename->import();
138              
139 15 100       54 if($ENV{'SCRIPT_NAME'}) {
140 11         333 $self->{script_name} = File::Basename::basename($ENV{'SCRIPT_NAME'});
141             } else {
142 4         212 $self->{script_name} = File::Basename::basename($0);
143             }
144             $self->{script_name} = $self->_untaint_filename({
145             filename => $self->{script_name}
146 15         84 });
147              
148 15 100 66     131 if($ENV{'SCRIPT_FILENAME'}) {
    100 66        
    100          
    50          
149 1         3 $self->{script_path} = $ENV{'SCRIPT_FILENAME'};
150             } elsif($ENV{'SCRIPT_NAME'} && $ENV{'DOCUMENT_ROOT'}) {
151 5         9 my $script_name = $ENV{'SCRIPT_NAME'};
152 5 100       17 if($script_name =~ /^\/(.+)/) {
153             # It's usually the case, e.g. /cgi-bin/foo.pl
154 3         7 $script_name = $1;
155             }
156 5         49 $self->{script_path} = File::Spec->catfile($ENV{'DOCUMENT_ROOT' }, $script_name);
157             } elsif($ENV{'SCRIPT_NAME'} && !$ENV{'DOCUMENT_ROOT'}) {
158 5 100 100     157 if(File::Spec->file_name_is_absolute($ENV{'SCRIPT_NAME'}) &&
159             (-r $ENV{'SCRIPT_NAME'})) {
160             # Called from a command line with a full path
161 1         6 $self->{script_path} = $ENV{'SCRIPT_NAME'};
162             } else {
163 4         25 require Cwd;
164 4         102 Cwd->import;
165              
166 4         11 my $script_name = $ENV{'SCRIPT_NAME'};
167 4 100       17 if($script_name =~ /^\/(.+)/) {
168             # It's usually the case, e.g. /cgi-bin/foo.pl
169 2         7 $script_name = $1;
170             }
171              
172 4         112 $self->{script_path} = File::Spec->catfile(Cwd::abs_path(), $script_name);
173             }
174             } elsif(File::Spec->file_name_is_absolute($0)) {
175             # Called from a command line with a full path
176 0         0 $self->{script_path} = $0;
177             } else {
178 4         163 $self->{script_path} = File::Spec->rel2abs($0);
179             }
180              
181             $self->{script_path} = $self->_untaint_filename({
182             filename => $self->{script_path}
183 15         80 });
184             }
185              
186             =head2 script_path
187              
188             Finds the full path name of the script.
189              
190             use CGI::Info;
191              
192             my $info = CGI::Info->new();
193             my $fullname = $info->script_path();
194             my @statb = stat($fullname);
195              
196             if(@statb) {
197             my $mtime = localtime $statb[9];
198             print "Last-Modified: $mtime\n";
199             # TODO: only for HTTP/1.1 connections
200             # $etag = Digest::MD5::md5_hex($html);
201             printf "ETag: \"%x\"\n", $statb[9];
202             }
203             =cut
204              
205             sub script_path {
206 22     22 1 12588 my $self = shift;
207              
208 22 100       72 unless($self->{script_path}) {
209 5         13 $self->_find_paths();
210             }
211 22         225 return $self->{script_path};
212             }
213              
214             =head2 script_dir
215              
216             Returns the file system directory containing the script.
217              
218             use CGI::Info;
219             use File::Spec;
220              
221             my $info = CGI::Info->new();
222              
223             print 'HTML files are normally stored in ', $info->script_dir(), '/', File::Spec->updir(), "\n";
224              
225             =cut
226              
227             sub script_dir {
228 11     11 1 27 my $self = shift;
229              
230 11 100       33 unless($self->{script_path}) {
231 1         3 $self->_find_paths();
232             }
233              
234             # Don't use File::Spec->splitpath() since that can leave the trailing slash
235 11 50       29 if($^O eq 'MSWin32') {
236 0 0       0 if($self->{script_path} =~ /(.+)\\.+?$/) {
237 0         0 return $1;
238             }
239             } else {
240 11 50       80 if($self->{script_path} =~ /(.+)\/.+?$/) {
241 11         136 return $1;
242             }
243             }
244 0         0 return $self->{script_path};
245             }
246              
247             =head2 host_name
248              
249             Return the host-name of the current web server, according to CGI.
250             If the name can't be determined from the web server, the system's host-name
251             is used as a fall back.
252             This may not be the same as the machine that the CGI script is running on,
253             some ISPs and other sites run scripts on different machines from those
254             delivering static content.
255             There is a good chance that this will be domain_name() prepended with either
256             'www' or 'cgi'.
257              
258             use CGI::Info;
259              
260             my $info = CGI::Info->new();
261             my $host_name = $info->host_name();
262             my $protocol = $info->protocol();
263             # ...
264             print "Thank you for visiting our Website!";
265              
266             =cut
267              
268             sub host_name {
269 8     8 1 685 my $self = shift;
270              
271 8 100       25 unless($self->{site}) {
272 2         5 $self->_find_site_details();
273             }
274              
275 8         43 return $self->{site};
276             }
277              
278             sub _find_site_details {
279 7     7   13 my $self = shift;
280              
281 7 100       18 if($self->{logger}) {
282 4         12 $self->{logger}->trace('Entering _find_site_details');
283             }
284 7 50 66     41 if($self->{site} && $self->{cgi_site}) {
285 2         3 return;
286             }
287              
288 5         499 require URI::Heuristic;
289 5         2108 URI::Heuristic->import;
290              
291 5 100       21 if($ENV{'HTTP_HOST'}) {
    100          
292 1         8 $self->{cgi_site} = URI::Heuristic::uf_uristr($ENV{'HTTP_HOST'});
293             # Remove trailing dots from the name. They are legal in URLs
294             # and some sites link using them to avoid spoofing (nice)
295 1 50       35 if($self->{cgi_site} =~ /(.*)\.+$/) {
296 1         3 $self->{cgi_site} = $1;
297             }
298             } elsif($ENV{'SERVER_NAME'}) {
299 3         16 $self->{cgi_site} = URI::Heuristic::uf_uristr($ENV{'SERVER_NAME'});
300 3 100 66     65 if(defined($self->protocol()) && ($self->protocol() ne 'http')) {
301 1         6 $self->{cgi_site} =~ s/^http//;
302 1         4 $self->{cgi_site} = $self->protocol() . $self->{cgi_site};
303             }
304             } else {
305 1         7 require Sys::Hostname;
306 1         26 Sys::Hostname->import;
307              
308 1 50       8 if($self->{logger}) {
309 1         7 $self->{logger}->debug('Falling back to using hostname');
310             }
311              
312 1         9 $self->{cgi_site} = Sys::Hostname::hostname();
313             }
314              
315 5 50       22 unless($self->{site}) {
316 5         11 $self->{site} = $self->{cgi_site};
317             }
318 5 100       22 if($self->{site} =~ /^https?:\/\/(.+)/) {
319 4         12 $self->{site} = $1;
320             }
321 5 100       19 unless($self->{cgi_site} =~ /^https?:\/\//) {
322 1         6 my $protocol = $self->protocol();
323              
324 1 50       7 unless($protocol) {
325 0         0 $protocol = 'http';
326             }
327 1         8 $self->{cgi_site} = "$protocol://" . $self->{cgi_site};
328             }
329 5 50 33     22 unless($self->{site} && $self->{cgi_site}) {
330 0         0 $self->_warn('Could not determine site name');
331             }
332 5 100       15 if($self->{logger}) {
333 3         13 $self->{logger}->trace('Leaving _find_site_details');
334             }
335             }
336              
337             =head2 domain_name
338              
339             Domain_name is the name of the controlling domain for this website.
340             Usually it will be similar to host_name, but will lack the http:// prefix.
341              
342             =cut
343              
344             sub domain_name {
345 5     5 1 14 my $self = shift;
346              
347 5 100       15 if($self->{domain}) {
348 3         14 return $self->{domain};
349             }
350 2         5 $self->_find_site_details();
351              
352 2 50       11 if($self->{site}) {
353 2         6 $self->{domain} = $self->{site};
354 2 100       11 if($self->{domain} =~ /^www\.(.+)/) {
355 1         4 $self->{domain} = $1;
356             }
357             }
358              
359 2         9 return $self->{domain};
360             }
361              
362             =head2 cgi_host_url
363              
364             Return the URL of the machine running the CGI script.
365              
366             =cut
367              
368             sub cgi_host_url {
369 7     7 1 36 my $self = shift;
370              
371 7 100       21 unless($self->{cgi_site}) {
372 3         7 $self->_find_site_details();
373             }
374              
375 7         60 return $self->{cgi_site};
376             }
377              
378             =head2 params
379              
380             Returns a reference to a hash list of the CGI arguments.
381              
382             CGI::Info helps you to test your script prior to deployment on a website:
383             if it is not in a CGI environment (e.g. the script is being tested from the
384             command line), the program's command line arguments (a list of key=value pairs)
385             are used, if there are no command line arguments then they are read from stdin
386             as a list of key=value lines. Also you can give one of --tablet, --search-engine,
387             --mobile and --robot to mimic those agents. For example:
388              
389             ./script.cgi --mobile name=Nigel
390              
391             Returns undef if the parameters can't be determined or if none were given.
392              
393             If an argument is given twice or more, then the values are put in a comma
394             separated string.
395              
396             The returned hash value can be passed into L.
397              
398             Takes four optional parameters: allow, expect, logger and upload_dir.
399             The parameters are passed in a hash, or a reference to a hash.
400             The latter is more efficient since it puts less on the stack.
401              
402             Allow is a reference to a hash list of CGI parameters that you will allow.
403             The value for each entry is a regular expression of permitted values for
404             the key.
405             A undef value means that any value will be allowed.
406             Arguments not in the list are silently ignored.
407             This is useful to help to block attacks on your site.
408              
409             Expect is a reference to a list of arguments that you expect to see and pass on.
410             Arguments not in the list are silently ignored.
411             This is useful to help to block attacks on your site.
412             Its use is deprecated, use allow instead.
413             Expect will be removed in a later version.
414              
415             Upload_dir is a string containing a directory where files being uploaded are to
416             be stored.
417              
418             Takes optional parameter logger, an object which is used for warnings and
419             traces.
420             This logger object is an object that understands warn() and trace() messages,
421             such as a L or L object.
422              
423             The allow, expect, logger and upload_dir arguments can also be passed to the
424             constructor.
425              
426             use CGI::Info;
427             use CGI::Untaint;
428             # ...
429             my $info = CGI::Info->new();
430             my %params;
431             if($info->params()) {
432             %params = %{$info->params()};
433             }
434             # ...
435             foreach(keys %params) {
436             print "$_ => $params{$_}\n";
437             }
438             my $u = CGI::Untaint->new(%params);
439              
440             use CGI::Info;
441             use CGI::IDS;
442             # ...
443             my $info = CGI::Info->new();
444             my $allowed = {
445             'foo' => qr(^\d*$), # foo must be a number, or empty
446             'bar' => undef,
447             'xyzzy' => qr(^[\w\s-]+$), # must be alphanumeric
448             # to prevent XSS, and non-empty
449             # as a sanity check
450             };
451             my $paramsref = $info->params(allow => $allowed);
452             # or
453             my @expected = ('foo', 'bar');
454             my $paramsref = $info->params({
455             expect => \@expected,
456             upload_dir = $info->tmpdir()
457             });
458             if(defined($paramsref)) {
459             my $ids = CGI::IDS->new();
460             $ids->set_scan_keys(scan_keys => 1);
461             if($ids->detect_attacks(request => $paramsref) > 0) {
462             die 'horribly';
463             }
464             }
465              
466             If the request is an XML request (i.e. the content type of the POST is text/xml),
467             CGI::Info will put the request into the params element 'XML', thus:
468              
469             use CGI::Info;
470             # ...
471             my $info = CGI::Info->new();
472             my $paramsref = $info->params(); # See BUGS below
473             my $xml = $$paramsref{'XML'};
474             # ... parse and process the XML request in $xml
475              
476             =cut
477              
478             sub params {
479 162     162 1 7054 my $self = shift;
480              
481 162 100       488 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  4         20  
482              
483 162 100 66     596 if((defined($self->{paramref})) && ((!defined($args{'allow'})) || defined($self->{allow}) && ($args{'allow'} eq $self->{allow}))) {
      66        
484 93         479 return $self->{paramref};
485             }
486              
487 69 100       187 if(defined($args{allow})) {
488 4         11 $self->{allow} = $args{allow};
489             }
490 69 100       173 if(defined($args{expect})) {
491 3 100       9 if(ref($args{expect}) eq 'ARRAY') {
492 2         4 $self->{expect} = $args{expect};
493             } else {
494 1         5 $self->_warn('expect must be a reference to an array');
495             }
496             }
497 69 100       783 if(defined($args{upload_dir})) {
498 1         4 $self->{upload_dir} = $args{upload_dir};
499             }
500 69 100       156 if(defined($args{logger})) {
501 1         3 $self->{logger} = $args{logger};
502             }
503 69 100       160 if($self->{logger}) {
504 2         11 $self->{logger}->trace('Entering params');
505             }
506              
507 69         114 my @pairs;
508 69         130 my $content_type = $ENV{'CONTENT_TYPE'};
509 69         113 my %FORM;
510              
511 69 100 66     503 if((!$ENV{'GATEWAY_INTERFACE'}) || (!$ENV{'REQUEST_METHOD'})) {
    100 100        
    100          
    50          
    100          
512 6 100       21 if(@ARGV) {
    50          
    50          
513 5         14 @pairs = @ARGV;
514 5 50       12 if(defined($pairs[0])) {
515 5 100       64 if($pairs[0] eq '--robot') {
    100          
    100          
    100          
516 1         4 $self->{is_robot} = 1;
517 1         3 shift @pairs;
518             } elsif($pairs[0] eq '--mobile') {
519 1         5 $self->{is_mobile} = 1;
520 1         3 shift @pairs;
521             } elsif($pairs[0] eq '--search-engine') {
522 1         3 $self->{is_search_engine} = 1;
523 1         3 shift @pairs;
524             } elsif($pairs[0] eq '--tablet') {
525 1         3 $self->{is_tablet} = 1;
526 1         3 shift @pairs;
527             }
528             }
529             } elsif($stdin_data) {
530 0         0 @pairs = split(/\n/, $stdin_data);
531             } elsif(!$self->{args_read}) {
532 1         5 my $oldfh = select(STDOUT);
533 1         25 print "Entering debug mode\n",
534             "Enter key=value pairs - end with quit\n";
535 1         6 select($oldfh);
536              
537             # Avoid prompting for the arguments more than once
538             # if just 'quit' is entered
539 1         10 $self->{args_read} = 1;
540              
541 1         9 while() {
542 2         27 chop(my $line = $_);
543 2         8 $line =~ s/[\r\n]//g;
544 2 100       9 last if $line eq 'quit';
545 1         3 push(@pairs, $line);
546 1         5 $stdin_data .= "$line\n";
547             }
548             }
549             } elsif(($ENV{'REQUEST_METHOD'} eq 'GET') || ($ENV{'REQUEST_METHOD'} eq 'HEAD')) {
550 44 100       115 if(my $query = $ENV{'QUERY_STRING'}) {
551 41 100 66     112 if((defined($content_type)) && ($content_type =~ /multipart\/form-data/i)) {
552 1         5 $self->_warn('Multipart/form-data not supported for GET');
553             }
554 40         96 $query =~ s/\\u0026/\&/g;
555 40         138 @pairs = split(/&/, $query);
556             } else {
557 3         15 return;
558             }
559             } elsif($ENV{'REQUEST_METHOD'} eq 'POST') {
560 16 100       50 if(!defined($ENV{'CONTENT_LENGTH'})) {
561 2         5 $self->{status} = 411;
562 2         8 return;
563             }
564 14         29 my $content_length = $ENV{'CONTENT_LENGTH'};
565 14 50 33     92 if(($self->{max_upload_size} >= 0) && ($content_length > $self->{max_upload_size})) { # Set maximum posts
566             # TODO: Design a way to tell the caller to send HTTP
567             # status 413
568 0         0 $self->{status} = 413;
569 0         0 $self->_warn('Large upload prohibited');
570 0         0 return;
571             }
572              
573 14 100 66     113 if((!defined($content_type)) || ($content_type =~ /application\/x-www-form-urlencoded/)) {
    100          
    100          
    100          
574 3         7 my $buffer;
575 3 100       8 if($stdin_data) {
576 1         4 $buffer = $stdin_data;
577             } else {
578 2 100       15 if(read(STDIN, $buffer, $content_length) != $content_length) {
579 1         3 $self->_warn('POST failed: something else may have read STDIN');
580             }
581 2         496 $stdin_data = $buffer;
582             }
583 3         17 @pairs = split(/&/, $buffer);
584              
585             # if($ENV{'QUERY_STRING'}) {
586             # my @getpairs = split(/&/, $ENV{'QUERY_STRING'});
587             # push(@pairs, @getpairs);
588             # }
589             } elsif($content_type =~ /multipart\/form-data/i) {
590 8 100       21 if(!defined($self->{upload_dir})) {
591 1         6 $self->_warn({
592             warning => 'Attempt to upload a file when upload_dir has not been set'
593             });
594 0         0 return;
595             }
596 7 100       55 if(!File::Spec->file_name_is_absolute($self->{upload_dir})) {
597 1         8 $self->_warn({
598             warning => "upload_dir $self->{upload_dir} isn't a full pathname"
599             });
600 0         0 delete $self->{upload_dir};
601 0         0 return;
602             }
603 6 100       136 if(!-d $self->{upload_dir}) {
604 2         22 $self->_warn({
605             warning => "upload_dir $self->{upload_dir} isn't a directory"
606             });
607 0         0 delete $self->{upload_dir};
608 0         0 return;
609             }
610 4 50       56 if(!-w $self->{upload_dir}) {
611 0         0 delete $self->{paramref};
612 0         0 $self->_warn({
613             warning => "upload_dir $self->{upload_dir} isn't writeable"
614             });
615 0         0 delete $self->{upload_dir};
616 0         0 return;
617             }
618 4 50       31 if($content_type =~ /boundary=(\S+)$/) {
619 4         29 @pairs = $self->_multipart_data({
620             length => $content_length,
621             boundary => $1
622             });
623             }
624             } elsif($content_type =~ /text\/xml/i) {
625 1         2 my $buffer;
626 1 50       2 if($stdin_data) {
627 0         0 $buffer = $stdin_data;
628             } else {
629 1 50       7 if(read(STDIN, $buffer, $content_length) != $content_length) {
630 0         0 $self->_warn({
631             warning => 'XML failed: something else may have read STDIN'
632             });
633             }
634 1         3 $stdin_data = $buffer;
635             }
636              
637 1         5 $FORM{XML} = $buffer;
638              
639 1         3 $self->{paramref} = \%FORM;
640              
641 1         8 return \%FORM;
642             } elsif($content_type =~ /application\/json/i) {
643 1         2 my $buffer;
644 1 50       3 if($stdin_data) {
645 0         0 $buffer = $stdin_data;
646             } else {
647 1         6 require JSON::MaybeXS;
648 1         39 JSON::MaybeXS->import();
649              
650 1 50       8 if(read(STDIN, $buffer, $content_length) != $content_length) {
651 0         0 $self->_warn({
652             warning => 'read failed: something else may have read STDIN'
653             });
654             }
655 1         2 $stdin_data = $buffer;
656             # JSON::Parse::assert_valid_json($buffer);
657             # my $paramref = JSON::Parse::parse_json($buffer);
658 1         11 my $paramref = decode_json($buffer);
659 1         2 foreach my $key(keys(%{$paramref})) {
  1         4  
660 2         11 push @pairs, "$key=" . $paramref->{$key};
661             }
662             }
663             } else {
664 1         2 my $buffer;
665 1 50       4 if($stdin_data) {
666 0         0 $buffer = $stdin_data;
667             } else {
668 1 50       9 if(read(STDIN, $buffer, $content_length) != $content_length) {
669 0         0 $self->_warn({
670             warning => 'read failed: something else may have read STDIN'
671             });
672             }
673 1         3 $stdin_data = $buffer;
674             }
675              
676 1         10 $self->_warn({
677             warning => "POST: Invalid or unsupported content type: $content_type: $buffer",
678             });
679             }
680             } elsif($ENV{'REQUEST_METHOD'} eq 'OPTIONS') {
681 0         0 $self->{status} = 405;
682 0         0 return;
683             } elsif($ENV{'REQUEST_METHOD'} eq 'DELETE') {
684 1         3 $self->{status} = 405;
685 1         6 return;
686             } else {
687             # TODO: Design a way to tell the caller to send HTTP
688             # status 501
689 2         7 $self->{status} = 501;
690 2         11 $self->_warn({
691             warning => 'Use POST, GET or HEAD'
692             });
693             }
694              
695 53 100       660 unless(scalar @pairs) {
696 1         6 return;
697             }
698              
699 52         3821 require String::Clean::XSS;
700 52         127000 String::Clean::XSS->import();
701             # require String::EscapeCage;
702             # String::EscapeCage->import();
703              
704 52         133 foreach my $arg (@pairs) {
705 102         369 my($key, $value) = split(/=/, $arg, 2);
706              
707 102 100       253 next unless($key);
708              
709 98         218 $key =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  1         16  
710 98         202 $key =~ tr/+/ /;
711 98 50       241 if(defined($value)) {
712 98         162 $value =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  17         78  
713 98         195 $value =~ tr/+/ /;
714             } else {
715 0         0 $value = '';
716             }
717              
718 98         234 $key = _sanitise_input($key);
719              
720 98 100       314922 if($self->{allow}) {
721             # Is this a permitted argument?
722 25 100       84 if(!exists($self->{allow}->{$key})) {
723 11 50       28 if($self->{logger}) {
724 0         0 $self->{logger}->info("discard $key");
725             }
726 11         25 next;
727             }
728              
729             # Do we allow any value, or must it be validated?
730 14 100       48 if(defined($self->{allow}->{$key})) {
731 9 100       52 if($value !~ $self->{allow}->{$key}) {
732 7 50       22 if($self->{logger}) {
733 0         0 $self->{logger}->info("block $key = $value");
734             }
735 7         15 next;
736             }
737             }
738             }
739              
740 80 100 100 5   300 if($self->{expect} && (List::MoreUtils::none { $_ eq $key } @{$self->{expect}})) {
  5         22  
  5         23  
741 2         8 next;
742             }
743 78         191 $value = _sanitise_input($value);
744              
745 78 100 100     166906 if((!defined($ENV{'REQUEST_METHOD'})) || ($ENV{'REQUEST_METHOD'} eq 'GET')) {
746             # From http://www.symantec.com/connect/articles/detection-sql-injection-and-cross-site-scripting-attacks
747 67 50 66     1242 if(($value =~ /(\%27)|(\')|(\-\-)|(\%23)|(\#)/ix) ||
      66        
      33        
      33        
      33        
      33        
748             ($value =~ /((\%3D)|(=))[^\n]*((\%27)|(\')|(\-\-)|(\%3B)|(;))/i) ||
749             ($value =~ /\w*((\%27)|(\'))((\%6F)|o|(\%4F))((\%72)|r|(\%52))/ix) ||
750             ($value =~ /((\%27)|(\'))union/ix) ||
751             ($value =~ /select[[a-z]\s\*]from/ix) ||
752             ($value =~ /\sAND\s1=1/ix) ||
753             ($value =~ /exec(\s|\+)+(s|x)p\w+/ix)) {
754 3 50       10 if($self->{logger}) {
755 0 0       0 if($ENV{'REMOTE_ADDR'}) {
756 0         0 $self->{logger}->warn($ENV{'REMOTE_ADDR'}, ": SQL injection attempt blocked for '$value'");
757             } else {
758 0         0 $self->{logger}->warn("SQL injection attempt blocked for '$value'");
759             }
760             }
761 3         12 $self->status(403);
762 3         17 return;
763             }
764 64 50 33     326 if(($value =~ /((\%3C)|<)((\%2F)|\/)*[a-z0-9\%]+((\%3E)|>)/ix) ||
765             ($value =~ /((\%3C)|<)[^\n]+((\%3E)|>)/i)) {
766 0 0       0 if($self->{logger}) {
767 0         0 $self->{logger}->warn("XSS injection attempt blocked for '$value'");
768             }
769 0         0 $self->status(403);
770 0         0 return;
771             }
772 64 50       182 if($value eq '../') {
773 0 0       0 if($self->{logger}) {
774 0         0 $self->{logger}->warn("Blocked directory traversal attack for $key");
775             }
776 0         0 $self->status(403);
777 0         0 return;
778             }
779             }
780 75 100       196 if(length($value) > 0) {
781             # Don't add if it's already there
782 71 100 100     247 if($FORM{$key} && ($FORM{$key} ne $value)) {
783 5         27 $FORM{$key} .= ",$value";
784             } else {
785 66         240 $FORM{$key} = $value;
786             }
787             }
788             }
789              
790 49 100       119 unless(%FORM) {
791 9         68 return;
792             }
793              
794 40 100       110 if($self->{logger}) {
795 2         14 while(my ($key,$value) = each %FORM) {
796 4         34 $self->{logger}->debug("$key=$value");
797 4         40 $log->debug("$key=$value");
798             }
799             }
800              
801 40         94 $self->{paramref} = \%FORM;
802              
803 40         288 return \%FORM;
804             }
805              
806             =head2 param
807              
808             Get a single parameter.
809             Takes an optional single string parameter which is the argument to return. If
810             that parameter is not given param() is a wrapper to params() with no arguments.
811              
812             use CGI::Info;
813             # ...
814             my $info = CGI::Info->new();
815             my $bar = $info->param('foo');
816              
817             If the requested parameter isn't in the allowed list, an error message will
818             be thrown:
819              
820             use CGI::Info;
821             my $allowed = {
822             'foo' => qr(\d+),
823             };
824             my $xyzzy = $info->params(allow => $allowed);
825             my $bar = $info->param('bar'); # Gives an error message
826              
827             Returns undef if the requested parameter was not given
828              
829             =cut
830              
831             sub param {
832 26     26 1 3032 my ($self, $field) = @_;
833              
834 26 100       75 if(!defined($field)) {
835 1         4 return $self->params();
836             }
837             # Is this a permitted argument?
838 25 100 100     105 if($self->{allow} && !exists($self->{allow}->{$field})) {
839 4         25 $self->_warn({
840             warning => "param: $field isn't in the allow list"
841             });
842 0         0 return;
843             }
844              
845 21 100       55 if(defined($self->params())) {
846 20         48 return $self->params()->{$field};
847             }
848 1         25 return;
849             }
850              
851             # Emit a warning message somewhere
852             sub _warn {
853 19     19   44 my $self = shift;
854              
855 19         27 my %params;
856 19 100       76 if(ref($_[0]) eq 'HASH') {
    50          
857 11         20 %params = %{$_[0]};
  11         68  
858             } elsif(scalar(@_) % 2 == 0) {
859 0         0 %params = @_;
860             } else {
861 8         21 $params{'warning'} = shift;
862             }
863              
864 19         45 my $warning = $params{'warning'};
865              
866 19 50       45 return unless($warning);
867 19 50       60 if($self eq __PACKAGE__) {
868             # Called from class method
869 0         0 carp($warning);
870 0         0 return;
871             }
872             # return if($self eq __PACKAGE__); # Called from class method
873              
874 19 50       48 if($self->{syslog}) {
875 0         0 require Sys::Syslog;
876              
877 0         0 Sys::Syslog->import();
878 0 0       0 if(ref($self->{syslog} eq 'HASH')) {
879 0         0 Sys::Syslog::setlogsock($self->{syslog});
880             }
881 0         0 openlog($self->script_name(), 'cons,pid', 'user');
882 0         0 syslog('warning', $warning);
883 0         0 closelog();
884             }
885              
886 19 50       84 if($self->{logger}) {
    50          
887 0         0 $self->{logger}->warn($warning);
888             } elsif(!defined($self->{syslog})) {
889 19         312 Carp::carp($warning);
890             }
891             }
892              
893             sub _sanitise_input($) {
894 176     176   301 my $arg = shift;
895              
896             # Remove hacking attempts and spaces
897 176         385 $arg =~ s/[\r\n]//g;
898 176         332 $arg =~ s/\s+$//;
899 176         366 $arg =~ s/^\s//;
900              
901 176         256 $arg =~ s///g;
902             # Allow :
903             # $arg =~ s/[;<>\*|`&\$!?#\(\)\[\]\{\}'"\\\r]//g;
904              
905             # return $arg;
906             # return String::EscapeCage->new(convert_XSS($arg))->escapecstring();
907 176         448 return convert_XSS($arg);
908             }
909              
910             sub _multipart_data {
911 4     4   10 my ($self, $args) = @_;
912              
913 4 50       13 if($self->{logger}) {
914 0         0 $self->{logger}->trace('Entering _multipart_data');
915             }
916 4         8 my $total_bytes = $$args{length};
917              
918 4 50       9 if($self->{logger}) {
919 0         0 $self->{logger}->trace("_multipart_data: total_bytes = $total_bytes");
920             }
921 4 50       11 if($total_bytes == 0) {
922 0         0 return;
923             }
924              
925 4 50       11 unless($stdin_data) {
926 4         25 while() {
927 44         101 chop(my $line = $_);
928 44         81 $line =~ s/[\r\n]//g;
929 44         164 $stdin_data .= "$line\n";
930             }
931 4 50       13 if(!$stdin_data) {
932 0         0 return;
933             }
934             }
935              
936 4         9 my $boundary = $$args{boundary};
937              
938 4         6 my @pairs;
939 4         7 my $writing_file = 0;
940 4         7 my $key;
941             my $value;
942 4         4 my $in_header = 0;
943 4         8 my $fout;
944              
945 4         29 foreach my $line(split(/\n/, $stdin_data)) {
946 34 100       147 if($line =~ /^--\Q$boundary\E--$/) {
947 2         5 last;
948             }
949 32 100       119 if($line =~ /^--\Q$boundary\E$/) {
    100          
950 8 50       34 if($writing_file) {
    100          
951 0         0 close $fout;
952 0         0 $writing_file = 0;
953             } elsif(defined($key)) {
954 4         15 push(@pairs, "$key=$value");
955 4         9 $value = undef;
956             }
957 8         39 $in_header = 1;
958             } elsif($in_header) {
959 16 100       66 if(length($line) == 0) {
    100          
960 6         14 $in_header = 0;
961             } elsif($line =~ /^Content-Disposition: (.+)/i) {
962 8         20 my $field = $1;
963 8 50       42 if($field =~ /name="(.+?)"/) {
964 8         19 $key = $1;
965             }
966 8 100       34 if($field =~ /filename="(.+)?"/) {
967 4         9 my $filename = $1;
968 4 100       15 unless(defined($filename)) {
    50          
969 0         0 $self->_warn('No upload filename given');
970 0         0 } elsif($filename =~ /[\\\/\|]/) {
971 2         9 $self->_warn("Disallowing invalid filename: $filename");
972             } else {
973 2         9 $filename = $self->_create_file_name({
974             filename => $filename
975             });
976              
977             # Don't do this since it taints the string and I can't work out how to untaint it
978             # my $full_path = Cwd::realpath(File::Spec->catfile($self->{upload_dir}, $filename));
979             # $full_path =~ m/^(\/[\w\.]+)$/;
980 2         30 my $full_path = File::Spec->catfile($self->{upload_dir}, $filename);
981 2 50       275 unless(open($fout, '>', $full_path)) {
982 0         0 $self->_warn("Can't open $full_path");
983             }
984 2         10 $writing_file = 1;
985 2         31 push(@pairs, "$key=$filename");
986             }
987             }
988             }
989             # TODO: handle Content-Type: text/plain, etc.
990             } else {
991 8 100       15 if($writing_file) {
992 4         34 print $fout "$line\n";
993             } else {
994 4         21 $value .= $line;
995             }
996             }
997             }
998              
999 2 50       9 if($writing_file) {
1000 2         105 close $fout;
1001             }
1002              
1003 2         26 return @pairs;
1004             }
1005              
1006             sub _create_file_name {
1007 2     2   6 my ($self, $args) = @_;
1008              
1009 2         10 return $$args{filename} . '_' . time;
1010             }
1011              
1012             # Untaint a filename. Regex from CGI::Untaint::Filenames
1013             sub _untaint_filename {
1014 35     35   85 my ($self, $args) = @_;
1015              
1016 35 50       233 if($$args{filename} =~ /(^[\w\+_\040\#\(\)\{\}\[\]\/\-\^,\.:;&%@\\~]+\$?$)/) {
1017 35         149 return $1;
1018             }
1019             # return undef;
1020             }
1021              
1022             =head2 is_mobile
1023              
1024             Returns a boolean if the website is being viewed on a mobile
1025             device such as a smart-phone.
1026             All tablets are mobile, but not all mobile devices are tablets.
1027              
1028             =cut
1029              
1030             sub is_mobile {
1031 38     38 1 1700 my $self = shift;
1032              
1033 38 100       111 if(defined($self->{is_mobile})) {
1034 11         39 return $self->{is_mobile};
1035             }
1036              
1037             # Support Sec-CH-UA-Mobile
1038 27 100       82 if(my $ch_ua_mobile = $ENV{'HTTP_SEC_CH_UA_MOBILE'}) {
1039 3 100       8 if($ch_ua_mobile eq '?1') {
1040 1         3 $self->{is_mobile} = 1;
1041 1         5 return 1;
1042             }
1043             }
1044 26 100       102 if($ENV{'HTTP_X_WAP_PROFILE'}) {
1045             # E.g. Blackberry
1046             # TODO: Check the sanity of this variable
1047 1         5 $self->{is_mobile} = 1;
1048 1         4 return 1;
1049             }
1050              
1051 25 100       74 if(my $agent = $ENV{'HTTP_USER_AGENT'}) {
1052 16 100       1949 if($agent =~ /.+(Android|iPhone).+/) {
1053 2         6 $self->{is_mobile} = 1;
1054 2         9 return 1;
1055             }
1056              
1057             # From http://detectmobilebrowsers.com/
1058 14 100 66     423 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) {
1059 1         3 $self->{is_mobile} = 1;
1060 1         5 return 1;
1061             }
1062              
1063             # Save loading and calling HTTP::BrowserDetect
1064 13         28 my $remote = $ENV{'REMOTE_ADDR'};
1065 13 50 66     46 if(defined($remote) && $self->{cache}) {
1066 0 0       0 if(my $type = $self->{cache}->get("$remote/$agent")) {
1067 0         0 return $self->{is_mobile} = ($type eq 'mobile');
1068             }
1069             }
1070              
1071 13 100       34 unless($self->{browser_detect}) {
1072 7 50       13 if(eval { require HTTP::BrowserDetect; }) {
  7         2687  
1073 7         60208 HTTP::BrowserDetect->import();
1074 7         26 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1075             }
1076             }
1077 13 50       1283 if($self->{browser_detect}) {
1078 13         79 my $device = $self->{browser_detect}->device();
1079 13   66     156 my $is_mobile = (defined($device) && ($device =~ /blackberry|webos|iphone|ipod|ipad|android/i));
1080 13 50 66     39 if($is_mobile && $self->{cache} && defined($remote)) {
      33        
1081 0         0 $self->{cache}->set("$remote/$agent", 'mobile', '1 day');
1082             }
1083 13         68 return $self->{is_mobile} = $is_mobile;
1084             }
1085             }
1086              
1087 9         39 return 0;
1088             }
1089              
1090             =head2 is_tablet
1091              
1092             Returns a boolean if the website is being viewed on a tablet such as an iPad.
1093              
1094             =cut
1095              
1096             sub is_tablet {
1097 6     6 1 37 my $self = shift;
1098              
1099 6 100       19 if(defined($self->{is_tablet})) {
1100 1         7 return $self->{is_tablet};
1101             }
1102              
1103 5 100 100     335 if($ENV{'HTTP_USER_AGENT'} && ($ENV{'HTTP_USER_AGENT'} =~ /.+(iPad|TabletPC).+/)) {
1104             # TODO: add others when I see some nice user_agents
1105 1         3 $self->{is_tablet} = 1;
1106             } else {
1107 4         9 $self->{is_tablet} = 0;
1108             }
1109              
1110 5         26 return $self->{is_tablet};
1111             }
1112              
1113             =head2 as_string
1114              
1115             Returns the parameters as a string, which is useful for debugging or
1116             generating keys for a cache.
1117              
1118             =cut
1119              
1120             sub as_string {
1121 35     35 1 14126 my $self = shift;
1122              
1123 35 100       96 unless($self->params()) {
1124 7         49 return '';
1125             }
1126              
1127 28         48 my %f = %{$self->params()};
  28         60  
1128              
1129 28         53 my $rc;
1130              
1131 28         114 foreach (sort keys %f) {
1132 40         82 my $value = $f{$_};
1133 40         97 $value =~ s/\\/\\\\/g;
1134 40         145 $value =~ s/(;|=)/\\$1/g;
1135 40 100       84 if(defined($rc)) {
1136 12         43 $rc .= ";$_=$value";
1137             } else {
1138 28         95 $rc = "$_=$value";
1139             }
1140             }
1141 28 100 66     145 if($rc && $self->{logger}) {
1142 1         9 $self->{logger}->debug("is_string: returning '$rc'");
1143             }
1144              
1145 28 50       208 return defined($rc) ? $rc : '';
1146             }
1147              
1148             =head2 protocol
1149              
1150             Returns the connection protocol, presumably 'http' or 'https', or undef if
1151             it can't be determined.
1152              
1153             =cut
1154              
1155             sub protocol {
1156 21     21 1 1698 my $self = shift;
1157              
1158 21 100 100     75 if($ENV{'SCRIPT_URI'} && ($ENV{'SCRIPT_URI'} =~ /^(.+):\/\/.+/)) {
1159 2         15 return $1;
1160             }
1161 19 100 100     66 if($ENV{'SERVER_PROTOCOL'} && ($ENV{'SERVER_PROTOCOL'} =~ /^HTTP\//)) {
1162 2         13 return 'http';
1163             }
1164              
1165 17         27 my $port = $ENV{'SERVER_PORT'};
1166 17 100       36 if(defined($port)) {
1167 14 50       2173 if(defined(my $name = getservbyport($port, 'tcp'))) {
    0          
    0          
1168 14 100       100 if($name =~ /https?/) {
    50          
1169 12         79 return $name;
1170             } elsif($name eq 'www') {
1171             # e.g. NetBSD and OpenBSD
1172 0         0 return 'http';
1173             }
1174             # Return an error, maybe missing something
1175             } elsif($port == 80) {
1176             # e.g. Solaris
1177 0         0 return 'http';
1178             } elsif($port == 443) {
1179 0         0 return 'https';
1180             }
1181             }
1182              
1183 5 50       14 if($ENV{'REMOTE_ADDR'}) {
1184 0         0 $self->_warn("Can't determine the calling protocol");
1185             }
1186 5         38 return;
1187             }
1188              
1189             =head2 tmpdir
1190              
1191             Returns the name of a directory that you can use to create temporary files
1192             in.
1193              
1194             The routine is preferable to L since CGI programs are
1195             often running on shared servers. Having said that, tmpdir will fall back
1196             to File::Spec->tmpdir() if it can't find somewhere better.
1197              
1198             If the parameter 'default' is given, then use that directory as a
1199             fall-back rather than the value in File::Spec->tmpdir().
1200             No sanity tests are done, so if you give the default value of
1201             '/non-existant', that will be returned.
1202              
1203             Tmpdir allows a reference of the options to be passed.
1204              
1205             use CGI::Info;
1206              
1207             my $info = CGI::Info->new();
1208             my $dir = $info->tmpdir(default => '/var/tmp');
1209             $dir = $info->tmpdir({ default => '/var/tmp' });
1210              
1211             # or
1212              
1213             my $dir = CGI::Info->tmpdir();
1214             =cut
1215              
1216             sub tmpdir {
1217 15     15 1 2236 my $self = shift;
1218 15 100       54 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  1         7  
1219              
1220 15         25 my $name = 'tmp';
1221 15 50       47 if($^O eq 'MSWin32') {
1222 0         0 $name = 'temp';
1223             }
1224              
1225 15         21 my $dir;
1226              
1227 15 100       35 if(!ref($self)) {
1228 3         9 $self = __PACKAGE__->new();
1229             }
1230              
1231 15 100 100     169 if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) {
1232 4         45 $dir = File::Spec->catdir($ENV{'C_DOCUMENT_ROOT'}, $name);
1233 4 100 66     87 if((-d $dir) && (-w $dir)) {
1234 2         12 return $self->_untaint_filename({ filename => $dir });
1235             }
1236 2         8 $dir = $ENV{'C_DOCUMENT_ROOT'};
1237 2 50 33     48 if((-d $dir) && (-w $dir)) {
1238 2         13 return $self->_untaint_filename({ filename => $dir });
1239             }
1240             }
1241 11 100 100     133 if($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) {
1242 1         21 $dir = File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, File::Spec->updir(), $name);
1243 1 50 33     30 if((-d $dir) && (-w $dir)) {
1244 1         8 return $self->_untaint_filename({ filename => $dir });
1245             }
1246             }
1247 10 100       285 return $params{default} ? $params{default} : File::Spec->tmpdir();
1248             }
1249              
1250             =head2 rootdir
1251              
1252             Returns the document root. This is preferable to looking at DOCUMENT_ROOT
1253             in the environment because it will also work when we're not running as a CGI
1254             script, which is useful for script debugging.
1255              
1256             This can be run as a class or object method.
1257              
1258             use CGI::Info;
1259              
1260             print CGI::Info->rootdir();
1261              
1262             =cut
1263              
1264             sub rootdir {
1265 9 50 66 9 1 6423 if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) {
    100 100        
1266 0         0 return $ENV{'C_DOCUMENT_ROOT'};
1267             } elsif($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) {
1268 3         26 return $ENV{'DOCUMENT_ROOT'};
1269             }
1270 6         32 my $script_name = $0;
1271              
1272 6 50       74 unless(File::Spec->file_name_is_absolute($script_name)) {
1273 6         242 $script_name = File::Spec->rel2abs($script_name);
1274             }
1275 6 50       39 if($script_name =~ /.cgi\-bin.*/) { # kludge for outside CGI environment
1276 0         0 $script_name =~ s/.cgi\-bin.*//;
1277             }
1278 6 50       144 if(-f $script_name) { # More kludge
1279 6 50       36 if($^O eq 'MSWin32') {
1280 0 0       0 if($script_name =~ /(.+)\\.+?$/) {
1281 0         0 return $1;
1282             }
1283             } else {
1284 6 50       60 if($script_name =~ /(.+)\/.+?$/) {
1285 6         51 return $1;
1286             }
1287             }
1288             }
1289 0         0 return $script_name;
1290             }
1291              
1292             =head2 logdir
1293              
1294             Gets and sets the name of a directory that you can use to store logs in.
1295              
1296             =cut
1297              
1298             sub logdir {
1299 4     4 1 2311 my $self = shift;
1300 4         7 my $dir = shift;
1301              
1302 4 100       12 if(!ref($self)) {
1303 1         3 $self = __PACKAGE__->new();
1304             }
1305              
1306 4 100       10 if(defined($dir)) {
1307             # No sanity testing is done
1308 1         6 return $self->{logdir} = $dir;
1309             }
1310              
1311 3         23 foreach my $rc($self->{logdir}, $ENV{'LOGDIR'}, Sys::Path->logdir(), $self->tmpdir()) {
1312 9 100 66     158 if(defined($rc) && length($rc) && (-d $rc) && (-w $rc)) {
      100        
      66        
1313 3         9 $dir = $rc;
1314 3         6 last;
1315             }
1316             }
1317 3 50 33     18 carp("Can't determine logdir") if((!defined($dir)) || (length($dir) == 0));
1318 3   66     13 $self->{logdir} ||= $dir;
1319              
1320 3         21 return $dir;
1321             }
1322              
1323             =head2 is_robot
1324              
1325             Is the visitor a real person or a robot?
1326              
1327             use CGI::Info;
1328              
1329             my $info = CGI::Info->new();
1330             unless($info->is_robot()) {
1331             # update site visitor statistics
1332             }
1333              
1334             =cut
1335              
1336             sub is_robot {
1337 19     19 1 847 my $self = shift;
1338              
1339 19 100       55 if(defined($self->{is_robot})) {
1340 3         11 return $self->{is_robot};
1341             }
1342              
1343 16         35 my $agent = $ENV{'HTTP_USER_AGENT'};
1344 16         27 my $remote = $ENV{'REMOTE_ADDR'};
1345              
1346 16 100 100     62 unless($remote && $agent) {
1347             # Probably not running in CGI - assume real person
1348 7         21 return 0;
1349             }
1350              
1351 9 100       500 if($agent =~ /.+bot|bytespider|msnptc|is_archiver|backstreet|spider|scoutjet|gingersoftware|heritrix|dodnetdotcom|yandex|nutch|ezooms|plukkie|nova\.6scan\.com|Twitterbot|adscanner|python-requests|Mediatoolkitbot|NetcraftSurveyAgent|Expanse|serpstatbot|DreamHost SiteMonitor 1.0/i) {
1352 3         7 $self->{is_robot} = 1;
1353 3         15 return 1;
1354             }
1355              
1356 6         19 my $key = "$remote/$agent";
1357              
1358 6 100       19 if(my $referrer = $ENV{'HTTP_REFERER'}) {
1359             # https://agency.ohow.co/google-analytics-implementation-audit/google-analytics-historical-spam-list/
1360 2         12 my @crawler_lists = (
1361             'http://fix-website-errors.com',
1362             'http://keywords-monitoring-your-success.com',
1363             'http://free-video-tool.com',
1364             'http://magnet-to-torrent.com',
1365             'http://torrent-to-magnet.com',
1366             'http://dogsrun.net',
1367             'http://###.responsive-test.net',
1368             'http://uptime.com',
1369             'http://uptimechecker.com',
1370             'http://top1-seo-service.com',
1371             'http://fast-wordpress-start.com',
1372             'http://wordpress-crew.net',
1373             'http://dbutton.net',
1374             'http://justprofit.xyz',
1375             'http://video--production.com',
1376             'http://buttons-for-website.com',
1377             'http://buttons-for-your-website.com',
1378             'http://success-seo.com',
1379             'http://videos-for-your-business.com',
1380             'http://semaltmedia.com',
1381             'http://dailyrank.net',
1382             'http://uptimebot.net',
1383             'http://sitevaluation.org',
1384             'http://100dollars-seo.com',
1385             'http://forum69.info',
1386             'http://partner.semalt.com',
1387             'http://best-seo-offer.com',
1388             'http://best-seo-solution.com',
1389             'http://semalt.semalt.com',
1390             'http://semalt.com',
1391             'http://7makemoneyonline.com',
1392             'http://anticrawler.org',
1393             'http://baixar-musicas-gratis.com',
1394             'http://descargar-musica-gratis.net',
1395              
1396             # Mine
1397             'http://www.seokicks.de/robot.html',
1398             );
1399 2         6 $referrer =~ s/\\/_/g;
1400 2 50 66 3   22 if(($referrer =~ /\)/) || (List::MoreUtils::any { $_ =~ /^$referrer/ } @crawler_lists)) {
  3         27  
1401 2 50       6 if($self->{logger}) {
1402 2         8 $self->{logger}->debug("is_robot: blocked trawler $referrer");
1403             }
1404 2 50       12 if($self->{cache}) {
1405 0         0 $self->{cache}->set($key, 'robot', '1 day');
1406             }
1407 2         4 $self->{is_robot} = 1;
1408 2         11 return 1;
1409             }
1410             }
1411              
1412 4 50       11 if($self->{cache}) {
1413 0 0 0     0 if(defined($remote) && $self->{cache}) {
1414 0 0       0 if(my $type = $self->{cache}->get("$remote/$agent")) {
1415 0         0 return $self->{is_robot} = ($type eq 'robot');
1416             }
1417             }
1418             }
1419              
1420 4 100       10 unless($self->{browser_detect}) {
1421 3 50       7 if(eval { require HTTP::BrowserDetect; }) {
  3         20  
1422 3         12 HTTP::BrowserDetect->import();
1423 3         8 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1424             }
1425             }
1426 4 50       571 if($self->{browser_detect}) {
1427 4         19 my $is_robot = $self->{browser_detect}->robot();
1428 4 100 100     841 if(defined($is_robot) && $self->{logger}) {
1429 1         10 $self->{logger}->debug("HTTP::BrowserDetect '$ENV{HTTP_USER_AGENT}' returns $is_robot");
1430             }
1431 4 100 66     22 $is_robot = (defined($is_robot) && ($is_robot)) ? 1 : 0;
1432 4 100       11 if($self->{logger}) {
1433 2         8 $self->{logger}->debug("is_robot: $is_robot");
1434             }
1435              
1436 4 100       15 if($is_robot) {
1437 2 50       7 if($self->{cache}) {
1438 0         0 $self->{cache}->set($key, 'robot', '1 day');
1439             }
1440 2         5 $self->{is_robot} = $is_robot;
1441 2         8 return $is_robot;
1442             }
1443             }
1444              
1445 2 50       6 if($self->{cache}) {
1446 0         0 $self->{cache}->set($key, 'unknown', '1 day');
1447             }
1448 2         6 $self->{is_robot} = 0;
1449 2         10 return 0;
1450             }
1451              
1452             =head2 is_search_engine
1453              
1454             Is the visitor a search engine?
1455              
1456             use CGI::Info;
1457              
1458             if(CGI::Info->new()->is_search_engine()) {
1459             # display generic information about yourself
1460             } else {
1461             # allow the user to pick and choose something to display
1462             }
1463              
1464             =cut
1465              
1466             sub is_search_engine {
1467 25     25 1 846 my $self = shift;
1468              
1469 25 100       69 if(defined($self->{is_search_engine})) {
1470 6         26 return $self->{is_search_engine};
1471             }
1472              
1473 19         41 my $remote = $ENV{'REMOTE_ADDR'};
1474 19         31 my $agent = $ENV{'HTTP_USER_AGENT'};
1475              
1476 19 100 100     71 unless($remote && $agent) {
1477             # Probably not running in CGI - assume not a search engine
1478 8         28 return 0;
1479             }
1480              
1481 11         18 my $key;
1482              
1483 11 50       25 if($self->{cache}) {
1484 0         0 $key = "$remote/$agent";
1485 0 0 0     0 if(defined($remote) && $self->{cache}) {
1486 0 0       0 if(my $type = $self->{cache}->get("$remote/$agent")) {
1487 0         0 return $self->{is_search} = ($type eq 'search');
1488             }
1489             }
1490             }
1491              
1492             # Don't use HTTP_USER_AGENT to detect more than we really have to since
1493             # that is easily spoofed
1494 11 50       62 if($agent =~ /www\.majestic12\.co\.uk|facebookexternal/) {
1495 0 0       0 if($self->{cache}) {
1496 0         0 $self->{cache}->set($key, 'search', '1 day');
1497             }
1498 0         0 return 1;
1499             }
1500              
1501 11 100       29 unless($self->{browser_detect}) {
1502 7 50       14 if(eval { require HTTP::BrowserDetect; }) {
  7         1068  
1503 7         20791 HTTP::BrowserDetect->import();
1504 7         22 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1505             }
1506             }
1507 11 50       1186 if(my $browser = $self->{browser_detect}) {
1508 11   66     27 my $is_search = ($browser->google() || $browser->msn() || $browser->baidu() || $browser->altavista() || $browser->yahoo() || $browser->bingbot());
1509 11 100 100     3049 if((!$is_search) && $agent =~ /SeznamBot\//) {
1510 1         3 $is_search = 1;
1511             }
1512 11 50 66     39 if($is_search && $self->{cache}) {
1513 0         0 $self->{cache}->set($key, 'search', '1 day');
1514             }
1515 11         73 return $self->{is_search_engine} = $is_search;
1516             }
1517              
1518             # TODO: DNS lookup, not gethostbyaddr - though that will be slow
1519 0   0     0 my $hostname = gethostbyaddr(inet_aton($remote), AF_INET) || $remote;
1520              
1521 0 0 0     0 if(defined($hostname) && ($hostname =~ /google|msnbot|bingbot|amazonbot/) && ($hostname !~ /^google-proxy/)) {
      0        
1522 0 0       0 if($self->{cache}) {
1523 0         0 $self->{cache}->set($key, 'search', '1 day');
1524             }
1525 0         0 $self->{is_search_engine} = 1;
1526 0         0 return 1;
1527             }
1528              
1529 0         0 $self->{is_search_engine} = 0;
1530 0         0 return 0;
1531             }
1532              
1533             =head2 browser_type
1534              
1535             Returns one of 'web', 'search', 'robot' and 'mobile'.
1536              
1537             # Code to display a different web page for a browser, search engine and
1538             # smartphone
1539             use Template;
1540             use CGI::Info;
1541              
1542             my $info = CGI::Info->new();
1543             my $dir = $info->rootdir() . '/templates/' . $info->browser_type();
1544              
1545             my $filename = ref($self);
1546             $filename =~ s/::/\//g;
1547             $filename = "$dir/$filename.tmpl";
1548              
1549             if((!-f $filename) || (!-r $filename)) {
1550             die "Can't open $filename";
1551             }
1552             my $template = Template->new();
1553             $template->process($filename, {}) || die $template->error();
1554              
1555             =cut
1556              
1557             sub browser_type {
1558 21     21 1 45 my $self = shift;
1559              
1560 21 100       57 if($self->is_mobile()) {
1561 8         38 return 'mobile';
1562             }
1563 13 100       39 if($self->is_search_engine()) {
1564 6         32 return 'search';
1565             }
1566 7 100       20 if($self->is_robot()) {
1567 3         16 return 'robot';
1568             }
1569 4         17 return 'web';
1570             }
1571              
1572             =head2 get_cookie
1573              
1574             Returns a cookie's value, or undef if no name is given, or the requested
1575             cookie isn't in the jar.
1576              
1577             Deprecated - use cookie() instead.
1578              
1579             use CGI::Info;
1580              
1581             my $i = CGI::Info->new();
1582             my $name = $i->get_cookie(cookie_name => 'name');
1583             print "Your name is $name\n";
1584             my $address = $i->get_cookie('address');
1585             print "Your address is $address\n";
1586              
1587             =cut
1588              
1589             sub get_cookie {
1590 13     13 1 694 my $self = shift;
1591 13         25 my %params;
1592              
1593 13 100       45 if(ref($_[0]) eq 'HASH') {
    100          
1594 3         5 %params = %{$_[0]};
  3         12  
1595             } elsif(scalar(@_) % 2 == 0) {
1596 9         27 %params = @_;
1597             } else {
1598 1         3 $params{'cookie_name'} = shift;
1599             }
1600              
1601 13 100       39 if(!defined($params{'cookie_name'})) {
1602 3         9 $self->_warn('cookie_name argument not given');
1603 2         673 return;
1604             }
1605              
1606 10 100       24 unless($self->{jar}) {
1607 4 100       10 unless(defined($ENV{'HTTP_COOKIE'})) {
1608 1         5 return;
1609             }
1610 3         17 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1611              
1612 3         8 foreach my $cookie(@cookies) {
1613 11         27 my ($name, $value) = split(/=/, $cookie);
1614 11         28 $self->{jar}->{$name} = $value;
1615             }
1616             }
1617              
1618 9 100       28 if(exists($self->{jar}->{$params{'cookie_name'}})) {
1619 6         34 return $self->{jar}->{$params{'cookie_name'}};
1620             }
1621 3         12 return; # Return undef
1622             }
1623              
1624             =head2 cookie
1625              
1626             Returns a cookie's value, or undef if no name is given, or the requested
1627             cookie isn't in the jar.
1628             API is the same as "param", it will replace the "get_cookie" method in the future.
1629              
1630             use CGI::Info;
1631              
1632             my $name = CGI::Info->new()->cookie('name');
1633             print "Your name is $name\n";
1634              
1635             =cut
1636              
1637             sub cookie {
1638 2     2 1 8 my ($self, $field) = @_;
1639              
1640 2 50       7 if(!defined($field)) {
1641 0         0 $self->_warn('what cookie do you want?');
1642 0         0 return;
1643             }
1644              
1645 2 50       5 unless($self->{jar}) {
1646 0 0       0 unless(defined($ENV{'HTTP_COOKIE'})) {
1647 0         0 return;
1648             }
1649 0         0 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1650              
1651 0         0 foreach my $cookie(@cookies) {
1652 0         0 my ($name, $value) = split(/=/, $cookie);
1653 0         0 $self->{jar}->{$name} = $value;
1654             }
1655             }
1656              
1657 2 50       6 if(exists($self->{jar}->{$field})) {
1658 2         10 return $self->{jar}->{$field};
1659             }
1660 0         0 return; # Return undef
1661             }
1662              
1663             =head2 status
1664              
1665             Sets or returns the status of the object, 200 for OK, otherwise an HTTP error code
1666              
1667             =cut
1668              
1669             sub status {
1670 12     12 1 2852 my $self = shift;
1671              
1672 12 100       52 if(my $status = shift) {
    100          
1673 4         13 $self->{status} = $status;
1674             } elsif(!defined($self->{status})) {
1675 5 100       19 if(defined(my $method = $ENV{'REQUEST_METHOD'})) {
1676 4 100 66     48 if(($method eq 'OPTIONS') || ($method eq 'DELETE')) {
    100 66        
1677 1         11 return 405;
1678             } elsif(($method eq 'POST') && !defined($ENV{'CONTENT_LENGTH'})) {
1679 1         5 return 411;
1680             }
1681             }
1682 3         17 return 200;
1683             }
1684              
1685 7   50     35 return $self->{status} || 200;
1686             }
1687              
1688             =head2 set_logger
1689              
1690             Sometimes you don't know what the logger is until you've instantiated the class.
1691             This function fixes the catch22 situation.
1692              
1693             =cut
1694              
1695             sub set_logger {
1696 3     3 1 53 my $self = shift;
1697 3         5 my %params;
1698              
1699 3 100       17 if(ref($_[0]) eq 'HASH') {
    100          
1700 1         3 %params = %{$_[0]};
  1         5  
1701             } elsif(scalar(@_) % 2 == 0) {
1702 1         4 %params = @_;
1703             } else {
1704 1         4 $params{'logger'} = shift;
1705             }
1706              
1707 3         8 $self->{logger} = $params{'logger'};
1708              
1709 3         6 return $self;
1710             }
1711              
1712             =head2 reset
1713              
1714             Class method to reset the class.
1715             You should do this in an FCGI environment before instantiating, but nowhere else.
1716              
1717             =cut
1718              
1719             sub reset {
1720 11     11 1 11178 my $class = shift;
1721              
1722 11 100       35 unless($class eq __PACKAGE__) {
1723 1         22 carp('Reset is a class method');
1724 0         0 return;
1725             }
1726              
1727 10         29 $stdin_data = undef;
1728             }
1729              
1730             sub AUTOLOAD {
1731 157     157   66897 our $AUTOLOAD;
1732 157         321 my $param = $AUTOLOAD;
1733              
1734 157         990 $param =~ s/.*:://;
1735              
1736 157 100       3318 return if($param eq 'DESTROY');
1737              
1738 4         8 my $self = shift;
1739              
1740 4 50       14 return if(ref($self) ne __PACKAGE__);
1741              
1742 4         16 return $self->param($param);
1743             }
1744              
1745             =head1 AUTHOR
1746              
1747             Nigel Horne, C<< >>
1748              
1749             =head1 BUGS
1750              
1751             is_tablet() only currently detects the iPad and Windows PCs. Android strings
1752             don't differ between tablets and smart-phones.
1753              
1754             Please report any bugs or feature requests to C,
1755             or through the web interface at
1756             L.
1757             I will be notified, and then you'll
1758             automatically be notified of progress on your bug as I make changes.
1759              
1760             params() returns a ref which means that calling routines can change the hash
1761             for other routines.
1762             Take a local copy before making amendments to the table if you don't want unexpected
1763             things to happen.
1764              
1765             =head1 SEE ALSO
1766              
1767             L
1768              
1769             =head1 SUPPORT
1770              
1771             You can find documentation for this module with the perldoc command.
1772              
1773             perldoc CGI::Info
1774              
1775             You can also look for information at:
1776              
1777             =over 4
1778              
1779             =item * MetaCPAN
1780              
1781             L
1782              
1783             =item * RT: CPAN's request tracker
1784              
1785             L
1786              
1787             =item * CPAN Testers' Matrix
1788              
1789             L
1790              
1791             =item * CPAN Testers Dependencies
1792              
1793             L
1794              
1795             =back
1796              
1797             =head1 LICENSE AND COPYRIGHT
1798              
1799             Copyright 2010-2023 Nigel Horne.
1800              
1801             This program is released under the following licence: GPL2
1802              
1803             =cut
1804              
1805             1;