web/application.py
changeset 8312 6c2119509fac
parent 8311 76a44a0d7f4b
child 8390 637b934bc742
equal deleted inserted replaced
8311:76a44a0d7f4b 8312:6c2119509fac
    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 
   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         try:
       
   352             self.connect(req)
       
   353             # DENY https acces for anonymous_user
       
   354             if (req.https
       
   355                 and req.session.anonymous_session
       
   356                 and self.vreg.config['https-deny-anonymous']):
       
   357                 # don't allow anonymous on https connection
       
   358                 raise AuthenticationError()
       
   359             content = ''
       
   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 
   353         # remove user callbacks on a new request (except for json controllers
   429         # remove user callbacks on a new request (except for json controllers
   354         # to avoid callbacks being unregistered before they could be called)
   430         # to avoid callbacks being unregistered before they could be called)
   355         tstart = clock()
   431         tstart = clock()
   356         commited = False
   432         commited = False
   357         try:
   433         try:
       
   434             ### standard processing of the request
   358             try:
   435             try:
   359                 ctrlid, rset = self.url_resolver.process(req, path)
   436                 ctrlid, rset = self.url_resolver.process(req, path)
   360                 try:
   437                 try:
   361                     controller = self.vreg['controllers'].select(ctrlid, req,
   438                     controller = self.vreg['controllers'].select(ctrlid, req,
   362                                                                  appli=self)
   439                                                                  appli=self)
   363                 except NoSelectableObject:
   440                 except NoSelectableObject:
   364                     raise Unauthorized(req._('not authorized'))
   441                     raise Unauthorized(req._('not authorized'))
   365                 req.update_search_state()
   442                 req.update_search_state()
   366                 result = controller.publish(rset=rset)
   443                 result = controller.publish(rset=rset)
   367                 if req.cnx:
   444             except StatusResponse, ex:
   368                     # no req.cnx if anonymous aren't allowed and we are
   445                 warn('StatusResponse is deprecated use req.status_out',
   369                     # displaying some anonymous enabled view such as the cookie
   446                      DeprecationWarning)
   370                     # authentication form
   447                 result = ex.content
   371                     txuuid = req.cnx.commit()
   448                 req.status_out = ex.status
   372                     if txuuid is not None:
   449             except Redirect, ex:
   373                         req.data['last_undoable_transaction'] = txuuid
   450                 # handle redirect
   374                     commited = True
   451                 # - comply to ex status
   375             except (StatusResponse, DirectResponse):
   452                 # - set header field
   376                 if req.cnx:
   453                 #
   377                     req.cnx.commit()
   454                 # Redirect Maybe be is raised by edit controller when
   378                 raise
   455                 # everything went fine, so try to commit
   379             except (AuthenticationError, LogOut):
   456                 self.debug('redirecting to %s', str(ex.location))
   380                 raise
   457                 req.headers_out.setHeader('location', str(ex.location))
   381             except Redirect:
   458                 assert 300<= ex.status < 400
   382                 # redirect is raised by edit controller when everything went fine,
   459                 req.status_out = ex.status
   383                 # so try to commit
   460                 result = ''
   384                 try:
   461             if req.cnx:
   385                     if req.cnx:
   462                 txuuid = req.cnx.commit()
   386                         txuuid = req.cnx.commit()
   463                 commited = True
   387                         if txuuid is not None:
   464                 if txuuid is not None:
   388                             req.data['last_undoable_transaction'] = txuuid
   465                     req.data['last_undoable_transaction'] = txuuid
   389                 except ValidationError, ex:
   466         ### error case
   390                     self.validation_error_handler(req, ex)
   467         except NotFound, ex:
   391                 except Unauthorized, ex:
   468             result = self.notfound_content(req)
   392                     req.data['errmsg'] = req._('You\'re not authorized to access this page. '
   469             req.status_out = ex.status
   393                                                'If you think you should, please contact the site administrator.')
   470         except ValidationError, ex:
   394                     self.error_handler(req, ex, tb=False)
   471             req.status_out = httplib.CONFLICT
   395                 except Exception, ex:
   472             result = self.validation_error_handler(req, ex)
   396                     self.error_handler(req, ex, tb=True)
   473         except RemoteCallFailed, ex:
   397                 else:
   474             result = self.ajax_error_handler(req, ex)
   398                     self.add_undo_link_to_msg(req)
   475         except Unauthorized, ex:
   399                     # delete validation errors which may have been previously set
   476             req.data['errmsg'] = req._('You\'re not authorized to access this page. '
   400                     if '__errorurl' in req.form:
   477                                        'If you think you should, please contact the site administrator.')
   401                         req.session.data.pop(req.form['__errorurl'], None)
   478             req.status_out = httplib.UNAUTHORIZED
   402                     raise
   479             result = self.error_handler(req, ex, tb=False)
   403             except RemoteCallFailed, ex:
   480         except (BadRQLQuery, RequestError), ex:
   404                 req.set_header('content-type', 'application/json')
   481             result = self.error_handler(req, ex, tb=False)
   405                 raise StatusResponse(500, ex.dumps())
   482         ### pass through exception
   406             except NotFound:
   483         except DirectResponse:
   407                 raise StatusResponse(404, self.notfound_content(req))
   484             if req.cnx:
   408             except ValidationError, ex:
   485                 req.cnx.commit()
   409                 self.validation_error_handler(req, ex)
   486             raise
   410             except Unauthorized, ex:
   487         except (AuthenticationError, LogOut):
   411                 self.error_handler(req, ex, tb=False, code=403)
   488             # the rollback is handled in the finally
   412             except (BadRQLQuery, RequestError), ex:
   489             raise
   413                 self.error_handler(req, ex, tb=False)
   490         ### Last defence line
   414             except BaseException, ex:
   491         except BaseException, ex:
   415                 self.error_handler(req, ex, tb=True)
   492             result = self.error_handler(req, ex, tb=True)
   416             except:
       
   417                 self.critical('Catch all triggered!!!')
       
   418                 self.exception('this is what happened')
       
   419                 result = 'oops'
       
   420         finally:
   493         finally:
   421             if req.cnx and not commited:
   494             if req.cnx and not commited:
   422                 try:
   495                 try:
   423                     req.cnx.rollback()
   496                     req.cnx.rollback()
   424                 except Exception:
   497                 except Exception:
   425                     pass # ignore rollback error at this point
   498                     pass # ignore rollback error at this point
       
   499             # request may be referenced by "onetime callback", so clear its entity
       
   500             # cache to avoid memory usage
       
   501             req.drop_entity_cache()
   426         self.add_undo_link_to_msg(req)
   502         self.add_undo_link_to_msg(req)
   427         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
   503         self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
   428         return result
   504         return result
   429 
   505 
   430     def add_undo_link_to_msg(self, req):
   506     ### Error handler
   431         txuuid = req.data.get('last_undoable_transaction')
       
   432         if txuuid is not None:
       
   433             msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
       
   434             req.build_url('undo', txuuid=txuuid), req._('undo'))
       
   435             req.append_to_redirect_message(msg)
       
   436 
       
   437     def validation_error_handler(self, req, ex):
   507     def validation_error_handler(self, req, ex):
   438         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())
   439         if '__errorurl' in req.form:
   509         if '__errorurl' in req.form:
   440             forminfo = {'error': ex,
   510             forminfo = {'error': ex,
   441                         'values': req.form,
   511                         'values': req.form,
   444             req.session.data[req.form['__errorurl']] = forminfo
   514             req.session.data[req.form['__errorurl']] = forminfo
   445             # XXX form session key / __error_url should be differentiated:
   515             # XXX form session key / __error_url should be differentiated:
   446             # 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
   447             # 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
   448             # messages.
   518             # messages.
   449             raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
   519             location = req.form['__errorurl'].rsplit('#', 1)[0]
   450         self.error_handler(req, ex, tb=False)
   520             req.headers_out.setHeader('location', str(location))
   451 
   521             req.status_out = httplib.SEE_OTHER
   452     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):
   453         excinfo = sys.exc_info()
   526         excinfo = sys.exc_info()
   454         self.exception(repr(ex))
   527         self.exception(repr(ex))
   455         req.set_header('Cache-Control', 'no-cache')
   528         req.set_header('Cache-Control', 'no-cache')
   456         req.remove_header('Etag')
   529         req.remove_header('Etag')
   457         req.reset_message()
   530         req.reset_message()
   458         req.reset_headers()
   531         req.reset_headers()
   459         if req.ajax_request:
   532         if req.ajax_request:
   460             raise RemoteCallFailed(unicode(ex))
   533             return ajax_error_handler(req, ex)
   461         try:
   534         try:
   462             req.data['ex'] = ex
   535             req.data['ex'] = ex
   463             if tb:
   536             if tb:
   464                 req.data['excinfo'] = excinfo
   537                 req.data['excinfo'] = excinfo
   465             req.form['vid'] = 'error'
   538             req.form['vid'] = 'error'
   466             errview = self.vreg['views'].select('error', req)
   539             errview = self.vreg['views'].select('error', req)
   467             template = self.main_template_id(req)
   540             template = self.main_template_id(req)
   468             content = self.vreg['views'].main_template(req, template, view=errview)
   541             content = self.vreg['views'].main_template(req, template, view=errview)
   469         except Exception:
   542         except Exception:
   470             content = self.vreg['views'].main_template(req, 'error-template')
   543             content = self.vreg['views'].main_template(req, 'error-template')
   471         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
   472 
   567 
   473     def need_login_content(self, req):
   568     def need_login_content(self, req):
   474         return self.vreg['views'].main_template(req, 'login')
   569         return self.vreg['views'].main_template(req, 'login')
   475 
   570 
   476     def loggedout_content(self, req):
   571     def loggedout_content(self, req):
   480         req.form['vid'] = '404'
   575         req.form['vid'] = '404'
   481         view = self.vreg['views'].select('404', req)
   576         view = self.vreg['views'].select('404', req)
   482         template = self.main_template_id(req)
   577         template = self.main_template_id(req)
   483         return self.vreg['views'].main_template(req, template, view=view)
   578         return self.vreg['views'].main_template(req, template, view=view)
   484 
   579 
       
   580     # template stuff
       
   581 
   485     def main_template_id(self, req):
   582     def main_template_id(self, req):
   486         template = req.form.get('__template', req.property_value('ui.main-template'))
   583         template = req.form.get('__template', req.property_value('ui.main-template'))
   487         if template not in self.vreg['views']:
   584         if template not in self.vreg['views']:
   488             template = 'main-template'
   585             template = 'main-template'
   489         return template
   586         return template