File Coverage

blib/lib/Plack/Middleware/TazXSLT.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Plack::Middleware::TazXSLT;
2 3     3   35608 use strict;
  3         9  
  3         90  
3 3     3   15 use warnings;
  3         5  
  3         75  
4              
5 3     3   1640 use parent qw( Plack::Middleware );
  3         587  
  3         19  
6             use Plack::Util::Accessor
7 3     3   39408 qw(user_agent xml_parser xslt_parser name timeout log_filter response request );
  3         5  
  3         19  
8 3     3   4344 use XML::LibXML;
  0            
  0            
9             use XML::LibXSLT;
10             use LWP::UserAgent;
11             use HTTP::Response;
12             use HTTP::Message::PSGI;
13             use Plack::Response;
14             use Plack::Request;
15             use Try::Tiny;
16             use Capture::Tiny qw(capture);
17             use URI::QueryParam;
18             use Plack::Util;
19              
20             use constant TAZ_XSLT_PROFILE => $ENV{TAZ_XSLT_PROFILE};
21              
22             our $VERSION = '0.55';
23              
24             sub HTTP::Response::to_psgi {
25             my ($self) = @_;
26             return Plack::Response->new( $self->code, $self->headers, $self->content )
27             ->finalize;
28             }
29              
30             sub prepare_app {
31             my $self = shift;
32             $self->timeout(19) unless defined $self->timeout;
33             $self->name('tazxslt') unless defined $self->name;
34             $self->xslt_parser( XML::LibXSLT->new() ) unless defined $self->xslt_parser;
35             $self->xml_parser( XML::LibXML->new() ) unless defined $self->xml_parser;
36             $self->log_filter( \&parse_libxml_error ) unless defined $self->log_filter;
37             $self->user_agent( $self->build_user_agent() )
38             unless defined $self->user_agent;
39             return;
40             }
41              
42             sub call {
43             my ( $self, $env ) = @_;
44             my $request = Plack::Request->new($env);
45             my $uri = $request->uri;
46             my $res = $self->unbuffer( $self->app->($env) );
47             my $backend_response = HTTP::Response->from_psgi($res);
48              
49             $self->request( $request );
50             $self->response( $backend_response );
51              
52             if ( !$self->is_transformable_response() ) {
53             $backend_response->header( x_taz_mode => 'proxy' );
54             return $backend_response->to_psgi;
55             }
56              
57             my $content_type;
58             my $response = try {
59             my $xml_dom = $self->parse_xml();
60             my $xsl_uri = try { $self->find_pi( $xml_dom ) };
61              
62             if ( !$xsl_uri ) {
63             $self->response->header( x_taz_mode => 'proxy' );
64             return $self->response;
65             }
66              
67             $xsl_uri = URI->new_abs( $xsl_uri, $request->uri );
68              
69             my $xslt_response = $self->get_stylesheet($xsl_uri);
70              
71             $self->xml_parser->base_uri($xsl_uri);
72              
73             my $xslt_dom = $self->parse_stylesheet($xslt_response);
74              
75             $self->register_elements( $xslt_dom, $xml_dom );
76              
77             my $result;
78             my ( $stdout, $stderr ) = do {
79              
80             ## stdin, stderr and stdout needs to be localised to
81             ## retain the originals values when running under fcgi, see
82             ## https://metacpan.org/module/Capture::Tiny#Modifying-filehandles-before-capturing
83              
84             local ( *STDIN, *STDERR, *STDOUT );
85             capture {
86             if (TAZ_XSLT_PROFILE) {
87             my $start = [Time::HiRes::gettimeofday];
88             $result =
89             $self->apply_transformation( $xslt_dom, $xml_dom );
90             my $end = [Time::HiRes::gettimeofday];
91             printf STDERR "xslt-processing-time: %.6f\n",
92             Time::HiRes::tv_interval $start, $end;
93             }
94             else {
95             $result =
96             $self->apply_transformation( $xslt_dom, $xml_dom );
97             }
98             };
99             };
100              
101             if ($stderr) {
102             $env->{'psgi.errors'}->print("[$uri] $stderr");
103             }
104              
105             $content_type = $xslt_dom->media_type();
106             $content_type .= ';charset=' . $xslt_dom->output_encoding();
107              
108             my $content = $xslt_dom->output_as_bytes($result);
109             $backend_response->content_length(
110             do { use bytes; length $content }
111             );
112             $self->response->content_type($content_type);
113             $self->response->content($content);
114             $self->response->header( x_taz_mode => 'transform' );
115             return $self->response;
116             }
117             catch {
118             $env->{'psgi.errors'}->print("[$uri] $_");
119             return HTTP::Response->new(500);
120             };
121              
122             return $response->to_psgi;
123             }
124              
125             sub parse_stylesheet {
126             my ( $self, $response ) = @_;
127             my $xslt_dom = try {
128             $self->xslt_parser->parse_stylesheet(
129             $self->xml_parser->parse_string( $response->decoded_content ) );
130             }
131             catch {
132             die "Can't parse stylesheet: $_";
133             };
134             return $xslt_dom;
135             }
136              
137             sub apply_transformation {
138             my ( $self, $xslt_dom, $xml_dom ) = @_;
139             my $result;
140             try {
141             $result = $xslt_dom->transform( $xml_dom, $self->xslt_variables() );
142             }
143             catch {
144             s/\s at \s \S+ \s line \s \d+ [.\s]+//smx;
145             die "Error while transforming (died): $_\n";
146             };
147             return $result;
148             }
149              
150             sub parse_libxml_error {
151             my $libxml_error = shift;
152             $libxml_error =~ s/\A(.+?)\s+\^.*/$1/sm;
153             $libxml_error =~ s/\s+/ /smg;
154             return $libxml_error;
155             }
156              
157             sub get_stylesheet {
158             my ( $self, $xsl_uri ) = @_;
159             my $response = $self->user_agent->get($xsl_uri);
160              
161             if ( not $response->is_success ) {
162             die "Can't get xslt stylesheet: " . $response->status_line() . "\n";
163             }
164             return $response;
165             }
166              
167             sub is_transformable_response {
168             my $self = shift;
169              
170             return 0
171             if $self->response->is_redirect
172             || !$self->response->is_success
173             || $self->request->method eq 'HEAD'
174             || ( defined $self->response->content_length
175             && $self->response->content_length == 0 )
176             || !$self->response->content_is_xml;
177             return 1;
178             }
179              
180             sub parse_xml {
181             my $self = shift;
182             my $body = $self->response->content;
183             my $xml_dom = try {
184             $self->xml_parser->parse_string($body);
185             }
186             catch {
187             s/\s at \s \S+ \s line \s \d+ .*?$//smx;
188             die "Can't parse xml: " . $self->log_filter->($_) . "\n";
189             };
190             return $xml_dom;
191             }
192              
193             sub find_pi {
194             my ( $self, $dom ) = @_;
195             my $xsl_uri;
196             if ( $self->request->header('X-Taz-XSLT-Stylesheet') ) {
197             $xsl_uri = $self->request->header('X-Taz-XSLT-Stylesheet');
198             }
199             else {
200             my $stylesheet_href;
201             my $pi_str =
202             ( $dom->findnodes('processing-instruction()') )[0]->getData;
203             if ( $pi_str
204             and ( $pi_str =~ m{type="text/xsl} or $pi_str !~ /type=/ ) )
205             {
206             ($stylesheet_href) = ( $pi_str =~ m{href="([^"]*)"} );
207             }
208             if ($stylesheet_href) {
209             $xsl_uri = replace_header( $self->request, $stylesheet_href );
210             }
211             }
212             return $xsl_uri;
213             }
214              
215             sub register_elements {
216             my ( $self, $xslt_dom, $xml_dom ) = @_;
217              
218             $xslt_dom->register_element( 'http://www.mod-xslt2.com/ns/1.0', 'header-set', sub { return; } );
219              
220             $xslt_dom->register_function( 'http://taz.de/xmlns/tazxslt/http_response',
221             'header',
222             sub {
223             $self->response->header( @_ );
224             }
225             );
226              
227             $xslt_dom->register_function( 'http://taz.de/xmlns/tazxslt/http_response',
228             'code',
229             sub {
230             $self->response->code( @_ );
231             }
232             );
233              
234             $xslt_dom->register_element(
235             'http://www.mod-xslt2.com/ns/1.0',
236             'value-of',
237             sub {
238             my $string = $_[2]->getAttribute("select");
239             return XML::LibXML::Text->new(
240             replace_header( $self->request, $string ) );
241             }
242             );
243             return;
244             }
245              
246             sub build_user_agent {
247             my $self = shift;
248             my $ua = LWP::UserAgent->new;
249             $ua->timeout( $self->timeout );
250             $ua->env_proxy;
251             return $ua;
252             }
253              
254             sub xslt_variables {
255             my $self = shift;
256             return XML::LibXSLT::xpath_to_string(
257             'modxslt-name' => $self->name,
258             'modxslt-version' => $self->VERSION,
259             );
260             }
261              
262             sub replace_header {
263             my ( $request, $string ) = @_;
264             $string =~ s/\$HEADER\[(.*?)\]/$request->header($1)||''/ge;
265             $string =~ s/\$GET\[(.*?)\]/$request->uri->query_param($1)||''/ge;
266             return $string;
267             }
268              
269             sub unbuffer {
270             my ( $self, $res ) = @_;
271             return $res if ref($res) ne 'CODE';
272              
273             my $ret;
274             $res->(
275             sub {
276             my $write = shift;
277             if ( @$write == 2 ) {
278             my @body;
279             $ret = [ @$write, \@body ];
280             return Plack::Util::inline_object(
281             write => sub { push @body, $_[0] },
282             close => sub { },
283             );
284             }
285             else {
286             $ret = $write;
287             return;
288             }
289             }
290             );
291             return $ret;
292             }
293              
294             1;
295              
296             __END__