server/session.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Repository users' and internal' sessions."""
       
    19 from __future__ import print_function
       
    20 
       
    21 __docformat__ = "restructuredtext en"
       
    22 
       
    23 import sys
       
    24 from time import time
       
    25 from uuid import uuid4
       
    26 from warnings import warn
       
    27 import functools
       
    28 from contextlib import contextmanager
       
    29 
       
    30 from six import text_type
       
    31 
       
    32 from logilab.common.deprecation import deprecated
       
    33 from logilab.common.textutils import unormalize
       
    34 from logilab.common.registry import objectify_predicate
       
    35 
       
    36 from cubicweb import QueryError, schema, server, ProgrammingError
       
    37 from cubicweb.req import RequestSessionBase
       
    38 from cubicweb.utils import make_uid
       
    39 from cubicweb.rqlrewrite import RQLRewriter
       
    40 from cubicweb.server.edition import EditedEntity
       
    41 
       
    42 
       
    43 NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
       
    44 NO_UNDO_TYPES.add('CWCache')
       
    45 # is / is_instance_of are usually added by sql hooks except when using
       
    46 # dataimport.NoHookRQLObjectStore, and we don't want to record them
       
    47 # anyway in the later case
       
    48 NO_UNDO_TYPES.add('is')
       
    49 NO_UNDO_TYPES.add('is_instance_of')
       
    50 NO_UNDO_TYPES.add('cw_source')
       
    51 # XXX rememberme,forgotpwd,apycot,vcsfile
       
    52 
       
    53 @objectify_predicate
       
    54 def is_user_session(cls, req, **kwargs):
       
    55     """return 1 when session is not internal.
       
    56 
       
    57     This predicate can only be used repository side only. """
       
    58     return not req.is_internal_session
       
    59 
       
    60 @objectify_predicate
       
    61 def is_internal_session(cls, req, **kwargs):
       
    62     """return 1 when session is not internal.
       
    63 
       
    64     This predicate can only be used repository side only. """
       
    65     return req.is_internal_session
       
    66 
       
    67 @objectify_predicate
       
    68 def repairing(cls, req, **kwargs):
       
    69     """return 1 when repository is running in repair mode"""
       
    70     return req.vreg.config.repairing
       
    71 
       
    72 
       
    73 @deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
       
    74 def hooks_control(obj, mode, *categories):
       
    75     assert mode in  (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
       
    76     if mode == HOOKS_ALLOW_ALL:
       
    77         return obj.allow_all_hooks_but(*categories)
       
    78     elif mode == HOOKS_DENY_ALL:
       
    79         return obj.deny_all_hooks_but(*categories)
       
    80 
       
    81 
       
    82 class _hooks_control(object):
       
    83     """context manager to control activated hooks categories.
       
    84 
       
    85     If mode is `HOOKS_DENY_ALL`, given hooks categories will
       
    86     be enabled.
       
    87 
       
    88     If mode is `HOOKS_ALLOW_ALL`, given hooks categories will
       
    89     be disabled.
       
    90 
       
    91     .. sourcecode:: python
       
    92 
       
    93        with _hooks_control(cnx, HOOKS_ALLOW_ALL, 'integrity'):
       
    94            # ... do stuff with all but 'integrity' hooks activated
       
    95 
       
    96        with _hooks_control(cnx, HOOKS_DENY_ALL, 'integrity'):
       
    97            # ... do stuff with none but 'integrity' hooks activated
       
    98 
       
    99     This is an internal API, you should rather use
       
   100     :meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` or
       
   101     :meth:`~cubicweb.server.session.Connection.allow_all_hooks_but`
       
   102     Connection methods.
       
   103     """
       
   104     def __init__(self, cnx, mode, *categories):
       
   105         assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
       
   106         self.cnx = cnx
       
   107         self.mode = mode
       
   108         self.categories = categories
       
   109         self.oldmode = None
       
   110         self.changes = ()
       
   111 
       
   112     def __enter__(self):
       
   113         self.oldmode = self.cnx.hooks_mode
       
   114         self.cnx.hooks_mode = self.mode
       
   115         if self.mode is HOOKS_DENY_ALL:
       
   116             self.changes = self.cnx.enable_hook_categories(*self.categories)
       
   117         else:
       
   118             self.changes = self.cnx.disable_hook_categories(*self.categories)
       
   119         self.cnx.ctx_count += 1
       
   120 
       
   121     def __exit__(self, exctype, exc, traceback):
       
   122         self.cnx.ctx_count -= 1
       
   123         try:
       
   124             if self.categories:
       
   125                 if self.mode is HOOKS_DENY_ALL:
       
   126                     self.cnx.disable_hook_categories(*self.categories)
       
   127                 else:
       
   128                     self.cnx.enable_hook_categories(*self.categories)
       
   129         finally:
       
   130             self.cnx.hooks_mode = self.oldmode
       
   131 
       
   132 
       
   133 @deprecated('[3.17] use <object>.security_enabled instead')
       
   134 def security_enabled(obj, *args, **kwargs):
       
   135     return obj.security_enabled(*args, **kwargs)
       
   136 
       
   137 class _security_enabled(object):
       
   138     """context manager to control security w/ session.execute,
       
   139 
       
   140     By default security is disabled on queries executed on the repository
       
   141     side.
       
   142     """
       
   143     def __init__(self, cnx, read=None, write=None):
       
   144         self.cnx = cnx
       
   145         self.read = read
       
   146         self.write = write
       
   147         self.oldread = None
       
   148         self.oldwrite = None
       
   149 
       
   150     def __enter__(self):
       
   151         if self.read is None:
       
   152             self.oldread = None
       
   153         else:
       
   154             self.oldread = self.cnx.read_security
       
   155             self.cnx.read_security = self.read
       
   156         if self.write is None:
       
   157             self.oldwrite = None
       
   158         else:
       
   159             self.oldwrite = self.cnx.write_security
       
   160             self.cnx.write_security = self.write
       
   161         self.cnx.ctx_count += 1
       
   162 
       
   163     def __exit__(self, exctype, exc, traceback):
       
   164         self.cnx.ctx_count -= 1
       
   165         if self.oldread is not None:
       
   166             self.cnx.read_security = self.oldread
       
   167         if self.oldwrite is not None:
       
   168             self.cnx.write_security = self.oldwrite
       
   169 
       
   170 HOOKS_ALLOW_ALL = object()
       
   171 HOOKS_DENY_ALL = object()
       
   172 DEFAULT_SECURITY = object() # evaluated to true by design
       
   173 
       
   174 class SessionClosedError(RuntimeError):
       
   175     pass
       
   176 
       
   177 
       
   178 def _open_only(func):
       
   179     """decorator for Connection method that check it is open"""
       
   180     @functools.wraps(func)
       
   181     def check_open(cnx, *args, **kwargs):
       
   182         if not cnx._open:
       
   183             raise ProgrammingError('Closed Connection: %s'
       
   184                                     % cnx.connectionid)
       
   185         return func(cnx, *args, **kwargs)
       
   186     return check_open
       
   187 
       
   188 
       
   189 class Connection(RequestSessionBase):
       
   190     """Repository Connection
       
   191 
       
   192     Holds all connection related data
       
   193 
       
   194     Database connection resources:
       
   195 
       
   196       :attr:`hooks_in_progress`, boolean flag telling if the executing
       
   197       query is coming from a repoapi connection or is a query from
       
   198       within the repository (e.g. started by hooks)
       
   199 
       
   200       :attr:`cnxset`, the connections set to use to execute queries on sources.
       
   201       If the transaction is read only, the connection set may be freed between
       
   202       actual queries. This allows multiple connections with a reasonably low
       
   203       connection set pool size.  Control mechanism is detailed below.
       
   204 
       
   205     .. automethod:: cubicweb.server.session.Connection.set_cnxset
       
   206     .. automethod:: cubicweb.server.session.Connection.free_cnxset
       
   207 
       
   208       :attr:`mode`, string telling the connections set handling mode, may be one
       
   209       of 'read' (connections set may be freed), 'write' (some write was done in
       
   210       the connections set, it can't be freed before end of the transaction),
       
   211       'transaction' (we want to keep the connections set during all the
       
   212       transaction, with or without writing)
       
   213 
       
   214     Shared data:
       
   215 
       
   216       :attr:`data` is a dictionary bound to the underlying session,
       
   217       who will be present for the life time of the session. This may
       
   218       be useful for web clients that rely on the server for managing
       
   219       bits of session-scoped data.
       
   220 
       
   221       :attr:`transaction_data` is a dictionary cleared at the end of
       
   222       the transaction. Hooks and operations may put arbitrary data in
       
   223       there.
       
   224 
       
   225     Internal state:
       
   226 
       
   227       :attr:`pending_operations`, ordered list of operations to be processed on
       
   228       commit/rollback
       
   229 
       
   230       :attr:`commit_state`, describing the transaction commit state, may be one
       
   231       of None (not yet committing), 'precommit' (calling precommit event on
       
   232       operations), 'postcommit' (calling postcommit event on operations),
       
   233       'uncommitable' (some :exc:`ValidationError` or :exc:`Unauthorized` error
       
   234       has been raised during the transaction and so it must be rolled back).
       
   235 
       
   236     Hooks controls:
       
   237 
       
   238       :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`.
       
   239 
       
   240       :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is
       
   241       `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled.
       
   242 
       
   243       :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is
       
   244       `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled.
       
   245 
       
   246     Security level Management:
       
   247 
       
   248       :attr:`read_security` and :attr:`write_security`, boolean flags telling if
       
   249       read/write security is currently activated.
       
   250 
       
   251     """
       
   252     is_request = False
       
   253     hooks_in_progress = False
       
   254     is_repo_in_memory = True # bw compat
       
   255 
       
   256     def __init__(self, session):
       
   257         # using super(Connection, self) confuse some test hack
       
   258         RequestSessionBase.__init__(self, session.vreg)
       
   259         #: connection unique id
       
   260         self._open = None
       
   261         self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
       
   262         self.session = session
       
   263         self.sessionid = session.sessionid
       
   264         #: reentrance handling
       
   265         self.ctx_count = 0
       
   266 
       
   267         #: server.Repository object
       
   268         self.repo = session.repo
       
   269         self.vreg = self.repo.vreg
       
   270         self._execute = self.repo.querier.execute
       
   271 
       
   272         # other session utility
       
   273         self._session_timestamp = session._timestamp
       
   274 
       
   275         # internal (root) session
       
   276         self.is_internal_session = isinstance(session.user, InternalManager)
       
   277 
       
   278         #: dict containing arbitrary data cleared at the end of the transaction
       
   279         self.transaction_data = {}
       
   280         self._session_data = session.data
       
   281         #: ordered list of operations to be processed on commit/rollback
       
   282         self.pending_operations = []
       
   283         #: (None, 'precommit', 'postcommit', 'uncommitable')
       
   284         self.commit_state = None
       
   285 
       
   286         ### hook control attribute
       
   287         self.hooks_mode = HOOKS_ALLOW_ALL
       
   288         self.disabled_hook_cats = set()
       
   289         self.enabled_hook_cats = set()
       
   290         self.pruned_hooks_cache = {}
       
   291 
       
   292 
       
   293         ### security control attributes
       
   294         self._read_security = DEFAULT_SECURITY # handled by a property
       
   295         self.write_security = DEFAULT_SECURITY
       
   296 
       
   297         # undo control
       
   298         config = session.repo.config
       
   299         if config.creating or config.repairing or self.is_internal_session:
       
   300             self.undo_actions = False
       
   301         else:
       
   302             self.undo_actions = config['undo-enabled']
       
   303 
       
   304         # RQLRewriter are not thread safe
       
   305         self._rewriter = RQLRewriter(self)
       
   306 
       
   307         # other session utility
       
   308         if session.user.login == '__internal_manager__':
       
   309             self.user = session.user
       
   310             self.set_language(self.user.prefered_language())
       
   311         else:
       
   312             self._set_user(session.user)
       
   313 
       
   314     @_open_only
       
   315     def source_defs(self):
       
   316         """Return the definition of sources used by the repository."""
       
   317         return self.session.repo.source_defs()
       
   318 
       
   319     @_open_only
       
   320     def get_schema(self):
       
   321         """Return the schema currently used by the repository."""
       
   322         return self.session.repo.source_defs()
       
   323 
       
   324     @_open_only
       
   325     def get_option_value(self, option):
       
   326         """Return the value for `option` in the configuration."""
       
   327         return self.session.repo.get_option_value(option)
       
   328 
       
   329     # transaction api
       
   330 
       
   331     @_open_only
       
   332     def undoable_transactions(self, ueid=None, **actionfilters):
       
   333         """Return a list of undoable transaction objects by the connection's
       
   334         user, ordered by descendant transaction time.
       
   335 
       
   336         Managers may filter according to user (eid) who has done the transaction
       
   337         using the `ueid` argument. Others will only see their own transactions.
       
   338 
       
   339         Additional filtering capabilities is provided by using the following
       
   340         named arguments:
       
   341 
       
   342         * `etype` to get only transactions creating/updating/deleting entities
       
   343           of the given type
       
   344 
       
   345         * `eid` to get only transactions applied to entity of the given eid
       
   346 
       
   347         * `action` to get only transactions doing the given action (action in
       
   348           'C', 'U', 'D', 'A', 'R'). If `etype`, action can only be 'C', 'U' or
       
   349           'D'.
       
   350 
       
   351         * `public`: when additional filtering is provided, they are by default
       
   352           only searched in 'public' actions, unless a `public` argument is given
       
   353           and set to false.
       
   354         """
       
   355         return self.repo.system_source.undoable_transactions(self, ueid,
       
   356                                                              **actionfilters)
       
   357 
       
   358     @_open_only
       
   359     def transaction_info(self, txuuid):
       
   360         """Return transaction object for the given uid.
       
   361 
       
   362         raise `NoSuchTransaction` if not found or if session's user is
       
   363         not allowed (eg not in managers group and the transaction
       
   364         doesn't belong to him).
       
   365         """
       
   366         return self.repo.system_source.tx_info(self, txuuid)
       
   367 
       
   368     @_open_only
       
   369     def transaction_actions(self, txuuid, public=True):
       
   370         """Return an ordered list of actions effectued during that transaction.
       
   371 
       
   372         If public is true, return only 'public' actions, i.e. not ones
       
   373         triggered under the cover by hooks, else return all actions.
       
   374 
       
   375         raise `NoSuchTransaction` if the transaction is not found or
       
   376         if the user is not allowed (eg not in managers group).
       
   377         """
       
   378         return self.repo.system_source.tx_actions(self, txuuid, public)
       
   379 
       
   380     @_open_only
       
   381     def undo_transaction(self, txuuid):
       
   382         """Undo the given transaction. Return potential restoration errors.
       
   383 
       
   384         raise `NoSuchTransaction` if not found or if user is not
       
   385         allowed (eg not in managers group).
       
   386         """
       
   387         return self.repo.system_source.undo_transaction(self, txuuid)
       
   388 
       
   389     # life cycle handling ####################################################
       
   390 
       
   391     def __enter__(self):
       
   392         assert self._open is None # first opening
       
   393         self._open = True
       
   394         self.cnxset = self.repo._get_cnxset()
       
   395         return self
       
   396 
       
   397     def __exit__(self, exctype=None, excvalue=None, tb=None):
       
   398         assert self._open # actually already open
       
   399         self.rollback()
       
   400         self._open = False
       
   401         self.cnxset.cnxset_freed()
       
   402         self.repo._free_cnxset(self.cnxset)
       
   403         self.cnxset = None
       
   404 
       
   405     @contextmanager
       
   406     def running_hooks_ops(self):
       
   407         """this context manager should be called whenever hooks or operations
       
   408         are about to be run (but after hook selection)
       
   409 
       
   410         It will help the undo logic record pertinent metadata or some
       
   411         hooks to run (or not) depending on who/what issued the query.
       
   412         """
       
   413         prevmode = self.hooks_in_progress
       
   414         self.hooks_in_progress = True
       
   415         yield
       
   416         self.hooks_in_progress = prevmode
       
   417 
       
   418     # shared data handling ###################################################
       
   419 
       
   420     @property
       
   421     def data(self):
       
   422         return self._session_data
       
   423 
       
   424     @property
       
   425     def rql_rewriter(self):
       
   426         return self._rewriter
       
   427 
       
   428     @_open_only
       
   429     @deprecated('[3.19] use session or transaction data', stacklevel=3)
       
   430     def get_shared_data(self, key, default=None, pop=False, txdata=False):
       
   431         """return value associated to `key` in session data"""
       
   432         if txdata:
       
   433             data = self.transaction_data
       
   434         else:
       
   435             data = self._session_data
       
   436         if pop:
       
   437             return data.pop(key, default)
       
   438         else:
       
   439             return data.get(key, default)
       
   440 
       
   441     @_open_only
       
   442     @deprecated('[3.19] use session or transaction data', stacklevel=3)
       
   443     def set_shared_data(self, key, value, txdata=False):
       
   444         """set value associated to `key` in session data"""
       
   445         if txdata:
       
   446             self.transaction_data[key] = value
       
   447         else:
       
   448             self._session_data[key] = value
       
   449 
       
   450     def clear(self):
       
   451         """reset internal data"""
       
   452         self.transaction_data = {}
       
   453         #: ordered list of operations to be processed on commit/rollback
       
   454         self.pending_operations = []
       
   455         #: (None, 'precommit', 'postcommit', 'uncommitable')
       
   456         self.commit_state = None
       
   457         self.pruned_hooks_cache = {}
       
   458         self.local_perm_cache.clear()
       
   459         self.rewriter = RQLRewriter(self)
       
   460 
       
   461     @deprecated('[3.19] cnxset are automatically managed now.'
       
   462                 ' stop using explicit set and free.')
       
   463     def set_cnxset(self):
       
   464         pass
       
   465 
       
   466     @deprecated('[3.19] cnxset are automatically managed now.'
       
   467                 ' stop using explicit set and free.')
       
   468     def free_cnxset(self, ignoremode=False):
       
   469         pass
       
   470 
       
   471     @property
       
   472     @contextmanager
       
   473     @_open_only
       
   474     @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
       
   475                 ' stop using .ensure_cnx_set')
       
   476     def ensure_cnx_set(self):
       
   477         yield
       
   478 
       
   479     @property
       
   480     def anonymous_connection(self):
       
   481         return self.session.anonymous_session
       
   482 
       
   483     # Entity cache management #################################################
       
   484     #
       
   485     # The connection entity cache as held in cnx.transaction_data is removed at the
       
   486     # end of the connection (commit and rollback)
       
   487     #
       
   488     # XXX connection level caching may be a pb with multiple repository
       
   489     # instances, but 1. this is probably not the only one :$ and 2. it may be
       
   490     # an acceptable risk. Anyway we could activate it or not according to a
       
   491     # configuration option
       
   492 
       
   493     def set_entity_cache(self, entity):
       
   494         """Add `entity` to the connection entity cache"""
       
   495         # XXX not using _open_only because before at creation time. _set_user
       
   496         # call this function to cache the Connection user.
       
   497         if entity.cw_etype != 'CWUser' and not self._open:
       
   498             raise ProgrammingError('Closed Connection: %s'
       
   499                                     % self.connectionid)
       
   500         ecache = self.transaction_data.setdefault('ecache', {})
       
   501         ecache.setdefault(entity.eid, entity)
       
   502 
       
   503     @_open_only
       
   504     def entity_cache(self, eid):
       
   505         """get cache entity for `eid`"""
       
   506         return self.transaction_data['ecache'][eid]
       
   507 
       
   508     @_open_only
       
   509     def cached_entities(self):
       
   510         """return the whole entity cache"""
       
   511         return self.transaction_data.get('ecache', {}).values()
       
   512 
       
   513     @_open_only
       
   514     def drop_entity_cache(self, eid=None):
       
   515         """drop entity from the cache
       
   516 
       
   517         If eid is None, the whole cache is dropped"""
       
   518         if eid is None:
       
   519             self.transaction_data.pop('ecache', None)
       
   520         else:
       
   521             del self.transaction_data['ecache'][eid]
       
   522 
       
   523     # relations handling #######################################################
       
   524 
       
   525     @_open_only
       
   526     def add_relation(self, fromeid, rtype, toeid):
       
   527         """provide direct access to the repository method to add a relation.
       
   528 
       
   529         This is equivalent to the following rql query:
       
   530 
       
   531           SET X rtype Y WHERE X eid  fromeid, T eid toeid
       
   532 
       
   533         without read security check but also all the burden of rql execution.
       
   534         You may use this in hooks when you know both eids of the relation you
       
   535         want to add.
       
   536         """
       
   537         self.add_relations([(rtype, [(fromeid,  toeid)])])
       
   538 
       
   539     @_open_only
       
   540     def add_relations(self, relations):
       
   541         '''set many relation using a shortcut similar to the one in add_relation
       
   542 
       
   543         relations is a list of 2-uples, the first element of each
       
   544         2-uple is the rtype, and the second is a list of (fromeid,
       
   545         toeid) tuples
       
   546         '''
       
   547         edited_entities = {}
       
   548         relations_dict = {}
       
   549         with self.security_enabled(False, False):
       
   550             for rtype, eids in relations:
       
   551                 if self.vreg.schema[rtype].inlined:
       
   552                     for fromeid, toeid in eids:
       
   553                         if fromeid not in edited_entities:
       
   554                             entity = self.entity_from_eid(fromeid)
       
   555                             edited = EditedEntity(entity)
       
   556                             edited_entities[fromeid] = edited
       
   557                         else:
       
   558                             edited = edited_entities[fromeid]
       
   559                         edited.edited_attribute(rtype, toeid)
       
   560                 else:
       
   561                     relations_dict[rtype] = eids
       
   562             self.repo.glob_add_relations(self, relations_dict)
       
   563             for edited in edited_entities.values():
       
   564                 self.repo.glob_update_entity(self, edited)
       
   565 
       
   566 
       
   567     @_open_only
       
   568     def delete_relation(self, fromeid, rtype, toeid):
       
   569         """provide direct access to the repository method to delete a relation.
       
   570 
       
   571         This is equivalent to the following rql query:
       
   572 
       
   573           DELETE X rtype Y WHERE X eid  fromeid, T eid toeid
       
   574 
       
   575         without read security check but also all the burden of rql execution.
       
   576         You may use this in hooks when you know both eids of the relation you
       
   577         want to delete.
       
   578         """
       
   579         with self.security_enabled(False, False):
       
   580             if self.vreg.schema[rtype].inlined:
       
   581                 entity = self.entity_from_eid(fromeid)
       
   582                 entity.cw_attr_cache[rtype] = None
       
   583                 self.repo.glob_update_entity(self, entity, set((rtype,)))
       
   584             else:
       
   585                 self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
       
   586 
       
   587     # relations cache handling #################################################
       
   588 
       
   589     @_open_only
       
   590     def update_rel_cache_add(self, subject, rtype, object, symmetric=False):
       
   591         self._update_entity_rel_cache_add(subject, rtype, 'subject', object)
       
   592         if symmetric:
       
   593             self._update_entity_rel_cache_add(object, rtype, 'subject', subject)
       
   594         else:
       
   595             self._update_entity_rel_cache_add(object, rtype, 'object', subject)
       
   596 
       
   597     @_open_only
       
   598     def update_rel_cache_del(self, subject, rtype, object, symmetric=False):
       
   599         self._update_entity_rel_cache_del(subject, rtype, 'subject', object)
       
   600         if symmetric:
       
   601             self._update_entity_rel_cache_del(object, rtype, 'object', object)
       
   602         else:
       
   603             self._update_entity_rel_cache_del(object, rtype, 'object', subject)
       
   604 
       
   605     @_open_only
       
   606     def _update_entity_rel_cache_add(self, eid, rtype, role, targeteid):
       
   607         try:
       
   608             entity = self.entity_cache(eid)
       
   609         except KeyError:
       
   610             return
       
   611         rcache = entity.cw_relation_cached(rtype, role)
       
   612         if rcache is not None:
       
   613             rset, entities = rcache
       
   614             rset = rset.copy()
       
   615             entities = list(entities)
       
   616             rset.rows.append([targeteid])
       
   617             if not isinstance(rset.description, list): # else description not set
       
   618                 rset.description = list(rset.description)
       
   619             rset.description.append([self.entity_metas(targeteid)['type']])
       
   620             targetentity = self.entity_from_eid(targeteid)
       
   621             if targetentity.cw_rset is None:
       
   622                 targetentity.cw_rset = rset
       
   623                 targetentity.cw_row = rset.rowcount
       
   624                 targetentity.cw_col = 0
       
   625             rset.rowcount += 1
       
   626             entities.append(targetentity)
       
   627             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
       
   628                 rset, tuple(entities))
       
   629 
       
   630     @_open_only
       
   631     def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
       
   632         try:
       
   633             entity = self.entity_cache(eid)
       
   634         except KeyError:
       
   635             return
       
   636         rcache = entity.cw_relation_cached(rtype, role)
       
   637         if rcache is not None:
       
   638             rset, entities = rcache
       
   639             for idx, row in enumerate(rset.rows):
       
   640                 if row[0] == targeteid:
       
   641                     break
       
   642             else:
       
   643                 # this may occurs if the cache has been filed by a hook
       
   644                 # after the database update
       
   645                 self.debug('cache inconsistency for %s %s %s %s', eid, rtype,
       
   646                            role, targeteid)
       
   647                 return
       
   648             rset = rset.copy()
       
   649             entities = list(entities)
       
   650             del rset.rows[idx]
       
   651             if isinstance(rset.description, list): # else description not set
       
   652                 del rset.description[idx]
       
   653             del entities[idx]
       
   654             rset.rowcount -= 1
       
   655             entity._cw_related_cache['%s_%s' % (rtype, role)] = (
       
   656                 rset, tuple(entities))
       
   657 
       
   658     # Tracking of entities added of removed in the transaction ##################
       
   659 
       
   660     @_open_only
       
   661     def deleted_in_transaction(self, eid):
       
   662         """return True if the entity of the given eid is being deleted in the
       
   663         current transaction
       
   664         """
       
   665         return eid in self.transaction_data.get('pendingeids', ())
       
   666 
       
   667     @_open_only
       
   668     def added_in_transaction(self, eid):
       
   669         """return True if the entity of the given eid is being created in the
       
   670         current transaction
       
   671         """
       
   672         return eid in self.transaction_data.get('neweids', ())
       
   673 
       
   674     # Operation management ####################################################
       
   675 
       
   676     @_open_only
       
   677     def add_operation(self, operation, index=None):
       
   678         """add an operation to be executed at the end of the transaction"""
       
   679         if index is None:
       
   680             self.pending_operations.append(operation)
       
   681         else:
       
   682             self.pending_operations.insert(index, operation)
       
   683 
       
   684     # Hooks control ###########################################################
       
   685 
       
   686     @_open_only
       
   687     def allow_all_hooks_but(self, *categories):
       
   688         return _hooks_control(self, HOOKS_ALLOW_ALL, *categories)
       
   689 
       
   690     @_open_only
       
   691     def deny_all_hooks_but(self, *categories):
       
   692         return _hooks_control(self, HOOKS_DENY_ALL, *categories)
       
   693 
       
   694     @_open_only
       
   695     def disable_hook_categories(self, *categories):
       
   696         """disable the given hook categories:
       
   697 
       
   698         - on HOOKS_DENY_ALL mode, ensure those categories are not enabled
       
   699         - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
       
   700         """
       
   701         changes = set()
       
   702         self.pruned_hooks_cache.clear()
       
   703         categories = set(categories)
       
   704         if self.hooks_mode is HOOKS_DENY_ALL:
       
   705             enabledcats = self.enabled_hook_cats
       
   706             changes = enabledcats & categories
       
   707             enabledcats -= changes # changes is small hence faster
       
   708         else:
       
   709             disabledcats = self.disabled_hook_cats
       
   710             changes = categories - disabledcats
       
   711             disabledcats |= changes # changes is small hence faster
       
   712         return tuple(changes)
       
   713 
       
   714     @_open_only
       
   715     def enable_hook_categories(self, *categories):
       
   716         """enable the given hook categories:
       
   717 
       
   718         - on HOOKS_DENY_ALL mode, ensure those categories are enabled
       
   719         - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
       
   720         """
       
   721         changes = set()
       
   722         self.pruned_hooks_cache.clear()
       
   723         categories = set(categories)
       
   724         if self.hooks_mode is HOOKS_DENY_ALL:
       
   725             enabledcats = self.enabled_hook_cats
       
   726             changes = categories - enabledcats
       
   727             enabledcats |= changes # changes is small hence faster
       
   728         else:
       
   729             disabledcats = self.disabled_hook_cats
       
   730             changes = disabledcats & categories
       
   731             disabledcats -= changes # changes is small hence faster
       
   732         return tuple(changes)
       
   733 
       
   734     @_open_only
       
   735     def is_hook_category_activated(self, category):
       
   736         """return a boolean telling if the given category is currently activated
       
   737         or not
       
   738         """
       
   739         if self.hooks_mode is HOOKS_DENY_ALL:
       
   740             return category in self.enabled_hook_cats
       
   741         return category not in self.disabled_hook_cats
       
   742 
       
   743     @_open_only
       
   744     def is_hook_activated(self, hook):
       
   745         """return a boolean telling if the given hook class is currently
       
   746         activated or not
       
   747         """
       
   748         return self.is_hook_category_activated(hook.category)
       
   749 
       
   750     # Security management #####################################################
       
   751 
       
   752     @_open_only
       
   753     def security_enabled(self, read=None, write=None):
       
   754         return _security_enabled(self, read=read, write=write)
       
   755 
       
   756     @property
       
   757     @_open_only
       
   758     def read_security(self):
       
   759         return self._read_security
       
   760 
       
   761     @read_security.setter
       
   762     @_open_only
       
   763     def read_security(self, activated):
       
   764         self._read_security = activated
       
   765 
       
   766     # undo support ############################################################
       
   767 
       
   768     @_open_only
       
   769     def ertype_supports_undo(self, ertype):
       
   770         return self.undo_actions and ertype not in NO_UNDO_TYPES
       
   771 
       
   772     @_open_only
       
   773     def transaction_uuid(self, set=True):
       
   774         uuid = self.transaction_data.get('tx_uuid')
       
   775         if set and uuid is None:
       
   776             self.transaction_data['tx_uuid'] = uuid = text_type(uuid4().hex)
       
   777             self.repo.system_source.start_undoable_transaction(self, uuid)
       
   778         return uuid
       
   779 
       
   780     @_open_only
       
   781     def transaction_inc_action_counter(self):
       
   782         num = self.transaction_data.setdefault('tx_action_count', 0) + 1
       
   783         self.transaction_data['tx_action_count'] = num
       
   784         return num
       
   785 
       
   786     # db-api like interface ###################################################
       
   787 
       
   788     @_open_only
       
   789     def source_defs(self):
       
   790         return self.repo.source_defs()
       
   791 
       
   792     @deprecated('[3.19] use .entity_metas(eid) instead')
       
   793     @_open_only
       
   794     def describe(self, eid, asdict=False):
       
   795         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   796         etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
       
   797         metas = {'type': etype, 'source': source, 'extid': extid}
       
   798         if asdict:
       
   799             metas['asource'] = metas['source'] # XXX pre 3.19 client compat
       
   800             return metas
       
   801         return etype, source, extid
       
   802 
       
   803     @_open_only
       
   804     def entity_metas(self, eid):
       
   805         """return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
       
   806         etype, extid, source = self.repo.type_and_source_from_eid(eid, self)
       
   807         return {'type': etype, 'source': source, 'extid': extid}
       
   808 
       
   809     # core method #############################################################
       
   810 
       
   811     @_open_only
       
   812     def execute(self, rql, kwargs=None, build_descr=True):
       
   813         """db-api like method directly linked to the querier execute method.
       
   814 
       
   815         See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
       
   816         """
       
   817         self._session_timestamp.touch()
       
   818         rset = self._execute(self, rql, kwargs, build_descr)
       
   819         rset.req = self
       
   820         self._session_timestamp.touch()
       
   821         return rset
       
   822 
       
   823     @_open_only
       
   824     def rollback(self, free_cnxset=None, reset_pool=None):
       
   825         """rollback the current transaction"""
       
   826         if free_cnxset is not None:
       
   827             warn('[3.21] free_cnxset is now unneeded',
       
   828                  DeprecationWarning, stacklevel=2)
       
   829         if reset_pool is not None:
       
   830             warn('[3.13] reset_pool is now unneeded',
       
   831                  DeprecationWarning, stacklevel=2)
       
   832         cnxset = self.cnxset
       
   833         assert cnxset is not None
       
   834         try:
       
   835             # by default, operations are executed with security turned off
       
   836             with self.security_enabled(False, False):
       
   837                 while self.pending_operations:
       
   838                     try:
       
   839                         operation = self.pending_operations.pop(0)
       
   840                         operation.handle_event('rollback_event')
       
   841                     except BaseException:
       
   842                         self.critical('rollback error', exc_info=sys.exc_info())
       
   843                         continue
       
   844                 cnxset.rollback()
       
   845                 self.debug('rollback for transaction %s done', self.connectionid)
       
   846         finally:
       
   847             self._session_timestamp.touch()
       
   848             self.clear()
       
   849 
       
   850     @_open_only
       
   851     def commit(self, free_cnxset=None, reset_pool=None):
       
   852         """commit the current session's transaction"""
       
   853         if free_cnxset is not None:
       
   854             warn('[3.21] free_cnxset is now unneeded',
       
   855                  DeprecationWarning, stacklevel=2)
       
   856         if reset_pool is not None:
       
   857             warn('[3.13] reset_pool is now unneeded',
       
   858                  DeprecationWarning, stacklevel=2)
       
   859         assert self.cnxset is not None
       
   860         cstate = self.commit_state
       
   861         if cstate == 'uncommitable':
       
   862             raise QueryError('transaction must be rolled back')
       
   863         if cstate == 'precommit':
       
   864             self.warn('calling commit in precommit makes no sense; ignoring commit')
       
   865             return
       
   866         if cstate == 'postcommit':
       
   867             self.critical('postcommit phase is not allowed to write to the db; ignoring commit')
       
   868             return
       
   869         assert cstate is None
       
   870         # on rollback, an operation should have the following state
       
   871         # information:
       
   872         # - processed by the precommit/commit event or not
       
   873         # - if processed, is it the failed operation
       
   874         debug = server.DEBUG & server.DBG_OPS
       
   875         try:
       
   876             # by default, operations are executed with security turned off
       
   877             with self.security_enabled(False, False):
       
   878                 processed = []
       
   879                 self.commit_state = 'precommit'
       
   880                 if debug:
       
   881                     print(self.commit_state, '*' * 20)
       
   882                 try:
       
   883                     with self.running_hooks_ops():
       
   884                         while self.pending_operations:
       
   885                             operation = self.pending_operations.pop(0)
       
   886                             operation.processed = 'precommit'
       
   887                             processed.append(operation)
       
   888                             if debug:
       
   889                                 print(operation)
       
   890                             operation.handle_event('precommit_event')
       
   891                     self.pending_operations[:] = processed
       
   892                     self.debug('precommit transaction %s done', self.connectionid)
       
   893                 except BaseException:
       
   894                     # if error on [pre]commit:
       
   895                     #
       
   896                     # * set .failed = True on the operation causing the failure
       
   897                     # * call revert<event>_event on processed operations
       
   898                     # * call rollback_event on *all* operations
       
   899                     #
       
   900                     # that seems more natural than not calling rollback_event
       
   901                     # for processed operations, and allow generic rollback
       
   902                     # instead of having to implements rollback, revertprecommit
       
   903                     # and revertcommit, that will be enough in mont case.
       
   904                     operation.failed = True
       
   905                     if debug:
       
   906                         print(self.commit_state, '*' * 20)
       
   907                     with self.running_hooks_ops():
       
   908                         for operation in reversed(processed):
       
   909                             if debug:
       
   910                                 print(operation)
       
   911                             try:
       
   912                                 operation.handle_event('revertprecommit_event')
       
   913                             except BaseException:
       
   914                                 self.critical('error while reverting precommit',
       
   915                                               exc_info=True)
       
   916                     # XXX use slice notation since self.pending_operations is a
       
   917                     # read-only property.
       
   918                     self.pending_operations[:] = processed + self.pending_operations
       
   919                     self.rollback()
       
   920                     raise
       
   921                 self.cnxset.commit()
       
   922                 self.commit_state = 'postcommit'
       
   923                 if debug:
       
   924                     print(self.commit_state, '*' * 20)
       
   925                 with self.running_hooks_ops():
       
   926                     while self.pending_operations:
       
   927                         operation = self.pending_operations.pop(0)
       
   928                         if debug:
       
   929                             print(operation)
       
   930                         operation.processed = 'postcommit'
       
   931                         try:
       
   932                             operation.handle_event('postcommit_event')
       
   933                         except BaseException:
       
   934                             self.critical('error while postcommit',
       
   935                                           exc_info=sys.exc_info())
       
   936                 self.debug('postcommit transaction %s done', self.connectionid)
       
   937                 return self.transaction_uuid(set=False)
       
   938         finally:
       
   939             self._session_timestamp.touch()
       
   940             self.clear()
       
   941 
       
   942     # resource accessors ######################################################
       
   943 
       
   944     @_open_only
       
   945     def call_service(self, regid, **kwargs):
       
   946         self.debug('calling service %s', regid)
       
   947         service = self.vreg['services'].select(regid, self, **kwargs)
       
   948         return service.call(**kwargs)
       
   949 
       
   950     @_open_only
       
   951     def system_sql(self, sql, args=None, rollback_on_failure=True):
       
   952         """return a sql cursor on the system database"""
       
   953         source = self.repo.system_source
       
   954         try:
       
   955             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   956         except (source.OperationalError, source.InterfaceError):
       
   957             if not rollback_on_failure:
       
   958                 raise
       
   959             source.warning("trying to reconnect")
       
   960             self.cnxset.reconnect()
       
   961             return source.doexec(self, sql, args, rollback=rollback_on_failure)
       
   962 
       
   963     @_open_only
       
   964     def rtype_eids_rdef(self, rtype, eidfrom, eidto):
       
   965         # use type_and_source_from_eid instead of type_from_eid for optimization
       
   966         # (avoid two extra methods call)
       
   967         subjtype = self.repo.type_and_source_from_eid(eidfrom, self)[0]
       
   968         objtype = self.repo.type_and_source_from_eid(eidto, self)[0]
       
   969         return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
       
   970 
       
   971 
       
   972 def cnx_attr(attr_name, writable=False):
       
   973     """return a property to forward attribute access to connection.
       
   974 
       
   975     This is to be used by session"""
       
   976     args = {}
       
   977     @deprecated('[3.19] use a Connection object instead')
       
   978     def attr_from_cnx(session):
       
   979         return getattr(session._cnx, attr_name)
       
   980     args['fget'] = attr_from_cnx
       
   981     if writable:
       
   982         @deprecated('[3.19] use a Connection object instead')
       
   983         def write_attr(session, value):
       
   984             return setattr(session._cnx, attr_name, value)
       
   985         args['fset'] = write_attr
       
   986     return property(**args)
       
   987 
       
   988 
       
   989 class Timestamp(object):
       
   990 
       
   991     def __init__(self):
       
   992         self.value = time()
       
   993 
       
   994     def touch(self):
       
   995         self.value = time()
       
   996 
       
   997     def __float__(self):
       
   998         return float(self.value)
       
   999 
       
  1000 
       
  1001 class Session(object):
       
  1002     """Repository user session
       
  1003 
       
  1004     This ties all together:
       
  1005      * session id,
       
  1006      * user,
       
  1007      * other session data.
       
  1008     """
       
  1009 
       
  1010     def __init__(self, user, repo, cnxprops=None, _id=None):
       
  1011         self.sessionid = _id or make_uid(unormalize(user.login))
       
  1012         self.user = user # XXX repoapi: deprecated and store only a login.
       
  1013         self.repo = repo
       
  1014         self.vreg = repo.vreg
       
  1015         self._timestamp = Timestamp()
       
  1016         self.data = {}
       
  1017         self.closed = False
       
  1018 
       
  1019     def close(self):
       
  1020         self.closed = True
       
  1021 
       
  1022     def __enter__(self):
       
  1023         return self
       
  1024 
       
  1025     def __exit__(self, *args):
       
  1026         pass
       
  1027 
       
  1028     def __unicode__(self):
       
  1029         return '<session %s (%s 0x%x)>' % (
       
  1030             unicode(self.user.login), self.sessionid, id(self))
       
  1031 
       
  1032     @property
       
  1033     def timestamp(self):
       
  1034         return float(self._timestamp)
       
  1035 
       
  1036     @property
       
  1037     @deprecated('[3.19] session.id is deprecated, use session.sessionid')
       
  1038     def id(self):
       
  1039         return self.sessionid
       
  1040 
       
  1041     @property
       
  1042     def login(self):
       
  1043         return self.user.login
       
  1044 
       
  1045     def new_cnx(self):
       
  1046         """Return a new Connection object linked to the session
       
  1047 
       
  1048         The returned Connection will *not* be managed by the Session.
       
  1049         """
       
  1050         return Connection(self)
       
  1051 
       
  1052     @deprecated('[3.19] use a Connection object instead')
       
  1053     def get_option_value(self, option, foreid=None):
       
  1054         if foreid is not None:
       
  1055             warn('[3.19] foreid argument is deprecated', DeprecationWarning,
       
  1056                  stacklevel=2)
       
  1057         return self.repo.get_option_value(option)
       
  1058 
       
  1059     def _touch(self):
       
  1060         """update latest session usage timestamp and reset mode to read"""
       
  1061         self._timestamp.touch()
       
  1062 
       
  1063     local_perm_cache = cnx_attr('local_perm_cache')
       
  1064     @local_perm_cache.setter
       
  1065     def local_perm_cache(self, value):
       
  1066         #base class assign an empty dict:-(
       
  1067         assert value == {}
       
  1068         pass
       
  1069 
       
  1070     # deprecated ###############################################################
       
  1071 
       
  1072     @property
       
  1073     def anonymous_session(self):
       
  1074         # XXX for now, anonymous_user only exists in webconfig (and testconfig).
       
  1075         # It will only be present inside all-in-one instance.
       
  1076         # there is plan to move it down to global config.
       
  1077         if not hasattr(self.repo.config, 'anonymous_user'):
       
  1078             # not a web or test config, no anonymous user
       
  1079             return False
       
  1080         return self.user.login == self.repo.config.anonymous_user()[0]
       
  1081 
       
  1082     @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
       
  1083     def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
       
  1084         return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
       
  1085 
       
  1086     # these are overridden by set_log_methods below
       
  1087     # only defining here to prevent pylint from complaining
       
  1088     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
  1089 
       
  1090 
       
  1091 
       
  1092 class InternalManager(object):
       
  1093     """a manager user with all access rights used internally for task such as
       
  1094     bootstrapping the repository or creating regular users according to
       
  1095     repository content
       
  1096     """
       
  1097 
       
  1098     def __init__(self, lang='en'):
       
  1099         self.eid = -1
       
  1100         self.login = u'__internal_manager__'
       
  1101         self.properties = {}
       
  1102         self.groups = set(['managers'])
       
  1103         self.lang = lang
       
  1104 
       
  1105     def matching_groups(self, groups):
       
  1106         return 1
       
  1107 
       
  1108     def is_in_group(self, group):
       
  1109         return True
       
  1110 
       
  1111     def owns(self, eid):
       
  1112         return True
       
  1113 
       
  1114     def property_value(self, key):
       
  1115         if key == 'ui.language':
       
  1116             return self.lang
       
  1117         return None
       
  1118 
       
  1119     def prefered_language(self, language=None):
       
  1120         # mock CWUser.prefered_language, mainly for testing purpose
       
  1121         return self.property_value('ui.language')
       
  1122 
       
  1123     # CWUser compat for notification ###########################################
       
  1124 
       
  1125     def name(self):
       
  1126         return 'cubicweb'
       
  1127 
       
  1128     class _IEmailable:
       
  1129         @staticmethod
       
  1130         def get_email():
       
  1131             return ''
       
  1132 
       
  1133     def cw_adapt_to(self, iface):
       
  1134         if iface == 'IEmailable':
       
  1135             return self._IEmailable
       
  1136         return None
       
  1137 
       
  1138 from logging import getLogger
       
  1139 from cubicweb import set_log_methods
       
  1140 set_log_methods(Session, getLogger('cubicweb.session'))
       
  1141 set_log_methods(Connection, getLogger('cubicweb.session'))