File Coverage

blib/lib/CGI/Info.pm
Criterion Covered Total %
statement 579 692 83.6
branch 356 470 75.7
condition 116 173 67.0
subroutine 44 44 100.0
pod 24 24 100.0
total 1119 1403 79.7


line stmt bran cond sub pod time code
1             package CGI::Info;
2              
3             # TODO: remove the expect argument
4              
5 22     22   3501875 use warnings;
  22         318  
  22         803  
6 22     22   160 use strict;
  22         77  
  22         435  
7 22     22   107 use Carp;
  22         52  
  22         1250  
8 22     22   162 use File::Spec;
  22         59  
  22         721  
9 22     22   13998 use Socket; # For AF_INET
  22         87898  
  22         9391  
10 22     22   586 use 5.008;
  22         70  
11 22     22   10729 use Log::Any qw($log);
  22         191671  
  22         134  
12             # use Cwd;
13             # use JSON::Parse;
14 22     22   62616 use List::MoreUtils; # Can go when expect goes
  22         324602  
  22         162  
15             # use Sub::Private;
16 22     22   35294 use Sys::Path;
  22         621991  
  22         871  
17              
18 22     22   11356 use namespace::clean;
  22         367737  
  22         177  
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.76
29              
30             =cut
31              
32             our $VERSION = '0.76';
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 107665 my $class = $_[0];
79              
80 154         265 shift;
81 154 50       555 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  0         0  
82              
83 154 100 100     754 if($args{expect} && (ref($args{expect}) ne 'ARRAY')) {
84 1         21 warn __PACKAGE__, ': expect must be a reference to an array';
85 1         86 return;
86             }
87              
88 153 100       611 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         12 return bless { %{$class}, %args }, ref($class);
  4         57  
