Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions django/conf/global_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ def gettext_noop(s):
"django.core.files.uploadhandler.TemporaryFileUploadHandler",
]

PARSER_CLASSES = ["django.http.parsers.JSONParser"]

# Maximum size, in bytes, of a request before it will be streamed to the
# file system instead of into memory.
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
Expand Down
11 changes: 6 additions & 5 deletions django/core/handlers/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,21 @@ def GET(self):
def _get_scheme(self):
return self.scope.get("scheme") or super()._get_scheme()

def _get_post(self):
@property
def data(self):
if not hasattr(self, "_post"):
self._load_post_and_files()
self._load_post_and_files() # TODO: rename this method
return self._post

def _set_post(self, post):
self._post = post
@data.setter
def data(self, value):
self._post = value

def _get_files(self):
if not hasattr(self, "_files"):
self._load_post_and_files()
return self._files

POST = property(_get_post, _set_post)
FILES = property(_get_files)

@cached_property
Expand Down
10 changes: 5 additions & 5 deletions django/core/handlers/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ def GET(self):
raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "")
return QueryDict(raw_query_string, encoding=self._encoding)

def _get_post(self):
@property
def data(self):
if not hasattr(self, "_post"):
self._load_post_and_files()
return self._post

def _set_post(self, post):
self._post = post
@data.setter
def data(self, value):
self._post = value

@cached_property
def COOKIES(self):
Expand All @@ -107,8 +109,6 @@ def FILES(self):
self._load_post_and_files()
return self._files

POST = property(_get_post, _set_post)


class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
Expand Down
20 changes: 17 additions & 3 deletions django/http/multipartparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import binascii
import collections
import html
import json

from django.conf import settings
from django.core.exceptions import (
Expand Down Expand Up @@ -211,6 +212,10 @@ def _parse(self):
except (KeyError, IndexError, AttributeError):
continue

part_content_type = meta_data.get("content-type")
if part_content_type is not None:
part_content_type = part_content_type[0].strip()

transfer_encoding = meta_data.get("content-transfer-encoding")
if transfer_encoding is not None:
transfer_encoding = transfer_encoding[0].strip()
Expand Down Expand Up @@ -247,9 +252,18 @@ def _parse(self):
"settings.DATA_UPLOAD_MAX_MEMORY_SIZE."
)

self._post.appendlist(
field_name, force_str(data, encoding, errors="replace")
)
if part_content_type == "application/json":
try:
self._post.appendlist(
field_name,
json.loads(force_str(data, encoding, errors="replace")),
)
except Exception:
raise MultiPartParserError("Faulty json data")
else:
self._post.appendlist(
field_name, force_str(data, encoding, errors="replace")
)
elif item_type == FILE:
# Avoid storing more than DATA_UPLOAD_MAX_NUMBER_FILES.
num_files += 1
Expand Down
39 changes: 39 additions & 0 deletions django/http/parsers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json

from django.utils.module_loading import import_string

# from django.http import QueryDict


class ParserException(Exception):
"""
The selected parser cannot parse the contents of the request.
"""


class BaseParser:
media_type = None

def can_accept(self, media_type=None):
if media_type == self.media_type:
return True

def parse(self, stream):
pass


class JSONParser(BaseParser):
media_type = "application/json"

def parse(self, stream):
try:
return json.loads(stream)
except ValueError as e:
raise ParserException(f"JSON parse error - {e}")


def load_parser(path, *args, **kwargs):
"""
Given a path to a parser, return an instance of that parser.
"""
return import_string(path)(*args, **kwargs)
65 changes: 58 additions & 7 deletions django/http/request.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import codecs
import copy
import json
from io import BytesIO
from itertools import chain
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
Expand All @@ -18,6 +19,7 @@
MultiPartParserError,
TooManyFilesSent,
)
from django.http.parsers import ParserException, load_parser
from django.utils.datastructures import (
CaseInsensitiveMapping,
ImmutableList,
Expand Down Expand Up @@ -54,14 +56,15 @@ class HttpRequest:
# The encoding used in GET/POST dicts. None means use default setting.
_encoding = None
_upload_handlers = []
_parser_classes = []

def __init__(self):
# WARNING: The `WSGIRequest` subclass doesn't call `super`.
# Any variable assignment made here should also happen in
# `WSGIRequest.__init__()`.

self.GET = QueryDict(mutable=True)
self.POST = QueryDict(mutable=True)
self.data = QueryDict(mutable=True)
self.COOKIES = {}
self.META = {}
self.FILES = MultiValueDict()
Expand Down Expand Up @@ -318,6 +321,30 @@ def parse_file_upload(self, META, post_data):
parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
return parser.parse()

def initialize_parsers(self):
self._parser_classes = [
load_parser(parser) for parser in settings.PARSER_CLASSES
]

@property
def parser_classes(self):
if not self._parser_classes:
self.initialize_parsers()
return self._parser_classes

@parser_classes.setter
def parser_classes(self, parser_classes):
if hasattr(self, "_post"):
raise AttributeError("cannot change parsers after request has been parsed.")
self._parser_classes = parser_classes

def _select_parser(self):
for parser in self.parser_classes:
if parser.can_accept(self.content_type):
return parser

return None

@property
def body(self):
if not hasattr(self, "_body"):
Expand Down Expand Up @@ -351,16 +378,12 @@ def _mark_post_parse_error(self):

def _load_post_and_files(self):
"""Populate self._post and self._files if the content-type is a form type"""
if self.method != "POST":
self._post, self._files = (
QueryDict(encoding=self._encoding),
MultiValueDict(),
)
return
if self._read_started and not hasattr(self, "_body"):
self._mark_post_parse_error()
return

parser = self._select_parser()

if self.content_type == "multipart/form-data":
if hasattr(self, "_body"):
# Use already read data
Expand All @@ -381,6 +404,13 @@ def _load_post_and_files(self):
QueryDict(self.body, encoding=self._encoding),
MultiValueDict(),
)
elif parser:
try:
self._post = parser.parse(self.body)
self._files = MultiValueDict()
except Exception:
raise ParserException()

else:
self._post, self._files = (
QueryDict(encoding=self._encoding),
Expand Down Expand Up @@ -420,6 +450,27 @@ def __iter__(self):
def readlines(self):
return list(self)

@property
def POST(self):
if self.method != "POST":
return QueryDict(encoding=self._encoding)
if not hasattr(self, "_post"):
self._load_post_and_files()
if self.content_type == "application/x-www-form-urlencoded":
return self.data
elif self.content_type == "multipart/form-data":
self._post_copy = self._post.copy()
for key, value in self._post_copy.items():
if isinstance(value, dict):
self._post_copy[key] = json.dumps(value)
self._post_copy._mutable = False
return self._post_copy
return QueryDict(encoding=self._encoding)

@POST.setter
def POST(self, post):
self.data = post


class HttpHeaders(CaseInsensitiveMapping):
HTTP_PREFIX = "HTTP_"
Expand Down
4 changes: 2 additions & 2 deletions tests/csrf_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,10 @@ class PostErrorRequest(TestingHttpRequest):
def _get_post(self):
if self.post_error is not None:
raise self.post_error
return self._post
return self.data

def _set_post(self, post):
self._post = post
self.data = post

POST = property(_get_post, _set_post)

Expand Down
Loading