parsers.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. # Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
  2. # Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
  3. # Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as
  6. # published by the Free Software Foundation, either version 3 of the
  7. # License, or (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. # This code is partially taken from django-rest-framework:
  17. # Copyright (c) 2011-2014, Tom Christie
  18. """
  19. Parsers are used to parse the content of incoming HTTP requests.
  20. They give us a generic way of being able to handle various media types
  21. on the request, such as form content or json encoded data.
  22. """
  23. from django.conf import settings
  24. from django.core.files.uploadhandler import StopFutureHandlers
  25. from django.http import QueryDict
  26. from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
  27. from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
  28. from django.utils import six
  29. from taiga.base.exceptions import ParseError
  30. from taiga.base.api import renderers
  31. import json
  32. import datetime
  33. import decimal
  34. class DataAndFiles(object):
  35. def __init__(self, data, files):
  36. self.data = data
  37. self.files = files
  38. class BaseParser(object):
  39. """
  40. All parsers should extend `BaseParser`, specifying a `media_type`
  41. attribute, and overriding the `.parse()` method.
  42. """
  43. media_type = None
  44. def parse(self, stream, media_type=None, parser_context=None):
  45. """
  46. Given a stream to read from, return the parsed representation.
  47. Should return parsed data, or a `DataAndFiles` object consisting of the
  48. parsed data and files.
  49. """
  50. raise NotImplementedError(".parse() must be overridden.")
  51. class JSONParser(BaseParser):
  52. """
  53. Parses JSON-serialized data.
  54. """
  55. media_type = "application/json"
  56. renderer_class = renderers.UnicodeJSONRenderer
  57. def parse(self, stream, media_type=None, parser_context=None):
  58. """
  59. Parses the incoming bytestream as JSON and returns the resulting data.
  60. """
  61. parser_context = parser_context or {}
  62. encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
  63. try:
  64. data = stream.read().decode(encoding)
  65. return json.loads(data)
  66. except ValueError as exc:
  67. raise ParseError("JSON parse error - %s" % six.text_type(exc))
  68. class FormParser(BaseParser):
  69. """
  70. Parser for form data.
  71. """
  72. media_type = "application/x-www-form-urlencoded"
  73. def parse(self, stream, media_type=None, parser_context=None):
  74. """
  75. Parses the incoming bytestream as a URL encoded form,
  76. and returns the resulting QueryDict.
  77. """
  78. parser_context = parser_context or {}
  79. encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
  80. data = QueryDict(stream.read(), encoding=encoding)
  81. return data
  82. class MultiPartParser(BaseParser):
  83. """
  84. Parser for multipart form data, which may include file data.
  85. """
  86. media_type = "multipart/form-data"
  87. def parse(self, stream, media_type=None, parser_context=None):
  88. """
  89. Parses the incoming bytestream as a multipart encoded form,
  90. and returns a DataAndFiles object.
  91. `.data` will be a `QueryDict` containing all the form parameters.
  92. `.files` will be a `QueryDict` containing all the form files.
  93. """
  94. parser_context = parser_context or {}
  95. request = parser_context["request"]
  96. encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
  97. meta = request.META.copy()
  98. meta["CONTENT_TYPE"] = media_type
  99. upload_handlers = request.upload_handlers
  100. try:
  101. parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding)
  102. data, files = parser.parse()
  103. return DataAndFiles(data, files)
  104. except MultiPartParserError as exc:
  105. raise ParseError("Multipart form parse error - %s" % str(exc))
  106. class FileUploadParser(BaseParser):
  107. """
  108. Parser for file upload data.
  109. """
  110. media_type = "*/*"
  111. def parse(self, stream, media_type=None, parser_context=None):
  112. """
  113. Treats the incoming bytestream as a raw file upload and returns
  114. a `DateAndFiles` object.
  115. `.data` will be None (we expect request body to be a file content).
  116. `.files` will be a `QueryDict` containing one "file" element.
  117. """
  118. parser_context = parser_context or {}
  119. request = parser_context["request"]
  120. encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET)
  121. meta = request.META
  122. upload_handlers = request.upload_handlers
  123. filename = self.get_filename(stream, media_type, parser_context)
  124. # Note that this code is extracted from Django's handling of
  125. # file uploads in MultiPartParser.
  126. content_type = meta.get("HTTP_CONTENT_TYPE",
  127. meta.get("CONTENT_TYPE", ""))
  128. try:
  129. content_length = int(meta.get("HTTP_CONTENT_LENGTH",
  130. meta.get("CONTENT_LENGTH", 0)))
  131. except (ValueError, TypeError):
  132. content_length = None
  133. # See if the handler will want to take care of the parsing.
  134. for handler in upload_handlers:
  135. result = handler.handle_raw_input(None,
  136. meta,
  137. content_length,
  138. None,
  139. encoding)
  140. if result is not None:
  141. return DataAndFiles(None, {"file": result[1]})
  142. # This is the standard case.
  143. possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
  144. chunk_size = min([2 ** 31 - 4] + possible_sizes)
  145. chunks = ChunkIter(stream, chunk_size)
  146. counters = [0] * len(upload_handlers)
  147. for handler in upload_handlers:
  148. try:
  149. handler.new_file(None, filename, content_type,
  150. content_length, encoding)
  151. except StopFutureHandlers:
  152. break
  153. for chunk in chunks:
  154. for i, handler in enumerate(upload_handlers):
  155. chunk_length = len(chunk)
  156. chunk = handler.receive_data_chunk(chunk, counters[i])
  157. counters[i] += chunk_length
  158. if chunk is None:
  159. break
  160. for i, handler in enumerate(upload_handlers):
  161. file_obj = handler.file_complete(counters[i])
  162. if file_obj:
  163. return DataAndFiles(None, {"file": file_obj})
  164. raise ParseError("FileUpload parse error - "
  165. "none of upload handlers can handle the stream")
  166. def get_filename(self, stream, media_type, parser_context):
  167. """
  168. Detects the uploaded file name. First searches a "filename" url kwarg.
  169. Then tries to parse Content-Disposition header.
  170. """
  171. try:
  172. return parser_context["kwargs"]["filename"]
  173. except KeyError:
  174. pass
  175. try:
  176. meta = parser_context["request"].META
  177. disposition = parse_header(meta["HTTP_CONTENT_DISPOSITION"])
  178. return disposition[1]["filename"]
  179. except (AttributeError, KeyError):
  180. pass