123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- """
- 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.
- """
- import codecs
- from urllib import parse
- from django.conf import settings
- from django.core.files.uploadhandler import StopFutureHandlers
- from django.http import QueryDict
- from django.http.multipartparser import ChunkIter
- from django.http.multipartparser import \
- MultiPartParser as DjangoMultiPartParser
- from django.http.multipartparser import MultiPartParserError, parse_header
- from django.utils.encoding import force_str
- from rest_framework import renderers
- from rest_framework.exceptions import ParseError
- from rest_framework.settings import api_settings
- from rest_framework.utils import json
- class DataAndFiles:
- def __init__(self, data, files):
- self.data = data
- self.files = files
- class BaseParser:
- """
- All parsers should extend `BaseParser`, specifying a `media_type`
- attribute, and overriding the `.parse()` method.
- """
- media_type = None
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Given a stream to read from, return the parsed representation.
- Should return parsed data, or a `DataAndFiles` object consisting of the
- parsed data and files.
- """
- raise NotImplementedError(".parse() must be overridden.")
- class JSONParser(BaseParser):
- """
- Parses JSON-serialized data.
- """
- media_type = 'application/json'
- renderer_class = renderers.JSONRenderer
- strict = api_settings.STRICT_JSON
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as JSON and returns the resulting data.
- """
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- try:
- decoded_stream = codecs.getreader(encoding)(stream)
- parse_constant = json.strict_constant if self.strict else None
- return json.load(decoded_stream, parse_constant=parse_constant)
- except ValueError as exc:
- raise ParseError('JSON parse error - %s' % str(exc))
- class FormParser(BaseParser):
- """
- Parser for form data.
- """
- media_type = 'application/x-www-form-urlencoded'
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as a URL encoded form,
- and returns the resulting QueryDict.
- """
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- return QueryDict(stream.read(), encoding=encoding)
- class MultiPartParser(BaseParser):
- """
- Parser for multipart form data, which may include file data.
- """
- media_type = 'multipart/form-data'
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as a multipart encoded form,
- and returns a DataAndFiles object.
- `.data` will be a `QueryDict` containing all the form parameters.
- `.files` will be a `QueryDict` containing all the form files.
- """
- parser_context = parser_context or {}
- request = parser_context['request']
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- meta = request.META.copy()
- meta['CONTENT_TYPE'] = media_type
- upload_handlers = request.upload_handlers
- try:
- parser = DjangoMultiPartParser(meta, stream, upload_handlers, encoding)
- data, files = parser.parse()
- return DataAndFiles(data, files)
- except MultiPartParserError as exc:
- raise ParseError('Multipart form parse error - %s' % str(exc))
- class FileUploadParser(BaseParser):
- """
- Parser for file upload data.
- """
- media_type = '*/*'
- errors = {
- 'unhandled': 'FileUpload parse error - none of upload handlers can handle the stream',
- 'no_filename': 'Missing filename. Request should include a Content-Disposition header with a filename parameter.',
- }
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Treats the incoming bytestream as a raw file upload and 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)
- if not filename:
- raise ParseError(self.errors['no_filename'])
- # 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(stream,
- meta,
- content_length,
- None,
- encoding)
- if result is not None:
- return DataAndFiles({}, {'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 index, handler in enumerate(upload_handlers):
- try:
- handler.new_file(None, filename, content_type,
- content_length, encoding)
- except StopFutureHandlers:
- upload_handlers = upload_handlers[:index + 1]
- break
- for chunk in chunks:
- for index, handler in enumerate(upload_handlers):
- chunk_length = len(chunk)
- chunk = handler.receive_data_chunk(chunk, counters[index])
- counters[index] += chunk_length
- if chunk is None:
- break
- for index, handler in enumerate(upload_handlers):
- file_obj = handler.file_complete(counters[index])
- if file_obj is not None:
- return DataAndFiles({}, {'file': file_obj})
- raise ParseError(self.errors['unhandled'])
- 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'].encode())
- filename_parm = disposition[1]
- if 'filename*' in filename_parm:
- return self.get_encoded_filename(filename_parm)
- return force_str(filename_parm['filename'])
- except (AttributeError, KeyError, ValueError):
- pass
- def get_encoded_filename(self, filename_parm):
- """
- Handle encoded filenames per RFC6266. See also:
- https://tools.ietf.org/html/rfc2231#section-4
- """
- encoded_filename = force_str(filename_parm['filename*'])
- try:
- charset, lang, filename = encoded_filename.split('\'', 2)
- filename = parse.unquote(filename)
- except (ValueError, LookupError):
- filename = force_str(filename_parm['filename'])
- return filename
|