Add built local::lib
[catagits/Gitalist.git] / local-lib5 / lib / perl5 / XML / Atom / Client.pm
CommitLineData
3fea05b9 1# $Id$
2
3package XML::Atom::Client;
4use strict;
5
6use XML::Atom;
7use base qw( XML::Atom::ErrorHandler );
8use LWP::UserAgent;
9use XML::Atom::Entry;
10use XML::Atom::Feed;
11use XML::Atom::Util qw( first textValue );
12use Digest::SHA1 qw( sha1 );
13use MIME::Base64 qw( encode_base64 );
14use DateTime;
15
16use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
17
18sub new {
19 my $class = shift;
20 my $client = bless { }, $class;
21 $client->init(@_) or return $class->error($client->errstr);
22 $client;
23}
24
25sub 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
34sub username {
35 my $client = shift;
36 $client->{username} = shift if @_;
37 $client->{username};
38}
39
40sub password {
41 my $client = shift;
42 $client->{password} = shift if @_;
43 $client->{password};
44}
45
46sub use_soap {
47 my $client = shift;
48 $client->{use_soap} = shift if @_;
49 $client->{use_soap};
50}
51
52sub auth_digest {
53 my $client = shift;
54 $client->{auth_digest} = shift if @_;
55 $client->{auth_digest};
56}
57
58sub 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
68sub 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
85sub 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
100sub 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
110sub 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
124sub 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
134sub 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>
169SOAP
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
185sub 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 = XML::LibXML->new;
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
212sub make_nonce { sha1(sha1(time() . {} . rand() . $$)) }
213
214sub _utf8_off {
215 if ($] >= 5.008) {
216 require Encode;
217 Encode::_utf8_off($_[0]);
218 }
219}
220
221package LWP::UserAgent::AtomClient;
222use strict;
223use Scalar::Util;
224
225use base qw( LWP::UserAgent );
226
227my %ClientOf;
228sub new {
229 my($class, $client) = @_;
230 my $ua = $class->SUPER::new;
231 $ClientOf{$ua} = $client;
232 Scalar::Util::weaken($ClientOf{$ua});
233 $ua;
234}
235
236sub get_basic_credentials {
237 my($ua, $realm, $url, $proxy) = @_;
238 my $client = $ClientOf{$ua} or die "Cannot find $ua";
239 return $client->username, $client->password;
240}
241
242sub DESTROY {
243 my $self = shift;
244 delete $ClientOf{$self};
245}
246
2471;
248__END__
249
250=head1 NAME
251
252XML::Atom::Client - A client for the Atom API
253
254=head1 SYNOPSIS
255
256 use XML::Atom::Client;
257 use XML::Atom::Entry;
258 my $api = XML::Atom::Client->new;
259 $api->username('Melody');
260 $api->password('Nelson');
261
262 my $entry = XML::Atom::Entry->new;
263 $entry->title('New Post');
264 $entry->content('Content of my post.');
265 my $EditURI = $api->createEntry($PostURI, $entry);
266
267 my $feed = $api->getFeed($FeedURI);
268 my @entries = $feed->entries;
269
270 my $entry = $api->getEntry($EditURI);
271
272=head1 DESCRIPTION
273
274I<XML::Atom::Client> implements a client for the Atom API described at
275I<http://bitworking.org/projects/atom/draft-gregorio-09.html>, with the
276authentication scheme described at
277I<http://www.intertwingly.net/wiki/pie/DifferentlyAbledClients>.
278
279B<NOTE:> the API, and particularly the authentication scheme, are still
280in flux.
281
282=head1 USAGE
283
284=head2 XML::Atom::Client->new(%param)
285
286=head2 $api->use_soap([ 0 | 1 ])
287
288I<XML::Atom::Client> supports both the REST and SOAP-wrapper versions of the
289Atom API. By default, the REST version of the API will be used, but you can
290turn on the SOAP wrapper--for example, if you need to connect to a server
291that supports only the SOAP wrapper--by calling I<use_soap> with a value of
292C<1>:
293
294 $api->use_soap(1);
295
296If called without arguments, returns the current value of the flag.
297
298=head2 $api->username([ $username ])
299
300If called with an argument, sets the username for login to I<$username>.
301
302Returns the current username that will be used when logging in to the
303Atom server.
304
305=head2 $api->password([ $password ])
306
307If called with an argument, sets the password for login to I<$password>.
308
309Returns the current password that will be used when logging in to the
310Atom server.
311
312=head2 $api->createEntry($PostURI, $entry)
313
314Creates a new entry.
315
316I<$entry> must be an I<XML::Atom::Entry> object.
317
318=head2 $api->getEntry($EditURI)
319
320Retrieves the entry with the given URL I<$EditURI>.
321
322Returns an I<XML::Atom::Entry> object.
323
324=head2 $api->updateEntry($EditURI, $entry)
325
326Updates the entry at URL I<$EditURI> with the entry I<$entry>, which must be
327an I<XML::Atom::Entry> object.
328
329Returns true on success, false otherwise.
330
331=head2 $api->deleteEntry($EditURI)
332
333Deletes the entry at URL I<$EditURI>.
334
335=head2 $api->getFeed($FeedURI)
336
337Retrieves the feed at I<$FeedURI>.
338
339Returns an I<XML::Atom::Feed> object representing the feed returned
340from the server.
341
342=head2 ERROR HANDLING
343
344Methods return C<undef> on error, and the error message can be retrieved
345using the I<errstr> method.
346
347=head1 AUTHOR & COPYRIGHT
348
349Please see the I<XML::Atom> manpage for author, copyright, and license
350information.
351
352=cut