File Coverage

blib/lib/XML/Atom/Client.pm
Criterion Covered Total %
statement 59 166 35.5
branch 1 40 2.5
condition 0 13 0.0
subroutine 18 34 52.9
pod 9 16 56.2
total 87 269 32.3


line stmt bran cond sub pod time code
1             # $Id$
2              
3             package XML::Atom::Client;
4 2     2   733099 use strict;
  2         22  
  2         64  
5              
6 2     2   825 use XML::Atom;
  2         8  
  2         133  
7 2     2   14 use base qw( XML::Atom::ErrorHandler );
  2         5  
  2         126  
8 2     2   1497 use LWP::UserAgent;
  2         88625  
  2         84  
9 2     2   1023 use XML::Atom::Entry;
  2         6  
  2         58  
10 2     2   15 use XML::Atom::Feed;
  2         4  
  2         50  
11 2     2   10 use XML::Atom::Util qw( first textValue );
  2         3  
  2         92  
12 2     2   1188 use Digest::SHA qw( sha1 );
  2         5698  
  2         162  
13 2     2   18 use MIME::Base64 qw( encode_base64 );
  2         4  
  2         98  
14 2     2   909 use DateTime;
  2         506096  
  2         94  
15              
16 2     2   16 use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
  2         4  
  2         3539  
