File Coverage

lib/Plack/Middleware/SSI.pm
Criterion Covered Total %
statement 159 170 93.5
branch 56 84 66.6
condition 18 33 54.5
subroutine 29 29 100.0
pod 1 1 100.0
total 263 317 82.9


line stmt bran cond sub pod time code
1             package Plack::Middleware::SSI;
2              
3             =head1 NAME
4              
5             Plack::Middleware::SSI - PSGI middleware for server side include content
6              
7             =head1 VERSION
8              
9             0.12
10              
11             =head1 DESCRIPTION
12              
13             Will try to handle HTML with server side include directives as well as doing
14             what L does for "regular files".
15              
16             =head1 SUPPORTED SSI DIRECTIVES
17              
18             See L,
19             L or
20             L for more details.
21              
22             =head2 set
23              
24            
25              
26             =head2 echo
27              
28            
29              
30             =head2 config
31              
32            
33            
34              
35             =head2 exec
36              
37            
38              
39             =head2 flastmod
40              
41            
42              
43             =head2 fsize
44              
45            
46              
47             =head2 include
48              
49            
50            
51              
52             =head1 SUPPORTED SSI VARIABLES
53              
54             =head2 Standard variables
55              
56             DATE_GMT, DATE_LOCAL, DOCUMENT_NAME, DOCUMENT_URI, LAST_MODIFIED and
57             QUERY_STRING_UNESCAPED.
58              
59             =head2 Extended by this module
60              
61             Any variable defined in L C<$env> will be avaiable in the SSI
62             document. Even so, it is not recommended to use any of those, since
63             it may not be compatible with Apache and friends.
64              
65             =head1 SYNOPSIS
66              
67             $app = builder { enable 'SSI'; $app };
68              
69             See L for more details.
70              
71             =cut
72              
73 1     1   83592 use strict;
  1         3  
  1         33  
74 1     1   5 use warnings;
  1         3  
  1         26  
75 1     1   7 use File::Basename;
  1         2  
  1         106  
76 1     1   935 use POSIX ();
  1         18305  
  1         31  
77 1     1   9 use HTTP::Date;
  1         2  
  1         62  
78 1     1   866 use HTTP::Request;
  1         54814  
  1         47  
79 1     1   12636 use HTTP::Response;
  1         8231  
  1         41  
80 1     1   1123 use HTTP::Message::PSGI;
  1         12363  
  1         197  
81 1 50   1   14 use constant DEBUG => $ENV{'PLACK_SSI_TRACE'} ? 1 : 0;
  1         2  
  1         78  
82              
83 1     1   26 use base 'Plack::Middleware';
  1         2  
  1         2314  
