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
davisagli committed Jun 26, 2018
1 parent e6a85ac commit 6aafdfb
Show file tree
Hide file tree
Showing 7 changed files with 458 additions and 30 deletions.
13 changes: 13 additions & 0 deletions CHANGES.rst
Expand Up @@ -11,13 +11,26 @@ 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.

- 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>`_)

- 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
33 changes: 22 additions & 11 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,26 @@ def processInputs(
environ['QUERY_STRING'] = ''

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

# Workaround for https://bugs.python.org/issue27777:
# If Content-Length is nonzero, manufacture a Content-Disposition
# with a filename to make sure a binary file is opened.
headers = None
if 'CONTENT_LENGTH' in environ and environ['CONTENT_LENGTH'] != '0':
# In order to override content-disposition we need to
# specify the full headers; this is based on FileStorage.__init__
headers = {}
if method == 'POST':
# Set default content-type for POST to what's traditional
headers['content-type'] = "application/x-www-form-urlencoded"
if 'CONTENT_TYPE' in environ:
headers['content-type'] = environ['CONTENT_TYPE']
if 'QUERY_STRING' in environ:
self.qs_on_post = environ['QUERY_STRING']
headers['content-length'] = environ['CONTENT_LENGTH']
headers['content-disposition'] = 'inline; filename="stdin"'
fs = FieldStorage(
fp=fp, headers=headers, 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 +526,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
12 changes: 12 additions & 0 deletions src/ZPublisher/tests/testHTTPRequest.py
Expand Up @@ -766,6 +766,18 @@ 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_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 6aafdfb

Please sign in to comment.