Skip to content

Commit

Permalink
HTTPResponse: default content type to text/plain (#1075)
Browse files Browse the repository at this point in the history
* WIP HTTPResponse: default content type to text/plain

 * drop implicit detection of content type
 * drop support for returning HTML with ``response.setBody((title, body))``

* - add a change log entry and improve wording [ci skip]

Co-authored-by: Jens Vagelpohl <[email protected]>
  • Loading branch information
perrinjerome and dataflake authored Dec 16, 2022
1 parent 26291e6 commit 0c4fc50
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 64 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
5.7.1 (unreleased)
------------------

- Set the published default ``Content-Type`` header to ``text/plain``
if none has been set explicitly to prevent a cross-site scripting attack.
Also remove the old behavior of constructing an HTML page for published
methods returning a two-item tuple.

- Update to newest compatible versions of dependencies.


Expand Down
32 changes: 7 additions & 25 deletions docs/zdgbook/ObjectPublishing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -261,25 +261,6 @@ the chapter.
Depending on the used client, it looks like nothing happens.


Optionally, the published method can return a tuple with
the title and the body of the response. In this case, the publisher
returns a generated HTML page, with the first item of the tuple used
for the value of the HTML ``title`` tag of the page, and the second
item as the content of the HTML ``body`` tag.


For example a response of::

("my_title", "my_text")


is turned into this HTML page::

<html>
<head><title>my_title</title></head>
<body>my_text</body>
</html>


Controlling Base HREF
---------------------
Expand Down Expand Up @@ -348,10 +329,10 @@ base with a *base* tag in your ``index_html`` method output.
Response Headers
----------------

The publisher and the web server take care of setting response headers
such as *Content-Length* and *Content-Type*. Later in the chapter
you'll find out how to control these headers and also how exceptions
are used to set the HTTP response code.
The publisher and the web server take care of setting the *Content-Length*
response header. Later in the chapter you'll find out how to control
response headers and also how exceptions are used to set the HTTP
response code.


Pre-Traversal Hook
Expand Down Expand Up @@ -1121,7 +1102,7 @@ Known issues and caveats

- unrecognized directives are silently ignored

- if a request paramater contains several converter directives, the
- if a request parameter contains several converter directives, the
leftmost wins

- if a request paramter contains several encoding directives, the
Expand Down Expand Up @@ -1163,7 +1144,7 @@ Exceptions
----------

When the object publisher catches an unhandled exception, it tries to
match it with a set of predifined exceptions coming from the
match it with a set of predefined exceptions coming from the
**zExceptions** package, such as **HTTPNoContent**, **HTTPNotFound**,
**HTTPUnauthorized**.

Expand Down Expand Up @@ -1282,6 +1263,7 @@ being called from the web. Consider this function::
...
result = ...
if REQUEST is not None:
REQUEST.RESPONSE.setHeader("Content-Type", "text/html")
return "<html><p>Result: %s </p></html>" % result
return result

Expand Down
1 change: 1 addition & 0 deletions src/App/Management.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def manage_zmi_logout(self, REQUEST, RESPONSE):
realm = RESPONSE.realm
RESPONSE.setStatus(401)
RESPONSE.setHeader('WWW-Authenticate', 'basic realm="%s"' % realm, 1)
RESPONSE.setHeader('Content-Type', 'text/html')
RESPONSE.setBody("""<html>
<head><title>Logout</title></head>
<body>
Expand Down
34 changes: 15 additions & 19 deletions src/ZPublisher/HTTPResponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,6 @@ def setBody(self, body, title='', is_error=False, lock=None):
You can also specify a title, in which case the title and body
will be wrapped up in html, head, title, and body tags.
If the body is a 2-element tuple, then it will be treated
as (title,body)
If body has an 'asHTML' method, replace it by the result of that
method.
Expand All @@ -539,13 +536,12 @@ def setBody(self, body, title='', is_error=False, lock=None):
if not body:
return self

if isinstance(body, tuple) and len(body) == 2:
title, body = body
content_type = self.headers.get('content-type')

if hasattr(body, 'asHTML'):
body = body.asHTML()

content_type = self.headers.get('content-type')
if content_type is None:
content_type = 'text/html'

if isinstance(body, (bytes, bytearray, memoryview)):
body = bytes(body)
Expand All @@ -567,6 +563,7 @@ def setBody(self, body, title='', is_error=False, lock=None):
else:
if title:
title = str(title)
content_type = 'text/html'
if not is_error:
self.body = body = self._html(
title, body.decode(self.charset)).encode(self.charset)
Expand All @@ -577,10 +574,7 @@ def setBody(self, body, title='', is_error=False, lock=None):
self.body = body

if content_type is None:
if self.isHTML(body):
content_type = f'text/html; charset={self.charset}'
else:
content_type = f'text/plain; charset={self.charset}'
content_type = f'text/plain; charset={self.charset}'
self.setHeader('content-type', content_type)
else:
if content_type.startswith('text/') and \
Expand Down Expand Up @@ -892,10 +886,11 @@ def exception(self, fatal=0, info=None, abort=1):
b = '<unprintable %s object>' % type(b).__name__

if fatal and t is SystemExit and v.code == 0:
self.setHeader('content-type', 'text/html')
body = self.setBody(
(str(t),
'Zope has exited normally.<p>'
+ self._traceback(t, v, tb) + '</p>'),
'Zope has exited normally.<p>'
+ self._traceback(t, v, tb) + '</p>',
title=t,
is_error=True)
else:
try:
Expand All @@ -904,10 +899,11 @@ def exception(self, fatal=0, info=None, abort=1):
match = None

if match is None:
self.setHeader('content-type', 'text/html')
body = self.setBody(
(str(t),
'Sorry, a site error occurred.<p>'
+ self._traceback(t, v, tb) + '</p>'),
'Sorry, a site error occurred.<p>'
+ self._traceback(t, v, tb) + '</p>',
title=t,
is_error=True)
elif self.isHTML(b):
# error is an HTML document, not just a snippet of html
Expand All @@ -918,8 +914,8 @@ def exception(self, fatal=0, info=None, abort=1):
body = self.setBody(b, is_error=True)
else:
body = self.setBody(
(str(t),
b + self._traceback(t, '(see above)', tb, 0)),
b + self._traceback(t, '(see above)', tb, 0),
title=t,
is_error=True)
del tb
return body
Expand Down
31 changes: 11 additions & 20 deletions src/ZPublisher/tests/testHTTPResponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,25 +559,10 @@ def test_setBody_empty_unchanged(self):
self.assertEqual(response.getHeader('Content-Type'), None)
self.assertEqual(response.getHeader('Content-Length'), None)

def test_setBody_2_tuple_wo_is_error_converted_to_HTML(self):
EXPECTED = (b"<html>\n"
b"<head>\n<title>TITLE</title>\n</head>\n"
b"<body>\nBODY\n</body>\n"
b"</html>\n")
def test_setBody_with_is_error_converted_to_Site_Error(self):
response = self._makeOne()
response.body = b'BEFORE'
result = response.setBody(('TITLE', b'BODY'))
self.assertTrue(result)
self.assertEqual(response.body, EXPECTED)
self.assertEqual(response.getHeader('Content-Type'),
'text/html; charset=utf-8')
self.assertEqual(response.getHeader('Content-Length'),
str(len(EXPECTED)))

def test_setBody_2_tuple_w_is_error_converted_to_Site_Error(self):
response = self._makeOne()
response.body = b'BEFORE'
result = response.setBody(('TITLE', b'BODY'), is_error=True)
result = response.setBody(b'BODY', 'TITLE', is_error=True)
self.assertTrue(result)
self.assertFalse(b'BEFORE' in response.body)
self.assertTrue(b'<h2>Site Error</h2>' in response.body)
Expand All @@ -595,14 +580,16 @@ def test_setBody_string_not_HTML(self):
'text/plain; charset=utf-8')
self.assertEqual(response.getHeader('Content-Length'), '4')

def test_setBody_string_HTML(self):
def test_setBody_string_HTML_uses_text_plain(self):
HTML = '<html><head></head><body></body></html>'
response = self._makeOne()
result = response.setBody(HTML)
self.assertTrue(result)
self.assertEqual(response.body, HTML.encode('utf-8'))
# content type is set as text/plain, even though body
# could be guessed as html
self.assertEqual(response.getHeader('Content-Type'),
'text/html; charset=utf-8')
'text/plain; charset=utf-8')
self.assertEqual(response.getHeader('Content-Length'), str(len(HTML)))

def test_setBody_object_with_asHTML(self):
Expand All @@ -628,7 +615,7 @@ def test_setBody_object_with_unicode(self):
self.assertTrue(result)
self.assertEqual(response.body, ENCODED)
self.assertEqual(response.getHeader('Content-Type'),
'text/html; charset=utf-8')
'text/plain; charset=utf-8')
self.assertEqual(response.getHeader('Content-Length'),
str(len(ENCODED)))

Expand Down Expand Up @@ -683,6 +670,10 @@ def test_setBody_tuple(self):
response = self._makeOne()
response.setBody(('a',))
self.assertEqual(b"('a',)", response.body)
response.setBody(('a', 'b'))
self.assertEqual(b"('a', 'b')", response.body)
response.setBody(('a', 'b', 'c'))
self.assertEqual(b"('a', 'b', 'c')", response.body)

def test_setBody_calls_insertBase(self):
response = self._makeOne()
Expand Down

0 comments on commit 0c4fc50

Please sign in to comment.