File Coverage

blib/lib/Net/Yadis.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             # Copyright 2006 JanRain Inc. Licensed under LGPL
3             # Author: Dag Arneson
4              
5             package Net::Yadis;
6              
7 1     1   48036 use warnings;
  1         4  
  1         43  
8 1     1   7 use strict;
  1         2  
  1         237  
9              
10             our $VERSION = "1.0";
11              
12 1     1   1544 use XML::XPath;
  0            
  0            
13              
14             eval "use LWPx::ParanoidAgent;";
15             my $userAgentClass;
16             if($@) {
17             warn "consider installing more secure LWPx::ParanoidAgent\n";
18             use LWP::UserAgent;
19             $userAgentClass = "LWP::UserAgent";
20             }
21             else {
22             $userAgentClass = "LWPx::ParanoidAgent";
23             }
24             sub _userAgentClass { # Mainly for testing. Needs to be able to get and post
25             my $agent = shift;
26             $userAgentClass = $agent if $agent;
27             return $userAgentClass;
28             }
29              
30             # finds meta http-equiv tags
31             use Net::Yadis::HTMLParse qw(parseMetaTags);
32              
33             # must be lowercase.
34             my $YADIS_HEADER = 'x-xrds-location'; # this header is in the 1.0 yadis spec
35             # The following header was in an early version of the spec, and was
36             # still in wide use at the time of writing
37             my $COMPAT_YADIS_HEADER = 'x-yadis-location';
38              
39             =head1 Net::Yadis
40              
41             This package performs the Yadis service discovery protocol, and parses
42             XRDS xml documents.
43              
44             =head2 Methods
45              
46             =head3 discover
47              
48             This constructor performs the discovery protocol on a url and returns
49             a yadis object that parses the XRDS document for you.
50              
51             eval {
52             $yadis=Net::Yadis->discover($url);
53             }
54             warn "Yadis failed: $@" if $@;
55            
56             Will die on errors: HTTP errors, missing Yadis magic, malformed XRDS
57              
58             =cut
59              
60             sub discover {
61             my $caller = shift;
62             my $uri = shift;
63              
64             my $ua = $userAgentClass->new;
65             my $resp = $ua->get($uri, 'Accept' => 'application/xrds+xml');
66              
67             die "Failed to fetch $uri" unless $resp->is_success;
68             $uri = $resp->base;
69             my ($xrds_text, $xrds_uri);
70             my $ct = $resp->header('content-type');
71             if ($ct and $ct eq 'application/xrds+xml') {
72             $xrds_text = $resp->content;
73             $xrds_uri = $resp->base;
74             }
75             else {
76             my $yadloc = $resp->header($YADIS_HEADER) || $resp->header($COMPAT_YADIS_HEADER);
77            
78             unless($yadloc) {
79             my $equiv_headers = parseMetaTags($resp->content);
80             $yadloc = $equiv_headers->{$YADIS_HEADER} || $equiv_headers->{$COMPAT_YADIS_HEADER};
81             }
82             if($yadloc) {
83             my $resp2 = $ua->get($yadloc);
84             die "Bad Yadis URL: $uri - Could not fetch $yadloc" unless $resp2->is_success;
85             $xrds_text = $resp2->content;
86             $xrds_uri = $resp2->base; # but out of spec if not equal to $yadloc
87             }
88             else {
89             die "$uri is not a YADIS URL";
90             }
91             }
92             $caller->new($uri, $xrds_uri, $xrds_text)
93             }
94              
95             =head3 new
96              
97             You may also skip discovery and go straight to xrds parsing with the C
98             constructor.
99              
100             $yadis = Net::Yadis->new($yadis_url, $xrds_url, $xml);
101              
102             =over
103              
104             =item $yadis_url
105              
106             the identity URL
107              
108             =item $xrds_url
109              
110             where we got the xrds document
111              
112             =item $xml
113              
114             the XRDS xml as text
115              
116             =back
117              
118             We don't trap death from XML::XPath; malformed xml causes this
119              
120             =cut
121              
122             sub new {
123             my $caller = shift;
124             my ($yadis_url, $xrds_url, $xml) = @_;
125              
126             my $class = ref($caller) || $caller;
127              
128             my $xrds;
129             $xrds = XML::XPath->new(xml => $xml);
130             $xrds->set_namespace("xrds", 'xri://$xrds');
131             $xrds->set_namespace("xrd", 'xri://$xrd*($v*2.0)');
132            
133             my @svc_nodes = sort byPriority
134             $xrds->findnodes("/xrds:XRDS/xrd:XRD[last()]/xrd:Service");
135             my @services;
136             for(@svc_nodes) {
137             push @services, Net::Yadis::Service->new($xrds, $_);
138             }
139            
140             my $self = {
141             yadis_url => $yadis_url,
142             xrds_url => $xrds_url,
143             xrds => $xrds,
144             xml => $xml,
145             services => \@services,
146             };
147              
148             bless ($self, $class);
149             }
150              
151             =head3 Accessor methods
152              
153             =over
154              
155             =item xml
156              
157             The XML text of the XRDS document.
158              
159             =item url
160              
161             The Yadis URL.
162              
163             =item xrds_url
164              
165             The URL where the XRDS document was found.
166              
167             =item xrds_xpath
168              
169             The XML::XPath object used internally is made available to allow custom
170             XPath queries.
171              
172             =item services
173              
174             An array of Net::Yadis::Service objects representing the services
175             advertised in the XRDS file.
176              
177             =back
178              
179             =cut
180              
181             sub xml {
182             my $self = shift;
183             $self->{xml};
184             }
185             sub url {
186             my $self = shift;
187             $self->{yadis_url};
188             }
189             sub xrds_url {
190             my $self = shift;
191             $self->{xrds_url};
192             }
193             sub xrds_xpath {
194             my $self = shift;
195             $self->{xrds};
196             }
197              
198             # sorting helper function for xpath nodes
199             # I wonder if doing the random order for the services significantly
200             # increases the running time of this function.
201             sub byPriority {
202             my $apriori = $a->getAttribute('priority');
203             my $bpriori = $b->getAttribute('priority');
204             srand;
205             # a defined priority comes before an undefined priority.
206             if (not defined($apriori)) { # we assume nothing
207             return defined($bpriori) || ((rand > 0.5) ? 1 : -1);
208             }
209             elsif (not defined($bpriori)) {
210             return -1;
211             }
212             int($apriori) <=> int($bpriori) || ((rand > 0.5) ? 1 : -1);
213             }
214              
215             # using a sorting helper from another package doesn't work, so
216             # we use this function when sorting URIs in the service object
217             sub _triage {
218             sort byPriority @_;
219             }
220              
221             sub services {
222             my $self = shift;
223             return @{$self->{services}}
224             }
225              
226             =head3 filter_services
227              
228             Pass in a filter function reference to this guy. The filter function
229             must take a Net::Yadis::Service object, and return a scalar of some sort
230             or undef. The scalars returned from the filter will be returned in an
231             array from this method.
232              
233             =head4 Example
234              
235             my $filter = sub {
236             my $service = shift;
237             if ($service->is_type($typere)) {
238             # here we simply return the service object, but you may return
239             # something else if you wish to extract the data and discard
240             # the xpath object contained in the service object.
241             return $service;
242             }
243             else {
244             return undef;
245             }
246             };
247              
248             my $typeservices = $yadis->filter_services($filter);
249              
250             =cut
251              
252             sub filter_services {
253             my $self = shift;
254             my $filter = shift;
255            
256             my @allservices = $self->services;
257             my @filteredservices;
258             for my $service (@allservices) {
259             my $filtered_service = &$filter($service);
260             push @filteredservices, $filtered_service if defined($filtered_service);
261             }
262              
263             return @filteredservices;
264             }
265              
266             =head3 services_of_type
267              
268             A predefined filtering method that takes a regexp for filtering service
269             types.
270              
271             =cut
272              
273             # here is an example using a filter function
274             sub services_of_type {
275             my $self = shift;
276             my $typere = shift;
277            
278             my $filter = sub {
279             my $service = shift;
280             if ($service->is_type($typere)) {
281             # here we simply return the service object, but you may return
282             # something else if you wish to extract the data and discard
283             # the xpath object contained in the service object.
284             return $service;
285             }
286             else {
287             return undef;
288             }
289             };
290             return $self->filter_services($filter);
291             }
292              
293             =head3 service_of_type
294              
295             Hey, a perl generator! sequential calls will return the services one
296             at a time, in ascending priority order with ties randomly decided.
297             make sure that the type argument is identical for each call, or the list
298             will start again from the top. You'll have to store the yadis object in
299             a session for this guy to be useful.
300              
301             =cut
302              
303             sub service_of_type {
304             my $self = shift;
305             my $typere = shift;
306              
307             # remaining services of type
308             my $rsot = $self->{rsot};
309             my @remaining_services;
310             if (defined($rsot->{$typere})) {
311             @remaining_services = @{$rsot->{$typere}};
312             }
313             else {
314             @remaining_services = $self->services_of_type($typere);
315             }
316             my $service = shift @remaining_services;
317             $rsot->{$typere} = \@remaining_services;
318             $self->{rsot}=$rsot;
319             return $service;
320             }
321              
322             1;
323              
324             package Net::Yadis::Service;
325              
326             =head1 Net::Yadis::Service
327              
328             An object representing a service tag in an XRDS document.
329              
330             =head2 Methods
331              
332             =head3 is_type
333              
334             Takes a regexp or a string and returns a boolean value: do any of the
335             C<< >> tags in the C<< >> tag match this type?
336              
337             =cut
338              
339             #typere: regexp or string
340             sub is_type {
341             my $self = shift;
342             my $typere = shift;
343            
344             my $xrds = $self->{xrds};
345             my $typenodes = $xrds->findnodes("./xrd:Type", $self->{node});
346             my $is_type = 0;
347             while($typenodes->size) {
348             # string_value contains the first node's value
349             if ($typenodes->string_value =~ qr{$typere}) {
350             $is_type = 1;
351             last;
352             }
353             $typenodes->shift;
354             }
355             return $is_type;
356             }
357              
358             =head3 types
359              
360             Returns a list of the contents of the C<< >> tags of this service
361             element.
362              
363             =cut
364              
365             sub types {
366             my $self = shift;
367            
368             my $xrds = $self->{xrds};
369             my @typenodes = $xrds->findnodes("./xrd:Type", $self->{node});
370             my @types;
371             for my $tn (@typenodes) {
372             push @types, $xrds->getNodeText($tn);
373             }
374             return @types;
375             }
376              
377             =head3 uris
378              
379             Returns a list of the contents of the C<< >> tags of this service
380             element, in priority order, ties randomly decided.
381              
382             =cut
383              
384              
385             sub uris {
386             my $self = shift;
387            
388             my $xrds = $self->{xrds};
389             my @urinodes = Net::Yadis::_triage $xrds->findnodes("./xrd:URI", $self->{node});
390             my @uris;
391             for my $un (@urinodes) {
392             push @uris, $xrds->getNodeText($un);
393             }
394             return @uris;
395             }
396              
397             =head3 uri
398              
399             another perl 'generator'. sequential calls will return the uris one
400             at a time, in ascending priority order with ties randomly decided
401              
402             =cut
403              
404             sub uri {
405             my $self = shift;
406             my @untried_uris;
407             if (defined($self->{untried_uris})) {
408             @untried_uris = @{$self->{untried_uris}};
409             } else {
410             @untried_uris = $self->uris;
411             }
412             my $uri = shift (@untried_uris);
413             $self->{untried_uris} = \@untried_uris;
414             return $uri;
415             }
416              
417             =head3 getAttribute
418              
419             Get an attribute of the service tag by name.
420              
421             $priority = $service->getAttribute('priority');
422              
423             =cut
424              
425             sub getAttribute {
426             my $self = shift;
427             my $key = shift;
428             my $node = $self->{node};
429             $node->getAttribute($key);
430             }
431              
432             =head3 findTag
433              
434             Get the contents of a child tag of the service tag.
435              
436             $service->findTag($tag_name, $namespace);
437              
438             For example:
439              
440             $delegate = $service->findTag('Delegate', $OPENID_NS);
441              
442             =cut
443              
444             sub findTag {
445             my $self = shift;
446             my $tagname = shift;
447             my $namespace = shift;
448              
449             my $xrds = $self->{xrds};
450             my $svcnode = $self->{node};
451            
452             my $value;
453             if($namespace) {
454             $xrds->set_namespace("asdf", $namespace);
455             $value = $xrds->findvalue("./asdf:$tagname", $svcnode);
456             }
457             else {
458             $value = $xrds->findvalue("./$tagname", $svcnode);
459             }
460            
461             return $value;
462             }
463              
464             =head3 xrds
465              
466             Returns the xrds document as an XML::XPath for custom XPath queries.
467              
468             =cut
469              
470             sub xrds {
471             my $self = shift;
472             return $self->{xrds};
473             }
474              
475             =head3 node
476              
477             Returns the XPath node of the C<< >> tag, for custom XPath queries.
478              
479             =cut
480              
481             sub node {
482             my $self = shift;
483             return $self->{node};
484             }
485              
486             sub new {
487             my $caller = shift;
488             my ($xrds, $node) = @_;
489              
490             my $class = ref($caller) || $caller;
491              
492             my $self = {
493             xrds => $xrds,
494             node => $node,
495             };
496              
497             bless($self, $class);
498             }
499              
500             1;
501