File Coverage

blib/lib/Moxy.pm
Criterion Covered Total %
statement 151 298 50.6
branch 13 50 26.0
condition 5 31 16.1
subroutine 38 52 73.0
pod 1 9 11.1
total 208 440 47.2


line stmt bran cond sub pod time code
1             package Moxy;
2 14     14   269431 use 5.00800;
  14         55  
  14         550  
3 14     14   71 use strict;
  14         23  
  14         498  
4 14     14   78 use warnings;
  14         38  
  14         459  
5 14     14   73 use base qw/Class::Accessor::Fast/;
  14         23  
  14         16824  
6 14     14   72174 use Class::Component 0.16;
  14         267723  
  14         117  
7              
8             our $VERSION = '0.70';
9              
10 14     14   1371 use Carp;
  14         25  
  14         1107  
11 14     14   15604 use Encode;
  14         196351  
  14         1377  
12 14     14   13719 use File::Spec::Functions;
  14         13066  
  14         1273  
13 14     14   89 use File::Basename;
  14         28  
  14         1332  
14 14     14   13028 use FindBin;
  14         16172  
  14         623  
15 14     14   14712 use HTML::Entities;
  14         107609  
  14         1609  
16 14     14   128 use HTML::Parser;
  14         22  
  14         178  
17 14     14   15704 use HTML::TreeBuilder::XPath;
  14         1110087  
  14         183  
18 14     14   619 use HTML::TreeBuilder;
  14         34  
  14         108  
19 14     14   15231 use HTTP::Cookies;
  14         216330  
  14         228  
20 14     14   12217 use HTTP::Session;
  14         64827  
  14         133  
21 14     14   18636 use LWP::UserAgent;
  14         625900  
  14         215  
22 14     14   14802 use MIME::Base64;
  14         21198  
  14         1093  
23 14     14   8134 use Moxy::Util;
  14         51  
  14         232  
24 14     14   18429 use Params::Validate ':all';
  14         127974  
  14         3415  
25 14     14   12535 use Path::Class;
  14         784953  
  14         1121  
26 14     14   217 use Scalar::Util qw/blessed/;
  14         35  
  14         758  
27 14     14   127 use UNIVERSAL::require;
  14         33  
  14         133  
28 14     14   300 use URI::Escape;
  14         32  
  14         865  
29 14     14   14051 use URI::Heuristic qw(uf_uristr);
  14         40222  
  14         1024  
30 14     14   136 use URI;
  14         30  
  14         204  
31 14     14   12579 use YAML;
  14         112469  
  14         885  
32 14     14   14285 use Time::HiRes ();
  14         27769  
  14         422  
33 14     14   12626 use Plack::Response;
  14         37401  
  14         197  
34 14     14   8080 use Moxy::Request;
  14         59  
  14         163  
35 14     14   14008 use HTTP::Message::PSGI;
  14         82481  
  14         877  
36 14     14   197 use File::Temp;
  14         31  
  14         1303  
37 14     14   76 use File::Spec;
  14         28  
  14         106  
38 14         212 use HTTP::MobileAttribute plugins => [
39             qw/CarrierLetter IS/,
40             {
41             module => 'Display',
42             config => {
43             DoCoMoMap => YAML::LoadFile(
44             catfile( 'assets', 'common', 'docomo-display-map.yaml' )
45             )
46             }
47             },
48 14     14   13614 ];
  14         681317  
