[wf] state/transition may only belong to one workflow
"""CubicWeb web client application object:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"importsysfromtimeimportclock,timefromlogilab.common.deprecationimportdeprecatedfromrqlimportBadRQLQueryfromcubicwebimportset_log_methods,cwvregfromcubicwebimport(ValidationError,Unauthorized,AuthenticationError,NoSelectableObject,RepositoryError,CW_EVENT_MANAGER)fromcubicweb.webimportLOGGER,componentfromcubicweb.webimport(StatusResponse,DirectResponse,Redirect,NotFound,RemoteCallFailed,ExplicitLogin,InvalidSession,RequestError)# make session manager available through a global variable so the debug view can# print information about web sessionSESSION_MANAGER=NoneclassAbstractSessionManager(component.Component):"""manage session data associated to a session identifier"""id='sessionmanager'def__init__(self):self.session_time=self.vreg.config['http-session-time']orNoneassertself.session_timeisNoneorself.session_time>0self.cleanup_session_time=self.vreg.config['cleanup-session-time']or43200assertself.cleanup_session_time>0self.cleanup_anon_session_time=self.vreg.config['cleanup-anonymous-session-time']or120assertself.cleanup_anon_session_time>0ifself.session_time:assertself.cleanup_session_time<self.session_timeassertself.cleanup_anon_session_time<self.session_timeself.authmanager=self.vreg['components'].select('authmanager')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')closed,total=0,0forsessioninself.current_sessions():no_use_time=(time()-session.last_usage_time)total+=1ifsession.anonymous_connection:ifno_use_time>=self.cleanup_anon_session_time:self.close_session(session)closed+=1elifno_use_time>=self.cleanup_session_time:self.close_session(session)closed+=1returnclosed,total-closeddefhas_expired(self,session):"""return True if the web session associated to the session is expired """returnnot(self.session_timeisNoneortime()<session.last_usage_time+self.session_time)defcurrent_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 ExplicitLogin: if authentication is required """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"""id='authmanager'defauthenticate(self,req):"""authenticate user and return corresponding user object :raise ExplicitLogin: if authentication is required (no authentication info found or wrong user/password) """raiseNotImplementedError()classCookieSessionHandler(object):"""a session handler using a cookie to store the session identifier :cvar SESSION_VAR: string giving the name of the variable used to store the session identifier """SESSION_VAR='__session'def__init__(self,appli):self.vreg=appli.vregself.session_manager=self.vreg['components'].select('sessionmanager')globalSESSION_MANAGERSESSION_MANAGER=self.session_managerifnot'last_login_time'inself.vreg.schema:self._update_last_login_time=lambdax:NoneCW_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')self.session_manager.restore_data(data)globalSESSION_MANAGERSESSION_MANAGER=self.session_managerdefclean_sessions(self):"""cleanup sessions which has not been unused since a given amount of time """self.session_manager.clean_sessions()defset_session(self,req):"""associate a session to the request Session id is searched from : - # form variable - cookie if no session id is found, open a new session for the connected user or request authentification as needed :raise Redirect: if authentication has occured and succeed """assertreq.cnxisNone# at this point no cnx should be set on the requestcookie=req.get_cookie()try:sessionid=str(cookie[self.SESSION_VAR].value)exceptKeyError:# no session cookiesession=self.open_session(req)else:try:session=self.get_session(req,sessionid)exceptInvalidSession:try:session=self.open_session(req)exceptExplicitLogin:req.remove_cookie(cookie,self.SESSION_VAR)raise# remember last usage time for web session trackingsession.last_usage_time=time()defget_session(self,req,sessionid):returnself.session_manager.get_session(req,sessionid)defopen_session(self,req):session=self.session_manager.open_session(req)cookie=req.get_cookie()cookie[self.SESSION_VAR]=session.sessionidreq.set_cookie(cookie,self.SESSION_VAR,maxage=None)# remember last usage time for web session trackingsession.last_usage_time=time()ifnotsession.anonymous_connection:self._postlogin(req)returnsessiondef_update_last_login_time(self,req):try:req.execute('SET X last_login_time NOW WHERE X eid %(x)s',{'x':req.user.eid},'x')req.cnx.commit()except(RepositoryError,Unauthorized):# ldap user are not writeable for instancereq.cnx.rollback()except:req.cnx.rollback()raisedef_postlogin(self,req):"""postlogin: the user has been authenticated, redirect to the original page (index by default) with a welcome message """# Update last connection date# XXX: this should be in a post login hook in the repository, but there# we can't differentiate actual login of automatic session# reopening. Is it actually a problem?self._update_last_login_time(req)args=req.formargs['__message']=req._('welcome %s !')%req.user.loginif'vid'inreq.form:args['vid']=req.form['vid']if'rql'inreq.form:args['rql']=req.form['rql']path=req.relative_path(False)ifpath=='login':path='view'raiseRedirect(req.build_url(path,**args))deflogout(self,req):"""logout from the instance by cleaning the session and raising `AuthenticationError` """self.session_manager.close_session(req.cnx)req.remove_cookie(req.get_cookie(),self.SESSION_VAR)raiseAuthenticationError()classCubicWebPublisher(object):"""the publisher is a singleton hold by the web frontend, and is responsible to publish HTTP request. """def__init__(self,config,debug=None,session_handler_fact=CookieSessionHandler,vreg=None):super(CubicWebPublisher,self).__init__()# connect to the repository and get instance's schemaifvregisNone:vreg=cwvreg.CubicWebVRegistry(config,debug=debug)self.vreg=vregself.info('starting web instance from %s',config.apphome)self.repo=config.repository(vreg)ifnotvreg.initialized:self.config.init_cubes(self.repo.get_cubes())vreg.init_properties(self.repo.properties())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.publish=self.log_publishself._logfile_lock=Lock()else:self._query_log=Noneself.publish=self.main_publish# 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')defconnect(self,req):"""return a connection for a logged user object according to existing sessions (i.e. a new connection may be created or an already existing one may be reused """self.session_handler.set_session(req)# publish methods #########################################################deflog_publish(self,path,req):"""wrapper around _publish to log all queries executed for a given accessed path """try:returnself.main_publish(path,req)finally:cnx=req.cnxself._logfile_lock.acquire()try: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')finally:self._logfile_lock.release()@deprecated("use vreg.select('controllers', ...)")defselect_controller(self,oid,req):try:returnself.vreg['controllers'].select(oid,req=req,appli=self)exceptNoSelectableObject:raiseUnauthorized(req._('not authorized'))defmain_publish(self,path,req):"""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 """path=pathor'view'# don't log form values they may contains sensitive informationself.info('publish "%s" (form params: %s)',path,req.form.keys())# remove user callbacks on a new request (except for json controllers# to avoid callbacks being unregistered before they could be called)tstart=clock()try:try: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)ifreq.cnxisnotNone:# req.cnx is None if anonymous aren't allowed and we are# displaying the cookie authentication formreq.cnx.commit()except(StatusResponse,DirectResponse):req.cnx.commit()raiseexceptRedirect:# redirect is raised by edit controller when everything went fine,# so try to committry:req.cnx.commit()exceptValidationError,ex:self.validation_error_handler(req,ex)exceptUnauthorized,ex:req.data['errmsg']=req._('You\'re not authorized to access this page. ''If you think you should, please contact the site administrator.')self.error_handler(req,ex,tb=False)exceptException,ex:self.error_handler(req,ex,tb=True)else:# delete validation errors which may have been previously setif'__errorurl'inreq.form:req.del_session_data(req.form['__errorurl'])raiseexcept(AuthenticationError,NotFound,RemoteCallFailed):raiseexceptValidationError,ex:self.validation_error_handler(req,ex)except(Unauthorized,BadRQLQuery,RequestError),ex:self.error_handler(req,ex,tb=False)exceptException,ex:self.error_handler(req,ex,tb=True)finally:ifreq.cnxisnotNone:try:req.cnx.rollback()except:pass# ignore rollback error at this pointself.info('query %s executed in %s sec',req.relative_path(),clock()-tstart)returnresultdefvalidation_error_handler(self,req,ex):ex.errors=dict((k,v)fork,vinex.errors.items())if'__errorurl'inreq.form:forminfo={'errors':ex,'values':req.form,'eidmap':req.data.get('eidmap',{})}req.set_session_data(req.form['__errorurl'],forminfo)raiseRedirect(req.form['__errorurl'])self.error_handler(req,ex,tb=False)deferror_handler(self,req,ex,tb=False):excinfo=sys.exc_info()self.exception(repr(ex))req.set_header('Cache-Control','no-cache')req.remove_header('Etag')req.message=Nonereq.reset_headers()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)except:content=self.vreg['views'].main_template(req,'error-template')raiseStatusResponse(500,content)defneed_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)defmain_template_id(self,req):template=req.form.get('__template',req.property_value('ui.main-template'))iftemplatenotinself.vreg['views']:template='main-template'returntemplateset_log_methods(CubicWebPublisher,LOGGER)set_log_methods(CookieSessionHandler,LOGGER)