Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

292 rindas
8.5 KiB

  1. # coding: utf-8
  2. """
  3. Functions to convert unicode IRIs into ASCII byte string URIs and back. Exports
  4. the following items:
  5. - iri_to_uri()
  6. - uri_to_iri()
  7. """
  8. from __future__ import unicode_literals, division, absolute_import, print_function
  9. from encodings import idna # noqa
  10. import codecs
  11. import re
  12. import sys
  13. from ._errors import unwrap
  14. from ._types import byte_cls, str_cls, type_name, bytes_to_list, int_types
  15. if sys.version_info < (3,):
  16. from urlparse import urlsplit, urlunsplit
  17. from urllib import (
  18. quote as urlquote,
  19. unquote as unquote_to_bytes,
  20. )
  21. else:
  22. from urllib.parse import (
  23. quote as urlquote,
  24. unquote_to_bytes,
  25. urlsplit,
  26. urlunsplit,
  27. )
  28. def iri_to_uri(value, normalize=False):
  29. """
  30. Encodes a unicode IRI into an ASCII byte string URI
  31. :param value:
  32. A unicode string of an IRI
  33. :param normalize:
  34. A bool that controls URI normalization
  35. :return:
  36. A byte string of the ASCII-encoded URI
  37. """
  38. if not isinstance(value, str_cls):
  39. raise TypeError(unwrap(
  40. '''
  41. value must be a unicode string, not %s
  42. ''',
  43. type_name(value)
  44. ))
  45. scheme = None
  46. # Python 2.6 doesn't split properly is the URL doesn't start with http:// or https://
  47. if sys.version_info < (2, 7) and not value.startswith('http://') and not value.startswith('https://'):
  48. real_prefix = None
  49. prefix_match = re.match('^[^:]*://', value)
  50. if prefix_match:
  51. real_prefix = prefix_match.group(0)
  52. value = 'http://' + value[len(real_prefix):]
  53. parsed = urlsplit(value)
  54. if real_prefix:
  55. value = real_prefix + value[7:]
  56. scheme = _urlquote(real_prefix[:-3])
  57. else:
  58. parsed = urlsplit(value)
  59. if scheme is None:
  60. scheme = _urlquote(parsed.scheme)
  61. hostname = parsed.hostname
  62. if hostname is not None:
  63. hostname = hostname.encode('idna')
  64. # RFC 3986 allows userinfo to contain sub-delims
  65. username = _urlquote(parsed.username, safe='!$&\'()*+,;=')
  66. password = _urlquote(parsed.password, safe='!$&\'()*+,;=')
  67. port = parsed.port
  68. if port is not None:
  69. port = str_cls(port).encode('ascii')
  70. netloc = b''
  71. if username is not None:
  72. netloc += username
  73. if password:
  74. netloc += b':' + password
  75. netloc += b'@'
  76. if hostname is not None:
  77. netloc += hostname
  78. if port is not None:
  79. default_http = scheme == b'http' and port == b'80'
  80. default_https = scheme == b'https' and port == b'443'
  81. if not normalize or (not default_http and not default_https):
  82. netloc += b':' + port
  83. # RFC 3986 allows a path to contain sub-delims, plus "@" and ":"
  84. path = _urlquote(parsed.path, safe='/!$&\'()*+,;=@:')
  85. # RFC 3986 allows the query to contain sub-delims, plus "@", ":" , "/" and "?"
  86. query = _urlquote(parsed.query, safe='/?!$&\'()*+,;=@:')
  87. # RFC 3986 allows the fragment to contain sub-delims, plus "@", ":" , "/" and "?"
  88. fragment = _urlquote(parsed.fragment, safe='/?!$&\'()*+,;=@:')
  89. if normalize and query is None and fragment is None and path == b'/':
  90. path = None
  91. # Python 2.7 compat
  92. if path is None:
  93. path = ''
  94. output = urlunsplit((scheme, netloc, path, query, fragment))
  95. if isinstance(output, str_cls):
  96. output = output.encode('latin1')
  97. return output
  98. def uri_to_iri(value):
  99. """
  100. Converts an ASCII URI byte string into a unicode IRI
  101. :param value:
  102. An ASCII-encoded byte string of the URI
  103. :return:
  104. A unicode string of the IRI
  105. """
  106. if not isinstance(value, byte_cls):
  107. raise TypeError(unwrap(
  108. '''
  109. value must be a byte string, not %s
  110. ''',
  111. type_name(value)
  112. ))
  113. parsed = urlsplit(value)
  114. scheme = parsed.scheme
  115. if scheme is not None:
  116. scheme = scheme.decode('ascii')
  117. username = _urlunquote(parsed.username, remap=[':', '@'])
  118. password = _urlunquote(parsed.password, remap=[':', '@'])
  119. hostname = parsed.hostname
  120. if hostname:
  121. hostname = hostname.decode('idna')
  122. port = parsed.port
  123. if port and not isinstance(port, int_types):
  124. port = port.decode('ascii')
  125. netloc = ''
  126. if username is not None:
  127. netloc += username
  128. if password:
  129. netloc += ':' + password
  130. netloc += '@'
  131. if hostname is not None:
  132. netloc += hostname
  133. if port is not None:
  134. netloc += ':' + str_cls(port)
  135. path = _urlunquote(parsed.path, remap=['/'], preserve=True)
  136. query = _urlunquote(parsed.query, remap=['&', '='], preserve=True)
  137. fragment = _urlunquote(parsed.fragment)
  138. return urlunsplit((scheme, netloc, path, query, fragment))
  139. def _iri_utf8_errors_handler(exc):
  140. """
  141. Error handler for decoding UTF-8 parts of a URI into an IRI. Leaves byte
  142. sequences encoded in %XX format, but as part of a unicode string.
  143. :param exc:
  144. The UnicodeDecodeError exception
  145. :return:
  146. A 2-element tuple of (replacement unicode string, integer index to
  147. resume at)
  148. """
  149. bytes_as_ints = bytes_to_list(exc.object[exc.start:exc.end])
  150. replacements = ['%%%02x' % num for num in bytes_as_ints]
  151. return (''.join(replacements), exc.end)
  152. codecs.register_error('iriutf8', _iri_utf8_errors_handler)
  153. def _urlquote(string, safe=''):
  154. """
  155. Quotes a unicode string for use in a URL
  156. :param string:
  157. A unicode string
  158. :param safe:
  159. A unicode string of character to not encode
  160. :return:
  161. None (if string is None) or an ASCII byte string of the quoted string
  162. """
  163. if string is None or string == '':
  164. return None
  165. # Anything already hex quoted is pulled out of the URL and unquoted if
  166. # possible
  167. escapes = []
  168. if re.search('%[0-9a-fA-F]{2}', string):
  169. # Try to unquote any percent values, restoring them if they are not
  170. # valid UTF-8. Also, requote any safe chars since encoded versions of
  171. # those are functionally different than the unquoted ones.
  172. def _try_unescape(match):
  173. byte_string = unquote_to_bytes(match.group(0))
  174. unicode_string = byte_string.decode('utf-8', 'iriutf8')
  175. for safe_char in list(safe):
  176. unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char))
  177. return unicode_string
  178. string = re.sub('(?:%[0-9a-fA-F]{2})+', _try_unescape, string)
  179. # Once we have the minimal set of hex quoted values, removed them from
  180. # the string so that they are not double quoted
  181. def _extract_escape(match):
  182. escapes.append(match.group(0).encode('ascii'))
  183. return '\x00'
  184. string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string)
  185. output = urlquote(string.encode('utf-8'), safe=safe.encode('utf-8'))
  186. if not isinstance(output, byte_cls):
  187. output = output.encode('ascii')
  188. # Restore the existing quoted values that we extracted
  189. if len(escapes) > 0:
  190. def _return_escape(_):
  191. return escapes.pop(0)
  192. output = re.sub(b'%00', _return_escape, output)
  193. return output
  194. def _urlunquote(byte_string, remap=None, preserve=None):
  195. """
  196. Unquotes a URI portion from a byte string into unicode using UTF-8
  197. :param byte_string:
  198. A byte string of the data to unquote
  199. :param remap:
  200. A list of characters (as unicode) that should be re-mapped to a
  201. %XX encoding. This is used when characters are not valid in part of a
  202. URL.
  203. :param preserve:
  204. A bool - indicates that the chars to be remapped if they occur in
  205. non-hex form, should be preserved. E.g. / for URL path.
  206. :return:
  207. A unicode string
  208. """
  209. if byte_string is None:
  210. return byte_string
  211. if byte_string == b'':
  212. return ''
  213. if preserve:
  214. replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F']
  215. preserve_unmap = {}
  216. for char in remap:
  217. replacement = replacements.pop(0)
  218. preserve_unmap[replacement] = char
  219. byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii'))
  220. byte_string = unquote_to_bytes(byte_string)
  221. if remap:
  222. for char in remap:
  223. byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii'))
  224. output = byte_string.decode('utf-8', 'iriutf8')
  225. if preserve:
  226. for replacement, original in preserve_unmap.items():
  227. output = output.replace(replacement, original)
  228. return output