| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
package XML::Atom::Client; |
|---|
| 4 |
use strict; |
|---|
| 5 |
|
|---|
| 6 |
use XML::Atom; |
|---|
| 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('application/x.atom+xml'); |
|---|
| 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('application/x.atom+xml'); |
|---|
| 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/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 |
$req->header('X-WSSE', sprintf |
|---|
| 177 |
qq(UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"), |
|---|
| 178 |
$client->username || '', $digest, $nonce_enc, $now); |
|---|
| 179 |
$req->header('Authorization', 'WSSE profile="UsernameToken"'); |
|---|
| 180 |
} |
|---|
| 181 |
} |
|---|
| 182 |
|
|---|
| 183 |
sub munge_response { |
|---|
| 184 |
my $client = shift; |
|---|
| 185 |
my($res) = @_; |
|---|
| 186 |
if ($client->use_soap && (my $xml = $res->content)) { |
|---|
| 187 |
my $doc; |
|---|
| 188 |
if (LIBXML) { |
|---|
| 189 |
my $parser = XML::LibXML->new; |
|---|
| 190 |
$doc = $parser->parse_string($xml); |
|---|
| 191 |
} else { |
|---|
| 192 |
my $xp = XML::XPath->new(xml => $xml); |
|---|
| 193 |
$doc = ($xp->find('/')->get_nodelist)[0]; |
|---|
| 194 |
} |
|---|
| 195 |
my $body = first($doc, NS_SOAP, 'Body'); |
|---|
| 196 |
if (my $fault = first($body, NS_SOAP, 'Fault')) { |
|---|
| 197 |
$res->code(textValue($fault, undef, 'faultcode')); |
|---|
| 198 |
$res->message(textValue($fault, undef, 'faultstring')); |
|---|
| 199 |
$res->content(''); |
|---|
| 200 |
$res->content_length(0); |
|---|
| 201 |
} else { |
|---|
| 202 |
$xml = join '', map $_->toString(LIBXML ? 1 : 0), |
|---|
| 203 |
LIBXML ? $body->childNodes : $body->getChildNodes; |
|---|
| 204 |
$res->content($xml); |
|---|
| 205 |
$res->content_length(1); |
|---|
| 206 |
} |
|---|
| 207 |
} |
|---|
| 208 |
} |
|---|
| 209 |
|
|---|
| 210 |
sub make_nonce { sha1(sha1(time() . {} . rand() . $$)) } |
|---|
| 211 |
|
|---|
| 212 |
sub _utf8_off { |
|---|
| 213 |
if ($] >= 5.008) { |
|---|
| 214 |
require Encode; |
|---|
| 215 |
Encode::_utf8_off($_[0]); |
|---|
| 216 |
} |
|---|
| 217 |
} |
|---|
| 218 |
|
|---|
| 219 |
package LWP::UserAgent::AtomClient; |
|---|
| 220 |
use strict; |
|---|
| 221 |
|
|---|
| 222 |
use base qw( LWP::UserAgent ); |
|---|
| 223 |
|
|---|
| 224 |
my %ClientOf; |
|---|
| 225 |
sub new { |
|---|
| 226 |
my($class, $client) = @_; |
|---|
| 227 |
my $ua = $class->SUPER::new; |
|---|
| 228 |
$ClientOf{$ua} = $client; |
|---|
| 229 |
$ua; |
|---|
| 230 |
} |
|---|
| 231 |
|
|---|
| 232 |
sub get_basic_credentials { |
|---|
| 233 |
my($ua, $realm, $url, $proxy) = @_; |
|---|
| 234 |
my $client = $ClientOf{$ua} or die "Cannot find $ua"; |
|---|
| 235 |
return $client->username, $client->password; |
|---|
| 236 |
} |
|---|
| 237 |
|
|---|
| 238 |
sub DESTROY { |
|---|
| 239 |
my $self = shift; |
|---|
| 240 |
delete $ClientOf{$self}; |
|---|
| 241 |
} |
|---|
| 242 |
|
|---|
| 243 |
1; |
|---|
| 244 |
__END__ |
|---|
| 245 |
|
|---|
| 246 |
=head1 NAME |
|---|
| 247 |
|
|---|
| 248 |
XML::Atom::Client - A client for the Atom API |
|---|
| 249 |
|
|---|
| 250 |
=head1 SYNOPSIS |
|---|
| 251 |
|
|---|
| 252 |
use XML::Atom::Client; |
|---|
| 253 |
use XML::Atom::Entry; |
|---|
| 254 |
my $api = XML::Atom::Client->new; |
|---|
| 255 |
$api->username('Melody'); |
|---|
| 256 |
$api->password('Nelson'); |
|---|
| 257 |
|
|---|
| 258 |
my $entry = XML::Atom::Entry->new; |
|---|
| 259 |
$entry->title('New Post'); |
|---|
| 260 |
$entry->content('Content of my post.'); |
|---|
| 261 |
my $EditURI = $api->createEntry($PostURI, $entry); |
|---|
| 262 |
|
|---|
| 263 |
my $feed = $api->getFeed($FeedURI); |
|---|
| 264 |
my @entries = $feed->entries; |
|---|
| 265 |
|
|---|
| 266 |
my $entry = $api->getEntry($EditURI); |
|---|
| 267 |
|
|---|
| 268 |
=head1 DESCRIPTION |
|---|
| 269 |
|
|---|
| 270 |
I<XML::Atom::Client> implements a client for the Atom API described at |
|---|
| 271 |
I<http://bitworking.org/projects/atom/draft-gregorio-09.html>, with the |
|---|
| 272 |
authentication scheme described at |
|---|
| 273 |
I<http://www.intertwingly.net/wiki/pie/DifferentlyAbledClients>. |
|---|
| 274 |
|
|---|
| 275 |
B<NOTE:> the API, and particularly the authentication scheme, are still |
|---|
| 276 |
in flux. |
|---|
| 277 |
|
|---|
| 278 |
=head1 USAGE |
|---|
| 279 |
|
|---|
| 280 |
=head2 XML::Atom::Client->new(%param) |
|---|
| 281 |
|
|---|
| 282 |
=head2 $api->use_soap([ 0 | 1 ]) |
|---|
| 283 |
|
|---|
| 284 |
I<XML::Atom::Client> supports both the REST and SOAP-wrapper versions of the |
|---|
| 285 |
Atom API. By default, the REST version of the API will be used, but you can |
|---|
| 286 |
turn on the SOAP wrapper--for example, if you need to connect to a server |
|---|
| 287 |
that supports only the SOAP wrapper--by calling I<use_soap> with a value of |
|---|
| 288 |
C<1>: |
|---|
| 289 |
|
|---|
| 290 |
$api->use_soap(1); |
|---|
| 291 |
|
|---|
| 292 |
If called without arguments, returns the current value of the flag. |
|---|
| 293 |
|
|---|
| 294 |
=head2 $api->username([ $username ]) |
|---|
| 295 |
|
|---|
| 296 |
If called with an argument, sets the username for login to I<$username>. |
|---|
| 297 |
|
|---|
| 298 |
Returns the current username that will be used when logging in to the |
|---|
| 299 |
Atom server. |
|---|
| 300 |
|
|---|
| 301 |
=head2 $api->password([ $password ]) |
|---|
| 302 |
|
|---|
| 303 |
If called with an argument, sets the password for login to I<$password>. |
|---|
| 304 |
|
|---|
| 305 |
Returns the current password that will be used when logging in to the |
|---|
| 306 |
Atom server. |
|---|
| 307 |
|
|---|
| 308 |
=head2 $api->createEntry($PostURI, $entry) |
|---|
| 309 |
|
|---|
| 310 |
Creates a new entry. |
|---|
| 311 |
|
|---|
| 312 |
I<$entry> must be an I<XML::Atom::Entry> object. |
|---|
| 313 |
|
|---|
| 314 |
=head2 $api->getEntry($EditURI) |
|---|
| 315 |
|
|---|
| 316 |
Retrieves the entry with the given URL I<$EditURI>. |
|---|
| 317 |
|
|---|
| 318 |
Returns an I<XML::Atom::Entry> object. |
|---|
| 319 |
|
|---|
| 320 |
=head2 $api->updateEntry($EditURI, $entry) |
|---|
| 321 |
|
|---|
| 322 |
Updates the entry at URL I<$EditURI> with the entry I<$entry>, which must be |
|---|
| 323 |
an I<XML::Atom::Entry> object. |
|---|
| 324 |
|
|---|
| 325 |
Returns true on success, false otherwise. |
|---|
| 326 |
|
|---|
| 327 |
=head2 $api->deleteEntry($EditURI) |
|---|
| 328 |
|
|---|
| 329 |
Deletes the entry at URL I<$EditURI>. |
|---|
| 330 |
|
|---|
| 331 |
=head2 $api->getFeed($FeedURI) |
|---|
| 332 |
|
|---|
| 333 |
Retrieves the feed at I<$FeedURI>. |
|---|
| 334 |
|
|---|
| 335 |
Returns an I<XML::Atom::Feed> object representing the feed returned |
|---|
| 336 |
from the server. |
|---|
| 337 |
|
|---|
| 338 |
=head2 ERROR HANDLING |
|---|
| 339 |
|
|---|
| 340 |
Methods return C<undef> on error, and the error message can be retrieved |
|---|
| 341 |
using the I<errstr> method. |
|---|
| 342 |
|
|---|
| 343 |
=head1 AUTHOR & COPYRIGHT |
|---|
| 344 |
|
|---|
| 345 |
Please see the I<XML::Atom> manpage for author, copyright, and license |
|---|
| 346 |
information. |
|---|
| 347 |
|
|---|
| 348 |
=cut |
|---|
| 349 |
|
|---|