File Coverage

blib/lib/Apache2/SSI/File.pm
Criterion Covered Total %
statement 125 159 78.6
branch 39 70 55.7
condition 8 29 27.5
subroutine 21 22 95.4
pod 8 10 80.0
total 201 290 69.3


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## Apache2 Server Side Include Parser - ~/lib/Apache2/SSI/File.pm
3             ## Version v0.1.0
4             ## Copyright(c) 2021 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest <jack@deguest.jp>
6             ## Created 2020/12/18
7             ## Modified 2021/01/13
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package Apache2::SSI::File;
14             BEGIN
15             {
16 15     15   106917 use strict;
  15         45  
  15         522  
17 15     15   77 use warnings;
  15         24  
  15         430  
18 15     15   75 use warnings::register;
  15         31  
  15         2091  
19 15     15   600 use parent qw( Apache2::SSI::Common );
  15         332  
  15         157  
20 15     15   9474 use Apache2::SSI::Finfo;
  15         53  
  15         1052  
21 15     15   102 use File::Spec ();
  15         27  
  15         220  
22 15     15   67 use Scalar::Util ();
  15         25  
  15         229  
23 15     15   66 use URI::file ();
  15         23  
  15         2027  
24 15 50   15   1479 if( $ENV{MOD_PERL} )
25             {
26 0         0 require Apache2::RequestRec;
27 0         0 require Apache2::RequestUtil;
28 0         0 require Apache2::SubRequest;
29 0         0 require Apache2::Access;
30 0         0 require Apache2::Const;
31 0         0 Apache2::Const->import( compile => qw( :common :http OK DECLINED ) );
32 0         0 require APR::Const;
33 0         0 APR::Const->import( -compile => qw( :filetype FINFO_NORM ) );
34             }
35             ## use Devel::Confess;
36 15         28 our( $DEBUG );
37             use overload (
38 3     3   9 q{""} => sub { $_[0]->filename },
39             bool => sub () { 1 },
40 15         93 fallback => 1,
41 15     15   86 );
  15         30  
42 15         27 our $VERSION = 'v0.1.0';
43 15         19797 our $DIR_SEP = $Apache2::SSI::Common::DIR_SEP;
44             };
45              
46             sub init
47             {
48 8     8 1 16423 my $self = shift( @_ );
49 8         44 my $file = shift( @_ );
50 8 50 33     127 return( $self->error( "No file was provided." ) ) if( !defined( $file ) || !length( $file ) );
51 8         338 $self->{apache_request} = '';
52 8 50       71 $self->{base_dir} = '' unless( length( $self->{base_dir} ) );
53 8         45 $self->{base_file} = '';
54 8         35 $self->{code} = 200;
55 8         41 $self->{finfo} = '';
56 8         55 $self->{_init_strict_use_sub} = 1;
57 8 50       83 $self->SUPER::init( @_ ) || return;
58 8         570 $self->message( 3, "Returning object for file \"$file\"." );
59 8         150 my $base_dir = '';
60 8 100       85 if( length( $self->{base_file} ) )
    100          
61             {
62 3 50       67 if( -d( $self->{base_file} ) )
63             {
64 0         0 $base_dir = $self->{base_file};
65             }
66             else
67             {
68 3         62 my @segments = split( "\Q${DIR_SEP}\E", $self->{base_file}, -1 );
69 3         8 pop( @segments );
70 3         13 $base_dir = join( $DIR_SEP, @segments );
71             }
72 3         7 $self->{base_dir} = $base_dir;
73             }
74             elsif( !length( $self->{base_dir} ) )
75             {
76 4         41 $base_dir = URI->new( URI::file->cwd )->file( $^O );
77 4         36469 $self->{base_dir} = $base_dir;
78             }
79 8 50       175 $self->filename( $file ) || return;
80 8         155 return( $self );
81             }
82              
83 56     56 1 418 sub apache_request { return( shift->_set_get_object_without_init( 'apache_request', 'Apache2::RequestRec', @_ ) ); }
84              
85 10     10 1 466 sub base_dir { return( shift->_make_abs( 'base_dir', @_ ) ); }
86              
87 3     3 0 326 sub base_file { return( shift->_make_abs( 'base_file', @_ ) ); }
88              
89             sub clone
90             {
91 1     1 1 10 my $self = shift( @_ );
92 1         10 my $new = {};
93 1         56 my @fields = grep( !/^(apache_request|finfo)$/, keys( %$self ) );
94 1         28 @$new{ @fields } = @$self{ @fields };
95 1         9 $new->{apache_request} = $self->{apache_request};
96 1   33     19 return( bless( $new => ( ref( $self ) || $self ) ) );
97             }
98              
99             sub code
100             {
101 11     11 1 2393 my $self = shift( @_ );
102 11         34 my $r = $self->apache_request;
103 11 50       192 if( $r )
104             {
105 0 0       0 $r->status( @_ ) if( @_ );
106 0         0 return( $r->status );
107             }
108             else
109             {
110 11 100       59 $self->{code} = shift( @_ ) if( @_ );
111 11         73 $self->message( 3, "Returning code '$self->{code}'" );
112 11         211 return( $self->{code} );
113             }
114             }
115              
116             sub filename
117             {
118 19     19 1 87 my $self = shift( @_ );
119 19         85 my $newfile;
120 19 100       143 if( @_ )
121             {
122 9         91 $newfile = shift( @_ );
123 9 50 33     288 return( $self->error( "New file provided, but it was an empty string." ) ) if( !defined( $newfile ) || !length( $newfile ) );
124             }
125            
126 19         148 my $r = $self->apache_request;
127 19 50       702 if( $r )
128             {
129 0 0       0 if( defined( $newfile ) )
    0          
130             {
131 0         0 $self->message( 3, "Setting new file path '$newfile'. Looking up file." );
132 0 0       0 $r = $r->is_initial_req ? $r : $r->main;
133 0         0 my $rr = $r->lookup_file( $newfile );
134 0   0     0 $self->message( 3, "File found \"", $rr->filename, "\" has status '", $rr->status, "' and file type '", ( ( $rr->finfo && $rr->finfo->filetype ) || '' ), "'." );
135             ## Amazingly, lookup_file will return ok even if it does not find the file
136 0 0 0     0 if( $rr->status == Apache2::Const::HTTP_OK &&
      0        
137             $rr->finfo &&
138             $rr->finfo->filetype != APR::Const::FILETYPE_NOFILE )
139             {
140 0         0 $self->apache_request( $rr );
141 0         0 $newfile = $rr->filename;
142 0         0 $self->message( 3, "File found and resolved to: '$newfile' with code '", $rr->status, "' with finfo object '", $r->finfo, "'." );
143 0         0 my $finfo = $rr->finfo;
144 0 0       0 if( $finfo )
145             {
146 0         0 $self->message( 3, "File type is '", $finfo->filetype, "'." );
147             }
148             }
149             else
150             {
151 0         0 $self->message( 3, "File is not found." );
152 0         0 $self->code( 404 );
153 0         0 $newfile = $self->collapse_dots( $newfile, { separator => $DIR_SEP });
154             ## We don't pass it the Apache2::RequestRec object, because it would trigger a fatal error since the file does not exist. Instead, we use the api without Apache2::RequestRec which is more tolerant
155             ## We do this so the user can call our object $file->finfo->filetype == Apache2::SSI::Finfo::FILETYPE_NOFILE
156 0         0 $self->{finfo} = Apache2::SSI::Finfo->new( $newfile );
157             }
158 0         0 $self->{filename} = $newfile;
159             }
160             elsif( !length( $self->{filename} ) )
161             {
162 0         0 $self->{filename} = $r->filename;
163             }
164             }
165             else
166             {
167 19 100       132 if( defined( $newfile ) )
168             {
169 9         89 my $base_dir = $self->base_dir;
170 9 100       160 $base_dir .= $DIR_SEP unless( substr( $base_dir, -length( $DIR_SEP ), length( $DIR_SEP ) ) eq $DIR_SEP );
171 9         191 $self->message( 3, "New file path provided is: '$newfile' and base directory is '$base_dir' and directory separator is '$DIR_SEP'" );
172             ## If we provide a string for the abs() method it works on Unix, but not on Windows
173             ## By providing an object, we make it work
174 9         310 $newfile = URI::file->new( $newfile )->abs( URI::file->new( $base_dir ) )->file( $^O );
175 9         5573 $self->message( 3, "Getting the new file real path: '$newfile'" );
176 9         405 $self->{filename} = $self->collapse_dots( $newfile, { separator => $DIR_SEP })->file( $^O );
177 9         1279 $self->message( 3, "Filename after dot collapsing is: '$self->{filename}'" );
178 9         230 $self->finfo( $newfile );
179 9         37 my $finfo = $self->finfo;
180 9         44 $self->message( 3, "finfo is '", overload::StrVal( $finfo ), "'." );
181 9 100       249 if( !$finfo->exists )
182             {
183 2         49 $self->code( 404 );
184             }
185             ## Force to create new Apache2::SSI::URI object
186             }
187             }
188 19         118 $self->message( 3, "Returning filename '$self->{filename}'" );
189 19         521 return( $self->{filename} );
190             }
191              
192             ## Alias
193 0     0 0 0 sub filepath { return( shift->filename( @_ ) ); }
194              
195             sub finfo
196             {
197 25     25 1 337 my $self = shift( @_ );
198 25         108 my $r = $self->apache_request;
199 25         394 my $newfile;
200 25 100       139 if( @_ )
    50          
201             {
202 9         33 $newfile = shift( @_ );
203 9 50 33     124 return( $self->error( "New file path specified but is an empty string." ) ) if( !defined( $newfile ) || !length( $newfile ) );
204             }
205             elsif( !$self->{finfo} )
206             {
207 0         0 $newfile = $self->filename;
208 0         0 $self->message( 3, "Initiating finfo object using filename '$newfile'." );
209 0 0       0 return( $self->error( "No file path set. This should not happen." ) ) if( !$newfile );
210             }
211            
212 25 100       110 if( defined( $newfile ) )
213             {
214 9 50       129 $self->{finfo} = Apache2::SSI::Finfo->new( $newfile, ( $r ? ( apache_request => $r ) : () ), debug => $self->debug );
215 9         62 $self->message( 3, "finfo object is now '", overload::StrVal( $self->{finfo} ), "'" );
216 9 50       264 $self->message( 3, "Error occurred: ", Apache2::SSI::Finfo->error ) if( !$self->{finfo} );
217 9 50       57 return( $self->pass_error( Apache2::SSI::Finfo->error ) ) if( !$self->{finfo} );
218             }
219 25         82 $self->message( 3, "Returning finfo object '", overload::StrVal( $self->{finfo} ), "' for file '$self->{finfo}'." );
220 25         498 return( $self->{finfo} );
221             }
222              
223             sub parent
224             {
225 1     1 1 3 my $self = shift( @_ );
226 1         8 my $r = $self->apache_request;
227             ## I deliberately did not do split( '/', $path, -1 ) so that if there is a trailing '/', it will not be counted
228             ## 2021-03-27: Was working well, but only on Unix systems...
229             ## my @segments = split( '/', $self->filename, -1 );
230 1         29 my( $vol, $parent, $file ) = File::Spec->splitpath( $self->filename );
231 1   50     5 $vol //= '';
232 1   50     10 $file //= '';
233 1         17 $self->message( 3, "Filename is '", $self->filename, "', volume is '$vol', parent '$parent' and file is '$file'." );
234 1         65 my @segments = File::Spec->splitpath( File::Spec->catfile( $parent, $file ) );
235             ## $self->message( 3, "Path segments are: ", sub{ $self->dump( \@segments )} );
236 1         8 pop( @segments );
237 1 50       12 return( $self ) if( !scalar( @segments ) );
238 1         12 $self->message( 3, "Creating new object with document uri '", $vol . File::Spec->catdir( @segments ), "'." );
239             ## return( $self->new( join( '/', @segments ), ( $r ? ( apache_request => $r ) : () ) ) );
240 1 50       33 return( $self->new( $vol . File::Spec->catdir( @segments ), ( $r ? ( apache_request => $r ) : () ) ) );
241             }
242              
243             sub _make_abs
244             {
245 13     13   85 my $self = shift( @_ );
246 13   50     132 my $field = shift( @_ ) || return( $self->error( "No field provided." ) );
247 13 100       153 if( @_ )
248             {
249 4         18 my $this = shift( @_ );
250 4         64 $self->message( 3, "Setting $field to '$this'." );
251 4 50 33     181 if( Scalar::Util::blessed( $this ) && $this->isa( 'URI::file' ) )
    100          
252             {
253 0         0 $this = URI->new_abs( $this )->file( $^O );
254             }
255             ## elsif( substr( $this, 0, 1 ) ne '/' )
256             elsif( !File::Spec->file_name_is_absolute( $this ) )
257             {
258 1         23 $this = URI::file->new_abs( $this )->file( $^O );
259             }
260 4         10935 $self->message( 3, "$field is now '$this'" );
261 4         137 $self->{ $field } = $this;
262             }
263 13         95 return( $self->{ $field } );
264             }
265              
266             1;
267              
268             __END__
269              
270             =encoding utf-8
271              
272             =head1 NAME
273              
274             Apache2::SSI::File - Apache2 Server Side Include File Object Class
275              
276             =head1 SYNOPSIS
277              
278             my $f = Apache2::SSI::File->new(
279             '/some/file/path/file.html',
280             apache_request => $r,
281             base_dir => '/home/john/www',
282             );
283             $f->base_dir( '/home/joe/www' );
284             my $f2 = $f->clone;
285             unless( $f->code == Apache2::Const::HTTP_OK )
286             {
287             die( "File is not there!\n" );
288             }
289             # You can also use $f->filepath which is an alias to $f->filename
290             print "Actual file is here: ", $f->filename, "\n";
291             my $finfo = $f->finfo;
292             if( $finfo->can_exec )
293             {
294             # do something
295             }
296             # prints Parent is: /some/file/path
297             print "Parent is: ", $f->parent, "\n";
298              
299             =head1 VERSION
300              
301             v0.1.0
302              
303             =head1 DESCRIPTION
304              
305             This packages serves to resolve files whether inside Apache scope with mod_perl or outside, providing a unified api.
306              
307             =head1 METHODS
308              
309             =head2 new
310              
311             This instantiates an object that is used to access other key methods. It takes the following parameters:
312              
313             =over 4
314              
315             =item I<apache_request>
316              
317             This is the L<Apache2::RequestRec> object that is provided if running under mod_perl.
318              
319             it can be retrieved from L<Apache2::RequestUtil/request> or via L<Apache2::Filter/r>
320              
321             You can get this L<Apache2::RequestRec> object by requiring L<Apache2::RequestUtil> and calling its class method L<Apache2::RequestUtil/request> such as C<Apache2::RequestUtil->request> and assuming you have set C<PerlOptions +GlobalRequest> in your Apache Virtual Host configuration.
322              
323             Note that there is a main request object and subprocess request object, so to find out which one you are dealing with, use L<Apache2::RequestUtil/is_initial_req>, such as:
324              
325             use Apache2::RequestUtil (); # extends Apache2::RequestRec objects
326             my $r = $r->is_initial_req ? $r : $r->main;
327              
328             =back
329              
330             =head2 apache_request
331              
332             Sets or gets the L<Apache2::RequestRec> object. As explained in the L</new> method, you can get this Apache object by requiring the package L<Apache2::RequestUtil> and calling L<Apache2::RequestUtil/request> such as C<Apache2::RequestUtil->request> assuming you have set C<PerlOptions +GlobalRequest> in your Apache Virtual Host configuration.
333              
334             When running under Apache mod_perl this is set automatically from the special L</handler> method, such as:
335              
336             my $r = $f->r; # $f is the Apache2::Filter object provided by Apache
337              
338             =head2 base_dir
339              
340             Sets or gets the base directory to be used as a reference to the files provided so they can be transformed into absolute file path.
341              
342             my $f = Apache2::SSI::File->new( './index.html',
343             base_dir => '/home/joe/www',
344             );
345             # This would now be /home/joe/www/index.html
346             $f->filename;
347              
348             =head2 clone
349              
350             Create a clone of the object and return it.
351              
352             =head2 code
353              
354             Sets or gets the http code for this file.
355              
356             $f->code( 404 );
357              
358             =head2 collapse_dots
359              
360             Provided with an uri or a file path, and this will resolve the path and removing the dots, such as C<.> and C<..> and return an L<URI> object.
361              
362             This is done as per the L<RFC 3986 section 5.2.4 algorithm|https://tools.ietf.org/html/rfc3986#page-33>
363              
364             my $file = $f->collapse_dots( '/../a/b/../c/./d.html' );
365             # would become /a/c/d.html
366              
367             =head2 filename
368              
369             Sets or gets the system file path to the file, as a string.
370              
371             If a new file name is provided, under Apache/mod_perl2, this will perform a query with L<Apache2::SubRequest/lookup_file>
372              
373             Any filename provided will be resolved with its dots flattened and transformed into an absolute system file path if it is not already.
374              
375             =head2 finfo
376              
377             Returns a L<Apache2::SSI::Finfo> object. This provides access to L<perlfunc/stat> information as methods, taking advantage of L<APR::Finfo> when running under Apache, and an identical interface otherwise. See L<Apache2::SSI::Finfo> for more information.
378              
379             =head2 parent
380              
381             Returns the parent of the file, or if there is no parent, it returns the current object itself.
382              
383             my $up = $f->parent;
384             # would return /home/john/some/path assuming the file was /home/john/some/path/file.html
385              
386             =head2 slurp
387              
388             It returns the content of the L</filename>
389              
390             it takes an hash reference of parameters:
391              
392             =over 4
393              
394             =item I<binmode>
395              
396             my $content = $uri->slurp({ binmode => ':utf-8' });
397              
398             =back
399              
400             It will return undef and sets an L<Module::Generic/error> if there is no L</filename> value set or if the file cannot be opened.
401              
402             =head2 slurp_utf8
403              
404             It returns the content of the file L</filename> utf-8 decoded.
405              
406             This is equivalent to:
407              
408             my $content = $uri->slurp({ binmode => ':utf8' });
409              
410             C<:utf8> is slightly a bit more lax than C<:utf-8>, so it you want strict utf8, you can do:
411              
412             my $content = $uri->slurp({ binmode => ':utf-8' });
413              
414             =head1 AUTHOR
415              
416             Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>
417              
418             CPAN ID: jdeguest
419              
420             L<https://git.deguest.jp/jack/Apache2-SSI>
421              
422             =head1 SEE ALSO
423              
424             L<Apache2::SSI::URI>, L<Apache2::SSI::Finfo>, L<Apache2::SSI>
425              
426             mod_include, mod_perl(3), L<APR::URI>, L<URI>
427             L<https://httpd.apache.org/docs/current/en/mod/mod_include.html>,
428             L<https://httpd.apache.org/docs/current/en/howto/ssi.html>,
429             L<https://httpd.apache.org/docs/current/en/expr.html>
430             L<https://perl.apache.org/docs/2.0/user/handlers/filters.html#C_PerlOutputFilterHandler_>
431              
432             =head1 COPYRIGHT & LICENSE
433              
434             Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
435              
436             You can use, copy, modify and redistribute this package and associated
437             files under the same terms as Perl itself.
438              
439             =cut
440