root/branches/openid2/perl/Net-OpenID-Consumer/lib/Net/OpenID/Yadis.pm

Revision 138, 11.8 kB (checked in by mart, 1 month ago)

Rework the discovery bits so that they all go through the same code path for HTTP, and implement a caching layer that actually works for both OpenID and Yadis.

URI::Fetch isn't really suitable since it doesn't cache enough information for our purposes. Hopefully at some point URI::Fetch can be improved so that we can use it, but for now this works.

Line 
1 package Net::OpenID::Yadis;
2
3 use strict;
4 use warnings;
5 use vars qw($VERSION @EXPORT);
6 $VERSION = "0.05";
7
8 use base qw(Exporter);
9 use Carp ();
10 use URI::Fetch 0.02;
11 use XML::Simple;
12 use Net::OpenID::Yadis::Service;
13
14 @EXPORT = qw(YR_HEAD YR_GET YR_XRDS);
15
16 use constant {
17     YR_GET => 1,
18     YR_XRDS => 2,
19 };
20
21 use fields (
22             'last_errcode',    # last error code we got
23             'last_errtext',    # last error code we got
24             'debug',           # debug flag or codeblock
25             'consumer',        # consumer object
26             'identity_url',    # URL to be identified
27             'xrd_url',         # URL of XRD file
28             'xrd_objects',     # Yadis XRD decoded objects
29             );
30
31 sub new {
32     my $self = shift;
33     $self = fields::new( $self ) unless ref $self;
34     my %opts = @_;
35
36     $self->consumer(delete($opts{consumer}));
37
38     $self->{debug} = delete $opts{debug};
39
40     Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
41
42     return $self;
43 }
44
45 sub consumer { &_getset; }
46
47 sub identity_url { &_getset; }
48 sub xrd_url { &_getset; }
49 sub xrd_objects { _pack_array(&_getset); }
50 sub _getset {
51     my $self = shift;
52     my $param = (caller(1))[3];
53     $param =~ s/.+:://;
54
55     if (@_) {
56         my $val = shift;
57         Carp::croak("Too many parameters") if @_;
58         $self->{$param} = $val;
59     }
60     return $self->{$param};
61 }
62
63 sub _debug {
64     my $self = shift;
65     return unless $self->{debug};
66
67     if (ref $self->{debug} eq "CODE") {
68         $self->{debug}->($_[0]);
69     } else {
70         print STDERR "[DEBUG Net::OpenID::Yadis] $_[0]\n";
71     }
72 }
73
74 sub _fail {
75     my $self = shift;
76     my ($code, $text) = @_;
77
78     $text ||= {
79         'xrd_parse_error' => "Error occured since parsing yadis document.",
80         'xrd_format_error' => "This is not yadis document (not xrds format).",
81         'too_many_hops' => 'Too many hops by X-XRDS-Location.',
82         'empty_url' => 'Empty URL',
83         'no_yadis_document' => 'Cannot find yadis Document',
84         'url_gone' => 'URL is no longer available',
85     }->{$code};
86
87     $self->{last_errcode} = $code;
88     $self->{last_errtext} = $text;
89
90     $self->_debug("fail($code) $text");
91     wantarray ? () : undef;
92 }
93 sub err {
94     my $self = shift;
95     $self->{last_errcode} . ": " . $self->{last_errtext};
96 }
97 sub errcode {
98     my $self = shift;
99     $self->{last_errcode};
100 }
101 sub errtext {
102     my $self = shift;
103     $self->{last_errtext};
104 }
105 sub _clear_err {
106     my $self = shift;
107     $self->{last_errtext} = '';
108     $self->{last_errcode} = '';
109 }
110
111 sub _get_contents {
112     my $self = shift;
113     my  ($url, $final_url_ref, $content_ref, $headers_ref) = @_;
114
115     my $alter_hook = sub {
116         my $htmlref = shift;
117         $$htmlref =~ s/<body\b.*//is;
118     };
119
120     my $res = Net::OpenID::URIFetch->fetch($url, $self->consumer, $alter_hook);
121
122     if ($res) {
123         $$final_url_ref = $res->final_uri;
124         my $headers = $res->headers;
125         foreach my $k (keys %$headers) {
126             $headers_ref->{$k} ||= $headers->{$k};
127         }
128         $$content_ref = $res->content;
129         return 1;
130     }
131     else {
132         return undef;
133     }
134 }
135
136 sub discover {
137     my $self = shift;
138     my $url = shift or return $self->_fail("empty_url");
139     my $count = shift || YR_GET;
140     Carp::croak("Too many parameters") if @_;
141
142     # trim whitespace
143     $url =~ s/^\s+//;
144     $url =~ s/\s+$//;
145     return $self->_fail("empty_url") unless $url;
146
147     my $final_url;
148     my %headers;
149
150     my $xrd;
151     $self->_get_contents($url, \$final_url, \$xrd, \%headers) or return;
152
153     $self->identity_url($final_url) if ($count < YR_XRDS);
154
155     my $doc_url;
156     if (($doc_url = $headers{'x-yadis-location'} || $headers{'x-xrds-location'}) && ($count < YR_XRDS)) {
157         return $self->discover($doc_url, YR_XRDS);
158     }
159     elsif ($headers{'content-type'} eq 'application/xrds+xml') {
160         $self->xrd_url($final_url);
161         return $self->parse_xrd($xrd);
162     }
163     else {
164         return $self->_fail($count == YR_GET ? "no_yadis_document" : "too_many_hops");
165     }
166 }
167
168 sub parse_xrd {
169     my $self = shift;
170     my $xrd = shift;
171     Carp::croak("Too many parameters") if @_;
172
173     my $xs_hash = XMLin($xrd) or return $self->_fail("xrd_parse_error");
174     ($xs_hash->{'xmlns'} and $xs_hash->{'xmlns'} eq 'xri://$xrd*($v*2.0)') or $self->_fail("xrd_format_error");
175     my %xmlns;
176     foreach (map { /^(xmlns:(.+))$/ and [$1,$2] } keys %$xs_hash) {
177         next unless ($_);
178         $xmlns{$_->[1]} = $xs_hash->{$_->[0]};
179     }
180     my @priority;
181     my @nopriority;
182     foreach my $service (_pack_array($xs_hash->{'XRD'}{'Service'})) {
183         bless $service, "Net::OpenID::Yadis::Service";
184         $service->{'Type'} or next;
185         $service->{'URI'} ||= $self->identity_url;
186
187         foreach my $sname (keys %$service) {
188             foreach my $ns (keys %xmlns) {
189                 $service->{"{$xmlns{$ns}}$1"} = delete $service->{$sname} if ($sname =~ /^${ns}:(.+)$/);
190             }
191         }
192         defined($service->{'priority'}) ? push(@priority,$service) : push(@nopriority,$service);
193         # Services without priority fields are lowest priority
194     }
195     my @service = sort {$a->{'priority'} <=> $b->{'priority'}} @priority;
196     push (@service,@nopriority);
197     foreach (grep {/^_protocol/} keys %$self) { delete $self->{$_} }
198
199     $self->xrd_objects(\@service);
200 }
201
202 sub _pack_array { wantarray ? ref($_[0]) eq 'ARRAY' ? @{$_[0]} : ($_[0]) : $_[0] }
203
204 sub services {
205     my $self = shift;
206     my %protocols;
207     my @protocols;
208     my $code_ref;
209     my $protocol = undef;
210
211     Carp::croak("You haven't called the discover method yet") unless $self->xrd_objects;
212
213     foreach my $option (@_) {
214         Carp::croak("No further arguments allowed after code reference argument") if $code_ref;
215         my $ref = ref($option);
216         if ($ref eq 'CODE') {
217             $code_ref = $option;
218         } else {
219             my $default = {versionarray => []};
220
221             $protocols{$option} = $default;
222             $protocol = $option;
223             push @protocols, $option;
224         }
225     }
226
227     my @servers;
228     @servers = $self->xrd_objects if (keys %protocols == 0);
229     foreach my $key (@protocols) {
230         my $regex = $protocols{$key}->{urlregex} || $key;
231         my @ver = @{$protocols{$key}->{versionarray}};
232         my $ver_regex = @ver ? '('.join('|',map { $_ =~ s/\./\\./g; $_ } @ver).')' : '.+' ;
233         $regex =~ s/\\ver/$ver_regex/;
234
235         push (@servers,map { $protocols{$key}->{objectclass} ? bless($_ , $protocols{$key}->{objectclass}) : $_ } grep {join(",",$_->Type) =~ /$regex/} $self->xrd_objects);
236     }
237
238     @servers = $code_ref->(@servers) if ($code_ref);
239
240     wantarray ? @servers : \@servers;
241 }
242
243 1;
244 __END__
245
246 =head1 NAME
247
248 Net::OpenID::Yadis - Perform Yadis discovery on URLs
249
250 =head1 SYNOPSIS
251
252   use Net::OpenID::Yadis;
253  
254   my $disc = Net::OpenID::Yadis->new(
255       consumer => $consumer, # Net::OpenID::Consumer object
256   );
257
258   my $xrd = $disc->discover("http://id.example.com/") or Carp::croak($disc->err);
259
260   print $disc->identity_url;       # Yadis URL (Final URL if redirected)
261   print $disc->xrd_url;            # Yadis Resourse Descriptor URL
262
263   foreach my $srv (@$xrd) {        # Loop for Each Service in Yadis Resourse Descriptor
264     print $srv->priority;          # Service priority (sorted)
265     print $srv->Type;              # Identifier of some version of some service (scalar, array or array ref)
266     print $srv->URI;               # URI that resolves to a resource providing the service (scalar, array or array ref)
267     print $srv->extra_field("Delegate","http://openid.net/xmlns/1.0");
268                                    # Extra field of some service
269   }
270
271   # If you are interested only in OpenID. (either 1.1 or 2.0)
272   my $xrd = $self->services(
273     'http://specs.openid.net/auth/2.0/signon',
274     'http://specs.openid.net/auth/2.0/server',
275     'http://openid.net/signon/1.1',
276   );
277
278   # If you want to choose random server by code-ref.
279   my $xrd = $self->services(sub{($_[int(rand(@_))])});
280
281 =head1 DESCRIPTION
282
283 This module provides an implementation of the Yadis protocol, which does
284 XRDS-based service discovery on URLs.
285
286 This module was originally developed by OHTSUKA Ko-hei as L<Net::Yadis::Discovery>,
287 but was forked and simplified for inclusion in the core OpenID Consumer package.
288
289 This simplified version is tailored for the needs of Net::OpenID::Consumer; for other
290 uses, L<Net::Yadis::Discovery> is probably a better choice.
291
292 =head1 CONSTRUCTOR
293
294 =over 4
295
296 =item C<new>
297
298 my $disc = Net::OpenID::Yadis->new([ %opts ]);
299
300 You can set the C<consumer> in the constructor.  See the corresponding
301 method description below.
302
303 =back
304
305 =head1 EXPORT
306
307 This module exports three constant values to use with discover method.
308
309 =over 4
310
311 =item C<YR_GET>
312
313 If you set this, module check Yadis URL start from HTTP GET request. This is the default.
314
315 =item C<YR_XRDS>
316
317 If you set this, this module consider Yadis URL as Yadis Resource Descriptor URL.
318 If not so, an error is returned.
319
320 =back
321
322 =head1 METHODS
323
324 =over 4
325
326 =item $disc->B<consumer>($consumer)
327
328 =item $disc->B<consumer>
329
330 Get or set the Net::OpenID::Consumer object that this object is associated with.
331
332 =item $disc->B<discover>($url,[$request_method])
333
334 Given a user-entered $url (which could be missing http://, or have
335 extra whitespace, etc), returns either array/array ref of Net::OpenID::Yadis::Service
336 objects, or undef on failure.
337
338 $request_method is optional, and if set this, you can change the HTTP
339 request method of fetching Yadis URL.
340 See EXPORT to know the value you can set, and default is YR_HEAD.
341
342 If this method returns undef, you can rely on the following errors
343 codes (from $csr->B<errcode>) to decide what to present to the user:
344
345 =over 8
346
347 =item xrd_parse_error
348
349 =item xrd_format_error
350
351 =item too_many_hops
352
353 =item no_yadis_document
354
355 =item url_fetch_err
356
357 =item empty_url
358
359 =item url_gone
360
361 =back
362
363 =item $disc->B<xrd_objects>
364
365 Returns array/array ref of Net::OpenID::Yadis objects.
366 It is same what could be got by discover method.
367
368 =item $disc->B<identity_url>
369
370 Returns Yadis URL.
371 If not redirected, it is same with the argument of discover method.
372
373 =item $disc->B<xrd_url>
374
375 Returns Yadis Resource Descriptor URL.
376
377 =item $disc->B<servers>($protocol,$protocol,...)
378
379 =item $disc->B<servers>($protocol=>[$version1,$version2],...)
380
381 =item $disc->B<servers>($protocol,....,$code_ref);
382
383 Filter method of xrd_objects.
384
385 If no opton is defined, returns same result with xrd_objects method.
386
387 protocol names or Type URLs are given, filter only given protocol.
388 Two or more protocols are given, return and results of filtering.
389
390 Sample:
391   $disc->servers("openid","http://lid.netmesh.org/sso/1.0");
392
393 If reference of version numbers array is given after protocol names,
394 filter only given version of protocol.
395
396 Sample:
397   $disc->servers("openid"=>['1.0','1.1'],"lid"=>['1.0']);
398
399 If you want to use version numbers limitation with type URL, you can use
400 \ver as place holder of version number.
401
402 Sample:
403   $disc->servers("http://lid.netmesh.org/sso/\ver"=>['1.0','2.0']);
404
405 If code reference is given as argument , you can make your own filter rule.
406 code reference is executed at the last of filtering logic, like this:
407
408   @results = $code_ref->(@temporary_results)
409
410 Sample: If you want to filter OpenID server and get only first one:
411   ($openid_server) = $disc->servers("openid",sub{$_[0]});
412
413 =item $disc->B<err>
414
415 Returns the last error, in form "errcode: errtext"
416
417 =item $disc->B<errcode>
418
419 Returns the last error code.
420
421 =item $disc->B<errtext>
422
423 Returns the last error text.
424
425 =back
426
427 =head1 COPYRIGHT
428
429 This module is Copyright (c) 2006 OHTSUKA Ko-hei.
430 All rights reserved.
431
432 You may distribute under the terms of either the GNU General Public
433 License or the Artistic License, as specified in the Perl README file.
434
435 =head1 WARRANTY
436
437 This is free software. IT COMES WITHOUT WARRANTY OF ANY KIND.
438
439 =head1 SEE ALSO
440
441 Yadis website:  L<http://yadis.org/>
442
443 L<Net::OpenID::Yadis::Service>
444
445 L<Net::OpenID::Consumer>
446
447 =head1 AUTHORS
448
449 Based on L<Net::Yadis::Discovery> by OHTSUKA Ko-hei <nene@kokogiko.net>
450
451 Martin Atkins <mart@degeneration.co.uk>
452
453 =cut
Note: See TracBrowser for help on using the browser.