File Coverage

blib/lib/MojoX/Template/PHP.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package MojoX::Template::PHP;
2 1     1   23 use 5.010;
  1         4  
  1         49  
3 1     1   12 use Mojo::Base -base;
  1         2  
  1         7  
4 1     1   164 use Carp 'croak';
  1         2  
  1         47  
5 1     1   487 use PHP 0.15;
  0            
  0            
6             use Mojo::ByteStream;
7             use Mojo::Exception;
8             use Mojo::Util qw(decode encode monkey_patch slurp url_unescape);
9             use File::Temp;
10             use constant DEBUG => # not used ...
11             $ENV{MOJO_TEMPLATE_DEBUG} || $ENV{MOJOX_TEMPLATE_PHP_DEBUG} || 0;
12              
13             use Data::Dumper;
14             $Data::Dumper::Indent = $Data::Dumper::Sortkeys = 1;
15              
16             our $VERSION = '0.03';
17              
18             #has [qw(auto_escape)];
19             has [qw(code include_file)] => '';
20             has encoding => 'UTF-8'; # documented, not used
21             has name => 'template.php';
22             has template => "";
23              
24             sub interpret {
25             no strict 'refs'; # let callbacks be fully qualified subroutine names
26              
27             my $self = shift;
28             my $c = shift // {};
29             my $log = $c->app->log;
30             local $SIG{__DIE__} = sub {
31             CORE::die($_[0]) if ref $_[0];
32             Mojo::Exception->throw( shift,
33             [ $self->template, $self->include_file, $self->code ] );
34             };
35              
36             PHP::__reset;
37              
38             if (DEBUG) {
39             $log->debug(" Request: ", Dumper($c->req) );
40             }
41              
42             my $callbacks = $c && $c->app->config->{'MojoX::Template::PHP'};
43             $callbacks ||= {};
44              
45             # prepare global variables for the PHP interpreter
46             my $variables_order = PHP::eval_return( "ini_get('variables_order')" );
47             my $cookie_params = { };
48             my $params = $c ? { %{$c->{stash}}, c => $c } : { };
49              
50             if ($variables_order =~ /S/) {
51             $params->{_SERVER} = $self->_server_params($c);
52             $params->{_ENV} = \%ENV;
53             } elsif ($variables_order =~ /E/) {
54             $params->{_ENV} = \%ENV;
55             }
56             if ($variables_order =~ /C/) {
57             $cookie_params = $self->_cookie_params($c);
58             $params->{_COOKIE} = $cookie_params;
59             }
60              
61             $params->{_FILES} = $self->_files_params($c);
62              
63             $self->_set_get_post_request_params( $c, $params, $variables_order );
64              
65             # XXX - should we always set $HTTP_RAW_POST_DATA?
66             my $input = $c->req->body;
67             if (my $len = length($input)) {
68             PHP::set_php_input( "$input" );
69             $params->{HTTP_RAW_POST_DATA} = "$input";
70             }
71              
72             # hook to make adjustments to %$params
73             if ($callbacks && $callbacks->{php_var_preprocessor}) {
74             $callbacks->{php_var_preprocessor}->($params);
75             }
76              
77             if (DEBUG) {
78             $log->debug("Super globals for request " . $self->include_file . ":"
79             . Data::Dumper::Dumper({_GET => $params->{_GET},
80             _POST => $params->{_POST},
81             _REQUEST => $params->{_REQUEST},
82             _FILES => $params->{_FILES},
83             _SERVER => $params->{_SERVER} }));
84             }
85              
86             while (my ($param_name, $param_value) = each %$params) {
87             next if 'CODE' eq ref $param_value;
88             PHP::assign_global($param_name, $param_value);
89             }
90             $c && $c->stash( 'php_params', $params );
91              
92              
93             my $OUTPUT;
94             my $ERROR = "";
95             PHP::options(
96             stdout => sub {
97             $OUTPUT .= $_[0];
98             } );
99             PHP::options(
100             stderr => sub {
101             $ERROR .= $_[0];
102             if ($callbacks && $callbacks->{php_stderr_processor}) {
103             $callbacks->{php_stderr_processor}->($_[0]);
104             }
105             } );
106             PHP::options(
107             header => sub {
108             my ($keyval, $replace) = @_;
109             my ($key,$val) = split /: /, $keyval, 2;
110             my $keep = 1;
111             if ($callbacks && $callbacks->{php_header_processor}) {
112             $keep &&= $callbacks->{php_header_processor}
113             ->($key, $val, $replace);
114             }
115             return if !$keep;
116              
117             if ($replace) {
118             $c->res->headers->header($key,$val);
119             } else {
120             $c->res->headers->add($key,$val);
121             }
122             if ($key =~ /^[Ss]tatus$/) {
123             my ($code) = $val =~ /^\s*(\d+)/;
124             if ($code) {
125             $c->res->code($code);
126             } else {
127             $log->error("Unrecognized Status header: '"
128             . $keyval . "' from PHP");
129             }
130             }
131             } );
132              
133             if (my $ipath = $c->stash("__php_include_path")) {
134             PHP::set_include_path( $ipath );
135             $log->info("include path: $ipath") if DEBUG;
136             }
137              
138             if ($self->include_file) {
139             if (DEBUG) {
140             $log->info("executing " . $self->include_file . " in PHP engine");
141             }
142             eval { PHP::include( $self->include_file ) };
143             } else {
144             my $len = length($self->code);
145             if (DEBUG) {
146             if ($len < 1000) {
147             $log->info("executing code:\n\n" . $self->code
148             . "\nin PHP engine");
149             } else {
150             $log->info("executing $len bytes of code in PHP engine");
151             }
152             }
153             eval { PHP::eval( "?>" . $self->code ); };
154             }
155              
156             if ($@) {
157             if (length($OUTPUT || "") < 1000 || DEBUG) {
158             $log->error("Output from PHP engine: (" . $self->name .
159             "):\n\n" . ($OUTPUT // "") . "\n");
160             } else {
161             $log->error("Output from PHP engine (" . $self->name . "): "
162             . length($OUTPUT) . " bytes");
163             }
164             $log->error("PHP error from template " . $self->name . ": $@");
165              
166             # when does $@ indicate a serious (server) error,
167             # and when can it be ignored? The value of $@ is often
168             # something like "PHP error: PHP::eval failed at
169             # .../i686-linux/PHP.pm line 25.", which sometimes just
170             # means that WordPress called exit()
171              
172             if (!$OUTPUT && $@ !~ /PHP::eval failed at /) {
173             # maybe we are changing the response code to 500 too much
174             $log->info( "changing response code from "
175             . ($c->res->code || "") . " to 500" );
176             $OUTPUT = $@;
177             $c->res->code(500);
178             }
179              
180             undef $@;
181             }
182             if ($ERROR) {
183             $log->warn("Error from PHP: $ERROR");
184             }
185              
186             my $output = $OUTPUT;
187              
188             if ($callbacks && $callbacks->{php_output_postprocessor}) {
189             $callbacks->{php_output_postprocessor}->(
190             \$output, $c && $c->res->headers, $c);
191             }
192             if ($c->res->headers->header('Location')) {
193              
194             # this is disappointing. if the $output string is empty,
195             # Mojo will automatically sets a 404 status code?
196             if ("" eq ($output // "")) {
197             $output = chr(0);
198             }
199             if (!$c->res->code) {
200             $c->res->code(302);
201             } elsif (500 == $c->res->code) {
202             $log->info("changing response code from 500 to 302 because there's a location header");
203             $c->res->code(302);
204             $log->info("output is\n\n" . $output);
205             $log->info("active exception msg is: " . ($@ || ""));
206             undef $@;
207             }
208             }
209              
210             return $output unless $@;
211             return Mojo::Exception->new( $@, [$self->template, $self->code] );
212             }
213              
214             sub _get_upload_metadata {
215             my ($self, $upload) = @_;
216              
217             my ($temp_fh, $tempname) = File::Temp::tempfile( UNLINK => 1 );
218             print $temp_fh $upload->slurp;
219             close $temp_fh;
220             PHP::_spoof_rfc1867( $tempname || "" );
221              
222             return {
223             name => $upload->name,
224             type => $upload->headers->content_type,
225             size => $upload->size,
226             filename => $upload->filename,
227             tmp_name => $tempname,
228             error => 0
229             };
230             }
231              
232             sub _files_params {
233             my ($self, $c) = @_;
234             my $_files = {};
235             my $uploads = $c->req->uploads;
236              
237             if ($uploads) {
238              
239             foreach my $upload (@$uploads) {
240              
241             DEBUG && $c->app->log->debug("\n--------\nUPLOAD:\n---------\n"
242             . Data::Dumper::Dumper($upload)
243             . "\n-------------------\n");
244              
245             my $metadata = $self->_get_upload_metadata($upload);
246             if ($metadata->{name} =~ s/\[\]//) {
247             my $name = $metadata->{name};
248             $metadata->{name} = $metadata->{filename};
249             if ($_files->{$name} && !ref $_files->{$name}) {
250             # upload of foo[] overwrites upload of foo
251             delete $_files->{$name};
252             }
253             for my $attrib (qw(name size type tmp_name error)) {
254             push @{$_files->{$name}{$attrib}},
255             $metadata->{$attrib};
256             }
257             } elsif ($metadata->{name} =~ s/\[(.*?)\]//) {
258             # XXX -- need test in t/20-uploads.t for this branch
259             my $index = $1;
260             my $name = $metadata->{name};
261             $metadata->{name} = delete $metadata->{filename};
262             $_files->{$name}{$index} = $metadata;
263             } else {
264             my $name = $metadata->{name};
265             $metadata->{name} = delete $metadata->{filename};
266             $_files->{$name} = $metadata;
267             }
268             }
269             # $_files = _files_params_000($self, $c);
270             }
271             if (DEBUG && keys %$_files) {
272             $c->app->log->debug("\$_FILES => " . Data::Dumper::Dumper($_files));
273             }
274             return $_files;
275             }
276              
277             sub _cookie_params {
278             my ($self, $c) = @_;
279             if (@{$c->req->cookies}) {
280             $DB::single = 'cookies!';
281             }
282              
283             # Mojo: $c->req->cookies is [], in Catalyst it is {}
284             my $p = {
285             map {;
286             $_->name => url_unescape $_->value
287             } @{$c->req->cookies} };
288             return $p;
289             }
290              
291             sub _server_params {
292             use Socket;
293             use Sys::Hostname;
294             my ($self, $c) = @_;
295              
296             my $tx = $c->tx;
297             my $req = $c->req;
298             my $headers = $req->headers;
299              
300             # see Mojolicious::Plugin::CGI
301             return {
302             CONTENT_LENGTH => $headers->content_length || 0,
303             CONTENT_TYPE => $headers->content_type || 0,
304             GATEWAY_INTERFACE => 'PHP/5.x',
305             HTTP_COOKIE => $headers->cookie || '',
306             HTTP_HOST => $headers->host || '',
307             HTTP_REFERER => $headers->referrer || '',
308             HTTP_USER_AGENT => $headers->user_agent || '',
309             HTTPS => $req->is_secure ? 'YES' : 'NO',
310             PATH_INFO => $req->{__old_path} || $req->url->path->to_string,
311             QUERY_STRING => $req->url->query->to_string,
312             REMOTE_ADDR => $tx->remote_address,
313             REMOTE_HOST => gethostbyaddr( inet_aton( $tx->remote_address ), AF_INET ) || '',
314             REMOTE_PORT => $tx->remote_port,
315             REQUEST_METHOD => $req->method,
316             REQUEST_URI => $req->url->to_string,
317             SERVER_NAME => hostname,
318             SERVER_PORT => $tx->local_port,
319             SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP',
320             SERVER_SOFTWARE => __PACKAGE__
321             };
322             }
323              
324             sub _mojoparams_to_phpparams {
325             my ($query, @order) = @_;
326             my $existing_params = {};
327             foreach my $name ($query->param) {
328             my @p = $query->param($name);
329             $existing_params->{$name} = @p > 1 ? [ @p ] : $p[0];
330             }
331              
332             # XXX - what if parameter value is a Mojo::Upload ? Do we still
333             # save it in the $_GET/$_POST array?
334              
335              
336             # The conventional ways to parse input parameters with Perl (CGI/Catalyst)
337             # are different from the way that PHP parses the input, and we may need
338             # to translate the Perl-style parameters to PHP-style. Some examples:
339             #
340             # 1. foo=first&foo=second&foo=last
341             #
342             # In Perl, value for the parameter 'foo' is an array ref with 3 values
343             # In PHP, value for param 'foo' is 'last', whatever the last value was
344             # See also example #5
345             #
346             # 2. foo[bar]=value1&foo[baz]=value2
347             #
348             # In Perl, this creates scalar parameters 'foo[bar]' and 'foo[baz]'
349             # In PHP, this creates the parameter 'foo' with an associative array
350             # value ('bar'=>'value1', 'baz'=>'value2')
351             #
352             # 3. foo[bar]=value1&foo=value2&foo[baz]=value3
353             #
354             # In Perl, this creates parameters 'foo[bar]', 'foo', and 'foo[baz]'
355             # In PHP, this create the parameter 'foo' with an associative array
356             # with value ('baz'=>'value3'). The values associated with
357             # 'foo[bar]' and 'foo' are lost.
358             #
359             # 4. foo[2][bar]=value1&foo[2][baz]=value2
360             #
361             # In Perl, this creates parameters 'foo[2][bar]' and 'foo[2][baz]'
362             # In PHP, this creates a 2-level hash 'foo'
363             #
364             # 5. foo[]=123&foo[]=234&foo[]=345
365             # In Perl, parameter 'foo[]' assigned to array ref [123,234,345]
366             # In PHP, parameter 'foo' is an array with elem (123,234,345)
367             #
368             # For a given set of Perl-parsed parameter input, this function returns
369             # a hashref that resembles what the same parameters would look like
370             # to PHP.
371              
372             my $new_params = {};
373             foreach my $pp (@order) {
374             my $p = $pp;
375             if ($p =~ s/\[(.+)\]$//) {
376             my $key = $1;
377             s/%(..)/chr hex $1/ge for $p, $pp, $key;
378              
379             if ($key ne '' && $new_params->{$p}
380             && ref($new_params->{$p} ne 'HASH')) {
381             $new_params->{$p} = {};
382             }
383              
384             # XXX - how to generalize this from 2 to n level deep hash?
385             if ($key =~ /\]\[/) {
386             my ($key1, $key2) = split /\]\[/, $key;
387             $new_params->{$p}{$key1}{$key2} = $existing_params->{$pp};
388             } else {
389             $new_params->{$p}{$key} = $existing_params->{$pp};
390             }
391             } elsif ($p =~ s/\[\]$//) {
392             # expect $existing_params->{$pp} to already be an array ref
393             $p =~ s/%(..)/chr hex $1/ge;
394             $new_params->{$p} = $existing_params->{$pp};
395             } else {
396             $p =~ s/%(..)/chr hex $1/ge;
397             $new_params->{$p} = $existing_params->{$p};
398             if ('ARRAY' eq ref $new_params->{$p}) {
399             $new_params->{$p} = $new_params->{$p}[-1];
400             }
401             }
402             }
403             return $new_params;
404             }
405              
406             sub _set_get_post_request_params {
407             my ($self, $c, $params, $var_order) = @_;
408             my $order = PHP::eval_return( 'ini_get("request_order")' ) || $var_order;
409             $params->{$_} = {} for qw(_GET _POST _REQUEST);
410             if ($var_order =~ /G/) {
411             my $query = $c->req->url && $c->req->url->query;
412             if ($query) {
413             $query =~ s/%(5[BD])/chr hex $1/ge;
414             my @order = map { s/=.*//; $_ } split /&/, $query;
415             $params->{_GET} = _mojoparams_to_phpparams(
416             $c->req->url->query, @order );
417             }
418             }
419              
420             if ($var_order =~ /P/ && $c->req->method eq 'POST') {
421             my $order = [ $c->req->body_params->param ];
422             $params->{_POST} = _mojoparams_to_phpparams(
423             $c->req->body_params, @$order );
424             }
425              
426             $params->{_REQUEST} = {};
427             foreach my $reqvar (split //, uc $order) {
428             if ($reqvar eq 'C') {
429             $params->{_REQUEST} = { %{$params->{_REQUEST}},
430             %{$params->{_COOKIE}} };
431             } elsif ($reqvar eq 'G') {
432             $params->{_REQUEST} = { %{$params->{_REQUEST}},
433             %{$params->{_GET}} };
434             } elsif ($reqvar eq 'P') {
435             $params->{_REQUEST} = { %{$params->{_REQUEST}},
436             %{$params->{_POST}} };
437             }
438             }
439             return;
440             }
441              
442             sub render {
443             my $self = shift;
444             my $c = pop if @_ && ref $_[-1];
445             $self->code( join '', @_ );
446             $self->include_file('');
447             return $self->interpret($c);
448             }
449              
450             sub render_file {
451             my ($self, $path) = (shift, shift);
452             $self->name($path) unless defined $self->{name};
453             $self->include_file($path);
454             return $self->interpret(@_);
455             }
456              
457             unless (caller) {
458             my $mt = MojoX::Template::PHP->new;
459             my $output = $mt->render(<<'EOF');
460            
461             Simple
462            
463             Time:
464            
465            
466             EOF
467             say $output;
468              
469             open my $fh, '>/tmp/test.php' or die;
470             print $fh <<'EOF';
471            
472             HeLlO WoRlD!
473            
474             EOF
475             close $fh;
476             $output = $mt->render_file( '/tmp/test.php' );
477             say $output;
478             unlink '/tmp/test.php';
479             }
480              
481             1;
482              
483             =encoding utf8
484              
485             =head1 NAME
486              
487             MojoX::Template::PHP - PHP processing engine for MojoX::Plugin::PHP
488              
489             =head1 VERSION
490              
491             0.03
492              
493             =head1 SYNOPSIS
494              
495             use MojoX::Template::PHP;
496             my $mt = MojoX::Template::PHP->new;
497             my $output = $mt->render(<<'EOF');
498            
499             Simple
500             Time:
501            
502            
503            
504             EOF
505             say $output;
506              
507             my $output = $mt->render_file( '/path/to/some/template.php' );
508             say $output;
509              
510             =head1 DESCRIPTION
511              
512             L is a way to use PHP as a templating
513             system for your Mojolicious application.
514              
515             =over 4
516              
517             =item 1. You can put a Mojolicious wrapper around some decent
518             PHP application (say, WordPress)
519              
520             =item 2. You are on a development project with Perl and PHP
521             programmers, and you want to use Mojolicious as a backend
522             without scaring the PHP developers.
523              
524             =back
525              
526             =head1 ATTRIBUTES
527              
528             L implements the following attributes:
529              
530             =head2 code
531              
532             my $code = $mt->code;
533             $mt = $mt->code($code);
534              
535             Inline PHP code for template. The L<"interpret"> method
536             will check the L<"include_file"> attribute first, and then
537             this attribute to decide what to pass to the PHP interpreter.
538              
539             =head2 encoding
540              
541             my $encoding = $mt->encoding;
542             $mt = $mt->encoding( $charset );
543              
544             Encoding used for template files.
545              
546             =head2 include_file
547              
548             my $file = $mt->include_file;
549             $mt = $mt->include_file( $path );
550              
551             PHP template file to be interpreted. The L<"interpret"> method
552             will check this attribute, and then the L<"code"> attribute
553             to decide what to pass to the PHP interpreter.
554              
555             =head2 name
556              
557             my $name = $mt->name;
558             $mt = $mt->name('foo.php');
559              
560             Name of the template currently being processed. Defaults to
561             C. This value should not contain quotes or
562             newline characters, or error messages might end up being wrong.
563              
564             =head2 template
565              
566             my $template = $mt->template;
567             $mt = $mt->template( $template_name );
568              
569             Should contain the name of the template currently being processed,
570             but I don't think it is ever set to anything now. This value will
571             appear in exception messages.
572              
573             =head1 METHODS
574              
575             L inherits all methods from
576             L, and the following new ones:
577              
578             =head2 interpret
579              
580             my $output = $mt->interpret($c)
581              
582             Interpret template code. Starts the PHP engine and evaluates the
583             template code with it. See L<"CONFIG"/MojoX::Plugin::PHP> for
584             information about various callbacks that can be used to change
585             and extend the behavior of the PHP templating engine.
586              
587             =head2 render
588              
589             my $output = $mt->render($template);
590              
591             Render a PHP template.
592              
593             =head2 render_file
594              
595             my $output = $mt->render_file( $php_file_path );
596              
597             Render template file.
598              
599             =cut
600              
601             #=head1 DEBUGGING
602             #
603             #You can set either the C or
604             #C environment variable to enable
605             #some diagnostics information printed to C.
606              
607             =head1 SEE ALSO
608              
609             L, L, L,
610             L
611              
612             =head1 AUTHOR
613              
614             Marty O'Brien Emob@cpan.orgE
615              
616             =head1 COPYRIGHT
617              
618             Copyright 2013, Marty O'Brien. All rights reserved.
619              
620             This library is free software; you can redistribute it and/or modify it
621             under the terms of either: the GNU General Public License as published
622             by the Free Sortware Foundation; or the Artistic License.
623              
624             See http://dev.perl.org/licenses for more information.
625              
626             =cut