diff options
Diffstat (limited to 'rest_framework/parsers.py')
| -rw-r--r-- | rest_framework/parsers.py | 146 |
1 files changed, 126 insertions, 20 deletions
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 4841676c..25be2e6a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -4,15 +4,16 @@ Parsers are used to parse the content of incoming HTTP requests. They give us a generic way of being able to handle various media types on the request, such as form content or json encoded data. """ - +from __future__ import unicode_literals +from django.conf import settings +from django.core.files.uploadhandler import StopFutureHandlers from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser -from django.http.multipartparser import MultiPartParserError -from django.utils import simplejson as json -from rest_framework.compat import yaml, ETParseError +from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter +from rest_framework.compat import yaml, etree from rest_framework.exceptions import ParseError -from xml.etree import ElementTree as ET -from xml.parsers.expat import ExpatError +from rest_framework.compat import six +import json import datetime import decimal @@ -54,10 +55,14 @@ class JSONParser(BaseParser): `data` will be an object which is the parsed content of the response. `files` will always be `None`. """ + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + try: - return json.load(stream) - except ValueError, exc: - raise ParseError('JSON parse error - %s' % unicode(exc)) + data = stream.read().decode(encoding) + return json.loads(data) + except ValueError as exc: + raise ParseError('JSON parse error - %s' % six.text_type(exc)) class YAMLParser(BaseParser): @@ -74,10 +79,16 @@ class YAMLParser(BaseParser): `data` will be an object which is the parsed content of the response. `files` will always be `None`. """ + assert yaml, 'YAMLParser requires pyyaml to be installed' + + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + try: - return yaml.safe_load(stream) - except (ValueError, yaml.parser.ParserError), exc: - raise ParseError('YAML parse error - %s' % unicode(exc)) + data = stream.read().decode(encoding) + return yaml.safe_load(data) + except (ValueError, yaml.parser.ParserError) as exc: + raise ParseError('YAML parse error - %s' % six.u(exc)) class FormParser(BaseParser): @@ -94,7 +105,9 @@ class FormParser(BaseParser): `data` will be a :class:`QueryDict` containing all the form parameters. `files` will always be :const:`None`. """ - data = QueryDict(stream.read()) + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + data = QueryDict(stream.read(), encoding=encoding) return data @@ -114,15 +127,16 @@ class MultiPartParser(BaseParser): """ parser_context = parser_context or {} request = parser_context['request'] + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) meta = request.META upload_handlers = request.upload_handlers try: - parser = DjangoMultiPartParser(meta, stream, upload_handlers) + parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding) data, files = parser.parse() return DataAndFiles(data, files) - except MultiPartParserError, exc: - raise ParseError('Multipart form parse error - %s' % unicode(exc)) + except MultiPartParserError as exc: + raise ParseError('Multipart form parse error - %s' % six.u(exc)) class XMLParser(BaseParser): @@ -133,10 +147,15 @@ class XMLParser(BaseParser): media_type = 'application/xml' def parse(self, stream, media_type=None, parser_context=None): + assert etree, 'XMLParser requires defusedxml to be installed' + + parser_context = parser_context or {} + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + parser = etree.DefusedXMLParser(encoding=encoding) try: - tree = ET.parse(stream) - except (ExpatError, ETParseError, ValueError), exc: - raise ParseError('XML parse error - %s' % unicode(exc)) + tree = etree.parse(stream, parser=parser, forbid_dtd=True) + except (etree.ParseError, ValueError) as exc: + raise ParseError('XML parse error - %s' % six.u(exc)) data = self._xml_convert(tree.getroot()) return data @@ -146,7 +165,7 @@ class XMLParser(BaseParser): convert the xml `element` into the corresponding python object """ - children = element.getchildren() + children = list(element) if len(children) == 0: return self._type_convert(element.text) @@ -187,3 +206,90 @@ class XMLParser(BaseParser): pass return value + + +class FileUploadParser(BaseParser): + """ + Parser for file upload data. + """ + media_type = '*/*' + + def parse(self, stream, media_type=None, parser_context=None): + """ + Returns a DataAndFiles object. + + `.data` will be None (we expect request body to be a file content). + `.files` will be a `QueryDict` containing one 'file' element. + """ + + parser_context = parser_context or {} + request = parser_context['request'] + encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) + meta = request.META + upload_handlers = request.upload_handlers + filename = self.get_filename(stream, media_type, parser_context) + + # Note that this code is extracted from Django's handling of + # file uploads in MultiPartParser. + content_type = meta.get('HTTP_CONTENT_TYPE', + meta.get('CONTENT_TYPE', '')) + try: + content_length = int(meta.get('HTTP_CONTENT_LENGTH', + meta.get('CONTENT_LENGTH', 0))) + except (ValueError, TypeError): + content_length = None + + # See if the handler will want to take care of the parsing. + for handler in upload_handlers: + result = handler.handle_raw_input(None, + meta, + content_length, + None, + encoding) + if result is not None: + return DataAndFiles(None, {'file': result[1]}) + + # This is the standard case. + possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] + chunk_size = min([2 ** 31 - 4] + possible_sizes) + chunks = ChunkIter(stream, chunk_size) + counters = [0] * len(upload_handlers) + + for handler in upload_handlers: + try: + handler.new_file(None, filename, content_type, + content_length, encoding) + except StopFutureHandlers: + break + + for chunk in chunks: + for i, handler in enumerate(upload_handlers): + chunk_length = len(chunk) + chunk = handler.receive_data_chunk(chunk, counters[i]) + counters[i] += chunk_length + if chunk is None: + break + + for i, handler in enumerate(upload_handlers): + file_obj = handler.file_complete(counters[i]) + if file_obj: + return DataAndFiles(None, {'file': file_obj}) + raise ParseError("FileUpload parse error - " + "none of upload handlers can handle the stream") + + def get_filename(self, stream, media_type, parser_context): + """ + Detects the uploaded file name. First searches a 'filename' url kwarg. + Then tries to parse Content-Disposition header. + """ + try: + return parser_context['kwargs']['filename'] + except KeyError: + pass + + try: + meta = parser_context['request'].META + disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION']) + return disposition[1]['filename'] + except (AttributeError, KeyError): + pass |
