Skip to content

Commit 5bc5145

Browse files
Read json from request BODYFILE instead of BODY.
This fixes a regression due to a low Zope form memory limit of 1MB used since Plone 6.0.7. See `CMFPlone issue 3848 <https://github.com/plone/Products.CMFPlone/issues/3848>`_ and `Zope PR 1142 <https://github.com/zopefoundation/Zope/pull/1142>`_.
1 parent 3b4c810 commit 5bc5145

File tree

11 files changed

+71
-32
lines changed

11 files changed

+71
-32
lines changed

news/3848.bugfix

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Read json from request ``BODYFILE`` instead of ``BODY``.
2+
This fixes a regression due to a low Zope form memory limit of 1MB used since Plone 6.0.7.
3+
See `CMFPlone issue 3848 <https://github.com/plone/Products.CMFPlone/issues/3848>`_ and `Zope PR 1142 <https://github.com/zopefoundation/Zope/pull/1142>`_.
4+
@maurits and @davisagli

src/plone/restapi/deserializer/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
def json_body(request):
77
try:
8-
data = json.loads(request.get("BODY") or "{}")
8+
bodyfile = request.get("BODYFILE")
9+
data = {} if bodyfile is None else json.load(bodyfile)
910
except ValueError:
1011
raise DeserializationError("No JSON object could be decoded")
1112
if not isinstance(data, dict):

src/plone/restapi/services/querystringsearch/get.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from io import BytesIO
12
from pkg_resources import get_distribution
23
from pkg_resources import parse_version
34
from plone.restapi.bbb import IPloneSiteRoot
@@ -101,10 +102,13 @@ class QuerystringSearchGet(Service):
101102

102103
def reply(self):
103104
# We need to copy the JSON query parameters from the querystring
104-
# into the request body, because that's where other code expects to find them
105-
self.request["BODY"] = parse.unquote(
106-
self.request.form.get("query", "{}")
107-
).encode(self.request.charset)
105+
# into the request BODY and BODYFILE, because that's where other code
106+
# expects to find them.
107+
body = parse.unquote(self.request.form.get("query", "{}")).encode(
108+
self.request.charset
109+
)
110+
self.request["BODY"] = body
111+
self.request["BODYFILE"] = BytesIO(body)
108112

109113
# unset the get parameters
110114
self.request.form = {}

src/plone/restapi/services/registry/update.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from plone.registry import field
22
from plone.registry.interfaces import IRegistry
3+
from plone.restapi.deserializer import json_body
34
from plone.restapi.services import Service
45
from zope.component import getUtility
56
from zope.interface import alsoProvides
67
from zope.schema.interfaces import WrongType
78

8-
import json
99
import plone.protect.interfaces
1010

1111

1212
class RegistryUpdate(Service):
1313
def reply(self):
14-
records_to_update = json.loads(self.request.get("BODY", "{}"))
14+
records_to_update = json_body(self.request)
1515
registry = getUtility(IRegistry)
1616

1717
# Disable CSRF protection

src/plone/restapi/services/users/update.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from OFS.Image import Image
55
from plone.restapi import _
66
from plone.restapi.bbb import ISecuritySchema
7+
from plone.restapi.deserializer import json_body
78
from plone.restapi.services import Service
89
from Products.CMFCore.permissions import SetOwnPassword
910
from Products.CMFCore.utils import getToolByName
@@ -18,7 +19,6 @@
1819
from zope.publisher.interfaces import IPublishTraverse
1920

2021
import codecs
21-
import json
2222
import plone
2323

2424

@@ -57,7 +57,7 @@ def translate(self, msgid):
5757
)
5858

5959
def reply(self):
60-
user_settings_to_update = json.loads(self.request.get("BODY", "{}"))
60+
user_settings_to_update = json_body(self.request)
6161
user = self._get_user(self._get_user_id)
6262

6363
# Disable CSRF protection

src/plone/restapi/testing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pylint: disable=E1002
22
# E1002: Use of super on an old style class
3+
from io import BytesIO
34
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
45
from plone.app.i18n.locales.interfaces import IContentLanguages
56
from plone.app.i18n.locales.interfaces import IMetadataLanguages
@@ -355,3 +356,10 @@ def normalize_html(value):
355356
line = " " + line
356357
lines.append(line)
357358
return "".join(lines).strip()
359+
360+
361+
def set_request_body(request, body):
362+
if isinstance(body, str):
363+
body = body.encode("utf-8")
364+
request["BODY"] = body
365+
request["BODYFILE"] = BytesIO(body)

src/plone/restapi/tests/test_auth.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from plone.app.testing import SITE_OWNER_PASSWORD
33
from plone.app.testing import TEST_USER_PASSWORD
44
from plone.restapi.permissions import UseRESTAPI
5+
from plone.restapi.testing import set_request_body
56
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
67
from unittest import TestCase
78
from zExceptions import Unauthorized
@@ -40,16 +41,19 @@ def test_login_without_credentials_fails(self):
4041
self.assertNotIn("token", res)
4142

4243
def test_login_with_invalid_credentials_fails(self):
43-
self.request["BODY"] = '{"login": "admin", "password": "admin"}'
44+
set_request_body(self.request, '{"login": "admin", "password": "admin"}')
4445
service = self.traverse()
4546
res = service.reply()
4647
self.assertIn("error", res)
4748
self.assertNotIn("token", res)
4849