98             }
99              
100 149         586 my %defaults = (
101             max_upload_size => 512 * 1024,
102             allow => undef,
103             expect => undef,
104             upload_dir => undef
105             );
106              
107 149         932 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 341 my $self = shift;
126              
127 15 100       39 unless($self->{script_name}) {
128 9         21 $self->_find_paths();
129             }
130 15         100 return $self->{script_name};
131             }
132              
133             sub _find_paths {
134 15     15   22 my $self = shift;
135              
136 15         107 require File::Basename;
137 15         463 File::Basename->import();
138              
139 15 100       53 if($ENV{'SCRIPT_NAME'}) {
140 11         335 $self->{script_name} = File::Basename::basename($ENV{'SCRIPT_NAME'});
141             } else {
142 4         233 $self->{script_name} = File::Basename::basename($0);
143             }
144             $self->{script_name} = $self->_untaint_filename({
145             filename => $self->{script_name}
146 15         81 });
147              
148 15 100 66     136 if($ENV{'SCRIPT_FILENAME'}) {
    100 66        
    100          
    50          
149 1         2 $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     243 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         5 $self->{script_path} = $ENV{'SCRIPT_NAME'};
162             } else {
163 4         25 require Cwd;
164 4         103 Cwd->import;
165              
166 4         10 my $script_name = $ENV{'SCRIPT_NAME'};
167 4 100       18 if($script_name =~ /^\/(.+)/) {
168             # It's usually the case, e.g. /cgi-bin/foo.pl
169 2         6 $script_name = $1;
170             }
171              
172 4         110 $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         306 $self->{script_path} = File::Spec->rel2abs($0);
179             }
180              
181             $self->{script_path} = $self->_untaint_filename({
182             filename => $self->{script_path}
183 15         90 });
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 12435 my $self = shift;
207              
208 22 100       74 unless($self->{script_path}) {
209 5         16 $self->_find_paths();
210             }
211 22         238 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       30 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       32 if($^O eq 'MSWin32') {
236 0 0       0 if($self->{script_path} =~ /(.+)\\.+?$/) {
237 0         0 return $1;
238             }
239             } else {
240 11 50       79 if($self->{script_path} =~ /(.+)\/.+?$/) {
241 11         119 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 827 my $self = shift;
270              
271 8 100       22 unless($self->{site}) {
272 2         4 $self->_find_site_details();
273             }
274              
275 8         45 return $self->{site};
276             }
277              
278             sub _find_site_details {
279 7     7   12 my $self = shift;
280              
281 7 100       19 if($self->{logger}) {
282 4         13 $self->{logger}->trace('Entering _find_site_details');
283             }
284 7 50 66     48 if($self->{site} && $self->{cgi_site}) {
285 2         3 return;
286             }
287              
288 5         505 require URI::Heuristic;
289 5         2132 URI::Heuristic->import;
290              
291 5 100       19 if($ENV{'HTTP_HOST'}) {
    100          
292 1         10 $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       36 if($self->{cgi_site} =~ /(.*)\.+$/) {
296 1         3 $self->{cgi_site} = $1;
297             }
298             } elsif($ENV{'SERVER_NAME'}) {
299 3         10 $self->{cgi_site} = URI::Heuristic::uf_uristr($ENV{'SERVER_NAME'});
300 3 100 66     66 if(defined($self->protocol()) && ($self->protocol() ne 'http')) {
301 1         8 $self->{cgi_site} =~ s/^http//;
302 1         3 $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       10 if($self->{logger}) {
309 1         4 $self->{logger}->debug('Falling back to using hostname');
310             }
311              
312 1         14 $self->{cgi_site} = Sys::Hostname::hostname();
313             }
314              
315 5 50       29 unless($self->{site}) {
316 5         17 $self->{site} = $self->{cgi_site};
317             }
318 5 100       20 if($self->{site} =~ /^https?:\/\/(.+)/) {
319 4         12 $self->{site} = $1;
320             }
321 5 100       17 unless($self->{cgi_site} =~ /^https?:\/\//) {
322 1         4 my $protocol = $self->protocol();
323              
324 1 50       7 unless($protocol) {
325 0         0 $protocol = 'http';
326             }
327 1         5 $self->{cgi_site} = "$protocol://" . $self->{cgi_site};
328             }
329 5 50 33     24 unless($self->{site} && $self->{cgi_site}) {
330 0         0 $self->_warn('Could not determine site name');
331             }
332 5 100       14 if($self->{logger}) {
333 3         11 $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 22 my $self = shift;
346              
347 5 100       17 if($self->{domain}) {
348 3         18 return $self->{domain};
349             }
350 2         6 $self->_find_site_details();
351              
352 2 50       7 if($self->{site}) {
353 2         7 $self->{domain} = $self->{site};
354 2 100       12 if($self->{domain} =~ /^www\.(.+)/) {
355 1         4 $self->{domain} = $1;
356             }
357             }
358              
359 2         8 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 44 my $self = shift;
370              
371 7 100       19 unless($self->{cgi_site}) {
372 3         7 $self->_find_site_details();
373             }
374              
375 7         66 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 6993 my $self = shift;
480              
481 162 100       488 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  4         19  
482              
483 162 100 66     638 if((defined($self->{paramref})) && ((!defined($args{'allow'})) || defined($self->{allow}) && ($args{'allow'} eq $self->{allow}))) {
      66        
484 93         436 return $self->{paramref};
485             }
486              
487 69 100       181 if(defined($args{allow})) {
488 4         14 $self->{allow} = $args{allow};
489             }
490 69 100       157 if(defined($args{expect})) {
491 3 100       32 if(ref($args{expect}) eq 'ARRAY') {
492 2         7 $self->{expect} = $args{expect};
493             } else {
494 1         5 $self->_warn('expect must be a reference to an array');
495             }
496             }
497 69 100       876 if(defined($args{upload_dir})) {
498 1         4 $self->{upload_dir} = $args{upload_dir};
499             }
500 69 100       153 if(defined($args{logger})) {
501 1         9 $self->{logger} = $args{logger};
502             }
503 69 100       160 if($self->{logger}) {
504 2         17 $self->{logger}->trace('Entering params');
505             }
506              
507 69         111 my @pairs;
508 69         149 my $content_type = $ENV{'CONTENT_TYPE'};
509 69         96 my %FORM;
510              
511 69 100 66     514 if((!$ENV{'GATEWAY_INTERFACE'}) || (!$ENV{'REQUEST_METHOD'})) {
    100 100        
    100          
    50          
    100          
512 6 100       22 if(@ARGV) {
    50          
    50          
513 5         16 @pairs = @ARGV;
514 5 50       17 if(defined($pairs[0])) {
515 5 100       22 if($pairs[0] eq '--robot') {
    100          
    100          
    100          
516 1         3 $self->{is_robot} = 1;
517 1         3 shift @pairs;
518             } elsif($pairs[0] eq '--mobile') {
519 1         3 $self->{is_mobile} = 1;
520 1         4 shift @pairs;
521             } elsif($pairs[0] eq '--search-engine') {
522 1         3 $self->{is_search_engine} = 1;
523 1         2 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         48 print "Entering debug mode\n",
534             "Enter key=value pairs - end with quit\n";
535 1         8 select($oldfh);
536              
537             # Avoid prompting for the arguments more than once
538             # if just 'quit' is entered
539 1         4 $self->{args_read} = 1;
540              
541 1         8 while() {
542 2         32 chop(my $line = $_);
543 2         6 $line =~ s/[\r\n]//g;
544 2 100       13 last if $line eq 'quit';
545 1         12 push(@pairs, $line);
546 1         7 $stdin_data .= "$line\n";
547             }
548             }
549             } elsif(($ENV{'REQUEST_METHOD'} eq 'GET') || ($ENV{'REQUEST_METHOD'} eq 'HEAD')) {
550 44 100       119 if(my $query = $ENV{'QUERY_STRING'}) {
551 41 100 66     110 if((defined($content_type)) && ($content_type =~ /multipart\/form-data/i)) {
552 1         4 $self->_warn('Multipart/form-data not supported for GET');
553             }
554 40         86 $query =~ s/\\u0026/\&/g;
555 40         141 @pairs = split(/&/, $query);
556             } else {
557 3         55 return;
558             }
559             } elsif($ENV{'REQUEST_METHOD'} eq 'POST') {
560 16 100       49 if(!defined($ENV{'CONTENT_LENGTH'})) {
561 2         4 $self->{status} = 411;
562 2         9 return;
563             }
564 14         33 my $content_length = $ENV{'CONTENT_LENGTH'};
565 14 50 33     83 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     135 if((!defined($content_type)) || ($content_type =~ /application\/x-www-form-urlencoded/)) {
    100          
    100          
    100          
574 3         5 my $buffer;
575 3 100       8 if($stdin_data) {
576 1         3 $buffer = $stdin_data;
577             } else {
578 2 100       14 if(read(STDIN, $buffer, $content_length) != $content_length) {
579 1         3 $self->_warn('POST failed: something else may have read STDIN');
580             }
581 2         352 $stdin_data = $buffer;
582             }
583 3         18 @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       23 if(!defined($self->{upload_dir})) {
591 1         13 $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       50 if(!File::Spec->file_name_is_absolute($self->{upload_dir})) {
597 1         16 $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       204 if(!-d $self->{upload_dir}) {
604 2         24 $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       52 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       33 if($content_type =~ /boundary=(\S+)$/) {
619 4         33 @pairs = $self->_multipart_data({
620             length => $content_length,
621             boundary => $1
622             });
623             }
624             } elsif($content_type =~ /text\/xml/i) {
625 1         3 my $buffer;
626 1 50       3 if($stdin_data) {
627 0         0 $buffer = $stdin_data;
628             } else {
629 1 50       8 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         4 $FORM{XML} = $buffer;
638              
639 1         5 $self->{paramref} = \%FORM;
640              
641 1         9 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         8 require JSON::MaybeXS;
648 1         39 JSON::MaybeXS->import();
649              
650 1 50       9 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         4 $stdin_data = $buffer;
656             # JSON::Parse::assert_valid_json($buffer);
657             # my $paramref = JSON::Parse::parse_json($buffer);
658 1         10 my $paramref = decode_json($buffer);
659 1         4 foreach my $key(keys(%{$paramref})) {
  1         4  
660 2         11 push @pairs, "$key=" . $paramref->{$key};
661             }
662             }
663             } else {
664 1         3 my $buffer;
665 1 50       3 if($stdin_data) {
666 0         0 $buffer = $stdin_data;
667             } else {
668 1 50       8 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         4 $stdin_data = $buffer;
674             }
675              
676 1         9 $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         6 $self->{status} = 501;
690 2         12 $self->_warn({
691             warning => 'Use POST, GET or HEAD'
692             });
693             }
694              
695 53 100       484 unless(scalar @pairs) {
696 1         4 return;
697             }
698              
699 52         3676 require String::Clean::XSS;
700 52         124315 String::Clean::XSS->import();
701             # require String::EscapeCage;
702             # String::EscapeCage->import();
703              
704 52         135 foreach my $arg (@pairs) {
705 102         369 my($key, $value) = split(/=/, $arg, 2);
706              
707 102 100       269 next unless($key);
708              
709 98         196 $key =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  1         12  
710 98         194 $key =~ tr/+/ /;
711 98 50       220 if(defined($value)) {
712 98         160 $value =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  17         83  
713 98         172 $value =~ tr/+/ /;
714             } else {
715 0         0 $value = '';
716             }
717              
718 98         234 $key = _sanitise_input($key);
719              
720 98 100       314275 if($self->{allow}) {
721             # Is this a permitted argument?
722 25 100       82 if(!exists($self->{allow}->{$key})) {
723 11 50       28 if($self->{logger}) {
724 0         0 $self->{logger}->info("discard $key");
725             }
726 11         27 next;
727             }
728              
729             # Do we allow any value, or must it be validated?
730 14 100       45 if(defined($self->{allow}->{$key})) {
731 9 100       57 if($value !~ $self->{allow}->{$key}) {
732 7 50       21 if($self->{logger}) {
733 0         0 $self->{logger}->info("block $key = $value");
734             }
735 7         16 next;
736             }
737             }
738             }
739              
740 80 100 100 5   288 if($self->{expect} && (List::MoreUtils::none { $_ eq $key } @{$self->{expect}})) {
  5         31  
  5         27  
741 2         8 next;
742             }
743 78         171 $value = _sanitise_input($value);
744              
745 78 100 100     165934 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     1269 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       14 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         13 $self->status(403);
762 3         19 return;
763             }
764 64 50 33     331 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       153 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       195 if(length($value) > 0) {
781             # Don't add if it's already there
782 71 100 100     245 if($FORM{$key} && ($FORM{$key} ne $value)) {
783 5         23 $FORM{$key} .= ",$value";
784             } else {
785 66         280 $FORM{$key} = $value;
786             }
787             }
788             }
789              
790 49 100       121 unless(%FORM) {
791 9         47 return;
792             }
793              
794 40 100       111 if($self->{logger}) {
795 2         30 while(my ($key,$value) = each %FORM) {
796 4         40 $self->{logger}->debug("$key=$value");
797 4         40 $log->debug("$key=$value");
798             }
799             }
800              
801 40         104 $self->{paramref} = \%FORM;
802              
803 40         337 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 2762 my ($self, $field) = @_;
833              
834 26 100       72 if(!defined($field)) {
835 1         4 return $self->params();
836             }
837             # Is this a permitted argument?
838 25 100 100     97 if($self->{allow} && !exists($self->{allow}->{$field})) {
839 4         28 $self->_warn({
840             warning => "param: $field isn't in the allow list"
841             });
842 0         0 return;
843             }
844              
845 21 100       52 if(defined($self->params())) {
846 20         49 return $self->params()->{$field};
847             }
848 1         5 return;
849             }
850              
851             # Emit a warning message somewhere
852             sub _warn {
853 19     19   35 my $self = shift;
854              
855 19         34 my %params;
856 19 100       70 if(ref($_[0]) eq 'HASH') {
    50          
857 11         16 %params = %{$_[0]};
  11         59  
858             } elsif(scalar(@_) % 2 == 0) {
859 0         0 %params = @_;
860             } else {
861 8         18 $params{'warning'} = shift;
862             }
863              
864 19         46 my $warning = $params{'warning'};
865              
866 19 50       49 return unless($warning);
867 19 50       56 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       52 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       127 if($self->{logger}) {
    50          
887 0         0 $self->{logger}->warn($warning);
888             } elsif(!defined($self->{syslog})) {
889 19         276 Carp::carp($warning);
890             }
891             }
892              
893             sub _sanitise_input($) {
894 176     176   321 my $arg = shift;
895              
896             # Remove hacking attempts and spaces
897 176         382 $arg =~ s/[\r\n]//g;
898 176         322 $arg =~ s/\s+$//;
899 176         351 $arg =~ s/^\s//;
900              
901 176         240 $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         465 return convert_XSS($arg);
908             }
909              
910             sub _multipart_data {
911 4     4   11 my ($self, $args) = @_;
912              
913 4 50       11 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       10 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         35 while() {
927 44         109 chop(my $line = $_);
928 44         96 $line =~ s/[\r\n]//g;
929 44         174 $stdin_data .= "$line\n";
930             }
931 4 50       16 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         5 my $writing_file = 0;
940 4         8 my $key;
941             my $value;
942 4         7 my $in_header = 0;
943 4         8 my $fout;
944              
945 4         29 foreach my $line(split(/\n/, $stdin_data)) {
946 34 100       144 if($line =~ /^--\Q$boundary\E--$/) {
947 2         6 last;
948             }
949 32 100       134 if($line =~ /^--\Q$boundary\E$/) {
    100          
950 8 50       32 if($writing_file) {
    100          
951 0         0 close $fout;
952 0         0 $writing_file = 0;
953             } elsif(defined($key)) {
954 4         17 push(@pairs, "$key=$value");
955 4         9 $value = undef;
956             }
957 8         20 $in_header = 1;
958             } elsif($in_header) {
959 16 100       85 if(length($line) == 0) {
    100          
960 6         16 $in_header = 0;
961             } elsif($line =~ /^Content-Disposition: (.+)/i) {
962 8         25 my $field = $1;
963 8 50       46 if($field =~ /name="(.+?)"/) {
964 8         22 $key = $1;
965             }
966 8 100       42 if($field =~ /filename="(.+)?"/) {
967 4         10 my $filename = $1;
968 4 100       14 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         12 $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         32 my $full_path = File::Spec->catfile($self->{upload_dir}, $filename);
981 2 50       240 unless(open($fout, '>', $full_path)) {
982 0         0 $self->_warn("Can't open $full_path");
983             }
984 2         8 $writing_file = 1;
985 2         17 push(@pairs, "$key=$filename");
986             }
987             }
988             }
989             # TODO: handle Content-Type: text/plain, etc.
990             } else {
991 8 100       18 if($writing_file) {
992 4         62 print $fout "$line\n";
993             } else {
994 4         20 $value .= $line;
995             }
996             }
997             }
998              
999 2 50       9 if($writing_file) {
1000 2         107 close $fout;
1001             }
1002              
1003 2         29 return @pairs;
1004             }
1005              
1006             sub _create_file_name {
1007 2     2   7 my ($self, $args) = @_;
1008              
1009 2         9 return $$args{filename} . '_' . time;
1010             }
1011              
1012             # Untaint a filename. Regex from CGI::Untaint::Filenames
1013             sub _untaint_filename {
1014 35     35   92 my ($self, $args) = @_;
1015              
1016 35 50       201 if($$args{filename} =~ /(^[\w\+_\040\#\(\)\{\}\[\]\/\-\^,\.:;&%@\\~]+\$?$)/) {
1017 35         173 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 40     40 1 2189 my $self = shift;
1032              
1033 40 100       126 if(defined($self->{is_mobile})) {
1034 13         45 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       12 if($ch_ua_mobile eq '?1') {
1040 1         3 $self->{is_mobile} = 1;
1041 1         4 return 1;
1042             }
1043             }
1044 26 100       69 if($ENV{'HTTP_X_WAP_PROFILE'}) {
1045             # E.g. Blackberry
1046             # TODO: Check the sanity of this variable
1047 1         2 $self->{is_mobile} = 1;
1048 1         6 return 1;
1049             }
1050              
1051 25 100       75 if(my $agent = $ENV{'HTTP_USER_AGENT'}) {
1052 16 100       1967 if($agent =~ /.+(Android|iPhone).+/) {
1053 2         5 $self->{is_mobile} = 1;
1054 2         10 return 1;
1055             }
1056              
1057             # From http://detectmobilebrowsers.com/
1058 14 100 66     445 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         6 return 1;
1061             }
1062              
1063             # Save loading and calling HTTP::BrowserDetect
1064 13         30 my $remote = $ENV{'REMOTE_ADDR'};
1065 13 50 66     48 if(defined($remote) && $self->{cache}) {
1066 0         0 my $is_mobile = $self->{cache}->get("is_mobile/$remote/$agent");
1067 0 0       0 if(defined($is_mobile)) {
1068 0         0 $self->{is_mobile} = $is_mobile;
1069 0         0 return $is_mobile;
1070             }
1071             }
1072              
1073 13 100       36 unless($self->{browser_detect}) {
1074 7 50       12 if(eval { require HTTP::BrowserDetect; }) {
  7         2861  
1075 7         61345 HTTP::BrowserDetect->import();
1076 7         28 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1077             }
1078             }
1079 13 50       1261 if($self->{browser_detect}) {
1080 13         46 my $device = $self->{browser_detect}->device();
1081 13   66     128 my $is_mobile = (defined($device) && ($device =~ /blackberry|webos|iphone|ipod|ipad|android/i));
1082 13 50 33     45 if($self->{cache} && defined($remote)) {
1083 0         0 $self->{cache}->set("is_mobile/$remote/$agent", $is_mobile, '1 day');
1084             }
1085 13         32 $self->{is_mobile} = $is_mobile;
1086 13         54 return $is_mobile;
1087             }
1088             }
1089              
1090 9         40 return 0;
1091             }
1092              
1093             =head2 is_tablet
1094              
1095             Returns a boolean if the website is being viewed on a tablet such as an iPad.
1096              
1097             =cut
1098              
1099             sub is_tablet {
1100 6     6 1 32 my $self = shift;
1101              
1102 6 100       28 if(defined($self->{is_tablet})) {
1103 1         6 return $self->{is_tablet};
1104             }
1105              
1106 5 100 100     352 if($ENV{'HTTP_USER_AGENT'} && ($ENV{'HTTP_USER_AGENT'} =~ /.+(iPad|TabletPC).+/)) {
1107             # TODO: add others when I see some nice user_agents
1108 1         4 $self->{is_tablet} = 1;
1109             } else {
1110 4         10 $self->{is_tablet} = 0;
1111             }
1112              
1113 5         23 return $self->{is_tablet};
1114             }
1115              
1116             =head2 as_string
1117              
1118             Returns the parameters as a string, which is useful for debugging or
1119             generating keys for a cache.
1120              
1121             =cut
1122              
1123             sub as_string {
1124 35     35 1 15320 my $self = shift;
1125              
1126 35 100       103 unless($self->params()) {
1127 7         44 return '';
1128             }
1129              
1130 28         58 my %f = %{$self->params()};
  28         57  
1131              
1132 28         53 my $rc;
1133              
1134 28         129 foreach (sort keys %f) {
1135 40         81 my $value = $f{$_};
1136 40         101 $value =~ s/\\/\\\\/g;
1137 40         164 $value =~ s/(;|=)/\\$1/g;
1138 40 100       94 if(defined($rc)) {
1139 12         47 $rc .= ";$_=$value";
1140             } else {
1141 28         105 $rc = "$_=$value";
1142             }
1143             }
1144 28 100 66     151 if($rc && $self->{logger}) {
1145 1         8 $self->{logger}->debug("is_string: returning '$rc'");
1146             }
1147              
1148 28 50       250 return defined($rc) ? $rc : '';
1149             }
1150              
1151             =head2 protocol
1152              
1153             Returns the connection protocol, presumably 'http' or 'https', or undef if
1154             it can't be determined.
1155              
1156             =cut
1157              
1158             sub protocol {
1159 21     21 1 1911 my $self = shift;
1160              
1161 21 100 100     85 if($ENV{'SCRIPT_URI'} && ($ENV{'SCRIPT_URI'} =~ /^(.+):\/\/.+/)) {
1162 2         14 return $1;
1163             }
1164 19 100 100     59 if($ENV{'SERVER_PROTOCOL'} && ($ENV{'SERVER_PROTOCOL'} =~ /^HTTP\//)) {
1165 2         10 return 'http';
1166             }
1167              
1168 17         33 my $port = $ENV{'SERVER_PORT'};
1169 17 100       38 if(defined($port)) {
1170 14 50       2268 if(defined(my $name = getservbyport($port, 'tcp'))) {
    0          
    0          
1171 14 100       107 if($name =~ /https?/) {
    50          
1172 12         129 return $name;
1173             } elsif($name eq 'www') {
1174             # e.g. NetBSD and OpenBSD
1175 0         0 return 'http';
1176             }
1177             # Return an error, maybe missing something
1178             } elsif($port == 80) {
1179             # e.g. Solaris
1180 0         0 return 'http';
1181             } elsif($port == 443) {
1182 0         0 return 'https';
1183             }
1184             }
1185              
1186 5 50       12 if($ENV{'REMOTE_ADDR'}) {
1187 0         0 $self->_warn("Can't determine the calling protocol");
1188             }
1189 5         35 return;
1190             }
1191              
1192             =head2 tmpdir
1193              
1194             Returns the name of a directory that you can use to create temporary files
1195             in.
1196              
1197             The routine is preferable to L since CGI programs are
1198             often running on shared servers. Having said that, tmpdir will fall back
1199             to File::Spec->tmpdir() if it can't find somewhere better.
1200              
1201             If the parameter 'default' is given, then use that directory as a
1202             fall-back rather than the value in File::Spec->tmpdir().
1203             No sanity tests are done, so if you give the default value of
1204             '/non-existant', that will be returned.
1205              
1206             Tmpdir allows a reference of the options to be passed.
1207              
1208             use CGI::Info;
1209              
1210             my $info = CGI::Info->new();
1211             my $dir = $info->tmpdir(default => '/var/tmp');
1212             $dir = $info->tmpdir({ default => '/var/tmp' });
1213              
1214             # or
1215              
1216             my $dir = CGI::Info->tmpdir();
1217             =cut
1218              
1219             sub tmpdir {
1220 15     15 1 2150 my $self = shift;
1221 15 100       51 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  1         4  
1222              
1223 15         22 my $name = 'tmp';
1224 15 50       51 if($^O eq 'MSWin32') {
1225 0         0 $name = 'temp';
1226             }
1227              
1228 15         23 my $dir;
1229              
1230 15 100       43 if(!ref($self)) {
1231 3         9 $self = __PACKAGE__->new();
1232             }
1233              
1234 15 100 100     167 if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) {
1235 4         42 $dir = File::Spec->catdir($ENV{'C_DOCUMENT_ROOT'}, $name);
1236 4 100 66     105 if((-d $dir) && (-w $dir)) {
1237 2         11 return $self->_untaint_filename({ filename => $dir });
1238             }
1239 2         9 $dir = $ENV{'C_DOCUMENT_ROOT'};
1240 2 50 33     48 if((-d $dir) && (-w $dir)) {
1241 2         13 return $self->_untaint_filename({ filename => $dir });
1242             }
1243             }
1244 11 100 100     191 if($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) {
1245 1         20 $dir = File::Spec->catdir($ENV{'DOCUMENT_ROOT'}, File::Spec->updir(), $name);
1246 1 50 33     32 if((-d $dir) && (-w $dir)) {
1247 1         8 return $self->_untaint_filename({ filename => $dir });
1248             }
1249             }
1250 10 100       283 return $params{default} ? $params{default} : File::Spec->tmpdir();
1251             }
1252              
1253             =head2 rootdir
1254              
1255             Returns the document root. This is preferable to looking at DOCUMENT_ROOT
1256             in the environment because it will also work when we're not running as a CGI
1257             script, which is useful for script debugging.
1258              
1259             This can be run as a class or object method.
1260              
1261             use CGI::Info;
1262              
1263             print CGI::Info->rootdir();
1264              
1265             =cut
1266              
1267             sub rootdir {
1268 9 50 66 9 1 5328 if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) {
    100 100        
1269 0         0 return $ENV{'C_DOCUMENT_ROOT'};
1270             } elsif($ENV{'DOCUMENT_ROOT'} && (-d $ENV{'DOCUMENT_ROOT'})) {
1271 3         20 return $ENV{'DOCUMENT_ROOT'};
1272             }
1273 6         32 my $script_name = $0;
1274              
1275 6 50       54 unless(File::Spec->file_name_is_absolute($script_name)) {
1276 6         206 $script_name = File::Spec->rel2abs($script_name);
1277             }
1278 6 50       31 if($script_name =~ /.cgi\-bin.*/) { # kludge for outside CGI environment
1279 0         0 $script_name =~ s/.cgi\-bin.*//;
1280             }
1281 6 50       114 if(-f $script_name) { # More kludge
1282 6 50       29 if($^O eq 'MSWin32') {
1283 0 0       0 if($script_name =~ /(.+)\\.+?$/) {
1284 0         0 return $1;
1285             }
1286             } else {
1287 6 50       115 if($script_name =~ /(.+)\/.+?$/) {
1288 6         45 return $1;
1289             }
1290             }
1291             }
1292 0         0 return $script_name;
1293             }
1294              
1295             =head2 logdir
1296              
1297             Gets and sets the name of a directory that you can use to store logs in.
1298              
1299             =cut
1300              
1301             sub logdir {
1302 4     4 1 2382 my $self = shift;
1303 4         7 my $dir = shift;
1304              
1305 4 100       12 if(!ref($self)) {
1306 1         8 $self = __PACKAGE__->new();
1307             }
1308              
1309 4 100       11 if(defined($dir)) {
1310             # No sanity testing is done
1311 1         16 return $self->{logdir} = $dir;
1312             }
1313              
1314 3         20 foreach my $rc($self->{logdir}, $ENV{'LOGDIR'}, Sys::Path->logdir(), $self->tmpdir()) {
1315 9 100 66     175 if(defined($rc) && length($rc) && (-d $rc) && (-w $rc)) {
      100        
      66        
1316 3         38 $dir = $rc;
1317 3         7 last;
1318             }
1319             }
1320 3 50 33     16 carp("Can't determine logdir") if((!defined($dir)) || (length($dir) == 0));
1321 3   66     12 $self->{logdir} ||= $dir;
1322              
1323 3         29 return $dir;
1324             }
1325              
1326             =head2 is_robot
1327              
1328             Is the visitor a real person or a robot?
1329              
1330             use CGI::Info;
1331              
1332             my $info = CGI::Info->new();
1333             unless($info->is_robot()) {
1334             # update site visitor statistics
1335             }
1336              
1337             =cut
1338              
1339             sub is_robot {
1340 20     20 1 583 my $self = shift;
1341              
1342 20 100       63 if(defined($self->{is_robot})) {
1343 4         15 return $self->{is_robot};
1344             }
1345              
1346 16         29 my $agent = $ENV{'HTTP_USER_AGENT'};
1347 16         28 my $remote = $ENV{'REMOTE_ADDR'};
1348              
1349 16 100 100     64 unless($remote && $agent) {
1350             # Probably not running in CGI - assume real person
1351 7         25 return 0;
1352             }
1353              
1354 9 100       489 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/i) {
1355 3         8 $self->{is_robot} = 1;
1356 3         17 return 1;
1357             }
1358              
1359 6 100       24 if(my $referrer = $ENV{'HTTP_REFERER'}) {
1360             # https://agency.ohow.co/google-analytics-implementation-audit/google-analytics-historical-spam-list/
1361 2         14 my @crawler_lists = (
1362             'http://fix-website-errors.com',
1363             'http://keywords-monitoring-your-success.com',
1364             'http://free-video-tool.com',
1365             'http://magnet-to-torrent.com',
1366             'http://torrent-to-magnet.com',
1367             'http://dogsrun.net',
1368             'http://###.responsive-test.net',
1369             'http://uptime.com',
1370             'http://uptimechecker.com',
1371             'http://top1-seo-service.com',
1372             'http://fast-wordpress-start.com',
1373             'http://wordpress-crew.net',
1374             'http://dbutton.net',
1375             'http://justprofit.xyz',
1376             'http://video--production.com',
1377             'http://buttons-for-website.com',
1378             'http://buttons-for-your-website.com',
1379             'http://success-seo.com',
1380             'http://videos-for-your-business.com',
1381             'http://semaltmedia.com',
1382             'http://dailyrank.net',
1383             'http://uptimebot.net',
1384             'http://sitevaluation.org',
1385             'http://100dollars-seo.com',
1386             'http://forum69.info',
1387             'http://partner.semalt.com',
1388             'http://best-seo-offer.com',
1389             'http://best-seo-solution.com',
1390             'http://semalt.semalt.com',
1391             'http://semalt.com',
1392             'http://7makemoneyonline.com',
1393             'http://anticrawler.org',
1394             'http://baixar-musicas-gratis.com',
1395             'http://descargar-musica-gratis.net',
1396              
1397             # Mine
1398             'http://www.seokicks.de/robot.html',
1399             );
1400 2         5 $referrer =~ s/\\/_/g;
1401 2 50 66 3   22 if(($referrer =~ /\)/) || (List::MoreUtils::any { $_ =~ /^$referrer/ } @crawler_lists)) {
  3         27  
1402 2 50       6 if($self->{logger}) {
1403 2         9 $self->{logger}->debug("is_robot: blocked trawler $referrer");
1404             }
1405 2         10 $self->{is_robot} = 1;
1406 2         13 return 1;
1407             }
1408             }
1409              
1410 4         7 my $key;
1411              
1412 4 50       12 if($self->{cache}) {
1413 0         0 $key = "is_robot/$remote/$agent";
1414 0 0       0 if(defined(my $is_robot = $self->{cache}->get($key))) {
1415 0         0 $self->{is_robot} = $is_robot;
1416 0         0 return $is_robot;
1417             }
1418             }
1419              
1420 4 100       10 unless($self->{browser_detect}) {
1421 3 50       5 if(eval { require HTTP::BrowserDetect; }) {
  3         20  
1422 3         18 HTTP::BrowserDetect->import();
1423 3         7 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1424             }
1425             }
1426 4 50       596 if($self->{browser_detect}) {
1427 4         13 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     23 $is_robot = (defined($is_robot) && ($is_robot)) ? 1 : 0;
1432 4 100       14 if($self->{logger}) {
1433 2         9 $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, $is_robot, '1 day');
1439             }
1440 2         5 $self->{is_robot} = $is_robot;
1441 2         9 return $is_robot;
1442             }
1443             }
1444              
1445 2 50       6 if($self->{cache}) {
1446 0         0 $self->{cache}->set($key, 0, '1 day');
1447             }
1448 2         7 $self->{is_robot} = 0;
1449 2         9 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 27     27 1 556 my $self = shift;
1468              
1469 27 100       76 if(defined($self->{is_search_engine})) {
1470 8         33 return $self->{is_search_engine};
1471             }
1472              
1473 19         42 my $remote = $ENV{'REMOTE_ADDR'};
1474 19         36 my $agent = $ENV{'HTTP_USER_AGENT'};
1475              
1476 19 100 100     74 unless($remote && $agent) {
1477             # Probably not running in CGI - assume not a search engine
1478 8         28 return 0;
1479             }
1480              
1481 11         15 my $key;
1482              
1483 11 50       30 if($self->{cache}) {
1484 0         0 $key = "is_search/$remote/$agent";
1485              
1486 0         0 my $is_search = $self->{cache}->get($key);
1487 0 0       0 if(defined($is_search)) {
1488 0         0 $self->{is_search_engine} = $is_search;
1489 0         0 return $is_search;
1490             }
1491             }
1492              
1493             # Don't use HTTP_USER_AGENT to detect more than we really have to since
1494             # that is easily spoofed
1495 11 50       59 if($agent =~ /www\.majestic12\.co\.uk|facebookexternal/) {
1496 0 0       0 if($self->{cache}) {
1497 0         0 $self->{cache}->set($key, 1, '1 day');
1498             }
1499 0         0 return 1;
1500             }
1501              
1502 11 100       29 unless($self->{browser_detect}) {
1503 7 50       9 if(eval { require HTTP::BrowserDetect; }) {
  7         1010  
1504 7         20375 HTTP::BrowserDetect->import();
1505 7         21 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1506             }
1507             }
1508 11 50       1175 if(my $browser = $self->{browser_detect}) {
1509 11   66     28 my $is_search = ($browser->google() || $browser->msn() || $browser->baidu() || $browser->altavista() || $browser->yahoo() || $browser->bingbot());
1510 11 100 100     3033 if((!$is_search) && $agent =~ /SeznamBot\//) {
1511 1         3 $is_search = 1;
1512             }
1513 11 50       30 if($self->{cache}) {
1514 0         0 $self->{cache}->set($key, $is_search, '1 day');
1515             }
1516 11         22 $self->{is_search_engine} = $is_search;
1517 11         61 return $is_search;
1518             }
1519              
1520             # TODO: DNS lookup, not gethostbyaddr - though that will be slow
1521 0   0     0 my $hostname = gethostbyaddr(inet_aton($remote), AF_INET) || $remote;
1522              
1523 0 0 0     0 if(defined($hostname) && ($hostname =~ /google|msnbot|bingbot|amazonbot/) && ($hostname !~ /^google-proxy/)) {
      0        
1524 0 0       0 if($self->{cache}) {
1525 0         0 $self->{cache}->set($key, 1, '1 day');
1526             }
1527 0         0 $self->{is_search_engine} = 1;
1528 0         0 return 1;
1529             }
1530              
1531 0 0       0 if($self->{cache}) {
1532 0         0 $self->{cache}->set($key, 0, '1 day');
1533             }
1534 0         0 $self->{is_search_engine} = 0;
1535 0         0 return 0;
1536             }
1537              
1538             =head2 browser_type
1539              
1540             Returns one of 'web', 'search', 'robot' and 'mobile'.
1541              
1542             # Code to display a different web page for a browser, search engine and
1543             # smartphone
1544             use Template;
1545             use CGI::Info;
1546              
1547             my $info = CGI::Info->new();
1548             my $dir = $info->rootdir() . '/templates/' . $info->browser_type();
1549              
1550             my $filename = ref($self);
1551             $filename =~ s/::/\//g;
1552             $filename = "$dir/$filename.tmpl";
1553              
1554             if((!-f $filename) || (!-r $filename)) {
1555             die "Can't open $filename";
1556             }
1557             my $template = Template->new();
1558             $template->process($filename, {}) || die $template->error();
1559              
1560             =cut
1561              
1562             sub browser_type {
1563 23     23 1 51 my $self = shift;
1564              
1565 23 100       59 if($self->is_mobile()) {
1566 8         44 return 'mobile';
1567             }
1568 15 100       39 if($self->is_search_engine()) {
1569 7         41 return 'search';
1570             }
1571 8 100       28 if($self->is_robot()) {
1572 4         19 return 'robot';
1573             }
1574 4         23 return 'web';
1575             }
1576              
1577             =head2 get_cookie
1578              
1579             Returns a cookie's value, or undef if no name is given, or the requested
1580             cookie isn't in the jar.
1581              
1582             Deprecated - use cookie() instead.
1583              
1584             use CGI::Info;
1585              
1586             my $i = CGI::Info->new();
1587             my $name = $i->get_cookie(cookie_name => 'name');
1588             print "Your name is $name\n";
1589             my $address = $i->get_cookie('address');
1590             print "Your address is $address\n";
1591              
1592             =cut
1593              
1594             sub get_cookie {
1595 13     13 1 645 my $self = shift;
1596 13         22 my %params;
1597              
1598 13 100       49 if(ref($_[0]) eq 'HASH') {
    100          
1599 3         5 %params = %{$_[0]};
  3         12  
1600             } elsif(scalar(@_) % 2 == 0) {
1601 9         27 %params = @_;
1602             } else {
1603 1         5 $params{'cookie_name'} = shift;
1604             }
1605              
1606 13 100       33 if(!defined($params{'cookie_name'})) {
1607 3         11 $self->_warn('cookie_name argument not given');
1608 2         647 return;
1609             }
1610              
1611 10 100       27 unless($self->{jar}) {
1612 4 100       12 unless(defined($ENV{'HTTP_COOKIE'})) {
1613 1         6 return;
1614             }
1615 3         16 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1616              
1617 3         9 foreach my $cookie(@cookies) {
1618 11         25 my ($name, $value) = split(/=/, $cookie);
1619 11         29 $self->{jar}->{$name} = $value;
1620             }
1621             }
1622              
1623 9 100       24 if(exists($self->{jar}->{$params{'cookie_name'}})) {
1624 6         32 return $self->{jar}->{$params{'cookie_name'}};
1625             }
1626 3         13 return; # Return undef
1627             }
1628              
1629             =head2 cookie
1630              
1631             Returns a cookie's value, or undef if no name is given, or the requested
1632             cookie isn't in the jar.
1633             API is the same as "param", it will replace the "get_cookie" method in the future.
1634              
1635             use CGI::Info;
1636              
1637             my $name = CGI::Info->new()->cookie('name');
1638             print "Your name is $name\n";
1639              
1640             =cut
1641              
1642             sub cookie {
1643 2     2 1 6 my ($self, $field) = @_;
1644              
1645 2 50       6 if(!defined($field)) {
1646 0         0 $self->_warn('what cookie do you want?');
1647 0         0 return;
1648             }
1649              
1650 2 50       7 unless($self->{jar}) {
1651 0 0       0 unless(defined($ENV{'HTTP_COOKIE'})) {
1652 0         0 return;
1653             }
1654 0         0 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1655              
1656 0         0 foreach my $cookie(@cookies) {
1657 0         0 my ($name, $value) = split(/=/, $cookie);
1658 0         0 $self->{jar}->{$name} = $value;
1659             }
1660             }
1661              
1662 2 50       8 if(exists($self->{jar}->{$field})) {
1663 2         8 return $self->{jar}->{$field};
1664             }
1665 0         0 return; # Return undef
1666             }
1667              
1668             =head2 status
1669              
1670             Sets or returns the status of the object, 200 for OK, otherwise an HTTP error code
1671              
1672             =cut
1673              
1674             sub status {
1675 12     12 1 2877 my $self = shift;
1676              
1677 12 100       52 if(my $status = shift) {
    100          
1678 4         10 $self->{status} = $status;
1679             } elsif(!defined($self->{status})) {
1680 5 100       17 if(defined(my $method = $ENV{'REQUEST_METHOD'})) {
1681 4 100 66     37 if(($method eq 'OPTIONS') || ($method eq 'DELETE')) {
    100 66        
1682 1         7 return 405;
1683             } elsif(($method eq 'POST') && !defined($ENV{'CONTENT_LENGTH'})) {
1684 1         4 return 411;
1685             }
1686             }
1687 3         17 return 200;
1688             }
1689              
1690 7   50     35 return $self->{status} || 200;
1691             }
1692              
1693             =head2 set_logger
1694              
1695             Sometimes you don't know what the logger is until you've instantiated the class.
1696             This function fixes the catch22 situation.
1697              
1698             =cut
1699              
1700             sub set_logger {
1701 3     3 1 63 my $self = shift;
1702 3         4 my %params;
1703              
1704 3 100       17 if(ref($_[0]) eq 'HASH') {
    100          
1705 1         1 %params = %{$_[0]};
  1         5  
1706             } elsif(scalar(@_) % 2 == 0) {
1707 1         4 %params = @_;
1708             } else {
1709 1         3 $params{'logger'} = shift;
1710             }
1711              
1712 3         7 $self->{logger} = $params{'logger'};
1713              
1714 3         8 return $self;
1715             }
1716              
1717             =head2 reset
1718              
1719             Class method to reset the class.
1720             You should do this in an FCGI environment before instantiating, but nowhere else.
1721              
1722             =cut
1723              
1724             sub reset {
1725 11     11 1 10943 my $class = shift;
1726              
1727 11 100       38 unless($class eq __PACKAGE__) {
1728 1         26 carp('Reset is a class method');
1729 0         0 return;
1730             }
1731              
1732 10         29 $stdin_data = undef;
1733             }
1734              
1735             sub AUTOLOAD {
1736 157     157   64381 our $AUTOLOAD;
1737 157         305 my $param = $AUTOLOAD;
1738              
1739 157         1007 $param =~ s/.*:://;
1740              
1741 157 100       3259 return if($param eq 'DESTROY');
1742              
1743 4         8 my $self = shift;
1744              
1745 4 50       16 return if(ref($self) ne __PACKAGE__);
1746              
1747 4         15 return $self->param($param);
1748             }
1749              
1750             =head1 AUTHOR
1751              
1752             Nigel Horne, C<< >>
1753              
1754             =head1 BUGS
1755              
1756             is_tablet() only currently detects the iPad and Windows PCs. Android strings
1757             don't differ between tablets and smart-phones.
1758              
1759             Please report any bugs or feature requests to C,
1760             or through the web interface at
1761             L.
1762             I will be notified, and then you'll
1763             automatically be notified of progress on your bug as I make changes.
1764              
1765             params() returns a ref which means that calling routines can change the hash
1766             for other routines.
1767             Take a local copy before making amendments to the table if you don't want unexpected
1768             things to happen.
1769              
1770             =head1 SEE ALSO
1771              
1772             L
1773              
1774             =head1 SUPPORT
1775              
1776             You can find documentation for this module with the perldoc command.
1777              
1778             perldoc CGI::Info
1779              
1780             You can also look for information at:
1781              
1782             =over 4
1783              
1784             =item * MetaCPAN
1785              
1786             L
1787              
1788             =item * RT: CPAN's request tracker
1789              
1790             L
1791              
1792             =item * CPAN Testers' Matrix
1793              
1794             L
1795              
1796             =item * CPAN Testers Dependencies
1797              
1798             L
1799              
1800             =back
1801              
1802             =head1 LICENSE AND COPYRIGHT
1803              
1804             Copyright 2010-2023 Nigel Horne.
1805              
1806             This program is released under the following licence: GPL2
1807              
1808             =cut
1809              
1810             1;