xri.py 3.4 KB

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