Commit f33ea934 authored by Bryton Lacquement's avatar Bryton Lacquement 🚪 Committed by Julien Muchembled

patches: make the WSGIPublisher backport work with Zope 2.13

Parts of ZPublisher.utils are also backported.
parent 81c8663f
......@@ -21,6 +21,7 @@
##############################################################################
# Load all monkey patches
from Products.ERP5Type.patches import WSGIPublisher
from Products.ERP5Type.patches import HTTPRequest
from Products.ERP5Type.patches import AccessControl_patch
from Products.ERP5Type.patches import Restricted
......
# Backport from Zope4
# Backport (with modified code) from Zope4
##############################################################################
#
......@@ -19,16 +19,22 @@ from contextlib import closing
from contextlib import contextmanager
from io import BytesIO
from io import IOBase
import logging
from six import binary_type
from six import PY3
from six import reraise
from six import text_type
from six.moves._thread import allocate_lock
import transaction
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from Acquisition import aq_acquire
from Acquisition import aq_inner
from Acquisition import aq_parent
from transaction.interfaces import TransientError
from zExceptions import Redirect
from zExceptions import Unauthorized
from zExceptions import upgradeException
from zope.component import queryMultiAdapter
......@@ -38,12 +44,12 @@ from zope.globalrequest import setRequest
from zope.publisher.skinnable import setDefaultSkin
from zope.security.management import endInteraction
from zope.security.management import newInteraction
from ZPublisher import pubevents
from ZPublisher.HTTPRequest import WSGIRequest
from ZPublisher.HTTPResponse import WSGIResponse
from Zope2.App.startup import validated_hook
from ZPublisher import pubevents, Retry
from ZPublisher.HTTPRequest import HTTPRequest
from ZPublisher.Iterators import IUnboundStreamIterator
from ZPublisher.mapply import mapply
from ZPublisher.utils import recordMetaData
from ZPublisher.WSGIPublisher import call_object, missing_name, WSGIResponse
if sys.version_info >= (3, ):
......@@ -57,32 +63,113 @@ _MODULE_LOCK = allocate_lock()
_MODULES = {}
def call_object(obj, args, request):
return obj(*args)
AC_LOGGER = logging.getLogger('event.AccessControl')
if 1: # upstream moved WSGIResponse to HTTPResponse.py
def dont_publish_class(klass, request):
request.response.forbiddenError("class %s" % klass.__name__)
# According to PEP 333, WSGI applications and middleware are forbidden from
# using HTTP/1.1 "hop-by-hop" features or headers. This patch prevents Zope
# from sending 'Connection' and 'Transfer-Encoding' headers.
def finalize(self):
headers = self.headers
body = self.body
# <patch>
# There's a bug in 'App.ImageFile.index_html': when it returns a 304 status
# code, 'Content-Length' is equal to a nonzero value.
if self.status == 304:
headers.pop('content-length', None)
# Force the removal of "hop-by-hop" headers
headers.pop('Connection', None)
# </patch>
# set 204 (no content) status if 200 and response is empty
# and not streaming
if ('content-type' not in headers and
'content-length' not in headers and
not self._streaming and self.status == 200):
self.setStatus('nocontent')
# add content length if not streaming
content_length = headers.get('content-length')
if content_length is None and not self._streaming:
self.setHeader('content-length', len(body))
# <patch>
# backport from Zope 4.0b1
# (see commit be5b14bd858da787c41a39e2533b0aabcd246fd5)
# </patch>
return '%s %s' % (self.status, self.errmsg), self.listHeaders()
WSGIResponse.finalize = finalize
def missing_name(name, request):
if name == 'self':
return request['PARENTS'][0]
request.response.badRequestError(name)
# From ZPublisher.utils
def recordMetaData(object, request):
if hasattr(object, 'getPhysicalPath'):
path = '/'.join(object.getPhysicalPath())
else:
# Try hard to get the physical path of the object,
# but there are many circumstances where that's not possible.
to_append = ()
def validate_user(request, user):
newSecurityManager(request, user)
if hasattr(object, '__self__') and hasattr(object, '__name__'):
# object is a Python method.
to_append = (object.__name__,)
object = object.__self__
while object is not None and not hasattr(object, 'getPhysicalPath'):
if getattr(object, '__name__', None) is None:
object = None
break
to_append = (object.__name__,) + to_append
object = aq_parent(aq_inner(object))
if object is not None:
path = '/'.join(object.getPhysicalPath() + to_append)
else:
# As Jim would say, "Waaaaaaaa!"
# This may cause problems with virtual hosts
# since the physical path is different from the path
# used to retrieve the object.
path = request.get('PATH_INFO')
T = transaction.get()
T.note(safe_unicode(path))
auth_user = request.get('AUTHENTICATED_USER', None)
if auth_user:
auth_folder = aq_parent(auth_user)
if auth_folder is None:
AC_LOGGER.warning(
'A user object of type %s has no aq_parent.',
type(auth_user))
auth_path = request.get('AUTHENTICATION_PATH')
else:
auth_path = '/'.join(auth_folder.getPhysicalPath()[1:-1])
user_id = auth_user.getId()
user_id = safe_unicode(user_id) if user_id else u'None'
T.setUser(user_id, safe_unicode(auth_path))
def set_default_debug_mode(debug_mode):
global _DEFAULT_DEBUG_MODE
_DEFAULT_DEBUG_MODE = debug_mode
def safe_unicode(value):
if isinstance(value, text_type):
return value
elif isinstance(value, binary_type):
try:
value = text_type(value, 'utf-8')
except UnicodeDecodeError:
value = value.decode('utf-8', 'replace')
return value
def set_default_authentication_realm(realm):
global _DEFAULT_REALM
_DEFAULT_REALM = realm
def dont_publish_class(klass, request):
request.response.forbiddenError("class %s" % klass.__name__)
def get_module_info(module_name='Zope2'):
......@@ -95,7 +182,8 @@ def get_module_info(module_name='Zope2'):
module = __import__(module_name)
app = getattr(module, 'bobo_application', module)
realm = _DEFAULT_REALM if _DEFAULT_REALM is not None else module_name
_MODULES[module_name] = info = (app, realm, _DEFAULT_DEBUG_MODE)
error_hook = getattr(module,'zpublisher_exception_hook', None)
_MODULES[module_name] = info = (app, realm, _DEFAULT_DEBUG_MODE, error_hook)
return info
......@@ -135,7 +223,7 @@ def _exc_view_created_response(exc, request, response):
@contextmanager
def transaction_pubevents(request, response, tm=transaction.manager):
def transaction_pubevents(request, response, err_hook, tm=transaction.manager):
try:
setDefaultSkin(request)
newInteraction()
......@@ -166,21 +254,44 @@ def transaction_pubevents(request, response, tm=transaction.manager):
if request.environ.get('x-wsgiorg.throw_errors', False):
reraise(*exc_info)
# Handle exception view
exc_view_created = _exc_view_created_response(
exc, request, response)
if isinstance(exc, Unauthorized):
# _unauthorized modifies the response in-place. If this hook
# is used, an exception view for Unauthorized has to merge
# the state of the response and the exception instance.
exc.setRealm(response.realm)
response._unauthorized()
response.setStatus(exc.getStatus())
retry = False
if isinstance(exc, TransientError) and request.supports_retry():
retry = True
if err_hook:
parents = request['PARENTS']
if parents:
parents = parents[0]
retry = False
try:
try:
r = err_hook(parents, request, *exc_info)
assert r is response
exc_view_created = True
except Retry:
if request.supports_retry():
retry = True
else:
r = err_hook(parents, request, *sys.exc_info())
assert r is response
exc_view_created = True
except (Redirect, Unauthorized):
response.exception()
exc_view_created = True
except BaseException as e:
if e is not exc:
raise
exc_view_created = False
else:
# Handle exception view
exc_view_created = _exc_view_created_response(
exc, request, response)
if isinstance(exc, Unauthorized):
# _unauthorized modifies the response in-place. If this hook
# is used, an exception view for Unauthorized has to merge
# the state of the response and the exception instance.
exc.setRealm(response.realm)
response._unauthorized()
response.setStatus(exc.getStatus())
retry = isinstance(exc, TransientError) and request.supports_retry()
notify(pubevents.PubBeforeAbort(request, exc_info, retry))
tm.abort()
......@@ -217,7 +328,7 @@ def publish(request, module_info):
path = request.get('PATH_INFO')
request['PARENTS'] = [obj]
obj = request.traverse(path, validated_hook=validate_user)
obj = request.traverse(path, validated_hook=validated_hook)
notify(pubevents.PubAfterTraversal(request))
recordMetaData(obj, request)
......@@ -245,7 +356,7 @@ def load_app(module_info):
try:
yield (app, realm, debug_mode)
finally:
if transaction.manager.manager._txn is not None:
if transaction.manager._txn is not None:
# Only abort a transaction, if one exists. Otherwise the
# abort creates a new transaction just to abort it.
transaction.abort()
......@@ -257,9 +368,10 @@ def publish_module(environ, start_response,
_response=None,
_response_factory=WSGIResponse,
_request=None,
_request_factory=WSGIRequest,
_request_factory=HTTPRequest,
_module_name='Zope2'):
module_info = get_module_info(_module_name)
module_info, err_hook = module_info[:3], module_info[3]
result = ()
path_info = environ.get('PATH_INFO')
......@@ -294,7 +406,7 @@ def publish_module(environ, start_response,
setRequest(request)
try:
with load_app(module_info) as new_mod_info:
with transaction_pubevents(request, response):
with transaction_pubevents(request, response, err_hook):
response = _publish(request, new_mod_info)
break
except TransientError:
......@@ -324,3 +436,6 @@ def publish_module(environ, start_response,
# Return the result body iterable.
return result
sys.modules['ZPublisher.WSGIPublisher'] = sys.modules[__name__]
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment