Skip to content

Commit

Permalink
Serve MessageDialogs and exception views as text/html (4.x) (#1082)
Browse files Browse the repository at this point in the history
* Restore serving MessageDialogs as text/html

Now that Zope uses text/plain by default, MessageDialogs were served as
text/plain. Keep compatibility by returning a special string with
`asHTML` method, that ZPublisher.HTTPResponse.HTTPResponse.setBody
understands.

MessageDialogs are deprecated and do not integrate well in Zope >= 4
ZMI, but they are used in some old products.

* - move tests into separate module

* - add fix for exception views

* - typo [ci skip]

* - typo [ci skip]

Co-authored-by: Jens Vagelpohl <jens@plyp.com>
  • Loading branch information
perrinjerome and dataflake committed Dec 19, 2022
1 parent 6c199cd commit 8d8d51b
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ https://zope.readthedocs.io/en/2.13/CHANGES.html
4.8.6 (unreleased)
------------------

- Explicitly serve ``App.Dialogs.MessageDialog`` and exception views as HTML
due to the changed default content type from `#1075
<https://github.com/zopefoundation/Zope/pull/1075>`_.


4.8.5 (2022-12-17)
------------------
Expand Down
15 changes: 14 additions & 1 deletion src/App/Dialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,20 @@
from App.special_dtml import HTML


MessageDialog = HTML("""
class MessageDialogHTML(HTML):
"""A special version of HTML which is always published as text/html
"""
def __call__(self, *args, **kw):
class _HTMLString(str):
"""A special string that will be published as text/html
"""
def asHTML(self):
return self
return _HTMLString(
super(MessageDialogHTML, self).__call__(*args, **kw))


MessageDialog = MessageDialogHTML("""
<HTML>
<HEAD>
<TITLE>&dtml-title;</TITLE>
Expand Down
19 changes: 19 additions & 0 deletions src/App/tests/test_Dialogs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Testing.ZopeTestCase


class TestMessageDialog(Testing.ZopeTestCase.ZopeTestCase):

def test_publish_set_content_type(self):
from App.Dialogs import MessageDialog

md = MessageDialog(
title='dialog title',
message='dialog message',
action='action'
)
self.assertIn('dialog title', md)
self.assertIn('dialog message', md)
self.assertIn('action', md)
req = self.app.REQUEST
req.RESPONSE.setBody(md)
self.assertIn('text/html', req.RESPONSE.getHeader('Content-Type'))
5 changes: 5 additions & 0 deletions src/ZPublisher/WSGIPublisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ def _exc_view_created_response(exc, request, response):
for key, value in exc.headers.items():
response.setHeader(key, value)

# Explicitly set the content type header if it's not there yet so
# the response doesn't get served with the text/plain default
if not response.getHeader('Content-Type'):
response.setHeader('Content-Type', 'text/html')

# Set the response body to the result of calling the view.
response.setBody(view())
return True
Expand Down
24 changes: 22 additions & 2 deletions src/ZPublisher/tests/test_WSGIPublisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,10 @@ def _request_factory(stdin, environ, response):
self.assertTrue(app_iter[1].startswith(
'Exception View: ConflictError'))
self.assertEqual(_request.retry_count, _request.retry_max_count)

# The Content-Type response header should be set to text/html
self.assertIn('text/html', _request.response.getHeader('Content-Type'))

unregisterExceptionView(Exception)

def testCustomExceptionViewUnauthorized(self):
Expand Down Expand Up @@ -871,10 +875,10 @@ def _callFUT(self, exc):
from zope.interface import directlyProvides
from zope.publisher.browser import IDefaultBrowserLayer
from ZPublisher.WSGIPublisher import _exc_view_created_response
req = DummyRequest()
req = self.app.REQUEST
req['PARENTS'] = [self.app]
directlyProvides(req, IDefaultBrowserLayer)
return _exc_view_created_response(exc, req, DummyResponse())
return _exc_view_created_response(exc, req, req.RESPONSE)

def _registerStandardErrorView(self):
from OFS.browser import StandardErrorMessageView
Expand All @@ -901,14 +905,22 @@ def testWithStandardErrorMessage(self):
from OFS.DTMLMethod import addDTMLMethod
from zExceptions import NotFound
self._registerStandardErrorView()
response = self.app.REQUEST.RESPONSE

addDTMLMethod(self.app, 'standard_error_message', file='OOPS')

# The response content-type header is not set before rendering
# the standard error template
self.assertFalse(response.getHeader('Content-Type'))

try:
self.assertTrue(self._callFUT(NotFound))
finally:
self._unregisterStandardErrorView()

# After rendering the response content-type header is set
self.assertIn('text/html', response.getHeader('Content-Type'))


class WSGIPublisherTests(FunctionalTestCase):

Expand Down Expand Up @@ -1063,6 +1075,14 @@ def setStatus(self, status, reason=None, lock=None):

status = property(lambda self: self._status, setStatus)

def getHeader(self, header):
return dict(self._headers).get(header, None)

def setHeader(self, header, value):
headers = dict(self._headers)
headers[header] = value
self._headers = tuple(headers.items())


class DummyCallable(object):
_called_with = _raise = _result = None
Expand Down

0 comments on commit 8d8d51b

Please sign in to comment.