File Coverage

blib/lib/Apache2/SSI/File.pm
Criterion Covered Total %
statement 116 150 77.3
branch 39 70 55.7
condition 7 25 28.0
subroutine 20 21 95.2
pod 8 10 80.0
total 190 276 68.8


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