[client-connection] add a repo property for dbapi compatibility
To ease transition from dbapi to repoapi we need the ClientConnection to be as
compatible as possible with the dbapi. Adding this method goes in this
direction.
It'll get deprecated in the deprecation wave that will conclude the repoapi
refactoring.
related to #2503918
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""CubicWeb web client application object"""__docformat__="restructuredtext en"importsysfromtimeimportclock,timefromcontextlibimportcontextmanagerfromwarningsimportwarnimporthttplibfromlogilab.common.deprecationimportdeprecatedfromrqlimportBadRQLQueryfromcubicwebimportset_log_methods,cwvregfromcubicwebimport(ValidationError,Unauthorized,Forbidden,AuthenticationError,NoSelectableObject,BadConnectionId,CW_EVENT_MANAGER)fromcubicweb.dbapiimportDBAPISession,anonymous_sessionfromcubicweb.webimportLOGGER,componentfromcubicweb.webimport(StatusResponse,DirectResponse,Redirect,NotFound,LogOut,RemoteCallFailed,InvalidSession,RequestError)fromcubicweb.web.requestimportCubicWebRequestBase# make session manager available through a global variable so the debug view can# print information about web sessionSESSION_MANAGER=None@contextmanagerdefanonymized_request(req):orig_session=req.sessionreq.set_session(anonymous_session(req.vreg))try:yieldreqfinally:req.set_session(orig_session)classAbstractSessionManager(component.Component):"""manage session data associated to a session identifier"""__regid__='sessionmanager'def__init__(self,repo):vreg=repo.vregself.session_time=vreg.config['http-session-time']orNoneself.authmanager=vreg['components'].select('authmanager',repo=repo)interval=(self.session_timeor0)/2.ifvreg.config.anonymous_user()isnotNone:self.cleanup_anon_session_time=vreg.config['cleanup-anonymous-session-time']or5*60assertself.cleanup_anon_session_time>0ifself.session_timeisnotNone:self.cleanup_anon_session_time=min(self.session_time,self.cleanup_anon_session_time)interval=self.cleanup_anon_session_time/2.# we don't want to check session more than once every 5 minutesself.clean_sessions_interval=max(5*60,interval)defclean_sessions(self):"""cleanup sessions which has not been unused since a given amount of time. Return the number of sessions which have been closed. """self.debug('cleaning http sessions')session_time=self.session_timeclosed,total=0,0forsessioninself.current_sessions():total+=1try:last_usage_time=session.cnx.check()exceptAttributeError:last_usage_time=session.mtimeexceptBadConnectionId:self.close_session(session)closed+=1else:no_use_time=(time()-last_usage_time)ifsession.anonymous_session:ifno_use_time>=self.cleanup_anon_session_time:self.close_session(session)closed+=1elifsession_timeisnotNoneandno_use_time>=session_time:self.close_session(session)closed+=1returnclosed,total-closeddefcurrent_sessions(self):"""return currently open sessions"""raiseNotImplementedError()defget_session(self,req,sessionid):"""return existing session for the given session identifier"""raiseNotImplementedError()defopen_session(self,req):"""open and return a new session for the given request. raise :exc:`cubicweb.AuthenticationError` if authentication failed (no authentication info found or wrong user/password) """raiseNotImplementedError()defclose_session(self,session):"""close session on logout or on invalid session detected (expired out, corrupted...) """raiseNotImplementedError()classAbstractAuthenticationManager(component.Component):"""authenticate user associated to a request and check session validity"""__regid__='authmanager'def__init__(self,repo):self.vreg=repo.vregdefvalidate_session(self,req,session):"""check session validity, reconnecting it to the repository if the associated connection expired in the repository side (hence the necessity for this method). raise :exc:`InvalidSession` if session is corrupted for a reason or another and should be closed """raiseNotImplementedError()defauthenticate(self,req):"""authenticate user using connection information found in the request, and return corresponding a :class:`~cubicweb.dbapi.Connection` instance, as well as login and authentication information dictionary used to open the connection. raise :exc:`cubicweb.AuthenticationError` if authentication failed (no authentication info found or wrong user/password) """raiseNotImplementedError()classCookieSessionHandler(object):"""a session handler using a cookie to store the session identifier"""def__init__(self,appli):self.repo=appli.repoself.vreg=appli.vregself.session_manager=self.vreg['components'].select('sessionmanager',repo=self.repo)globalSESSION_MANAGERSESSION_MANAGER=self.session_managerifself.vreg.config.mode!='test':# don't try to reset session manager during test, this leads to# weird failures when running multiple testsCW_EVENT_MANAGER.bind('after-registry-reload',self.reset_session_manager)defreset_session_manager(self):data=self.session_manager.dump_data()self.session_manager=self.vreg['components'].select('sessionmanager',repo=self.repo)self.session_manager.restore_data(data)globalSESSION_MANAGERSESSION_MANAGER=self.session_manager@propertydefclean_sessions_interval(self):returnself.session_manager.clean_sessions_intervaldefclean_sessions(self):"""cleanup sessions which has not been unused since a given amount of time """self.session_manager.clean_sessions()defsession_cookie(self,req):"""return a string giving the name of the cookie used to store the session identifier. """ifreq.https:return'__%s_https_session'%self.vreg.config.appidreturn'__%s_session'%self.vreg.config.appiddefget_session(self,req):"""Return a session object corresponding to credentials held by the req Session id is searched from : - # form variable - cookie If no session id is found, try opening a new session with credentials found in the request. Raises AuthenticationError if no session can be found or created. """cookie=req.get_cookie()sessioncookie=self.session_cookie(req)try:sessionid=str(cookie[sessioncookie].value)session=self.get_session_by_id(req,sessionid)except(KeyError,InvalidSession):# no valid session cookiesession=self.open_session(req)returnsessiondefget_session_by_id(self,req,sessionid):session=self.session_manager.get_session(req,sessionid)session.mtime=time()returnsessiondefopen_session(self,req):session=self.session_manager.open_session(req)sessioncookie=self.session_cookie(req)secure=req.httpsandreq.base_url().startswith('https://')req.set_cookie(sessioncookie,session.sessionid,maxage=None,secure=secure)ifnotsession.anonymous_session:self.session_manager.postlogin(req,session)returnsessiondeflogout(self,req,goto_url):"""logout from the instance by cleaning the session and raising `AuthenticationError` """self.session_manager.close_session(req.session)req.remove_cookie(self.session_cookie(req))raiseLogOut(url=goto_url)# these are overridden by set_log_methods below# only defining here to prevent pylint from complaininginfo=warning=error=critical=exception=debug=lambdamsg,*a,**kw:NoneclassCubicWebPublisher(object):"""the publisher is a singleton hold by the web frontend, and is responsible to publish HTTP request. The http server will call its main entry point ``application.handle_request``. .. automethod:: cubicweb.web.application.CubicWebPublisher.main_handle_request You have to provide both a repository and web-server config at initialization. In all in one instance both config will be the same. """def__init__(self,repo,config,session_handler_fact=CookieSessionHandler):self.info('starting web instance from %s',config.apphome)self.repo=repoself.vreg=repo.vreg# get instance's schemaifnotself.vreg.initialized:config.init_cubes(self.repo.get_cubes())self.vreg.init_properties(self.repo.properties())self.vreg.set_schema(self.repo.get_schema())# set the correct publish methodifconfig['query-log-file']:fromthreadingimportLockself._query_log=open(config['query-log-file'],'a')self.handle_request=self.log_handle_requestself._logfile_lock=Lock()else:self._query_log=Noneself.handle_request=self.main_handle_request# instantiate session and url resolving helpersself.session_handler=session_handler_fact(self)self.set_urlresolver()CW_EVENT_MANAGER.bind('after-registry-reload',self.set_urlresolver)defset_urlresolver(self):self.url_resolver=self.vreg['components'].select('urlpublisher',vreg=self.vreg)defget_session(self,req):"""Return a session object corresponding to credentials held by the req May raise AuthenticationError. """returnself.session_handler.get_session(req)# publish methods #########################################################deflog_handle_request(self,req,path):"""wrapper around _publish to log all queries executed for a given accessed path """try:returnself.main_handle_request(req,path)finally:cnx=req.cnxifcnx:withself._logfile_lock:try:result=['\n'+'*'*80]result.append(req.url())result+=['%s%s -- (%.3f sec, %.3f CPU sec)'%qforqincnx.executed_queries]cnx.executed_queries=[]self._query_log.write('\n'.join(result).encode(req.encoding))self._query_log.flush()exceptException:self.exception('error while logging queries')defmain_handle_request(self,req,path):"""Process and http request Arguments are: - a Request object - path of the request object It return the content of the http response. HTTP header and status are are set on the Request Object. """ifnotisinstance(req,CubicWebRequestBase):warn('[3.15] Application entry poin arguments are now (req, path) ''not (path, req)',DeprecationWarning,2)req,path=path,reqifreq.authmode=='http':# activate realm-based authrealm=self.vreg.config['realm']req.set_header('WWW-Authenticate',[('Basic',{'realm':realm})],raw=False)content=''try:try:session=self.get_session(req)req.set_session(session)exceptAuthenticationError:# Keep the dummy session set at initialisation.# such session with work to an some extend but raise an# AuthenticationError on any database access.pass# XXX We want to clean up this approach in the future. But# several cubes like registration or forgotten password rely on# this principle.assertreq.sessionisnotNone# DENY https acces for anonymous_userif(req.httpsandreq.session.anonymous_sessionandself.vreg.config['https-deny-anonymous']):# don't allow anonymous on https connectionraiseAuthenticationError()# nested try to allow LogOut to delegate logic to AuthenticationError# handlertry:### Try to generate the actual request contentcontent=self.core_handle(req,path)# Handle user log-outexceptLogOutasex:# When authentification is handled by cookie the code that# raised LogOut must has invalidated the cookie. We can just# reload the original url without authentificationifself.vreg.config['auth-mode']=='cookie'andex.url:req.headers_out.setHeader('location',str(ex.url))ifex.statusisnotNone:req.status_out=httplib.SEE_OTHER# When the authentification is handled by http we must# explicitly ask for authentification to flush current http# authentification informationelse:# Render "logged out" content.# assignement to ``content`` prevent standard# AuthenticationError code to overwrite it.content=self.loggedout_content(req)# let the explicitly reset http credentialraiseAuthenticationError()exceptRedirectasex:# authentication needs redirection (eg openid)content=self.redirect_handler(req,ex)# Wrong, absent or Reseted credentialexceptAuthenticationError:# If there is an https url configured and# the request do not used https, redirect to login formhttps_url=self.vreg.config['https-url']ifhttps_urlandreq.base_url()!=https_url:req.status_out=httplib.SEE_OTHERreq.headers_out.setHeader('location',https_url+'login')else:# We assume here that in http auth mode the user *May* provide# Authentification Credential if asked kindly.ifself.vreg.config['auth-mode']=='http':req.status_out=httplib.UNAUTHORIZED# In the other case (coky auth) we assume that there is no way# for the user to provide them...# XXX But WHY ?else:req.status_out=httplib.FORBIDDEN# If previous error handling already generated a custom content# do not overwrite it. This is used by LogOut Except# XXX ensure we don't actually serve contentifnotcontent:content=self.need_login_content(req)returncontentdefcore_handle(self,req,path):"""method called by the main publisher to process <path> should return a string containing the resulting page or raise a `NotFound` exception :type path: str :param path: the path part of the url to publish :type req: `web.Request` :param req: the request object :rtype: str :return: the result of the pusblished url """# don't log form values they may contains sensitive informationself.debug('publish "%s" (%s, form params: %s)',path,req.session.sessionid,list(req.form))# remove user callbacks on a new request (except for json controllers# to avoid callbacks being unregistered before they could be called)tstart=clock()commited=Falsetry:### standard processing of the requesttry:ctrlid,rset=self.url_resolver.process(req,path)try:controller=self.vreg['controllers'].select(ctrlid,req,appli=self)exceptNoSelectableObject:raiseUnauthorized(req._('not authorized'))req.update_search_state()result=controller.publish(rset=rset)exceptStatusResponseasex:warn('StatusResponse is deprecated use req.status_out',DeprecationWarning)result=ex.contentreq.status_out=ex.statusexceptRedirectasex:# Redirect may be raised by edit controller when everything went# fine, so attempt to commitresult=self.redirect_handler(req,ex)ifreq.cnx:txuuid=req.cnx.commit()commited=TrueiftxuuidisnotNone:req.data['last_undoable_transaction']=txuuid### error caseexceptNotFoundasex:result=self.notfound_content(req)req.status_out=ex.statusexceptValidationErrorasex:result=self.validation_error_handler(req,ex)exceptRemoteCallFailedasex:result=self.ajax_error_handler(req,ex)exceptUnauthorizedasex:req.data['errmsg']=req._('You\'re not authorized to access this page. ''If you think you should, please contact the site administrator.')req.status_out=httplib.UNAUTHORIZEDresult=self.error_handler(req,ex,tb=False)exceptForbiddenasex:req.data['errmsg']=req._('This action is forbidden. ''If you think it should be allowed, please contact the site administrator.')req.status_out=httplib.FORBIDDENresult=self.error_handler(req,ex,tb=False)except(BadRQLQuery,RequestError)asex:result=self.error_handler(req,ex,tb=False)### pass through exceptionexceptDirectResponse:ifreq.cnx:req.cnx.commit()raiseexcept(AuthenticationError,LogOut):# the rollback is handled in the finallyraise### Last defense lineexceptBaseExceptionasex:req.status_out=httplib.INTERNAL_SERVER_ERRORresult=self.error_handler(req,ex,tb=True)finally:ifreq.cnxandnotcommited:try:req.cnx.rollback()exceptException:pass# ignore rollback error at this point# request may be referenced by "onetime callback", so clear its entity# cache to avoid memory usagereq.drop_entity_cache()self.add_undo_link_to_msg(req)self.debug('query %s executed in %s sec',req.relative_path(),clock()-tstart)returnresult# Error handlersdefredirect_handler(self,req,ex):"""handle redirect - comply to ex status - set header field - return empty content """self.debug('redirecting to %s',str(ex.location))req.headers_out.setHeader('location',str(ex.location))assert300<=ex.status<400req.status_out=ex.statusreturn''defvalidation_error_handler(self,req,ex):ex.translate(req._)# translate messages using ui languageif'__errorurl'inreq.form:forminfo={'error':ex,'values':req.form,'eidmap':req.data.get('eidmap',{})}req.session.data[req.form['__errorurl']]=forminfo# XXX form session key / __error_url should be differentiated:# session key is 'url + #<form dom id', though we usually don't want# the browser to move to the form since it hides the global# messages.location=req.form['__errorurl'].rsplit('#',1)[0]req.headers_out.setHeader('location',str(location))req.status_out=httplib.SEE_OTHERreturn''req.status_out=httplib.CONFLICTreturnself.error_handler(req,ex,tb=False)deferror_handler(self,req,ex,tb=False):excinfo=sys.exc_info()iftb:self.exception(repr(ex))req.set_header('Cache-Control','no-cache')req.remove_header('Etag')req.remove_header('Content-disposition')req.reset_message()req.reset_headers()ifreq.ajax_request:returnself.ajax_error_handler(req,ex)try:req.data['ex']=exiftb:req.data['excinfo']=excinforeq.form['vid']='error'errview=self.vreg['views'].select('error',req)template=self.main_template_id(req)content=self.vreg['views'].main_template(req,template,view=errview)exceptException:content=self.vreg['views'].main_template(req,'error-template')ifgetattr(ex,'status',None)isnotNone:req.status_out=ex.statusreturncontentdefadd_undo_link_to_msg(self,req):txuuid=req.data.get('last_undoable_transaction')iftxuuidisnotNone:msg=u'<span class="undo">[<a href="%s">%s</a>]</span>'%(req.build_url('undo',txuuid=txuuid),req._('undo'))req.append_to_redirect_message(msg)defajax_error_handler(self,req,ex):req.set_header('content-type','application/json')status=ex.statusifstatusisNone:status=httplib.INTERNAL_SERVER_ERRORjson_dumper=getattr(ex,'dumps',lambda:unicode(ex))req.status_out=statusreturnjson_dumper()# special case handlingdefneed_login_content(self,req):returnself.vreg['views'].main_template(req,'login')defloggedout_content(self,req):returnself.vreg['views'].main_template(req,'loggedout')defnotfound_content(self,req):req.form['vid']='404'view=self.vreg['views'].select('404',req)template=self.main_template_id(req)returnself.vreg['views'].main_template(req,template,view=view)# template stuffdefmain_template_id(self,req):template=req.form.get('__template',req.property_value('ui.main-template'))iftemplatenotinself.vreg['views']:template='main-template'returntemplate# these are overridden by set_log_methods below# only defining here to prevent pylint from complaininginfo=warning=error=critical=exception=debug=lambdamsg,*a,**kw:Noneset_log_methods(CubicWebPublisher,LOGGER)set_log_methods(CookieSessionHandler,LOGGER)