From 81c8663ff7ab7fc8c5a5c7538948f1baaed4947d Mon Sep 17 00:00:00 2001
From: Bryton Lacquement <bryton.lacquement@nexedi.com>
Date: Tue, 4 Jun 2019 12:23:47 +0200
Subject: [PATCH] patches: backport WSGIPublisher from Zope4

This is a preliminary commit that only adds a verbatim copy of WSGIPublisher.py
---
 product/ERP5Type/patches/WSGIPublisher.py | 326 ++++++++++++++++++++++
 1 file changed, 326 insertions(+)
 create mode 100644 product/ERP5Type/patches/WSGIPublisher.py

diff --git a/product/ERP5Type/patches/WSGIPublisher.py b/product/ERP5Type/patches/WSGIPublisher.py
new file mode 100644
index 0000000000..dbffa335a7
--- /dev/null
+++ b/product/ERP5Type/patches/WSGIPublisher.py
@@ -0,0 +1,326 @@
+# Backport from Zope4
+
+##############################################################################
+#
+# Copyright (c) 2002 Zope Foundation and Contributors.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+""" Python Object Publisher -- Publish Python objects on web servers
+"""
+import sys
+from contextlib import closing
+from contextlib import contextmanager
+from io import BytesIO
+from io import IOBase
+
+from six import PY3
+from six import reraise
+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 transaction.interfaces import TransientError
+from zExceptions import Unauthorized
+from zExceptions import upgradeException
+from zope.component import queryMultiAdapter
+from zope.event import notify
+from zope.globalrequest import clearRequest
+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 ZPublisher.Iterators import IUnboundStreamIterator
+from ZPublisher.mapply import mapply
+from ZPublisher.utils import recordMetaData
+
+
+if sys.version_info >= (3, ):
+    _FILE_TYPES = (IOBase, )
+else:
+    _FILE_TYPES = (IOBase, file)  # NOQA
+
+_DEFAULT_DEBUG_MODE = False
+_DEFAULT_REALM = None
+_MODULE_LOCK = allocate_lock()
+_MODULES = {}
+
+
+def call_object(obj, args, request):
+    return obj(*args)
+
+
+def dont_publish_class(klass, request):
+    request.response.forbiddenError("class %s" % klass.__name__)
+
+
+def missing_name(name, request):
+    if name == 'self':
+        return request['PARENTS'][0]
+    request.response.badRequestError(name)
+
+
+def validate_user(request, user):
+    newSecurityManager(request, user)
+
+
+def set_default_debug_mode(debug_mode):
+    global _DEFAULT_DEBUG_MODE
+    _DEFAULT_DEBUG_MODE = debug_mode
+
+
+def set_default_authentication_realm(realm):
+    global _DEFAULT_REALM
+    _DEFAULT_REALM = realm
+
+
+def get_module_info(module_name='Zope2'):
+    global _MODULES
+    info = _MODULES.get(module_name)
+    if info is not None:
+        return info
+
+    with _MODULE_LOCK:
+        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)
+    return info
+
+
+def _exc_view_created_response(exc, request, response):
+    view = queryMultiAdapter((exc, request), name=u'index.html')
+    parents = request.get('PARENTS')
+
+    if view is None and parents:
+        # Try a fallback based on the old standard_error_message
+        # DTML Method in the ZODB
+        view = queryMultiAdapter((exc, request),
+                                 name=u'standard_error_message')
+        root_parent = parents[0]
+        try:
+            aq_acquire(root_parent, 'standard_error_message')
+        except (AttributeError, KeyError):
+            view = None
+
+    if view is not None:
+        # Wrap the view in the context in which the exception happened.
+        if parents:
+            view.__parent__ = parents[0]
+
+        # Set status and headers from the exception on the response,
+        # which would usually happen while calling the exception
+        # with the (environ, start_response) WSGI tuple.
+        response.setStatus(exc.__class__)
+        if hasattr(exc, 'headers'):
+            for key, value in exc.headers.items():
+                response.setHeader(key, value)
+
+        # Set the response body to the result of calling the view.
+        response.setBody(view())
+        return True
+
+    return False
+
+
+@contextmanager
+def transaction_pubevents(request, response, tm=transaction.manager):
+    try:
+        setDefaultSkin(request)
+        newInteraction()
+        tm.begin()
+        notify(pubevents.PubStart(request))
+
+        yield
+
+        notify(pubevents.PubBeforeCommit(request))
+        if tm.isDoomed():
+            tm.abort()
+        else:
+            tm.commit()
+        notify(pubevents.PubSuccess(request))
+    except Exception as exc:
+        # Normalize HTTP exceptions
+        # (For example turn zope.publisher NotFound into zExceptions NotFound)
+        exc_type, _ = upgradeException(exc.__class__, None)
+        if not isinstance(exc, exc_type):
+            exc = exc_type(str(exc))
+
+        # Create new exc_info with the upgraded exception.
+        exc_info = (exc_type, exc, sys.exc_info()[2])
+
+        try:
+            # Raise exception from app if handle-errors is False
+            # (set by zope.testbrowser in some cases)
+            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
+
+            notify(pubevents.PubBeforeAbort(request, exc_info, retry))
+            tm.abort()
+            notify(pubevents.PubFailure(request, exc_info, retry))
+
+            if retry:
+                reraise(*exc_info)
+
+            if not (exc_view_created or isinstance(exc, Unauthorized)):
+                reraise(*exc_info)
+        finally:
+            # Avoid traceback / exception reference cycle.
+            del exc, exc_info
+    finally:
+        endInteraction()
+
+
+def publish(request, module_info):
+    obj, realm, debug_mode = module_info
+
+    request.processInputs()
+    response = request.response
+
+    if debug_mode:
+        response.debug_mode = debug_mode
+
+    if realm and not request.get('REMOTE_USER', None):
+        response.realm = realm
+
+    noSecurityManager()
+
+    # Get the path list.
+    # According to RFC1738 a trailing space in the path is valid.
+    path = request.get('PATH_INFO')
+    request['PARENTS'] = [obj]
+
+    obj = request.traverse(path, validated_hook=validate_user)
+    notify(pubevents.PubAfterTraversal(request))
+    recordMetaData(obj, request)
+
+    result = mapply(obj,
+                    request.args,
+                    request,
+                    call_object,
+                    1,
+                    missing_name,
+                    dont_publish_class,
+                    request,
+                    bind=1)
+    if result is not response:
+        response.setBody(result)
+
+    return response
+
+
+@contextmanager
+def load_app(module_info):
+    app_wrapper, realm, debug_mode = module_info
+    # Loads the 'OFS.Application' from ZODB.
+    app = app_wrapper()
+
+    try:
+        yield (app, realm, debug_mode)
+    finally:
+        if transaction.manager.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()
+        app._p_jar.close()
+
+
+def publish_module(environ, start_response,
+                   _publish=publish,  # only for testing
+                   _response=None,
+                   _response_factory=WSGIResponse,
+                   _request=None,
+                   _request_factory=WSGIRequest,
+                   _module_name='Zope2'):
+    module_info = get_module_info(_module_name)
+    result = ()
+
+    path_info = environ.get('PATH_INFO')
+    if path_info and PY3:
+        # The WSGI server automatically treats the PATH_INFO as latin-1 encoded
+        # bytestrings. Typically this is a false assumption as the browser
+        # delivers utf-8 encoded PATH_INFO. We, therefore, need to encode it
+        # again with latin-1 to get a utf-8 encoded bytestring.
+        path_info = path_info.encode('latin-1')
+        # But in Python 3 we need text here, so we decode the bytestring.
+        path_info = path_info.decode('utf-8')
+
+        environ['PATH_INFO'] = path_info
+    with closing(BytesIO()) as stdout, closing(BytesIO()) as stderr:
+        new_response = (
+            _response
+            if _response is not None
+            else _response_factory(stdout=stdout, stderr=stderr))
+        new_response._http_version = environ['SERVER_PROTOCOL'].split('/')[1]
+        new_response._server_version = environ.get('SERVER_SOFTWARE')
+
+        new_request = (
+            _request
+            if _request is not None
+            else _request_factory(environ['wsgi.input'],
+                                  environ,
+                                  new_response))
+
+        for i in range(getattr(new_request, 'retry_max_count', 3) + 1):
+            request = new_request
+            response = new_response
+            setRequest(request)
+            try:
+                with load_app(module_info) as new_mod_info:
+                    with transaction_pubevents(request, response):
+                        response = _publish(request, new_mod_info)
+                break
+            except TransientError:
+                if request.supports_retry():
+                    new_request = request.retry()
+                    new_response = new_request.response
+                else:
+                    raise
+            finally:
+                request.close()
+                clearRequest()
+
+        # Start the WSGI server response
+        status, headers = response.finalize()
+        start_response(status, headers)
+
+        if isinstance(response.body, _FILE_TYPES) or \
+           IUnboundStreamIterator.providedBy(response.body):
+            result = response.body
+        else:
+            # If somebody used response.write, that data will be in the
+            # response.stdout BytesIO, so we put that before the body.
+            result = (response.stdout.getvalue(), response.body)
+
+        for func in response.after_list:
+            func()
+
+    # Return the result body iterable.
+    return result
-- 
2.30.9