[session] new methods on session to ease access to security/hooks control context managers
# copyright 2003-2011 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/>."""Repository users' and internal' sessions."""from__future__importwith_statement__docformat__="restructuredtext en"importsysimportthreadingfromtimeimporttimefromuuidimportuuid4fromwarningsimportwarnfromlogilab.common.deprecationimportdeprecatedfromrqlimportCoercionErrorfromrql.nodesimportETYPE_PYOBJ_MAP,etype_from_pyobjfromyamsimportBASE_TYPESfromcubicwebimportBinary,UnknownEid,QueryError,schemafromcubicweb.selectorsimportobjectify_selectorfromcubicweb.reqimportRequestSessionBasefromcubicweb.dbapiimportConnectionPropertiesfromcubicweb.utilsimportmake_uid,RepeatListfromcubicweb.rqlrewriteimportRQLRewriterfromcubicweb.server.editionimportEditedEntityETYPE_PYOBJ_MAP[Binary]='Bytes'NO_UNDO_TYPES=schema.SCHEMA_TYPES.copy()NO_UNDO_TYPES.add('CWCache')# is / is_instance_of are usually added by sql hooks except when using# dataimport.NoHookRQLObjectStore, and we don't want to record them# anyway in the later caseNO_UNDO_TYPES.add('is')NO_UNDO_TYPES.add('is_instance_of')NO_UNDO_TYPES.add('cw_source')# XXX rememberme,forgotpwd,apycot,vcsfiledef_make_description(selected,args,solution):"""return a description for a result set"""description=[]forterminselected:description.append(term.get_type(solution,args))returndescription@objectify_selectordefis_user_session(cls,req,**kwargs):"""repository side only selector returning 1 if the session is a regular user session and not an internal session """returnnotreq.is_internal_session@objectify_selectordefis_internal_session(cls,req,**kwargs):"""repository side only selector returning 1 if the session is not a regular user session but an internal session """returnreq.is_internal_sessionclasshooks_control(object):"""context manager to control activated hooks categories. If mode is session.`HOOKS_DENY_ALL`, given hooks categories will be enabled. If mode is session.`HOOKS_ALLOW_ALL`, given hooks categories will be disabled. .. sourcecode:: python with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'integrity'): # ... do stuff with all but 'integrity' hooks activated with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'): # ... do stuff with none but 'integrity' hooks activated """def__init__(self,session,mode,*categories):self.session=sessionself.mode=modeself.categories=categoriesdef__enter__(self):self.oldmode=self.session.set_hooks_mode(self.mode)ifself.modeisself.session.HOOKS_DENY_ALL:self.changes=self.session.enable_hook_categories(*self.categories)else:self.changes=self.session.disable_hook_categories(*self.categories)def__exit__(self,exctype,exc,traceback):ifself.changes:ifself.modeisself.session.HOOKS_DENY_ALL:self.session.disable_hook_categories(*self.changes)else:self.session.enable_hook_categories(*self.changes)self.session.set_hooks_mode(self.oldmode)INDENT=''classsecurity_enabled(object):"""context manager to control security w/ session.execute, since by default security is disabled on queries executed on the repository side. """def__init__(self,session,read=None,write=None):self.session=sessionself.read=readself.write=writedef__enter__(self):# global INDENTifself.readisnotNone:self.oldread=self.session.set_read_security(self.read)# print INDENT + 'read', self.read, self.oldreadifself.writeisnotNone:self.oldwrite=self.session.set_write_security(self.write)# print INDENT + 'write', self.write, self.oldwrite# INDENT += ' 'def__exit__(self,exctype,exc,traceback):# global INDENT# INDENT = INDENT[:-2]ifself.readisnotNone:self.session.set_read_security(self.oldread)# print INDENT + 'reset read to', self.oldreadifself.writeisnotNone:self.session.set_write_security(self.oldwrite)# print INDENT + 'reset write to', self.oldwriteclassTransactionData(object):def__init__(self,txid):self.transactionid=txidclassSession(RequestSessionBase):"""tie session id, user, connections pool and other session data all together """is_internal_session=Falsedef__init__(self,user,repo,cnxprops=None,_id=None):super(Session,self).__init__(repo.vreg)self.id=_idormake_uid(user.login.encode('UTF8'))cnxprops=cnxpropsorConnectionProperties('inmemory')self.user=userself.repo=repoself.cnxtype=cnxprops.cnxtypeself.timestamp=time()self.default_mode='read'# support undo for Create Update Delete entity / Add Remove relationifrepo.config.creatingorrepo.config.repairingorself.is_internal_session:self.undo_actions=()else:self.undo_actions=set(repo.config['undo-support'].upper())ifself.undo_actions-set('CUDAR'):raiseException('bad undo-support string in configuration')# short cut to querier .execute methodself._execute=repo.querier.execute# shared data, used to communicate extra information between the client# and the rql serverself.data={}# i18n initializationself.set_language(cnxprops.lang)# internalsself._tx_data={}self.__threaddata=threading.local()self._threads_in_transaction=set()self._closed=Falseself._closed_lock=threading.Lock()def__unicode__(self):return'<%ssession %s (%s 0x%x)>'%(self.cnxtype,unicode(self.user.login),self.id,id(self))defset_tx_data(self,txid=None):iftxidisNone:txid=threading.currentThread().getName()try:self.__threaddata.txdata=self._tx_data[txid]exceptKeyError:self.__threaddata.txdata=self._tx_data[txid]=TransactionData(txid)@propertydef_threaddata(self):try:returnself.__threaddata.txdataexceptAttributeError:self.set_tx_data()returnself.__threaddata.txdatadefhijack_user(self,user):"""return a fake request/session using specified user"""session=Session(user,self.repo)threaddata=session._threaddatathreaddata.pool=self.pool# share pending_operations, else operation added in the hi-jacked# session such as SendMailOp won't ever be processedthreaddata.pending_operations=self.pending_operations# everything in transaction_data should be copied back but the entity# type cache we don't want to avoid security pbthreaddata.transaction_data=self.transaction_data.copy()threaddata.transaction_data.pop('ecache',None)returnsessiondefadd_relation(self,fromeid,rtype,toeid):"""provide direct access to the repository method to add a relation. This is equivalent to the following rql query: SET X rtype Y WHERE X eid fromeid, T eid toeid without read security check but also all the burden of rql execution. You may use this in hooks when you know both eids of the relation you want to add. """self.add_relations([(rtype,[(fromeid,toeid)])])defadd_relations(self,relations):'''set many relation using a shortcut similar to the one in add_relation relations is a list of 2-uples, the first element of each 2-uple is the rtype, and the second is a list of (fromeid, toeid) tuples '''edited_entities={}relations_dict={}withsecurity_enabled(self,False,False):forrtype,eidsinrelations:ifself.vreg.schema[rtype].inlined:forfromeid,toeidineids:iffromeidnotinedited_entities:entity=self.entity_from_eid(fromeid)edited=EditedEntity(entity)edited_entities[fromeid]=editedelse:edited=edited_entities[fromeid]edited.edited_attribute(rtype,toeid)else:relations_dict[rtype]=eidsself.repo.glob_add_relations(self,relations_dict)foreditedinedited_entities.itervalues():self.repo.glob_update_entity(self,edited)defdelete_relation(self,fromeid,rtype,toeid):"""provide direct access to the repository method to delete a relation. This is equivalent to the following rql query: DELETE X rtype Y WHERE X eid fromeid, T eid toeid without read security check but also all the burden of rql execution. You may use this in hooks when you know both eids of the relation you want to delete. """withsecurity_enabled(self,False,False):ifself.vreg.schema[rtype].inlined:entity=self.entity_from_eid(fromeid)entity.cw_attr_cache[rtype]=Noneself.repo.glob_update_entity(self,entity,set((rtype,)))else:self.repo.glob_delete_relation(self,fromeid,rtype,toeid)# relations cache handling #################################################defupdate_rel_cache_add(self,subject,rtype,object,symmetric=False):self._update_entity_rel_cache_add(subject,rtype,'subject',object)ifsymmetric:self._update_entity_rel_cache_add(object,rtype,'subject',subject)else:self._update_entity_rel_cache_add(object,rtype,'object',subject)defupdate_rel_cache_del(self,subject,rtype,object,symmetric=False):self._update_entity_rel_cache_del(subject,rtype,'subject',object)ifsymmetric:self._update_entity_rel_cache_del(object,rtype,'object',object)else:self._update_entity_rel_cache_del(object,rtype,'object',subject)def_update_entity_rel_cache_add(self,eid,rtype,role,targeteid):try:entity=self.entity_cache(eid)exceptKeyError:returnrcache=entity.cw_relation_cached(rtype,role)ifrcacheisnotNone:rset,entities=rcacherset=rset.copy()entities=list(entities)rset.rows.append([targeteid])ifnotisinstance(rset.description,list):# else description not setrset.description=list(rset.description)rset.description.append([self.describe(targeteid)[0]])targetentity=self.entity_from_eid(targeteid)iftargetentity.cw_rsetisNone:targetentity.cw_rset=rsettargetentity.cw_row=rset.rowcounttargetentity.cw_col=0rset.rowcount+=1entities.append(targetentity)entity._cw_related_cache['%s_%s'%(rtype,role)]=(rset,tuple(entities))def_update_entity_rel_cache_del(self,eid,rtype,role,targeteid):try:entity=self.entity_cache(eid)exceptKeyError:returnrcache=entity.cw_relation_cached(rtype,role)ifrcacheisnotNone:rset,entities=rcacheforidx,rowinenumerate(rset.rows):ifrow[0]==targeteid:breakelse:# this may occurs if the cache has been filed by a hook# after the database updateself.debug('cache inconsistency for %s%s%s%s',eid,rtype,role,targeteid)returnrset=rset.copy()entities=list(entities)delrset.rows[idx]ifisinstance(rset.description,list):# else description not setdelrset.description[idx]delentities[idx]rset.rowcount-=1entity._cw_related_cache['%s_%s'%(rtype,role)]=(rset,tuple(entities))# resource accessors ######################################################defsystem_sql(self,sql,args=None,rollback_on_failure=True):"""return a sql cursor on the system database"""ifsql.split(None,1)[0].upper()!='SELECT':self.mode='write'source=self.pool.source('system')try:returnsource.doexec(self,sql,args,rollback=rollback_on_failure)except(source.OperationalError,source.InterfaceError):ifnotrollback_on_failure:raisesource.warning("trying to reconnect")self.pool.reconnect(source)returnsource.doexec(self,sql,args,rollback=rollback_on_failure)defset_language(self,language):"""i18n configuration for translation"""language=languageorself.user.property_value('ui.language')try:gettext,pgettext=self.vreg.config.translations[language]self._=self.__=gettextself.pgettext=pgettextexceptKeyError:language=self.vreg.property_value('ui.language')try:gettext,pgettext=self.vreg.config.translations[language]self._=self.__=gettextself.pgettext=pgettextexceptKeyError:self._=self.__=unicodeself.pgettext=lambdax,y:yself.lang=languagedefchange_property(self,prop,value):assertprop=='lang'# this is the only one changeable property for nowself.set_language(value)defdeleted_in_transaction(self,eid):"""return True if the entity of the given eid is being deleted in the current transaction """returneidinself.transaction_data.get('pendingeids',())defadded_in_transaction(self,eid):"""return True if the entity of the given eid is being created in the current transaction """returneidinself.transaction_data.get('neweids',())defschema_rproperty(self,rtype,eidfrom,eidto,rprop):rschema=self.repo.schema[rtype]subjtype=self.describe(eidfrom)[0]objtype=self.describe(eidto)[0]rdef=rschema.rdef(subjtype,objtype)returnrdef.get(rprop)# security control #########################################################DEFAULT_SECURITY=object()# evaluated to true by designdefsecurity_enabled(self,read=False,write=False):returnsecurity_enabled(self,read=read,write=write)@propertydefread_security(self):"""return a boolean telling if read security is activated or not"""txstore=self._threaddataiftxstoreisNone:returnself.DEFAULT_SECURITYtry:returntxstore.read_securityexceptAttributeError:txstore.read_security=self.DEFAULT_SECURITYreturntxstore.read_securitydefset_read_security(self,activated):"""[de]activate read security, returning the previous value set for later restoration. you should usually use the `security_enabled` context manager instead of this to change security settings. """txstore=self._threaddataiftxstoreisNone:returnself.DEFAULT_SECURITYoldmode=getattr(txstore,'read_security',self.DEFAULT_SECURITY)txstore.read_security=activated# dbapi_query used to detect hooks triggered by a 'dbapi' query (eg not# issued on the session). This is tricky since we the execution model of# a (write) user query is:## repository.execute (security enabled)# \-> querier.execute# \-> repo.glob_xxx (add/update/delete entity/relation)# \-> deactivate security before calling hooks# \-> WE WANT TO CHECK QUERY NATURE HERE# \-> potentially, other calls to querier.execute## so we can't rely on simply checking session.read_security, but# recalling the first transition from DEFAULT_SECURITY to something# else (False actually) is not perfect but should be enough## also reset dbapi_query to true when we go back to DEFAULT_SECURITYtxstore.dbapi_query=(oldmodeisself.DEFAULT_SECURITYoractivatedisself.DEFAULT_SECURITY)returnoldmode@propertydefwrite_security(self):"""return a boolean telling if write security is activated or not"""txstore=self._threaddataiftxstoreisNone:returnself.DEFAULT_SECURITYtry:returntxstore.write_securityexcept:txstore.write_security=self.DEFAULT_SECURITYreturntxstore.write_securitydefset_write_security(self,activated):"""[de]activate write security, returning the previous value set for later restoration. you should usually use the `security_enabled` context manager instead of this to change security settings. """txstore=self._threaddataiftxstoreisNone:returnself.DEFAULT_SECURITYoldmode=getattr(txstore,'write_security',self.DEFAULT_SECURITY)txstore.write_security=activatedreturnoldmode@propertydefrunning_dbapi_query(self):"""return a boolean telling if it's triggered by a db-api query or by a session query. To be used in hooks, else may have a wrong value. """returngetattr(self._threaddata,'dbapi_query',True)# hooks activation control ################################################## all hooks should be activated during normal executionHOOKS_ALLOW_ALL=object()HOOKS_DENY_ALL=object()defallow_all_hooks_but(self,*categories):returnhooks_control(self,self.HOOKS_ALLOW_ALL,*categories)defdeny_all_hooks_but(self,*categories):returnhooks_control(self,self.HOOKS_DENY_ALL,*categories)@propertydefhooks_mode(self):returngetattr(self._threaddata,'hooks_mode',self.HOOKS_ALLOW_ALL)defset_hooks_mode(self,mode):assertmodeisself.HOOKS_ALLOW_ALLormodeisself.HOOKS_DENY_ALLoldmode=getattr(self._threaddata,'hooks_mode',self.HOOKS_ALLOW_ALL)self._threaddata.hooks_mode=modereturnoldmode@propertydefdisabled_hook_categories(self):try:returngetattr(self._threaddata,'disabled_hook_cats')exceptAttributeError:cats=self._threaddata.disabled_hook_cats=set()returncats@propertydefenabled_hook_categories(self):try:returngetattr(self._threaddata,'enabled_hook_cats')exceptAttributeError:cats=self._threaddata.enabled_hook_cats=set()returncatsdefdisable_hook_categories(self,*categories):"""disable the given hook categories: - on HOOKS_DENY_ALL mode, ensure those categories are not enabled - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled """changes=set()ifself.hooks_modeisself.HOOKS_DENY_ALL:enablecats=self.enabled_hook_categoriesforcategoryincategories:ifcategoryinenablecats:enablecats.remove(category)changes.add(category)else:disablecats=self.disabled_hook_categoriesforcategoryincategories:ifcategorynotindisablecats:disablecats.add(category)changes.add(category)returntuple(changes)defenable_hook_categories(self,*categories):"""enable the given hook categories: - on HOOKS_DENY_ALL mode, ensure those categories are enabled - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled """changes=set()ifself.hooks_modeisself.HOOKS_DENY_ALL:enablecats=self.enabled_hook_categoriesforcategoryincategories:ifcategorynotinenablecats:enablecats.add(category)changes.add(category)else:disablecats=self.disabled_hook_categoriesforcategoryincategories:ifcategoryinself.disabled_hook_categories:disablecats.remove(category)changes.add(category)returntuple(changes)defis_hook_category_activated(self,category):"""return a boolean telling if the given category is currently activated or not """ifself.hooks_modeisself.HOOKS_DENY_ALL:returncategoryinself.enabled_hook_categoriesreturncategorynotinself.disabled_hook_categoriesdefis_hook_activated(self,hook):"""return a boolean telling if the given hook class is currently activated or not """returnself.is_hook_category_activated(hook.category)# connection management ###################################################defkeep_pool_mode(self,mode):"""set pool_mode, e.g. how the session will keep its pool: * if mode == 'write', the pool is freed after each ready query, but kept until the transaction's end (eg commit or rollback) when a write query is detected (eg INSERT/SET/DELETE queries) * if mode == 'transaction', the pool is only freed after the transaction's end notice that a repository has a limited set of pools, and a session has to wait for a free pool to run any rql query (unless it already has a pool set). """assertmodein('transaction','write')ifmode=='transaction':self.default_mode='transaction'else:# mode == 'write'self.default_mode='read'defget_mode(self):returngetattr(self._threaddata,'mode',self.default_mode)defset_mode(self,value):self._threaddata.mode=valuemode=property(get_mode,set_mode,doc='transaction mode (read/write/transaction), resetted to'' default_mode on commit / rollback')defget_commit_state(self):returngetattr(self._threaddata,'commit_state',None)defset_commit_state(self,value):self._threaddata.commit_state=valuecommit_state=property(get_commit_state,set_commit_state)@propertydefpool(self):"""connections pool, set according to transaction mode for each query"""ifself._closed:self.reset_pool(True)raiseException('try to access pool on a closed session')returngetattr(self._threaddata,'pool',None)defset_pool(self):"""the session need a pool to execute some queries"""withself._closed_lock:ifself._closed:self.reset_pool(True)raiseException('try to set pool on a closed session')ifself.poolisNone:# get pool first to avoid race-conditionself._threaddata.pool=pool=self.repo._get_pool()try:pool.pool_set()except:self._threaddata.pool=Noneself.repo._free_pool(pool)raiseself._threads_in_transaction.add((threading.currentThread(),pool))returnself._threaddata.pooldef_free_thread_pool(self,thread,pool,force_close=False):try:self._threads_in_transaction.remove((thread,pool))exceptKeyError:# race condition on pool freeing (freed by commit or rollback vs# close)passelse:ifforce_close:pool.reconnect()else:pool.pool_reset()# free pool once everything is done to avoid race-conditionself.repo._free_pool(pool)defreset_pool(self,ignoremode=False):"""the session is no longer using its pool, at least for some time"""# pool may be none if no operation has been done since last commit# or rollbackpool=getattr(self._threaddata,'pool',None)ifpoolisnotNoneand(ignoremodeorself.mode=='read'):# even in read mode, we must release the current transactionself._free_thread_pool(threading.currentThread(),pool)delself._threaddata.pooldef_touch(self):"""update latest session usage timestamp and reset mode to read"""self.timestamp=time()self.local_perm_cache.clear()# XXX simply move in transaction_data, no?# shared data handling ###################################################defget_shared_data(self,key,default=None,pop=False,txdata=False):"""return value associated to `key` in session data"""iftxdata:data=self.transaction_dataelse:data=self.dataifpop:returndata.pop(key,default)else:returndata.get(key,default)defset_shared_data(self,key,value,txdata=False):"""set value associated to `key` in session data"""iftxdata:self.transaction_data[key]=valueelse:self.data[key]=value# request interface #######################################################@propertydefcursor(self):"""return a rql cursor"""returnselfdefset_entity_cache(self,entity):# XXX session level caching may be a pb with multiple repository# instances, but 1. this is probably not the only one :$ and 2. it# may be an acceptable risk. Anyway we could activate it or not# according to a configuration optiontry:self.transaction_data['ecache'].setdefault(entity.eid,entity)exceptKeyError:self.transaction_data['ecache']=ecache={}ecache[entity.eid]=entitydefentity_cache(self,eid):returnself.transaction_data['ecache'][eid]defcached_entities(self):returnself.transaction_data.get('ecache',{}).values()defdrop_entity_cache(self,eid=None):ifeidisNone:self.transaction_data.pop('ecache',None)else:delself.transaction_data['ecache'][eid]deffrom_controller(self):"""return the id (string) of the controller issuing the request (no sense here, always return 'view') """return'view'defsource_defs(self):returnself.repo.source_defs()defdescribe(self,eid):"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""returnself.repo.type_and_source_from_eid(eid,self)# db-api like interface ###################################################defsource_from_eid(self,eid):"""return the source where the entity with id <eid> is located"""returnself.repo.source_from_eid(eid,self)defexecute(self,rql,kwargs=None,eid_key=None,build_descr=True):"""db-api like method directly linked to the querier execute method. See :meth:`cubicweb.dbapi.Cursor.execute` documentation. """ifeid_keyisnotNone:warn('[3.8] eid_key is deprecated, you can safely remove this argument',DeprecationWarning,stacklevel=2)self.timestamp=time()# update timestamprset=self._execute(self,rql,kwargs,build_descr)rset.req=selfreturnrsetdef_clear_thread_data(self,reset_pool=True):"""remove everything from the thread local storage, except pool which is explicitly removed by reset_pool, and mode which is set anyway by _touch """try:txstore=self.__threaddata.txdataexceptAttributeError:passelse:ifreset_pool:self._tx_data.pop(txstore.transactionid,None)try:delself.__threaddata.txdataexceptAttributeError:passelse:fornamein('commit_state','transaction_data','pending_operations','_rewriter'):try:delattr(txstore,name)exceptAttributeError:continuedefcommit(self,reset_pool=True):"""commit the current session's transaction"""ifself.poolisNone:assertnotself.pending_operationsself._clear_thread_data()self._touch()self.debug('commit session %s done (no db activity)',self.id)returncstate=self.commit_stateifcstate=='uncommitable':raiseQueryError('transaction must be rollbacked')ifcstateisnotNone:return# on rollback, an operation should have the following state# information:# - processed by the precommit/commit event or not# - if processed, is it the failed operationtry:# by default, operations are executed with security turned offwithsecurity_enabled(self,False,False):processed=[]self.commit_state='precommit'try:whileself.pending_operations:operation=self.pending_operations.pop(0)operation.processed='precommit'processed.append(operation)operation.handle_event('precommit_event')self.pending_operations[:]=processedself.debug('precommit session %s done',self.id)except:# if error on [pre]commit:## * set .failed = True on the operation causing the failure# * call revert<event>_event on processed operations# * call rollback_event on *all* operations## that seems more natural than not calling rollback_event# for processed operations, and allow generic rollback# instead of having to implements rollback, revertprecommit# and revertcommit, that will be enough in mont case.operation.failed=Trueforoperationinreversed(processed):try:operation.handle_event('revertprecommit_event')except:self.critical('error while reverting precommit',exc_info=True)# XXX use slice notation since self.pending_operations is a# read-only property.self.pending_operations[:]=processed+self.pending_operationsself.rollback(reset_pool)raiseself.pool.commit()self.commit_state='postcommit'whileself.pending_operations:operation=self.pending_operations.pop(0)operation.processed='postcommit'try:operation.handle_event('postcommit_event')except:self.critical('error while postcommit',exc_info=sys.exc_info())self.debug('postcommit session %s done',self.id)returnself.transaction_uuid(set=False)finally:self._touch()ifreset_pool:self.reset_pool(ignoremode=True)self._clear_thread_data(reset_pool)defrollback(self,reset_pool=True):"""rollback the current session's transaction"""# don't use self.pool, rollback may be called with _closed == Truepool=getattr(self._threaddata,'pool',None)ifpoolisNone:self._clear_thread_data()self._touch()self.debug('rollback session %s done (no db activity)',self.id)returntry:# by default, operations are executed with security turned offwithsecurity_enabled(self,False,False):whileself.pending_operations:try:operation=self.pending_operations.pop(0)operation.handle_event('rollback_event')except:self.critical('rollback error',exc_info=sys.exc_info())continuepool.rollback()self.debug('rollback for session %s done',self.id)finally:self._touch()ifreset_pool:self.reset_pool(ignoremode=True)self._clear_thread_data(reset_pool)defclose(self):"""do not close pool on session close, since they are shared now"""withself._closed_lock:self._closed=True# copy since _threads_in_transaction maybe modified while waitingforthread,poolinself._threads_in_transaction.copy():ifthreadisthreading.currentThread():continueself.info('waiting for thread %s',thread)# do this loop/break instead of a simple join(10) in case thread is# the main thread (in which case it will be removed from# self._threads_in_transaction but still be alive...)foriinxrange(10):thread.join(1)ifnot(thread.isAlive()and(thread,pool)inself._threads_in_transaction):breakelse:self.error('thread %s still alive after 10 seconds, will close ''session anyway',thread)self._free_thread_pool(thread,pool,force_close=True)self.rollback()delself.__threaddatadelself._tx_data@propertydefclosed(self):returnnothasattr(self,'_tx_data')# transaction data/operations management ##################################@propertydeftransaction_data(self):try:returnself._threaddata.transaction_dataexceptAttributeError:self._threaddata.transaction_data={}returnself._threaddata.transaction_data@propertydefpending_operations(self):try:returnself._threaddata.pending_operationsexceptAttributeError:self._threaddata.pending_operations=[]returnself._threaddata.pending_operationsdefadd_operation(self,operation,index=None):"""add an observer"""assertself.commit_state!='commit'ifindexisNone:self.pending_operations.append(operation)else:self.pending_operations.insert(index,operation)# undo support ############################################################defundoable_action(self,action,ertype):returnactioninself.undo_actionsandnotertypeinNO_UNDO_TYPES# XXX elif transaction on mark it partialdeftransaction_uuid(self,set=True):try:returnself.transaction_data['tx_uuid']exceptKeyError:ifnotset:returnself.transaction_data['tx_uuid']=uuid=uuid4().hexself.repo.system_source.start_undoable_transaction(self,uuid)returnuuiddeftransaction_inc_action_counter(self):num=self.transaction_data.setdefault('tx_action_count',0)+1self.transaction_data['tx_action_count']=numreturnnum# querier helpers #########################################################@propertydefrql_rewriter(self):# in thread local storage since the rewriter isn't thread safetry:returnself._threaddata._rewriterexceptAttributeError:self._threaddata._rewriter=RQLRewriter(self)returnself._threaddata._rewriterdefbuild_description(self,rqlst,args,result):"""build a description for a given result"""iflen(rqlst.children)==1andlen(rqlst.children[0].solutions)==1:# easy, all lines are identicalselected=rqlst.children[0].selectionsolution=rqlst.children[0].solutions[0]description=_make_description(selected,args,solution)returnRepeatList(len(result),tuple(description))# hard, delegate the work :o)returnself.manual_build_descr(rqlst,args,result)defmanual_build_descr(self,rqlst,args,result):"""build a description for a given result by analysing each row XXX could probably be done more efficiently during execution of query """# not so easy, looks for variable which changes from one solution# to anotherunstables=rqlst.get_variable_indices()basedescr=[]todetermine=[]sampleselect=rqlst.children[0]samplesols=sampleselect.solutions[0]fori,terminenumerate(sampleselect.selection):try:ttype=term.get_type(samplesols,args)exceptCoercionError:ttype=Noneisfinal=Trueelse:ifttypeisNoneorttype=='Any':ttype=Noneisfinal=Trueelse:isfinal=ttypeinBASE_TYPESifttypeisNoneoriinunstables:basedescr.append(None)todetermine.append((i,isfinal))else:basedescr.append(ttype)ifnottodetermine:returnRepeatList(len(result),tuple(basedescr))returnself._build_descr(result,basedescr,todetermine)def_build_descr(self,result,basedescription,todetermine):description=[]etype_from_eid=self.describeforrowinresult:row_descr=basedescription[:]forindex,isfinalintodetermine:value=row[index]ifvalueisNone:# None value inserted by an outer join, no typerow_descr[index]=Nonecontinueifisfinal:row_descr[index]=etype_from_pyobj(value)else:try:row_descr[index]=etype_from_eid(value)[0]exceptUnknownEid:self.critical('wrong eid %s in repository, you should ''db-check the database'%value)row_descr[index]=row[index]=Nonedescription.append(tuple(row_descr))returndescription# deprecated ###############################################################@deprecated("[3.7] execute is now unsafe by default in hooks/operation. You"" can also control security with the security_enabled context ""manager")defunsafe_execute(self,rql,kwargs=None,eid_key=None,build_descr=True,propagate=False):"""like .execute but with security checking disabled (this method is internal to the server, it's not part of the db-api) """withsecurity_enabled(self,read=False,write=False):returnself.execute(rql,kwargs,eid_key,build_descr)@property@deprecated("[3.7] is_super_session is deprecated, test ""session.read_security and or session.write_security")defis_super_session(self):returnnotself.read_securityornotself.write_security@deprecated("[3.7] session is actual session")defactual_session(self):"""return the original parent session if any, else self"""returnself@property@deprecated("[3.6] use session.vreg.schema")defschema(self):returnself.repo.schema@deprecated("[3.4] use vreg['etypes'].etype_class(etype)")defetype_class(self,etype):"""return an entity class for the given entity type"""returnself.vreg['etypes'].etype_class(etype)@deprecated('[3.4] use direct access to session.transaction_data')defquery_data(self,key,default=None,setdefault=False,pop=False):ifsetdefault:assertnotpopreturnself.transaction_data.setdefault(key,default)ifpop:returnself.transaction_data.pop(key,default)else:returnself.transaction_data.get(key,default)@deprecated('[3.4] use entity_from_eid(eid, etype=None)')defentity(self,eid):"""return a result set for the given eid"""returnself.entity_from_eid(eid)# these are overridden by set_log_methods below# only defining here to prevent pylint from complaininginfo=warning=error=critical=exception=debug=lambdamsg,*a,**kw:NoneclassInternalSession(Session):"""special session created internaly by the repository"""is_internal_session=Truerunning_dbapi_query=Falsedef__init__(self,repo,cnxprops=None):super(InternalSession,self).__init__(InternalManager(),repo,cnxprops,_id='internal')self.user._cw=self# XXX remove when "vreg = user._cw.vreg" hack in entity.py is goneself.cnxtype='inmemory'self.disable_hook_categories('integrity')@propertydefpool(self):"""connections pool, set according to transaction mode for each query"""ifself.repo.shutting_down:self.reset_pool(True)raiseException('repository is shutting down')returngetattr(self._threaddata,'pool',None)classInternalManager(object):"""a manager user with all access rights used internally for task such as bootstrapping the repository or creating regular users according to repository content """def__init__(self):self.eid=-1self.login=u'__internal_manager__'self.properties={}defmatching_groups(self,groups):return1defis_in_group(self,group):returnTruedefowns(self,eid):returnTruedefhas_permission(self,pname,contexteid=None):returnTruedefproperty_value(self,key):ifkey=='ui.language':return'en'returnNonefromloggingimportgetLoggerfromcubicwebimportset_log_methodsset_log_methods(Session,getLogger('cubicweb.session'))