17              
18             sub new {
19 1     1 1 1228 my $class = shift;
20 1         4 my $client = bless { }, $class;
21 1 50       5 $client->init(@_) or return $class->error($client->errstr);
22 1         4 $client;
23             }
24              
25             sub init {
26 1     1 0 2 my $client = shift;
27 1         3 my %param = @_;
28 1         5 $client->{ua} = LWP::UserAgent::AtomClient->new($client);
29 1         28 $client->{ua}->agent('XML::Atom/' . XML::Atom->VERSION);
30 1         75 $client->{ua}->parse_head(0);
31 1         87 $client;
32             }
33              
34             sub username {
35 0     0 1 0 my $client = shift;
36 0 0       0 $client->{username} = shift if @_;
37 0         0 $client->{username};
38             }
39              
40             sub password {
41 0     0 1 0 my $client = shift;
42 0 0       0 $client->{password} = shift if @_;
43 0         0 $client->{password};
44             }
45              
46             sub use_soap {
47 0     0 1 0 my $client = shift;
48 0 0       0 $client->{use_soap} = shift if @_;
49 0         0 $client->{use_soap};
50             }
51              
52             sub auth_digest {
53 0     0 0 0 my $client = shift;
54 0 0       0 $client->{auth_digest} = shift if @_;
55 0         0 $client->{auth_digest};
56             }
57              
58             sub getEntry {
59 0     0 1 0 my $client = shift;
60 0         0 my($url) = @_;
61 0         0 my $req = HTTP::Request->new(GET => $url);
62 0         0 my $res = $client->make_request($req);
63 0 0       0 return $client->error("Error on GET $url: " . $res->status_line)
64             unless $res->code == 200;
65 0         0 XML::Atom::Entry->new(Stream => \$res->content);
66             }
67              
68             sub createEntry {
69 0     0 1 0 my $client = shift;
70 0         0 my($uri, $entry) = @_;
71 0 0       0 return $client->error("Must pass a PostURI before posting")
72             unless $uri;
73 0         0 my $req = HTTP::Request->new(POST => $uri);
74 0         0 $req->content_type($entry->content_type);
75 0         0 my $xml = $entry->as_xml;
76 0         0 _utf8_off($xml);
77 0         0 $req->content_length(length $xml);
78 0         0 $req->content($xml);
79 0         0 my $res = $client->make_request($req);
80 0 0       0 return $client->error("Error on POST $uri: " . $res->status_line)
81             unless $res->code == 201;
82 0 0       0 $res->header('Location') || 1;
83             }
84              
85             sub updateEntry {
86 0     0 1 0 my $client = shift;
87 0         0 my($url, $entry) = @_;
88 0         0 my $req = HTTP::Request->new(PUT => $url);
89 0         0 $req->content_type($entry->content_type);
90 0         0 my $xml = $entry->as_xml;
91 0         0 _utf8_off($xml);
92 0         0 $req->content_length(length $xml);
93 0         0 $req->content($xml);
94 0         0 my $res = $client->make_request($req);
95 0 0       0 return $client->error("Error on PUT $url: " . $res->status_line)
96             unless $res->code == 200;
97 0         0 1;
98             }
99              
100             sub deleteEntry {
101 0     0 1 0 my $client = shift;
102 0         0 my($url) = @_;
103 0         0 my $req = HTTP::Request->new(DELETE => $url);
104 0         0 my $res = $client->make_request($req);
105 0 0       0 return $client->error("Error on DELETE $url: " . $res->status_line)
106             unless $res->code == 200;
107 0         0 1;
108             }
109              
110             sub getFeed {
111 0     0 1 0 my $client = shift;
112 0         0 my($uri) = @_;
113 0 0       0 return $client->error("Must pass a FeedURI before retrieving feed")
114             unless $uri;
115 0         0 my $req = HTTP::Request->new(GET => $uri);
116 0         0 my $res = $client->make_request($req);
117 0 0       0 return $client->error("Error on GET $uri: " . $res->status_line)
118             unless $res->code == 200;
119 0 0       0 my $feed = XML::Atom::Feed->new(Stream => \$res->content)
120             or return $client->error(XML::Atom::Feed->errstr);
121 0         0 $feed;
122             }
123              
124             sub make_request {
125 0     0 0 0 my $client = shift;
126 0         0 my($req) = @_;
127 0         0 $client->munge_request($req);
128 0         0 my $res = $client->{ua}->request($req);
129 0         0 $client->munge_response($res);
130 0         0 $client->{response} = $res;
131 0         0 $res;
132             }
133              
134             sub munge_request {
135 0     0 0 0 my $client = shift;
136 0         0 my($req) = @_;
137 0         0 $req->header(
138             Accept => 'application/atom+xml, application/x.atom+xml, application/xml, text/xml, */*',
139             );
140 0         0 my $nonce = $client->make_nonce;
141 0         0 my $nonce_enc = encode_base64($nonce, '');
142 0         0 my $now = DateTime->now->iso8601 . 'Z';
143 0   0     0 my $digest = encode_base64(sha1($nonce . $now . ($client->password || '')), '');
144 0 0       0 if ($client->use_soap) {
145 0   0     0 my $xml = $req->content || '';
146 0         0 $xml =~ s!^(<\?xml.*?\?>)!!;
147 0         0 my $method = $req->method;
148 0   0     0 $xml = ($1 || '') . <<SOAP;
149             <soap:Envelope
150             xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
151             xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
152             xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
153             <soap:Header>
154             <wsse:Security>
155             <wsse:UsernameToken>
156 0   0     0 <wsse:Username>@{[ $client->username || '' ]}</wsse:Username>
157             <wsse:Password Type="wsse:PasswordDigest">$digest</wsse:Password>
158             <wsse:Nonce>$nonce_enc</wsse:Nonce>
159             <wsu:Created>$now</wsu:Created>
160             </wsse:UsernameToken>
161             </wsse:Security>
162             </soap:Header>
163             <soap:Body>
164             <$method xmlns="http://schemas.xmlsoap.org/wsdl/http/">
165             $xml
166             </$method>
167             </soap:Body>
168             </soap:Envelope>
169             SOAP
170 0         0 $req->content($xml);
171 0         0 $req->content_length(length $xml);
172 0         0 $req->header('SOAPAction', 'http://schemas.xmlsoap.org/wsdl/http/' . $method);
173 0         0 $req->method('POST');
174 0         0 $req->content_type('text/xml');
175             } else {
176 0 0       0 if ($client->username) {
177 0   0     0 $req->header('X-WSSE', sprintf
178             qq(UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"),
179             $client->username || '', $digest, $nonce_enc, $now);
180 0         0 $req->header('Authorization', 'WSSE profile="UsernameToken"');
181             }
182             }
183             }
184              
185             sub munge_response {
186 0     0 0 0 my $client = shift;
187 0         0 my($res) = @_;
188 0 0 0     0 if ($client->use_soap && (my $xml = $res->content)) {
189 0         0 my $doc;
190 0         0 if (LIBXML) {
191 0         0 my $parser = $client->libxml_parser;
192 0         0 $doc = $parser->parse_string($xml);
193             } else {
194             my $xp = XML::XPath->new(xml => $xml);
195             $doc = ($xp->find('/')->get_nodelist)[0];
196             }
197 0         0 my $body = first($doc, NS_SOAP, 'Body');
198 0 0       0 if (my $fault = first($body, NS_SOAP, 'Fault')) {
199 0         0 $res->code(textValue($fault, undef, 'faultcode'));
200 0         0 $res->message(textValue($fault, undef, 'faultstring'));
201 0         0 $res->content('');
202 0         0 $res->content_length(0);
203             } else {
204 0         0 $xml = join '', map $_->toString(LIBXML ? 1 : 0),
205             LIBXML ? $body->childNodes : $body->getChildNodes;
206 0         0 $res->content($xml);
207 0         0 $res->content_length(1);
208             }
209             }
210             }
211              
212 0     0 0 0 sub make_nonce { sha1(sha1(time() . {} . rand() . $$)) }
213              
214             sub _utf8_off {
215 0 0   0   0 if ($] >= 5.008) {
216 0         0 require Encode;
217 0         0 Encode::_utf8_off($_[0]);
218             }
219             }
220              
221 0     0 0 0 sub libxml_parser { XML::Atom->libxml_parser }
222              
223             package LWP::UserAgent::AtomClient;
224 2     2   21 use strict;
  2         4  
  2         76  
