1# -*- test-case-name: openid.test.test_xri -*- 2"""Utility functions for handling XRIs. 3 4@see: XRI Syntax v2.0 at the U{OASIS XRI Technical Committee<http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=xri>} 5""" 6 7import re 8from functools import reduce 9 10from openid import codecutil # registers 'oid_percent_escape' encoding handler 11 12XRI_AUTHORITIES = ['!', '=', '@', '+', '$', '('] 13 14 15def identifierScheme(identifier): 16 """Determine if this identifier is an XRI or URI. 17 18 @returns: C{"XRI"} or C{"URI"} 19 """ 20 if identifier.startswith('xri://') or (identifier and 21 identifier[0] in XRI_AUTHORITIES): 22 return "XRI" 23 else: 24 return "URI" 25 26 27def toIRINormal(xri): 28 """Transform an XRI to IRI-normal form.""" 29 if not xri.startswith('xri://'): 30 xri = 'xri://' + xri 31 return escapeForIRI(xri) 32 33 34_xref_re = re.compile(r'\((.*?)\)') 35 36 37def _escape_xref(xref_match): 38 """Escape things that need to be escaped if they're in a cross-reference. 39 """ 40 xref = xref_match.group() 41 xref = xref.replace('/', '%2F') 42 xref = xref.replace('?', '%3F') 43 xref = xref.replace('#', '%23') 44 return xref 45 46 47def escapeForIRI(xri): 48 """Escape things that need to be escaped when transforming to an IRI.""" 49 xri = xri.replace('%', '%25') 50 xri = _xref_re.sub(_escape_xref, xri) 51 return xri 52 53 54def toURINormal(xri): 55 """Transform an XRI to URI normal form.""" 56 return iriToURI(toIRINormal(xri)) 57 58 59def iriToURI(iri): 60 """Transform an IRI to a URI by escaping unicode.""" 61 # According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" 62 if isinstance(iri, bytes): 63 iri = str(iri, encoding="utf-8") 64 return iri.encode('ascii', errors='oid_percent_escape').decode() 65 66 67def providerIsAuthoritative(providerID, canonicalID): 68 """Is this provider ID authoritative for this XRI? 69 70 @returntype: bool 71 """ 72 # XXX: can't use rsplit until we require python >= 2.4. 73 lastbang = canonicalID.rindex('!') 74 parent = canonicalID[:lastbang] 75 return parent == providerID 76 77 78def rootAuthority(xri): 79 """Return the root authority for an XRI. 80 81 Example:: 82 83 rootAuthority("xri://@example") == "xri://@" 84 85 @type xri: unicode 86 @returntype: unicode 87 """ 88 if xri.startswith('xri://'): 89 xri = xri[6:] 90 authority = xri.split('/', 1)[0] 91 if authority[0] == '(': 92 # Cross-reference. 93 # XXX: This is incorrect if someone nests cross-references so there 94 # is another close-paren in there. Hopefully nobody does that 95 # before we have a real xriparse function. Hopefully nobody does 96 # that *ever*. 97 root = authority[:authority.index(')') + 1] 98 elif authority[0] in XRI_AUTHORITIES: 99 # Other XRI reference. 100 root = authority[0] 101 else: 102 # IRI reference. XXX: Can IRI authorities have segments? 103 segments = authority.split('!') 104 segments = reduce(list.__add__, [s.split('*') for s in segments]) 105 root = segments[0] 106 107 return XRI(root) 108 109 110def XRI(xri): 111 """An XRI object allowing comparison of XRI. 112 113 Ideally, this would do full normalization and provide comparsion 114 operators as per XRI Syntax. Right now, it just does a bit of 115 canonicalization by ensuring the xri scheme is present. 116 117 @param xri: an xri string 118 @type xri: unicode 119 """ 120 if not xri.startswith('xri://'): 121 xri = 'xri://' + xri 122 return xri 123