• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zopefoundation / Zope / 3956162881

pending completion
3956162881

push

github

Michael Howitz
Update to deprecation warning free releases.

4401 of 7036 branches covered (62.55%)

Branch coverage included in aggregate %.

27161 of 31488 relevant lines covered (86.26%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.54
/src/ZPublisher/WSGIPublisher.py
1
##############################################################################
2
#
3
# Copyright (c) 2002 Zope Foundation and Contributors.
4
#
5
# This software is subject to the provisions of the Zope Public License,
6
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
7
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10
# FOR A PARTICULAR PURPOSE
11
#
12
##############################################################################
13
""" Python Object Publisher -- Publish Python objects on web servers
1✔
14
"""
15
import sys
1✔
16
from _thread import allocate_lock
1✔
17
from contextlib import closing
1✔
18
from contextlib import contextmanager
1✔
19
from io import BytesIO
1✔
20
from io import IOBase
1✔
21

22
import transaction
1✔
23
from AccessControl.SecurityManagement import getSecurityManager
1✔
24
from AccessControl.SecurityManagement import newSecurityManager
1✔
25
from AccessControl.SecurityManagement import noSecurityManager
1✔
26
from Acquisition import aq_acquire
1✔
27
from transaction.interfaces import TransientError
1✔
28
from zExceptions import Unauthorized
1✔
29
from zExceptions import upgradeException
1✔
30
from zope.component import queryMultiAdapter
1✔
31
from zope.event import notify
1✔
32
from zope.globalrequest import clearRequest
1✔
33
from zope.globalrequest import setRequest
1✔
34
from zope.publisher.skinnable import setDefaultSkin
1✔
35
from zope.security.management import endInteraction
1✔
36
from zope.security.management import newInteraction
1✔
37
from ZPublisher import pubevents
1✔
38
from ZPublisher.HTTPRequest import WSGIRequest
1✔
39
from ZPublisher.HTTPResponse import WSGIResponse
1✔
40
from ZPublisher.Iterators import IUnboundStreamIterator
1✔
41
from ZPublisher.mapply import mapply
1✔
42
from ZPublisher.utils import recordMetaData
1✔
43

44

45
_FILE_TYPES = (IOBase, )
1✔
46
_DEFAULT_DEBUG_EXCEPTIONS = False
1✔
47
_DEFAULT_DEBUG_MODE = False
1✔
48
_DEFAULT_REALM = None
1✔
49
_MODULE_LOCK = allocate_lock()
1✔
50
_MODULES = {}
1✔
51
_WEBDAV_SOURCE_PORT = 0
1✔
52

53

54
# This is copied from the six module
55
def reraise(tp, value, tb=None):
1✔
56
    try:
1✔
57
        if value is None:
1!
58
            value = tp()
×
59
        if value.__traceback__ is not tb:
1✔
60
            raise value.with_traceback(tb)
1✔
61
        raise value
1✔
62
    finally:
63
        value = None
1✔
64
        tb = None
1✔
65

66

67
def call_object(obj, args, request):
1✔
68
    return obj(*args)
1✔
69

70

71
def dont_publish_class(klass, request):
1✔
72
    request.response.forbiddenError("class %s" % klass.__name__)
×
73

74

75
def missing_name(name, request):
1✔
76
    if name == 'self':
1!
77
        return request['PARENTS'][0]
1✔
78
    request.response.badRequestError(name)
×
79

80

81
def validate_user(request, user):
1✔
82
    newSecurityManager(request, user)
1✔
83

84

85
def set_default_debug_exceptions(debug_exceptions):
1✔
86
    global _DEFAULT_DEBUG_EXCEPTIONS
87
    _DEFAULT_DEBUG_EXCEPTIONS = debug_exceptions
1✔
88

89

90
def set_webdav_source_port(port):
1✔
91
    global _WEBDAV_SOURCE_PORT
92
    _WEBDAV_SOURCE_PORT = port
1✔
93

94

95
def get_debug_exceptions():
1✔
96
    global _DEFAULT_DEBUG_EXCEPTIONS
97
    return _DEFAULT_DEBUG_EXCEPTIONS
1✔
98

99

100
def set_default_debug_mode(debug_mode):
1✔
101
    global _DEFAULT_DEBUG_MODE
102
    _DEFAULT_DEBUG_MODE = debug_mode
1✔
103

104

105
def set_default_authentication_realm(realm):
1✔
106
    global _DEFAULT_REALM
107
    _DEFAULT_REALM = realm
1✔
108

109

110
def get_module_info(module_name='Zope2'):
1✔
111
    global _MODULES
112
    info = _MODULES.get(module_name)
1✔
113
    if info is not None:
1✔
114
        return info
1✔
115

116
    with _MODULE_LOCK:
1✔
117
        module = __import__(module_name)
1✔
118
        app = getattr(module, 'bobo_application', module)
1✔
119
        realm = _DEFAULT_REALM if _DEFAULT_REALM is not None else module_name
1✔
120
        _MODULES[module_name] = info = (app, realm, _DEFAULT_DEBUG_MODE)
1✔
121
    return info
1✔
122

123

124
def _exc_view_created_response(exc, request, response):
1✔
125
    view = queryMultiAdapter((exc, request), name='index.html')
1✔
126
    parents = request.get('PARENTS')
1✔
127

128
    if view is None and parents:
1✔
129
        # Try a fallback based on the old standard_error_message
130
        # DTML Method in the ZODB
131
        view = queryMultiAdapter((exc, request),
1✔
132
                                 name='standard_error_message')
133
        root_parent = parents[0]
1✔
134
        try:
1✔
135
            aq_acquire(root_parent, 'standard_error_message')
1✔
136
        except (AttributeError, KeyError):
1✔
137
            view = None
1✔
138

139
    if view is not None:
1✔
140
        # Wrap the view in the context in which the exception happened.
141
        if parents:
1✔
142
            view.__parent__ = parents[0]
1✔
143

144
        # Set status and headers from the exception on the response,
145
        # which would usually happen while calling the exception
146
        # with the (environ, start_response) WSGI tuple.
147
        response.setStatus(exc.__class__)
1✔
148
        if hasattr(exc, 'headers'):
1✔
149
            for key, value in exc.headers.items():
1✔
150
                response.setHeader(key, value)
1✔
151

152
        # Call the view so we can use it as the response body.
153
        body = view()
1✔
154

155
        # Explicitly set the content type header if it's not there yet so
156
        # the response does not get served with the text/plain default.
157
        # But only do this when there is a body.
158
        # An empty body may indicate a 304 NotModified response,
159
        # and setting a content type header will change the stored header
160
        # in caching servers such as Varnish.
161
        # See https://github.com/zopefoundation/Zope/issues/1089
162
        if body and not response.getHeader('Content-Type'):
1✔
163
            response.setHeader('Content-Type', 'text/html')
1✔
164

165
        # Note: setBody would set the Content-Type header to text/plain
166
        # if it is not set yet, except when the body is empty.
167
        response.setBody(body)
1✔
168
        return True
1✔
169

170
    return False
1✔
171

172

173
@contextmanager
1✔
174
def transaction_pubevents(request, response, tm=transaction.manager):
1✔
175
    try:
1✔
176
        setDefaultSkin(request)
1✔
177
        newInteraction()
1✔
178
        tm.begin()
1✔
179
        notify(pubevents.PubStart(request))
1✔
180

181
        yield
1✔
182

183
        notify(pubevents.PubBeforeCommit(request))
1✔
184
        if tm.isDoomed():
1!
185
            tm.abort()
×
186
        else:
187
            tm.commit()
1✔
188
        notify(pubevents.PubSuccess(request))
1✔
189
    except Exception as exc:
1✔
190
        # Normalize HTTP exceptions
191
        # (For example turn zope.publisher NotFound into zExceptions NotFound)
192
        exc_type, _ = upgradeException(exc.__class__, None)
1✔
193
        if not isinstance(exc, exc_type):
1✔
194
            exc = exc_type(str(exc))
1✔
195

196
        # Create new exc_info with the upgraded exception.
197
        exc_info = (exc_type, exc, sys.exc_info()[2])
1✔
198

199
        try:
1✔
200
            # Raise exception from app if handle-errors is False
201
            # (set by zope.testbrowser in some cases)
202
            if request.environ.get('x-wsgiorg.throw_errors', False):
1✔
203
                reraise(*exc_info)
1✔
204

205
            retry = False
1✔
206
            unauth = False
1✔
207
            debug_exc = getattr(response, 'debug_exceptions', False)
1✔
208

209
            # If the exception is transient and the request can be retried,
210
            # shortcut further processing. It makes no sense to have an
211
            # exception view registered for this type of exception.
212
            if isinstance(exc, TransientError) and request.supports_retry():
1✔
213
                retry = True
1✔
214
            else:
215
                # Handle exception view. Make sure an exception view that
216
                # blows up doesn't leave the user e.g. unable to log in.
217
                try:
1✔
218
                    exc_view_created = _exc_view_created_response(
1✔
219
                        exc, request, response)
220
                except Exception:
×
221
                    exc_view_created = False
×
222

223
                # _unauthorized modifies the response in-place. If this hook
224
                # is used, an exception view for Unauthorized has to merge
225
                # the state of the response and the exception instance.
226
                if isinstance(exc, Unauthorized):
1✔
227
                    unauth = True
1✔
228
                    exc.setRealm(response.realm)
1✔
229
                    response._unauthorized()
1✔
230
                    response.setStatus(exc.getStatus())
1✔
231

232
            # Notify subscribers that this request is failing.
233
            notify(pubevents.PubBeforeAbort(request, exc_info, retry))
1✔
234
            tm.abort()
1✔
235
            notify(pubevents.PubFailure(request, exc_info, retry))
1✔
236

237
            if retry or \
1✔
238
               (not unauth and (debug_exc or not exc_view_created)):
239
                reraise(*exc_info)
1✔
240

241
        finally:
242
            # Avoid traceback / exception reference cycle.
243
            del exc, exc_info
1✔
244
    finally:
245
        endInteraction()
1✔
246

247

248
def publish(request, module_info):
1✔
249
    obj, realm, debug_mode = module_info
1✔
250

251
    request.processInputs()
1✔
252
    response = request.response
1✔
253

254
    response.debug_exceptions = get_debug_exceptions()
1✔
255

256
    if debug_mode:
1✔
257
        response.debug_mode = debug_mode
1✔
258

259
    if realm and not request.get('REMOTE_USER', None):
1✔
260
        response.realm = realm
1✔
261

262
    noSecurityManager()
1✔
263

264
    # Get the path list.
265
    # According to RFC1738 a trailing space in the path is valid.
266
    path = request.get('PATH_INFO')
1✔
267
    request['PARENTS'] = [obj]
1✔
268

269
    obj = request.traverse(path, validated_hook=validate_user)
1✔
270

271
    # Set debug information from the active request on the open connection
272
    # Used to be done in ZApplicationWrapper.__bobo_traverse__ for ZServer
273
    try:
1✔
274
        # Grab the connection from the last (root application) object,
275
        # which usually has a connection available.
276
        request['PARENTS'][-1]._p_jar.setDebugInfo(request.environ,
1✔
277
                                                   request.other)
278
    except AttributeError:
1✔
279
        # If there is no connection don't worry
280
        pass
1✔
281

282
    notify(pubevents.PubAfterTraversal(request))
1✔
283
    recordMetaData(obj, request)
1✔
284

285
    result = mapply(obj,
1✔
286
                    request.args,
287
                    request,
288
                    call_object,
289
                    1,
290
                    missing_name,
291
                    dont_publish_class,
292
                    request,
293
                    bind=1)
294
    if result is not response:
1✔
295
        response.setBody(result)
1✔
296

297
    return response
1✔
298

299

300
@contextmanager
1✔
301
def load_app(module_info):
1✔
302
    app_wrapper, realm, debug_mode = module_info
1✔
303
    # Loads the 'OFS.Application' from ZODB.
304
    app = app_wrapper()
1✔
305

306
    try:
1✔
307
        yield (app, realm, debug_mode)
1✔
308
    finally:
309
        if transaction.manager.manager._txn is not None:
1✔
310
            # Only abort a transaction, if one exists. Otherwise the
311
            # abort creates a new transaction just to abort it.
312
            transaction.abort()
1✔
313
        app._p_jar.close()
1✔
314

315

316
def publish_module(environ, start_response,
1✔
317
                   _publish=publish,  # only for testing
318
                   _response=None,
319
                   _response_factory=WSGIResponse,
320
                   _request=None,
321
                   _request_factory=WSGIRequest,
322
                   _module_name='Zope2'):
323
    module_info = get_module_info(_module_name)
1✔
324
    result = ()
1✔
325

326
    path_info = environ.get('PATH_INFO')
1✔
327
    if path_info:
1✔
328
        # BIG Comment, see discussion at
329
        # https://github.com/zopefoundation/Zope/issues/575
330
        #
331
        # The WSGI server automatically treats headers, including the
332
        # PATH_INFO, as latin-1 encoded bytestrings, according to PEP-3333. As
333
        # this causes headache I try to show the steps a URI takes in WebOb,
334
        # which is similar in other wsgi server implementations.
335
        # UTF-8 URL-encoded object-id 'täst':
336
        #   http://localhost/t%C3%A4st
337
        # unquote('/t%C3%A4st'.decode('ascii')) results in utf-8 encoded bytes
338
        #   b'/t\xc3\xa4st'
339
        # b'/t\xc3\xa4st'.decode('latin-1') latin-1 decoding due to PEP-3333
340
        #   '/täst'
341
        # We now have a latin-1 decoded text, which was actually utf-8 encoded.
342
        # To reverse this we have to encode with latin-1 first.
343
        path_info = path_info.encode('latin-1')
1✔
344

345
        # So we can now decode with the right (utf-8) encoding to get text.
346
        # This encode/decode two-step with different encodings works because
347
        # of the way PEP-3333 restricts the type of string allowable for
348
        # request and response metadata. The allowed characters match up in
349
        # both latin-1 and utf-8.
350
        path_info = path_info.decode('utf-8')
1✔
351

352
        environ['PATH_INFO'] = path_info
1✔
353

354
    # See if this should be be marked up as WebDAV request.
355
    try:
1✔
356
        server_port = int(environ['SERVER_PORT'])
1✔
357
    except (KeyError, ValueError):
1✔
358
        server_port = 0
1✔
359

360
    if _WEBDAV_SOURCE_PORT and _WEBDAV_SOURCE_PORT == server_port:
1✔
361
        environ['WEBDAV_SOURCE_PORT'] = 1
1✔
362

363
        # GET needs special treatment. Traversal is forced to the
364
        # manage_DAVget method to get the unrendered sources.
365
        if environ['REQUEST_METHOD'].upper() == 'GET':
1✔
366
            environ['PATH_INFO'] = '%s/manage_DAVget' % environ['PATH_INFO']
1✔
367

368
    with closing(BytesIO()) as stdout, closing(BytesIO()) as stderr:
1✔
369
        new_response = (
1✔
370
            _response
371
            if _response is not None
372
            else _response_factory(stdout=stdout, stderr=stderr))
373
        new_response._http_version = environ['SERVER_PROTOCOL'].split('/')[1]
1✔
374
        new_response._server_version = environ.get('SERVER_SOFTWARE')
1✔
375

376
        new_request = (
1✔
377
            _request
378
            if _request is not None
379
            else _request_factory(environ['wsgi.input'],
380
                                  environ,
381
                                  new_response))
382

383
        for i in range(getattr(new_request, 'retry_max_count', 3) + 1):
1!
384
            request = new_request
1✔
385
            response = new_response
1✔
386
            setRequest(request)
1✔
387
            try:
1✔
388
                with load_app(module_info) as new_mod_info:
1✔
389
                    with transaction_pubevents(request, response):
1✔
390
                        response = _publish(request, new_mod_info)
1✔
391

392
                        user = getSecurityManager().getUser()
1✔
393
                        if user is not None and \
1✔
394
                           user.getUserName() != 'Anonymous User':
395
                            environ['REMOTE_USER'] = user.getUserName()
1✔
396
                break
1✔
397
            except TransientError:
1✔
398
                if request.supports_retry():
1✔
399
                    request.delay_retry()  # Insert a time delay
1✔
400
                    new_request = request.retry()
1✔
401
                    new_response = new_request.response
1✔
402
                else:
403
                    raise
1✔
404
            finally:
405
                request.close()
1✔
406
                clearRequest()
1✔
407

408
        # Start the WSGI server response
409
        status, headers = response.finalize()
1✔
410
        start_response(status, headers)
1✔
411

412
        if isinstance(response.body, _FILE_TYPES) or \
1✔
413
           IUnboundStreamIterator.providedBy(response.body):
414
            if hasattr(response.body, 'read') and \
1✔
415
               'wsgi.file_wrapper' in environ:
416
                result = environ['wsgi.file_wrapper'](response.body)
1✔
417
            else:
418
                result = response.body
1✔
419
        else:
420
            # If somebody used response.write, that data will be in the
421
            # response.stdout BytesIO, so we put that before the body.
422
            result = (response.stdout.getvalue(), response.body)
1✔
423

424
        for func in response.after_list:
1✔
425
            func()
1✔
426

427
    # Return the result body iterable.
428
    return result
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc