diff -r 058bb3dc685f -r 0b59724cb3f2 server/session.py --- a/server/session.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1141 +0,0 @@ -# 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 . -"""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 .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 .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 """ - 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 """ - 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 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 '' % ( - 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'))