4950
def test_successful_login_returns_token(self):
50-
self.request["BODY"] = '{{"login": "{}", "password": "{}"}}'.format(
51-
SITE_OWNER_NAME,
52-
SITE_OWNER_PASSWORD,
51+
set_request_body(
52+
self.request,
53+
'{{"login": "{}", "password": "{}"}}'.format(
54+
SITE_OWNER_NAME,
55+
SITE_OWNER_PASSWORD,
56+
),
5357
)
5458
service = self.traverse()
5559
res = service.reply()
@@ -69,9 +73,12 @@ def test_expired_token_returns_400(self):
6973

7074
def test_login_without_api_permission(self):
7175
self.portal.manage_permission(UseRESTAPI, roles=[])
72-
self.request["BODY"] = '{{"login": "{}", "password": "{}"}}'.format(
73-
SITE_OWNER_NAME,
74-
SITE_OWNER_PASSWORD,
76+
set_request_body(
77+
self.request,
78+
'{{"login": "{}", "password": "{}"}}'.format(
79+
SITE_OWNER_NAME,
80+
SITE_OWNER_PASSWORD,
81+
),
7582
)
7683
service = self.traverse()
7784
res = service.render()
@@ -82,8 +89,9 @@ def test_login_with_zope_user_fails_without_pas_plugin(self):
8289
uf.plugins.users.addUser("zopeuser", "zopeuser", TEST_USER_PASSWORD)
8390
if "jwt_auth" in uf:
8491
uf["jwt_auth"].manage_activateInterfaces([])
85-
self.request["BODY"] = (
86-
'{"login": "zopeuser", "password": "' + TEST_USER_PASSWORD + '"}'
92+
set_request_body(
93+
self.request,
94+
'{"login": "zopeuser", "password": "' + TEST_USER_PASSWORD + '"}',
8795
)
8896
service = self.traverse()
8997
res = service.reply()
@@ -97,8 +105,9 @@ def test_login_with_zope_user(self):
97105
self.layer["app"].acl_users.plugins.users.addUser(
98106
"zopeuser", "zopeuser", TEST_USER_PASSWORD
99107
)
100-
self.request["BODY"] = (
101-
'{"login": "zopeuser", "password": "' + TEST_USER_PASSWORD + '"}'
108+
set_request_body(
109+
self.request,
110+
'{"login": "zopeuser", "password": "' + TEST_USER_PASSWORD + '"}',
102111
)
103112
service = self.traverse()
104113
res = service.reply()

src/plone/restapi/tests/test_blocks_deserializer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from plone.restapi.behaviors import IBlocks
44
from plone.restapi.interfaces import IBlockFieldDeserializationTransformer
55
from plone.restapi.interfaces import IDeserializeFromJson
6+
from plone.restapi.testing import set_request_body
67
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
78
from plone.uuid.interfaces import IUUID
89
from zope.component import adapter
@@ -42,7 +43,7 @@ def setUp(self):
4243
def deserialize(self, blocks=None, validate_all=False, context=None):
4344
blocks = blocks or ""
4445
context = context or self.portal.doc1
45-
self.request["BODY"] = json.dumps({"blocks": blocks})
46+
set_request_body(self.request, json.dumps({"blocks": blocks}))
4647
deserializer = getMultiAdapter((context, self.request), IDeserializeFromJson)
4748

4849
return deserializer(validate_all=validate_all)

src/plone/restapi/tests/test_dxcontent_deserializer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from plone.restapi.exceptions import DeserializationError
55
from plone.restapi.interfaces import IDeserializeFromJson
66
from plone.restapi.interfaces import ISerializeToJson
7+
from plone.restapi.testing import set_request_body
78
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
89
from plone.restapi.tests.dxtypes import ITestAnnotationsBehavior
910
from plone.restapi.tests.mixin_ordering import OrderingMixin
@@ -43,7 +44,7 @@ def setUp(self):
4344

4445
def deserialize(self, body="{}", validate_all=False, context=None, create=False):
4546
context = context or self.portal.doc1
46-
self.request["BODY"] = body
47+
set_request_body(self.request, body)
4748
deserializer = getMultiAdapter((context, self.request), IDeserializeFromJson)
4849
return deserializer(validate_all=validate_all, create=create)
4950

@@ -257,7 +258,7 @@ def deserialize(self, field, value, validate_all=False, context=None):
257258
body = {}
258259
body[field] = value
259260
body = json.dumps(body)
260-
self.request["BODY"] = body
261+
set_request_body(self.request, body)
261262
deserializer = getMultiAdapter((context, self.request), IDeserializeFromJson)
262263
return deserializer(validate_all=validate_all)
263264

src/plone/restapi/tests/test_site_deserializer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from plone.dexterity.interfaces import IDexterityFTI
33
from plone.dexterity.schema import SCHEMA_CACHE
44
from plone.restapi.interfaces import IDeserializeFromJson
5+
from plone.restapi.testing import set_request_body
56
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
67
from plone.restapi.tests.mixin_ordering import OrderingMixin
78
from zope.component import getMultiAdapter
@@ -34,7 +35,7 @@ def setUp(self):
3435

3536
def deserialize(self, body="{}", validate_all=False, context=None):
3637
context = context or self.portal
37-
self.request["BODY"] = body
38+
set_request_body(self.request, body)
3839
deserializer = getMultiAdapter((context, self.request), IDeserializeFromJson)
3940
return deserializer(validate_all=validate_all)
4041

@@ -70,7 +71,7 @@ def setUp(self):
7071

7172
def deserialize(self, body="{}", validate_all=False, context=None):
7273
context = context or self.portal
73-
self.request["BODY"] = body
74+
set_request_body(self.request, body)
7475
deserializer = getMultiAdapter((context, self.request), IDeserializeFromJson)
7576
return deserializer(validate_all=validate_all)
7677

0 commit comments

Comments
 (0)