49              
50             __PACKAGE__->load_components(qw/Plaggerize Autocall::InjectMethod Context/);
51              
52             __PACKAGE__->load_plugins(qw/
53             DisplayWidth ControlPanel LocationBar Pictogram
54             Status::401 Status::500 Status::404
55             UserID XMLisHTML UserAgentSwitcher RefererCutter CookieCutter FlashUseImgTag
56             DisableTableTag GPS HTTPHeader QRCode ShowHTTPHeaders
57             /);
58             __PACKAGE__->mk_accessors(qw/response_time/);
59              
60             sub new {
61 11     11 1 263855 my ($class, $config) = @_;
62              
63 11 50       79 if ( $config->{global}->{plugins} ) {
64 0         0 $class->load_plugins(@{ $config->{global}->{plugins} });
  0         0  
65             }
66              
67 11   100     469 $config->{global}->{log}->{level} ||= 'info';
68              
69 11   66     270 $config->{global}->{assets_path} ||= do {
70 9         34 my $libpath = $INC{'Moxy.pm'};
71 9         264 $libpath =~ s!(?:blib/)?lib/+Moxy\.pm$!!;
72 9   50     44 $libpath ||= './';
73 9         1006 $libpath = File::Spec->rel2abs($libpath);
74 9         464 File::Spec->catdir($libpath, 'assets');
75             };
76              
77 11         644 my $self = $class->NEXT( 'new' => { config => $config } );
78              
79             $self->conf->{global}->{session}->{store} ||= +{
80             module => 'File',
81             config => {
82 0   0     0 dir => do {
83 0         0 my $dir = File::Temp::tempdir('moxyXXXXXX', CLEANUP => 1, DIR => File::Spec->tmpdir);
84 0         0 $self->{__session} = $dir;
85 0         0 "$dir", # we need stringify for file::temp
86             }
87             },
88             };
89              
90 0   0     0 $self->conf->{global}->{log}->{fh} ||= \*STDERR;
91              
92 0         0 return $self;
93             }
94              
95 0     0 0 0 sub assets_path { shift->conf->{global}->{assets_path} }
96              
97             sub res {
98 0     0 0 0 Plack::Response->new(@_);
99             }
100             sub HTTP::Response::to_plack_response {
101 0     0 0 0 my $self = shift;
102 0         0 return res(
103             $self->code,
104             $self->headers,
105             $self->content,
106             );
107             }
108              
109             # -------------------------------------------------------------------------
110              
111             sub run_hook_and_get_response {
112 0     0 0 0 my ($self, $hook, @args) = @_;
113              
114 0         0 $self->log(debug => "Run hook and get response: $hook");
115 0         0 for my $action (@{$self->class_component_hooks->{$hook}}) {
  0         0  
116 0         0 my $code = $action->{plugin}->can($action->{method});
117 0         0 my $response = $code->($action->{plugin}, $self, @args);
118 0 0 0     0 return $response if blessed $response && $response->isa('HTTP::Response');
119             }
120 0         0 return; # not finished yet
121             }
122              
123             sub rewrite_css {
124 2     2 0 10138 my ($base, $css, $url) = @_;
125 2         15 my $base_url = URI->new($url);
126              
127 2         8514 $css =~ s{url\(([^\)]+)\)}{
128 2         6 my $x = $1;
129 2 50       173 sprintf "url(%s%s%s)",
130             $base,
131             ($base =~ m{/$} ? '' : '/'),
132             uri_escape( URI->new($x)->abs($base_url) )
133             }ge;
134              
135 2         3469 $css;
136             }
137              
138             sub rewrite_html {
139 12     12 0 27824 my ($base, $html, $url) = @_;
140              
141 12         53 my $base_url = URI->new($url);
142              
143             # parse.
144 12         9812 my $tree = HTML::TreeBuilder::XPath->new;
145 12         2425 $tree->implicit_tags(0);
146 12         117 $tree->no_space_compacting(1);
147 12         100 $tree->ignore_ignorable_whitespace(0);
148 12         87 $tree->store_comments(1);
149 12         96 $tree->ignore_unknown(0);
150 12         229 $tree->parse($html);
151 12         2181 $tree->eof;
152              
153             # define replacer.
154             my $replace = sub {
155 72     72   98 my ( $tag, $attr_name ) = @_;
156              
157 72         226 for my $node ( $tree->findnodes("//$tag") ) {
158 11 100       5897 if ( my $attr = $node->attr($attr_name) ) {
159 10 100       476 next if $attr =~ /^mailto:/;
160 9 100       25 if ($attr =~ /^tel:([0-9-]+)$/) {
161 1         4 my $tel = $1;
162 1         174 $node->attr(
163             'onclick' => qq{prompt('tel', '$1');return false;}
164             );
165             } else {
166             # maybe /https?/
167 8         29 my $target_url = URI->new($attr);
168 8 50       505 $target_url = $target_url->abs($base_url) if $base_url;
169 8 50       938 $node->attr(
170             $attr_name => sprintf( qq{%s%s%s},
171             $base,
172             ($base =~ m{/$} ? '' : '/'),
173             uri_escape( $target_url ) )
174             );
175             }
176             }
177             }
178 12         1319 };
179              
180             # replace.
181 12         26 $replace->( 'img' => 'src' );
182 12         5385 $replace->( 'script' => 'src' );
183 12         4014 $replace->( 'form' => 'action' );
184 12         3455 $replace->( 'a' => 'href' );
185 12         2090 $replace->( 'link' => 'href' );
186 12         7565 $replace->( 'object' => 'data' );
187              
188             # dump.
189 12         4088 my $result = '';
190 12         43 for my $elm ($tree->guts) {
191 24 100       3498 $result .= ref $elm ? $elm->as_HTML(q{<>"&'}, '', {}) : $elm;
192             }
193 12         52 $tree->delete; # cleanup :-) HTML::TreeBuilder needs this.
194              
195             # return result.
196 12 50       459 $result = ''.$result.'' unless $result =~ /<\s*html/;
197 12         221 return $result;
198             }
199              
200             sub to_app {
201 0     0 0   my ($self) = @_;
202             sub {
203 0     0     my $env = shift;
204 0           my $req = Moxy::Request->new($env);
205 0           my $res = $self->handle_request($req);
206 0           $res->content_length( length($res->content) ); # adjust content-length.
207 0           $res->finalize();
208 0           };
209             }
210              
211             sub handle_request {
212 0     0 0   my ($self, $req) = @_;
213              
214 0           $self->log(debug => "---------------------------");
215              
216 0           my $conf = $self->conf->{global}->{session};
217 0   0       my $state_type = $conf->{state}->{module} || 'BasicAuth';
218             my $state = sub {
219 0 0   0     if ($state_type eq 'Cookie') {
220 0           require HTTP::Session::State::Cookie;
221 0           HTTP::Session::State::Cookie->new(
222             $conf->{state}->{config}
223             );
224             } else {
225 0           require Moxy::Session::State::BasicAuth;
226 0   0       Moxy::Session::State::BasicAuth->new(
227             $conf->{state}->{config} || {}
228             );
229             }
230 0           }->();
231             my $store = sub {
232 0 0   0     my $postfix = $conf->{store}->{module} or die "missing session store module name";
233 0           my $klass = "HTTP::Session::Store::${postfix}";
234 0 0         $klass->require or die $@;
235 0           $klass->new( $conf->{store}->{config} );
236 0           }->();
237              
238 0           my $auth = join(',', $req->headers->authorization_basic);
239 0 0 0       if ($state->isa('Moxy::Session::State::BasicAuth') && !$auth) {
240 0           $self->log(debug => 'basicauth');
241 0           return res(
242             401,
243             [
244             WWW_Authenticate => qq{Basic realm="Moxy needs basic auth.Only for identification.Password is dummy."},
245             ],
246             'authentication required',
247             );
248             } else {
249 0           $self->log(debug => "session: state: $state, store: $store");
250 0           my $session = HTTP::Session->new(
251             state => $state,
252             store => $store,
253             request => $req,
254             );
255 0           $self->log(debug => "session: $session");
256 0           my $res = $self->_make_response(
257             req => $req,
258             session => $session,
259             );
260 0           $session->response_filter($res);
261 0           $session->finalize;
262              
263 0           return $res;
264             }
265             }
266              
267             sub _make_response {
268 0     0     my $self = shift;
269 0           my %args = validate(
270             @_ => +{
271             req => { isa => 'Moxy::Request', },
272             session => { type => OBJECT },
273             }
274             );
275 0           my $req = $args{req};
276              
277 0           my $base = $req->uri->clone;
278 0           $base->path('');
279 0           $base->query_form({});
280              
281 0           (my $url = $req->uri->path_query) =~ s!^/!!;
282 0           $url = uf_uristr(uri_unescape($url));
283              
284 0 0         if ($url) {
285             # do proxy
286 0           my $res = $self->_do_request(
287             url => $url,
288             request => $req->as_http_request,
289             session => $args{session},
290             );
291 0           $self->log(debug => '-- response status: ' . $res->code);
292              
293 0 0         if ($res->code == 302) {
294             # rewrite redirect
295 0           my $location = URI->new($res->header('Location'));
296 0           $self->log(debug => "redirect to $location");
297 0           my $uri = URI->new($url);
298 0 0         if (not defined $location->scheme) {
299             # path only redirect is invalid!
300             # e.g. Location: /foo/
301 0           $self->log(error => "----------------------------");
302 0           $self->log(error => "INVALID REDIRECT!! $location");
303 0           $self->log(error => "----------------------------");
304 0           $location = URI->new( $location->as_string, $uri->scheme );
305 0           $location->scheme($uri->scheme);
306 0           $location->host($uri->host);
307 0           $location->port($uri->port);
308 0           $self->log(error => "FIXED TO: $location");
309 0           $self->log(error => "----------------------------");
310             } else {
311 0 0 0       if ($uri->port != 80 && $location->port != $uri->port) {
312 0           $location->port($uri->port);
313             }
314             }
315 0           my $redirect = $base . '/' . uri_escape($location);
316 0           $self->log(debug => "redirect to $redirect");
317 0           return res(
318             302, [
319             Location => $redirect,
320             ],
321             );
322             } else {
323 0           my $content_type = $res->header('Content-Type');
324 0           $self->log(debug => "Content-Type: $content_type");
325 0 0         if ($content_type =~ /html/i) {
    0          
326 0           $res->content( encode($res->charset, rewrite_html($base, decode($res->charset, $res->content), $url), Encode::FB_HTMLCREF) );
327             } elsif ($content_type =~ m{text/css}) {
328 0           $res->content( encode($res->charset, rewrite_css($base, decode($res->charset, $res->content), $url), Encode::FB_HTMLCREF) );
329             }
330              
331 0           return $res->to_plack_response();
332             }
333             } else {
334             # please input url.
335 0           my $response = HTTP::Response->new(
336             200 => 'ok', HTTP::Headers->new(
337             'content-type' => 'text/html;charset=utf-8',
338             ), q{
339            
340            
341            
342             moxy start page
343            
344            
345            
346            

moxy start page

347            

please input url to location bar

348            
349            
350             },
351             );
352 0           $response->request($req->as_http_request);
353 0           $self->_post_process(
354             response => $response,
355             mobile_attribute => HTTP::MobileAttribute->new('KDDI-KC26 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0'),
356             session => $args{session},
357             );
358 0           $response->content( encode($response->charset, rewrite_html($base, decode($response->charset, $response->content), ''), Encode::FB_HTMLCREF) );
359 0           return $response->to_plack_response();
360             }
361             }
362              
363             sub _do_request {
364 0     0     my $self = shift;
365 0           my %args = validate(
366             @_ => +{
367             url => qr{^https?://},
368             request => { isa => 'HTTP::Request' },
369             session => { type => OBJECT },
370             }
371             );
372              
373             # make request
374 0           my $req = $args{request}->clone;
375 0           $req->uri($args{url});
376 0           $req->header('Host' => do {
377 0           my $u = URI->new($args{url});
378 0           my $header = $u->host;
379 0 0         $header .= ':' . $u->port if $u->port != 80;
380 0           $header;
381             }
382             );
383              
384 0           $self->run_hook(
385             'request_filter_process_agent',
386             { request => $req, # HTTP::Request object
387             session => $args{session},
388             }
389             );
390              
391 0           my $mobile_attribute = HTTP::MobileAttribute->new($req->headers);
392 0           my $carrier = $mobile_attribute->carrier;
393 0   0       my $cookie_jar = $args{session}->get('cookies') || HTTP::Cookies->new(); # load cookies
394 0 0         if ($mobile_attribute->is_docomo) {
395 0           undef $cookie_jar; # docomo phone doesn't support cookies
396             }
397              
398 0           for my $hook ('url_handle', "url_handle_$carrier") {
399 0           my $response = $self->run_hook_and_get_response(
400             $hook,
401             +{
402             request => $req, # HTTP::Request object
403             mobile_attribute => $mobile_attribute,
404             session => $args{session},
405             }
406             );
407 0 0         if ($response) {
408 0           return $response; # finished
409             }
410             }
411              
412             # do request
413 0   0       my $ua = LWP::UserAgent->new(
414             timeout => $self->conf->{global}->{timeout} || 10,
415             max_redirects => 0,
416             protocols_allowed => [qw/http https/],
417             parse_head => 0,
418             cookie_jar => $cookie_jar,
419             );
420             $ua->add_handler( request_prepare => sub {
421 0     0     my ($req, $ua, $h) = @_;
422              
423 0           for my $hook ('request_filter', "request_filter_$carrier") {
424 0           my $response = $self->run_hook_and_get_response(
425             $hook,
426             +{
427             request => $req, # HTTP::Request object
428             mobile_attribute => $mobile_attribute,
429             session => $args{session},
430             }
431             );
432 0 0         if ($response) {
433 0           return $response; # finished
434             }
435             }
436 0           $req->remove_header('Accept-Encoding'); # I HATE gziped CONTENT
437 0           $req->remove_header('Cookie'); # remove Cookie from the client
438              
439 0           $req;
440 0           });
441             $ua->add_handler( response_done => sub {
442 0     0     my ($response, $ua, $h) = @_;
443 0           my $location = $response->header('Location');
444 0 0         if ($location) {
445 0   0       my $content = $response->content || '';
446 0           $self->log(info => "redirect to '$location', $content");
447             }
448 0           $response;
449 0           });
450              
451 0           $self->log(debug => "request to @{[ $req->uri ]}");
  0            
452 0           my $t1 = Time::HiRes::gettimeofday();
453 0           my $response = $ua->request($req);
454 0           my $t2 = Time::HiRes::gettimeofday();
455 0           $self->response_time( $t2-$t1 );
456 0           $self->log(debug => "and, request was @{[ $response->request->uri ]}");
  0            
457              
458 0           $args{session}->set('cookies' => $cookie_jar); # save cookies
459              
460 0           $self->_post_process(
461             response => $response,
462             mobile_attribute => $mobile_attribute,
463             session => $args{session},
464             );
465 0           $self->response_time( -1 ); # clear response time
466              
467 0           $response;
468             }
469              
470             sub _post_process {
471 0     0     my $self = shift;
472 0           my %args = validate(
473             @_ => {
474             response => 1,
475             mobile_attribute => 1,
476             session => 1,
477             },
478             );
479              
480 0           my $carrier = $args{mobile_attribute}->carrier;
481 0           for my $hook (
482             'status_handler', 'security_filter',
483             'response_filter', "response_filter_$carrier",
484             'render_location_bar'
485             )
486             {
487 0           $self->run_hook( $hook, \%args );
488             }
489             }
490              
491             1;
492             __END__