xri.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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. XRI_AUTHORITIES = ['!', '=', '@', '+', '$', '(']
  7. try:
  8. unichr(0x10000)
  9. except ValueError:
  10. # narrow python build
  11. UCSCHAR = [
  12. (0xA0, 0xD7FF),
  13. (0xF900, 0xFDCF),
  14. (0xFDF0, 0xFFEF),
  15. ]
  16. IPRIVATE = [
  17. (0xE000, 0xF8FF),
  18. ]
  19. else:
  20. UCSCHAR = [
  21. (0xA0, 0xD7FF),
  22. (0xF900, 0xFDCF),
  23. (0xFDF0, 0xFFEF),
  24. (0x10000, 0x1FFFD),
  25. (0x20000, 0x2FFFD),
  26. (0x30000, 0x3FFFD),
  27. (0x40000, 0x4FFFD),
  28. (0x50000, 0x5FFFD),
  29. (0x60000, 0x6FFFD),
  30. (0x70000, 0x7FFFD),
  31. (0x80000, 0x8FFFD),
  32. (0x90000, 0x9FFFD),
  33. (0xA0000, 0xAFFFD),
  34. (0xB0000, 0xBFFFD),
  35. (0xC0000, 0xCFFFD),
  36. (0xD0000, 0xDFFFD),
  37. (0xE1000, 0xEFFFD),
  38. ]
  39. IPRIVATE = [
  40. (0xE000, 0xF8FF),
  41. (0xF0000, 0xFFFFD),
  42. (0x100000, 0x10FFFD),
  43. ]
  44. _escapeme_re = re.compile('[%s]' % (''.join(
  45. map(lambda (m, n): u'%s-%s' % (unichr(m), unichr(n)),
  46. UCSCHAR + IPRIVATE)),))
  47. def identifierScheme(identifier):
  48. """Determine if this identifier is an XRI or URI.
  49. @returns: C{"XRI"} or C{"URI"}
  50. """
  51. if identifier.startswith('xri://') or (
  52. identifier and identifier[0] in XRI_AUTHORITIES):
  53. return "XRI"
  54. else:
  55. return "URI"
  56. def toIRINormal(xri):
  57. """Transform an XRI to IRI-normal form."""
  58. if not xri.startswith('xri://'):
  59. xri = 'xri://' + xri
  60. return escapeForIRI(xri)
  61. _xref_re = re.compile('\((.*?)\)')
  62. def _escape_xref(xref_match):
  63. """Escape things that need to be escaped if they're in a cross-reference.
  64. """
  65. xref = xref_match.group()
  66. xref = xref.replace('/', '%2F')
  67. xref = xref.replace('?', '%3F')
  68. xref = xref.replace('#', '%23')
  69. return xref
  70. def escapeForIRI(xri):
  71. """Escape things that need to be escaped when transforming to an IRI."""
  72. xri = xri.replace('%', '%25')
  73. xri = _xref_re.sub(_escape_xref, xri)
  74. return xri
  75. def toURINormal(xri):
  76. """Transform an XRI to URI normal form."""
  77. return iriToURI(toIRINormal(xri))
  78. def _percentEscapeUnicode(char_match):
  79. c = char_match.group()
  80. return ''.join(['%%%X' % (ord(octet),) for octet in c.encode('utf-8')])
  81. def iriToURI(iri):
  82. """Transform an IRI to a URI by escaping unicode."""
  83. # According to RFC 3987, section 3.1, "Mapping of IRIs to URIs"
  84. return _escapeme_re.sub(_percentEscapeUnicode, iri)
  85. def providerIsAuthoritative(providerID, canonicalID):
  86. """Is this provider ID authoritative for this XRI?
  87. @returntype: bool
  88. """
  89. # XXX: can't use rsplit until we require python >= 2.4.
  90. lastbang = canonicalID.rindex('!')
  91. parent = canonicalID[:lastbang]
  92. return parent == providerID
  93. def rootAuthority(xri):
  94. """Return the root authority for an XRI.
  95. Example::
  96. rootAuthority("xri://@example") == "xri://@"
  97. @type xri: unicode
  98. @returntype: unicode
  99. """
  100. if xri.startswith('xri://'):
  101. xri = xri[6:]
  102. authority = xri.split('/', 1)[0]
  103. if authority[0] == '(':
  104. # Cross-reference.
  105. # XXX: This is incorrect if someone nests cross-references so there
  106. # is another close-paren in there. Hopefully nobody does that
  107. # before we have a real xriparse function. Hopefully nobody does
  108. # that *ever*.
  109. root = authority[:authority.index(')') + 1]
  110. elif authority[0] in XRI_AUTHORITIES:
  111. # Other XRI reference.
  112. root = authority[0]
  113. else:
  114. # IRI reference. XXX: Can IRI authorities have segments?
  115. segments = authority.split('!')
  116. segments = reduce(list.__add__,
  117. map(lambda s: s.split('*'), segments))
  118. root = segments[0]
  119. return XRI(root)
  120. def XRI(xri):
  121. """An XRI object allowing comparison of XRI.
  122. Ideally, this would do full normalization and provide comparsion
  123. operators as per XRI Syntax. Right now, it just does a bit of
  124. canonicalization by ensuring the xri scheme is present.
  125. @param xri: an xri string
  126. @type xri: unicode
  127. """
  128. if not xri.startswith('xri://'):
  129. xri = 'xri://' + xri
  130. return xri