1 # -*- test-case-name: yadis.test.test_etxrd -*-
3 ElementTree interface to an XRD document.
24 from datetime
import datetime
25 from time
import strptime
27 from openid
.oidutil
import importElementTree
28 ElementTree
= importElementTree()
30 # the different elementtree modules don't have a common exception
31 # model. We just want to be able to catch the exceptions that signify
32 # malformed XML data and wrap them, so that the other library code
33 # doesn't have to know which XML library we're using.
35 # Make the parser raise an exception so we can sniff out the type
37 ElementTree
.XML('> purposely malformed XML <')
38 except (SystemExit, MemoryError, AssertionError, ImportError):
41 XMLError
= sys
.exc_info()[0]
43 from openid
.yadis
import xri
45 class XRDSError(Exception):
46 """An error with the XRDS document."""
48 # The exception that triggered this exception
53 class XRDSFraud(XRDSError
):
54 """Raised when there's an assertion in the XRDS that it does not have
55 the authority to make.
61 """Parse the given text as an XRDS document.
63 @return: ElementTree containing an XRDS document
65 @raises XRDSError: When there is a parse error or the document does
69 element
= ElementTree
.XML(text
)
71 exc
= XRDSError('Error parsing document as XML')
75 tree
= ElementTree
.ElementTree(element
)
77 raise XRDSError('Not an XRDS document')
81 XRD_NS_2_0
= 'xri://$xrd*($v*2.0)'
82 XRDS_NS
= 'xri://$xrds'
85 return '{%s}%s' % (ns
, t
)
88 """basestring -> basestring
90 Create a tag name in the XRD 2.0 XML namespace suitable for using
93 return nsTag(XRD_NS_2_0
, t
)
96 """basestring -> basestring
98 Create a tag name in the XRDS XML namespace suitable for using
101 return nsTag(XRDS_NS
, t
)
103 # Tags that are used in Yadis documents
104 root_tag
= mkXRDSTag('XRDS')
105 service_tag
= mkXRDTag('Service')
106 xrd_tag
= mkXRDTag('XRD')
107 type_tag
= mkXRDTag('Type')
108 uri_tag
= mkXRDTag('URI')
109 expires_tag
= mkXRDTag('Expires')
112 canonicalID_tag
= mkXRDTag('CanonicalID')
114 def isXRDS(xrd_tree
):
115 """Is this document an XRDS document?"""
116 root
= xrd_tree
.getroot()
117 return root
.tag
== root_tag
119 def getYadisXRD(xrd_tree
):
120 """Return the XRD element that should contain the Yadis services"""
123 # for the side-effect of assigning the last one in the list to the
125 for xrd
in xrd_tree
.findall(xrd_tag
):
128 # There were no elements found, or else xrd would be set to the
131 raise XRDSError('No XRD present in tree')
135 def getXRDExpiration(xrd_element
, default
=None):
136 """Return the expiration date of this XRD element, or None if no
137 expiration was specified.
139 @type xrd_element: ElementTree node
141 @param default: The value to use as the expiration if no
142 expiration was specified in the XRD.
144 @rtype: datetime.datetime
146 @raises ValueError: If the xrd:Expires element is present, but its
147 contents are not formatted according to the specification.
149 expires_element
= xrd_element
.find(expires_tag
)
150 if expires_element
is None:
153 expires_string
= expires_element
.text
155 # Will raise ValueError if the string is not the expected format
156 expires_time
= strptime(expires_string
, "%Y-%m-%dT%H:%M:%SZ")
157 return datetime(*expires_time
[0:6])
159 def getCanonicalID(iname
, xrd_tree
):
160 """Return the CanonicalID from this XRDS document.
162 @param iname: the XRI being resolved.
165 @param xrd_tree: The XRDS output from the resolver.
166 @type xrd_tree: ElementTree
168 @returns: The XRI CanonicalID or None.
169 @returntype: unicode or None
171 xrd_list
= xrd_tree
.findall(xrd_tag
)
175 canonicalID
= xri
.XRI(xrd_list
[0].findall(canonicalID_tag
)[0].text
)
179 childID
= canonicalID
.lower()
181 for xrd
in xrd_list
[1:]:
182 # XXX: can't use rsplit until we require python >= 2.4.
183 parent_sought
= childID
[:childID
.rindex('!')]
184 parent
= xri
.XRI(xrd
.findtext(canonicalID_tag
))
185 if parent_sought
!= parent
.lower():
186 raise XRDSFraud("%r can not come from %s" % (childID
, parent
))
188 childID
= parent_sought
190 root
= xri
.rootAuthority(iname
)
191 if not xri
.providerIsAuthoritative(root
, childID
):
192 raise XRDSFraud("%r can not come from root %r" % (childID
, root
))
199 """Value that compares greater than any other value.
201 Should only be used as a singleton. Implemented for use as a
202 priority value for when a priority is not specified."""
203 def __cmp__(self
, other
):
211 def getPriorityStrict(element
):
212 """Get the priority of this element.
214 Raises ValueError if the value of the priority is invalid. If no
215 priority is specified, it returns a value that compares greater
216 than any other value.
218 prio_str
= element
.get('priority')
219 if prio_str
is not None:
220 prio_val
= int(prio_str
)
224 raise ValueError('Priority values must be non-negative integers')
226 # Any errors in parsing the priority fall through to here
229 def getPriority(element
):
230 """Get the priority of this element
232 Returns Max if no priority is specified or the priority value is invalid.
235 return getPriorityStrict(element
)
239 def prioSort(elements
):
240 """Sort a list of elements that have priority attributes"""
241 # Randomize the services before sorting so that equal priority
242 # elements are load-balanced.
243 random
.shuffle(elements
)
245 prio_elems
= [(getPriority(e
), e
) for e
in elements
]
247 sorted_elems
= [s
for (_
, s
) in prio_elems
]
250 def iterServices(xrd_tree
):
251 """Return an iterable over the Service elements in the Yadis XRD
253 sorted by priority"""
254 xrd
= getYadisXRD(xrd_tree
)
255 return prioSort(xrd
.findall(service_tag
))
257 def sortedURIs(service_element
):
258 """Given a Service element, return a list of the contents of all
259 URI tags in priority order."""
260 return [uri_element
.text
for uri_element
261 in prioSort(service_element
.findall(uri_tag
))]
263 def getTypeURIs(service_element
):
264 """Given a Service element, return a list of the contents of all
266 return [type_element
.text
for type_element
267 in service_element
.findall(type_tag
)]
269 def expandService(service_element
):
270 """Take a service element and expand it into an iterator of:
271 ([type_uri], uri, service_element)
273 uris
= sortedURIs(service_element
)
279 type_uris
= getTypeURIs(service_element
)
280 expanded
.append((type_uris
, uri
, service_element
))
284 def expandServices(service_elements
):
285 """Take a sorted iterator of service elements and expand it into a
287 ([type_uri], uri, service_element)
289 There may be more than one item in the resulting list for each
290 service element if there is more than one URI or type for a
291 service, but each triple will be unique.
293 If there is no URI or Type for a Service element, it will not
294 appear in the result.
297 for service_element
in service_elements
:
298 expanded
.extend(expandService(service_element
))