server/session.py
changeset 9077 55a88fbfd39c
parent 9076 7743e7b29635
child 9078 cfefd64c7039
equal deleted inserted replaced
9076:7743e7b29635 9077:55a88fbfd39c
   561         If eid is None, the whole cache is dropped"""
   561         If eid is None, the whole cache is dropped"""
   562         if eid is None:
   562         if eid is None:
   563             self.transaction_data.pop('ecache', None)
   563             self.transaction_data.pop('ecache', None)
   564         else:
   564         else:
   565             del self.transaction_data['ecache'][eid]
   565             del self.transaction_data['ecache'][eid]
   566 
       
   567     # Tracking of entity added of removed in the transaction ##################
       
   568     #
       
   569     # Those are function to  allows cheap call from client in other process.
       
   570 
       
   571     def deleted_in_transaction(self, eid):
       
   572         """return True if the entity of the given eid is being deleted in the
       
   573         current transaction
       
   574         """
       
   575         return eid in self.transaction_data.get('pendingeids', ())
       
   576 
       
   577     def added_in_transaction(self, eid):
       
   578         """return True if the entity of the given eid is being created in the
       
   579         current transaction
       
   580         """
       
   581         return eid in self.transaction_data.get('neweids', ())
       
   582 
       
   583     # Operation management ####################################################
       
   584 
       
   585     def add_operation(self, operation, index=None):
       
   586         """add an operation to be executed at the end of the transaction"""
       
   587         if index is None:
       
   588             self.pending_operations.append(operation)
       
   589         else:
       
   590             self.pending_operations.insert(index, operation)
       
   591 
       
   592     # Hooks control ###########################################################
       
   593 
       
   594     def allow_all_hooks_but(self, *categories):
       
   595         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
       
   596 
       
   597     def deny_all_hooks_but(self, *categories):
       
   598         return _hooks_control(self, HOOKS_DENY_ALL, *categories)
       
   599 
       
   600     def disable_hook_categories(self, *categories):
       
   601         """disable the given hook categories:
       
   602 
       
   603         - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
       
   604         - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
       
   605         """
       
   606         changes = set()
       
   607         self.pruned_hooks_cache.clear()
       
   608         categories = set(categories)
       
   609         if self.hooks_mode is HOOKS_DENY_ALL:
       
   610             enabledcats = self.enabled_hook_cats
       
   611             changes = enabledcats & categories
       
   612             enabledcats -= changes # changes is small hence faster
       
   613         else:
       
   614             disabledcats = self.disabled_hook_cats
       
   615             changes = categories - disabledcats
       
   616             disabledcats |= changes # changes is small hence faster
       
   617         return tuple(changes)
       
   618 
       
   619     def enable_hook_categories(self, *categories):
       
   620         """enable the given hook categories:
       
   621 
       
   622         - on HOOKS_DENY_ALL mode, ensure those categories are enabled
       
   623         - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
       
   624         """
       
   625         changes = set()
       
   626         self.pruned_hooks_cache.clear()
       
   627         categories = set(categories)
       
   628         if self.hooks_mode is HOOKS_DENY_ALL:
       
   629             enabledcats = self.enabled_hook_cats
       
   630             changes = categories - enabledcats
       
   631             enabledcats |= changes # changes is small hence faster
       
   632         else:
       
   633             disabledcats = self.disabled_hook_cats
       
   634             changes = disabledcats & categories
       
   635             disabledcats -= changes # changes is small hence faster
       
   636         return tuple(changes)
       
   637 
       
   638     def is_hook_category_activated(self, category):
       
   639         """return a boolean telling if the given category is currently activated
       
   640         or not
       
   641         """
       
   642         if self.hooks_mode is HOOKS_DENY_ALL:
       
   643             return category in self.enabled_hook_cats
       
   644         return category not in self.disabled_hook_cats
       
   645 
       
   646     def is_hook_activated(self, hook):
       
   647         """return a boolean telling if the given hook class is currently
       
   648         activated or not
       
   649         """
       
   650         return self.is_hook_category_activated(hook.category)
       
   651 
       
   652     # Security management #####################################################
       
   653 
       
   654     def security_enabled(self, read=None, write=None):
       
   655         return _security_enabled(self, read=read, write=write)
       
   656 
       
   657     @property
       
   658     def read_security(self):
       
   659         return self._read_security
       
   660 
       
   661     @read_security.setter
       
   662     def read_security(self, activated):
       
   663         oldmode = self._read_security
       
   664         self._read_security = activated
       
   665         # running_dbapi_query used to detect hooks triggered by a 'dbapi' query
       
   666         # (eg not issued on the session). This is tricky since we the execution
       
   667         # model of a (write) user query is:
       
   668         #
       
   669         # repository.execute (security enabled)
       
   670         #  \-> querier.execute
       
   671         #       \-> repo.glob_xxx (add/update/delete entity/relation)
       
   672         #            \-> deactivate security before calling hooks
       
   673         #                 \-> WE WANT TO CHECK QUERY NATURE HERE
       
   674         #                      \-> potentially, other calls to querier.execute
       
   675         #
       
   676         # so we can't rely on simply checking session.read_security, but
       
   677         # recalling the first transition from DEFAULT_SECURITY to something
       
   678         # else (False actually) is not perfect but should be enough
       
   679         #
       
   680         # also reset running_dbapi_query to true when we go back to
       
   681         # DEFAULT_SECURITY
       
   682         self.running_dbapi_query = (oldmode is DEFAULT_SECURITY
       
   683                                     or activated is DEFAULT_SECURITY)
       
   684 
       
   685     # undo support ############################################################
       
   686 
       
   687     def ertype_supports_undo(self, ertype):
       
   688         return self.undo_actions and ertype not in NO_UNDO_TYPES
       
   689 
       
   690     def transaction_uuid(self, set=True):
       
   691         uuid = self.transaction_data.get('tx_uuid')
       
   692         if set and uuid is None:
       
   693             self.transaction_data['tx_uuid'] = uuid = uuid4().hex
       
   694             self.repo.system_source.start_undoable_transaction(self, uuid)
       
   695         return uuid
       
   696 
       
   697     def transaction_inc_action_counter(self):
       
   698         num = self.transaction_data.setdefault('tx_action_count', 0) + 1
       
   699         self.transaction_data['tx_action_count'] = num
       
   700         return num
       
   701     # db-api like interface ###################################################
       
   702 
       
   703     def source_defs(self):
       
   704         return self.repo.source_defs()
       
   705 
       
   706     def describe(self, eid, asdict=False):
       
   707         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   708         metas = self.repo.type_and_source_from_eid(eid, self)
       
   709         if asdict:
       
   710             return dict(zip(('type', 'source', 'extid', 'asource'), metas))
       
   711        # XXX :-1 for cw compat, use asdict=True for full information
       
   712         return metas[:-1]
       
   713 
       
   714 
       
   715     def source_from_eid(self, eid):
       
   716         """return the source where the entity with id <eid> is located"""
       
   717         return self.repo.source_from_eid(eid, self)
       
   718 
       
   719     # resource accessors ######################################################
       
   720 
       
   721     def system_sql(self, sql, args=None, rollback_on_failure=True):
       
   722         """return a sql cursor on the system database"""
       
   723         if sql.split(None, 1)[0].upper() != 'SELECT':
       
   724             self.mode = 'write'
       
   725         source = self.cnxset.source('system')
       
   726         try:
       
   727             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   728         except (source.OperationalError, source.InterfaceError):
       
   729             if not rollback_on_failure:
       
   730                 raise
       
   731             source.warning("trying to reconnect")
       
   732             self.cnxset.reconnect(source)
       
   733             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   734 
       
   735     def rtype_eids_rdef(self, rtype, eidfrom, eidto):
       
   736         # use type_and_source_from_eid instead of type_from_eid for optimization
       
   737         # (avoid two extra methods call)
       
   738         subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
       
   739         objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
       
   740         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
       
   741 
       
   742 
       
   743 def cnx_attr(attr_name, writable=False):
       
   744     """return a property to forward attribute access to connection.
       
   745 
       
   746     This is to be used by session"""
       
   747     args = {}
       
   748     def attr_from_cnx(session):
       
   749         return getattr(session._cnx, attr_name)
       
   750     args['fget'] = attr_from_cnx
       
   751     if writable:
       
   752         def write_attr(session, value):
       
   753             return setattr(session._cnx, attr_name, value)
       
   754         args['fset'] = write_attr
       
   755     return property(**args)
       
   756 
       
   757 def cnx_meth(meth_name):
       
   758     """return a function forwarding calls to connection.
       
   759 
       
   760     This is to be used by session"""
       
   761     def meth_from_cnx(session, *args, **kwargs):
       
   762         return getattr(session._cnx, meth_name)(*args, **kwargs)
       
   763     return meth_from_cnx
       
   764 
       
   765 
       
   766 class Session(RequestSessionBase):
       
   767     """Repository user session
       
   768 
       
   769     This tie all together:
       
   770      * session id,
       
   771      * user,
       
   772      * connections set,
       
   773      * other session data.
       
   774 
       
   775     About session storage / transactions
       
   776     ------------------------------------
       
   777 
       
   778     Here is a description of internal session attributes. Besides :attr:`data`
       
   779     and :attr:`transaction_data`, you should not have to use attributes
       
   780     described here but higher level APIs.
       
   781 
       
   782       :attr:`data` is a dictionary containing shared data, used to communicate
       
   783       extra information between the client and the repository
       
   784 
       
   785       :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one
       
   786       for each running connection. The key is the connection id. By default
       
   787       the connection id is the thread name but it can be otherwise (per dbapi
       
   788       cursor for instance, or per thread name *from another process*).
       
   789 
       
   790       :attr:`__threaddata` is a thread local storage whose `cnx` attribute
       
   791       refers to the proper instance of :class:`Connection` according to the
       
   792       connection.
       
   793 
       
   794     You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`,
       
   795     simply access connection data transparently through the :attr:`_cnx`
       
   796     property. Also, you usually don't have to access it directly since current
       
   797     connection's data may be accessed/modified through properties / methods:
       
   798 
       
   799       :attr:`connection_data`, similarly to :attr:`data`, is a dictionary
       
   800       containing some shared data that should be cleared at the end of the
       
   801       connection. Hooks and operations may put arbitrary data in there, and
       
   802       this may also be used as a communication channel between the client and
       
   803       the repository.
       
   804 
       
   805     .. automethod:: cubicweb.server.session.Session.get_shared_data
       
   806     .. automethod:: cubicweb.server.session.Session.set_shared_data
       
   807     .. automethod:: cubicweb.server.session.Session.added_in_transaction
       
   808     .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
       
   809 
       
   810     Connection state information:
       
   811 
       
   812       :attr:`running_dbapi_query`, boolean flag telling if the executing query
       
   813       is coming from a dbapi connection or is a query from within the repository
       
   814 
       
   815       :attr:`cnxset`, the connections set to use to execute queries on sources.
       
   816       During a transaction, the connection set may be freed so that is may be
       
   817       used by another session as long as no writing is done. This means we can
       
   818       have multiple sessions with a reasonably low connections set pool size.
       
   819 
       
   820     .. automethod:: cubicweb.server.session.set_cnxset
       
   821     .. automethod:: cubicweb.server.session.free_cnxset
       
   822 
       
   823       :attr:`mode`, string telling the connections set handling mode, may be one
       
   824       of 'read' (connections set may be freed), 'write' (some write was done in
       
   825       the connections set, it can't be freed before end of the transaction),
       
   826       'transaction' (we want to keep the connections set during all the
       
   827       transaction, with or without writing)
       
   828 
       
   829       :attr:`pending_operations`, ordered list of operations to be processed on
       
   830       commit/rollback
       
   831 
       
   832       :attr:`commit_state`, describing the transaction commit state, may be one
       
   833       of None (not yet committing), 'precommit' (calling precommit event on
       
   834       operations), 'postcommit' (calling postcommit event on operations),
       
   835       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
       
   836       has been raised during the transaction and so it must be rollbacked).
       
   837 
       
   838     .. automethod:: cubicweb.server.session.Session.commit
       
   839     .. automethod:: cubicweb.server.session.Session.rollback
       
   840     .. automethod:: cubicweb.server.session.Session.close
       
   841     .. automethod:: cubicweb.server.session.Session.closed
       
   842 
       
   843     Security level Management:
       
   844 
       
   845       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
       
   846       read/write security is currently activated.
       
   847 
       
   848     .. automethod:: cubicweb.server.session.Session.security_enabled
       
   849 
       
   850     Hooks Management:
       
   851 
       
   852       :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
       
   853 
       
   854       :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is
       
   855       `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
       
   856 
       
   857       :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is
       
   858       `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
       
   859 
       
   860     .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but
       
   861     .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but
       
   862     .. automethod:: cubicweb.server.session.Session.is_hook_category_activated
       
   863     .. automethod:: cubicweb.server.session.Session.is_hook_activated
       
   864 
       
   865     Data manipulation:
       
   866 
       
   867     .. automethod:: cubicweb.server.session.Session.add_relation
       
   868     .. automethod:: cubicweb.server.session.Session.add_relations
       
   869     .. automethod:: cubicweb.server.session.Session.delete_relation
       
   870 
       
   871     Other:
       
   872 
       
   873     .. automethod:: cubicweb.server.session.Session.call_service
       
   874 
       
   875 
       
   876 
       
   877     """
       
   878     is_request = False
       
   879     is_internal_session = False
       
   880 
       
   881     def __init__(self, user, repo, cnxprops=None, _id=None):
       
   882         super(Session, self).__init__(repo.vreg)
       
   883         self.id = _id or make_uid(unormalize(user.login).encode('UTF8'))
       
   884         self.user = user
       
   885         self.repo = repo
       
   886         self.timestamp = time()
       
   887         self.default_mode = 'read'
       
   888         # short cut to querier .execute method
       
   889         self._execute = repo.querier.execute
       
   890         # shared data, used to communicate extra information between the client
       
   891         # and the rql server
       
   892         self.data = {}
       
   893         # i18n initialization
       
   894         self.set_language(user.prefered_language())
       
   895         ### internals
       
   896         # Connection of this section
       
   897         self._cnxs = {}
       
   898         # Data local to the thread
       
   899         self.__threaddata = threading.local()
       
   900         self._cnxset_tracker = CnxSetTracker()
       
   901         self._closed = False
       
   902         self._lock = threading.RLock()
       
   903 
       
   904     def __unicode__(self):
       
   905         return '<session %s (%s 0x%x)>' % (
       
   906             unicode(self.user.login), self.id, id(self))
       
   907 
       
   908     @property
       
   909     def sessionid(self):
       
   910         return self.id
       
   911 
       
   912     @property
       
   913     def login(self):
       
   914         # XXX backward compat with dbapi. deprecate me ASAP.
       
   915         return self.user.login
       
   916 
       
   917     def get_cnx(self, cnxid):
       
   918         """return the <cnxid> connection attached to this session
       
   919 
       
   920         Connection is created if necessary"""
       
   921         with self._lock: # no connection exist with the same id
       
   922             try:
       
   923                 if self.closed:
       
   924                     raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
       
   925                 cnx = self._cnxs[cnxid]
       
   926             except KeyError:
       
   927                 rewriter = RQLRewriter(self)
       
   928                 cnx = Connection(cnxid, self, rewriter)
       
   929                 self._cnxs[cnxid] = cnx
       
   930         return cnx
       
   931 
       
   932     def set_cnx(self, cnxid=None):
       
   933         """set the default connection of the current thread to <cnxid>
       
   934 
       
   935         Connection is created if necessary"""
       
   936         if cnxid is None:
       
   937             cnxid = threading.currentThread().getName()
       
   938         self.__threaddata.cnx = self.get_cnx(cnxid)
       
   939 
       
   940     @property
       
   941     def _cnx(self):
       
   942         """default connection for current session in current thread"""
       
   943         try:
       
   944             return self.__threaddata.cnx
       
   945         except AttributeError:
       
   946             self.set_cnx()
       
   947             return self.__threaddata.cnx
       
   948 
       
   949     @property
       
   950     def _current_cnx_id(self):
       
   951         """TRANSITIONAL PURPOSE"""
       
   952         try:
       
   953             return self.__threaddata.cnx.transactionid
       
   954         except AttributeError:
       
   955             return None
       
   956 
       
   957     def get_option_value(self, option, foreid=None):
       
   958         return self.repo.get_option_value(option, foreid)
       
   959 
       
   960     def transaction(self, free_cnxset=True):
       
   961         """return context manager to enter a transaction for the session: when
       
   962         exiting the `with` block on exception, call `session.rollback()`, else
       
   963         call `session.commit()` on normal exit.
       
   964 
       
   965         The `free_cnxset` will be given to rollback/commit methods to indicate
       
   966         wether the connections set should be freed or not.
       
   967         """
       
   968         return transaction(self, free_cnxset)
       
   969 
       
   970     def add_relation(self, fromeid, rtype, toeid):
       
   971         """provide direct access to the repository method to add a relation.
       
   972 
       
   973         This is equivalent to the following rql query:
       
   974 
       
   975           SET X rtype Y WHERE X eid  fromeid, T eid toeid
       
   976 
       
   977         without read security check but also all the burden of rql execution.
       
   978         You may use this in hooks when you know both eids of the relation you
       
   979         want to add.
       
   980         """
       
   981         self.add_relations([(rtype, [(fromeid,  toeid)])])
       
   982 
       
   983     def add_relations(self, relations):
       
   984         '''set many relation using a shortcut similar to the one in add_relation
       
   985 
       
   986         relations is a list of 2-uples, the first element of each
       
   987         2-uple is the rtype, and the second is a list of (fromeid,
       
   988         toeid) tuples
       
   989         '''
       
   990         edited_entities = {}
       
   991         relations_dict = {}
       
   992         with self.security_enabled(False, False):
       
   993             for rtype, eids in relations:
       
   994                 if self.vreg.schema[rtype].inlined:
       
   995                     for fromeid, toeid in eids:
       
   996                         if fromeid not in edited_entities:
       
   997                             entity = self.entity_from_eid(fromeid)
       
   998                             edited = EditedEntity(entity)
       
   999                             edited_entities[fromeid] = edited
       
  1000                         else:
       
  1001                             edited = edited_entities[fromeid]
       
  1002                         edited.edited_attribute(rtype, toeid)
       
  1003                 else:
       
  1004                     relations_dict[rtype] = eids
       
  1005             self.repo.glob_add_relations(self, relations_dict)
       
  1006             for edited in edited_entities.itervalues():
       
  1007                 self.repo.glob_update_entity(self, edited)
       
  1008 
       
  1009 
       
  1010     def delete_relation(self, fromeid, rtype, toeid):
       
  1011         """provide direct access to the repository method to delete a relation.
       
  1012 
       
  1013         This is equivalent to the following rql query:
       
  1014 
       
  1015           DELETE X rtype Y WHERE X eid  fromeid, T eid toeid
       
  1016 
       
  1017         without read security check but also all the burden of rql execution.
       
  1018         You may use this in hooks when you know both eids of the relation you
       
  1019         want to delete.
       
  1020         """
       
  1021         with self.security_enabled(False, False):
       
  1022             if self.vreg.schema[rtype].inlined:
       
  1023                 entity = self.entity_from_eid(fromeid)
       
  1024                 entity.cw_attr_cache[rtype] = None
       
  1025                 self.repo.glob_update_entity(self, entity, set((rtype,)))
       
  1026             else:
       
  1027                 self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
       
  1028 
   566 
  1029     # relations cache handling #################################################
   567     # relations cache handling #################################################
  1030 
   568 
  1031     def update_rel_cache_add(self, subject, rtype, object, symmetric=False):
   569     def update_rel_cache_add(self, subject, rtype, object, symmetric=False):
  1032         self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
   570         self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
  1090                 del rset.description[idx]
   628                 del rset.description[idx]
  1091             del entities[idx]
   629             del entities[idx]
  1092             rset.rowcount -= 1
   630             rset.rowcount -= 1
  1093             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
   631             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
  1094                 rset, tuple(entities))
   632                 rset, tuple(entities))
       
   633 
       
   634     # Tracking of entity added of removed in the transaction ##################
       
   635     #
       
   636     # Those are function to  allows cheap call from client in other process.
       
   637 
       
   638     def deleted_in_transaction(self, eid):
       
   639         """return True if the entity of the given eid is being deleted in the
       
   640         current transaction
       
   641         """
       
   642         return eid in self.transaction_data.get('pendingeids', ())
       
   643 
       
   644     def added_in_transaction(self, eid):
       
   645         """return True if the entity of the given eid is being created in the
       
   646         current transaction
       
   647         """
       
   648         return eid in self.transaction_data.get('neweids', ())
       
   649 
       
   650     # Operation management ####################################################
       
   651 
       
   652     def add_operation(self, operation, index=None):
       
   653         """add an operation to be executed at the end of the transaction"""
       
   654         if index is None:
       
   655             self.pending_operations.append(operation)
       
   656         else:
       
   657             self.pending_operations.insert(index, operation)
       
   658 
       
   659     # Hooks control ###########################################################
       
   660 
       
   661     def allow_all_hooks_but(self, *categories):
       
   662         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
       
   663 
       
   664     def deny_all_hooks_but(self, *categories):
       
   665         return _hooks_control(self, HOOKS_DENY_ALL, *categories)
       
   666 
       
   667     def disable_hook_categories(self, *categories):
       
   668         """disable the given hook categories:
       
   669 
       
   670         - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
       
   671         - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
       
   672         """
       
   673         changes = set()
       
   674         self.pruned_hooks_cache.clear()
       
   675         categories = set(categories)
       
   676         if self.hooks_mode is HOOKS_DENY_ALL:
       
   677             enabledcats = self.enabled_hook_cats
       
   678             changes = enabledcats & categories
       
   679             enabledcats -= changes # changes is small hence faster
       
   680         else:
       
   681             disabledcats = self.disabled_hook_cats
       
   682             changes = categories - disabledcats
       
   683             disabledcats |= changes # changes is small hence faster
       
   684         return tuple(changes)
       
   685 
       
   686     def enable_hook_categories(self, *categories):
       
   687         """enable the given hook categories:
       
   688 
       
   689         - on HOOKS_DENY_ALL mode, ensure those categories are enabled
       
   690         - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
       
   691         """
       
   692         changes = set()
       
   693         self.pruned_hooks_cache.clear()
       
   694         categories = set(categories)
       
   695         if self.hooks_mode is HOOKS_DENY_ALL:
       
   696             enabledcats = self.enabled_hook_cats
       
   697             changes = categories - enabledcats
       
   698             enabledcats |= changes # changes is small hence faster
       
   699         else:
       
   700             disabledcats = self.disabled_hook_cats
       
   701             changes = disabledcats & categories
       
   702             disabledcats -= changes # changes is small hence faster
       
   703         return tuple(changes)
       
   704 
       
   705     def is_hook_category_activated(self, category):
       
   706         """return a boolean telling if the given category is currently activated
       
   707         or not
       
   708         """
       
   709         if self.hooks_mode is HOOKS_DENY_ALL:
       
   710             return category in self.enabled_hook_cats
       
   711         return category not in self.disabled_hook_cats
       
   712 
       
   713     def is_hook_activated(self, hook):
       
   714         """return a boolean telling if the given hook class is currently
       
   715         activated or not
       
   716         """
       
   717         return self.is_hook_category_activated(hook.category)
       
   718 
       
   719     # Security management #####################################################
       
   720 
       
   721     def security_enabled(self, read=None, write=None):
       
   722         return _security_enabled(self, read=read, write=write)
       
   723 
       
   724     @property
       
   725     def read_security(self):
       
   726         return self._read_security
       
   727 
       
   728     @read_security.setter
       
   729     def read_security(self, activated):
       
   730         oldmode = self._read_security
       
   731         self._read_security = activated
       
   732         # running_dbapi_query used to detect hooks triggered by a 'dbapi' query
       
   733         # (eg not issued on the session). This is tricky since we the execution
       
   734         # model of a (write) user query is:
       
   735         #
       
   736         # repository.execute (security enabled)
       
   737         #  \-> querier.execute
       
   738         #       \-> repo.glob_xxx (add/update/delete entity/relation)
       
   739         #            \-> deactivate security before calling hooks
       
   740         #                 \-> WE WANT TO CHECK QUERY NATURE HERE
       
   741         #                      \-> potentially, other calls to querier.execute
       
   742         #
       
   743         # so we can't rely on simply checking session.read_security, but
       
   744         # recalling the first transition from DEFAULT_SECURITY to something
       
   745         # else (False actually) is not perfect but should be enough
       
   746         #
       
   747         # also reset running_dbapi_query to true when we go back to
       
   748         # DEFAULT_SECURITY
       
   749         self.running_dbapi_query = (oldmode is DEFAULT_SECURITY
       
   750                                     or activated is DEFAULT_SECURITY)
       
   751 
       
   752     # undo support ############################################################
       
   753 
       
   754     def ertype_supports_undo(self, ertype):
       
   755         return self.undo_actions and ertype not in NO_UNDO_TYPES
       
   756 
       
   757     def transaction_uuid(self, set=True):
       
   758         uuid = self.transaction_data.get('tx_uuid')
       
   759         if set and uuid is None:
       
   760             self.transaction_data['tx_uuid'] = uuid = uuid4().hex
       
   761             self.repo.system_source.start_undoable_transaction(self, uuid)
       
   762         return uuid
       
   763 
       
   764     def transaction_inc_action_counter(self):
       
   765         num = self.transaction_data.setdefault('tx_action_count', 0) + 1
       
   766         self.transaction_data['tx_action_count'] = num
       
   767         return num
       
   768     # db-api like interface ###################################################
       
   769 
       
   770     def source_defs(self):
       
   771         return self.repo.source_defs()
       
   772 
       
   773     def describe(self, eid, asdict=False):
       
   774         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   775         metas = self.repo.type_and_source_from_eid(eid, self)
       
   776         if asdict:
       
   777             return dict(zip(('type', 'source', 'extid', 'asource'), metas))
       
   778        # XXX :-1 for cw compat, use asdict=True for full information
       
   779         return metas[:-1]
       
   780 
       
   781 
       
   782     def source_from_eid(self, eid):
       
   783         """return the source where the entity with id <eid> is located"""
       
   784         return self.repo.source_from_eid(eid, self)
       
   785 
       
   786     # resource accessors ######################################################
       
   787 
       
   788     def system_sql(self, sql, args=None, rollback_on_failure=True):
       
   789         """return a sql cursor on the system database"""
       
   790         if sql.split(None, 1)[0].upper() != 'SELECT':
       
   791             self.mode = 'write'
       
   792         source = self.cnxset.source('system')
       
   793         try:
       
   794             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   795         except (source.OperationalError, source.InterfaceError):
       
   796             if not rollback_on_failure:
       
   797                 raise
       
   798             source.warning("trying to reconnect")
       
   799             self.cnxset.reconnect(source)
       
   800             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   801 
       
   802     def rtype_eids_rdef(self, rtype, eidfrom, eidto):
       
   803         # use type_and_source_from_eid instead of type_from_eid for optimization
       
   804         # (avoid two extra methods call)
       
   805         subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
       
   806         objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
       
   807         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
       
   808 
       
   809 
       
   810 def cnx_attr(attr_name, writable=False):
       
   811     """return a property to forward attribute access to connection.
       
   812 
       
   813     This is to be used by session"""
       
   814     args = {}
       
   815     def attr_from_cnx(session):
       
   816         return getattr(session._cnx, attr_name)
       
   817     args['fget'] = attr_from_cnx
       
   818     if writable:
       
   819         def write_attr(session, value):
       
   820             return setattr(session._cnx, attr_name, value)
       
   821         args['fset'] = write_attr
       
   822     return property(**args)
       
   823 
       
   824 def cnx_meth(meth_name):
       
   825     """return a function forwarding calls to connection.
       
   826 
       
   827     This is to be used by session"""
       
   828     def meth_from_cnx(session, *args, **kwargs):
       
   829         return getattr(session._cnx, meth_name)(*args, **kwargs)
       
   830     return meth_from_cnx
       
   831 
       
   832 
       
   833 class Session(RequestSessionBase):
       
   834     """Repository user session
       
   835 
       
   836     This tie all together:
       
   837      * session id,
       
   838      * user,
       
   839      * connections set,
       
   840      * other session data.
       
   841 
       
   842     About session storage / transactions
       
   843     ------------------------------------
       
   844 
       
   845     Here is a description of internal session attributes. Besides :attr:`data`
       
   846     and :attr:`transaction_data`, you should not have to use attributes
       
   847     described here but higher level APIs.
       
   848 
       
   849       :attr:`data` is a dictionary containing shared data, used to communicate
       
   850       extra information between the client and the repository
       
   851 
       
   852       :attr:`_cnxs` is a dictionary of :class:`Connection` instance, one
       
   853       for each running connection. The key is the connection id. By default
       
   854       the connection id is the thread name but it can be otherwise (per dbapi
       
   855       cursor for instance, or per thread name *from another process*).
       
   856 
       
   857       :attr:`__threaddata` is a thread local storage whose `cnx` attribute
       
   858       refers to the proper instance of :class:`Connection` according to the
       
   859       connection.
       
   860 
       
   861     You should not have to use neither :attr:`_cnx` nor :attr:`__threaddata`,
       
   862     simply access connection data transparently through the :attr:`_cnx`
       
   863     property. Also, you usually don't have to access it directly since current
       
   864     connection's data may be accessed/modified through properties / methods:
       
   865 
       
   866       :attr:`connection_data`, similarly to :attr:`data`, is a dictionary
       
   867       containing some shared data that should be cleared at the end of the
       
   868       connection. Hooks and operations may put arbitrary data in there, and
       
   869       this may also be used as a communication channel between the client and
       
   870       the repository.
       
   871 
       
   872     .. automethod:: cubicweb.server.session.Session.get_shared_data
       
   873     .. automethod:: cubicweb.server.session.Session.set_shared_data
       
   874     .. automethod:: cubicweb.server.session.Session.added_in_transaction
       
   875     .. automethod:: cubicweb.server.session.Session.deleted_in_transaction
       
   876 
       
   877     Connection state information:
       
   878 
       
   879       :attr:`running_dbapi_query`, boolean flag telling if the executing query
       
   880       is coming from a dbapi connection or is a query from within the repository
       
   881 
       
   882       :attr:`cnxset`, the connections set to use to execute queries on sources.
       
   883       During a transaction, the connection set may be freed so that is may be
       
   884       used by another session as long as no writing is done. This means we can
       
   885       have multiple sessions with a reasonably low connections set pool size.
       
   886 
       
   887     .. automethod:: cubicweb.server.session.set_cnxset
       
   888     .. automethod:: cubicweb.server.session.free_cnxset
       
   889 
       
   890       :attr:`mode`, string telling the connections set handling mode, may be one
       
   891       of 'read' (connections set may be freed), 'write' (some write was done in
       
   892       the connections set, it can't be freed before end of the transaction),
       
   893       'transaction' (we want to keep the connections set during all the
       
   894       transaction, with or without writing)
       
   895 
       
   896       :attr:`pending_operations`, ordered list of operations to be processed on
       
   897       commit/rollback
       
   898 
       
   899       :attr:`commit_state`, describing the transaction commit state, may be one
       
   900       of None (not yet committing), 'precommit' (calling precommit event on
       
   901       operations), 'postcommit' (calling postcommit event on operations),
       
   902       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
       
   903       has been raised during the transaction and so it must be rollbacked).
       
   904 
       
   905     .. automethod:: cubicweb.server.session.Session.commit
       
   906     .. automethod:: cubicweb.server.session.Session.rollback
       
   907     .. automethod:: cubicweb.server.session.Session.close
       
   908     .. automethod:: cubicweb.server.session.Session.closed
       
   909 
       
   910     Security level Management:
       
   911 
       
   912       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
       
   913       read/write security is currently activated.
       
   914 
       
   915     .. automethod:: cubicweb.server.session.Session.security_enabled
       
   916 
       
   917     Hooks Management:
       
   918 
       
   919       :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
       
   920 
       
   921       :attr:`enabled_hook_categories`, when :attr:`hooks_mode` is
       
   922       `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
       
   923 
       
   924       :attr:`disabled_hook_categories`, when :attr:`hooks_mode` is
       
   925       `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
       
   926 
       
   927     .. automethod:: cubicweb.server.session.Session.deny_all_hooks_but
       
   928     .. automethod:: cubicweb.server.session.Session.allow_all_hooks_but
       
   929     .. automethod:: cubicweb.server.session.Session.is_hook_category_activated
       
   930     .. automethod:: cubicweb.server.session.Session.is_hook_activated
       
   931 
       
   932     Data manipulation:
       
   933 
       
   934     .. automethod:: cubicweb.server.session.Session.add_relation
       
   935     .. automethod:: cubicweb.server.session.Session.add_relations
       
   936     .. automethod:: cubicweb.server.session.Session.delete_relation
       
   937 
       
   938     Other:
       
   939 
       
   940     .. automethod:: cubicweb.server.session.Session.call_service
       
   941 
       
   942 
       
   943 
       
   944     """
       
   945     is_request = False
       
   946     is_internal_session = False
       
   947 
       
   948     def __init__(self, user, repo, cnxprops=None, _id=None):
       
   949         super(Session, self).__init__(repo.vreg)
       
   950         self.id = _id or make_uid(unormalize(user.login).encode('UTF8'))
       
   951         self.user = user
       
   952         self.repo = repo
       
   953         self.timestamp = time()
       
   954         self.default_mode = 'read'
       
   955         # short cut to querier .execute method
       
   956         self._execute = repo.querier.execute
       
   957         # shared data, used to communicate extra information between the client
       
   958         # and the rql server
       
   959         self.data = {}
       
   960         # i18n initialization
       
   961         self.set_language(user.prefered_language())
       
   962         ### internals
       
   963         # Connection of this section
       
   964         self._cnxs = {}
       
   965         # Data local to the thread
       
   966         self.__threaddata = threading.local()
       
   967         self._cnxset_tracker = CnxSetTracker()
       
   968         self._closed = False
       
   969         self._lock = threading.RLock()
       
   970 
       
   971     def __unicode__(self):
       
   972         return '<session %s (%s 0x%x)>' % (
       
   973             unicode(self.user.login), self.id, id(self))
       
   974 
       
   975     @property
       
   976     def sessionid(self):
       
   977         return self.id
       
   978 
       
   979     @property
       
   980     def login(self):
       
   981         # XXX backward compat with dbapi. deprecate me ASAP.
       
   982         return self.user.login
       
   983 
       
   984     def get_cnx(self, cnxid):
       
   985         """return the <cnxid> connection attached to this session
       
   986 
       
   987         Connection is created if necessary"""
       
   988         with self._lock: # no connection exist with the same id
       
   989             try:
       
   990                 if self.closed:
       
   991                     raise SessionClosedError('try to access connections set on a closed session %s' % self.id)
       
   992                 cnx = self._cnxs[cnxid]
       
   993             except KeyError:
       
   994                 rewriter = RQLRewriter(self)
       
   995                 cnx = Connection(cnxid, self, rewriter)
       
   996                 self._cnxs[cnxid] = cnx
       
   997         return cnx
       
   998 
       
   999     def set_cnx(self, cnxid=None):
       
  1000         """set the default connection of the current thread to <cnxid>
       
  1001 
       
  1002         Connection is created if necessary"""
       
  1003         if cnxid is None:
       
  1004             cnxid = threading.currentThread().getName()
       
  1005         self.__threaddata.cnx = self.get_cnx(cnxid)
       
  1006 
       
  1007     @property
       
  1008     def _cnx(self):
       
  1009         """default connection for current session in current thread"""
       
  1010         try:
       
  1011             return self.__threaddata.cnx
       
  1012         except AttributeError:
       
  1013             self.set_cnx()
       
  1014             return self.__threaddata.cnx
       
  1015 
       
  1016     @property
       
  1017     def _current_cnx_id(self):
       
  1018         """TRANSITIONAL PURPOSE"""
       
  1019         try:
       
  1020             return self.__threaddata.cnx.transactionid
       
  1021         except AttributeError:
       
  1022             return None
       
  1023 
       
  1024     def get_option_value(self, option, foreid=None):
       
  1025         return self.repo.get_option_value(option, foreid)
       
  1026 
       
  1027     def transaction(self, free_cnxset=True):
       
  1028         """return context manager to enter a transaction for the session: when
       
  1029         exiting the `with` block on exception, call `session.rollback()`, else
       
  1030         call `session.commit()` on normal exit.
       
  1031 
       
  1032         The `free_cnxset` will be given to rollback/commit methods to indicate
       
  1033         wether the connections set should be freed or not.
       
  1034         """
       
  1035         return transaction(self, free_cnxset)
       
  1036 
       
  1037     def add_relation(self, fromeid, rtype, toeid):
       
  1038         """provide direct access to the repository method to add a relation.
       
  1039 
       
  1040         This is equivalent to the following rql query:
       
  1041 
       
  1042           SET X rtype Y WHERE X eid  fromeid, T eid toeid
       
  1043 
       
  1044         without read security check but also all the burden of rql execution.
       
  1045         You may use this in hooks when you know both eids of the relation you
       
  1046         want to add.
       
  1047         """
       
  1048         self.add_relations([(rtype, [(fromeid,  toeid)])])
       
  1049 
       
  1050     def add_relations(self, relations):
       
  1051         '''set many relation using a shortcut similar to the one in add_relation
       
  1052 
       
  1053         relations is a list of 2-uples, the first element of each
       
  1054         2-uple is the rtype, and the second is a list of (fromeid,
       
  1055         toeid) tuples
       
  1056         '''
       
  1057         edited_entities = {}
       
  1058         relations_dict = {}
       
  1059         with self.security_enabled(False, False):
       
  1060             for rtype, eids in relations:
       
  1061                 if self.vreg.schema[rtype].inlined:
       
  1062                     for fromeid, toeid in eids:
       
  1063                         if fromeid not in edited_entities:
       
  1064                             entity = self.entity_from_eid(fromeid)
       
  1065                             edited = EditedEntity(entity)
       
  1066                             edited_entities[fromeid] = edited
       
  1067                         else:
       
  1068                             edited = edited_entities[fromeid]
       
  1069                         edited.edited_attribute(rtype, toeid)
       
  1070                 else:
       
  1071                     relations_dict[rtype] = eids
       
  1072             self.repo.glob_add_relations(self, relations_dict)
       
  1073             for edited in edited_entities.itervalues():
       
  1074                 self.repo.glob_update_entity(self, edited)
       
  1075 
       
  1076 
       
  1077     def delete_relation(self, fromeid, rtype, toeid):
       
  1078         """provide direct access to the repository method to delete a relation.
       
  1079 
       
  1080         This is equivalent to the following rql query:
       
  1081 
       
  1082           DELETE X rtype Y WHERE X eid  fromeid, T eid toeid
       
  1083 
       
  1084         without read security check but also all the burden of rql execution.
       
  1085         You may use this in hooks when you know both eids of the relation you
       
  1086         want to delete.
       
  1087         """
       
  1088         with self.security_enabled(False, False):
       
  1089             if self.vreg.schema[rtype].inlined:
       
  1090                 entity = self.entity_from_eid(fromeid)
       
  1091                 entity.cw_attr_cache[rtype] = None
       
  1092                 self.repo.glob_update_entity(self, entity, set((rtype,)))
       
  1093             else:
       
  1094                 self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
       
  1095 
       
  1096     # relations cache handling #################################################
       
  1097 
       
  1098     update_rel_cache_add = cnx_meth('update_rel_cache_add')
       
  1099     update_rel_cache_del = cnx_meth('update_rel_cache_del')
  1095 
  1100 
  1096     # resource accessors ######################################################
  1101     # resource accessors ######################################################
  1097 
  1102 
  1098     system_sql = cnx_meth('system_sql')
  1103     system_sql = cnx_meth('system_sql')
  1099     deleted_in_transaction = cnx_meth('deleted_in_transaction')
  1104     deleted_in_transaction = cnx_meth('deleted_in_transaction')