17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """CubicWeb web client application object""" |
18 """CubicWeb web client application object""" |
19 |
19 |
20 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
21 |
21 |
|
22 import contextlib |
|
23 import json |
22 import sys |
24 import sys |
23 from time import clock, time |
25 from time import clock, time |
24 from contextlib import contextmanager |
26 from contextlib import contextmanager |
25 from warnings import warn |
27 from warnings import warn |
26 import json |
|
27 |
28 |
28 from six import text_type, binary_type |
29 from six import text_type, binary_type |
29 from six.moves import http_client |
30 from six.moves import http_client |
30 |
31 |
31 from logilab.common.deprecation import deprecated |
|
32 |
|
33 from rql import BadRQLQuery |
32 from rql import BadRQLQuery |
34 |
33 |
35 from cubicweb import set_log_methods, cwvreg |
34 from cubicweb import set_log_methods |
36 from cubicweb import ( |
35 from cubicweb import ( |
37 ValidationError, Unauthorized, Forbidden, |
36 CW_EVENT_MANAGER, ValidationError, Unauthorized, Forbidden, |
38 AuthenticationError, NoSelectableObject, |
37 AuthenticationError, NoSelectableObject) |
39 CW_EVENT_MANAGER) |
|
40 from cubicweb.repoapi import anonymous_cnx |
38 from cubicweb.repoapi import anonymous_cnx |
41 from cubicweb.web import LOGGER, component, cors |
39 from cubicweb.web import cors |
42 from cubicweb.web import ( |
40 from cubicweb.web import ( |
43 StatusResponse, DirectResponse, Redirect, NotFound, LogOut, |
41 LOGGER, StatusResponse, DirectResponse, Redirect, NotFound, LogOut, |
44 RemoteCallFailed, InvalidSession, RequestError, PublishException) |
42 RemoteCallFailed, InvalidSession, RequestError, PublishException) |
45 |
|
46 from cubicweb.web.request import CubicWebRequestBase |
43 from cubicweb.web.request import CubicWebRequestBase |
47 |
44 |
48 # make session manager available through a global variable so the debug view can |
45 # make session manager available through a global variable so the debug view can |
49 # print information about web session |
46 # print information about web session |
50 SESSION_MANAGER = None |
47 SESSION_MANAGER = None |
120 cookie = req.get_cookie() |
116 cookie = req.get_cookie() |
121 sessioncookie = self.session_cookie(req) |
117 sessioncookie = self.session_cookie(req) |
122 try: |
118 try: |
123 sessionid = str(cookie[sessioncookie].value) |
119 sessionid = str(cookie[sessioncookie].value) |
124 session = self.get_session_by_id(req, sessionid) |
120 session = self.get_session_by_id(req, sessionid) |
125 except (KeyError, InvalidSession): # no valid session cookie |
121 except (KeyError, InvalidSession): # no valid session cookie |
126 session = self.open_session(req) |
122 session = self.open_session(req) |
127 return session |
123 return session |
128 |
124 |
129 def get_session_by_id(self, req, sessionid): |
125 def get_session_by_id(self, req, sessionid): |
130 session = self.session_manager.get_session(req, sessionid) |
126 session = self.session_manager.get_session(req, sessionid) |
149 req.remove_cookie(self.session_cookie(req)) |
145 req.remove_cookie(self.session_cookie(req)) |
150 raise LogOut(url=goto_url) |
146 raise LogOut(url=goto_url) |
151 |
147 |
152 # these are overridden by set_log_methods below |
148 # these are overridden by set_log_methods below |
153 # only defining here to prevent pylint from complaining |
149 # only defining here to prevent pylint from complaining |
154 info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None |
150 info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None |
|
151 |
155 |
152 |
156 class CubicWebPublisher(object): |
153 class CubicWebPublisher(object): |
157 """the publisher is a singleton hold by the web frontend, and is responsible |
154 """the publisher is a singleton hold by the web frontend, and is responsible |
158 to publish HTTP request. |
155 to publish HTTP request. |
159 |
156 |
204 def log_handle_request(self, req, path): |
201 def log_handle_request(self, req, path): |
205 """wrapper around _publish to log all queries executed for a given |
202 """wrapper around _publish to log all queries executed for a given |
206 accessed path |
203 accessed path |
207 """ |
204 """ |
208 def wrap_set_cnx(func): |
205 def wrap_set_cnx(func): |
|
206 |
209 def wrap_execute(cnx): |
207 def wrap_execute(cnx): |
210 orig_execute = cnx.execute |
208 orig_execute = cnx.execute |
|
209 |
211 def execute(rql, kwargs=None, build_descr=True): |
210 def execute(rql, kwargs=None, build_descr=True): |
212 tstart, cstart = time(), clock() |
211 tstart, cstart = time(), clock() |
213 rset = orig_execute(rql, kwargs, build_descr=build_descr) |
212 rset = orig_execute(rql, kwargs, build_descr=build_descr) |
214 cnx.executed_queries.append((rql, kwargs, time() - tstart, clock() - cstart)) |
213 cnx.executed_queries.append((rql, kwargs, time() - tstart, clock() - cstart)) |
215 return rset |
214 return rset |
|
215 |
216 return execute |
216 return execute |
|
217 |
217 def set_cnx(cnx): |
218 def set_cnx(cnx): |
218 func(cnx) |
219 func(cnx) |
219 cnx.execute = wrap_execute(cnx) |
220 cnx.execute = wrap_execute(cnx) |
220 cnx.executed_queries = [] |
221 cnx.executed_queries = [] |
|
222 |
221 return set_cnx |
223 return set_cnx |
|
224 |
222 req.set_cnx = wrap_set_cnx(req.set_cnx) |
225 req.set_cnx = wrap_set_cnx(req.set_cnx) |
223 try: |
226 try: |
224 return self.main_handle_request(req, path) |
227 return self.main_handle_request(req, path) |
225 finally: |
228 finally: |
226 cnx = req.cnx |
229 cnx = req.cnx |
227 if cnx: |
230 if cnx: |
228 with self._logfile_lock: |
231 with self._logfile_lock: |
229 try: |
232 try: |
230 result = ['\n'+'*'*80] |
233 result = ['\n' + '*' * 80] |
231 result.append(req.url()) |
234 result.append(req.url()) |
232 result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q |
235 result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q |
233 for q in cnx.executed_queries] |
236 for q in cnx.executed_queries] |
234 cnx.executed_queries = [] |
237 cnx.executed_queries = [] |
235 self._query_log.write('\n'.join(result)) |
238 self._query_log.write('\n'.join(result)) |
236 self._query_log.flush() |
239 self._query_log.flush() |
237 except Exception: |
240 except Exception: |
238 self.exception('error while logging queries') |
241 self.exception('error while logging queries') |
239 |
242 |
240 |
|
241 def main_handle_request(self, req, path): |
243 def main_handle_request(self, req, path): |
242 """Process an http request |
244 """Process an http request |
243 |
245 |
244 Arguments are: |
246 Arguments are: |
245 - a Request object |
247 - a Request object |
253 'not (path, req)', DeprecationWarning, 2) |
255 'not (path, req)', DeprecationWarning, 2) |
254 req, path = path, req |
256 req, path = path, req |
255 if req.authmode == 'http': |
257 if req.authmode == 'http': |
256 # activate realm-based auth |
258 # activate realm-based auth |
257 realm = self.vreg.config['realm'] |
259 realm = self.vreg.config['realm'] |
258 req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False) |
260 req.set_header('WWW-Authenticate', [('Basic', {'realm': realm})], raw=False) |
259 content = b'' |
261 content = b'' |
260 try: |
262 try: |
261 try: |
263 try: |
262 session = self.get_session(req) |
264 session = self.get_session(req) |
263 from cubicweb import repoapi |
265 from cubicweb import repoapi |
264 cnx = repoapi.Connection(session) |
266 cnx = repoapi.Connection(session) |
265 req.set_cnx(cnx) |
267 req.set_cnx(cnx) |
266 except AuthenticationError: |
268 except AuthenticationError: |
267 # Keep the dummy session set at initialisation. |
269 # Keep the dummy session set at initialisation. such session will work to some |
268 # such session with work to an some extend but raise an |
270 # extend but raise an AuthenticationError on any database access. |
269 # AuthenticationError on any database access. |
271 # XXX We want to clean up this approach in the future. But several cubes like |
270 import contextlib |
272 # registration or forgotten password rely on this principle. |
271 @contextlib.contextmanager |
273 @contextlib.contextmanager |
272 def dummy(): |
274 def dummy(): |
273 yield |
275 yield |
274 cnx = dummy() |
276 cnx = dummy() |
275 # XXX We want to clean up this approach in the future. But |
|
276 # several cubes like registration or forgotten password rely on |
|
277 # this principle. |
|
278 |
|
279 # nested try to allow LogOut to delegate logic to AuthenticationError |
277 # nested try to allow LogOut to delegate logic to AuthenticationError |
280 # handler |
278 # handler |
281 try: |
279 try: |
282 ### Try to generate the actual request content |
280 # Try to generate the actual request content |
283 with cnx: |
281 with cnx: |
284 content = self.core_handle(req, path) |
282 content = self.core_handle(req, path) |
285 # Handle user log-out |
283 # Handle user log-out |
286 except LogOut as ex: |
284 except LogOut as ex: |
287 # When authentification is handled by cookie the code that |
285 # When authentification is handled by cookie the code that |
382 if req.cnx: |
379 if req.cnx: |
383 txuuid = req.cnx.commit() |
380 txuuid = req.cnx.commit() |
384 commited = True |
381 commited = True |
385 if txuuid is not None: |
382 if txuuid is not None: |
386 req.data['last_undoable_transaction'] = txuuid |
383 req.data['last_undoable_transaction'] = txuuid |
387 ### error case |
384 # error case |
388 except NotFound as ex: |
385 except NotFound as ex: |
389 result = self.notfound_content(req) |
386 result = self.notfound_content(req) |
390 req.status_out = ex.status |
387 req.status_out = ex.status |
391 except ValidationError as ex: |
388 except ValidationError as ex: |
392 result = self.validation_error_handler(req, ex) |
389 result = self.validation_error_handler(req, ex) |
393 except RemoteCallFailed as ex: |
390 except RemoteCallFailed as ex: |
394 result = self.ajax_error_handler(req, ex) |
391 result = self.ajax_error_handler(req, ex) |
395 except Unauthorized as ex: |
392 except Unauthorized as ex: |
396 req.data['errmsg'] = req._('You\'re not authorized to access this page. ' |
393 req.data['errmsg'] = req._( |
397 'If you think you should, please contact the site administrator.') |
394 'You\'re not authorized to access this page. ' |
|
395 'If you think you should, please contact the site administrator.') |
398 req.status_out = http_client.FORBIDDEN |
396 req.status_out = http_client.FORBIDDEN |
399 result = self.error_handler(req, ex, tb=False) |
397 result = self.error_handler(req, ex, tb=False) |
400 except Forbidden as ex: |
398 except Forbidden as ex: |
401 req.data['errmsg'] = req._('This action is forbidden. ' |
399 req.data['errmsg'] = req._( |
402 'If you think it should be allowed, please contact the site administrator.') |
400 'This action is forbidden. ' |
|
401 'If you think it should be allowed, please contact the site administrator.') |
403 req.status_out = http_client.FORBIDDEN |
402 req.status_out = http_client.FORBIDDEN |
404 result = self.error_handler(req, ex, tb=False) |
403 result = self.error_handler(req, ex, tb=False) |
405 except (BadRQLQuery, RequestError) as ex: |
404 except (BadRQLQuery, RequestError) as ex: |
406 result = self.error_handler(req, ex, tb=False) |
405 result = self.error_handler(req, ex, tb=False) |
407 ### pass through exception |
406 # pass through exception |
408 except DirectResponse: |
407 except DirectResponse: |
409 if req.cnx: |
408 if req.cnx: |
410 req.cnx.commit() |
409 req.cnx.commit() |
411 raise |
410 raise |
412 except (AuthenticationError, LogOut): |
411 except (AuthenticationError, LogOut): |
413 # the rollback is handled in the finally |
412 # the rollback is handled in the finally |
414 raise |
413 raise |
415 ### Last defense line |
414 # Last defense line |
416 except BaseException as ex: |
415 except BaseException as ex: |
417 req.status_out = http_client.INTERNAL_SERVER_ERROR |
416 req.status_out = http_client.INTERNAL_SERVER_ERROR |
418 result = self.error_handler(req, ex, tb=True) |
417 result = self.error_handler(req, ex, tb=True) |
419 finally: |
418 finally: |
420 if req.cnx and not commited: |
419 if req.cnx and not commited: |
421 try: |
420 try: |
422 req.cnx.rollback() |
421 req.cnx.rollback() |
423 except Exception: |
422 except Exception: |
424 pass # ignore rollback error at this point |
423 pass # ignore rollback error at this point |
425 self.add_undo_link_to_msg(req) |
424 self.add_undo_link_to_msg(req) |
426 self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart) |
425 self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart) |
427 return result |
426 return result |
428 |
427 |
429 # Error handlers |
428 # Error handlers |
439 assert 300 <= ex.status < 400 |
438 assert 300 <= ex.status < 400 |
440 req.status_out = ex.status |
439 req.status_out = ex.status |
441 return b'' |
440 return b'' |
442 |
441 |
443 def validation_error_handler(self, req, ex): |
442 def validation_error_handler(self, req, ex): |
444 ex.translate(req._) # translate messages using ui language |
443 ex.translate(req._) # translate messages using ui language |
445 if '__errorurl' in req.form: |
444 if '__errorurl' in req.form: |
446 forminfo = {'error': ex, |
445 forminfo = {'error': ex, |
447 'values': req.form, |
446 'values': req.form, |
448 'eidmap': req.data.get('eidmap', {}) |
447 'eidmap': req.data.get('eidmap', {}) |
449 } |
448 } |
484 return content |
483 return content |
485 |
484 |
486 def add_undo_link_to_msg(self, req): |
485 def add_undo_link_to_msg(self, req): |
487 txuuid = req.data.get('last_undoable_transaction') |
486 txuuid = req.data.get('last_undoable_transaction') |
488 if txuuid is not None: |
487 if txuuid is not None: |
489 msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %( |
488 msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' % ( |
490 req.build_url('undo', txuuid=txuuid), req._('undo')) |
489 req.build_url('undo', txuuid=txuuid), req._('undo')) |
491 req.append_to_redirect_message(msg) |
490 req.append_to_redirect_message(msg) |
492 |
491 |
493 def ajax_error_handler(self, req, ex): |
492 def ajax_error_handler(self, req, ex): |
494 req.set_header('content-type', 'application/json') |
493 req.set_header('content-type', 'application/json') |
495 status = http_client.INTERNAL_SERVER_ERROR |
494 status = http_client.INTERNAL_SERVER_ERROR |
496 if isinstance(ex, PublishException) and ex.status is not None: |
495 if isinstance(ex, PublishException) and ex.status is not None: |
497 status = ex.status |
496 status = ex.status |
498 if req.status_out < 400: |
497 if req.status_out < 400: |
499 # don't overwrite it if it's already set |
498 # don't overwrite it if it's already set |
500 req.status_out = status |
499 req.status_out = status |
501 json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': text_type(ex)})) |
500 json_dumper = getattr(ex, 'dumps', lambda: json.dumps({'reason': text_type(ex)})) |
502 return json_dumper().encode('utf-8') |
501 return json_dumper().encode('utf-8') |
503 |
502 |
504 # special case handling |
503 # special case handling |
505 |
504 |
506 def need_login_content(self, req): |
505 def need_login_content(self, req): |