File Coverage

blib/lib/CGI/apacheSSI.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package CGI::apacheSSI;
2 2     2   27245 use strict;
  2         5  
  2         84  
3            
4             # use HTML::SimpleParse;
5 2     2   1916 use File::Spec::Functions; # catfile()
  2         1781  
  2         273  
6 2     2   3111 use FindBin;
  2         2659  
  2         89  
7 2     2   4294 use LWP::UserAgent;
  2         131489  
  2         79  
8 2     2   23 use HTTP::Response;
  2         3  
  2         48  
9 2     2   4944 use HTTP::Cookies;
  2         22171  
  2         134  
10 2     2   17 use URI;
  2         4  
  2         52  
11 2     2   848 use Date::Format;
  0            
  0            
12            
13             our $VERSION = '0.93';
14            
15             our $DEBUG = 0;
16            
17             sub import {
18             my($class,%args) = @_;
19             return unless exists $args{'autotie'};
20             $args{'filehandle'} = $args{'autotie'} =~ /::/ ? $args{'autotie'} : caller().'::'.$args{'autotie'};
21             no strict 'refs';
22             my $self = tie(*{$args{'filehandle'}},$class,%args);
23             return $self;
24             }
25            
26             my($gmt,$loc,$lmod);
27            
28             # NOTE: check for escaped \( or \), what should it do?
29             our $L; # used to return the brackets count
30             our $RE_parens_2C = qr/
31             ( # g1, everything inside the brackets, incl brackets
32             \(
33             ( (?: # g2, everything inside the brackets
34             (?{ $L = 1 }) # $L counts ('s inside pattern
35             (?:
36             (?:"[^"\\]* (?: \\.[^"\\]* )* ")
37             | (?:'[^'\\]* (?: \\.[^'\\]* )* ')
38             | (?:`[^`\\]* (?: \\.[^`\\]* )* `)
39             | (?:[^"'`)(])
40             | (?: \(
41             (?{ local $L=$L+1; }) # new set of nested parens
42             )
43             | (?: \)
44             (?{ local $L=$L-1; }) # close a set of nested parens
45             (?(?{ $L==0 })(?!)) # ...if there was no matching open paren...
46             )
47             )*
48             )* ) # end g2
49             \)
50             ) # end g1
51             /x;
52            
53             our $RE_quote_dbl_NC = qr/(?:"[^"\\]* (?: \\.[^"\\]* )* ")/x;
54             our $RE_quote_single_NC = qr/(?:'[^'\\]* (?: \\.[^'\\]* )* ')/x;
55             our $RE_quote_backtick_NC = qr/(?:`[^`\\]* (?: \\.[^`\\]* )* `)/x;
56             our $RE_all_quote_NC = qr/$RE_quote_dbl_NC|$RE_quote_single_NC|$RE_quote_backtick_NC/;
57             our $RE_all_no_quote_NC = qr/$RE_all_quote_NC|[^'"`]/;
58             our $RE_all_no_paren_NC = qr/$RE_all_quote_NC|[^()'"`]/;
59             our $RE_all_no_paren_noop_NC = qr/$RE_all_quote_NC | [^()'"`&\|] | &[^&] | \|[^\|]/x;
60             our $RE_single_quote_false_NC = qr/^ (?:\s*'')+\s* [']* $
61             |^ '? (?:\\')* $/x;
62             # empty, or 1+ unspaced single quotes, trivially false
63             # pairs of empty single quotes, false
64             # alternating backslash-single quotes, false
65            
66            
67             # apache's own, special way of quoting strings
68             our $RE_apache_expr_quote = qr/ (?:"(?:[^"\\]|[\\]+[^\\])*?")
69             |(?:'(?:[^'\\]|[\\]+[^\\])*?')
70             |(?:`(?:[^`\\]|[\\]+[^\\])*?`)
71             /x;
72            
73             # NOTE: quotes that would be openers which are immediately preceeded by \w are treated as \w
74             # NOTE: needs to be preceeded by \s or =, otherwise becomes part of token (parsing oddity with apache 2.2.22)
75             our $RE_apache_expr_quote_all = qr/ $RE_apache_expr_quote | [^'"`\s]/x;
76             our $RE_runaway = qr/ \s+ \w+['"`]\S*\s+[^'"`]+['"`]+ /x;
77             our $RE_token_NC = qr{[[:alpha:]]\S+? (?:\s+ $RE_apache_expr_quote_all*? )*? $RE_runaway? }x;
78            
79            
80             sub new {
81             my($class,%args) = @_;
82             my $self = bless {}, $class;
83            
84             $self->{'_handle'} = undef;
85             my $script_name = '';
86             if(exists $ENV{'SCRIPT_NAME'}) {
87             ($script_name) = $ENV{'SCRIPT_NAME'} =~ /([^\/]+)$/;
88             }
89            
90             tie $gmt, 'CGI::apacheSSI::Gmt', $self;
91             tie $loc, 'CGI::apacheSSI::Local', $self;
92             tie $lmod, 'CGI::apacheSSI::LMOD', $self;
93            
94             $ENV{'DOCUMENT_ROOT'} ||= '';
95             $self->{'_variables'} = {
96             DOCUMENT_URI => ($args{'DOCUMENT_URI'} || $ENV{'SCRIPT_NAME'}),
97             DATE_GMT => $gmt,
98             DATE_LOCAL => $loc,
99             LAST_MODIFIED => $lmod,
100             DOCUMENT_NAME => ($args{'DOCUMENT_NAME'} || $script_name),
101             DOCUMENT_ROOT => ($args{'DOCUMENT_ROOT'} || $ENV{DOCUMENT_ROOT}),
102             };
103            
104             $self->{'_config'} = { # NOTE: TODO: get these from apache config
105             errmsg => ($args{'errmsg'} || '[an error occurred while processing this directive]'),
106             sizefmt => ($args{'sizefmt'} || 'abbrev'),
107             timefmt => ($args{'timefmt'} || undef),
108             SSIUndefinedEcho => ($args{'SSIUndefinedEcho'} || '(none)'),
109             _verbose_errors => ($args{'_verbose_errors'} || 0) # NOTE: TODO: document this option
110             };
111            
112             $self->{_max_recursions} = $args{MAX_RECURSIONS} || 100; # no "infinite" loops
113             $self->{_recursions} = {};
114            
115             $self->{_cookie_jar} = $args{COOKIE_JAR} || HTTP::Cookies->new();
116            
117             $self->{'_in_if'} = 0;
118             $self->{'_suspend'} = [0];
119             $self->{'_seen_true'} = [1];
120            
121             return $self;
122             }
123            
124             sub TIEHANDLE {
125             my($class,%args) = @_;
126             my $self = $class->new(%args);
127             $self->{'_handle'} = do { local *STDOUT };
128             my $handle_to_tie = '';
129             if($args{'filehandle'} !~ /::/) {
130             $handle_to_tie = caller().'::'.$args{'filehandle'};
131             } else {
132             $handle_to_tie = $args{'filehandle'};
133             }
134             open($self->{'_handle'},'>&'.$handle_to_tie) or die "Failed to copy the filehandle ($handle_to_tie): $!";
135             return $self;
136             }
137            
138             sub PRINT {
139             my $self = shift;
140             print {$self->{'_handle'}} map { $self->process($_) } @_;
141             }
142            
143             sub PRINTF {
144             my $self = shift;
145             my $fmt = shift;
146             printf {$self->{'_handle'}} $fmt, map { $self->process($_) } @_;
147             }
148            
149             sub CLOSE {
150             my($self) = @_;
151             close $self->{'_handle'};
152             }
153            
154             sub SSI_WARN {
155             my($self,$msg) = @_;
156             warn ref($self)." warn: $msg\n";
157             }
158            
159             sub SSI_ERROR {
160             (my $self, $@) = @_;
161             warn ref($self)." error: $@\n";
162             return; # returning false here allows us to do one line error returns.
163             }
164            
165             sub SSI_ERROR_FLUSH {
166             my($self,$msg) = @_;
167             if ($msg) {$self->SSI_ERROR($msg);}
168             $msg=$@; # NOTE: DEBUG ONLY!
169             undef $@;
170             return "[SSI ERROR=[$msg]]" if $self->{'_config'}->{'_verbose_errors'}; # NOTE: DEBUG ONLY!
171             return $self->{'_config'}->{'errmsg'};
172             }
173            
174            
175            
176            
177             # NOTE: "if" allows expr="myexpr1" expr="myexpr2" where myexpr2 overwrites myexpr1.
178            
179             sub process { # NOTE: -- FIXME -- this fails if we comment out the tokens.. ie
180             # NOTE: -- FIXME -- this should fail if we have any open quotes (ie, the --> doesnt magically close the tag.. in apache 2.2 at least)
181             my($self,@shtml) = @_;
182             my $processed = '';
183            
184             # NOTE: FIXME: would this be easier with a global replace s///ge ?
185             @shtml = split(m/()/sx, join '',@shtml); # this will slurp up anything inside quotes, single or double
186            
187             my $count=0;
188             for my $token (@shtml) {
189             if($token =~ /^$/sx) {
190             $processed .= $self->_process_ssi_text($1);
191             } else {
192             next if $self->_suspended;
193             $processed .= $token;
194             }
195             }
196             return $processed;
197             }
198            
199            
200            
201             sub _process_ssi_text {
202             my($self,$text) = @_;
203            
204             # what's the first \S+?
205             if($text !~ s/^(\S+)\s*//)
206             { return $self->SSI_ERROR_FLUSH("failed to find method name at beginning of string: '$text'."); }
207            
208             my $method = $1;
209             if (! $self->can($method) )
210             { return $self->SSI_ERROR_FLUSH("unknown directive \"$method\" in parsed doc."); }
211            
212             # are we suspended?
213             return '' if($self->_suspended and $method !~ /^(?:if|else|elif|endif)\b/);
214            
215             my $res = $self->$method( $self->parse_args($text, $method) );
216             if ($@) { return $self->SSI_ERROR_FLUSH();}
217             return $res;
218             }
219            
220            
221            
222             # many thanks to HTML::SimpleParse, with a couple of modifications
223             sub parse_args {
224             my ($self, $str, $method) = @_;
225             my @returns;
226            
227             # Make sure we start searching at the beginning of the string
228             pos($str) = 0;
229            
230             while (1) {
231             next if $str =~ m/\G\s+/gc; # Get rid of leading whitespace
232            
233             if ( $str =~ m/\G
234             ([\w.-]+)\s*=\s* # the key
235             (?:
236             # ($RE_all_quote_NC) \s* # anything in quotes
237             ($RE_apache_expr_quote_all) \s* # anything in quotes
238             | # or
239             ([^\s>]*) \s* # anything else, without whitespace or >
240             )/gcx ) {
241             my ($key, $val) = ($1, $+);
242             # ----- NOTE: if $key is not "expr" trim the quotes..
243             # ----- (apache evaluates differently depending on the type of quotes)
244             if ($key ne "expr") {$val =~ s/^['"`]?(.*?)['"`]?$/$1/;}
245             push @returns, $key, $val;
246             } elsif ( $str =~ m,\G/?([\w.-]+)\s*,gc ) {
247             push @returns, $1 , undef;
248             } else {
249             if ($str =~ m/\G(.+)/gc) # anything left over??
250             {
251             $self->SSI_ERROR("missing argument name for value to tag \"$method\" in");
252             # NOTE: notice this is NOT a "return".. we want processing to continue normally
253             }
254             last;
255             }
256             }
257            
258             # too many arguments for if element in
259             # else/endif/printenv directive does not take tags in
260             my %allowed_tag_count; # NOTE: this needs to be moved up
261             $allowed_tag_count{'if'}=["expr"];
262             $allowed_tag_count{'else'}=[];
263            
264             if (defined $allowed_tag_count{$method})
265             {
266             if (@returns > 2 * @{ $allowed_tag_count{$method} })
267             {
268             if (@{ $allowed_tag_count{$method} } == 0)
269             { $self->SSI_ERROR("\"$method\" directive does not take tags in");}
270             else
271             { $self->SSI_ERROR("too many arguments for \"$method\" element in");}
272             }
273             elsif (@returns < 2 * @{ $allowed_tag_count{$method} })
274             { $self->SSI_ERROR("missing arguments for directive \"$method\"");} # NOTE: fix this error message
275             }
276            
277             return @returns;
278             }
279            
280            
281             sub _interp_vars {
282             local $^W = 0;
283             my($self,$text,$setcmd) = @_;
284            
285             # NOTE: var name in ${} MUST start with at least one \w
286             $text =~ s{ ((\\*) ((\\)|(\$)) (\{)?(\w (?(6)(.*)\}|(\w*)) )) }
287             {
288             my ($all,$slashes, $slash,$dollar, $lbrak,$var)=($1,$2, $4,$5, $6,$7);
289             $slashes .= $slash; # NOTE: this can be improved
290             if ($lbrak) {chop $var};
291            
292             if (! $setcmd)
293             { chop($slashes); }
294            
295             if ($dollar && ! $slashes)
296             { $var = $self->_echo($var); }
297             else
298             {
299             $var = "{$var}" if ($lbrak) ;
300             $var = $dollar.$var;
301             }
302             $slashes.$var
303             }exg;
304            
305             return $text;
306             }
307            
308            
309            
310             # for internal use only - returns the thing passed in if it's not defined. echo() returns '' in that case.
311             sub _echo {
312             my($self,$key,$var) = @_;
313             $var = $key if @_ == 2;
314            
315             if($var eq 'DATE_LOCAL') {
316             return $loc;
317             } elsif($var eq 'DATE_GMT') {
318             return $gmt;
319             } elsif($var eq 'LAST_MODIFIED') {
320             return $lmod;
321             }
322            
323             return $self->{'_variables'}->{$var} if exists $self->{'_variables'}->{$var};
324             return $ENV{$var} if exists $ENV{$var};
325             return '';
326             }
327            
328             #
329             # ssi directive methods
330             #
331            
332             sub config {
333             my($self,$type,$value) = @_;
334             if($type =~ /^timefmt$/i) {
335             $self->{'_config'}->{'timefmt'} = $value;
336             } elsif($type =~ /^sizefmt$/i) {
337             if(lc $value eq 'abbrev') {
338             $self->{'_config'}->{'sizefmt'} = 'abbrev';
339             } elsif(lc $value eq 'bytes') {
340             $self->{'_config'}->{'sizefmt'} = 'bytes';
341             } else {
342             return $self->SSI_ERROR_FLUSH("value for sizefmt is '$value'. It must be 'abbrev' or 'bytes'.");
343             }
344             } elsif($type =~ /^errmsg$/i) {
345             $self->{'_config'}->{'errmsg'} = $value;
346             } elsif($type =~ /^_verbose_errors/i) {
347             $self->{'_config'}->{'_verbose_errors'} = $value;
348             } else {
349             return $self->SSI_ERROR_FLUSH("arg to config is '$type'. It must be one of: 'timefmt', 'sizefmt', or 'errmsg'.");
350             }
351             return '';
352             }
353            
354             sub set {
355             my($self,%args) = @_;
356             if(scalar keys %args > 1) {
357             $self->{'_variables'}->{$args{'var'}} = $self->_interp_vars($args{'value'}, 1);
358             } else { # var => value notation
359             my($var,$value) = %args;
360             $self->{'_variables'}->{$var} = $self->_interp_vars($value, 1);
361             }
362             return '';
363             }
364            
365             sub escaped {
366             my ($t)=@_;
367             $t =~ s/\\\$/\$/g;
368             return $t ;
369             }
370            
371             sub echo {
372             my($self,$key,$var) = @_;
373             $var = $key if @_ == 2;
374             my $encoding;
375             if ($key eq 'encoding') {
376             $encoding = $var; # NOTE: TODO: handle encoding.
377             ($key,$var) = @_[3,4];
378             $var = $key if (!$var);
379             }
380            
381             if($var eq 'DATE_LOCAL') {
382             return $loc;
383             } elsif($var eq 'DATE_GMT') {
384             return $gmt;
385             } elsif($var eq 'LAST_MODIFIED') {
386             return $lmod;
387             }
388             # it seems apache's "echo" command escapes out instances of "\$" to display just "$"
389             return &escaped($self->{'_variables'}->{$var}) if exists $self->{'_variables'}->{$var};
390             return &escaped($ENV{$var}) if exists $ENV{$var};
391             return $self->{'_config'}->{'SSIUndefinedEcho'};
392             }
393            
394             sub printenv {
395             return join "\n",map {"$_=$ENV{$_}"} keys %ENV;
396             }
397            
398             sub include {
399             $DEBUG and do { local $" = "','"; warn "DEBUG: include('@_')\n" };
400             my($self,$type,$filename) = @_;
401             if(lc $type eq 'file') {
402             return $self->_include_file($filename);
403             } elsif(lc $type eq 'virtual') {
404             return $self->_include_virtual($filename);
405             } else {
406             return $self->SSI_ERROR_FLUSH("arg to include is '$type'. It must be one of: 'file' or 'virtual'.");
407             }
408             }
409            
410             sub _include_file {
411             $DEBUG and do { local $" = "','"; warn "DEBUG: _include_file('@_')\n" };
412             my($self,$filename) = @_;
413            
414             # get the filename to open
415             $filename = catfile($FindBin::Bin,$filename) unless -e $filename;
416            
417             # if we've reached MAX_RECURSIONS for this filename, warn and return the error
418             if(++$self->{_recursions}->{$filename} >= $self->{_max_recursions}) {
419             return $self->SSI_ERROR_FLUSH("the maximum number of 'include file' recursions has been exceeded for '$filename'.");
420             }
421            
422             # open the file, or warn and return an error
423             my $fh = do { local *STDIN };
424             open($fh,$filename) or do {
425             return $self->SSI_ERROR_FLUSH("failed to open file ($filename): $!");
426             };
427            
428             # process the included file and return the result
429             return $self->process(join '',<$fh>);
430             }
431            
432             sub _include_virtual {
433             $DEBUG and do { local $" = "','"; warn "DEBUG: _include_virtual('@_')\n" };
434             my($self,$filename) = @_;
435            
436             # if this is a local file that we can just read, let's do that instead of getting it virtually
437             if($filename =~ m|^/(.+)|) { # could be on the local server: absolute filename, relative to ., relative to $ENV{DOCUMENT_ROOT}
438             my $file = $1;
439             if(-e '/'.$file) { # back to the original
440             $file = '/'.$file;
441             } elsif(-e catfile($self->{'_variables'}->{'DOCUMENT_ROOT'},$file)) {
442             $file = catfile($self->{'_variables'}->{'DOCUMENT_ROOT'},$file);
443             } elsif(-e catfile($FindBin::Bin,$file)) {
444             # $file = atfile($FindBin::Bin,$file); # <----- NOTE: is this a typo here??
445             $file = catfile($FindBin::Bin,$file); # fixing it just in case
446             }
447             return $self->_include_file($file) if -e $file;
448             }
449            
450             # create the URI to get(), or warn and return the error
451             my $uri = eval {
452             my $uri = URI->new($filename);
453             $uri->scheme($uri->scheme || ($ENV{HTTPS} ? 'https' : 'http')); # ??
454             $uri->host($uri->host || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'} || 'localhost');
455             $uri;
456             } or do {
457             return $self->SSI_ERROR_FLUSH("failed to create a URI based on '$filename'.");
458             };
459             if($@) {
460             return $self->SSI_ERROR_FLUSH("failed to create a URI based on '$filename'.");
461             }
462            
463             # get the content of the request
464             $self->{_ua} ||= $self->_get_ua();
465             my $url = $uri->canonical;
466            
467             # have we reached MAX_RECURSIONS?
468             if(++$self->{_recursions}->{$url} >= $self->{_max_recursions}) {
469             return $self->SSI_ERROR_FLUSH("the maximum number of 'include virtual' recursions has been exceeded for '$url'.");
470             }
471            
472             my $response = $self->{_ua}->get($url);
473            
474             # is it a success?
475             unless($response->is_success) {
476             return $self->SSI_ERROR_FLUSH("failed to get('$url'): ".$response->status_line.".");
477             }
478             # process the included content and return the result
479             return $self->process($response->content);
480             }
481            
482             sub _get_ua {
483             my $self = shift;
484             my %conf = ();
485             $conf{agent} = $ENV{HTTP_USER_AGENT} if $ENV{HTTP_USER_AGENT};
486             my $ua = LWP::UserAgent->new(%conf);
487             $ua->cookie_jar($self->{_cookie_jar});
488             return $ua;
489             }
490            
491             sub cookie_jar {
492             my $self = shift;
493             if(my $jar = shift) {
494             $self->{_cookie_jar} = $jar;
495             }
496             return $self->{_cookie_jar};
497             }
498            
499             sub exec {
500             my($self,$type,$filename) = @_;
501             if(lc $type eq 'cmd') {
502             return $self->_exec_cmd($filename);
503             } elsif(lc $type eq 'cgi') {
504             return $self->_exec_cgi($filename);
505             } else {
506             return $self->SSI_ERROR_FLUSH("arg to exec() is '$type'. It must be one of: 'cmd' or 'cgi'.");
507             }
508             }
509            
510             sub _exec_cmd {
511             my($self,$filename) = @_;
512            
513             # have we reached MAX_RECURSIONS?
514             if(++$self->{_recursions}->{$filename} >= $self->{_max_recursions}) {
515             return $self->SSI_ERROR_FLUSH("the maximum number of 'exec cmd' recursions has been exceeded for '$filename'.");
516             }
517            
518             my $output = `$filename`; # security here is mighty bad.
519            
520             # was the command a success?
521             if($?) {
522             return $self->SSI_ERROR_FLUSH("`$filename` was not successful.");
523             }
524            
525             # process the output, and return the result
526             return $self->process($output);
527             }
528            
529             sub _exec_cgi { # no relative $filename allowed.
530             my($self,$filename) = @_;
531            
532             # have we reached MAX_RECURSIONS?
533             if(++$self->{_recursions}->{$filename} >= $self->{_max_recursions}) {
534             return $self->SSI_ERROR_FLUSH("the maximum number of 'exec cgi' recursions has been exceeded for '$filename'.");
535             }
536            
537             # create the URI from the filename
538             my $uri = eval {
539             my $uri = URI->new($filename);
540             $uri->scheme($uri->scheme || ($ENV{HTTPS} ? 'https' : 'http')); # ??
541             $uri->host($uri->host || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'});
542             $uri->query($uri->query || $ENV{'QUERY_STRING'});
543             $uri;
544             } or do {
545             return $self->SSI_ERROR_FLUSH("failed to create a URI from '$filename'.");
546             };
547             if($@) {
548             return $self->SSI_ERROR_FLUSH("failed to create a URI from '$filename'.");
549             }
550            
551             # get the content
552             $self->{_ua} ||= $self->_get_ua();
553             my $url = $uri->canonical;
554             my $response = $self->{_ua}->get($url);
555            
556             # success?
557             unless($response->is_success) {
558             return $self->SSI_ERROR_FLUSH("failed to get('$filename').");
559             }
560            
561             # process the content and return the result
562             return $self->process($response->content);
563             }
564            
565             sub flastmod {
566             my($self,$type,$filename) = @_;
567            
568             if(lc $type eq 'file') {
569             $filename = catfile($FindBin::Bin,$filename) unless -e $filename;
570             } elsif(lc $type eq 'virtual') {
571             $filename = catfile($self->{'_variables'}->{'DOCUMENT_ROOT'},$filename)
572             unless $filename =~ /$self->{'_variables'}->{'DOCUMENT_ROOT'}/;
573             } else {
574             return $self->SSI_ERROR_FLUSH("the first argument to flastmod is '$type'. It must be one of: 'file' or 'virtual'.");
575             }
576            
577             unless(-e $filename) {
578             return $self->SSI_ERROR_FLUSH("flastmod failed to find '$filename'.");
579             }
580            
581             my $flastmod = (stat $filename)[9];
582            
583             if($self->{'_config'}->{'timefmt'}) {
584             my @localtime = localtime($flastmod); # need this??
585             return Date::Format::strftime($self->{'_config'}->{'timefmt'},@localtime);
586             } else {
587             return scalar localtime($flastmod);
588             }
589             }
590            
591             sub fsize {
592             my($self,$type,$filename) = @_;
593            
594             if(lc $type eq 'file') {
595             $filename = catfile($FindBin::Bin,$filename) unless -e $filename;
596             } elsif(lc $type eq 'virtual') {
597             $filename = catfile($ENV{'DOCUMENT_ROOT'},$filename) unless $filename =~ /$ENV{'DOCUMENT_ROOT'}/;
598             } else {
599             return $self->SSI_ERROR_FLUSH("the first argument to fsize is '$type'. It must be one of: 'file' or 'virtual'.");
600             }
601             unless(-e $filename) {
602             return $self->SSI_ERROR_FLUSH("fsize failed to find '$filename'.");
603             }
604            
605             my $fsize = (stat $filename)[7];
606            
607             if(lc $self->{'_config'}->{'sizefmt'} eq 'bytes') {
608             1 while $fsize =~ s/^(\d+)(\d{3})/$1,$2/g;
609             return $fsize;
610             } else { # abbrev
611             # gratefully lifted from Apache::SSI
612             return " 0k" unless $fsize;
613             return " 1k" if $fsize < 1024;
614             return sprintf("%4dk", ($fsize + 512)/1024) if $fsize < 1048576;
615             return sprintf("%4.1fM", $fsize/1048576.0) if $fsize < 103809024;
616             return sprintf("%4dM", ($fsize + 524288)/1048576) if $fsize < 1048576;
617             }
618             }
619            
620             #
621             # if/elsif/else/endif and related methods
622             #
623             # NOTE: anything calling _test should check $@
624             sub _test {
625             my($self,$test) = @_;
626             my $quote;
627             my ($pound, $pounds);
628            
629             $test =~ s/^(['"`])\s*(.*?)\s*(\1)$/$2/; # trim off surrounding (matching) quotes, and whitespace
630             $quote= $1;
631            
632             # trivial test returns:
633             return 0 if $test =~ /$RE_single_quote_false_NC/;
634             return 1 if $test =~ /^["`]+$/; # 1+ double quotes or backticks, trivially true
635             return 1 if $test =~ /^[\s`'"]*?([`'"])?[\s]+?\1$/; # whitespace inside second set of quotes, trivially true
636             return 1 if $test =~ /^[\w]+$/; # bareword (alphanum) trivially true
637            
638             if (1) # ($test =~ m{^\(})
639             { # need to do this otherwise it creates infinite loop for some reason
640             if ($test =~ m{
641             ((?:\!\s*)*) \s* # $1
642             ( # $2
643             $RE_parens_2C # ($3, $4) has 2 capture groups
644             |
645             (?:$RE_all_no_paren_noop_NC)*
646             ) \s*
647             (?:
648             (\&\& | \|\| )? \s* # $5
649             (.*) # $6
650             )? \s*
651             }x)
652             {
653             # $1 is pound, $4 is inside the brackets, $5 is the op, $6 is the RHS
654             my $LHS=$2;
655             my $LHS_parens=$4; # inside parentheses, does not include the parentheses
656             my $OP=$5;
657             my $RHS=$6;
658             # expr="x == '\\x'" is split into: LHS=[ x == ] RHS=[ '\\x' ]
659             $pounds=$pound=$1;
660             $pound=~s/(?:\!\s*\!\s*)*//; # remove even # of !s, as these cancel out
661            
662             # if no op, and LHS and RHS, FAIL... because (x) b.. -- can be no LHS but RHS and noop
663             # if no op and no $RHS, return pound != test(LHS)
664             # if op, and no RHS or no LHS, FAIL
665             # if op, do op.. return [pound != test(LHS)] op [test(LHS)]
666             if ($OP)
667             { # LOGICAL COMPARISON && and ||
668             # NOTE: && and || have equal precedence
669            
670             if ($LHS=~/^\s*$/)
671             {
672             return $self->SSI_ERROR("empty logical comparison in expr.");
673             }
674             if ($RHS=~/^\s*$/)
675             {
676             return $self->SSI_ERROR("empty logical comparison in expr.");
677             }
678            
679             if ($LHS_parens) {$LHS = $LHS_parens;} # needs to be done here, because of empty comparison checker
680             $LHS = $self->_test($quote.$LHS.$quote);
681            
682             if ($@) {return;} # there were errors in the test
683            
684             if ($pound) {$LHS = !$LHS;}
685             $RHS = $quote.$RHS.$quote;
686            
687             if ($OP eq "&&")
688             { return ($LHS && $self->_test($RHS)); } # short circuits, faster
689             else # ($OP eq "||")
690             { return ($LHS || $self->_test($RHS)); } # short circuits, faster
691             }
692             else
693             { # NO OP
694             if ($LHS && $RHS)
695             {
696             if ($LHS_parens)
697             {
698             # return $self->SSI_ERROR("error in expression."); # NOTE: FIXME: improve this error msg..
699             # return $self->SSI_ERROR("error in expression. LHS and RHS but no OP"); # NOTE: FIXME: improve this error msg..
700             return $self->SSI_ERROR("error in expression. LHS [$LHS] and RHS [$RHS] but no OP"); # NOTE: FIXME: improve this error msg..
701             }
702             $test = $LHS.$RHS;
703             }
704             elsif ($LHS) # brackets or balanced quotes
705             {
706             if ($LHS_parens)
707             {
708             $LHS = $self->_test($quote.$LHS_parens.$quote);
709             if ($pound) {$LHS = !$LHS;}
710             return $LHS;
711             }
712             $test = $LHS; # NOTE: is this redundant?
713             }
714             elsif ($RHS) # unbalanced quotes
715             { $test = $RHS; } # NOTE: is this redundant?
716             }
717             }
718             else
719             {
720             return $self->SSI_ERROR("unknown error in expression."); # SHOULD NOT REACH THIS
721             }
722             }
723            
724            
725             #--------------------------
726             # BAREWORD (no comparison sign)
727             if ($test =~ /^(?:$RE_all_quote_NC|(?:[^=<>\/]|[\\]\/)*)$/) # BAREWORD
728             {
729             if ($test =~ /^(['])(.*?)(?:\1)$/) {$test=$2;} # need to trim surrounding single quotes
730             if ($test =~ /^$/) {return ($pound);} # no need to parse
731             if ($test =~ /^["]/) {return (! $pound);} # no need to parse
732            
733             my $interp_test = $self->_interp_vars($test);
734             my $RET = ($interp_test =~ /[^']+/);
735             if ($interp_test ne $test)
736             { # var interpolation occurred, NOTE: apache deems only '' or empty to be false in this case.
737             $test = ($interp_test !~ /^$/) ;
738             return (($pound) xor ($test));
739             }
740             return (($pound) xor ($RET)); # non empty string is true,
741             }
742            
743            
744             #--------------------------
745             # STRING COMPARISON >,<,==,!=,=~
746             if ($test =~ m{ \s*((?:$RE_all_quote_NC|[^<>=])*?)\s*([<>=!]=?)\s*([^<>=]*)\s* }x)
747             {
748             if ($pounds)
749             { return $self->SSI_ERROR("invalid expression $quote$test$quote in file"); } # NOTE: FIXME
750            
751             my ($s1,$cmp,$s2)=($1, $2, $3);
752             if ($s1=~/^\s*$/)
753             { return $self->SSI_ERROR("problem in REGEX. blank comparison \$s1"); } # NOTE: FIXME
754             if ($s2=~/^\s*$/)
755             { return $self->SSI_ERROR("problem in REGEX. blank comparison \$s2"); } # NOTE: FIXME
756            
757             if ($s2 =~ m{^ \s* (?: (?:/\s*[^/]*) | // ) \s* $}x) # NOTE: what about escaped or stringed
758             {
759             if ($cmp =~ m/^==?$/) {return 1;}
760             elsif ($cmp =~ m/^!=$/) {return;}
761             else { return $self->SSI_ERROR("Invalid expression $quote$test$quote in string comparison."); }
762             }
763            
764             $s1=$self->_interp_vars($s1);
765             if ($s1 =~ /^(['"`])(.*?)(?:\1)$/) {$s1=$2;} # trim off surrounding (matching) quotes
766            
767             # REGEX
768             if ($s2 =~ m{^\s* / ((?:(?:(?:\\\\)*\\/) | [^/] )*) / (.*?)\s*$}x) # wrapped by /xx/
769             {
770             if ($2)
771             { return $self->SSI_ERROR("problem in REGEX. s2=[$s2] extra stuff=[$2]"); } # NOTE: FIXME
772             $s2=qr/$1/; # regex s2
773             $s2 = $self->_interp_vars($s2);
774             if ($cmp =~ m/^==?$/)
775             { return ($s1 =~ m/$s2/);}
776             elsif ($cmp eq "!=")
777             { return ($s1 !~ $s2); } # NOTE: FIXME!!!
778             }
779             else
780             {
781             if ($s2=~m|^[^\s/]+\s+/|) # unquoted, unescaped slash
782             { return $self->SSI_ERROR("problem in REGEX unquoted slash. s2=[$s2]"); } # NOTE: FIXME
783            
784             $s2 = $self->_interp_vars($s2);
785             if ($s2 =~ /^(['"])(.*?)(\1)$/) {$s2 = $2;} # trim off surrounding (matching) quotes
786             }
787            
788             my $ret;
789             $ret = $s1 cmp $s2;
790            
791             if ($cmp =~ m/^==?$/) {$ret = ($ret eq 0);}
792             elsif ($cmp =~ m/^!=$/) {$ret = ($ret ne 0);}
793            
794             elsif ($cmp =~ m/^<$/) {$ret = ($ret lt 0);}
795             elsif ($cmp =~ m/^<=$/) {$ret = ($ret le 0);}
796            
797             elsif ($cmp =~ m/^>$/) {$ret = ($ret gt 0);}
798             elsif ($cmp =~ m/^>=$/) {$ret = ($ret ge 0);}
799            
800             else { return $self->SSI_ERROR("unknown comparison"); } # UNKNOWN COMPARISON -- should never reach this
801            
802             return $ret;
803             }
804             else
805             {
806             if ($test =~ m{[^/]+\s+/}) # NOTE: UNFINISHED!! FIXME non empty unrecognized string that didnt fail
807             { return $self->SSI_ERROR("error in expression, regex found in string"); }
808             return 1;
809             }
810            
811             return; # return false.. it seems none of the ops applied..
812             }
813            
814             sub _entering_if {
815             my $self = shift;
816             $self->{'_in_if'}++;
817             $self->{'_suspend'}->[$self->{'_in_if'}] = $self->{'_suspend'}->[$self->{'_in_if'} - 1];
818             $self->{'_seen_true'}->[$self->{'_in_if'}] = 0;
819             }
820            
821             sub _seen_true {
822             my $self = shift;
823             return $self->{'_seen_true'}->[$self->{'_in_if'}];
824             }
825            
826             sub _suspended {
827             my $self = shift;
828             return $self->{'_suspend'}->[$self->{'_in_if'}];
829             }
830            
831             sub _leaving_if {
832             my $self = shift;
833             $self->{'_in_if'}-- if $self->{'_in_if'};
834             }
835            
836             sub _true {
837             my $self = shift;
838             return $self->{'_seen_true'}->[$self->{'_in_if'}]++;
839             }
840            
841             sub _suspend {
842             my $self = shift;
843             $self->{'_suspend'}->[$self->{'_in_if'}]++;
844             }
845            
846             sub _resume {
847             my $self = shift;
848             $self->{'_suspend'}->[$self->{'_in_if'}]--
849             if $self->{'_suspend'}->[$self->{'_in_if'}];
850             }
851            
852             sub _in_if {
853             my $self = shift;
854             return $self->{'_in_if'};
855             }
856            
857             sub if {
858             my($self,$expr,$test) = @_;
859             $expr = $test if @_ == 3;
860             $self->_entering_if();
861            
862             my $res=$self->_test($expr);
863            
864             if($@) {
865             $self->_true();
866             return;
867             } # any errors cause the expr to evaluate to true..??
868            
869             if($res) {
870             $self->_true();
871             } else {
872             $self->_suspend();
873             }
874             return '';
875             }
876            
877             sub elif {
878             my($self,$expr,$test) = @_;
879            
880             if (! $self->_in_if() )
881             {
882             $self->SSI_WARN("Incorrect use of elif ssi directive: no preceeding 'if'."); # NOTE: just a "warn"
883             $self->_suspend() unless $self->_suspended();
884             return;
885             }
886            
887             if ($self->_seen_true())
888             {
889             $self->_suspend() unless $self->_suspended();
890             return;
891             }
892            
893             $expr = $test if @_ == 3;
894            
895            
896             my $res= $self->_test($expr);
897            
898             if($@) {
899             $self->_suspend() unless $self->_suspended();
900             return;
901             }
902            
903             if($res) {
904             $self->_true();
905             $self->_resume();
906             } else {
907             $self->_suspend() unless $self->_suspended();
908             }
909             return '';
910             }
911            
912             sub else {
913             my $self = shift;
914            
915             if (! $self->_in_if() ) {
916             $self->SSI_WARN("Incorrect use of else ssi directive: no preceeding 'if'."); # NOTE: just a "warn"
917             $self->_suspend() unless $self->_suspended();
918             return;
919             }
920             if ($self->_seen_true()) {
921             $self->_suspend() unless $self->_suspended(); }
922             else {
923             $self->_resume(); }
924             return '';
925             }
926            
927             sub endif {
928             my $self = shift;
929             if (! $self->_in_if() )
930             {
931             # $self->SSI_ERROR("Incorrect use of endif ssi directive: no preceeding 'if'.");
932             $self->SSI_WARN("Incorrect use of endif ssi directive: no preceeding 'if'.");
933             }
934             else
935             { $self->_leaving_if(); }
936             $self->_resume() if $self->_suspended(); # might be suspended even if not in "if"
937             return '';
938             }
939            
940             #
941             # if we're called like this, it means that we're to handle a CGI request ourselves.
942             # that means that we're to open the file and process the content, sending it to STDOUT
943             # along with a standard HTTP content header
944             #
945             unless(caller) {
946             goto &handler;
947             }
948            
949             sub handler {
950             eval "use CGI qw(:standard);";
951             print header();
952            
953             unless(UNIVERSAL::isa(tied(*STDOUT),'CGI::apacheSSI')) {
954             tie *STDOUT, 'CGI::apacheSSI', filehandle => 'main::STDOUT';
955             }
956            
957             my $filename = "$ENV{DOCUMENT_ROOT}$ENV{REQUEST_URI}";
958             if(-f $filename) {
959             open my $fh, '<', $filename or die "Failed to open file ($filename): $!";
960             print <$fh>;
961             } else {
962             print "Failed to find file ($filename).";
963             }
964            
965             exit;
966             }
967            
968             #
969             # packages for tie()
970             #
971            
972             package CGI::apacheSSI::Gmt;
973            
974             sub TIESCALAR { bless [@_], shift() }
975             sub FETCH {
976             my $self = shift;
977             if($self->[-1]->{'_config'}->{'timefmt'}) {
978             my @gt = gmtime;
979             return Date::Format::strftime($self->[-1]->{'_config'}->{'timefmt'},@gt);
980             } else {
981             return scalar gmtime;
982             }
983             }
984            
985             package CGI::apacheSSI::Local;
986            
987             sub TIESCALAR { bless [@_], shift() }
988             sub FETCH {
989             my $self = shift;
990             if($self->[-1]->{'_config'}->{'timefmt'}) {
991             my @lt = localtime;
992             return Date::Format::strftime($self->[-1]->{'_config'}->{'timefmt'},@lt);
993             } else {
994             return scalar localtime;
995             }
996             }
997            
998             package CGI::apacheSSI::LMOD;
999            
1000             sub TIESCALAR { bless [@_], shift() }
1001             sub FETCH {
1002             my $self = shift;
1003             return $self->[-1]->flastmod('file', $ENV{'SCRIPT_FILENAME'} || $ENV{'PATH_TRANSLATED'} || '');
1004             }
1005            
1006             1;
1007             __END__