Skip to content

Commit

Permalink
Re-add support for XML-RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
David Glick authored and pbauer committed Sep 27, 2018
1 parent c9002f0 commit 6dcd4c7
Show file tree
Hide file tree
Showing 7 changed files with 461 additions and 31 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -11,9 +11,17 @@ https://github.com/zopefoundation/Zope/blob/4.0a6/CHANGES.rst
4.0b6 (unreleased)
------------------

New features
++++++++++++

- Restore support for XML-RPC when using the WSGI publisher - dropped in 4.0a2.

- Add a minimum ``buildout.cfg`` suggestion in the docs for creating ``wsgi``
instances.

Bugfixes
++++++++

- Fix ZMI upload of `DTMLMethod` and `DTMLDocument` to store the DTML as a
native ``str`` on both Python versions.
(`#265 <https://github.com/zopefoundation/Zope/pull/265>`_)
Expand All @@ -22,6 +30,10 @@ https://github.com/zopefoundation/Zope/blob/4.0a6/CHANGES.rst
dependency.
(`#307 <https://github.com/zopefoundation/Zope/pull/307>`_)

- Work around Python bug (https://bugs.python.org/issue27777)
when reading request bodies not encoded as application/x-www-form-urlencoded
or multipart/form-data.


4.0b5 (2018-05-18)
------------------
Expand Down
7 changes: 1 addition & 6 deletions src/ZPublisher/BaseRequest.py
Expand Up @@ -34,17 +34,12 @@
from zope.publisher.interfaces.browser import IBrowserPublisher
from zope.traversing.namespace import namespaceLookup
from zope.traversing.namespace import nsParse
from ZPublisher.xmlrpc import is_xmlrpc_response

from App.bbb import HAS_ZSERVER
from ZPublisher.Converters import type_converters
from ZPublisher.interfaces import UseTraversalDefault

if HAS_ZSERVER:
from ZServer.ZPublisher.xmlrpc import is_xmlrpc_response
else:
def is_xmlrpc_response(response):
return False

_marker = []
UNSPECIFIED_ROLES = ''

Expand Down
25 changes: 13 additions & 12 deletions src/ZPublisher/HTTPRequest.py
Expand Up @@ -24,7 +24,6 @@

from AccessControl.tainted import should_be_tainted
from AccessControl.tainted import taint_string
import pkg_resources
from six import binary_type
from six import PY2
from six import PY3
Expand All @@ -43,6 +42,7 @@
from ZPublisher.BaseRequest import quote
from ZPublisher.Converters import get_converter
from ZPublisher.utils import basic_auth_decode
from ZPublisher import xmlrpc

if PY3:
from html import escape
Expand All @@ -53,14 +53,6 @@
from urllib import splitport
from urllib import splittype

xmlrpc = None
try:
dist = pkg_resources.get_distribution('ZServer')
except pkg_resources.DistributionNotFound:
pass
else:
from ZServer.ZPublisher import xmlrpc

# Flags
SEQUENCE = 1
DEFAULT = 2
Expand Down Expand Up @@ -505,7 +497,8 @@ def processInputs(
environ['QUERY_STRING'] = ''

meth = None
fs = FieldStorage(fp=fp, environ=environ, keep_blank_values=1)
fs = ZopeFieldStorage(
fp=fp, environ=environ, keep_blank_values=1)

# Keep a reference to the FieldStorage. Otherwise it's
# __del__ method is called too early and closing FieldStorage.file.
Expand All @@ -515,7 +508,7 @@ def processInputs(
if 'HTTP_SOAPACTION' in environ:
# Stash XML request for interpretation by a SOAP-aware view
other['SOAPXML'] = fs.value
elif (xmlrpc is not None and method == 'POST' and
elif (method == 'POST' and
('content-type' in fs.headers and
'text/xml' in fs.headers['content-type'])):
# Ye haaa, XML-RPC!
Expand Down Expand Up @@ -1635,7 +1628,15 @@ def sane_environment(env):
return dict


ZopeFieldStorage = FieldStorage # BBB
class ZopeFieldStorage(FieldStorage):
"""This subclass exists to work around a Python bug
(see https://bugs.python.org/issue27777) to make sure
we can read binary data from a request body.
"""

def read_binary(self):
self._binary_file = True
return FieldStorage.read_binary(self)


# Original version: zope.publisher.browser.FileUpload
Expand Down
25 changes: 25 additions & 0 deletions src/ZPublisher/tests/testHTTPRequest.py
Expand Up @@ -766,6 +766,31 @@ def test_processInputs_w_cookie_parsing(self):
self.assertEqual(req.cookies['multi2'],
'cookie data with unquoted spaces')

def test_processInputs_xmlrpc(self):
TEST_METHOD_CALL = (
b'<?xml version="1.0"?>'
b'<methodCall><methodName>test</methodName></methodCall>'
)
environ = self._makePostEnviron(body=TEST_METHOD_CALL)
environ['CONTENT_TYPE'] = 'text/xml'
req = self._makeOne(stdin=BytesIO(TEST_METHOD_CALL), environ=environ)
req.processInputs()
self.assertEqual(req.PATH_INFO, '/test')
self.assertEqual(req.args, ())

def test_processInputs_w_urlencoded_and_qs(self):
body = b'foo=1'
environ = {
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'CONTENT_LENGTH': len(body),
'QUERY_STRING': 'bar=2',
'REQUEST_METHOD': 'POST',
}
req = self._makeOne(stdin=BytesIO(body), environ=environ)
req.processInputs()
self.assertEqual(req.form['foo'], '1')
self.assertEqual(req.form['bar'], '2')

def test_postProcessInputs(self):
from ZPublisher.HTTPRequest import default_encoding

Expand Down
203 changes: 203 additions & 0 deletions src/ZPublisher/tests/test_xmlrpc.py
@@ -0,0 +1,203 @@
import unittest
try:
import xmlrpc.client as xmlrpclib
except ImportError:
import xmlrpclib

from DateTime import DateTime


class FauxResponse(object):

def __init__(self):
self._headers = {}
self._body = None

def setBody(self, body):
self._body = body

def setHeader(self, name, value):
self._headers[name] = value

def setStatus(self, status):
self._status = status


class FauxInstance(object):
def __init__(self, **kw):
self.__dict__.update(kw)


class XMLRPCResponseTests(unittest.TestCase):

def _getTargetClass(self):
from ZPublisher.xmlrpc import Response
return Response

def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)

def test_setBody(self):
body = FauxInstance(_secret='abc', public='def')
faux = FauxResponse()
response = self._makeOne(faux)

response.setBody(body)

body_str = faux._body
self.assertEqual(type(body_str), type(''))

as_set, method = xmlrpclib.loads(body_str)
as_set = as_set[0]

self.assertEqual(method, None)
self.assertFalse('_secret' in as_set.keys())
self.assertTrue('public' in as_set.keys())
self.assertEqual(as_set['public'], 'def')

def test_nil(self):
body = FauxInstance(public=None)
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
self.assert_(data[0]['public'] is None)

def test_instance(self):
# Instances are turned into dicts with their private
# attributes removed.
body = FauxInstance(_secret='abc', public='def')
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]
self.assertEqual(data, {'public': 'def'})

def test_instanceattribute(self):
# While the removal of private ('_') attributes works fine for the
# top-level instance, how about attributes that are themselves
# instances?
body = FauxInstance(public=FauxInstance(_secret='abc', public='def'))
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['public']
self.assertEqual(data, {'public': 'def'})

def test_instanceattribute_recursive(self):
# Instance "flattening" should work recursively, ad infinitum
body = FauxInstance(public=FauxInstance(public=FauxInstance(
_secret='abc', public='def')))
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['public']['public']
self.assertEqual(data, {'public': 'def'})

def test_instance_in_list(self):
# Instances are turned into dicts with their private
# attributes removed, even when embedded in another
# data structure.
body = [FauxInstance(_secret='abc', public='def')]
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0][0]
self.assertEqual(data, {'public': 'def'})

def test_instance_in_dict(self):
# Instances are turned into dicts with their private
# attributes removed, even when embedded in another
# data structure.
body = {'faux': FauxInstance(_secret='abc', public='def')}
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['faux']
self.assertEqual(data, {'public': 'def'})

def test_zopedatetimeinstance(self):
# DateTime instance at top-level
body = DateTime('2006-05-24 07:00:00 GMT+0')
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]
self.assertTrue(isinstance(data, xmlrpclib.DateTime))
self.assertEqual(data.value, u'2006-05-24T07:00:00+00:00')

