/
WSGIPublisher.py
248 lines (200 loc) · 7.54 KB
/
WSGIPublisher.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
##############################################################################
#
# 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
"""
from contextlib import contextmanager, closing
from io import BytesIO
from io import IOBase
import sys
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import noSecurityManager
from six.moves._thread import allocate_lock
import transaction
from transaction.interfaces import TransientError
from zExceptions import (
HTTPOk,
HTTPRedirection,
Unauthorized,
)
from ZODB.POSException import ConflictError
from zope.component import queryMultiAdapter
from zope.event import notify
from zope.security.management import newInteraction, endInteraction
from zope.publisher.skinnable import setDefaultSkin
from ZPublisher.HTTPRequest import WSGIRequest
from ZPublisher.HTTPResponse import WSGIResponse
from ZPublisher.Iterators import IUnboundStreamIterator
from ZPublisher.mapply import mapply
from ZPublisher import pubevents
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
@contextmanager
def transaction_pubevents(request, tm=transaction.manager):
ok_exception = None
try:
setDefaultSkin(request)
newInteraction()
tm.begin()
notify(pubevents.PubStart(request))
try:
yield None
except (HTTPOk, HTTPRedirection) as exc:
ok_exception = exc
notify(pubevents.PubBeforeCommit(request))
if tm.isDoomed():
tm.abort()
else:
tm.commit()
notify(pubevents.PubSuccess(request))
except Exception:
exc_info = sys.exc_info()
notify(pubevents.PubBeforeAbort(
request, exc_info, request.supports_retry()))
tm.abort()
notify(pubevents.PubFailure(
request, exc_info, request.supports_retry()))
raise
finally:
endInteraction()
if ok_exception is not None:
raise ok_exception
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
def _publish_response(request, response, module_info, _publish=publish):
try:
with transaction_pubevents(request):
response = _publish(request, module_info)
except Exception as exc:
if not request.environ.get('wsgi.handleErrors', True):
raise
if isinstance(exc, HTTPRedirection):
response._redirect(exc)
elif isinstance(exc, Unauthorized):
response._unauthorized(exc)
view = queryMultiAdapter((exc, request), name=u'index.html')
if view is not None:
parents = request.get('PARENTS')
if parents:
view.__parent__ = parents[0]
response.setStatus(exc.__class__)
response.setBody(view())
return response
if isinstance(exc, (HTTPRedirection, Unauthorized)):
return response
raise
return response
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 = ()
with closing(BytesIO()) as stdout, closing(BytesIO()) as stderr:
response = (_response if _response is not None else
_response_factory(stdout=stdout, stderr=stderr))
response._http_version = environ['SERVER_PROTOCOL'].split('/')[1]
response._server_version = environ.get('SERVER_SOFTWARE')
request = (_request if _request is not None else
_request_factory(environ['wsgi.input'], environ, response))
for i in range(getattr(request, 'retry_max_count', 3) + 1):
try:
response = _publish_response(
request, response, module_info, _publish=_publish)
break
except (ConflictError, TransientError) as exc:
if request.supports_retry():
new_request = request.retry()
request.close()
request = new_request
response = new_request.response
else:
raise
finally:
request.close()
# 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
# stdout BytesIO, so we put that before the body.
result = (stdout.getvalue(), response.body)
for func in response.after_list:
func()
# Return the result body iterable.
return result