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   30 use 5.010;
  1         3  
3 1     1   9 use Mojo::Base -base;
  1         1  
  1         6  
4 1     1   107 use Carp 'croak';
  1         2  
  1         45  
5 1     1   333 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.04';
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             $DB::single = 1;
290              
291             # Mojo: $c->req->cookies is [], in Catalyst it is {}
292             my $p = {
293             map {;
294             $_->name => url_unescape $_->value
295             } @{$c->req->cookies} };
296             return $p;
297             }
298              
299             sub _server_params {
300             use Socket;
301             use Sys::Hostname;
302             my ($self, $c) = @_;
303              
304             my $tx = $c->tx;
305             my $req = $c->req;
306             my $headers = $req->headers;
307              
308             # see Mojolicious::Plugin::CGI
309             return {
310             CONTENT_LENGTH => $headers->content_length || 0,
311             CONTENT_TYPE => $headers->content_type || 0,
312             GATEWAY_INTERFACE => 'PHP/5.x',
313             HTTP_COOKIE => $headers->cookie || '',
314             HTTP_HOST => $headers->host || '',
315             HTTP_REFERER => $headers->referrer || '',
316             HTTP_USER_AGENT => $headers->user_agent || '',
317             HTTPS => $req->is_secure ? 'YES' : 'NO',
318             PATH_INFO => $req->{__old_path} || $req->url->path->to_string,
319             QUERY_STRING => $req->url->query->to_string,
320             REMOTE_ADDR => $tx->remote_address,
321             REMOTE_HOST => gethostbyaddr( inet_aton( $tx->remote_address ), AF_INET ) || '',
322             REMOTE_PORT => $tx->remote_port,
323             REQUEST_METHOD => $req->method,
324             REQUEST_URI => $req->url->to_string,
325             SERVER_NAME => hostname,
326             SERVER_PORT => $tx->local_port,
327             SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP',
328             SERVER_SOFTWARE => __PACKAGE__
329             };
330             }
331              
332             sub _mojoparams_to_phpparams {
333             my ($query, @order) = @_;
334             my $existing_params = {};
335             if ($Mojolicious::VERSION >= 6.00) {
336             my $p = $query->to_hash;
337             while (my ($k,$v) = each %$p) {
338             $existing_params->{$k} = $v;
339             }
340             } else {
341             my @names = $query->param;
342             foreach my $name (@names) {
343             my @p = $query->param($name);
344             $existing_params->{$name} = @p > 1 ? [ @p ] : $p[0];
345             }
346             }
347              
348             # XXX - what if parameter value is a Mojo::Upload ? Do we still
349             # save it in the $_GET/$_POST array?
350              
351              
352             # The conventional ways to parse input parameters with Perl (CGI/Catalyst)
353             # are different from the way that PHP parses the input, and we may need
354             # to translate the Perl-style parameters to PHP-style. Some examples:
355             #
356             # 1. foo=first&foo=second&foo=last
357             #
358             # In Perl, value for the parameter 'foo' is an array ref with 3 values
359             # In PHP, value for param 'foo' is 'last', whatever the last value was
360             # See also example #5
361             #
362             # 2. foo[bar]=value1&foo[baz]=value2
363             #
364             # In Perl, this creates scalar parameters 'foo[bar]' and 'foo[baz]'
365             # In PHP, this creates the parameter 'foo' with an associative array
366             # value ('bar'=>'value1', 'baz'=>'value2')
367             #
368             # 3. foo[bar]=value1&foo=value2&foo[baz]=value3
369             #
370             # In Perl, this creates parameters 'foo[bar]', 'foo', and 'foo[baz]'
371             # In PHP, this create the parameter 'foo' with an associative array
372             # with value ('baz'=>'value3'). The values associated with
373             # 'foo[bar]' and 'foo' are lost.
374             #
375             # 4. foo[2][bar]=value1&foo[2][baz]=value2
376             #
377             # In Perl, this creates parameters 'foo[2][bar]' and 'foo[2][baz]'
378             # In PHP, this creates a 2-level hash 'foo'
379             #
380             # 5. foo[]=123&foo[]=234&foo[]=345
381             # In Perl, parameter 'foo[]' assigned to array ref [123,234,345]
382             # In PHP, parameter 'foo' is an array with elem (123,234,345)
383             #
384             # For a given set of Perl-parsed parameter input, this function returns
385             # a hashref that resembles what the same parameters would look like
386             # to PHP.
387              
388             my $new_params = {};
389             foreach my $pp (@order) {
390             my $p = $pp;
391             if ($p =~ s/\[(.+)\]$//) {
392             my $key = $1;
393             s/%(..)/chr hex $1/ge for $p, $pp, $key;
394              
395             if ($key ne '' && $new_params->{$p}
396             && ref($new_params->{$p} ne 'HASH')) {
397             $new_params->{$p} = {};
398             }
399              
400             # XXX - how to generalize this from 2 to n level deep hash?
401             if ($key =~ /\]\[/) {
402             my ($key1, $key2) = split /\]\[/, $key;
403             $new_params->{$p}{$key1}{$key2} = $existing_params->{$pp};
404             } else {
405             $new_params->{$p}{$key} = $existing_params->{$pp};
406             }
407             } elsif ($p =~ s/\[\]$//) {
408             # expect $existing_params->{$pp} to already be an array ref
409             $p =~ s/%(..)/chr hex $1/ge;
410             $new_params->{$p} = $existing_params->{$pp};
411             } else {
412             $p =~ s/%(..)/chr hex $1/ge;
413             $new_params->{$p} = $existing_params->{$p};
414             if ('ARRAY' eq ref $new_params->{$p}) {
415             $new_params->{$p} = $new_params->{$p}[-1];
416             }
417             }
418             }
419             return $new_params;
420             }
421              
422             sub _set_get_post_request_params {
423             my ($self, $c, $params, $var_order) = @_;
424             my $order = PHP::eval_return( 'ini_get("request_order")' ) || $var_order;
425             $params->{$_} = {} for qw(_GET _POST _REQUEST);
426             if ($var_order =~ /G/) {
427             my $query = $c->req->url && $c->req->url->query;
428             if ($query) {
429             $query =~ s/%(5[BD])/chr hex $1/ge;
430             my @order = map { s/=.*//; $_ } split /&/, $query;
431             $params->{_GET} = _mojoparams_to_phpparams(
432             $c->req->url->query, @order );
433             }
434             }
435             if ($var_order =~ /P/ && $c->req->method eq 'POST') {
436             my $order = $Mojolicious::VERSION >= 6.00
437             ? $c->req->body_params->names
438             : [ $c->req->body_params->param ];
439             $params->{_POST} = _mojoparams_to_phpparams(
440             $c->req->body_params, @$order );
441             }
442              
443             $params->{_REQUEST} = {};
444             foreach my $reqvar (split //, uc $order) {
445             if ($reqvar eq 'C') {
446             $params->{_REQUEST} = { %{$params->{_REQUEST}},
447             %{$params->{_COOKIE}} };
448             } elsif ($reqvar eq 'G') {
449             $params->{_REQUEST} = { %{$params->{_REQUEST}},
450             %{$params->{_GET}} };
451             } elsif ($reqvar eq 'P') {
452             $params->{_REQUEST} = { %{$params->{_REQUEST}},
453             %{$params->{_POST}} };
454             }
455             }
456             return;
457             }
458              
459             sub render {
460             my $self = shift;
461             my $c = pop if @_ && ref $_[-1];
462             $self->code( join '', @_ );
463             $self->include_file('');
464             return $self->interpret($c);
465             }
466              
467             sub render_file {
468             my ($self, $path) = (shift, shift);
469             $self->name($path) unless defined $self->{name};
470             $self->include_file($path);
471             return $self->interpret(@_);
472             }
473              
474             unless (caller) {
475             my $mt = MojoX::Template::PHP->new;
476             my $output = $mt->render(<<'EOF');
477            
478             Simple
479            
480             Time:
481            
482            
483             EOF
484             say $output;
485              
486             open my $fh, '>/tmp/test.php' or die;
487             print $fh <<'EOF';
488            
489             HeLlO WoRlD!
490            
491             EOF
492             close $fh;
493             $output = $mt->render_file( '/tmp/test.php' );
494             say $output;
495             unlink '/tmp/test.php';
496             }
497              
498             1;
499              
500             =encoding utf8
501              
502             =head1 NAME
503              
504             MojoX::Template::PHP - PHP processing engine for MojoX::Plugin::PHP
505              
506             =head1 VERSION
507              
508             0.04
509              
510             =head1 SYNOPSIS
511              
512             use MojoX::Template::PHP;
513             my $mt = MojoX::Template::PHP->new;
514             my $output = $mt->render(<<'EOF');
515            
516             Simple
517             Time:
518            
519            
520            
521             EOF
522             say $output;
523              
524             my $output = $mt->render_file( '/path/to/some/template.php' );
525             say $output;
526              
527             =head1 DESCRIPTION
528              
529             L is a way to use PHP as a templating
530             system for your Mojolicious application.
531              
532             =over 4
533              
534             =item 1. You can put a Mojolicious wrapper around some decent
535             PHP application (say, WordPress)
536              
537             =item 2. You are on a development project with Perl and PHP
538             programmers, and you want to use Mojolicious as a backend
539             without scaring the PHP developers.
540              
541             =back
542              
543             =head1 ATTRIBUTES
544              
545             L implements the following attributes:
546              
547             =head2 code
548              
549             my $code = $mt->code;
550             $mt = $mt->code($code);
551              
552             Inline PHP code for template. The L<"interpret"> method
553             will check the L<"include_file"> attribute first, and then
554             this attribute to decide what to pass to the PHP interpreter.
555              
556             =head2 encoding
557              
558             my $encoding = $mt->encoding;
559             $mt = $mt->encoding( $charset );
560              
561             Encoding used for template files.
562              
563             =head2 include_file
564              
565             my $file = $mt->include_file;
566             $mt = $mt->include_file( $path );
567              
568             PHP template file to be interpreted. The L<"interpret"> method
569             will check this attribute, and then the L<"code"> attribute
570             to decide what to pass to the PHP interpreter.
571              
572             =head2 name
573              
574             my $name = $mt->name;
575             $mt = $mt->name('foo.php');
576              
577             Name of the template currently being processed. Defaults to
578             C. This value should not contain quotes or
579             newline characters, or error messages might end up being wrong.
580              
581             =head2 template
582              
583             my $template = $mt->template;
584             $mt = $mt->template( $template_name );
585              
586             Should contain the name of the template currently being processed,
587             but I don't think it is ever set to anything now. This value will
588             appear in exception messages.
589              
590             =head1 METHODS
591              
592             L inherits all methods from
593             L, and the following new ones:
594              
595             =head2 interpret
596              
597             my $output = $mt->interpret($c)
598              
599             Interpret template code. Starts the PHP engine and evaluates the
600             template code with it. See L<"CONFIG"/MojoX::Plugin::PHP> for
601             information about various callbacks that can be used to change
602             and extend the behavior of the PHP templating engine.
603              
604             =head2 render
605              
606             my $output = $mt->render($template);
607              
608             Render a PHP template.
609              
610             =head2 render_file
611              
612             my $output = $mt->render_file( $php_file_path );
613              
614             Render template file.
615              
616             =cut
617              
618             #=head1 DEBUGGING
619             #
620             #You can set either the C or
621             #C environment variable to enable
622             #some diagnostics information printed to C.
623              
624             =head1 SEE ALSO
625              
626             L, L, L,
627             L
628              
629             =head1 AUTHOR
630              
631             Marty O'Brien Emob@cpan.orgE
632              
633             =head1 COPYRIGHT
634              
635             Copyright 2013-2015, Marty O'Brien. All rights reserved.
636              
637             This library is free software; you can redistribute it and/or modify it
638             under the terms of either: the GNU General Public License as published
639             by the Free Sortware Foundation; or the Artistic License.
640              
641             See http://dev.perl.org/licenses for more information.
642              
643             =cut