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