File Coverage

blib/lib/Net/OpenID/Yadis.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Net::OpenID::Yadis;
2             $Net::OpenID::Yadis::VERSION = '1.20';
3 1     1   12695 use strict;
  1         1  
  1         21  
4 1     1   3 use warnings;
  1         1  
  1         19  
5              
6 1     1   3 use base qw(Exporter);
  1         1  
  1         71  
7 1     1   3 use Carp ();
  1         1  
  1         11  
8 1     1   322 use Net::OpenID::URIFetch;
  1         1  
  1         22  
9 1     1   712 use XML::Simple;
  1         5678  
  1         4  
10 1     1   425 use Net::OpenID::Yadis::Service;
  1         2  
  1         20  
11 1     1   336 use Net::OpenID::Common;
  0            
  0            
12             use HTTP::Headers::Util qw(split_header_words);
13             use Encode;
14              
15             our @EXPORT = qw(YR_HEAD YR_GET YR_XRDS);
16              
17             use constant YR_GET => 1;
18             use constant YR_XRDS => 2;
19              
20             use fields (
21             'last_errcode', # last error code we got
22             'last_errtext', # last error code we got
23             'debug', # debug flag or codeblock
24             'consumer', # consumer object
25             'identity_url', # URL to be identified
26             'xrd_url', # URL of XRD file
27             'xrd_objects', # Yadis XRD decoded objects
28             );
29              
30             sub new {
31             my $self = shift;
32             $self = fields::new( $self ) unless ref $self;
33             my %opts = @_;
34              
35             $self->consumer(delete($opts{consumer}));
36              
37             $self->{debug} = delete $opts{debug};
38              
39             Carp::croak("Unknown options: " . join(", ", keys %opts)) if %opts;
40              
41             return $self;
42             }
43              
44             sub consumer { &_getset; }
45              
46             sub identity_url { &_getset; }
47             sub xrd_url { &_getset; }
48             sub xrd_objects { _pack_array(&_getset); }
49             sub _getset {
50             my $self = shift;
51             my $param = (caller(1))[3];
52             $param =~ s/.+:://;
53              
54             if (@_) {
55             my $val = shift;
56             Carp::croak("Too many parameters") if @_;
57             $self->{$param} = $val;
58             }
59             return $self->{$param};
60             }
61              
62             sub _debug {
63             my $self = shift;
64             return unless $self->{debug};
65              
66             if (ref $self->{debug} eq "CODE") {
67             $self->{debug}->($_[0]);
68             } else {
69             print STDERR "[DEBUG Net::OpenID::Yadis] $_[0]\n";
70             }
71             }
72              
73             sub _fail {
74             my $self = shift;
75             my ($code, $text) = @_;
76              
77             $text ||= {
78             'xrd_parse_error' => "Error occured since parsing yadis document.",
79             'xrd_format_error' => "This is not yadis document (not xrds format).",
80             'too_many_hops' => 'Too many hops by X-XRDS-Location.',
81             'empty_url' => 'Empty URL',
82             'no_yadis_document' => 'Cannot find yadis Document',
83             'url_gone' => 'URL is no longer available',
84             }->{$code};
85              
86             $self->{last_errcode} = $code;
87             $self->{last_errtext} = $text;
88              
89             $self->_debug("fail($code) $text");
90             wantarray ? () : undef;
91             }
92             sub err {
93             my $self = shift;
94             $self->{last_errcode} . ": " . $self->{last_errtext};
95             }
96             sub errcode {
97             my $self = shift;
98             $self->{last_errcode};
99             }
100             sub errtext {
101             my $self = shift;
102             $self->{last_errtext};
103             }
104             sub _clear_err {
105             my $self = shift;
106             $self->{last_errtext} = '';
107             $self->{last_errcode} = '';
108             }
109              
110             sub _get_contents {
111             my $self = shift;
112             my ($url, $final_url_ref, $content_ref, $headers_ref) = @_;
113              
114             # we do NOT do elimination here because
115             # if it's an HTML document, we are only ever looking at the headers, and
116             # if it's a YADIS document, elimination is not appropriate
117             # (YADIS is not HTML; film at 11)
118             my $res = Net::OpenID::URIFetch->fetch($url, $self->consumer);
119              
120             if ($res) {
121             $$final_url_ref = $res->final_uri;
122             my $headers = $res->headers;
123             foreach my $k (keys %$headers) {
124             $headers_ref->{$k} ||= $headers->{$k};
125             }
126             $$content_ref = $res->content;
127             return 1;
128             }
129             else {
130             return undef;
131             }
132             }
133              
134             sub parse_content_type {
135             # stolen from HTTP::Headers but returns lc charset
136             my $h = shift;
137             $h = $h->[0] if ref($h);
138             $h = "" unless defined $h;
139             my ($v) = (split_header_words($h), []);
140             my($ct, undef, %ct_param) = @$v;
141             $ct ||= '';
142             $ct = lc($ct);
143             $ct =~ s/\s+//;
144             my $charset = lc($ct_param{charset} || '');
145             $charset =~ s/^\s+//;
146             $charset =~ s/\s+\z//;
147             return ($ct, $charset);
148             }
149              
150             sub discover {
151             my $self = shift;
152             my $url = shift or return $self->_fail("empty_url");
153             my $count = shift || YR_GET;
154             Carp::croak("Too many parameters") if @_;
155              
156             # trim whitespace
157             $url =~ s/^\s+//;
158             $url =~ s/\s+$//;
159             return $self->_fail("empty_url") unless $url;
160              
161             my $final_url;
162             my %headers;
163              
164             my $xrd;
165             $self->_get_contents($url, \$final_url, \$xrd, \%headers) or return;
166              
167             $self->identity_url($final_url) if ($count < YR_XRDS);
168              
169             # (1) found YADIS/XRDS-Location headers
170             if ($count < YR_XRDS and
171             my $doc_url = $headers{'x-yadis-location'} || $headers{'x-xrds-location'}
172             ) {
173             return $self->discover($doc_url, YR_XRDS);
174             }
175              
176             # (2) is content type YADIS document?
177             my ($ctype, $charset) = parse_content_type($headers{'content-type'});
178             if ($ctype eq 'application/xrds+xml') {
179             #survey says Yes!
180             $self->xrd_url($final_url);
181              
182             return $self->parse_xrd($xrd);
183             }
184              
185             # (3) YADIS/XRDS-location might be in a tag.
186             if ( $ctype eq 'text/html' and
187             my ($meta) = grep {
188             my $heqv = lc($_->{'http-equiv'}||'');
189             $heqv eq 'x-yadis-location' || $heqv eq 'x-xrds-location'
190             }
191             @{OpenID::util::html_extract_linkmetas($xrd)->{meta}||[]}
192             ) {
193             return $self->discover($meta->{content}, YR_XRDS);
194             }
195             return $self->_fail($count == YR_GET ? "no_yadis_document" : "too_many_hops");
196             }
197              
198             sub parse_xrd {
199             my $self = shift;
200             my $xrd = shift;
201             Carp::croak("Too many parameters") if @_;
202              
203             my $xs_hash = XMLin($xrd) or return $self->_fail("xrd_parse_error");
204             ($xs_hash->{'xmlns'} and $xs_hash->{'xmlns'} eq 'xri://$xrd*($v*2.0)') or $self->_fail("xrd_format_error");
205             my %xmlns;
206             foreach (map { /^(xmlns:(.+))$/ and [$1,$2] } keys %$xs_hash) {
207             next unless ($_);
208             $xmlns{$_->[1]} = $xs_hash->{$_->[0]};
209             }
210             my @priority;
211             my @nopriority;
212             foreach my $service (_pack_array($xs_hash->{'XRD'}{'Service'})) {
213             bless $service, "Net::OpenID::Yadis::Service";
214             $service->{'Type'} or next;
215             $service->{'URI'} ||= $self->identity_url;
216              
217             foreach my $sname (keys %$service) {
218             foreach my $ns (keys %xmlns) {
219             $service->{"{$xmlns{$ns}}$1"} = delete $service->{$sname} if ($sname =~ /^${ns}:(.+)$/);
220             }
221             }
222             defined($service->{'priority'}) ? push(@priority,$service) : push(@nopriority,$service);
223             # Services without priority fields are lowest priority
224             }
225             my @service = sort {$a->{'priority'} <=> $b->{'priority'}} @priority;
226             push (@service,@nopriority);
227             foreach (grep {/^_protocol/} keys %$self) { delete $self->{$_} }
228              
229             $self->xrd_objects(\@service);
230             }
231              
232             sub _pack_array { wantarray ? ref($_[0]) eq 'ARRAY' ? @{$_[0]} : ($_[0]) : $_[0] }
233              
234             sub services {
235             my $self = shift;
236             my %protocols;
237             my @protocols;
238             my $code_ref;
239             my $protocol = undef;
240              
241             Carp::croak("You haven't called the discover method yet") unless $self->xrd_objects;
242              
243             foreach my $option (@_) {
244             Carp::croak("No further arguments allowed after code reference argument") if $code_ref;
245             my $ref = ref($option);
246             if ($ref eq 'CODE') {
247             $code_ref = $option;
248             } else {
249             my $default = {versionarray => []};
250              
251             $protocols{$option} = $default;
252             $protocol = $option;
253             push @protocols, $option;
254             }
255             }
256              
257             my @servers;
258             @servers = $self->xrd_objects if (keys %protocols == 0);
259             foreach my $key (@protocols) {
260             my $regex = $protocols{$key}->{urlregex} || $key;
261             my @ver = @{$protocols{$key}->{versionarray}};
262             my $ver_regex = @ver ? '('.join('|',map { $_ =~ s/\./\\./g; $_ } @ver).')' : '.+' ;
263             $regex =~ s/\\ver/$ver_regex/;
264              
265             push (@servers,map { $protocols{$key}->{objectclass} ? bless($_ , $protocols{$key}->{objectclass}) : $_ } grep {join(",",$_->Type) =~ /$regex/} $self->xrd_objects);
266             }
267              
268             @servers = $code_ref->(@servers) if ($code_ref);
269              
270             wantarray ? @servers : \@servers;
271             }
272              
273             1;
274             __END__