File Coverage

blib/lib/CGI/Info.pm
Criterion Covered Total %
statement 584 694 84.1
branch 360 480 75.0
condition 127 200 63.5
subroutine 44 44 100.0
pod 24 24 100.0
total 1139 1442 78.9


line stmt bran cond sub pod time code
1             package CGI::Info;
2              
3             # TODO: remove the expect argument
4              
5 22     22   3325196 use warnings;
  22         238  
  22         725  
6 22     22   120 use strict;
  22         64  
  22         415  
7 22     22   110 use Carp;
  22         62  
  22         1222  
8 22     22   143 use File::Spec;
  22         55  
  22         692  
9 22     22   12403 use Socket; # For AF_INET
  22         80273  
  22         9110  
10 22     22   467 use 5.008;
  22         80  
11 22     22   9674 use Log::Any qw($log);
  22         181986  
  22         133  
12             # use Cwd;
13             # use JSON::Parse;
14 22     22   59230 use List::MoreUtils; # Can go when expect goes
  22         304396  
  22         143  
15             # use Sub::Private;
16 22     22   31837 use Sys::Path;
  22         598542  
  22         814  
17              
18 22     22   10250 use namespace::clean;
  22         348122  
  22         160  
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.78
29              
30             =cut
31              
32             our $VERSION = '0.78';
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 155     155 1 107895 my $class = $_[0];
79              
80 155         244 shift;
81 155 100       541 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  3         13  
82              
83 155 100 100     687 if($args{expect} && (ref($args{expect}) ne 'ARRAY')) {
84 1         13 warn __PACKAGE__, ': expect must be a reference to an array';
85 1         75 return;
86             }
87              
88 154 100       578 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         51  
98             }
99              
100 150         528 my %defaults = (
101             max_upload_size => 512 * 1024,
102             allow => undef,
103             expect => undef,
104             upload_dir => undef
105             );
106              
107 150         878 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 561 my $self = shift;
126              
127 15 100       41 unless($self->{script_name}) {
128 9         20 $self->_find_paths();
129             }
130 15         130 return $self->{script_name};
131             }
132              
133             sub _find_paths {
134 15     15   24 my $self = shift;
135              
136 15         82 require File::Basename;
137 15         468 File::Basename->import();
138              
139 15 100       55 if($ENV{'SCRIPT_NAME'}) {
140 11         326 $self->{script_name} = File::Basename::basename($ENV{'SCRIPT_NAME'});
141             } else {
142 4         220 $self->{script_name} = File::Basename::basename($0);
143             }
144             $self->{script_name} = $self->_untaint_filename({
145             filename => $self->{script_name}
146 15         87 });
147              
148 15 100 66     137 if($ENV{'SCRIPT_FILENAME'}) {
    100 66        
    100          
    50          
149 1         5 $self->{script_path} = $ENV{'SCRIPT_FILENAME'};
150             } elsif($ENV{'SCRIPT_NAME'} && $ENV{'DOCUMENT_ROOT'}) {
151 5         11 my $script_name = $ENV{'SCRIPT_NAME'};
152 5 100       18 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     239 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         24 require Cwd;
164 4         97 Cwd->import;
165              
166 4         12 my $script_name = $ENV{'SCRIPT_NAME'};
167 4 100       19 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         101 $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         159 $self->{script_path} = File::Spec->rel2abs($0);
179             }
180              
181             $self->{script_path} = $self->_untaint_filename({
182             filename => $self->{script_path}
183 15         92 });
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 13477 my $self = shift;
207              
208 22 100       68 unless($self->{script_path}) {
209 5         15 $self->_find_paths();
210             }
211 22         220 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 35 my $self = shift;
229              
230 11 100       32 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       35 if($^O eq 'MSWin32') {
236 0 0       0 if($self->{script_path} =~ /(.+)\\.+?$/) {
237 0         0 return $1;
238             }
239             } else {
240 11 50       76 if($self->{script_path} =~ /(.+)\/.+?$/) {
241 11         124 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 912 my $self = shift;
270              
271 8 100       23 unless($self->{site}) {
272 2         6 $self->_find_site_details();
273             }
274              
275 8         48 return $self->{site};
276             }
277              
278             sub _find_site_details {
279 7     7   13 my $self = shift;
280              
281 7 100       20 if($self->{logger}) {
282 4         14 $self->{logger}->trace('Entering _find_site_details');
283             }
284 7 50 66     39 if($self->{site} && $self->{cgi_site}) {
285 2         3 return;
286             }
287              
288 5         519 require URI::Heuristic;
289 5         2010 URI::Heuristic->import;
290              
291 5 100       20 if($ENV{'HTTP_HOST'}) {
    100          
292 1         4 $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       32 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     61 if(defined($self->protocol()) && ($self->protocol() ne 'http')) {
301 1         12 $self->{cgi_site} =~ s/^http//;
302 1         4 $self->{cgi_site} = $self->protocol() . $self->{cgi_site};
303             }
304             } else {
305 1         6 require Sys::Hostname;
306 1         24 Sys::Hostname->import;
307              
308 1 50       8 if($self->{logger}) {
309 1         8 $self->{logger}->debug('Falling back to using hostname');
310             }
311              
312 1         7 $self->{cgi_site} = Sys::Hostname::hostname();
313             }
314              
315 5 50       18 unless($self->{site}) {
316 5         10 $self->{site} = $self->{cgi_site};
317             }
318 5 100       22 if($self->{site} =~ /^https?:\/\/(.+)/) {
319 4         11 $self->{site} = $1;
320             }
321 5 100       18 unless($self->{cgi_site} =~ /^https?:\/\//) {
322 1         3 my $protocol = $self->protocol();
323              
324 1 50       4 unless($protocol) {
325 0         0 $protocol = 'http';
326             }
327 1         9 $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       13 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 15 my $self = shift;
346              
347 5 100       14 if($self->{domain}) {
348 3         12 return $self->{domain};
349             }
350 2         5 $self->_find_site_details();
351              
352 2 50       9 if($self->{site}) {
353 2         14 $self->{domain} = $self->{site};
354 2 100       11 if($self->{domain} =~ /^www\.(.+)/) {
355 1         5 $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         52 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 6682 my $self = shift;
480              
481 162 100       449 my %args = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  4         17  
482              
483 162 100 66     630 if((defined($self->{paramref})) && ((!defined($args{'allow'})) || defined($self->{allow}) && ($args{'allow'} eq $self->{allow}))) {
      66        
484 93         440 return $self->{paramref};
485             }
486              
487 69 100       173 if(defined($args{allow})) {
488 4         9 $self->{allow} = $args{allow};
489             }
490 69 100       150 if(defined($args{expect})) {
491 3 100       10 if(ref($args{expect}) eq 'ARRAY') {
492 2         4 $self->{expect} = $args{expect};
493             } else {
494 1         3 $self->_warn('expect must be a reference to an array');
495             }
496             }
497 69 100       811 if(defined($args{upload_dir})) {
498 1         10 $self->{upload_dir} = $args{upload_dir};
499             }
500 69 100       146 if(defined($args{logger})) {
501 1         4 $self->{logger} = $args{logger};
502             }
503 69 100       159 if($self->{logger}) {
504 2         12 $self->{logger}->trace('Entering params');
505             }
506              
507 69         119 my @pairs;
508 69         119 my $content_type = $ENV{'CONTENT_TYPE'};
509 69         125 my %FORM;
510              
511 69 100 66     479 if((!$ENV{'GATEWAY_INTERFACE'}) || (!$ENV{'REQUEST_METHOD'})) {
    100 100        
    100          
    50          
    100          
512 6 100       17 if(@ARGV) {
    50          
    50          
513 5         11 @pairs = @ARGV;
514 5 50       12 if(defined($pairs[0])) {
515 5 100       26 if($pairs[0] eq '--robot') {
    100          
    100          
    100          
516 1         5 $self->{is_robot} = 1;
517 1         3 shift @pairs;
518             } elsif($pairs[0] eq '--mobile') {
519 1         4 $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         2 shift @pairs;
524             } elsif($pairs[0] eq '--tablet') {
525 1         2 $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         4 my $oldfh = select(STDOUT);
533 1         309 print "Entering debug mode\n",
534             "Enter key=value pairs - end with quit\n";
535 1         13 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         29 chop(my $line = $_);
543 2         6 $line =~ s/[\r\n]//g;
544 2 100       10 last if $line eq 'quit';
545 1         4 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       129 if(my $query = $ENV{'QUERY_STRING'}) {
551 41 100 66     119 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         90 $query =~ s/\\u0026/\&/g;
555 40         131 @pairs = split(/&/, $query);
556             } else {
557 3         22 return;
558             }
559             } elsif($ENV{'REQUEST_METHOD'} eq 'POST') {
560 16 100       52 if(!defined($ENV{'CONTENT_LENGTH'})) {
561 2         8 $self->{status} = 411;
562 2         9 return;
563             }
564 14         33 my $content_length = $ENV{'CONTENT_LENGTH'};
565 14 50 33     140 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     119 if((!defined($content_type)) || ($content_type =~ /application\/x-www-form-urlencoded/)) {
    100          
    100          
    100          
574 3         7 my $buffer;
575 3 100       12 if($stdin_data) {
576 1         10 $buffer = $stdin_data;
577             } else {
578 2 100       17 if(read(STDIN, $buffer, $content_length) != $content_length) {
579 1         4 $self->_warn('POST failed: something else may have read STDIN');
580             }
581 2         348 $stdin_data = $buffer;
582             }
583 3         19 @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         5 $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       52 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       216 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       51 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       27 if($content_type =~ /boundary=(\S+)$/) {
619 4         26 @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         4 $stdin_data = $buffer;
635             }
636              
637 1         3 $FORM{XML} = $buffer;
638              
639 1         4 $self->{paramref} = \%FORM;
640              
641 1         7 return \%FORM;
642             } elsif($content_type =~ /application\/json/i) {
643 1         2 my $buffer;
644 1 50       2 if($stdin_data) {
645 0         0 $buffer = $stdin_data;
646             } else {
647 1         7 require JSON::MaybeXS;
648 1         31 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         3 $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         3 foreach my $key(keys(%{$paramref})) {
  1         3  
660 2         10 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       10 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         8 $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         18 $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         5 $self->{status} = 501;
690 2         11 $self->_warn({
691             warning => 'Use POST, GET or HEAD'
692             });
693             }
694              
695 53 100       474 unless(scalar @pairs) {
696 1         3 return;
697             }
698              
699 52         3372 require String::Clean::XSS;
700 52         120180 String::Clean::XSS->import();
701             # require String::EscapeCage;
702             # String::EscapeCage->import();
703              
704 52         146 foreach my $arg (@pairs) {
705 102         364 my($key, $value) = split(/=/, $arg, 2);
706              
707 102 100       273 next unless($key);
708              
709 98         229 $key =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  1         6  
710 98         214 $key =~ tr/+/ /;
711 98 50       209 if(defined($value)) {
712 98         163 $value =~ s/%([a-fA-F\d][a-fA-F\d])/pack("C", hex($1))/eg;
  17         67  
713 98         147 $value =~ tr/+/ /;
714             } else {
715 0         0 $value = '';
716             }
717              
718 98         213 $key = _sanitise_input($key);
719              
720 98 100       305384 if($self->{allow}) {
721             # Is this a permitted argument?
722 25 100       78 if(!exists($self->{allow}->{$key})) {
723 11 50       24 if($self->{logger}) {
724 0         0 $self->{logger}->info("discard $key");
725             }
726 11         24 next;
727             }
728              
729             # Do we allow any value, or must it be validated?
730 14 100       39 if(defined($self->{allow}->{$key})) {
731 9 100       47 if($value !~ $self->{allow}->{$key}) {
732 7 50       16 if($self->{logger}) {
733 0         0 $self->{logger}->info("block $key = $value");
734             }
735 7         17 next;
736             }
737             }
738             }
739              
740 80 100 100 5   260 if($self->{expect} && (List::MoreUtils::none { $_ eq $key } @{$self->{expect}})) {
  5         23  
  5         20  
741 2         8 next;
742             }
743 78         157 $value = _sanitise_input($value);
744              
745 78 100 100     163150 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     1236 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       8 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         18 return;
763             }
764 64 50 33     316 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       167 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       200 if(length($value) > 0) {
781             # Don't add if it's already there
782 71 100 100     233 if($FORM{$key} && ($FORM{$key} ne $value)) {
783 5         25 $FORM{$key} .= ",$value";
784             } else {
785 66         236 $FORM{$key} = $value;
786             }
787             }
788             }
789              
790 49 100       119 unless(%FORM) {
791 9         45 return;
792             }
793              
794 40 100       99 if($self->{logger}) {
795 2         18 while(my ($key,$value) = each %FORM) {
796 4         34 $self->{logger}->debug("$key=$value");
797 4         38 $log->debug("$key=$value");
798             }
799             }
800              
801 40         103 $self->{paramref} = \%FORM;
802              
803 40         278 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 2837 my ($self, $field) = @_;
833              
834 26 100       66 if(!defined($field)) {
835 1         5 return $self->params();
836             }
837             # Is this a permitted argument?
838 25 100 100     95 if($self->{allow} && !exists($self->{allow}->{$field})) {
839 4         20 $self->_warn({
840             warning => "param: $field isn't in the allow list"
841             });
842 0         0 return;
843             }
844              
845 21 100       49 if(defined($self->params())) {
846 20         48 return $self->params()->{$field};
847             }
848 1         6 return;
849             }
850              
851             # Emit a warning message somewhere
852             sub _warn {
853 19     19   68 my $self = shift;
854              
855 19         30 my %params;
856 19 100       78 if(ref($_[0]) eq 'HASH') {
    50          
857 11         17 %params = %{$_[0]};
  11         45  
858             } elsif(scalar(@_) % 2 == 0) {
859 0         0 %params = @_;
860             } else {
861 8         19 $params{'warning'} = shift;
862             }
863              
864 19         42 my $warning = $params{'warning'};
865              
866 19 50       45 return unless($warning);
867 19 50       58 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       45 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       78 if($self->{logger}) {
    50          
887 0         0 $self->{logger}->warn($warning);
888             } elsif(!defined($self->{syslog})) {
889 19         295 Carp::carp($warning);
890             }
891             }
892              
893             sub _sanitise_input($) {
894 176     176   313 my $arg = shift;
895              
896             # Remove hacking attempts and spaces
897 176         358 $arg =~ s/[\r\n]//g;
898 176         320 $arg =~ s/\s+$//;
899 176         347 $arg =~ s/^\s//;
900              
901 176         233 $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         435 return convert_XSS($arg);
908             }
909              
910             sub _multipart_data {
911 4     4   9 my ($self, $args) = @_;
912              
913 4 50       19 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       9 if($total_bytes == 0) {
922 0         0 return;
923             }
924              
925 4 50       10 unless($stdin_data) {
926 4         24 while() {
927 44         97 chop(my $line = $_);
928 44         77 $line =~ s/[\r\n]//g;
929 44         166 $stdin_data .= "$line\n";
930             }
931 4 50       12 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         7 my $key;
941             my $value;
942 4         6 my $in_header = 0;
943 4         5 my $fout;
944              
945 4         46 foreach my $line(split(/\n/, $stdin_data)) {
946 34 100       133 if($line =~ /^--\Q$boundary\E--$/) {
947 2         5 last;
948             }
949 32 100       112 if($line =~ /^--\Q$boundary\E$/) {
    100          
950 8 50       23 if($writing_file) {
    100          
951 0         0 close $fout;
952 0         0 $writing_file = 0;
953             } elsif(defined($key)) {
954 4         13 push(@pairs, "$key=$value");
955 4         9 $value = undef;
956             }
957 8         19 $in_header = 1;
958             } elsif($in_header) {
959 16 100       67 if(length($line) == 0) {
    100          
960 6         16 $in_header = 0;
961             } elsif($line =~ /^Content-Disposition: (.+)/i) {
962 8         21 my $field = $1;
963 8 50       49 if($field =~ /name="(.+?)"/) {
964 8         21 $key = $1;
965             }
966 8 100       34 if($field =~ /filename="(.+)?"/) {
967 4         9 my $filename = $1;
968 4 100       18 unless(defined($filename)) {
    50          
969 0         0 $self->_warn('No upload filename given');
970 0         0 } elsif($filename =~ /[\\\/\|]/) {
971 2         11 $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         28 my $full_path = File::Spec->catfile($self->{upload_dir}, $filename);
981 2 50       298 unless(open($fout, '>', $full_path)) {
982 0         0 $self->_warn("Can't open $full_path");
983             }
984 2         7 $writing_file = 1;
985 2         14 push(@pairs, "$key=$filename");
986             }
987             }
988             }
989             # TODO: handle Content-Type: text/plain, etc.
990             } else {
991 8 100       16 if($writing_file) {
992 4         31 print $fout "$line\n";
993             } else {
994 4         19 $value .= $line;
995             }
996             }
997             }
998              
999 2 50       9 if($writing_file) {
1000 2         2056 close $fout;
1001             }
1002              
1003 2         28 return @pairs;
1004             }
1005              
1006             sub _create_file_name {
1007 2     2   6 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   90 my ($self, $args) = @_;
1015              
1016 35 50       202 if($$args{filename} =~ /(^[\w\+_\040\#\(\)\{\}\[\]\/\-\^,\.:;&%@\\~]+\$?$)/) {
1017 35         160 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 1619 my $self = shift;
1032              
1033 38 100       112 if(defined($self->{is_mobile})) {
1034 11         34 return $self->{is_mobile};
1035             }
1036              
1037             # Support Sec-CH-UA-Mobile
1038 27 100       77 if(my $ch_ua_mobile = $ENV{'HTTP_SEC_CH_UA_MOBILE'}) {
1039 3 100       13 if($ch_ua_mobile eq '?1') {
1040 1         3 $self->{is_mobile} = 1;
1041 1         4 return 1;
1042             }
1043             }
1044 26 100       62 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         8 return 1;
1049             }
1050              
1051 25 100       66 if(my $agent = $ENV{'HTTP_USER_AGENT'}) {
1052 16 100       1990 if($agent =~ /.+(Android|iPhone).+/) {
1053 2         5 $self->{is_mobile} = 1;
1054 2         9 return 1;
1055             }
1056              
1057             # From http://detectmobilebrowsers.com/
1058 14 100 66     447 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         34 my $remote = $ENV{'REMOTE_ADDR'};
1065 13 50 66     44 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       32 unless($self->{browser_detect}) {
1072 7 50       12 if(eval { require HTTP::BrowserDetect; }) {
  7         2758  
1073 7         58535 HTTP::BrowserDetect->import();
1074 7         28 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1075             }
1076             }
1077 13 50       1184 if($self->{browser_detect}) {
1078 13         49 my $device = $self->{browser_detect}->device();
1079 13   66     118 my $is_mobile = (defined($device) && ($device =~ /blackberry|webos|iphone|ipod|ipad|android/i));
1080 13 50 66     37 if($is_mobile && $self->{cache} && defined($remote)) {
      33        
1081 0         0 $self->{cache}->set("$remote/$agent", 'mobile', '1 day');
1082             }
1083 13         64 return $self->{is_mobile} = $is_mobile;
1084             }
1085             }
1086              
1087 9         40 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 55 my $self = shift;
1098              
1099 6 100       22 if(defined($self->{is_tablet})) {
1100 1         4 return $self->{is_tablet};
1101             }
1102              
1103 5 100 100     349 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         22 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 14490 my $self = shift;
1122              
1123 35 100       97 unless($self->params()) {
1124 7         50 return '';
1125             }
1126              
1127 28         48 my %f = %{$self->params()};
  28         52  
1128              
1129 28         53 my $rc;
1130              
1131 28         115 foreach (sort keys %f) {
1132 40         76 my $value = $f{$_};
1133 40         87 $value =~ s/\\/\\\\/g;
1134 40         137 $value =~ s/(;|=)/\\$1/g;
1135 40 100       82 if(defined($rc)) {
1136 12         35 $rc .= ";$_=$value";
1137             } else {
1138 28         90 $rc = "$_=$value";
1139             }
1140             }
1141 28 100 66     142 if($rc && $self->{logger}) {
1142 1         6 $self->{logger}->debug("is_string: returning '$rc'");
1143             }
1144              
1145 28 50       222 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 1364 my $self = shift;
1157              
1158 21 100 100     79 if($ENV{'SCRIPT_URI'} && ($ENV{'SCRIPT_URI'} =~ /^(.+):\/\/.+/)) {
1159 2         12 return $1;
1160             }
1161 19 100 100     62 if($ENV{'SERVER_PROTOCOL'} && ($ENV{'SERVER_PROTOCOL'} =~ /^HTTP\//)) {
1162 2         11 return 'http';
1163             }
1164              
1165 17         31 my $port = $ENV{'SERVER_PORT'};
1166 17 100       34 if(defined($port)) {
1167 14 50       3414 if(defined(my $name = getservbyport($port, 'tcp'))) {
    0          
    0          
1168 14 100       102 if($name =~ /https?/) {
    50          
1169 12         88 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       13 if($ENV{'REMOTE_ADDR'}) {
1184 0         0 $self->_warn("Can't determine the calling protocol");
1185             }
1186 5         30 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 2299 my $self = shift;
1218 15 100       49 my %params = (ref($_[0]) eq 'HASH') ? %{$_[0]} : @_;
  1         4  
1219              
1220 15         25 my $name = 'tmp';
1221 15 50       47 if($^O eq 'MSWin32') {
1222 0         0 $name = 'temp';
1223             }
1224              
1225 15         19 my $dir;
1226              
1227 15 100       35 if(!ref($self)) {
1228 3         7 $self = __PACKAGE__->new();
1229             }
1230              
1231 15 100 100     170 if($ENV{'C_DOCUMENT_ROOT'} && (-d $ENV{'C_DOCUMENT_ROOT'})) {
1232 4         43 $dir = File::Spec->catdir($ENV{'C_DOCUMENT_ROOT'}, $name);
1233 4 100 66     96 if((-d $dir) && (-w $dir)) {
1234 2         12 return $self->_untaint_filename({ filename => $dir });
1235             }
1236 2         9 $dir = $ENV{'C_DOCUMENT_ROOT'};
1237 2 50 33     47 if((-d $dir) && (-w $dir)) {
1238 2         12 return $self->_untaint_filename({ filename => $dir });
1239             }
1240             }
1241 11 100 100     204 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     29 if((-d $dir) && (-w $dir)) {
1244 1         8 return $self->_untaint_filename({ filename => $dir });
1245             }
1246             }
1247 10 100       263 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 4976 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         20 return $ENV{'DOCUMENT_ROOT'};
1269             }
1270 6         24 my $script_name = $0;
1271              
1272 6 50       51 unless(File::Spec->file_name_is_absolute($script_name)) {
1273 6         199 $script_name = File::Spec->rel2abs($script_name);
1274             }
1275 6 50       33 if($script_name =~ /.cgi\-bin.*/) { # kludge for outside CGI environment
1276 0         0 $script_name =~ s/.cgi\-bin.*//;
1277             }
1278 6 50       104 if(-f $script_name) { # More kludge
1279 6 50       29 if($^O eq 'MSWin32') {
1280 0 0       0 if($script_name =~ /(.+)\\.+?$/) {
1281 0         0 return $1;
1282             }
1283             } else {
1284 6 50       44 if($script_name =~ /(.+)\/.+?$/) {
1285 6         37 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 2211 my $self = shift;
1300 4         8 my $dir = shift;
1301              
1302 4 100       11 if(!ref($self)) {
1303 1         4 $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         20 foreach my $rc($self->{logdir}, $ENV{'LOGDIR'}, Sys::Path->logdir(), $self->tmpdir()) {
1312 9 100 66     148 if(defined($rc) && length($rc) && (-d $rc) && (-w $rc)) {
      100        
      66        
1313 3         8 $dir = $rc;
1314 3         6 last;
1315             }
1316             }
1317 3 50 33     43 carp("Can't determine logdir") if((!defined($dir)) || (length($dir) == 0));
1318 3   66     15 $self->{logdir} ||= $dir;
1319              
1320 3         18 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 20     20 1 587 my $self = shift;
1338              
1339 20 100       62 if(defined($self->{is_robot})) {
1340 3         12 return $self->{is_robot};
1341             }
1342              
1343 17         30 my $agent = $ENV{'HTTP_USER_AGENT'};
1344 17         27 my $remote = $ENV{'REMOTE_ADDR'};
1345              
1346 17 100 100     62 unless($remote && $agent) {
1347             # Probably not running in CGI - assume real person
1348 7         21 return 0;
1349             }
1350              
1351 10 50 66     119 if(($agent =~ /SELECT.+AND.+/) || ($agent =~ /ORDER BY /) || ($agent =~ / OR NOT /) || ($agent =~ / AND \d+=\d+/) || ($agent =~ /THEN.+ELSE.+END/) || ($agent =~ /.+AND.+SELECT.+/)) {
      66        
      33        
      33        
      33        
1352 1         4 $self->status(403);
1353 1         2 $self->{is_robot} = 1;
1354 1 50       3 if($self->{logger}) {
1355 0 0       0 if($ENV{'REMOTE_ADDR'}) {
1356 0         0 $self->{logger}->warn($ENV{'REMOTE_ADDR'}, ": SQL injection attempt blocked for '$agent'");
1357             } else {
1358 0         0 $self->{logger}->warn("SQL injection attempt blocked for '$agent'");
1359             }
1360             }
1361 1         4 return 1;
1362             }
1363 9 100       493 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) {
1364 3         7 $self->{is_robot} = 1;
1365 3         13 return 1;
1366             }
1367              
1368 6         27 my $key = "$remote/$agent";
1369              
1370 6 100       20 if(my $referrer = $ENV{'HTTP_REFERER'}) {
1371             # https://agency.ohow.co/google-analytics-implementation-audit/google-analytics-historical-spam-list/
1372 2         14 my @crawler_lists = (
1373             'http://fix-website-errors.com',
1374             'http://keywords-monitoring-your-success.com',
1375             'http://free-video-tool.com',
1376             'http://magnet-to-torrent.com',
1377             'http://torrent-to-magnet.com',
1378             'http://dogsrun.net',
1379             'http://###.responsive-test.net',
1380             'http://uptime.com',
1381             'http://uptimechecker.com',
1382             'http://top1-seo-service.com',
1383             'http://fast-wordpress-start.com',
1384             'http://wordpress-crew.net',
1385             'http://dbutton.net',
1386             'http://justprofit.xyz',
1387             'http://video--production.com',
1388             'http://buttons-for-website.com',
1389             'http://buttons-for-your-website.com',
1390             'http://success-seo.com',
1391             'http://videos-for-your-business.com',
1392             'http://semaltmedia.com',
1393             'http://dailyrank.net',
1394             'http://uptimebot.net',
1395             'http://sitevaluation.org',
1396             'http://100dollars-seo.com',
1397             'http://forum69.info',
1398             'http://partner.semalt.com',
1399             'http://best-seo-offer.com',
1400             'http://best-seo-solution.com',
1401             'http://semalt.semalt.com',
1402             'http://semalt.com',
1403             'http://7makemoneyonline.com',
1404             'http://anticrawler.org',
1405             'http://baixar-musicas-gratis.com',
1406             'http://descargar-musica-gratis.net',
1407              
1408             # Mine
1409             'http://www.seokicks.de/robot.html',
1410             );
1411 2         4 $referrer =~ s/\\/_/g;
1412 2 50 66 3   15 if(($referrer =~ /\)/) || (List::MoreUtils::any { $_ =~ /^$referrer/ } @crawler_lists)) {
  3         25  
1413 2 50       6 if($self->{logger}) {
1414 2         8 $self->{logger}->debug("is_robot: blocked trawler $referrer");
1415             }
1416 2 50       12 if($self->{cache}) {
1417 0         0 $self->{cache}->set($key, 'robot', '1 day');
1418             }
1419 2         4 $self->{is_robot} = 1;
1420 2         13 return 1;
1421             }
1422             }
1423              
1424 4 50       10 if($self->{cache}) {
1425 0 0 0     0 if(defined($remote) && $self->{cache}) {
1426 0 0       0 if(my $type = $self->{cache}->get("$remote/$agent")) {
1427 0         0 return $self->{is_robot} = ($type eq 'robot');
1428             }
1429             }
1430             }
1431              
1432 4 100       12 unless($self->{browser_detect}) {
1433 3 50       5 if(eval { require HTTP::BrowserDetect; }) {
  3         19  
1434 3         11 HTTP::BrowserDetect->import();
1435 3         8 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1436             }
1437             }
1438 4 50       575 if($self->{browser_detect}) {
1439 4         20 my $is_robot = $self->{browser_detect}->robot();
1440 4 100 100     797 if(defined($is_robot) && $self->{logger}) {
1441 1         10 $self->{logger}->debug("HTTP::BrowserDetect '$ENV{HTTP_USER_AGENT}' returns $is_robot");
1442             }
1443 4 100 66     28 $is_robot = (defined($is_robot) && ($is_robot)) ? 1 : 0;
1444 4 100       14 if($self->{logger}) {
1445 2         8 $self->{logger}->debug("is_robot: $is_robot");
1446             }
1447              
1448 4 100       19 if($is_robot) {
1449 2 50       6 if($self->{cache}) {
1450 0         0 $self->{cache}->set($key, 'robot', '1 day');
1451             }
1452 2         5 $self->{is_robot} = $is_robot;
1453 2         11 return $is_robot;
1454             }
1455             }
1456              
1457 2 50       5 if($self->{cache}) {
1458 0         0 $self->{cache}->set($key, 'unknown', '1 day');
1459             }
1460 2         6 $self->{is_robot} = 0;
1461 2         9 return 0;
1462             }
1463              
1464             =head2 is_search_engine
1465              
1466             Is the visitor a search engine?
1467              
1468             use CGI::Info;
1469              
1470             if(CGI::Info->new()->is_search_engine()) {
1471             # display generic information about yourself
1472             } else {
1473             # allow the user to pick and choose something to display
1474             }
1475              
1476             =cut
1477              
1478             sub is_search_engine {
1479 25     25 1 531 my $self = shift;
1480              
1481 25 100       65 if(defined($self->{is_search_engine})) {
1482 6         26 return $self->{is_search_engine};
1483             }
1484              
1485 19         37 my $remote = $ENV{'REMOTE_ADDR'};
1486 19         32 my $agent = $ENV{'HTTP_USER_AGENT'};
1487              
1488 19 100 100     82 unless($remote && $agent) {
1489             # Probably not running in CGI - assume not a search engine
1490 8         27 return 0;
1491             }
1492              
1493 11         17 my $key;
1494              
1495 11 50       27 if($self->{cache}) {
1496 0         0 $key = "$remote/$agent";
1497 0 0 0     0 if(defined($remote) && $self->{cache}) {
1498 0 0       0 if(my $type = $self->{cache}->get("$remote/$agent")) {
1499 0         0 return $self->{is_search} = ($type eq 'search');
1500             }
1501             }
1502             }
1503              
1504             # Don't use HTTP_USER_AGENT to detect more than we really have to since
1505             # that is easily spoofed
1506 11 50       56 if($agent =~ /www\.majestic12\.co\.uk|facebookexternal/) {
1507 0 0       0 if($self->{cache}) {
1508 0         0 $self->{cache}->set($key, 'search', '1 day');
1509             }
1510 0         0 return 1;
1511             }
1512              
1513 11 100       24 unless($self->{browser_detect}) {
1514 7 50       11 if(eval { require HTTP::BrowserDetect; }) {
  7         969  
1515 7         19287 HTTP::BrowserDetect->import();
1516 7         20 $self->{browser_detect} = HTTP::BrowserDetect->new($agent);
1517             }
1518             }
1519 11 50       1227 if(my $browser = $self->{browser_detect}) {
1520 11   66     30 my $is_search = ($browser->google() || $browser->msn() || $browser->baidu() || $browser->altavista() || $browser->yahoo() || $browser->bingbot());
1521 11 100 100     2946 if((!$is_search) && $agent =~ /SeznamBot\//) {
1522 1         3 $is_search = 1;
1523             }
1524 11 50 66     44 if($is_search && $self->{cache}) {
1525 0         0 $self->{cache}->set($key, 'search', '1 day');
1526             }
1527 11         83 return $self->{is_search_engine} = $is_search;
1528             }
1529              
1530             # TODO: DNS lookup, not gethostbyaddr - though that will be slow
1531 0   0     0 my $hostname = gethostbyaddr(inet_aton($remote), AF_INET) || $remote;
1532              
1533 0 0 0     0 if(defined($hostname) && ($hostname =~ /google|msnbot|bingbot|amazonbot/) && ($hostname !~ /^google-proxy/)) {
      0        
1534 0 0       0 if($self->{cache}) {
1535 0         0 $self->{cache}->set($key, 'search', '1 day');
1536             }
1537 0         0 $self->{is_search_engine} = 1;
1538 0         0 return 1;
1539             }
1540              
1541 0         0 $self->{is_search_engine} = 0;
1542 0         0 return 0;
1543             }
1544              
1545             =head2 browser_type
1546              
1547             Returns one of 'web', 'search', 'robot' and 'mobile'.
1548              
1549             # Code to display a different web page for a browser, search engine and
1550             # smartphone
1551             use Template;
1552             use CGI::Info;
1553              
1554             my $info = CGI::Info->new();
1555             my $dir = $info->rootdir() . '/templates/' . $info->browser_type();
1556              
1557             my $filename = ref($self);
1558             $filename =~ s/::/\//g;
1559             $filename = "$dir/$filename.tmpl";
1560              
1561             if((!-f $filename) || (!-r $filename)) {
1562             die "Can't open $filename";
1563             }
1564             my $template = Template->new();
1565             $template->process($filename, {}) || die $template->error();
1566              
1567             =cut
1568              
1569             sub browser_type {
1570 21     21 1 55 my $self = shift;
1571              
1572 21 100       47 if($self->is_mobile()) {
1573 8         42 return 'mobile';
1574             }
1575 13 100       36 if($self->is_search_engine()) {
1576 6         40 return 'search';
1577             }
1578 7 100       21 if($self->is_robot()) {
1579 3         22 return 'robot';
1580             }
1581 4         16 return 'web';
1582             }
1583              
1584             =head2 get_cookie
1585              
1586             Returns a cookie's value, or undef if no name is given, or the requested
1587             cookie isn't in the jar.
1588              
1589             Deprecated - use cookie() instead.
1590              
1591             use CGI::Info;
1592              
1593             my $i = CGI::Info->new();
1594             my $name = $i->get_cookie(cookie_name => 'name');
1595             print "Your name is $name\n";
1596             my $address = $i->get_cookie('address');
1597             print "Your address is $address\n";
1598              
1599             =cut
1600              
1601             sub get_cookie {
1602 13     13 1 610 my $self = shift;
1603 13         19 my %params;
1604              
1605 13 100       45 if(ref($_[0]) eq 'HASH') {
    100          
1606 3         5 %params = %{$_[0]};
  3         9  
1607             } elsif(scalar(@_) % 2 == 0) {
1608 9         24 %params = @_;
1609             } else {
1610 1         2 $params{'cookie_name'} = shift;
1611             }
1612              
1613 13 100       36 if(!defined($params{'cookie_name'})) {
1614 3         9 $self->_warn('cookie_name argument not given');
1615 2         598 return;
1616             }
1617              
1618 10 100       23 unless($self->{jar}) {
1619 4 100       8 unless(defined($ENV{'HTTP_COOKIE'})) {
1620 1         5 return;
1621             }
1622 3         15 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1623              
1624 3         8 foreach my $cookie(@cookies) {
1625 11         26 my ($name, $value) = split(/=/, $cookie);
1626 11         41 $self->{jar}->{$name} = $value;
1627             }
1628             }
1629              
1630 9 100       21 if(exists($self->{jar}->{$params{'cookie_name'}})) {
1631 6         30 return $self->{jar}->{$params{'cookie_name'}};
1632             }
1633 3         14 return; # Return undef
1634             }
1635              
1636             =head2 cookie
1637              
1638             Returns a cookie's value, or undef if no name is given, or the requested
1639             cookie isn't in the jar.
1640             API is the same as "param", it will replace the "get_cookie" method in the future.
1641              
1642             use CGI::Info;
1643              
1644             my $name = CGI::Info->new()->cookie('name');
1645             print "Your name is $name\n";
1646              
1647             =cut
1648              
1649             sub cookie {
1650 2     2 1 8 my ($self, $field) = @_;
1651              
1652 2 50       6 if(!defined($field)) {
1653 0         0 $self->_warn('what cookie do you want?');
1654 0         0 return;
1655             }
1656              
1657 2 50       6 unless($self->{jar}) {
1658 0 0       0 unless(defined($ENV{'HTTP_COOKIE'})) {
1659 0         0 return;
1660             }
1661 0         0 my @cookies = split(/; /, $ENV{'HTTP_COOKIE'});
1662              
1663 0         0 foreach my $cookie(@cookies) {
1664 0         0 my ($name, $value) = split(/=/, $cookie);
1665 0         0 $self->{jar}->{$name} = $value;
1666             }
1667             }
1668              
1669 2 50       6 if(exists($self->{jar}->{$field})) {
1670 2         8 return $self->{jar}->{$field};
1671             }
1672 0         0 return; # Return undef
1673             }
1674              
1675             =head2 status
1676              
1677             Sets or returns the status of the object, 200 for OK, otherwise an HTTP error code
1678              
1679             =cut
1680              
1681             sub status {
1682 15     15 1 2514 my $self = shift;
1683              
1684 15 100       72 if(my $status = shift) {
    100          
1685 5         11 $self->{status} = $status;
1686             } elsif(!defined($self->{status})) {
1687 6 100       22 if(defined(my $method = $ENV{'REQUEST_METHOD'})) {
1688 4 100 66     34 if(($method eq 'OPTIONS') || ($method eq 'DELETE')) {
    100 66        
1689 1         5 return 405;
1690             } elsif(($method eq 'POST') && !defined($ENV{'CONTENT_LENGTH'})) {
1691 1         4 return 411;
1692             }
1693             }
1694 4         26 return 200;
1695             }
1696              
1697 9   50     41 return $self->{status} || 200;
1698             }
1699              
1700             =head2 set_logger
1701              
1702             Sometimes you don't know what the logger is until you've instantiated the class.
1703             This function fixes the catch22 situation.
1704              
1705             =cut
1706              
1707             sub set_logger {
1708 3     3 1 48 my $self = shift;
1709 3         6 my %params;
1710              
1711 3 100       14 if(ref($_[0]) eq 'HASH') {
    100          
1712 1         2 %params = %{$_[0]};
  1         4  
1713             } elsif(scalar(@_) % 2 == 0) {
1714 1         4 %params = @_;
1715             } else {
1716 1         2 $params{'logger'} = shift;
1717             }
1718              
1719 3         7 $self->{logger} = $params{'logger'};
1720              
1721 3         6 return $self;
1722             }
1723              
1724             =head2 reset
1725              
1726             Class method to reset the class.
1727             You should do this in an FCGI environment before instantiating, but nowhere else.
1728              
1729             =cut
1730              
1731             sub reset {
1732 11     11 1 11211 my $class = shift;
1733              
1734 11 100       36 unless($class eq __PACKAGE__) {
1735 1         21 carp('Reset is a class method');
1736 0         0 return;
1737             }
1738              
1739 10         29 $stdin_data = undef;
1740             }
1741              
1742             sub AUTOLOAD {
1743 158     158   63339 our $AUTOLOAD;
1744 158         302 my $param = $AUTOLOAD;
1745              
1746 158         985 $param =~ s/.*:://;
1747              
1748 158 100       3270 return if($param eq 'DESTROY');
1749              
1750 4         9 my $self = shift;
1751              
1752 4 50       13 return if(ref($self) ne __PACKAGE__);
1753              
1754 4         13 return $self->param($param);
1755             }
1756              
1757             =head1 AUTHOR
1758              
1759             Nigel Horne, C<< >>
1760              
1761             =head1 BUGS
1762              
1763             is_tablet() only currently detects the iPad and Windows PCs. Android strings
1764             don't differ between tablets and smart-phones.
1765              
1766             Please report any bugs or feature requests to C,
1767             or through the web interface at
1768             L.
1769             I will be notified, and then you'll
1770             automatically be notified of progress on your bug as I make changes.
1771              
1772             params() returns a ref which means that calling routines can change the hash
1773             for other routines.
1774             Take a local copy before making amendments to the table if you don't want unexpected
1775             things to happen.
1776              
1777             =head1 SEE ALSO
1778              
1779             L
1780              
1781             =head1 SUPPORT
1782              
1783             You can find documentation for this module with the perldoc command.
1784              
1785             perldoc CGI::Info
1786              
1787             You can also look for information at:
1788              
1789             =over 4
1790              
1791             =item * MetaCPAN
1792              
1793             L
1794              
1795             =item * RT: CPAN's request tracker
1796              
1797             L
1798              
1799             =item * CPAN Testers' Matrix
1800              
1801             L
1802              
1803             =item * CPAN Testers Dependencies
1804              
1805             L
1806              
1807             =back
1808              
1809             =head1 LICENSE AND COPYRIGHT
1810              
1811             Copyright 2010-2023 Nigel Horne.
1812              
1813             This program is released under the following licence: GPL2
1814              
1815             =cut
1816              
1817             1;