server/hooksmanager.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 02 Sep 2009 10:23:38 +0200
branch3.5
changeset 3084 096d680c9da2
parent 2918 452b4c9ee61d
child 2968 0e3460341023
child 3088 854a30d8c092
permissions -rw-r--r--
backport stable

"""Hooks management

Hooks are called before / after any individual update of entities / relations
in the repository.

Here is the prototype of the different hooks:

* filtered on the entity's type:

  before_add_entity    (session, entity)
  after_add_entity     (session, entity)
  before_update_entity (session, entity)
  after_update_entity  (session, entity)
  before_delete_entity (session, eid)
  after_delete_entity  (session, eid)

* filtered on the relation's type:

  before_add_relation    (session, fromeid, rtype, toeid)
  after_add_relation     (session, fromeid, rtype, toeid)
  before_delete_relation (session, fromeid, rtype, toeid)
  after_delete_relation  (session, fromeid, rtype, toeid)


:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

ENTITIES_HOOKS = ('before_add_entity',    'after_add_entity',
                  'before_update_entity', 'after_update_entity',
                  'before_delete_entity', 'after_delete_entity')
RELATIONS_HOOKS = ('before_add_relation',   'after_add_relation' ,
                   'before_delete_relation','after_delete_relation')
SYSTEM_HOOKS = ('server_backup', 'server_restore',
                'server_startup', 'server_shutdown',
                'session_open', 'session_close')

ALL_HOOKS = frozenset(ENTITIES_HOOKS + RELATIONS_HOOKS + SYSTEM_HOOKS)

class HooksManager(object):
    """handle hooks registration and calls
    """
    verification_hooks_activated = True

    def __init__(self, schema):
        self.set_schema(schema)

    def set_schema(self, schema):
        self._hooks = {}
        self.schema = schema
        self._init_hooks(schema)

    def register_hooks(self, hooks):
        """register a dictionary of hooks :

             {'event': {'entity or relation type': [callbacks list]}}
        """
        for event, subevents in hooks.items():
            for subevent, callbacks in subevents.items():
                for callback in callbacks:
                    self.register_hook(callback, event, subevent)

    def register_hook(self, function, event, etype=''):
        """register a function to call when <event> occurs

        <etype> is an entity/relation type or an empty string.

        If etype is the empty string, the function will be called at each event,
        else the function will be called only when event occurs on an entity or
        relation of the given type.
        """
        assert event in ALL_HOOKS, '%r NOT IN %r' % (event, ALL_HOOKS)
        assert (not event in SYSTEM_HOOKS or not etype), (event, etype)
        etype = etype or ''
        try:
            self._hooks[event][etype].append(function)
            self.debug('registered hook %s on %s (%s)', event, etype or 'any',
                       function.func_name)

        except KeyError:
            self.error('can\'t register hook %s on %s (%s)',
                       event, etype or 'any', function.func_name)

    def unregister_hook(self, function_or_cls, event=None, etype=''):
        """unregister a function to call when <event> occurs, or a Hook subclass.
        In the later case, event/type information are extracted from the given
        class.
        """
        if isinstance(function_or_cls, type) and issubclass(function_or_cls, Hook):
            for event, ertype in function_or_cls.register_to():
                for hook in self._hooks[event][ertype]:
                    if getattr(hook, 'im_self', None).__class__ is function_or_cls:
                        self._hooks[event][ertype].remove(hook)
                        self.info('unregister hook %s on %s (%s)', event, etype,
                                  function_or_cls.__name__)
                        break
                else:
                    self.warning("can't unregister hook %s on %s (%s), not found",
                                 event, etype, function_or_cls.__name__)
        else:
            assert event in ALL_HOOKS, event
            etype = etype or ''
            self.info('unregister hook %s on %s (%s)', event, etype,
                      function_or_cls.func_name)
            self._hooks[event][etype].remove(function_or_cls)

    def call_hooks(self, __event, __type='', *args, **kwargs):
        """call hook matching event and optional type"""
        if __type:
            self.info('calling hooks for event %s (%s)', __event, __type)
        else:
            self.info('calling hooks for event %s', __event)
        # call generic hooks first
        for hook in self._hooks[__event]['']:
            #print '[generic]', hook.__name__
            hook(*args, **kwargs)
        if __type:
            for hook in self._hooks[__event][__type]:
                #print '[%s]'%__type, hook.__name__
                hook(*args, **kwargs)

    def _init_hooks(self, schema):
        """initialize the hooks map"""
        for hook_event in ENTITIES_HOOKS:
            self._hooks[hook_event] = {'': []}
            for etype in schema.entities():
                self._hooks[hook_event][etype] = []
        for hook_event in RELATIONS_HOOKS:
            self._hooks[hook_event] = {'': []}
            for r_type in schema.relations():
                self._hooks[hook_event][r_type] = []
        for hook_event in SYSTEM_HOOKS:
            self._hooks[hook_event] = {'': []}

    def register_system_hooks(self, config):
        """register system hooks according to the configuration"""
        self.info('register core hooks')
        from cubicweb.server.hooks import _register_metadata_hooks, _register_wf_hooks
        _register_metadata_hooks(self)
        self.info('register workflow hooks')
        _register_wf_hooks(self)
        if config.core_hooks:
            from cubicweb.server.hooks import _register_core_hooks
            _register_core_hooks(self)
        if config.schema_hooks:
            from cubicweb.server.schemahooks import _register_schema_hooks
            self.info('register schema hooks')
            _register_schema_hooks(self)
        if config.usergroup_hooks:
            from cubicweb.server.hooks import _register_usergroup_hooks
            from cubicweb.server.hooks import _register_eproperty_hooks
            self.info('register user/group hooks')
            _register_usergroup_hooks(self)
            _register_eproperty_hooks(self)
        if config.security_hooks:
            from cubicweb.server.securityhooks import register_security_hooks
            self.info('register security hooks')
            register_security_hooks(self)
        if not self.verification_hooks_activated:
            self.deactivate_verification_hooks()

    def deactivate_verification_hooks(self):
        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
                                        cardinalitycheck_before_del_relation,
                                        cstrcheck_after_add_relation,
                                        uniquecstrcheck_before_modification)
        self.warning('deactivating verification hooks')
        self.verification_hooks_activated = False
        self.unregister_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
        self.unregister_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
        self.unregister_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
        self.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
        self.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
#         self.unregister_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
#         self.unregister_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')

    def reactivate_verification_hooks(self):
        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
                                        cardinalitycheck_before_del_relation,
                                        cstrcheck_after_add_relation,
                                        uniquecstrcheck_before_modification)
        self.warning('reactivating verification hooks')
        self.verification_hooks_activated = True
        self.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
        self.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
        self.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
        self.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
        self.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
#         self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
#         self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')

from cubicweb.selectors import yes
from cubicweb.appobject import AppObject

class autoid(type):
    """metaclass to create an unique 'id' attribute on the class using it"""
    # XXX is this metaclass really necessary ?
    def __new__(mcs, name, bases, classdict):
        cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
        cls.id = str(id(cls))
        return cls

class Hook(AppObject):
    __metaclass__ = autoid
    __registry__ = 'hooks'
    __select__ = yes()
    # set this in derivated classes
    events = None
    accepts = None
    enabled = True

    def __init__(self, event=None):
        super(Hook, self).__init__()
        self.event = event

    @classmethod
    def registered(cls, vreg):
        super(Hook, cls).registered(vreg)
        return cls()

    @classmethod
    def register_to(cls):
        if not cls.enabled:
            cls.warning('%s hook has been disabled', cls)
            return
        done = set()
        assert isinstance(cls.events, (tuple, list)), \
               '%s: events is expected to be a tuple, not %s' % (
            cls, type(cls.events))
        for event in cls.events:
            if event in SYSTEM_HOOKS:
                assert not cls.accepts or cls.accepts == ('Any',), \
                       '%s doesnt make sense on %s' % (cls.accepts, event)
                cls.accepts = ('Any',)
            for ertype in cls.accepts:
                if (event, ertype) in done:
                    continue
                yield event, ertype
                done.add((event, ertype))
                try:
                    eschema = cls.schema.eschema(ertype)
                except KeyError:
                    # relation schema
                    pass
                else:
                    for eetype in eschema.specialized_by():
                        if (event, eetype) in done:
                            continue
                        yield event, str(eetype)
                        done.add((event, eetype))


    def make_callback(self, event):
        if len(self.events) == 1:
            return self.call
        return self.__class__(event=event).call

    def call(self):
        raise NotImplementedError

class SystemHook(Hook):
    accepts = ()

from logging import getLogger
from cubicweb import set_log_methods
set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
set_log_methods(Hook, getLogger('cubicweb.hooks'))

# base classes for relation propagation ########################################

from cubicweb.server.pool import PreCommitOperation

class RQLPrecommitOperation(PreCommitOperation):
    def precommit_event(self):
        execute = self.session.unsafe_execute
        for rql in self.rqls:
            execute(*rql)


class PropagateSubjectRelationHook(Hook):
    """propagate permissions and nosy list when new entity are added"""
    events = ('after_add_relation',)
    # to set in concrete class
    rtype = None
    subject_relations = None
    object_relations = None
    accepts = None # subject_relations + object_relations

    def call(self, session, fromeid, rtype, toeid):
        for eid in (fromeid, toeid):
            etype = session.describe(eid)[0]
            if not self.schema.eschema(etype).has_subject_relation(self.rtype):
                return
        if rtype in self.subject_relations:
            meid, seid = fromeid, toeid
        else:
            assert rtype in self.object_relations
            meid, seid = toeid, fromeid
        rql = 'SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'\
              % (self.rtype, self.rtype, self.rtype)
        rqls = [(rql, {'x': meid, 'e': seid}, ('x', 'e'))]
        RQLPrecommitOperation(session, rqls=rqls)


class PropagateSubjectRelationAddHook(Hook):
    """propagate on existing entities when a permission or nosy list is added"""
    events = ('after_add_relation',)
    # to set in concrete class
    rtype = None
    subject_relations = None
    object_relations = None
    accepts = None # (self.rtype,)

    def call(self, session, fromeid, rtype, toeid):
        eschema = self.schema.eschema(session.describe(fromeid)[0])
        rqls = []
        for rel in self.subject_relations:
            if eschema.has_subject_relation(rel):
                rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                             'X %s R, NOT R %s P' % (rtype, rel, rtype),
                             {'x': fromeid, 'p': toeid}, 'x'))
        for rel in self.object_relations:
            if eschema.has_object_relation(rel):
                rqls.append(('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                             'R %s X, NOT R %s P' % (rtype, rel, rtype),
                             {'x': fromeid, 'p': toeid}, 'x'))
        if rqls:
            RQLPrecommitOperation(session, rqls=rqls)


class PropagateSubjectRelationDelHook(Hook):
    """propagate on existing entities when a permission is deleted"""
    events = ('after_delete_relation',)
    # to set in concrete class
    rtype = None
    subject_relations = None
    object_relations = None
    accepts = None # (self.rtype,)

    def call(self, session, fromeid, rtype, toeid):
        eschema = self.schema.eschema(session.describe(fromeid)[0])
        rqls = []
        for rel in self.subject_relations:
            if eschema.has_subject_relation(rel):
                rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                             'X %s R' % (rtype, rel),
                             {'x': fromeid, 'p': toeid}, 'x'))
        for rel in self.object_relations:
            if eschema.has_object_relation(rel):
                rqls.append(('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, '
                             'R %s X' % (rtype, rel),
                             {'x': fromeid, 'p': toeid}, 'x'))
        if rqls:
            RQLPrecommitOperation(session, rqls=rqls)