--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/session.py Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1141 @@
+# copyright 2003-2015 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__ import print_function
+
+__docformat__ = "restructuredtext en"
+
+import sys
+from time import time
+from uuid import uuid4
+from warnings import warn
+import functools
+from contextlib import contextmanager
+
+from six import text_type
+
+from logilab.common.deprecation import deprecated
+from logilab.common.textutils import unormalize
+from logilab.common.registry import objectify_predicate
+
+from cubicweb import QueryError, schema, server, ProgrammingError
+from cubicweb.req import RequestSessionBase
+from cubicweb.utils import make_uid
+from cubicweb.rqlrewrite import RQLRewriter
+from cubicweb.server.edition import EditedEntity
+
+
+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 case
+NO_UNDO_TYPES.add('is')
+NO_UNDO_TYPES.add('is_instance_of')
+NO_UNDO_TYPES.add('cw_source')
+# XXX rememberme,forgotpwd,apycot,vcsfile
+
+@objectify_predicate
+def is_user_session(cls, req, **kwargs):
+ """return 1 when session is not internal.
+
+ This predicate can only be used repository side only. """
+ return not req.is_internal_session
+
+@objectify_predicate
+def is_internal_session(cls, req, **kwargs):
+ """return 1 when session is not internal.
+
+ This predicate can only be used repository side only. """
+ return req.is_internal_session
+
+@objectify_predicate
+def repairing(cls, req, **kwargs):
+ """return 1 when repository is running in repair mode"""
+ return req.vreg.config.repairing
+
+
+@deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
+def hooks_control(obj, mode, *categories):
+ assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
+ if mode == HOOKS_ALLOW_ALL:
+ return obj.allow_all_hooks_but(*categories)
+ elif mode == HOOKS_DENY_ALL:
+ return obj.deny_all_hooks_but(*categories)
+
+
+class _hooks_control(object):
+ """context manager to control activated hooks categories.
+
+ If mode is `HOOKS_DENY_ALL`, given hooks categories will
+ be enabled.
+
+ If mode is `HOOKS_ALLOW_ALL`, given hooks categories will
+ be disabled.
+
+ .. sourcecode:: python
+
+ with _hooks_control(cnx, HOOKS_ALLOW_ALL, 'integrity'):
+ # ... do stuff with all but 'integrity' hooks activated
+
+ with _hooks_control(cnx, HOOKS_DENY_ALL, 'integrity'):
+ # ... do stuff with none but 'integrity' hooks activated
+
+ This is an internal API, you should rather use
+ :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` or
+ :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but`
+ Connection methods.
+ """
+ def __init__(self, cnx, mode, *categories):
+ assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
+ self.cnx = cnx
+ self.mode = mode
+ self.categories = categories
+ self.oldmode = None
+ self.changes = ()
+
+ def __enter__(self):
+ self.oldmode = self.cnx.hooks_mode
+ self.cnx.hooks_mode = self.mode
+ if self.mode is HOOKS_DENY_ALL:
+ self.changes = self.cnx.enable_hook_categories(*self.categories)
+ else:
+ self.changes = self.cnx.disable_hook_categories(*self.categories)
+ self.cnx.ctx_count += 1
+
+ def __exit__(self, exctype, exc, traceback):
+ self.cnx.ctx_count -= 1
+ try:
+ if self.categories:
+ if self.mode is HOOKS_DENY_ALL:
+ self.cnx.disable_hook_categories(*self.categories)
+ else:
+ self.cnx.enable_hook_categories(*self.categories)
+ finally:
+ self.cnx.hooks_mode = self.oldmode
+
+
+@deprecated('[3.17] use <object>.security_enabled instead')
+def security_enabled(obj, *args, **kwargs):
+ return obj.security_enabled(*args, **kwargs)
+
+class _security_enabled(object):
+ """context manager to control security w/ session.execute,
+
+ By default security is disabled on queries executed on the repository
+ side.
+ """
+ def __init__(self, cnx, read=None, write=None):
+ self.cnx = cnx
+ self.read = read
+ self.write = write
+ self.oldread = None
+ self.oldwrite = None
+
+ def __enter__(self):
+ if self.read is None:
+ self.oldread = None
+ else:
+ self.oldread = self.cnx.read_security
+ self.cnx.read_security = self.read
+ if self.write is None:
+ self.oldwrite = None
+ else:
+ self.oldwrite = self.cnx.write_security
+ self.cnx.write_security = self.write
+ self.cnx.ctx_count += 1
+
+ def __exit__(self, exctype, exc, traceback):
+ self.cnx.ctx_count -= 1
+ if self.oldread is not None:
+ self.cnx.read_security = self.oldread
+ if self.oldwrite is not None:
+ self.cnx.write_security = self.oldwrite
+
+HOOKS_ALLOW_ALL = object()
+HOOKS_DENY_ALL = object()
+DEFAULT_SECURITY = object() # evaluated to true by design
+
+class SessionClosedError(RuntimeError):
+ pass
+
+
+def _open_only(func):
+ """decorator for Connection method that check it is open"""
+ @functools.wraps(func)
+ def check_open(cnx, *args, **kwargs):
+ if not cnx._open:
+ raise ProgrammingError('Closed Connection: %s'
+ % cnx.connectionid)
+ return func(cnx, *args, **kwargs)
+ return check_open
+
+
+class Connection(RequestSessionBase):
+ """Repository Connection
+
+ Holds all connection related data
+
+ Database connection resources:
+
+ :attr:`hooks_in_progress`, boolean flag telling if the executing
+ query is coming from a repoapi connection or is a query from
+ within the repository (e.g. started by hooks)
+
+ :attr:`cnxset`, the connections set to use to execute queries on sources.
+ If the transaction is read only, the connection set may be freed between
+ actual queries. This allows multiple connections with a reasonably low
+ connection set pool size. Control mechanism is detailed below.
+
+ .. automethod:: cubicweb.server.session.Connection.set_cnxset
+ .. automethod:: cubicweb.server.session.Connection.free_cnxset
+
+ :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)
+
+ Shared data:
+
+ :attr:`data` is a dictionary bound to the underlying session,
+ who will be present for the life time of the session. This may
+ be useful for web clients that rely on the server for managing
+ bits of session-scoped data.
+
+ :attr:`transaction_data` is a dictionary cleared at the end of
+ the transaction. Hooks and operations may put arbitrary data in
+ there.
+
+ Internal state:
+
+ :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 rolled back).
+
+ Hooks controls:
+
+ :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
+
+ :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is
+ `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
+
+ :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is
+ `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
+
+ Security level Management:
+
+ :attr:`read_security` and :attr:`write_security`, boolean flags telling if
+ read/write security is currently activated.
+
+ """
+ is_request = False
+ hooks_in_progress = False
+ is_repo_in_memory = True # bw compat
+
+ def __init__(self, session):
+ # using super(Connection, self) confuse some test hack
+ RequestSessionBase.__init__(self, session.vreg)
+ #: connection unique id
+ self._open = None
+ self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
+ self.session = session
+ self.sessionid = session.sessionid
+ #: reentrance handling
+ self.ctx_count = 0
+
+ #: server.Repository object
+ self.repo = session.repo
+ self.vreg = self.repo.vreg
+ self._execute = self.repo.querier.execute
+
+ # other session utility
+ self._session_timestamp = session._timestamp
+
+ # internal (root) session
+ self.is_internal_session = isinstance(session.user, InternalManager)
+
+ #: dict containing arbitrary data cleared at the end of the transaction
+ self.transaction_data = {}
+ self._session_data = session.data
+ #: ordered list of operations to be processed on commit/rollback
+ self.pending_operations = []
+ #: (None, 'precommit', 'postcommit', 'uncommitable')
+ self.commit_state = None
+
+ ### hook control attribute
+ self.hooks_mode = HOOKS_ALLOW_ALL
+ self.disabled_hook_cats = set()
+ self.enabled_hook_cats = set()
+ self.pruned_hooks_cache = {}
+
+
+ ### security control attributes
+ self._read_security = DEFAULT_SECURITY # handled by a property
+ self.write_security = DEFAULT_SECURITY
+
+ # undo control
+ config = session.repo.config
+ if config.creating or config.repairing or self.is_internal_session:
+ self.undo_actions = False
+ else:
+ self.undo_actions = config['undo-enabled']
+
+ # RQLRewriter are not thread safe
+ self._rewriter = RQLRewriter(self)
+
+ # other session utility
+ if session.user.login == '__internal_manager__':
+ self.user = session.user
+ self.set_language(self.user.prefered_language())
+ else:
+ self._set_user(session.user)
+
+ @_open_only
+ def source_defs(self):
+ """Return the definition of sources used by the repository."""
+ return self.session.repo.source_defs()
+
+ @_open_only
+ def get_schema(self):
+ """Return the schema currently used by the repository."""
+ return self.session.repo.source_defs()
+
+ @_open_only
+ def get_option_value(self, option):
+ """Return the value for `option` in the configuration."""
+ return self.session.repo.get_option_value(option)
+
+ # transaction api
+
+ @_open_only
+ def undoable_transactions(self, ueid=None, **actionfilters):
+ """Return a list of undoable transaction objects by the connection's
+ user, ordered by descendant transaction time.
+
+ Managers may filter according to user (eid) who has done the transaction
+ using the `ueid` argument. Others will only see their own transactions.
+
+ Additional filtering capabilities is provided by using the following
+ named arguments:
+
+ * `etype` to get only transactions creating/updating/deleting entities
+ of the given type
+
+ * `eid` to get only transactions applied to entity of the given eid
+
+ * `action` to get only transactions doing the given action (action in
+ 'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
+ 'D'.
+
+ * `public`: when additional filtering is provided, they are by default
+ only searched in 'public' actions, unless a `public` argument is given
+ and set to false.
+ """
+ return self.repo.system_source.undoable_transactions(self, ueid,
+ **actionfilters)
+
+ @_open_only
+ def transaction_info(self, txuuid):
+ """Return transaction object for the given uid.
+
+ raise `NoSuchTransaction` if not found or if session's user is
+ not allowed (eg not in managers group and the transaction
+ doesn't belong to him).
+ """
+ return self.repo.system_source.tx_info(self, txuuid)
+
+ @_open_only
+ def transaction_actions(self, txuuid, public=True):
+ """Return an ordered list of actions effectued during that transaction.
+
+ If public is true, return only 'public' actions, i.e. not ones
+ triggered under the cover by hooks, else return all actions.
+
+ raise `NoSuchTransaction` if the transaction is not found or
+ if the user is not allowed (eg not in managers group).
+ """
+ return self.repo.system_source.tx_actions(self, txuuid, public)
+
+ @_open_only
+ def undo_transaction(self, txuuid):
+ """Undo the given transaction. Return potential restoration errors.
+
+ raise `NoSuchTransaction` if not found or if user is not
+ allowed (eg not in managers group).
+ """
+ return self.repo.system_source.undo_transaction(self, txuuid)
+
+ # life cycle handling ####################################################
+
+ def __enter__(self):
+ assert self._open is None # first opening
+ self._open = True
+ self.cnxset = self.repo._get_cnxset()
+ return self
+
+ def __exit__(self, exctype=None, excvalue=None, tb=None):
+ assert self._open # actually already open
+ self.rollback()
+ self._open = False
+ self.cnxset.cnxset_freed()
+ self.repo._free_cnxset(self.cnxset)
+ self.cnxset = None
+
+ @contextmanager
+ def running_hooks_ops(self):
+ """this context manager should be called whenever hooks or operations
+ are about to be run (but after hook selection)
+
+ It will help the undo logic record pertinent metadata or some
+ hooks to run (or not) depending on who/what issued the query.
+ """
+ prevmode = self.hooks_in_progress
+ self.hooks_in_progress = True
+ yield
+ self.hooks_in_progress = prevmode
+
+ # shared data handling ###################################################
+
+ @property
+ def data(self):
+ return self._session_data
+
+ @property
+ def rql_rewriter(self):
+ return self._rewriter
+
+ @_open_only
+ @deprecated('[3.19] use session or transaction data', stacklevel=3)
+ def get_shared_data(self, key, default=None, pop=False, txdata=False):
+ """return value associated to `key` in session data"""
+ if txdata:
+ data = self.transaction_data
+ else:
+ data = self._session_data
+ if pop:
+ return data.pop(key, default)
+ else:
+ return data.get(key, default)
+
+ @_open_only
+ @deprecated('[3.19] use session or transaction data', stacklevel=3)
+ def set_shared_data(self, key, value, txdata=False):
+ """set value associated to `key` in session data"""
+ if txdata:
+ self.transaction_data[key] = value
+ else:
+ self._session_data[key] = value
+
+ def clear(self):
+ """reset internal data"""
+ self.transaction_data = {}
+ #: ordered list of operations to be processed on commit/rollback
+ self.pending_operations = []
+ #: (None, 'precommit', 'postcommit', 'uncommitable')
+ self.commit_state = None
+ self.pruned_hooks_cache = {}
+ self.local_perm_cache.clear()
+ self.rewriter = RQLRewriter(self)
+
+ @deprecated('[3.19] cnxset are automatically managed now.'
+ ' stop using explicit set and free.')
+ def set_cnxset(self):
+ pass
+
+ @deprecated('[3.19] cnxset are automatically managed now.'
+ ' stop using explicit set and free.')
+ def free_cnxset(self, ignoremode=False):
+ pass
+
+ @property
+ @contextmanager
+ @_open_only
+ @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
+ ' stop using .ensure_cnx_set')
+ def ensure_cnx_set(self):
+ yield
+
+ @property
+ def anonymous_connection(self):
+ return self.session.anonymous_session
+
+ # Entity cache management #################################################
+ #
+ # The connection entity cache as held in cnx.transaction_data is removed at the
+ # end of the connection (commit and rollback)
+ #
+ # XXX connection 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 option
+
+ def set_entity_cache(self, entity):
+ """Add `entity` to the connection entity cache"""
+ # XXX not using _open_only because before at creation time. _set_user
+ # call this function to cache the Connection user.
+ if entity.cw_etype != 'CWUser' and not self._open:
+ raise ProgrammingError('Closed Connection: %s'
+ % self.connectionid)
+ ecache = self.transaction_data.setdefault('ecache', {})
+ ecache.setdefault(entity.eid, entity)
+
+ @_open_only
+ def entity_cache(self, eid):
+ """get cache entity for `eid`"""
+ return self.transaction_data['ecache'][eid]
+
+ @_open_only
+ def cached_entities(self):
+ """return the whole entity cache"""
+ return self.transaction_data.get('ecache', {}).values()
+
+ @_open_only
+ def drop_entity_cache(self, eid=None):
+ """drop entity from the cache
+
+ If eid is None, the whole cache is dropped"""
+ if eid is None:
+ self.transaction_data.pop('ecache', None)
+ else:
+ del self.transaction_data['ecache'][eid]
+
+ # relations handling #######################################################
+
+ @_open_only
+ def add_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)])])
+
+ @_open_only
+ def add_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 = {}
+ with self.security_enabled(False, False):
+ for rtype, eids in relations:
+ if self.vreg.schema[rtype].inlined:
+ for fromeid, toeid in eids:
+ if fromeid not in edited_entities:
+ entity = self.entity_from_eid(fromeid)
+ edited = EditedEntity(entity)
+ edited_entities[fromeid] = edited
+ else:
+ edited = edited_entities[fromeid]
+ edited.edited_attribute(rtype, toeid)
+ else:
+ relations_dict[rtype] = eids
+ self.repo.glob_add_relations(self, relations_dict)
+ for edited in edited_entities.values():
+ self.repo.glob_update_entity(self, edited)
+
+
+ @_open_only
+ def delete_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.
+ """
+ with self.security_enabled(False, False):
+ if self.vreg.schema[rtype].inlined:
+ entity = self.entity_from_eid(fromeid)
+ entity.cw_attr_cache[rtype] = None
+ self.repo.glob_update_entity(self, entity, set((rtype,)))
+ else:
+ self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
+
+ # relations cache handling #################################################
+
+ @_open_only
+ def update_rel_cache_add(self, subject, rtype, object, symmetric=False):
+ self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
+ if symmetric:
+ self._update_entity_rel_cache_add(object, rtype, 'subject', subject)
+ else:
+ self._update_entity_rel_cache_add(object, rtype, 'object', subject)
+
+ @_open_only
+ def update_rel_cache_del(self, subject, rtype, object, symmetric=False):
+ self._update_entity_rel_cache_del(subject, rtype, 'subject', object)
+ if symmetric:
+ self._update_entity_rel_cache_del(object, rtype, 'object', object)
+ else:
+ self._update_entity_rel_cache_del(object, rtype, 'object', subject)
+
+ @_open_only
+ def _update_entity_rel_cache_add(self, eid, rtype, role, targeteid):
+ try:
+ entity = self.entity_cache(eid)
+ except KeyError:
+ return
+ rcache = entity.cw_relation_cached(rtype, role)
+ if rcache is not None:
+ rset, entities = rcache
+ rset = rset.copy()
+ entities = list(entities)
+ rset.rows.append([targeteid])
+ if not isinstance(rset.description, list): # else description not set
+ rset.description = list(rset.description)
+ rset.description.append([self.entity_metas(targeteid)['type']])
+ targetentity = self.entity_from_eid(targeteid)
+ if targetentity.cw_rset is None:
+ targetentity.cw_rset = rset
+ targetentity.cw_row = rset.rowcount
+ targetentity.cw_col = 0
+ rset.rowcount += 1
+ entities.append(targetentity)
+ entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+ rset, tuple(entities))
+
+ @_open_only
+ def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
+ try:
+ entity = self.entity_cache(eid)
+ except KeyError:
+ return
+ rcache = entity.cw_relation_cached(rtype, role)
+ if rcache is not None:
+ rset, entities = rcache
+ for idx, row in enumerate(rset.rows):
+ if row[0] == targeteid:
+ break
+ else:
+ # this may occurs if the cache has been filed by a hook
+ # after the database update
+ self.debug('cache inconsistency for %s %s %s %s', eid, rtype,
+ role, targeteid)
+ return
+ rset = rset.copy()
+ entities = list(entities)
+ del rset.rows[idx]
+ if isinstance(rset.description, list): # else description not set
+ del rset.description[idx]
+ del entities[idx]
+ rset.rowcount -= 1
+ entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+ rset, tuple(entities))
+
+ # Tracking of entities added of removed in the transaction ##################
+
+ @_open_only
+ def deleted_in_transaction(self, eid):
+ """return True if the entity of the given eid is being deleted in the
+ current transaction
+ """
+ return eid in self.transaction_data.get('pendingeids', ())
+
+ @_open_only
+ def added_in_transaction(self, eid):
+ """return True if the entity of the given eid is being created in the
+ current transaction
+ """
+ return eid in self.transaction_data.get('neweids', ())
+
+ # Operation management ####################################################
+
+ @_open_only
+ def add_operation(self, operation, index=None):
+ """add an operation to be executed at the end of the transaction"""
+ if index is None:
+ self.pending_operations.append(operation)
+ else:
+ self.pending_operations.insert(index, operation)
+
+ # Hooks control ###########################################################
+
+ @_open_only
+ def allow_all_hooks_but(self, *categories):
+ return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
+
+ @_open_only
+ def deny_all_hooks_but(self, *categories):
+ return _hooks_control(self, HOOKS_DENY_ALL, *categories)
+
+ @_open_only
+ def disable_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()
+ categories = set(categories)
+ if self.hooks_mode is HOOKS_DENY_ALL:
+ enabledcats = self.enabled_hook_cats
+ changes = enabledcats & categories
+ enabledcats -= changes # changes is small hence faster
+ else:
+ disabledcats = self.disabled_hook_cats
+ changes = categories - disabledcats
+ disabledcats |= changes # changes is small hence faster
+ return tuple(changes)
+
+ @_open_only
+ def enable_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()
+ categories = set(categories)
+ if self.hooks_mode is HOOKS_DENY_ALL:
+ enabledcats = self.enabled_hook_cats
+ changes = categories - enabledcats
+ enabledcats |= changes # changes is small hence faster
+ else:
+ disabledcats = self.disabled_hook_cats
+ changes = disabledcats & categories
+ disabledcats -= changes # changes is small hence faster
+ return tuple(changes)
+
+ @_open_only
+ def is_hook_category_activated(self, category):
+ """return a boolean telling if the given category is currently activated
+ or not
+ """
+ if self.hooks_mode is HOOKS_DENY_ALL:
+ return category in self.enabled_hook_cats
+ return category not in self.disabled_hook_cats
+
+ @_open_only
+ def is_hook_activated(self, hook):
+ """return a boolean telling if the given hook class is currently
+ activated or not
+ """
+ return self.is_hook_category_activated(hook.category)
+
+ # Security management #####################################################
+
+ @_open_only
+ def security_enabled(self, read=None, write=None):
+ return _security_enabled(self, read=read, write=write)
+
+ @property
+ @_open_only
+ def read_security(self):
+ return self._read_security
+
+ @read_security.setter
+ @_open_only
+ def read_security(self, activated):
+ self._read_security = activated
+
+ # undo support ############################################################
+
+ @_open_only
+ def ertype_supports_undo(self, ertype):
+ return self.undo_actions and ertype not in NO_UNDO_TYPES
+
+ @_open_only
+ def transaction_uuid(self, set=True):
+ uuid = self.transaction_data.get('tx_uuid')
+ if set and uuid is None:
+ self.transaction_data['tx_uuid'] = uuid = text_type(uuid4().hex)
+ self.repo.system_source.start_undoable_transaction(self, uuid)
+ return uuid
+
+ @_open_only
+ def transaction_inc_action_counter(self):
+ num = self.transaction_data.setdefault('tx_action_count', 0) + 1
+ self.transaction_data['tx_action_count'] = num
+ return num
+
+ # db-api like interface ###################################################
+
+ @_open_only
+ def source_defs(self):
+ return self.repo.source_defs()
+
+ @deprecated('[3.19] use .entity_metas(eid) instead')
+ @_open_only
+ def describe(self, eid, asdict=False):
+ """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+ etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
+ metas = {'type': etype, 'source': source, 'extid': extid}
+ if asdict:
+ metas['asource'] = metas['source'] # XXX pre 3.19 client compat
+ return metas
+ return etype, source, extid
+
+ @_open_only
+ def entity_metas(self, eid):
+ """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
+ etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
+ return {'type': etype, 'source': source, 'extid': extid}
+
+ # core method #############################################################
+
+ @_open_only
+ def execute(self, rql, kwargs=None, build_descr=True):
+ """db-api like method directly linked to the querier execute method.
+
+ See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
+ """
+ self._session_timestamp.touch()
+ rset = self._execute(self, rql, kwargs, build_descr)
+ rset.req = self
+ self._session_timestamp.touch()
+ return rset
+
+ @_open_only
+ def rollback(self, free_cnxset=None, reset_pool=None):
+ """rollback the current transaction"""
+ if free_cnxset is not None:
+ warn('[3.21] free_cnxset is now unneeded',
+ DeprecationWarning, stacklevel=2)
+ if reset_pool is not None:
+ warn('[3.13] reset_pool is now unneeded',
+ DeprecationWarning, stacklevel=2)
+ cnxset = self.cnxset
+ assert cnxset is not None
+ try:
+ # by default, operations are executed with security turned off
+ with self.security_enabled(False, False):
+ while self.pending_operations:
+ try:
+ operation = self.pending_operations.pop(0)
+ operation.handle_event('rollback_event')
+ except BaseException:
+ self.critical('rollback error', exc_info=sys.exc_info())
+ continue
+ cnxset.rollback()
+ self.debug('rollback for transaction %s done', self.connectionid)
+ finally:
+ self._session_timestamp.touch()
+ self.clear()
+
+ @_open_only
+ def commit(self, free_cnxset=None, reset_pool=None):
+ """commit the current session's transaction"""
+ if free_cnxset is not None:
+ warn('[3.21] free_cnxset is now unneeded',
+ DeprecationWarning, stacklevel=2)
+ if reset_pool is not None:
+ warn('[3.13] reset_pool is now unneeded',
+ DeprecationWarning, stacklevel=2)
+ assert self.cnxset is not None
+ cstate = self.commit_state
+ if cstate == 'uncommitable':
+ raise QueryError('transaction must be rolled back')
+ if cstate == 'precommit':
+ self.warn('calling commit in precommit makes no sense; ignoring commit')
+ return
+ if cstate == 'postcommit':
+ self.critical('postcommit phase is not allowed to write to the db; ignoring commit')
+ return
+ assert cstate is None
+ # on rollback, an operation should have the following state
+ # information:
+ # - processed by the precommit/commit event or not
+ # - if processed, is it the failed operation
+ debug = server.DEBUG & server.DBG_OPS
+ try:
+ # by default, operations are executed with security turned off
+ with self.security_enabled(False, False):
+ processed = []
+ self.commit_state = 'precommit'
+ if debug:
+ print(self.commit_state, '*' * 20)
+ try:
+ with self.running_hooks_ops():
+ while self.pending_operations:
+ operation = self.pending_operations.pop(0)
+ operation.processed = 'precommit'
+ processed.append(operation)
+ if debug:
+ print(operation)
+ operation.handle_event('precommit_event')
+ self.pending_operations[:] = processed
+ self.debug('precommit transaction %s done', self.connectionid)
+ except BaseException:
+ # 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 = True
+ if debug:
+ print(self.commit_state, '*' * 20)
+ with self.running_hooks_ops():
+ for operation in reversed(processed):
+ if debug:
+ print(operation)
+ try:
+ operation.handle_event('revertprecommit_event')
+ except BaseException:
+ 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_operations
+ self.rollback()
+ raise
+ self.cnxset.commit()
+ self.commit_state = 'postcommit'
+ if debug:
+ print(self.commit_state, '*' * 20)
+ with self.running_hooks_ops():
+ while self.pending_operations:
+ operation = self.pending_operations.pop(0)
+ if debug:
+ print(operation)
+ operation.processed = 'postcommit'
+ try:
+ operation.handle_event('postcommit_event')
+ except BaseException:
+ self.critical('error while postcommit',
+ exc_info=sys.exc_info())
+ self.debug('postcommit transaction %s done', self.connectionid)
+ return self.transaction_uuid(set=False)
+ finally:
+ self._session_timestamp.touch()
+ self.clear()
+
+ # resource accessors ######################################################
+
+ @_open_only
+ def call_service(self, regid, **kwargs):
+ self.debug('calling service %s', regid)
+ service = self.vreg['services'].select(regid, self, **kwargs)
+ return service.call(**kwargs)
+
+ @_open_only
+ def system_sql(self, sql, args=None, rollback_on_failure=True):
+ """return a sql cursor on the system database"""
+ source = self.repo.system_source
+ try:
+ return source.doexec(self, sql, args, rollback=rollback_on_failure)
+ except (source.OperationalError, source.InterfaceError):
+ if not rollback_on_failure:
+ raise
+ source.warning("trying to reconnect")
+ self.cnxset.reconnect()
+ return source.doexec(self, sql, args, rollback=rollback_on_failure)
+
+ @_open_only
+ def rtype_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]
+ return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
+
+
+def cnx_attr(attr_name, writable=False):
+ """return a property to forward attribute access to connection.
+
+ This is to be used by session"""
+ args = {}
+ @deprecated('[3.19] use a Connection object instead')
+ def attr_from_cnx(session):
+ return getattr(session._cnx, attr_name)
+ args['fget'] = attr_from_cnx
+ if writable:
+ @deprecated('[3.19] use a Connection object instead')
+ def write_attr(session, value):
+ return setattr(session._cnx, attr_name, value)
+ args['fset'] = write_attr
+ return property(**args)
+
+
+class Timestamp(object):
+
+ def __init__(self):
+ self.value = time()
+
+ def touch(self):
+ self.value = time()
+
+ def __float__(self):
+ return float(self.value)
+
+
+class Session(object):
+ """Repository user session
+
+ This ties all together:
+ * session id,
+ * user,
+ * other session data.
+ """
+
+ def __init__(self, user, repo, cnxprops=None, _id=None):
+ self.sessionid = _id or make_uid(unormalize(user.login))
+ self.user = user # XXX repoapi: deprecated and store only a login.
+ self.repo = repo
+ self.vreg = repo.vreg
+ self._timestamp = Timestamp()
+ self.data = {}
+ self.closed = False
+
+ def close(self):
+ self.closed = True
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ pass
+
+ def __unicode__(self):
+ return '<session %s (%s 0x%x)>' % (
+ unicode(self.user.login), self.sessionid, id(self))
+
+ @property
+ def timestamp(self):
+ return float(self._timestamp)
+
+ @property
+ @deprecated('[3.19] session.id is deprecated, use session.sessionid')
+ def id(self):
+ return self.sessionid
+
+ @property
+ def login(self):
+ return self.user.login
+
+ def new_cnx(self):
+ """Return a new Connection object linked to the session
+
+ The returned Connection will *not* be managed by the Session.
+ """
+ return Connection(self)
+
+ @deprecated('[3.19] use a Connection object instead')
+ def get_option_value(self, option, foreid=None):
+ if foreid is not None:
+ warn('[3.19] foreid argument is deprecated', DeprecationWarning,
+ stacklevel=2)
+ return self.repo.get_option_value(option)
+
+ def _touch(self):
+ """update latest session usage timestamp and reset mode to read"""
+ self._timestamp.touch()
+
+ local_perm_cache = cnx_attr('local_perm_cache')
+ @local_perm_cache.setter
+ def local_perm_cache(self, value):
+ #base class assign an empty dict:-(
+ assert value == {}
+ pass
+
+ # deprecated ###############################################################
+
+ @property
+ def anonymous_session(self):
+ # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+ # It will only be present inside all-in-one instance.
+ # there is plan to move it down to global config.
+ if not hasattr(self.repo.config, 'anonymous_user'):
+ # not a web or test config, no anonymous user
+ return False
+ return self.user.login == self.repo.config.anonymous_user()[0]
+
+ @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
+ def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
+ return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
+
+ # these are overridden by set_log_methods below
+ # only defining here to prevent pylint from complaining
+ info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+
+
+
+class InternalManager(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, lang='en'):
+ self.eid = -1
+ self.login = u'__internal_manager__'
+ self.properties = {}
+ self.groups = set(['managers'])
+ self.lang = lang
+
+ def matching_groups(self, groups):
+ return 1
+
+ def is_in_group(self, group):
+ return True
+
+ def owns(self, eid):
+ return True
+
+ def property_value(self, key):
+ if key == 'ui.language':
+ return self.lang
+ return None
+
+ def prefered_language(self, language=None):
+ # mock CWUser.prefered_language, mainly for testing purpose
+ return self.property_value('ui.language')
+
+ # CWUser compat for notification ###########################################
+
+ def name(self):
+ return 'cubicweb'
+
+ class _IEmailable:
+ @staticmethod
+ def get_email():
+ return ''
+
+ def cw_adapt_to(self, iface):
+ if iface == 'IEmailable':
+ return self._IEmailable
+ return None
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(Session, getLogger('cubicweb.session'))
+set_log_methods(Connection, getLogger('cubicweb.session'))