|
1 import itertools |
|
2 |
|
3 from contextlib import contextmanager |
|
4 from warnings import warn |
|
5 from cgi import FieldStorage |
|
6 |
|
7 import rql |
|
8 |
|
9 from cubicweb.web.request import CubicWebRequestBase |
|
10 from cubicweb import repoapi |
|
11 |
|
12 import cubicweb |
|
13 import cubicweb.web |
|
14 from cubicweb.server import session as cwsession |
|
15 |
|
16 from pyramid import httpexceptions |
|
17 |
|
18 from cubicweb.pyramid import tools |
|
19 |
|
20 import logging |
|
21 |
|
22 log = logging.getLogger(__name__) |
|
23 |
|
24 |
|
25 CW_321 = cubicweb.__pkginfo__.numversion >= (3, 21, 0) |
|
26 |
|
27 |
|
28 class Connection(cwsession.Connection): |
|
29 """ A specialised Connection that access the session data through a |
|
30 property. |
|
31 |
|
32 This behavior makes sure the actual session data is not loaded until |
|
33 actually accessed. |
|
34 """ |
|
35 def __init__(self, session, *args, **kw): |
|
36 super(Connection, self).__init__(session, *args, **kw) |
|
37 self._session = session |
|
38 |
|
39 def _get_session_data(self): |
|
40 return self._session.data |
|
41 |
|
42 def _set_session_data(self, data): |
|
43 pass |
|
44 |
|
45 _session_data = property(_get_session_data, _set_session_data) |
|
46 |
|
47 |
|
48 class Session(cwsession.Session): |
|
49 """ A Session that access the session data through a property. |
|
50 |
|
51 Along with :class:`Connection`, it avoid any load of the pyramid session |
|
52 data until it is actually accessed. |
|
53 """ |
|
54 def __init__(self, pyramid_request, user, repo): |
|
55 super(Session, self).__init__(user, repo) |
|
56 self._pyramid_request = pyramid_request |
|
57 |
|
58 def get_data(self): |
|
59 if not getattr(self, '_protect_data_access', False): |
|
60 self._data_accessed = True |
|
61 return self._pyramid_request.session |
|
62 |
|
63 def set_data(self, data): |
|
64 if getattr(self, '_data_accessed', False): |
|
65 self._pyramid_request.session.clear() |
|
66 self._pyramid_request.session.update(data) |
|
67 |
|
68 data = property(get_data, set_data) |
|
69 |
|
70 def new_cnx(self): |
|
71 self._protect_data_access = True |
|
72 try: |
|
73 return Connection(self) |
|
74 finally: |
|
75 self._protect_data_access = False |
|
76 |
|
77 |
|
78 def cw_headers(request): |
|
79 return itertools.chain( |
|
80 *[[(k, item) for item in v] |
|
81 for k, v in request.cw_request.headers_out.getAllRawHeaders()]) |
|
82 |
|
83 |
|
84 @contextmanager |
|
85 def cw_to_pyramid(request): |
|
86 """ Context manager to wrap a call to the cubicweb API. |
|
87 |
|
88 All CW exceptions will be transformed into their pyramid equivalent. |
|
89 When needed, some CW reponse bits may be converted too (mainly headers)""" |
|
90 try: |
|
91 yield |
|
92 except cubicweb.web.Redirect as ex: |
|
93 assert 300 <= ex.status < 400 |
|
94 raise httpexceptions.status_map[ex.status]( |
|
95 ex.location, headers=cw_headers(request)) |
|
96 except cubicweb.web.StatusResponse as ex: |
|
97 warn('[3.16] StatusResponse is deprecated use req.status_out', |
|
98 DeprecationWarning, stacklevel=2) |
|
99 request.body = ex.content |
|
100 request.status_int = ex.status |
|
101 except cubicweb.web.Unauthorized as ex: |
|
102 raise httpexceptions.HTTPForbidden( |
|
103 request.cw_request._( |
|
104 'You\'re not authorized to access this page. ' |
|
105 'If you think you should, please contact the site ' |
|
106 'administrator.'), |
|
107 headers=cw_headers(request)) |
|
108 except cubicweb.web.Forbidden: |
|
109 raise httpexceptions.HTTPForbidden( |
|
110 request.cw_request._( |
|
111 'This action is forbidden. ' |
|
112 'If you think it should be allowed, please contact the site ' |
|
113 'administrator.'), |
|
114 headers=cw_headers(request)) |
|
115 except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex: |
|
116 raise |
|
117 |
|
118 |
|
119 class CubicWebPyramidRequest(CubicWebRequestBase): |
|
120 """ A CubicWeb request that only wraps a pyramid request. |
|
121 |
|
122 :param request: A pyramid request |
|
123 |
|
124 """ |
|
125 def __init__(self, request): |
|
126 self._request = request |
|
127 |
|
128 self.path = request.upath_info |
|
129 |
|
130 vreg = request.registry['cubicweb.registry'] |
|
131 https = request.scheme == 'https' |
|
132 |
|
133 post = request.params.mixed() |
|
134 headers_in = request.headers |
|
135 |
|
136 super(CubicWebPyramidRequest, self).__init__(vreg, https, post, |
|
137 headers=headers_in) |
|
138 |
|
139 self.content = request.body_file_seekable |
|
140 |
|
141 def setup_params(self, params): |
|
142 self.form = {} |
|
143 for param, val in params.items(): |
|
144 if param in self.no_script_form_params and val: |
|
145 val = self.no_script_form_param(param, val) |
|
146 if isinstance(val, FieldStorage) and val.file: |
|
147 val = (val.filename, val.file) |
|
148 if param == '_cwmsgid': |
|
149 self.set_message_id(val) |
|
150 elif param == '__message': |
|
151 warn('[3.13] __message in request parameter is deprecated ' |
|
152 '(may only be given to .build_url). Seeing this message ' |
|
153 'usualy means your application hold some <form> where ' |
|
154 'you should replace use of __message hidden input by ' |
|
155 'form.set_message, so new _cwmsgid mechanism is properly ' |
|
156 'used', |
|
157 DeprecationWarning) |
|
158 self.set_message(val) |
|
159 else: |
|
160 self.form[param] = val |
|
161 |
|
162 def is_secure(self): |
|
163 return self._request.scheme == 'https' |
|
164 |
|
165 def relative_path(self, includeparams=True): |
|
166 path = self._request.path[1:] |
|
167 if includeparams and self._request.query_string: |
|
168 return '%s?%s' % (path, self._request.query_string) |
|
169 return path |
|
170 |
|
171 def instance_uri(self): |
|
172 return self._request.application_url |
|
173 |
|
174 def get_full_path(self): |
|
175 path = self._request.path |
|
176 if self._request.query_string: |
|
177 return '%s?%s' % (path, self._request.query_string) |
|
178 return path |
|
179 |
|
180 def http_method(self): |
|
181 return self._request.method |
|
182 |
|
183 def _set_status_out(self, value): |
|
184 self._request.response.status_int = value |
|
185 |
|
186 def _get_status_out(self): |
|
187 return self._request.response.status_int |
|
188 |
|
189 status_out = property(_get_status_out, _set_status_out) |
|
190 |
|
191 @property |
|
192 def message(self): |
|
193 """Returns a '<br>' joined list of the cubicweb current message and the |
|
194 default pyramid flash queue messages. |
|
195 """ |
|
196 return u'\n<br>\n'.join( |
|
197 self._request.session.pop_flash() |
|
198 + self._request.session.pop_flash('cubicweb')) |
|
199 |
|
200 def set_message(self, msg): |
|
201 self.reset_message() |
|
202 self._request.session.flash(msg, 'cubicweb') |
|
203 |
|
204 def set_message_id(self, msgid): |
|
205 self.reset_message() |
|
206 self.set_message( |
|
207 self._request.session.pop(msgid, u'')) |
|
208 |
|
209 def reset_message(self): |
|
210 self._request.session.pop_flash('cubicweb') |
|
211 |
|
212 |
|
213 def render_view(request, vid, **kwargs): |
|
214 """ Helper function to render a CubicWeb view. |
|
215 |
|
216 :param request: A pyramid request |
|
217 :param vid: A CubicWeb view id |
|
218 :param **kwargs: Keyword arguments to select and instanciate the view |
|
219 :returns: The rendered view content |
|
220 """ |
|
221 vreg = request.registry['cubicweb.registry'] |
|
222 # XXX The select() function could, know how to handle a pyramid |
|
223 # request, and feed it directly to the views that supports it. |
|
224 # On the other hand, we could refine the View concept and decide it works |
|
225 # with a cnx, and never with a WebRequest |
|
226 |
|
227 with cw_to_pyramid(request): |
|
228 view = vreg['views'].select(vid, request.cw_request, **kwargs) |
|
229 view.set_stream() |
|
230 view.render() |
|
231 return view._stream.getvalue() |
|
232 |
|
233 |
|
234 def _cw_cnx(request): |
|
235 """ Obtains a cw session from a pyramid request |
|
236 |
|
237 The connection will be commited or rolled-back in a request finish |
|
238 callback (this is temporary, we should make use of the transaction manager |
|
239 in a later version). |
|
240 |
|
241 Not meant for direct use, use ``request.cw_cnx`` instead. |
|
242 |
|
243 :param request: A pyramid request |
|
244 :returns type: :class:`cubicweb.server.session.Connection` |
|
245 """ |
|
246 session = request.cw_session |
|
247 if session is None: |
|
248 return None |
|
249 |
|
250 if CW_321: |
|
251 cnx = session.new_cnx() |
|
252 |
|
253 def commit_state(cnx): |
|
254 return cnx.commit_state |
|
255 else: |
|
256 cnx = repoapi.ClientConnection(session) |
|
257 |
|
258 def commit_state(cnx): |
|
259 return cnx._cnx.commit_state |
|
260 |
|
261 def cleanup(request): |
|
262 try: |
|
263 if (request.exception is not None and not isinstance( |
|
264 request.exception, ( |
|
265 httpexceptions.HTTPSuccessful, |
|
266 httpexceptions.HTTPRedirection))): |
|
267 cnx.rollback() |
|
268 elif commit_state(cnx) == 'uncommitable': |
|
269 cnx.rollback() |
|
270 else: |
|
271 cnx.commit() |
|
272 finally: |
|
273 cnx.__exit__(None, None, None) |
|
274 |
|
275 request.add_finished_callback(cleanup) |
|
276 cnx.__enter__() |
|
277 return cnx |
|
278 |
|
279 |
|
280 def repo_connect(request, repo, eid): |
|
281 """A lightweight version of |
|
282 :meth:`cubicweb.server.repository.Repository.connect` that does not keep |
|
283 track of opened sessions, removing the need of closing them""" |
|
284 user = tools.cached_build_user(repo, eid) |
|
285 session = Session(request, user, repo) |
|
286 tools.cnx_attach_entity(session, user) |
|
287 # Calling the hooks should be done only once, disabling it completely for |
|
288 # now |
|
289 #with session.new_cnx() as cnx: |
|
290 #repo.hm.call_hooks('session_open', cnx) |
|
291 #cnx.commit() |
|
292 # repo._sessions[session.sessionid] = session |
|
293 return session |
|
294 |
|
295 |
|
296 def _cw_session(request): |
|
297 """Obtains a cw session from a pyramid request |
|
298 |
|
299 :param request: A pyramid request |
|
300 :returns type: :class:`cubicweb.server.session.Session` |
|
301 |
|
302 Not meant for direct use, use ``request.cw_session`` instead. |
|
303 """ |
|
304 repo = request.registry['cubicweb.repository'] |
|
305 |
|
306 if not request.authenticated_userid: |
|
307 eid = request.registry.get('cubicweb.anonymous_eid') |
|
308 if eid is None: |
|
309 return None |
|
310 session = repo_connect(request, repo, eid=eid) |
|
311 else: |
|
312 session = request._cw_cached_session |
|
313 |
|
314 return session |
|
315 |
|
316 |
|
317 def _cw_request(request): |
|
318 """ Obtains a CubicWeb request wrapper for the pyramid request. |
|
319 |
|
320 :param request: A pyramid request |
|
321 :return: A CubicWeb request |
|
322 :returns type: :class:`CubicWebPyramidRequest` |
|
323 |
|
324 Not meant for direct use, use ``request.cw_request`` instead. |
|
325 |
|
326 """ |
|
327 req = CubicWebPyramidRequest(request) |
|
328 cnx = request.cw_cnx |
|
329 if cnx is not None: |
|
330 req.set_cnx(request.cw_cnx) |
|
331 return req |
|
332 |
|
333 |
|
334 def get_principals(login, request): |
|
335 """ Returns the group names of the authenticated user. |
|
336 |
|
337 This function is meant to be used as an authentication policy callback. |
|
338 |
|
339 It also pre-open the cubicweb session and put it in |
|
340 request._cw_cached_session for later usage by :func:`_cw_session`. |
|
341 |
|
342 .. note:: |
|
343 |
|
344 If the default authentication policy is not used, make sure this |
|
345 function gets called by the active authentication policy. |
|
346 |
|
347 :param login: A cubicweb user eid |
|
348 :param request: A pyramid request |
|
349 :returns: A list of group names |
|
350 """ |
|
351 repo = request.registry['cubicweb.repository'] |
|
352 |
|
353 try: |
|
354 session = repo_connect(request, repo, eid=login) |
|
355 request._cw_cached_session = session |
|
356 except: |
|
357 log.exception("Failed") |
|
358 raise |
|
359 |
|
360 return session.user.groups |
|
361 |
|
362 |
|
363 def includeme(config): |
|
364 """ Enables the core features of Pyramid CubicWeb. |
|
365 |
|
366 Automatically called by the 'pyramid' command, or via |
|
367 ``config.include('cubicweb.pyramid.code')``. In the later case, |
|
368 the following registry entries must be defined first: |
|
369 |
|
370 'cubicweb.config' |
|
371 A cubicweb 'config' instance. |
|
372 |
|
373 'cubicweb.repository' |
|
374 The correponding cubicweb repository. |
|
375 |
|
376 'cubicweb.registry' |
|
377 The vreg. |
|
378 """ |
|
379 repo = config.registry['cubicweb.repository'] |
|
380 |
|
381 with repo.internal_cnx() as cnx: |
|
382 login = config.registry['cubicweb.config'].anonymous_user()[0] |
|
383 if login is not None: |
|
384 config.registry['cubicweb.anonymous_eid'] = cnx.find( |
|
385 'CWUser', login=login).one().eid |
|
386 |
|
387 config.add_request_method( |
|
388 _cw_session, name='cw_session', property=True, reify=True) |
|
389 config.add_request_method( |
|
390 _cw_cnx, name='cw_cnx', property=True, reify=True) |
|
391 config.add_request_method( |
|
392 _cw_request, name='cw_request', property=True, reify=True) |
|
393 |
|
394 cwcfg = config.registry['cubicweb.config'] |
|
395 for cube in cwcfg.cubes(): |
|
396 pkgname = 'cubes.' + cube |
|
397 mod = __import__(pkgname) |
|
398 mod = getattr(mod, cube) |
|
399 if hasattr(mod, 'includeme'): |
|
400 config.include('cubes.' + cube) |