File Coverage

blib/lib/XML/Atom/Client.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             # $Id$
2              
3             package XML::Atom::Client;
4 2     2   848708 use strict;
  2         8  
  2         317  
5              
6 2     2   831 use XML::Atom;
  0            
  0            
7             use base qw( XML::Atom::ErrorHandler );
8             use LWP::UserAgent;
9             use XML::Atom::Entry;
10             use XML::Atom::Feed;
11             use XML::Atom::Util qw( first textValue );
12             use Digest::SHA1 qw( sha1 );
13             use MIME::Base64 qw( encode_base64 );
14             use DateTime;
15              
16             use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
17              
18             sub new {
19             my $class = shift;
20             my $client = bless { }, $class;
21             $client->init(@_) or return $class->error($client->errstr);
22             $client;
23             }
24              
25             sub init {
26             my $client = shift;
27             my %param = @_;
28             $client->{ua} = LWP::UserAgent::AtomClient->new($client);
29             $client->{ua}->agent('XML::Atom/' . XML::Atom->VERSION);
30             $client->{ua}->parse_head(0);
31             $client;
32             }
33              
34             sub username {
35             my $client = shift;
36             $client->{username} = shift if @_;
37             $client->{username};
38             }
39              
40             sub password {
41             my $client = shift;
42             $client->{password} = shift if @_;
43             $client->{password};
44             }
45              
46             sub use_soap {
47             my $client = shift;
48             $client->{use_soap} = shift if @_;
49             $client->{use_soap};
50             }
51              
52             sub auth_digest {
53             my $client = shift;
54             $client->{auth_digest} = shift if @_;
55             $client->{auth_digest};
56             }
57              
58             sub getEntry {
59             my $client = shift;
60             my($url) = @_;
61             my $req = HTTP::Request->new(GET => $url);
62             my $res = $client->make_request($req);
63             return $client->error("Error on GET $url: " . $res->status_line)
64             unless $res->code == 200;
65             XML::Atom::Entry->new(Stream => \$res->content);
66             }
67              
68             sub createEntry {
69             my $client = shift;
70             my($uri, $entry) = @_;
71             return $client->error("Must pass a PostURI before posting")
72             unless $uri;
73             my $req = HTTP::Request->new(POST => $uri);
74             $req->content_type($entry->content_type);
75             my $xml = $entry->as_xml;
76             _utf8_off($xml);
77             $req->content_length(length $xml);
78             $req->content($xml);
79             my $res = $client->make_request($req);
80             return $client->error("Error on POST $uri: " . $res->status_line)
81             unless $res->code == 201;
82             $res->header('Location') || 1;
83             }
84              
85             sub updateEntry {
86             my $client = shift;
87             my($url, $entry) = @_;
88             my $req = HTTP::Request->new(PUT => $url);
89             $req->content_type($entry->content_type);
90             my $xml = $entry->as_xml;
91             _utf8_off($xml);
92             $req->content_length(length $xml);
93             $req->content($xml);
94             my $res = $client->make_request($req);
95             return $client->error("Error on PUT $url: " . $res->status_line)
96             unless $res->code == 200;
97             1;
98             }
99              
100             sub deleteEntry {
101             my $client = shift;
102             my($url) = @_;
103             my $req = HTTP::Request->new(DELETE => $url);
104             my $res = $client->make_request($req);
105             return $client->error("Error on DELETE $url: " . $res->status_line)
106             unless $res->code == 200;
107             1;
108             }
109              
110             sub getFeed {
111             my $client = shift;
112             my($uri) = @_;
113             return $client->error("Must pass a FeedURI before retrieving feed")
114             unless $uri;
115             my $req = HTTP::Request->new(GET => $uri);
116             my $res = $client->make_request($req);
117             return $client->error("Error on GET $uri: " . $res->status_line)
118             unless $res->code == 200;
119             my $feed = XML::Atom::Feed->new(Stream => \$res->content)
120             or return $client->error(XML::Atom::Feed->errstr);
121             $feed;
122             }
123              
124             sub make_request {
125             my $client = shift;
126             my($req) = @_;
127             $client->munge_request($req);
128             my $res = $client->{ua}->request($req);
129             $client->munge_response($res);
130             $client->{response} = $res;
131             $res;
132             }
133              
134             sub munge_request {
135             my $client = shift;
136             my($req) = @_;
137             $req->header(
138             Accept => 'application/atom+xml, application/x.atom+xml, application/xml, text/xml, */*',
139             );
140             my $nonce = $client->make_nonce;
141             my $nonce_enc = encode_base64($nonce, '');
142             my $now = DateTime->now->iso8601 . 'Z';
143             my $digest = encode_base64(sha1($nonce . $now . ($client->password || '')), '');
144             if ($client->use_soap) {
145             my $xml = $req->content || '';
146             $xml =~ s!^(<\?xml.*?\?>)!!;
147             my $method = $req->method;
148             $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             <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             $req->content($xml);
171             $req->content_length(length $xml);
172             $req->header('SOAPAction', 'http://schemas.xmlsoap.org/wsdl/http/' . $method);
173             $req->method('POST');
174             $req->content_type('text/xml');
175             } else {
176             if ($client->username) {
177             $req->header('X-WSSE', sprintf
178             qq(UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"),
179             $client->username || '', $digest, $nonce_enc, $now);
180             $req->header('Authorization', 'WSSE profile="UsernameToken"');
181             }
182             }
183             }
184              
185             sub munge_response {
186             my $client = shift;
187             my($res) = @_;
188             if ($client->use_soap && (my $xml = $res->content)) {
189             my $doc;
190             if (LIBXML) {
191             my $parser = $client->libxml_parser;
192             $doc = $parser->parse_string($xml);
193             } else {
194             my $xp = XML::XPath->new(xml => $xml);
195             $doc = ($xp->find('/')->get_nodelist)[0];
196             }
197             my $body = first($doc, NS_SOAP, 'Body');
198             if (my $fault = first($body, NS_SOAP, 'Fault')) {
199             $res->code(textValue($fault, undef, 'faultcode'));
200             $res->message(textValue($fault, undef, 'faultstring'));
201             $res->content('');
202             $res->content_length(0);
203             } else {
204             $xml = join '', map $_->toString(LIBXML ? 1 : 0),
205             LIBXML ? $body->childNodes : $body->getChildNodes;
206             $res->content($xml);
207             $res->content_length(1);
208             }
209             }
210             }
211              
212             sub make_nonce { sha1(sha1(time() . {} . rand() . $$)) }
213              
214             sub _utf8_off {
215             if ($] >= 5.008) {
216             require Encode;
217             Encode::_utf8_off($_[0]);
218             }
219             }
220              
221             sub libxml_parser { XML::Atom->libxml_parser }
222              
223             package LWP::UserAgent::AtomClient;
224             use strict;
225             use Scalar::Util;
226              
227             use base qw( LWP::UserAgent );
228              
229             my %ClientOf;
230             sub new {
231             my($class, $client) = @_;
232             my $ua = $class->SUPER::new;
233             $ClientOf{$ua} = $client;
234             Scalar::Util::weaken($ClientOf{$ua});
235             $ua;
236             }
237              
238             sub get_basic_credentials {
239             my($ua, $realm, $url, $proxy) = @_;
240             my $client = $ClientOf{$ua} or die "Cannot find $ua";
241             return $client->username, $client->password;
242             }
243              
244             sub DESTROY {
245             my $self = shift;
246             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