File Coverage

blib/lib/MojoX/Template/PHP.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


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