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