84              
85             our $VERSION = '0.12';
86              
87             my $DEFAULT_ERRMSG = '[an error occurred while processing this directive]';
88             my $DEFAULT_TIMEFMT = '%A, %d-%b-%Y %H:%M:%S %Z';
89             my $ANON = 'Plack::Middleware::SSI::__ANON__';
90             my $SKIP = '__________SKIP__________';
91             my $CONFIG = '__________CONFIG__________';
92             my $BUF = '__________BUF__________';
93              
94             =head1 METHODS
95              
96             =head2 call
97              
98             Returns a callback which can expand chunks of HTML with SSI directives
99             to a complete HTML document.
100              
101             =cut
102              
103             sub call {
104 1     1 1 12447 my($self, $env) = @_;
105              
106             return $self->response_cb($self->app->($env), sub {
107 1     1   628 my $res = shift;
108 1         19 my $headers = Plack::Util::headers($res->[1]);
109 1   50     90 my $content_type = $headers->get('Content-Type') || '';
110              
111 1 50 33     114 if($content_type =~ m{^text/} or $content_type =~ m{^application/xh?t?ml\b}) {
112 1         10 my $buf = '';
113 1   50     13 my $ssi_variables = {
      50        
      50        
114             %$env,
115             LAST_MODIFIED_TS => HTTP::Date::str2time($headers->get('Last-Modified') || ''),
116             DOCUMENT_NAME => basename($env->{'PATH_INFO'}),
117             DOCUMENT_URI => $env->{'REQUEST_URI'} || '',
118             QUERY_STRING_UNESCAPED => $env->{'QUERY_STRING'} || '',
119             $BUF => \$buf,
120             };
121              
122 1         335 return sub { $self->_parse_ssi_chunk($ssi_variables, @_) };
  3         963  
123             }
124              
125 0         0 return;
126 1         16 });
127             }
128              
129             # will match partial expression at end of string
130             my $SSI_EXPRESSION = qr{
131             < (?:\z| # accept end-of-string after each character
132             ! (?:\z|
133             - (?:\z|
134             - (?:\z|
135             \# (?:\z|
136             (.*?) \s* (?:\z| # this capture contains the actual expression
137             - (?:\z|
138             - (?:\z|
139             (>) # this capture serves as a flag that we reached end-of-expr
140             ))))))))
141             }sx;
142              
143             sub _parse_ssi_chunk {
144 21     21   46934 my($self, $ssi_variables, $chunk) = @_;
145 21         47 my $buf = $ssi_variables->{$BUF};
146 21         42 my $out = \do { my $tmp = '' };
  21         68  
147              
148 21 100       77 unless(defined $chunk) {
149 6 100       37 return $$buf if(delete $ssi_variables->{$BUF}); # return the rest of buffer
150 1         700 return; # ...before EOF
151             }
152              
153 15         50 $$buf .= $chunk;
154              
155 15         23 my $do_keep_buffer;
156              
157 15         282 while(my($expression, $is_complete) = $$buf =~ $SSI_EXPRESSION) {
158 31 100       172 $$out .= substr $$buf, 0, $-[0] unless($ssi_variables->{$SKIP});
159 31 50       216 $$buf = substr $$buf, $is_complete ? $+[0] : $-[0];
160              
161             # matched incompletely at end of string,
162             # will need more chunks to finish the expression
163 31 50       88 $do_keep_buffer = 1, last if not $is_complete;
164              
165 31 50       5558 my $method = $expression =~ s/^(\w+)// ? "_ssi_exp_$1" : '_ssi_exp_unknown';
166 31 100 33     1425 my $value = $self->can($method)
167             ? $self->$method($expression, $ssi_variables)
168             : $ssi_variables->{$CONFIG}{'errmsg'} || $DEFAULT_ERRMSG;
169              
170 31 100       467 $$out .= $value unless($ssi_variables->{$SKIP});
171             }
172              
173 15 50       47 if(not $do_keep_buffer) {
174 15 100       78 length $$out ? ($$out .= $$buf) : ($out = $buf) # swap when possible, append if necessary
    50          
175             unless($ssi_variables->{$SKIP});
176 15         23 $ssi_variables->{$BUF} = \do { my $tmp = '' };
  15         50  
177             }
178              
179 15         92 return $$out;
180             }
181              
182             #=============================================================================
183             # SSI expression parsers
184              
185             sub _ssi_exp_set {
186 5     5   10 my($self, $expression, $ssi_variables) = @_;
187 5 50       45 my $name = $expression =~ /var="([^"]+)"/ ? $1 : undef;
188 5 50       33 my $value = $expression =~ /value="([^"]*)"/ ? $1 : '';
189              
190 5 50       12 if(defined $name) {
191 5         29 $ssi_variables->{$name} = $value;
192             }
193             else {
194 0         0 warn "Found SSI set expression, but no variable name ($expression)" if DEBUG;
195             }
196              
197 5         16 return '';
198             }
199              
200             sub _ssi_exp_echo {
201 7     7   16 my($self, $expression, $ssi_variables) = @_;
202 7 50       50 my($name) = $expression =~ /var="([^"]+)"/ ? $1 : undef;
203              
204 7 50       20 if(defined $name) {
205 7         53 return $ANON->__eval_condition("\$$name", $ssi_variables);
206             }
207              
208 0         0 warn "Found SSI echo expression, but no variable name ($expression)" if DEBUG;
209 0         0 return '';
210             }
211              
212             sub _ssi_exp_config {
213 2     2   10 my($self, $expression, $ssi_variables) = @_;
214 2 50       37 my($key, $value) = $expression =~ /(\w+)="([^"]*)"/ ? ($1, $2) : ();
215              
216 2 50       8 if(defined $key) {
217 2         20 $ssi_variables->{$CONFIG}{$key} = $value;
218             }
219              
220 2         7 return '';
221             }
222              
223             sub _ssi_exp_exec {
224 1     1   19 my($self, $expression, $ssi_variables) = @_;
225 1 50       38 my($cmd) = $expression =~ /cmd="([^"]+)"/ ? $1 : undef;
226              
227 1 50       11 if(defined $cmd) {
228 1         7270 return join '', qx{$cmd};
229             }
230              
231 0         0 warn "Found SSI cmd expression, but no command ($expression)" if DEBUG;
232 0         0 return '';
233             }
234              
235             sub _ssi_exp_fsize {
236 1     1   2 my($self, $expression, $ssi_variables) = @_;
237 1 50       6 my $file = $self->_expression_to_file($expression) or return '';
238              
239 1   50     48 return (stat $file->{'name'})[7] || '';
240             }
241              
242             sub _ssi_exp_flastmod {
243 1     1   3 my($self, $expression, $ssi_variables) = @_;
244 1 50       5 my $file = $self->_expression_to_file($expression) or return '';
245 1   33     7 my $fmt = $ssi_variables->{$CONFIG}{'timefmt'} || $DEFAULT_TIMEFMT;
246              
247 1   50     103 return POSIX::strftime($fmt, localtime +(stat $file->{'name'})[9]) || '';
248             }
249              
250             sub _ssi_exp_include {
251 1     1   2 my($self, $expression, $ssi_variables) = @_;
252 1 50       6 my $file = $self->_expression_to_file($expression) or return '';
253 1         3 my $buf = '';
254 1         2 my $text = '';
255              
256 1         66 local $ssi_variables->{'DOCUMENT_NAME'} = basename $file->{'name'};
257 1         3 local $ssi_variables->{'LAST_MODIFIED_TS'} = $file->{'mtime'};
258 1         7 local $ssi_variables->{$BUF} = \$buf;
259              
260 1         7 while(my $line = readline $file->{'filehandle'}) {
261 2         18 $text .= $self->_parse_ssi_chunk($ssi_variables, $line);
262             }
263              
264             # get the rest
265 1         4 $text .= $self->_parse_ssi_chunk($ssi_variables);
266              
267 1         8 return $text;
268             }
269              
270 3     3   20 sub _ssi_exp_if { $_[0]->_evaluate_if_elif_else($_[1], $_[2]) }
271 3     3   14 sub _ssi_exp_elif { $_[0]->_evaluate_if_elif_else($_[1], $_[2]) }
272 3     3   12 sub _ssi_exp_else { $_[0]->_evaluate_if_elif_else('expr="1"', $_[2]) }
273              
274             sub _evaluate_if_elif_else {
275 9     9   17 my($self, $expression, $ssi_variables) = @_;
276 9 50       53 my $condition = $expression =~ /expr="([^"]+)"/ ? $1 : undef;
277              
278 9 50       24 unless(defined $condition) {
279 0         0 warn "Found SSI if expression, but no expression ($expression)" if DEBUG;
280 0         0 return '';
281             }
282              
283 9 100 100     69 if(defined $ssi_variables->{$SKIP} and $ssi_variables->{$SKIP} != 1) {
    100          
284 3         7 $ssi_variables->{$SKIP} = 2; # previously true
285             }
286             elsif($ANON->__eval_condition($condition, $ssi_variables)) {
287 3         8 $ssi_variables->{$SKIP} = 0; # true
288             }
289             else {
290 3         13 $ssi_variables->{$SKIP} = 1; # false
291             }
292              
293 9         35 return '';
294             }
295              
296             sub _ssi_exp_endif {
297 3     3   7 my($self, $expression, $ssi_variables) = @_;
298 3         8 delete $ssi_variables->{$SKIP};
299 3         8 return '';
300             }
301              
302             sub _expression_to_file {
303 3     3   6 my($self, $expression) = @_;
304              
305 3 100       22 if($expression =~ /file="([^"]+)"/) {
    50          
