"""CubicWeb web client application object:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importsysfromtimeimportclock,timefromrqlimportBadRQLQueryfromcubicwebimportset_log_methodsfromcubicwebimport(ValidationError,Unauthorized,AuthenticationError,NoSelectableObject,RepositoryError)fromcubicweb.cwconfigimportCubicWebConfigurationfromcubicweb.cwvregimportCubicWebRegistryfromcubicweb.webimport(LOGGER,StatusResponse,DirectResponse,Redirect,NotFound,RemoteCallFailed,ExplicitLogin,InvalidSession)fromcubicweb.web.componentimportSingletonComponent# make session manager available through a global variable so the debug view can# print information about web sessionSESSION_MANAGER=NoneclassAbstractSessionManager(SingletonComponent):"""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.select_component('authmanager')assertself.authmanager,'no authentication manager found'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(SingletonComponent):"""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.session_manager=appli.vreg.select_component('sessionmanager')assertself.session_manager,'no session manager found'globalSESSION_MANAGERSESSION_MANAGER=self.session_managerifnot'last_login_time'inappli.vreg.schema:self._update_last_login_time=lambdax:Nonedefclean_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 application 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):"""Central registry for the web application. This is one of the central object in the web application, coupling dynamically loaded objects with the application's schema and the application's configuration objects. It specializes the VRegistry by adding some convenience methods to access to stored objects. Currently we have the following registries of objects known by the web application (library may use some others additional registries): * controllers, which are directly plugged into the application object to handle request publishing * views * templates * components * actions """def__init__(self,config,debug=None,session_handler_fact=CookieSessionHandler,vreg=None):super(CubicWebPublisher,self).__init__()# connect to the repository and get application's schemaifvregisNone:vreg=CubicWebRegistry(config,debug=debug)self.vreg=vregself.info('starting web application 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.url_resolver=vreg.select_component('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)defselect_controller(self,oid,req):"""return the most specific view according to the resultset"""vreg=self.vregtry:returnvreg.select(vreg.registry_objects('controllers',oid),req=req,appli=self)exceptNoSelectableObject:raiseUnauthorized(req._('not authorized'))# 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()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)controller=self.select_controller(ctrlid,req)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),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'content=self.vreg.main_template(req,'main')except:content=self.vreg.main_template(req,'error')raiseStatusResponse(500,content)defneed_login_content(self,req):returnself.vreg.main_template(req,'login')defloggedout_content(self,req):returnself.vreg.main_template(req,'loggedout')defnotfound_content(self,req):template=req.property_value('ui.main-template')or'main'req.form['vid']='404'returnself.vreg.main_template(req,template)set_log_methods(CubicWebPublisher,LOGGER)set_log_methods(CookieSessionHandler,LOGGER)