cubicweb/web/application.py
changeset 11348 70337ad23145
parent 11163 141e96f93c4d
child 11699 b48020a80dc3
equal deleted inserted replaced
11347:b4dcfd734686 11348:70337ad23145
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    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
    58     try:
    55     try:
    59         with anon_cnx:
    56         with anon_cnx:
    60             yield req
    57             yield req
    61     finally:
    58     finally:
    62         req.set_cnx(orig_cnx)
    59         req.set_cnx(orig_cnx)
    63 
       
    64 
    60 
    65 
    61 
    66 class CookieSessionHandler(object):
    62 class CookieSessionHandler(object):
    67     """a session handler using a cookie to store the session identifier"""
    63     """a session handler using a cookie to store the session identifier"""
    68 
    64 
   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
   328                 if not content:
   326                 if not content:
   329                     content = self.need_login_content(req)
   327                     content = self.need_login_content(req)
   330         assert isinstance(content, binary_type)
   328         assert isinstance(content, binary_type)
   331         return content
   329         return content
   332 
   330 
   333 
       
   334     def core_handle(self, req, path):
   331     def core_handle(self, req, path):
   335         """method called by the main publisher to process <path>
   332         """method called by the main publisher to process <path>
   336 
   333 
   337         should return a string containing the resulting page or raise a
   334         should return a string containing the resulting page or raise a
   338         `NotFound` exception
   335         `NotFound` exception
   352         # remove user callbacks on a new request (except for json controllers
   349         # remove user callbacks on a new request (except for json controllers
   353         # to avoid callbacks being unregistered before they could be called)
   350         # to avoid callbacks being unregistered before they could be called)
   354         tstart = clock()
   351         tstart = clock()
   355         commited = False
   352         commited = False
   356         try:
   353         try:
   357             ### standard processing of the request
   354             # standard processing of the request
   358             try:
   355             try:
   359                 # apply CORS sanity checks
   356                 # apply CORS sanity checks
   360                 cors.process_request(req, self.vreg.config)
   357                 cors.process_request(req, self.vreg.config)
   361                 ctrlid, rset = self.url_resolver.process(req, path)
   358                 ctrlid, rset = self.url_resolver.process(req, path)
   362                 try:
   359                 try:
   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):
   523             template = 'main-template'
   522             template = 'main-template'
   524         return template
   523         return template
   525 
   524 
   526     # these are overridden by set_log_methods below
   525     # these are overridden by set_log_methods below
   527     # only defining here to prevent pylint from complaining
   526     # only defining here to prevent pylint from complaining
   528     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
   527     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
       
   528 
   529 
   529 
   530 set_log_methods(CubicWebPublisher, LOGGER)
   530 set_log_methods(CubicWebPublisher, LOGGER)
   531 set_log_methods(CookieSessionHandler, LOGGER)
   531 set_log_methods(CookieSessionHandler, LOGGER)