def test_zopedatetimeattribute(self):
# DateTime instance as attribute
body = FauxInstance(public=DateTime('2006-05-24 07:00:00 GMT+0'))
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['public']
self.assertTrue(isinstance(data, xmlrpclib.DateTime))
self.assertEqual(data.value, u'2006-05-24T07:00:00+00:00')

def test_zopedatetimeattribute_recursive(self):
# DateTime encoding should work recursively
body = FauxInstance(public=FauxInstance(
public=DateTime('2006-05-24 07:00:00 GMT+0')))
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['public']['public']
self.assertTrue(isinstance(data, xmlrpclib.DateTime))
self.assertEqual(data.value, u'2006-05-24T07:00:00+00:00')

def test_zopedatetimeinstance_in_list(self):
# DateTime instance embedded in a list
body = [DateTime('2006-05-24 07:00:00 GMT+0')]
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0][0]
self.assertTrue(isinstance(data, xmlrpclib.DateTime))
self.assertEqual(data.value, u'2006-05-24T07:00:00+00:00')

def test_zopedatetimeinstance_in_dict(self):
# DateTime instance embedded in a dict
body = {'date': DateTime('2006-05-24 07:00:00 GMT+0')}
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]['date']
self.assertTrue(isinstance(data, xmlrpclib.DateTime))
self.assertEqual(data.value, u'2006-05-24T07:00:00+00:00')

def test_functionattribute(self):
# Cannot marshal functions or methods, obviously

def foo():
pass

body = FauxInstance(public=foo)
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
func = xmlrpclib.loads(faux._body)
self.assertEqual(func, (({'public': {}},), None))

def test_emptystringattribute(self):
# Test an edge case: attribute name '' is possible,
# at least in theory.
body = FauxInstance(_secret='abc')
setattr(body, '', True)
faux = FauxResponse()
response = self._makeOne(faux)
response.setBody(body)
data, method = xmlrpclib.loads(faux._body)
data = data[0]
self.assertEqual(data, {'': True})

0 comments on commit 6dcd4c7

Please sign in to comment.