225 2     2   31 use Scalar::Util;
  2         7  
  2         120  
226              
227 2     2   12 use base qw( LWP::UserAgent );
  2         14  
  2         622  
228              
229             my %ClientOf;
230             sub new {
231 1     1   3 my($class, $client) = @_;
232 1         8 my $ua = $class->SUPER::new;
233 1         3175 $ClientOf{$ua} = $client;
234 1         6 Scalar::Util::weaken($ClientOf{$ua});
235 1         8 $ua;
236             }
237              
238             sub get_basic_credentials {
239 0     0   0 my($ua, $realm, $url, $proxy) = @_;
240 0 0       0 my $client = $ClientOf{$ua} or die "Cannot find $ua";
241 0         0 return $client->username, $client->password;
242             }
243              
244             sub DESTROY {
245 1     1   11 my $self = shift;
246 1         4 delete $ClientOf{$self};
247             }
248              
249             1;
250             __END__
251              
252             =head1 NAME
253              
254             XML::Atom::Client - A client for the Atom API
255              
256             =head1 SYNOPSIS
257              
258             use XML::Atom::Client;
259             use XML::Atom::Entry;
260             my $api = XML::Atom::Client->new;
261             $api->username('Melody');
262             $api->password('Nelson');
263              
264             my $entry = XML::Atom::Entry->new;
265             $entry->title('New Post');
266             $entry->content('Content of my post.');
267             my $EditURI = $api->createEntry($PostURI, $entry);
268              
269             my $feed = $api->getFeed($FeedURI);
270             my @entries = $feed->entries;
271              
272             my $entry = $api->getEntry($EditURI);
273              
274             =head1 DESCRIPTION
275              
276             I<XML::Atom::Client> implements a client for the Atom API described at
277             I<http://bitworking.org/projects/atom/draft-gregorio-09.html>, with the
278             authentication scheme described at
279             I<http://www.intertwingly.net/wiki/pie/DifferentlyAbledClients>.
280              
281             B<NOTE:> the API, and particularly the authentication scheme, are still
282             in flux.
283              
284             =head1 USAGE
285              
286             =head2 XML::Atom::Client->new(%param)
287              
288             =head2 $api->use_soap([ 0 | 1 ])
289              
290             I<XML::Atom::Client> supports both the REST and SOAP-wrapper versions of the
291             Atom API. By default, the REST version of the API will be used, but you can
292             turn on the SOAP wrapper--for example, if you need to connect to a server
293             that supports only the SOAP wrapper--by calling I<use_soap> with a value of
294             C<1>:
295              
296             $api->use_soap(1);
297              
298             If called without arguments, returns the current value of the flag.
299              
300             =head2 $api->username([ $username ])
301              
302             If called with an argument, sets the username for login to I<$username>.
303              
304             Returns the current username that will be used when logging in to the
305             Atom server.
306              
307             =head2 $api->password([ $password ])
308              
309             If called with an argument, sets the password for login to I<$password>.
310              
311             Returns the current password that will be used when logging in to the
312             Atom server.
313              
314             =head2 $api->createEntry($PostURI, $entry)
315              
316             Creates a new entry.
317              
318             I<$entry> must be an I<XML::Atom::Entry> object.
319              
320             =head2 $api->getEntry($EditURI)
321              
322             Retrieves the entry with the given URL I<$EditURI>.
323              
324             Returns an I<XML::Atom::Entry> object.
325              
326             =head2 $api->updateEntry($EditURI, $entry)
327              
328             Updates the entry at URL I<$EditURI> with the entry I<$entry>, which must be
329             an I<XML::Atom::Entry> object.
330              
331             Returns true on success, false otherwise.
332              
333             =head2 $api->deleteEntry($EditURI)
334              
335             Deletes the entry at URL I<$EditURI>.
336              
337             =head2 $api->getFeed($FeedURI)
338              
339             Retrieves the feed at I<$FeedURI>.
340              
341             Returns an I<XML::Atom::Feed> object representing the feed returned
342             from the server.
343              
344             =head2 ERROR HANDLING
345              
346             Methods return C<undef> on error, and the error message can be retrieved
347             using the I<errstr> method.
348              
349             =head1 AUTHOR & COPYRIGHT
350              
351             Please see the I<XML::Atom> manpage for author, copyright, and license
352             information.
353              
354             =cut