web/application.py
changeset 0 b97547f5f1fa
child 168 f6be0c92fc38
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """CubicWeb web client application object
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import sys
       
    10 from time import clock, time
       
    11 
       
    12 from rql import BadRQLQuery
       
    13 
       
    14 from cubicweb import set_log_methods
       
    15 from cubicweb import (ValidationError, Unauthorized, AuthenticationError,
       
    16                    NoSelectableObject, RepositoryError)
       
    17 from cubicweb.cwconfig import CubicWebConfiguration
       
    18 from cubicweb.cwvreg import CubicWebRegistry
       
    19 from cubicweb.web import (LOGGER, StatusResponse, DirectResponse, Redirect, NotFound,
       
    20                        RemoteCallFailed, ExplicitLogin, InvalidSession)
       
    21 from cubicweb.web.component import SingletonComponent
       
    22 
       
    23 # make session manager available through a global variable so the debug view can
       
    24 # print information about web session
       
    25 SESSION_MANAGER = None
       
    26 
       
    27 class AbstractSessionManager(SingletonComponent):
       
    28     """manage session data associated to a session identifier"""
       
    29     id = 'sessionmanager'
       
    30     
       
    31     def __init__(self):
       
    32         self.session_time = self.vreg.config['http-session-time'] or None
       
    33         assert self.session_time is None or self.session_time > 0
       
    34         self.cleanup_session_time = self.vreg.config['cleanup-session-time'] or 120
       
    35         assert self.cleanup_session_time > 0
       
    36         self.cleanup_anon_session_time = self.vreg.config['cleanup-anonymous-session-time'] or 720
       
    37         assert self.cleanup_anon_session_time > 0
       
    38         if self.session_time:
       
    39             assert self.cleanup_session_time < self.session_time
       
    40             assert self.cleanup_anon_session_time < self.session_time
       
    41         self.authmanager = self.vreg.select_component('authmanager')
       
    42         assert self.authmanager, 'no authentication manager found'
       
    43         
       
    44     def clean_sessions(self):
       
    45         """cleanup sessions which has not been unused since a given amount of
       
    46         time. Return the number of sessions which have been closed.
       
    47         """
       
    48         self.debug('cleaning http sessions')
       
    49         closed, total = 0, 0
       
    50         for session in self.current_sessions():
       
    51             no_use_time = (time() - session.last_usage_time)
       
    52             total += 1
       
    53             if session.anonymous_connection:
       
    54                 if no_use_time >= self.cleanup_anon_session_time:
       
    55                     self.close_session(session)
       
    56                     closed += 1
       
    57             elif no_use_time >= self.cleanup_session_time:
       
    58                 self.close_session(session)
       
    59                 closed += 1
       
    60         return closed, total - closed
       
    61     
       
    62     def has_expired(self, session):
       
    63         """return True if the web session associated to the session is expired
       
    64         """
       
    65         return not (self.session_time is None or
       
    66                     time() < session.last_usage_time + self.session_time)
       
    67                 
       
    68     def current_sessions(self):
       
    69         """return currently open sessions"""
       
    70         raise NotImplementedError()
       
    71             
       
    72     def get_session(self, req, sessionid):
       
    73         """return existing session for the given session identifier"""
       
    74         raise NotImplementedError()
       
    75 
       
    76     def open_session(self, req):
       
    77         """open and return a new session for the given request
       
    78         
       
    79         :raise ExplicitLogin: if authentication is required
       
    80         """
       
    81         raise NotImplementedError()
       
    82     
       
    83     def close_session(self, session):
       
    84         """close session on logout or on invalid session detected (expired out,
       
    85         corrupted...)
       
    86         """
       
    87         raise NotImplementedError()
       
    88 
       
    89 
       
    90 class AbstractAuthenticationManager(SingletonComponent):
       
    91     """authenticate user associated to a request and check session validity"""
       
    92     id = 'authmanager'
       
    93 
       
    94     def authenticate(self, req):
       
    95         """authenticate user and return corresponding user object
       
    96         
       
    97         :raise ExplicitLogin: if authentication is required (no authentication
       
    98         info found or wrong user/password)
       
    99         """
       
   100         raise NotImplementedError()
       
   101 
       
   102     
       
   103 class CookieSessionHandler(object):
       
   104     """a session handler using a cookie to store the session identifier
       
   105 
       
   106     :cvar SESSION_VAR:
       
   107       string giving the name of the variable used to store the session
       
   108       identifier
       
   109     """
       
   110     SESSION_VAR = '__session'
       
   111     
       
   112     def __init__(self, appli):
       
   113         self.session_manager = appli.vreg.select_component('sessionmanager')
       
   114         assert self.session_manager, 'no session manager found'
       
   115         global SESSION_MANAGER
       
   116         SESSION_MANAGER = self.session_manager
       
   117         if not 'last_login_time' in appli.vreg.schema:
       
   118             self._update_last_login_time = lambda x: None
       
   119 
       
   120     def clean_sessions(self):
       
   121         """cleanup sessions which has not been unused since a given amount of
       
   122         time
       
   123         """
       
   124         self.session_manager.clean_sessions()
       
   125         
       
   126     def set_session(self, req):
       
   127         """associate a session to the request
       
   128 
       
   129         Session id is searched from :
       
   130         - # form variable
       
   131         - cookie
       
   132 
       
   133         if no session id is found, open a new session for the connected user
       
   134         or request authentification as needed
       
   135 
       
   136         :raise Redirect: if authentication has occured and succeed        
       
   137         """
       
   138         assert req.cnx is None # at this point no cnx should be set on the request
       
   139         cookie = req.get_cookie()
       
   140         try:
       
   141             sessionid = str(cookie[self.SESSION_VAR].value)
       
   142         except KeyError: # no session cookie
       
   143             session = self.open_session(req)
       
   144         else:
       
   145             try:
       
   146                 session = self.get_session(req, sessionid)
       
   147             except InvalidSession:
       
   148                 try:
       
   149                     session = self.open_session(req)
       
   150                 except ExplicitLogin:
       
   151                     req.remove_cookie(cookie, self.SESSION_VAR)
       
   152                     raise
       
   153         # remember last usage time for web session tracking
       
   154         session.last_usage_time = time()
       
   155 
       
   156     def get_session(self, req, sessionid):
       
   157         return self.session_manager.get_session(req, sessionid)
       
   158     
       
   159     def open_session(self, req):
       
   160         session = self.session_manager.open_session(req)
       
   161         cookie = req.get_cookie()
       
   162         cookie[self.SESSION_VAR] = session.sessionid
       
   163         req.set_cookie(cookie, self.SESSION_VAR, maxage=None)
       
   164         # remember last usage time for web session tracking
       
   165         session.last_usage_time = time()
       
   166         if not session.anonymous_connection:
       
   167             self._postlogin(req)
       
   168         return session
       
   169 
       
   170     def _update_last_login_time(self, req):
       
   171         try:
       
   172             req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
       
   173                         {'x' : req.user.eid}, 'x')
       
   174             req.cnx.commit()
       
   175         except (RepositoryError, Unauthorized):
       
   176             # ldap user are not writeable for instance
       
   177             req.cnx.rollback()
       
   178         except:
       
   179             req.cnx.rollback()
       
   180             raise
       
   181         
       
   182     def _postlogin(self, req):
       
   183         """postlogin: the user has been authenticated, redirect to the original
       
   184         page (index by default) with a welcome message
       
   185         """
       
   186         # Update last connection date
       
   187         # XXX: this should be in a post login hook in the repository, but there
       
   188         #      we can't differentiate actual login of automatic session
       
   189         #      reopening. Is it actually a problem?
       
   190         self._update_last_login_time(req)
       
   191         args = req.form
       
   192         args['__message'] = req._('welcome %s !') % req.user.login
       
   193         if 'vid' in req.form:
       
   194             args['vid'] = req.form['vid']
       
   195         if 'rql' in req.form:
       
   196             args['rql'] = req.form['rql']
       
   197         path = req.relative_path(False)
       
   198         if path == 'login':
       
   199             path = 'view'
       
   200         raise Redirect(req.build_url(path, **args))
       
   201     
       
   202     def logout(self, req):
       
   203         """logout from the application by cleaning the session and raising
       
   204         `AuthenticationError`
       
   205         """
       
   206         self.session_manager.close_session(req.cnx)
       
   207         req.remove_cookie(req.get_cookie(), self.SESSION_VAR)
       
   208         raise AuthenticationError()
       
   209 
       
   210 
       
   211 class CubicWebPublisher(object):
       
   212     """Central registry for the web application. This is one of the central
       
   213     object in the web application, coupling dynamically loaded objects with
       
   214     the application's schema and the application's configuration objects.
       
   215     
       
   216     It specializes the VRegistry by adding some convenience methods to
       
   217     access to stored objects. Currently we have the following registries
       
   218     of objects known by the web application (library may use some others
       
   219     additional registries):
       
   220     * controllers, which are directly plugged into the application
       
   221       object to handle request publishing
       
   222     * views
       
   223     * templates
       
   224     * components
       
   225     * actions
       
   226     """
       
   227     
       
   228     def __init__(self, config, debug=None,
       
   229                  session_handler_fact=CookieSessionHandler,
       
   230                  vreg=None):
       
   231         super(CubicWebPublisher, self).__init__()
       
   232         # connect to the repository and get application's schema
       
   233         if vreg is None:
       
   234             vreg = CubicWebRegistry(config, debug=debug)
       
   235         self.vreg = vreg
       
   236         self.info('starting web application from %s', config.apphome)
       
   237         self.repo = config.repository(vreg)
       
   238         if not vreg.initialized:
       
   239             self.config.init_cubes(self.repo.get_cubes())
       
   240             vreg.init_properties(self.repo.properties())
       
   241         vreg.set_schema(self.repo.get_schema())
       
   242         # set the correct publish method
       
   243         if config['query-log-file']:
       
   244             from threading import Lock
       
   245             self._query_log = open(config['query-log-file'], 'a')
       
   246             self.publish = self.log_publish
       
   247             self._logfile_lock = Lock()            
       
   248         else:
       
   249             self._query_log = None
       
   250             self.publish = self.main_publish
       
   251         # instantiate session and url resolving helpers
       
   252         self.session_handler = session_handler_fact(self)
       
   253         self.url_resolver = vreg.select_component('urlpublisher')
       
   254     
       
   255     def connect(self, req):
       
   256         """return a connection for a logged user object according to existing
       
   257         sessions (i.e. a new connection may be created or an already existing
       
   258         one may be reused
       
   259         """
       
   260         self.session_handler.set_session(req)
       
   261 
       
   262     def select_controller(self, oid, req):
       
   263         """return the most specific view according to the resultset"""
       
   264         vreg = self.vreg
       
   265         try:
       
   266             return vreg.select(vreg.registry_objects('controllers', oid),
       
   267                                req=req, appli=self)
       
   268         except NoSelectableObject:
       
   269             raise Unauthorized(req._('not authorized'))
       
   270             
       
   271     # publish methods #########################################################
       
   272         
       
   273     def log_publish(self, path, req):
       
   274         """wrapper around _publish to log all queries executed for a given
       
   275         accessed path
       
   276         """
       
   277         try:
       
   278             return self.main_publish(path, req)
       
   279         finally:
       
   280             cnx = req.cnx
       
   281             self._logfile_lock.acquire()
       
   282             try:
       
   283                 try:
       
   284                     result = ['\n'+'*'*80]
       
   285                     result.append(req.url())
       
   286                     result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q for q in cnx.executed_queries]
       
   287                     cnx.executed_queries = []
       
   288                     self._query_log.write('\n'.join(result).encode(req.encoding))
       
   289                     self._query_log.flush()
       
   290                 except Exception:
       
   291                     self.exception('error while logging queries')
       
   292             finally:
       
   293                 self._logfile_lock.release()
       
   294 
       
   295     def main_publish(self, path, req):
       
   296         """method called by the main publisher to process <path>
       
   297         
       
   298         should return a string containing the resulting page or raise a
       
   299         `NotFound` exception
       
   300 
       
   301         :type path: str
       
   302         :param path: the path part of the url to publish
       
   303         
       
   304         :type req: `web.Request`
       
   305         :param req: the request object
       
   306 
       
   307         :rtype: str
       
   308         :return: the result of the pusblished url
       
   309         """
       
   310         path = path or 'view'
       
   311         # don't log form values they may contains sensitive information
       
   312         self.info('publish "%s" (form params: %s)', path, req.form.keys())
       
   313         # remove user callbacks on a new request (except for json controllers
       
   314         # to avoid callbacks being unregistered before they could be called)
       
   315         tstart = clock()
       
   316         try:
       
   317             try:
       
   318                 ctrlid, rset = self.url_resolver.process(req, path)
       
   319                 controller = self.select_controller(ctrlid, req)
       
   320                 result = controller.publish(rset=rset)
       
   321                 if req.cnx is not None:
       
   322                     # req.cnx is None if anonymous aren't allowed and we are
       
   323                     # displaying the cookie authentication form
       
   324                     req.cnx.commit()
       
   325             except (StatusResponse, DirectResponse):
       
   326                 req.cnx.commit()
       
   327                 raise
       
   328             except Redirect:
       
   329                 # redirect is raised by edit controller when everything went fine,
       
   330                 # so try to commit
       
   331                 try:
       
   332                     req.cnx.commit()
       
   333                 except ValidationError, ex:
       
   334                     self.validation_error_handler(req, ex)
       
   335                 except Unauthorized, ex:
       
   336                     req.data['errmsg'] = req._('You\'re not authorized to access this page. '
       
   337                                                'If you think you should, please contact the site administrator.')
       
   338                     self.error_handler(req, ex, tb=False)
       
   339                 except Exception, ex:
       
   340                     self.error_handler(req, ex, tb=True)
       
   341                 else:
       
   342                     # delete validation errors which may have been previously set
       
   343                     if '__errorurl' in req.form:
       
   344                         req.del_session_data(req.form['__errorurl'])
       
   345                     raise
       
   346             except (AuthenticationError, NotFound, RemoteCallFailed):
       
   347                 raise
       
   348             except ValidationError, ex:
       
   349                 self.validation_error_handler(req, ex)
       
   350             except (Unauthorized, BadRQLQuery), ex:
       
   351                 self.error_handler(req, ex, tb=False)
       
   352             except Exception, ex:
       
   353                 self.error_handler(req, ex, tb=True)
       
   354         finally:
       
   355             if req.cnx is not None:
       
   356                 try:
       
   357                     req.cnx.rollback()
       
   358                 except:
       
   359                     pass # ignore rollback error at this point
       
   360         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
       
   361         return result
       
   362 
       
   363     def validation_error_handler(self, req, ex):
       
   364         ex.errors = dict((k, v) for k, v in ex.errors.items())
       
   365         if '__errorurl' in req.form:
       
   366             forminfo = {'errors': ex,
       
   367                         'values': req.form,
       
   368                         'eidmap': req.data.get('eidmap', {})
       
   369                         }
       
   370             req.set_session_data(req.form['__errorurl'], forminfo)
       
   371             raise Redirect(req.form['__errorurl'])
       
   372         self.error_handler(req, ex, tb=False)
       
   373         
       
   374     def error_handler(self, req, ex, tb=False):
       
   375         excinfo = sys.exc_info()
       
   376         self.exception(repr(ex))
       
   377         req.set_header('Cache-Control', 'no-cache')
       
   378         req.remove_header('Etag')
       
   379         req.message = None
       
   380         req.reset_headers()
       
   381         try:
       
   382             req.data['ex'] = ex
       
   383             if tb:
       
   384                 req.data['excinfo'] = excinfo
       
   385             req.form['vid'] = 'error'
       
   386             content = self.vreg.main_template(req, 'main')
       
   387         except:
       
   388             content = self.vreg.main_template(req, 'error')
       
   389         raise StatusResponse(500, content)
       
   390     
       
   391     def need_login_content(self, req):
       
   392         return self.vreg.main_template(req, 'login')
       
   393     
       
   394     def loggedout_content(self, req):
       
   395         return self.vreg.main_template(req, 'loggedout')
       
   396     
       
   397     def notfound_content(self, req):
       
   398         template = req.property_value('ui.main-template') or 'main'
       
   399         req.form['vid'] = '404'
       
   400         return self.vreg.main_template(req, template)
       
   401 
       
   402 
       
   403 set_log_methods(CubicWebPublisher, LOGGER)
       
   404 set_log_methods(CookieSessionHandler, LOGGER)