306 2         7 my $file = $1;
307 2 50       252 if(open my $FH, '<', $file) {
308 2         20 return { name => $file, filehandle => $FH };
309             }
310             }
311             elsif($expression =~ /virtual="([^"]+)"/) {
312 1         3 my $file = $1;
313 1         14 my $request = HTTP::Request->new(GET => $file);
314 1         7938 my $response;
315              
316 1 50       8 $request->uri->scheme('http') unless(defined $request->uri->scheme);
317 1 50       25650 $request->uri->host('localhost') unless(defined $request->uri->host);
318 1         352 $response = HTTP::Response->from_psgi( $self->app->($request->to_psgi) );
319              
320 1 50       4030 if($response->code == 200) {
321 1         20 open my $FH, '<', \$response->content;
322 1         44 return { name => $file, filehandle => $FH };
323             }
324             }
325              
326 0         0 warn "Could not find file from SSI expression ($expression)" if DEBUG;
327 0         0 return;
328             }
329              
330             #=============================================================================
331             # INTERNAL FUNCTIONS
332              
333             sub __readline {
334 3     3   987 my($buf, $FH) = @_;
335 3         47 my $tmp = readline $FH;
336 3 100       320 return unless(defined $tmp);
337 2         6 $$buf .= $tmp;
338 2         8 return 1;
339             }
340              
341             =head1 COPYRIGHT & LICENSE
342              
343             This library is free software. You can redistribute it and/or modify
344             it under the same terms as Perl itself.
345              
346             =head1 AUTHOR
347              
348             Jan Henning Thorsen C<< jhthorsen at cpan.org >>
349              
350             =cut
351              
352              
353             package # hide from CPAN
354             Plack::Middleware::SSI::__ANON__;
355              
356             my $pkg = __PACKAGE__;
357              
358             sub __eval_condition {
359 15     15   840 my($class, $expression, $ssi_variables) = @_;
360              
361 1     1   10509 no strict;
  1         2  
  1         592  
362              
363 15 100       66 if($expression =~ /\$/) { # 1 is always true. do not need variables to figure that out
364 14   66     80 my $fmt = $ssi_variables->{$CONFIG}{'timefmt'} || $DEFAULT_TIMEFMT;
365              
366 14   66     71 $ssi_variables->{"__{$fmt}__DATE_GMT"} ||= do { local $_ = POSIX::strftime($fmt, gmtime); $_ };
  8         894  
  8         56  
367 14   66     503 $ssi_variables->{"__{$fmt}__DATE_LOCAL"} ||= POSIX::strftime($fmt, localtime);
368 14         57 $ssi_variables->{'DATE_GMT'} = $ssi_variables->{"__{$fmt}__DATE_GMT"};
369 14         44 $ssi_variables->{'DATE_LOCAL'} = $ssi_variables->{"__{$fmt}__DATE_LOCAL"};
370              
371 14 100       39 if(my $mtime = $ssi_variables->{'LAST_MODIFIED_TS'}) {
372 5         2492 $ssi_variables->{'LAST_MODIFIED'} = POSIX::strftime($fmt, localtime $mtime);
373             }
374              
375 14         25 for my $key (keys %{"$pkg\::"}) {
  14         305  
376 235 100       1000 next if($key eq '__eval_condition');
377 221         2063 delete ${"$pkg\::"}{$key};
  221         1076  
378             }
379 14         97 for my $key (keys %$ssi_variables) {
380 253 50       5677 next if($key eq '__eval_condition');
381 253         960 *{"$pkg\::$key"} = \$ssi_variables->{$key};
  253         4433  
382             }
383             }
384              
385 15         207 warn "eval ($expression)" if Plack::Middleware::SSI::DEBUG;
386              
387 15 100       8312 if(my $res = eval $expression) {
388 11         46 return $res;
389             }
390 4 50       24 if($@) {
391 0         0 warn $@;
392             }
393              
394 4         18 return '';
395             }
396              
397             1;