File Coverage

blib/lib/CAM/SOAPApp.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package CAM::SOAPApp;
2              
3 3     3   106088 use warnings;
  3         7  
  3         129  
4 3     3   19 use strict;
  3         6  
  3         138  
5             require 5.005_62;
6              
7 3     3   1946 use SOAP::Lite;
  0            
  0            
8             use base 'CAM::App';
9              
10             our $VERSION = '1.06';
11              
12              
13             =for stopwords BadID SOAPAction SOAPApp
14              
15             =head1 NAME
16              
17             CAM::SOAPApp - SOAP application framework
18              
19             =head1 LICENSE
20              
21             Copyright 2006 Clotho Advanced Media, Inc.,
22              
23             This library is free software; you can redistribute it and/or modify it
24             under the same terms as Perl itself.
25              
26             =head1 SYNOPSIS
27              
28             Do NOT subclass from this module to create your SOAP methods! That
29             would make a big security hole. Instead, write your application like
30             this example:
31              
32             use CAM::SOAPApp;
33             SOAP::Transport::HTTP::CGI
34             -> dispatch_to('My::Class')
35             -> handle;
36            
37             package My::Class;
38             our @ISA = qw(SOAP::Server::Parameters);
39             sub isLeapYear {
40             my $pkg = shift;
41             my $app = CAM::SOAPApp->new(soapdata => \@_);
42             if (!$app) {
43             CAM::SOAPApp->error('Internal', 'Failed to initialize the SOAP app');
44             }
45             my %data = $app->getSOAPData();
46             if (!defined $data{year}) {
47             $app->error('NoYear', 'No year specified in the query');
48             }
49             if ($data{year} !~ /^\d+$/) {
50             $app->error('BadYear', 'The year must be an integer');
51             }
52             my $leapyear = ($data{year} % 4 == 0 &&
53             ($data{year} % 100 != 0 ||
54             $data{year} % 400 == 0));
55             return $app->response(leapyear => $leapyear ? 1 : 0);
56             }
57              
58             =head1 DESCRIPTION
59              
60             CAM::SOAPApp is a framework to assist SOAP applications. This package
61             abstracts away a lot of the tedious interaction with SOAP and the
62             application configuration state. CAM::SOAPApp is a subclass of
63             CAM::App and therefore inherits all of its handy features.
64              
65             When you create a class to hold your SOAP methods, that class should
66             be a subclass of SOAP::Server::Parameters. It should NOT be a
67             subclass of CAM::SOAPApp. If you were to do the latter, then all of
68             the CAM::App and CAM::SOAPApp methods would be exposed as SOAP
69             methods, which would be a big security hole, so don't make that
70             mistake.
71              
72             =head1 OPTIONS
73              
74             When loading this module, there are a few different options that can
75             be selected. These can be mixed and matched as desired.
76              
77             =over
78              
79             =item use CAM::SOAPApp;
80              
81             This initializes SOAPApp with all of the default SOAP::Lite options.
82              
83             =item use CAM::SOAPApp (lenient => 1);
84              
85             This tweaks some SOAP::Lite and environment variables to make the
86             server work with SOAP-challenged clients. These tweaks specifically
87             enable HTTP::CGI and HTTP::Daemon modes for client environments which
88             don't offer full control over their HTTP channel (like Flash and Apple
89             Sherlock 3).
90              
91             Specifically, the tweaks include the following:
92              
93             =over
94              
95             =item Content-Type
96              
97             Sets Content-Type to C if it is not set or is set
98             incorrectly.
99              
100             =item SOAPAction
101              
102             Replaces missing SOAPAction header fields with ''.
103              
104             =item Charset
105              
106             Turns off charset output for the Content-Type (i.e. 'text/xml' instead
107             of 'text/xml; charset=utf-8').
108              
109             =item HTTP 500
110              
111             Outputs HTTP 200 instead of HTTP 500 for faults.
112              
113             =item XML trailing character
114              
115             Adds a trailing '>' to the XML if one is missing. This is to correct
116             a bug in the way Safari 1.0 posts XML from Flash.
117              
118             =back
119              
120             =item use CAM::SOAPApp (handle => PACKAGE);
121              
122             (Experimental!) Kick off the SOAP handler automatically. This runs
123             the following code immediately:
124              
125             SOAP::Transport::HTTP::CGI
126             -> dispatch_to(PACKAGE)
127             -> handle;
128              
129             Note that you must load PACKAGE before this statement.
130              
131             =back
132              
133             =cut
134              
135             sub import
136             {
137             my $pkg = shift;
138             while (@_ > 0)
139             {
140             my $key = lc shift;
141             my $value = shift;
142             $key =~ s/\A -//xms; # leading dash is optional
143              
144             if ($key eq 'lenient' && $value)
145             {
146             ### This is a SOAP::Lite version check, but is no longer applicable.
147             ### The changes further below work fine with SOAP::Lite v0.55 - v0.67
148              
149             #if (!$CAM::SOAPApp::NO_SOAP_LITE_WARNING &&
150             # (!defined $SOAP::Lite::VERSION ||
151             # $SOAP::Lite::VERSION ne '0.55'))
152             #{
153             # warn("SOAP::Lite version is not v0.55\n".
154             # " $pkg lenient mode is optimized for SOAP::Lite v0.55.\n" .
155             # " It has not been tested with other SOAP::Lite versions.\n".
156             # " To silence this warning set\n".
157             # " $CAM::SOAPApp::NO_SOAP_LITE_WARNING = 1;\n");
158             #}
159              
160             ## Hack to repair content-type for clients who send the wrong
161             ## value or no value (notably the Apple Sherlock 3 interface
162             ## and Flash)
163              
164             # This doesn't actually work for servers, but we'll include
165             #it in case SOAP::Lite ever gets fixed.
166             $SOAP::Constants::DO_NOT_CHECK_CONTENT_TYPE = 1;
167              
168             # CGI version
169             if (!$ENV{CONTENT_TYPE} ||
170             $ENV{CONTENT_TYPE} !~ m{\A (?: text/xml | multipart/(?:related|form-data) ) }xms)
171             {
172             $ENV{CONTENT_TYPE} = 'text/xml';
173             }
174              
175             # Daemon support
176             *SOAP::Transport::HTTP::Daemon::request = sub
177             {
178             my $self = shift->new;
179             if (@_)
180             {
181             $self->{_request} = shift;
182             $self->{_request}->content_type('text/xml');
183             # work around an 'uninitialized' warning
184             if (SOAP::Lite->VERSION() =~ m/\A 0.6[67] /xms)
185             {
186             if (! defined $self->request->header('Expect'))
187             {
188             $self->request->header('Expect', q{});
189             }
190             }
191             return $self;
192             }
193             else
194             {
195             return $self->{_request};
196             }
197             };
198              
199              
200             ## Allow missing SOAPAction header values (needed for Flash 6
201             ## which cannot send arbitrary HTTP headers)
202              
203             # CGI support
204             $ENV{HTTP_SOAPACTION} ||= q{""};
205              
206             # Daemon version
207             # Patch to return '""' instead of undef
208             {
209             no warnings 'redefine'; ## no critic - quiet the redefined sub warning
210             *SOAP::Server::action = sub
211             {
212             my $self = shift->new;
213             @_ ?
214             ($self->{_action} = shift, return $self) :
215             return $self->{_action} || q{""};
216             };
217             }
218              
219             ## Repair for clients which are unhappy with response
220             ## Content-Type values that are anything other than text/xml
221             ## (like Flash 6)
222             $SOAP::Constants::DO_NOT_USE_CHARSET = 1;
223            
224             ## Keep Apache from sending our faults as HTTP errors,
225             ## which confuse dumb clients like Flash 6
226             $SOAP::Constants::HTTP_ON_FAULT_CODE = 200;
227              
228             ## Override the request() method on HTTP::Server to fix the
229             ## request if the browser has broken the XML (namely Safari
230             ## v1.0 POSTing from Flash. This is a hack that detects the
231             ## missing ">" at the end of the XML request and appends it.
232             require SOAP::Transport::HTTP;
233             {
234             no warnings 'redefine'; ## no critic - quiet the redefined sub warning
235             *SOAP::Transport::HTTP::Server::request = sub {
236             my $self = shift->new;
237             if (@_)
238             {
239             $self->{_request} = shift;
240             if ($self->request->content =~ m{
241             {
242             # close unclosed tag
243             $self->request->content($self->request->content . '>');
244             }
245             # work around an 'uninitialized' warning
246             if (SOAP::Lite->VERSION() =~ m/\A 0.6[67] /xms)
247             {
248             if (! defined $self->request->header('Expect'))
249             {
250             $self->request->header('Expect', q{});
251             }
252             }
253             return $self;
254             }
255             else
256             {
257             return $self->{_request};
258             }
259             };
260             }
261             }
262             elsif ($key eq 'handle' && $value)
263             {
264             require SOAP::Transport::HTTP;
265             SOAP::Transport::HTTP::CGI
266             -> dispatch_to($value)
267             -> handle;
268             }
269             }
270             return;
271             }
272              
273             =head1 METHODS
274              
275             =over
276              
277             =item CAM::SOAPApp->new(soapdata => \@array)
278              
279             Create a new application instance. The arguments passed to the SOAP
280             method should all be passed verbatim to this method as a reference,
281             less the package reference. This should be like the following:
282              
283             sub myMethod {
284             my $pkg = shift;
285             my $app = CAM::SOAPApp->new(soapdata => \@_);
286             ...
287             }
288              
289             =cut
290              
291             sub new
292             {
293             my $pkg = shift;
294             my %args = @_;
295              
296             my $self = $pkg->SUPER::new(cgi => undef, @_);
297              
298             my $soapdata = $args{soapdata};
299             my $tail = $soapdata->[-1];
300             if (eval{ $tail->isa('SOAP::SOM') })
301             {
302             $self->{envelope} = pop @{$soapdata}; # remove tail from the list
303             # get the envelope data, or the empty set
304             # Note: method() returns "" on no data, hence the "|| {}" below
305             $self->{soapdata} = $self->{envelope}->method() || {};
306             }
307             else
308             {
309             if (0 != @{$soapdata} % 2)
310             {
311             push @{$soapdata}, undef; # even out the hash key/value pairs
312             }
313             $self->{soapdata} = {@{$soapdata}}; # copy as hash
314             }
315             return $self;
316             }
317              
318             =item $app->getSOAPData()
319              
320             Returns a hash of data passed to the application. This is a massaged
321             version of the C array passed to new().
322              
323             =cut
324              
325             sub getSOAPData
326             {
327             my $self = shift;
328             return %{$self->{soapdata}};
329             }
330              
331             =item $app->response($key1 => $value1, $key2 => $value2, ...)
332              
333             Prepare data to return from a SOAP method. For example:
334              
335             sub myMethod {
336             ...
337             return $app->response(year => 2003, month => 3, date => 26);
338             }
339              
340             yields SOAP XML that looks like this (namespaces and data types
341             omitted for brevity):
342              
343            
344            
345            
346             2003
347             3
348             26
349            
350            
351            
352              
353             =cut
354              
355             sub response
356             {
357             my $self = shift;
358             return $self->encodeHash({@_});
359             }
360              
361             =item $app->error()
362              
363             =item $app->error($faultcode)
364              
365             =item $app->error($faultcode, $faultstring)
366              
367             =item $app->error($faultcode, $faultstring, $key1 => $value1, $key2 => $value2, ...)
368              
369             Emit a SOAP fault indicating a failure. The C should be a
370             short, computer-readable string (like "Error" or "Denied" or "BadID").
371             The C should be a human-readable string that explains the
372             error. Additional values are encapsulated as C fields for
373             optional context for the error. The result of this method will look
374             like this (namespaces and data types omitted for brevity).
375              
376            
377            
378            
379             $faultcode
380             $faultstring
381            
382            
383             <$key1>$value1
384             <$key2>$value2
385             ...
386            
387            
388            
389            
390            
391              
392             =cut
393              
394             sub error
395             {
396             my $pkg_or_self = shift;
397             my $code = shift || 'Internal';
398             my $string = shift || 'Application Error';
399             # rest of args handled below
400              
401             my $fault = SOAP::Fault->faultcode($code)->faultstring($string);
402             if (@_)
403             {
404             if (0 != @_ % 2)
405             {
406             push @_, undef; # even out the hash key/value pairs
407             }
408             $fault = $fault->faultdetail(SOAP::Data->name('data' => {@_}));
409             }
410             die $fault;
411             return; # never get here
412             }
413              
414             =item $app->encodeHash(\%hash)
415              
416             This is a helper function used by response() to encode hash data into
417             a SOAP-friendly array of key-value pairs that are easily transformed
418             into XML tags by SOAP::Lite. You should generally use response()
419             instead of this function unless you have a good reason.
420              
421             =cut
422              
423             sub encodeHash
424             {
425             my $pkg_or_self = shift;
426             my $data = $_[0];
427              
428             return @_ if (!$data);
429             return @_ if (!ref $data);
430             return @_ if ('HASH' ne ref $data);
431              
432             my @out;
433             foreach my $key (sort keys %{$data})
434             {
435             push @out, SOAP::Data->name($key => $data->{$key});
436             }
437             return @out;
438             }
439              
440             1;
441              
442             __END__