File Coverage

blib/lib/Plack/Middleware/FixIEXDomainRequestBug.pm
Criterion Covered Total %
statement 33 36 91.6
branch 13 20 65.0
condition 8 14 57.1
subroutine 11 11 100.0
pod 2 7 28.5
total 67 88 76.1


line stmt bran cond sub pod time code
1             package Plack::Middleware::FixIEXDomainRequestBug;
2              
3 2     2   413369 use strict;
  2         5  
  2         71  
4 2     2   10 use warnings;
  2         4  
  2         65  
5 2     2   12 use base 'Plack::Middleware';
  2         8  
  2         26513  
6 2     2   44823 use Plack::Util::Accessor 'force_content_type', 'guess_with';
  2         4  
  2         11  
7              
8             our $VERSION = '0.001';
9              
10 2 50   2 0 17 sub is_POST { uc(pop->{REQUEST_METHOD}) eq 'POST' ? 1:0 };
11              
12             sub missing_or_textplain_content {
13 2 100   2 0 6 if(my $content_type = pop->{CONTENT_TYPE}) {
14 1 50       11 return lc($content_type) eq 'text/plain' ? 1:0;
15             } else {
16 1         6 return 1;
17             }
18             }
19              
20             sub is_ie8_or9 {
21 2   100 2 0 16 my ($v) = lc(pop->{HTTP_USER_AGENT}||'')=~m/msie\s(\d).+?;/i; # Works for all Version of IE I know
22 2 100 66     16 return $v && ($v eq '8' || $v eq '9') ? 1:0;
23             }
24              
25             sub meets_criteria {
26 2     2 0 3 my ($self, $env) = @_;
27 2 100 33     6 if(
      66        
28             $self->is_POST($env) &&
29             $self->missing_or_textplain_content($env) &&
30             $self->is_ie8_or9($env)
31             ) {
32 1         4 return 1;
33             } else {
34 1         7 return 0;
35             }
36             }
37              
38             sub provide_content_type {
39 1     1 0 3 my ($self, $env) = @_;
40 1 50       5 if(my $force = $self->force_content_type) {
    0          
41 1         11 return $force;
42             } elsif (my $code = $self->guess_with) {
43 0         0 return $code->($env);
44             }
45             }
46              
47             sub prepare_app {
48 1     1 1 66 my $self = shift;
49 1 50 33     8 unless ($self->force_content_type || $self->guess_with) {
50 0         0 die "You must set either 'force_content_type' or 'guess_with' in order to use the FixIEXDomainRequestBug middleware.";
51             }
52             }
53              
54             sub call {
55 2     2 1 20720 my($self, $env) = @_;
56 2 100       9 if($self->meets_criteria($env)) {
57 1 50       4 if(my $new_content_type = $self->provide_content_type($env)) {
58 1         3 $env->{'plack.middleware.fixiexdomainrequestbug.overrode_content_type'} = 1;
59 1         3 $env->{'CONTENT_TYPE'} = $new_content_type;
60             } else {
61 0         0 warn "You asked me to fix the IE XDomainRequest bug, but I could not provide a new content-type.";
62             }
63             }
64              
65 2         15 return $self->app->($env);
66             }
67              
68             1;
69              
70             =head1 NAME
71              
72             Plack::Middleware::FixIEXDomainRequestBug - Fix IE8/IE9 XDomainRequest Missing Content Type
73              
74             =head1 SYNOPSIS
75              
76             The Following two examples encompass most likely usage
77              
78             =head2 Specify Mimetype
79              
80             Specify the mimetype (assumes you control all ends)
81              
82             use Plack::Builder;
83             builder {
84             enable 'FixIEXDomainRequestBug',
85             force_content_type => 'application/json';
86             $app;
87             };
88              
89             =head2 Custom Provider
90              
91             Use some custom code to provide a valid mimetype
92              
93             use Plack::Builder;
94             builder {
95             enable 'FixIEXDomainRequestBug',
96             guess_with => sub {
97             my $env = shift;
98             if($env->{PATH_INFO} =~ m{^/api}) {
99             return 'application/json';
100             } else {
101             return 'application/x-www-form-urlencoded';
102             }
103             };
104             $app;
105             };
106              
107             You may also consider strategies where you apply the middleware differently
108             under different mount points.
109              
110             =head1 DESCRIPTION
111              
112             Here's a good explanation of the issue we are attempting to solve:
113              
114             L
115              
116             Basically Internet Explorer 8 and 9 have a proprietary approach to allow cross
117             domain AJAX safely. However in the attempt to lock down the interface as much
118             as possible Microsoft introduced what is widely considered a major bug, which
119             vastly decreases the value of the feature. What happens is that any type of
120             attempt to use the XDomainRequest activeX control sets the request content type
121             to nothing or text/plain (the docs say text/plain is the only type allowed, but
122             web search and the experience we have seen is that the content type is empty).
123             As a result, when a framework like L trys to parse the POST body, it
124             can't figure out what to do, so it punts, typically busting this code. Since
125             it is common with web applications to use a Javascript framework to paper over
126             browser differences, this means that an application doing cross domain access
127             might easily work with Firefox but totally bust with IE 8 or 9 (at the time of
128             this writing these browsers are still the most popular for people using Windows
129             on the desktop, and typically represent 20%+ total web traffic)
130              
131             This distribution attempts to solve this problem at the middleware level. What
132             it does is check to see if the user agent identifies itself as Internet Explorer
133             8 or 9, AND the method is POST (only GET and POST http methods are allowed with
134             XDomainRequest anyway) AND content-type is nothing or text/plain, THEN we do
135             of the following:
136              
137             We create the following custom key
138              
139             $env->{'plack.middleware.fixiexdomainrequestbug.overrode_content_type'} = 1
140              
141             You can test if this value is true to detect if we changed the C,
142             should you wish to know (might be useful for debugging).
143              
144             Then we change $env->{'CONTENT_TYPE'} in one of the following ways
145              
146             If you've specified a C configuration, we always use that,
147             and change the http content type to match.
148              
149             Otherwise, if you've set a C configuration we assume that is an
150             anonymous sub and invoke that with $env. That coderef is expected to return
151             a string which is a valid C. Commonly you may wish to set a
152             custom search query parameter as a fallback mechanism for setting the
153             C, or set the expected content-type based on the requested path (
154             as seen in the L example.
155              
156             use Plack::Builder;
157             builder {
158             enable 'FixIEXDomainRequestBug',
159             guess_with => sub {
160             my $env = shift;
161             my $req = Plack::Request->new($env);
162              
163             ## Assume a request url like "http://myapp.com/path?format=application/json"
164             return $req->query_parameters->get('format')
165             };
166             $app;
167             };
168              
169             =head1 ATTRIBUTES
170              
171             This middleware has the following attributes used to inform how a missing or
172             invalid HTTP C is altered. The listing is in order of priority.
173              
174             It goes without saying that although both attributes are not required, you need
175             at least one of them for the middleware to function.
176              
177             See L for examples.
178              
179             =head2 force_content_type
180              
181             String: Default is empty, not required.
182              
183             This is a string which must be, if defined, a valid mimetype suitable for
184             populating the HTTP Header C. If this attribute is set, and
185             a request meeting the defined criteria is detected, the C is
186             forced to the set value.
187              
188             =head2 guess_with
189              
190             CodeRef: Default is empty, not required.
191              
192             This coderef is used if you wish to create a custom mechanism for figuring out
193             what the C should be when the defined criteria (described above)
194             is detected. It gets passed the L env and is expected to return a string
195             which is suitable as a value for HTTP Header C.
196              
197             =head1 SEE ALSO
198              
199             L, L.
200              
201             =head1 AUTHOR
202              
203             John Napiorkowski L
204              
205             =head1 COPYRIGHT & LICENSE
206              
207             Copyright 2013, John Napiorkowski L
208              
209             This library is free software; you can redistribute it and/or modify it under
210             the same terms as Perl itself.
211              
212             =cut