Added tag cubicweb-version-3.14.10 for changeset 0ff798f80138
# 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.deprecationimportdeprecatedfromlogilab.common.textutilsimportunormalizefromrqlimportCoercionErrorfromrql.nodesimportETYPE_PYOBJ_MAP,etype_from_pyobjfromyamsimportBASE_TYPESfromcubicwebimportBinary,UnknownEid,QueryError,schemafromcubicweb.selectorsimportobjectify_selectorfromcubicweb.reqimportRequestSessionBasefromcubicweb.dbapiimportConnectionPropertiesfromcubicweb.utilsimportmake_uid,RepeatListfromcubicweb.rqlrewriteimportRQLRewriterfromcubicweb.serverimportShuttingDownfromcubicweb.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))returndescriptiondefselection_idx_type(i,rqlst,args):"""try to return type of term at index `i` of the rqlst's selection"""forselectinrqlst.children:term=select.selection[i]forsolutioninselect.solutions:try:ttype=term.get_type(solution,args)ifttypeisnotNone:returnttypeexceptCoercionError:returnNone@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_session@objectify_selectordefrepairing(cls,req,**kwargs):"""repository side only selector returning 1 if the session is not a regular user session but an internal session """returnreq.vreg.config.repairingclasstransaction(object):"""context manager to enter a transaction for a session: when exiting the `with` block on exception, call `session.rollback()`, else call `session.commit()` on normal exit """def__init__(self,session,free_cnxset=True):self.session=sessionself.free_cnxset=free_cnxsetdef__enter__(self):passdef__exit__(self,exctype,exc,traceback):ifexctype:self.session.rollback(free_cnxset=self.free_cnxset)else:self.session.commit(free_cnxset=self.free_cnxset)classhooks_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.changes=self.session.init_hooks_mode_categories(self.mode,self.categories)def__exit__(self,exctype,exc,traceback):self.session.reset_hooks_mode_categories(self.oldmode,self.mode,self.changes)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):self.oldread,self.oldwrite=self.session.init_security(self.read,self.write)def__exit__(self,exctype,exc,traceback):self.session.reset_security(self.oldread,self.oldwrite)classTransactionData(object):def__init__(self,txid):self.transactionid=txidself.ctx_count=0classSession(RequestSessionBase):"""Repository usersession, tie a session id, user, connections set and other session data all together. About session storage / transactions ------------------------------------ Here is a description of internal session attributes. Besides :attr:`data` and :attr:`transaction_data`, you should not have to use attributes described here but higher level APIs. :attr:`data` is a dictionary containing shared data, used to communicate extra information between the client and the repository :attr:`_tx_data` is a dictionary of :class:`TransactionData` instance, one for each running transaction. The key is the transaction id. By default the transaction id is the thread name but it can be otherwise (per dbapi cursor for instance, or per thread name *from another process*). :attr:`__threaddata` is a thread local storage whose `txdata` attribute refers to the proper instance of :class:`TransactionData` according to the transaction. :attr:`_threads_in_transaction` is a set of (thread, connections set) referencing threads that currently hold a connections set for the session. You should not have to use neither :attr:`_txdata` nor :attr:`__threaddata`, simply access transaction data transparently through the :attr:`_threaddata` property. Also, you usually don't have to access it directly since current transaction's data may be accessed/modified through properties / methods: :attr:`transaction_data`, similarly to :attr:`data`, is a dictionary containing some shared data that should be cleared at the end of the transaction. Hooks and operations may put arbitrary data in there, and this may also be used as a communication channel between the client and the repository. :attr:`cnxset`, the connections set to use to execute queries on sources. During a transaction, the connection set may be freed so that is may be used by another session as long as no writing is done. This means we can have multiple sessions with a reasonably low connections set pool size. :attr:`mode`, string telling the connections set handling mode, may be one of 'read' (connections set may be freed), 'write' (some write was done in the connections set, it can't be freed before end of the transaction), 'transaction' (we want to keep the connections set during all the transaction, with or without writing) :attr:`pending_operations`, ordered list of operations to be processed on commit/rollback :attr:`commit_state`, describing the transaction commit state, may be one of None (not yet committing), 'precommit' (calling precommit event on operations), 'postcommit' (calling postcommit event on operations), 'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error has been raised during the transaction and so it must be rollbacked). :attr:`read_security` and :attr:`write_security`, boolean flags telling if read/write security is currently activated. :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. :attr:`running_dbapi_query`, boolean flag telling if the executing query is coming from a dbapi connection or is a query from within the repository """is_internal_session=Falsedef__init__(self,user,repo,cnxprops=None,_id=None):super(Session,self).__init__(repo.vreg)self.id=_idormake_uid(unormalize(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))deftransaction(self,free_cnxset=True):"""return context manager to enter a transaction for the session: when exiting the `with` block on exception, call `session.rollback()`, else call `session.commit()` on normal exit. The `free_cnxset` will be given to rollback/commit methods to indicate wether the connections set should be freed or not. """returntransaction(self,free_cnxset)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.cnxset=self.cnxset# we attributed a connections set, need to update ctx_count else it will be freed# while undesiredthreaddata.ctx_count=1# 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.cnxset.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.cnxset.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',())defrtype_eids_rdef(self,rtype,eidfrom,eidto):# use type_and_source_from_eid instead of type_from_eid for optimization# (avoid two extra methods call)subjtype=self.repo.type_and_source_from_eid(eidfrom,self)[0]objtype=self.repo.type_and_source_from_eid(eidto,self)[0]returnself.vreg.schema.rschema(rtype).rdefs[(subjtype,objtype)]# security control #########################################################DEFAULT_SECURITY=object()# evaluated to true by designdefsecurity_enabled(self,read=False,write=False):returnsecurity_enabled(self,read=read,write=write)definit_security(self,read,write):ifreadisNone:oldread=Noneelse:oldread=self.set_read_security(read)ifwriteisNone:oldwrite=Noneelse:oldwrite=self.set_write_security(write)self._threaddata.ctx_count+=1returnoldread,oldwritedefreset_security(self,read,write):txstore=self._threaddatatxstore.ctx_count-=1iftxstore.ctx_count==0:self._clear_thread_storage(txstore)else:ifreadisnotNone:self.set_read_security(read)ifwriteisnotNone:self.set_write_security(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_securityexceptAttributeError: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=modereturnoldmodedefinit_hooks_mode_categories(self,mode,categories):oldmode=self.set_hooks_mode(mode)ifmodeisself.HOOKS_DENY_ALL:changes=self.enable_hook_categories(*categories)else:changes=self.disable_hook_categories(*categories)self._threaddata.ctx_count+=1returnoldmode,changesdefreset_hooks_mode_categories(self,oldmode,mode,categories):txstore=self._threaddatatxstore.ctx_count-=1iftxstore.ctx_count==0:self._clear_thread_storage(txstore)else:try:ifcategories:ifmodeisself.HOOKS_DENY_ALL:returnself.disable_hook_categories(*categories)else:returnself.enable_hook_categories(*categories)finally:self.set_hooks_mode(oldmode)@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()self.pruned_hooks_cache.clear()ifself.hooks_modeisself.HOOKS_DENY_ALL:enabledcats=self.enabled_hook_categoriesforcategoryincategories:ifcategoryinenabledcats:enabledcats.remove(category)changes.add(category)else:disabledcats=self.disabled_hook_categoriesforcategoryincategories:ifcategorynotindisabledcats:disabledcats.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()self.pruned_hooks_cache.clear()ifself.hooks_modeisself.HOOKS_DENY_ALL:enabledcats=self.enabled_hook_categoriesforcategoryincategories:ifcategorynotinenabledcats:enabledcats.add(category)changes.add(category)else:disabledcats=self.disabled_hook_categoriesforcategoryincategories:ifcategoryindisabledcats:disabledcats.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_cnxset_mode(self,mode):"""set `mode`, e.g. how the session will keep its connections set: * if mode == 'write', the connections set 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 connections set is only freed after the transaction's end notice that a repository has a limited set of connections sets, and a session has to wait for a free connections set to run any rql query (unless it already has one 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)@propertydefcnxset(self):"""connections set, set according to transaction mode for each query"""ifself._closed:self.free_cnxset(True)raiseException('try to access connections set on a closed session %s'%self.id)returngetattr(self._threaddata,'cnxset',None)defset_cnxset(self):"""the session need a connections set to execute some queries"""withself._closed_lock:ifself._closed:self.free_cnxset(True)raiseException('try to set connections set on a closed session %s'%self.id)ifself.cnxsetisNone:# get connections set first to avoid race-conditionself._threaddata.cnxset=cnxset=self.repo._get_cnxset()self._threaddata.ctx_count+=1try:cnxset.cnxset_set()exceptException:self._threaddata.cnxset=Noneself.repo._free_cnxset(cnxset)raiseself._threads_in_transaction.add((threading.currentThread(),cnxset))returnself._threaddata.cnxsetdef_free_thread_cnxset(self,thread,cnxset,force_close=False):try:self._threads_in_transaction.remove((thread,cnxset))exceptKeyError:# race condition on cnxset freeing (freed by commit or rollback vs# close)passelse:ifforce_close:cnxset.reconnect()else:cnxset.cnxset_freed()# free cnxset once everything is done to avoid race-conditionself.repo._free_cnxset(cnxset)deffree_cnxset(self,ignoremode=False):"""the session is no longer using its connections set, at least for some time"""# cnxset may be none if no operation has been done since last commit# or rollbackcnxset=getattr(self._threaddata,'cnxset',None)ifcnxsetisnotNoneand(ignoremodeorself.mode=='read'):# even in read mode, we must release the current transactionself._free_thread_cnxset(threading.currentThread(),cnxset)delself._threaddata.cnxsetself._threaddata.ctx_count-=1def_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,asdict=False):"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""metas=self.repo.type_and_source_from_eid(eid,self)ifasdict:returndict(zip(('type','source','extid','asource'),metas))# XXX :-1 for cw compat, use asdict=True for full informationreturnmetas[:-1]# 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,free_cnxset=True):"""remove everything from the thread local storage, except connections set which is explicitly removed by free_cnxset, and mode which is set anyway by _touch """try:txstore=self.__threaddata.txdataexceptAttributeError:passelse:iffree_cnxset:self.free_cnxset()iftxstore.ctx_count==0:self._clear_thread_storage(txstore)else:self._clear_tx_storage(txstore)else:self._clear_tx_storage(txstore)def_clear_thread_storage(self,txstore):self._tx_data.pop(txstore.transactionid,None)try:delself.__threaddata.txdataexceptAttributeError:passdef_clear_tx_storage(self,txstore):fornamein('commit_state','transaction_data','pending_operations','_rewriter','pruned_hooks_cache'):try:delattr(txstore,name)exceptAttributeError:continuedefcommit(self,free_cnxset=True,reset_pool=None):"""commit the current session's transaction"""ifreset_poolisnotNone:warn('[3.13] use free_cnxset argument instead for reset_pool',DeprecationWarning,stacklevel=2)free_cnxset=reset_poolifself.cnxsetisNone: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)exceptBaseException:# 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')exceptBaseException: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(free_cnxset)raiseself.cnxset.commit()self.commit_state='postcommit'whileself.pending_operations:operation=self.pending_operations.pop(0)operation.processed='postcommit'try:operation.handle_event('postcommit_event')exceptBaseException: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()iffree_cnxset:self.free_cnxset(ignoremode=True)self._clear_thread_data(free_cnxset)defrollback(self,free_cnxset=True,reset_pool=None):"""rollback the current session's transaction"""ifreset_poolisnotNone:warn('[3.13] use free_cnxset argument instead for reset_pool',DeprecationWarning,stacklevel=2)free_cnxset=reset_pool# don't use self.cnxset, rollback may be called with _closed == Truecnxset=getattr(self._threaddata,'cnxset',None)ifcnxsetisNone: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')exceptBaseException:self.critical('rollback error',exc_info=sys.exc_info())continuecnxset.rollback()self.debug('rollback for session %s done',self.id)finally:self._touch()iffree_cnxset:self.free_cnxset(ignoremode=True)self._clear_thread_data(free_cnxset)defclose(self):"""do not close connections set on session close, since they are shared now"""withself._closed_lock:self._closed=True# copy since _threads_in_transaction maybe modified while waitingforthread,cnxsetinself._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,cnxset)inself._threads_in_transaction):breakelse:self.error('thread %s still alive after 10 seconds, will close ''session anyway',thread)self._free_thread_cnxset(thread,cnxset,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_operations@propertydefpruned_hooks_cache(self):try:returnself._threaddata.pruned_hooks_cacheexceptAttributeError:self._threaddata.pruned_hooks_cache={}returnself._threaddata.pruned_hooks_cachedefadd_operation(self,operation,index=None):"""add an operation"""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=[]foriinxrange(len(rqlst.children[0].selection)):ttype=selection_idx_type(i,rqlst,args)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.describetodel=[]fori,rowinenumerate(result):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.error('wrong eid %s in repository, you should ''db-check the database'%value)todel.append(i)breakelse:description.append(tuple(row_descr))foriinreversed(todel):delresult[i]returndescription# deprecated ###############################################################@deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')defschema_rproperty(self,rtype,eidfrom,eidto,rprop):returngetattr(self.rtype_eids_rdef(rtype,eidfrom,eidto),rprop)@property@deprecated("[3.13] use .cnxset attribute instead of .pool")defpool(self):returnself.cnxset@deprecated("[3.13] use .set_cnxset() method instead of .set_pool()")defset_pool(self):returnself.set_cnxset()@deprecated("[3.13] use .free_cnxset() method instead of .reset_pool()")defreset_pool(self):returnself.free_cnxset()@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# 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,safe=False):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'ifnotsafe:self.disable_hook_categories('integrity')@propertydefcnxset(self):"""connections set, set according to transaction mode for each query"""ifself.repo.shutting_down:self.free_cnxset(True)raiseShuttingDown('repository is shutting down')returngetattr(self._threaddata,'cnxset',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):returnTruedefproperty_value(self,key):ifkey=='ui.language':return'en'returnNonefromloggingimportgetLoggerfromcubicwebimportset_log_methodsset_log_methods(Session,getLogger('cubicweb.session'))