web/application.py
branchstable
changeset 8463 a964c40adbe3
parent 8443 86fc11fb6f99
child 8466 92c668170ef9
equal deleted inserted replaced
8461:8af7c6d86efb 8463:a964c40adbe3
    22 __docformat__ = "restructuredtext en"
    22 __docformat__ = "restructuredtext en"
    23 
    23 
    24 import sys
    24 import sys
    25 from time import clock, time
    25 from time import clock, time
    26 from contextlib import contextmanager
    26 from contextlib import contextmanager
       
    27 from warnings import warn
       
    28 
       
    29 import httplib
    27 
    30 
    28 from logilab.common.deprecation import deprecated
    31 from logilab.common.deprecation import deprecated
    29 
    32 
    30 from rql import BadRQLQuery
    33 from rql import BadRQLQuery
    31 
    34 
    36 from cubicweb.dbapi import DBAPISession, anonymous_session
    39 from cubicweb.dbapi import DBAPISession, anonymous_session
    37 from cubicweb.web import LOGGER, component
    40 from cubicweb.web import LOGGER, component
    38 from cubicweb.web import (
    41 from cubicweb.web import (
    39     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
    42     StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
    40     RemoteCallFailed, InvalidSession, RequestError)
    43     RemoteCallFailed, InvalidSession, RequestError)
       
    44 
       
    45 from cubicweb.web.request import CubicWebRequestBase
    41 
    46 
    42 # make session manager available through a global variable so the debug view can
    47 # make session manager available through a global variable so the debug view can
    43 # print information about web session
    48 # print information about web session
    44 SESSION_MANAGER = None
    49 SESSION_MANAGER = None
    45 
    50 
   274     def __init__(self, config,
   279     def __init__(self, config,
   275                  session_handler_fact=CookieSessionHandler,
   280                  session_handler_fact=CookieSessionHandler,
   276                  vreg=None):
   281                  vreg=None):
   277         self.info('starting web instance from %s', config.apphome)
   282         self.info('starting web instance from %s', config.apphome)
   278         if vreg is None:
   283         if vreg is None:
   279             vreg = cwvreg.CubicWebVRegistry(config)
   284             vreg = cwvreg.CWRegistryStore(config)
   280         self.vreg = vreg
   285         self.vreg = vreg
   281         # connect to the repository and get instance's schema
   286         # connect to the repository and get instance's schema
   282         self.repo = config.repository(vreg)
   287         self.repo = config.repository(vreg)
   283         if not vreg.initialized:
   288         if not vreg.initialized:
   284             config.init_cubes(self.repo.get_cubes())
   289             config.init_cubes(self.repo.get_cubes())
   286             vreg.set_schema(self.repo.get_schema())
   291             vreg.set_schema(self.repo.get_schema())
   287         # set the correct publish method
   292         # set the correct publish method
   288         if config['query-log-file']:
   293         if config['query-log-file']:
   289             from threading import Lock
   294             from threading import Lock
   290             self._query_log = open(config['query-log-file'], 'a')
   295             self._query_log = open(config['query-log-file'], 'a')
   291             self.publish = self.log_publish
   296             self.handle_request = self.log_handle_request
   292             self._logfile_lock = Lock()
   297             self._logfile_lock = Lock()
   293         else:
   298         else:
   294             self._query_log = None
   299             self._query_log = None
   295             self.publish = self.main_publish
   300             self.handle_request = self.main_handle_request
   296         # instantiate session and url resolving helpers
   301         # instantiate session and url resolving helpers
   297         self.session_handler = session_handler_fact(self)
   302         self.session_handler = session_handler_fact(self)
   298         self.set_urlresolver()
   303         self.set_urlresolver()
   299         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_urlresolver)
   304         CW_EVENT_MANAGER.bind('after-registry-reload', self.set_urlresolver)
   300 
   305 
   309         """
   314         """
   310         self.session_handler.set_session(req)
   315         self.session_handler.set_session(req)
   311 
   316 
   312     # publish methods #########################################################
   317     # publish methods #########################################################
   313 
   318 
   314     def log_publish(self, path, req):
   319     def log_handle_request(self, req, path):
   315         """wrapper around _publish to log all queries executed for a given
   320         """wrapper around _publish to log all queries executed for a given
   316         accessed path
   321         accessed path
   317         """
   322         """
   318         try:
   323         try:
   319             return self.main_publish(path, req)
   324             return self.main_handle_request(req, path)
   320         finally:
   325         finally:
   321             cnx = req.cnx
   326             cnx = req.cnx
   322             if cnx:
   327             if cnx:
   323                 with self._logfile_lock:
   328                 with self._logfile_lock:
   324                     try:
   329                     try:
   330                         self._query_log.write('\n'.join(result).encode(req.encoding))
   335                         self._query_log.write('\n'.join(result).encode(req.encoding))
   331                         self._query_log.flush()
   336                         self._query_log.flush()
   332                     except Exception:
   337                     except Exception:
   333                         self.exception('error while logging queries')
   338                         self.exception('error while logging queries')
   334 
   339 
   335     def main_publish(self, path, req):
   340 
       
   341 
       
   342     def main_handle_request(self, req, path):
       
   343         if not isinstance(req, CubicWebRequestBase):
       
   344             warn('[3.15] Application entry poin arguments are now (req, path) '
       
   345                  'not (path, req)', DeprecationWarning, 2)
       
   346             req, path = path, req
       
   347         if req.authmode == 'http':
       
   348             # activate realm-based auth
       
   349             realm = self.vreg.config['realm']
       
   350             req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
       
   351         content = ''
       
   352         try:
       
   353             self.connect(req)
       
   354             # DENY https acces for anonymous_user
       
   355             if (req.https
       
   356                 and req.session.anonymous_session
       
   357                 and self.vreg.config['https-deny-anonymous']):
       
   358                 # don't allow anonymous on https connection
       
   359                 raise AuthenticationError()
       
   360             # nested try to allow LogOut to delegate logic to AuthenticationError
       
   361             # handler
       
   362             try:
       
   363                 ### Try to generate the actual request content
       
   364                 content = self.core_handle(req, path)
       
   365             # Handle user log-out
       
   366             except LogOut, ex:
       
   367                 # When authentification is handled by cookie the code that
       
   368                 # raised LogOut must has invalidated the cookie. We can just
       
   369                 # reload the original url without authentification
       
   370                 if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
       
   371                     req.headers_out.setHeader('location', str(ex.url))
       
   372                 if ex.status is not None:
       
   373                     req.status_out = httplib.SEE_OTHER
       
   374                 # When the authentification is handled by http we must
       
   375                 # explicitly ask for authentification to flush current http
       
   376                 # authentification information
       
   377                 else:
       
   378                     # Render "logged out" content.
       
   379                     # assignement to ``content`` prevent standard
       
   380                     # AuthenticationError code to overwrite it.
       
   381                     content = self.loggedout_content(req)
       
   382                     # let the explicitly reset http credential
       
   383                     raise AuthenticationError()
       
   384         # Wrong, absent or Reseted credential
       
   385         except AuthenticationError:
       
   386             # If there is an https url configured and
       
   387             # the request do not used https, redirect to login form
       
   388             https_url = self.vreg.config['https-url']
       
   389             if https_url and req.base_url() != https_url:
       
   390                 req.status_out = httplib.SEE_OTHER
       
   391                 req.headers_out.setHeader('location', https_url + 'login')
       
   392             else:
       
   393                 # We assume here that in http auth mode the user *May* provide
       
   394                 # Authentification Credential if asked kindly.
       
   395                 if self.vreg.config['auth-mode'] == 'http':
       
   396                     req.status_out = httplib.UNAUTHORIZED
       
   397                 # In the other case (coky auth) we assume that there is no way
       
   398                 # for the user to provide them...
       
   399                 # XXX But WHY ?
       
   400                 else:
       
   401                     req.status_out = httplib.FORBIDDEN
       
   402                 # If previous error handling already generated a custom content
       
   403                 # do not overwrite it. This is used by LogOut Except
       
   404                 # XXX ensure we don't actually serve content
       
   405                 if not content:
       
   406                     content = self.need_login_content(req)
       
   407         return content
       
   408 
       
   409 
       
   410 
       
   411     def core_handle(self, req, path):
   336         """method called by the main publisher to process <path>
   412         """method called by the main publisher to process <path>
   337 
   413 
   338         should return a string containing the resulting page or raise a
   414         should return a string containing the resulting page or raise a
   339         `NotFound` exception
   415         `NotFound` exception
   340 
   416 
   345         :param req: the request object
   421         :param req: the request object
   346 
   422 
   347         :rtype: str
   423         :rtype: str
   348         :return: the result of the pusblished url
   424         :return: the result of the pusblished url
   349         """
   425         """
   350         path = path or 'view'
       
   351         # don't log form values they may contains sensitive information
   426         # don't log form values they may contains sensitive information
   352         self.info('publish "%s" (%s, form params: %s)',
   427         self.debug('publish "%s" (%s, form params: %s)',
   353                   path, req.session.sessionid, req.form.keys())
   428                    path, req.session.sessionid, req.form.keys())
   354         # remove user callbacks on a new request (except for json controllers
   429         # remove user callbacks on a new request (except for json controllers
   355         # to avoid callbacks being unregistered before they could be called)
   430         # to avoid callbacks being unregistered before they could be called)
   356         tstart = clock()
   431         tstart = clock()
   357         commited = False
   432         commited = False
   358         try:
   433         try:
       
   434             ### standard processing of the request
   359             try:
   435             try:
   360                 ctrlid, rset = self.url_resolver.process(req, path)
   436                 ctrlid, rset = self.url_resolver.process(req, path)
   361                 try:
   437                 try:
   362                     controller = self.vreg['controllers'].select(ctrlid, req,
   438                     controller = self.vreg['controllers'].select(ctrlid, req,
   363                                                                  appli=self)
   439                                                                  appli=self)
   364                 except NoSelectableObject:
   440                 except NoSelectableObject:
   365                     if ctrlid == 'login':
       
   366                         raise Unauthorized(req._('log out first'))
       
   367                     raise Unauthorized(req._('not authorized'))
   441                     raise Unauthorized(req._('not authorized'))
   368                 req.update_search_state()
   442                 req.update_search_state()
   369                 result = controller.publish(rset=rset)
   443                 result = controller.publish(rset=rset)
   370                 if req.cnx:
   444             except StatusResponse, ex:
   371                     # no req.cnx if anonymous aren't allowed and we are
   445                 warn('StatusResponse is deprecated use req.status_out',
   372                     # displaying some anonymous enabled view such as the cookie
   446                      DeprecationWarning)
   373                     # authentication form
   447                 result = ex.content
   374                     req.cnx.commit()
   448                 req.status_out = ex.status
   375                     commited = True
   449             except Redirect, ex:
   376             except (StatusResponse, DirectResponse):
   450                 # handle redirect
   377                 if req.cnx:
   451                 # - comply to ex status
   378                     req.cnx.commit()
   452                 # - set header field
   379                 raise
   453                 #
   380             except (AuthenticationError, LogOut):
   454                 # Redirect Maybe be is raised by edit controller when
   381                 raise
   455                 # everything went fine, so try to commit
   382             except Redirect:
   456                 self.debug('redirecting to %s', str(ex.location))
   383                 # redirect is raised by edit controller when everything went fine,
   457                 req.headers_out.setHeader('location', str(ex.location))
   384                 # so try to commit
   458                 assert 300<= ex.status < 400
   385                 try:
   459                 req.status_out = ex.status
   386                     if req.cnx:
   460                 result = ''
   387                         txuuid = req.cnx.commit()
   461             if req.cnx:
   388                         if txuuid is not None:
   462                 txuuid = req.cnx.commit()
   389                             msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
   463                 commited = True
   390                                 req.build_url('undo', txuuid=txuuid), req._('undo'))
   464                 if txuuid is not None:
   391                             req.append_to_redirect_message(msg)
   465                     req.data['last_undoable_transaction'] = txuuid
   392                 except ValidationError, ex:
   466         ### error case
   393                     self.validation_error_handler(req, ex)
   467         except NotFound, ex:
   394                 except Unauthorized, ex:
   468             result = self.notfound_content(req)
   395                     req.data['errmsg'] = req._('You\'re not authorized to access this page. '
   469             req.status_out = ex.status
   396                                                'If you think you should, please contact the site administrator.')
   470         except ValidationError, ex:
   397                     self.error_handler(req, ex, tb=False)
   471             req.status_out = httplib.CONFLICT
   398                 except Exception, ex:
   472             result = self.validation_error_handler(req, ex)
   399                     self.error_handler(req, ex, tb=True)
   473         except RemoteCallFailed, ex:
   400                 else:
   474             result = self.ajax_error_handler(req, ex)
   401                     # delete validation errors which may have been previously set
   475         except Unauthorized, ex:
   402                     if '__errorurl' in req.form:
   476             req.data['errmsg'] = req._('You\'re not authorized to access this page. '
   403                         req.session.data.pop(req.form['__errorurl'], None)
   477                                        'If you think you should, please contact the site administrator.')
   404                     raise
   478             req.status_out = httplib.UNAUTHORIZED
   405             except RemoteCallFailed, ex:
   479             result = self.error_handler(req, ex, tb=False)
   406                 req.set_header('content-type', 'application/json')
   480         except (BadRQLQuery, RequestError), ex:
   407                 raise StatusResponse(500, ex.dumps())
   481             result = self.error_handler(req, ex, tb=False)
   408             except NotFound:
   482         ### pass through exception
   409                 raise StatusResponse(404, self.notfound_content(req))
   483         except DirectResponse:
   410             except ValidationError, ex:
   484             if req.cnx:
   411                 self.validation_error_handler(req, ex)
   485                 req.cnx.commit()
   412             except Unauthorized, ex:
   486             raise
   413                 self.error_handler(req, ex, tb=False, code=403)
   487         except (AuthenticationError, LogOut):
   414             except (BadRQLQuery, RequestError), ex:
   488             # the rollback is handled in the finally
   415                 self.error_handler(req, ex, tb=False)
   489             raise
   416             except BaseException, ex:
   490         ### Last defence line
   417                 self.error_handler(req, ex, tb=True)
   491         except BaseException, ex:
   418             except:
   492             result = self.error_handler(req, ex, tb=True)
   419                 self.critical('Catch all triggered!!!')
       
   420                 self.exception('this is what happened')
       
   421                 result = 'oops'
       
   422         finally:
   493         finally:
   423             if req.cnx and not commited:
   494             if req.cnx and not commited:
   424                 try:
   495                 try:
   425                     req.cnx.rollback()
   496                     req.cnx.rollback()
   426                 except Exception:
   497                 except Exception:
   427                     pass # ignore rollback error at this point
   498                     pass # ignore rollback error at this point
   428         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
   499             # request may be referenced by "onetime callback", so clear its entity
       
   500             # cache to avoid memory usage
       
   501             req.drop_entity_cache()
       
   502         self.add_undo_link_to_msg(req)
       
   503         self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
   429         return result
   504         return result
   430 
   505 
       
   506     ### Error handler
   431     def validation_error_handler(self, req, ex):
   507     def validation_error_handler(self, req, ex):
   432         ex.errors = dict((k, v) for k, v in ex.errors.items())
   508         ex.errors = dict((k, v) for k, v in ex.errors.items())
   433         if '__errorurl' in req.form:
   509         if '__errorurl' in req.form:
   434             forminfo = {'error': ex,
   510             forminfo = {'error': ex,
   435                         'values': req.form,
   511                         'values': req.form,
   438             req.session.data[req.form['__errorurl']] = forminfo
   514             req.session.data[req.form['__errorurl']] = forminfo
   439             # XXX form session key / __error_url should be differentiated:
   515             # XXX form session key / __error_url should be differentiated:
   440             # session key is 'url + #<form dom id', though we usually don't want
   516             # session key is 'url + #<form dom id', though we usually don't want
   441             # the browser to move to the form since it hides the global
   517             # the browser to move to the form since it hides the global
   442             # messages.
   518             # messages.
   443             raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
   519             location = req.form['__errorurl'].rsplit('#', 1)[0]
   444         self.error_handler(req, ex, tb=False)
   520             req.headers_out.setHeader('location', str(location))
   445 
   521             req.status_out = httplib.SEE_OTHER
   446     def error_handler(self, req, ex, tb=False, code=500):
   522             return ''
       
   523         return self.error_handler(req, ex, tb=False)
       
   524 
       
   525     def error_handler(self, req, ex, tb=False):
   447         excinfo = sys.exc_info()
   526         excinfo = sys.exc_info()
   448         self.exception(repr(ex))
   527         self.exception(repr(ex))
   449         req.set_header('Cache-Control', 'no-cache')
   528         req.set_header('Cache-Control', 'no-cache')
   450         req.remove_header('Etag')
   529         req.remove_header('Etag')
   451         req.reset_message()
   530         req.reset_message()
   452         req.reset_headers()
   531         req.reset_headers()
   453         if req.json_request:
   532         if req.ajax_request:
   454             raise RemoteCallFailed(unicode(ex))
   533             return ajax_error_handler(req, ex)
   455         try:
   534         try:
   456             req.data['ex'] = ex
   535             req.data['ex'] = ex
   457             if tb:
   536             if tb:
   458                 req.data['excinfo'] = excinfo
   537                 req.data['excinfo'] = excinfo
   459             req.form['vid'] = 'error'
   538             req.form['vid'] = 'error'
   460             errview = self.vreg['views'].select('error', req)
   539             errview = self.vreg['views'].select('error', req)
   461             template = self.main_template_id(req)
   540             template = self.main_template_id(req)
   462             content = self.vreg['views'].main_template(req, template, view=errview)
   541             content = self.vreg['views'].main_template(req, template, view=errview)
   463         except Exception:
   542         except Exception:
   464             content = self.vreg['views'].main_template(req, 'error-template')
   543             content = self.vreg['views'].main_template(req, 'error-template')
   465         raise StatusResponse(code, content)
   544         if getattr(ex, 'status', None) is not None:
       
   545             req.status_out = ex.status
       
   546         return content
       
   547 
       
   548     def add_undo_link_to_msg(self, req):
       
   549         txuuid = req.data.get('last_undoable_transaction')
       
   550         if txuuid is not None:
       
   551             msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
       
   552             req.build_url('undo', txuuid=txuuid), req._('undo'))
       
   553             req.append_to_redirect_message(msg)
       
   554 
       
   555 
       
   556 
       
   557     def ajax_error_handler(self, req, ex):
       
   558         req.set_header('content-type', 'application/json')
       
   559         status = ex.status
       
   560         if status is None:
       
   561             status = httplib.INTERNAL_SERVER_ERROR
       
   562         json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
       
   563         req.status_out = status
       
   564         return json_dumper()
       
   565 
       
   566     # special case handling
   466 
   567 
   467     def need_login_content(self, req):
   568     def need_login_content(self, req):
   468         return self.vreg['views'].main_template(req, 'login')
   569         return self.vreg['views'].main_template(req, 'login')
   469 
   570 
   470     def loggedout_content(self, req):
   571     def loggedout_content(self, req):
   474         req.form['vid'] = '404'
   575         req.form['vid'] = '404'
   475         view = self.vreg['views'].select('404', req)
   576         view = self.vreg['views'].select('404', req)
   476         template = self.main_template_id(req)
   577         template = self.main_template_id(req)
   477         return self.vreg['views'].main_template(req, template, view=view)
   578         return self.vreg['views'].main_template(req, template, view=view)
   478 
   579 
       
   580     # template stuff
       
   581 
   479     def main_template_id(self, req):
   582     def main_template_id(self, req):
   480         template = req.form.get('__template', req.property_value('ui.main-template'))
   583         template = req.form.get('__template', req.property_value('ui.main-template'))
   481         if template not in self.vreg['views']:
   584         if template not in self.vreg['views']:
   482             template = 'main-template'
   585             template = 'main-template'
   483         return template
   586         return template