Added tag cubicweb-version-3.20.7, cubicweb-debian-version-3.20.7-1, cubicweb-centos-version-3.20.7-1 for changeset 359d68bc1260
# copyright 2003-2014 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,timefromcontextlibimportcontextmanagerfromwarningsimportwarnimportjsonimporthttplibfromlogilab.common.deprecationimportdeprecatedfromrqlimportBadRQLQueryfromcubicwebimportset_log_methods,cwvregfromcubicwebimport(ValidationError,Unauthorized,Forbidden,AuthenticationError,NoSelectableObject,CW_EVENT_MANAGER)fromcubicweb.repoapiimportanonymous_cnxfromcubicweb.webimportLOGGER,component,corsfromcubicweb.webimport(StatusResponse,DirectResponse,Redirect,NotFound,LogOut,RemoteCallFailed,InvalidSession,RequestError,PublishException)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_cnx=req.cnxanon_clt_cnx=anonymous_cnx(orig_cnx._session.repo)req.set_cnx(anon_clt_cnx)try:withanon_clt_cnx:yieldreqfinally:req.set_cnx(orig_cnx)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()[0]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+=1last_usage_time=session.mtimeno_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,httponly=True)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 """defwrap_set_cnx(func):defwrap_execute(cnx):orig_execute=cnx.executedefexecute(rql,kwargs=None,build_descr=True):tstart,cstart=time(),clock()rset=orig_execute(rql,kwargs,build_descr=build_descr)cnx.executed_queries.append((rql,kwargs,time()-tstart,clock()-cstart))returnrsetreturnexecutedefset_cnx(cnx):func(cnx)cnx.execute=wrap_execute(cnx)cnx.executed_queries=[]returnset_cnxreq.set_cnx=wrap_set_cnx(req.set_cnx)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 an http request Arguments are: - a Request object - path of the request object It returns the content of the http response. HTTP header and status are set on the Request object. """ifnotisinstance(req,CubicWebRequestBase):warn('[3.15] Application entry point 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)fromcubicwebimportrepoapicnx=repoapi.ClientConnection(session)req.set_cnx(cnx)exceptAuthenticationError:# Keep the dummy session set at initialisation.# such session with work to an some extend but raise an# AuthenticationError on any database access.importcontextlib@contextlib.contextmanagerdefdummy():yieldcnx=dummy()# XXX We want to clean up this approach in the future. But# several cubes like registration or forgotten password rely on# this principle.# 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 contentwithcnx:content=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 does not use 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:# apply CORS sanity checkscors.process_request(req,self.vreg.config)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)exceptcors.CORSPreflight:# Return directly an empty 200req.status_out=200result=''exceptStatusResponseasex:warn('[3.16] StatusResponse is deprecated use req.status_out',DeprecationWarning,stacklevel=2)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.FORBIDDENresult=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 pointself.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')ifisinstance(ex,PublishException)andex.statusisnotNone: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=httplib.INTERNAL_SERVER_ERRORifisinstance(ex,PublishException)andex.statusisnotNone:status=ex.statusifreq.status_out<400:# don't overwrite it if it's already setreq.status_out=statusjson_dumper=getattr(ex,'dumps',lambda:json.dumps({'reason':unicode(ex)}))returnjson_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)