Skip to content
Open
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 news/1583.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Provide a JsonCompatible adapter for RichTextValue objects to fix crashes during nested serialization and schema default values.
[erral]
14 changes: 8 additions & 6 deletions src/plone/restapi/serializer/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
from plone.app.textfield.interfaces import IRichTextValue
from plone.dexterity.interfaces import IDexterityContent
from plone.restapi.bbb import safe_text
from plone.restapi.interfaces import IContextawareJsonCompatible
from plone.restapi.interfaces import IJsonCompatible
from zope.component import adapter
from zope.component import queryMultiAdapter
from zope.component.hooks import getSite
from zope.globalrequest import getRequest
from zope.i18n import translate
from zope.i18nmessageid.message import Message
Expand Down Expand Up @@ -62,12 +62,15 @@ def json_compatible(value, context=None):
Because of that the `json_compatible` helper method should always be
used for converting values that may be None.
"""
if context is None:
context = getSite()

if context is not None:
adapter = queryMultiAdapter((value, context), IContextawareJsonCompatible)
if adapter:
return adapter()
else:
return IJsonCompatible(value, None)

return IJsonCompatible(value, None)


@adapter(Interface)
Expand All @@ -80,8 +83,7 @@ def default_converter(value):
return value

raise TypeError(
"No converter for making"
" {!r} ({}) JSON compatible.".format(value, type(value))
f"No converter for making {value!r} ({type(value)}) JSON compatible."
)


Expand Down Expand Up @@ -175,7 +177,7 @@ def timedelta_converter(value):
return json_compatible(value.total_seconds())


@adapter(IRichTextValue, IDexterityContent)
@adapter(IRichTextValue, Interface)
@implementer(IContextawareJsonCompatible)
class RichtextDXContextConverter:
def __init__(self, value, context):
Expand Down
24 changes: 24 additions & 0 deletions src/plone/restapi/tests/test_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from plone.app.textfield import RichTextValue
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING

import unittest


class TestConverters(unittest.TestCase):

layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING

def test_richtextvalue_converter(self):
"""test that a RichTextValue is converted to a proper JSON structure"""
html = "<p>This is a demo HTML</p>"
value = RichTextValue(html, "text/html", "text/html")
json_compatible_value = json_compatible(value)
self.assertEqual(
json_compatible_value,
{
"data": html,
"content-type": "text/html",
"encoding": "utf-8",
},
)
31 changes: 31 additions & 0 deletions src/plone/restapi/tests/test_serializer_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from DateTime import DateTime
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
from plone.app.textfield.value import RichTextValue
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from unittest import TestCase
Expand Down Expand Up @@ -218,3 +219,33 @@ def test_i18n_message(self):

def test_missing_value(self):
self.assertEqual(None, json_compatible(Missing.Value))

def test_richtext_value_nested(self):
value = RichTextValue(
raw="<p>foo</p>", mimeType="text/html", outputMimeType="text/x-html-safe"
)
expected = {
"data": "<p>foo</p>",
"content-type": "text/html",
"encoding": "utf-8",
}
self.assertEqual(expected, json_compatible(value))
self.assertEqual(
expected,
json_compatible({"foo": value})["foo"],
)

def test_richtext_value_link_resolution_with_fallback(self):
# Verify that relative links are resolved against the site root
# when using the fallback context
html = '<p><a href="resolveuid/12345">link</a></p>'
value = RichTextValue(
raw=html, mimeType="text/html", outputMimeType="text/x-html-safe"
)
# Note: Since the UID isn't real, it won't be resolved to a URL,
# but output_relative_to will still be called.
# If we had a real object, we could test it further.
# The main thing is that it doesn't crash and returns valid JSON.
result = json_compatible(value)
self.assertIn("data", result)
self.assertEqual(result["content-type"], "text/html")
55 changes: 51 additions & 4 deletions src/plone/restapi/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from decimal import Decimal
from plone.app.multilingual.dx import directives
from plone.app.textfield import RichText
from plone.app.textfield.value import RichTextValue
from plone.autoform import directives as form
from plone.dexterity.fti import DexterityFTI
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from plone.restapi.types.interfaces import IJsonSchemaProvider
from plone.restapi.types.utils import get_fieldsets
Expand Down Expand Up @@ -46,7 +48,9 @@ class ITaggedValuesSchema(model.Schema):

parametrized_widget_field = schema.TextLine(title="Parametrized widget field")
form.widget(
"parametrized_widget_field", a_param="some_value", defaultFactory=lambda: "Foo"
"parametrized_widget_field",
a_param="some_value",
defaultFactory=lambda: "Foo",
)

not_parametrized_widget_field = schema.TextLine(
Expand Down Expand Up @@ -459,7 +463,11 @@ def test_decimal(self):

def test_int(self):
field = schema.Int(
title="My field", description="My great field", min=0, max=100, default=50
title="My field",
description="My great field",
min=0,
max=100,
default=50,
)
adapter = getMultiAdapter(
(field, self.portal, self.request), IJsonSchemaProvider
Expand Down Expand Up @@ -742,9 +750,46 @@ def test_richtext(self):
adapter.get_schema(),
)

def test_richtext_with_default(self):
field = RichText(
title="My field",
description="My great field",
default=RichTextValue(
raw="<p>Some default value</p>",
mimeType="text/html",
outputMimeType="text/x-html-safe",
),
)
adapter = getMultiAdapter(
(field, self.portal, self.request), IJsonSchemaProvider
)

schema = adapter.get_schema()
self.assertIsInstance(schema["default"], RichTextValue)

# After conversion to JSON compatible it should be a dict
schema = json_compatible(schema)
self.assertEqual(
{
"type": "string",
"title": "My field",
"factory": "Rich Text",
"description": "My great field",
"widget": "richtext",
"default": {
"data": "<p>Some default value</p>",
"content-type": "text/html",
"encoding": "utf-8",
},
},
schema,
)

def test_date(self):
field = schema.Date(
title="My field", description="My great field", default=date(2016, 1, 1)
title="My field",
description="My great field",
default=date(2016, 1, 1),
)
adapter = getMultiAdapter(
(field, self.portal, self.request), IJsonSchemaProvider
Expand Down Expand Up @@ -781,7 +826,9 @@ def test_datetime(self):

def test_jsonfield(self):
field = JSONField(
title="My field", description="My great field", widget="my_widget_name"
title="My field",
description="My great field",
widget="my_widget_name",
)
adapter = getMultiAdapter(
(field, self.portal, self.request), IJsonSchemaProvider
Expand Down