schema.py
branchstable
changeset 9542 79b9bf88be28
parent 9395 96dba2efd16d
child 9469 032825bbacab
child 9600 bde625698f44
equal deleted inserted replaced
9540:43b4895a150f 9542:79b9bf88be28
    19 
    19 
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    21 _ = unicode
    21 _ = unicode
    22 
    22 
    23 import re
    23 import re
    24 from os.path import join
    24 from os.path import join, basename
    25 from logging import getLogger
    25 from logging import getLogger
    26 from warnings import warn
    26 from warnings import warn
    27 
    27 
       
    28 from logilab.common import tempattr
    28 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
    29 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
    29 from logilab.common.logging_ext import set_log_methods
    30 from logilab.common.logging_ext import set_log_methods
    30 from logilab.common.deprecation import deprecated, class_moved, moved
    31 from logilab.common.deprecation import deprecated, class_moved, moved
    31 from logilab.common.textutils import splitstrip
    32 from logilab.common.textutils import splitstrip
    32 from logilab.common.graph import get_cycles
    33 from logilab.common.graph import get_cycles
    41 
    42 
    42 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    43 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    43 
    44 
    44 import cubicweb
    45 import cubicweb
    45 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
    46 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
       
    47 
       
    48 try:
       
    49     from cubicweb import server
       
    50 except ImportError:
       
    51     # We need to lookup DEBUG from there,
       
    52     # however a pure dbapi client may not have it.
       
    53     class server(object): pass
       
    54     server.DEBUG = False
       
    55 
    46 
    56 
    47 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
    57 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
    48 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
    58 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
    49 
    59 
    50 # set of meta-relations available for every entity types
    60 # set of meta-relations available for every entity types
    91 _LOGGER = getLogger('cubicweb.schemaloader')
   101 _LOGGER = getLogger('cubicweb.schemaloader')
    92 
   102 
    93 # entity and relation schema created from serialized schema have an eid
   103 # entity and relation schema created from serialized schema have an eid
    94 ybo.ETYPE_PROPERTIES += ('eid',)
   104 ybo.ETYPE_PROPERTIES += ('eid',)
    95 ybo.RTYPE_PROPERTIES += ('eid',)
   105 ybo.RTYPE_PROPERTIES += ('eid',)
    96 
       
    97 PUB_SYSTEM_ENTITY_PERMS = {
       
    98     'read':   ('managers', 'users', 'guests',),
       
    99     'add':    ('managers',),
       
   100     'delete': ('managers',),
       
   101     'update': ('managers',),
       
   102     }
       
   103 PUB_SYSTEM_REL_PERMS = {
       
   104     'read':   ('managers', 'users', 'guests',),
       
   105     'add':    ('managers',),
       
   106     'delete': ('managers',),
       
   107     }
       
   108 PUB_SYSTEM_ATTR_PERMS = {
       
   109     'read':   ('managers', 'users', 'guests',),
       
   110     'update': ('managers',),
       
   111     }
       
   112 RO_REL_PERMS = {
       
   113     'read':   ('managers', 'users', 'guests',),
       
   114     'add':    (),
       
   115     'delete': (),
       
   116     }
       
   117 RO_ATTR_PERMS = {
       
   118     'read':   ('managers', 'users', 'guests',),
       
   119     'update': (),
       
   120     }
       
   121 
       
   122 # XXX same algorithm as in reorder_cubes and probably other place,
       
   123 # may probably extract a generic function
       
   124 def order_eschemas(eschemas):
       
   125     """return entity schemas ordered such that entity types which specializes an
       
   126     other one appears after that one
       
   127     """
       
   128     graph = {}
       
   129     for eschema in eschemas:
       
   130         if eschema.specializes():
       
   131             graph[eschema] = set((eschema.specializes(),))
       
   132         else:
       
   133             graph[eschema] = set()
       
   134     cycles = get_cycles(graph)
       
   135     if cycles:
       
   136         cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
       
   137         raise Exception('cycles in entity schema specialization: %s'
       
   138                         % cycles)
       
   139     eschemas = []
       
   140     while graph:
       
   141         # sorted to get predictable results
       
   142         for eschema, deps in sorted(graph.items()):
       
   143             if not deps:
       
   144                 eschemas.append(eschema)
       
   145                 del graph[eschema]
       
   146                 for deps in graph.itervalues():
       
   147                     try:
       
   148                         deps.remove(eschema)
       
   149                     except KeyError:
       
   150                         continue
       
   151     return eschemas
       
   152 
       
   153 def bw_normalize_etype(etype):
       
   154     if etype in ETYPE_NAME_MAP:
       
   155         msg = '%s has been renamed to %s, please update your code' % (
       
   156             etype, ETYPE_NAME_MAP[etype])
       
   157         warn(msg, DeprecationWarning, stacklevel=4)
       
   158         etype = ETYPE_NAME_MAP[etype]
       
   159     return etype
       
   160 
       
   161 def display_name(req, key, form='', context=None):
       
   162     """return a internationalized string for the key (schema entity or relation
       
   163     name) in a given form
       
   164     """
       
   165     assert form in ('', 'plural', 'subject', 'object')
       
   166     if form == 'subject':
       
   167         form = ''
       
   168     if form:
       
   169         key = key + '_' + form
       
   170     # ensure unicode
       
   171     if context is not None:
       
   172         return unicode(req.pgettext(context, key))
       
   173     else:
       
   174         return unicode(req._(key))
       
   175 
       
   176 
       
   177 # Schema objects definition ###################################################
       
   178 
       
   179 def ERSchema_display_name(self, req, form='', context=None):
       
   180     """return a internationalized string for the entity/relation type name in
       
   181     a given form
       
   182     """
       
   183     return display_name(req, self.type, form, context)
       
   184 ERSchema.display_name = ERSchema_display_name
       
   185 
       
   186 @cached
       
   187 def get_groups(self, action):
       
   188     """return the groups authorized to perform <action> on entities of
       
   189     this type
       
   190 
       
   191     :type action: str
       
   192     :param action: the name of a permission
       
   193 
       
   194     :rtype: tuple
       
   195     :return: names of the groups with the given permission
       
   196     """
       
   197     assert action in self.ACTIONS, action
       
   198     #assert action in self._groups, '%s %s' % (self, action)
       
   199     try:
       
   200         return frozenset(g for g in self.permissions[action] if isinstance(g, basestring))
       
   201     except KeyError:
       
   202         return ()
       
   203 PermissionMixIn.get_groups = get_groups
       
   204 
       
   205 @cached
       
   206 def get_rqlexprs(self, action):
       
   207     """return the rql expressions representing queries to check the user is allowed
       
   208     to perform <action> on entities of this type
       
   209 
       
   210     :type action: str
       
   211     :param action: the name of a permission
       
   212 
       
   213     :rtype: tuple
       
   214     :return: the rql expressions with the given permission
       
   215     """
       
   216     assert action in self.ACTIONS, action
       
   217     #assert action in self._rqlexprs, '%s %s' % (self, action)
       
   218     try:
       
   219         return tuple(g for g in self.permissions[action] if not isinstance(g, basestring))
       
   220     except KeyError:
       
   221         return ()
       
   222 PermissionMixIn.get_rqlexprs = get_rqlexprs
       
   223 
       
   224 orig_set_action_permissions = PermissionMixIn.set_action_permissions
       
   225 def set_action_permissions(self, action, permissions):
       
   226     """set the groups and rql expressions allowing to perform <action> on
       
   227     entities of this type
       
   228 
       
   229     :type action: str
       
   230     :param action: the name of a permission
       
   231 
       
   232     :type permissions: tuple
       
   233     :param permissions: the groups and rql expressions allowing the given action
       
   234     """
       
   235     orig_set_action_permissions(self, action, tuple(permissions))
       
   236     clear_cache(self, 'get_rqlexprs')
       
   237     clear_cache(self, 'get_groups')
       
   238 PermissionMixIn.set_action_permissions = set_action_permissions
       
   239 
       
   240 def has_local_role(self, action):
       
   241     """return true if the action *may* be granted localy (eg either rql
       
   242     expressions or the owners group are used in security definition)
       
   243 
       
   244     XXX this method is only there since we don't know well how to deal with
       
   245     'add' action checking. Also find a better name would be nice.
       
   246     """
       
   247     assert action in self.ACTIONS, action
       
   248     if self.get_rqlexprs(action):
       
   249         return True
       
   250     if action in ('update', 'delete'):
       
   251         return 'owners' in self.get_groups(action)
       
   252     return False
       
   253 PermissionMixIn.has_local_role = has_local_role
       
   254 
       
   255 def may_have_permission(self, action, req):
       
   256     if action != 'read' and not (self.has_local_role('read') or
       
   257                                  self.has_perm(req, 'read')):
       
   258         return False
       
   259     return self.has_local_role(action) or self.has_perm(req, action)
       
   260 PermissionMixIn.may_have_permission = may_have_permission
       
   261 
       
   262 def has_perm(self, _cw, action, **kwargs):
       
   263     """return true if the action is granted globaly or localy"""
       
   264     try:
       
   265         self.check_perm(_cw, action, **kwargs)
       
   266         return True
       
   267     except Unauthorized:
       
   268         return False
       
   269 PermissionMixIn.has_perm = has_perm
       
   270 
       
   271 def check_perm(self, _cw, action, **kwargs):
       
   272     # NB: _cw may be a server transaction or a request object.
       
   273     #
       
   274     # check user is in an allowed group, if so that's enough internal
       
   275     # transactions should always stop there
       
   276     groups = self.get_groups(action)
       
   277     if _cw.user.matching_groups(groups):
       
   278         return
       
   279     # if 'owners' in allowed groups, check if the user actually owns this
       
   280     # object, if so that's enough
       
   281     #
       
   282     # NB: give _cw to user.owns since user is not be bound to a transaction on
       
   283     # the repository side
       
   284     if 'owners' in groups and (
       
   285           kwargs.get('creating')
       
   286           or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
       
   287         return
       
   288     # else if there is some rql expressions, check them
       
   289     if any(rqlexpr.check(_cw, **kwargs)
       
   290            for rqlexpr in self.get_rqlexprs(action)):
       
   291         return
       
   292     raise Unauthorized(action, str(self))
       
   293 PermissionMixIn.check_perm = check_perm
       
   294 
       
   295 
       
   296 RelationDefinitionSchema._RPROPERTIES['eid'] = None
       
   297 # remember rproperties defined at this point. Others will have to be serialized in
       
   298 # CWAttribute.extra_props
       
   299 KNOWN_RPROPERTIES = RelationDefinitionSchema.ALL_PROPERTIES()
       
   300 
       
   301 def rql_expression(self, expression, mainvars=None, eid=None):
       
   302     """rql expression factory"""
       
   303     if self.rtype.final:
       
   304         return ERQLExpression(expression, mainvars, eid)
       
   305     return RRQLExpression(expression, mainvars, eid)
       
   306 RelationDefinitionSchema.rql_expression = rql_expression
       
   307 
       
   308 orig_check_permission_definitions = RelationDefinitionSchema.check_permission_definitions
       
   309 def check_permission_definitions(self):
       
   310     orig_check_permission_definitions(self)
       
   311     schema = self.subject.schema
       
   312     for action, groups in self.permissions.iteritems():
       
   313         for group_or_rqlexpr in groups:
       
   314             if action == 'read' and \
       
   315                    isinstance(group_or_rqlexpr, RQLExpression):
       
   316                 msg = "can't use rql expression for read permission of %s"
       
   317                 raise BadSchemaDefinition(msg % self)
       
   318             if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   319                 msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   320                 raise BadSchemaDefinition(msg % self)
       
   321             if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
       
   322                 msg = "can't use ERQLExpression on %s, use a RRQLExpression"
       
   323                 raise BadSchemaDefinition(msg % self)
       
   324 RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
       
   325 
       
   326 
       
   327 class CubicWebEntitySchema(EntitySchema):
       
   328     """a entity has a type, a set of subject and or object relations
       
   329     the entity schema defines the possible relations for a given type and some
       
   330     constraints on those relations
       
   331     """
       
   332     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
       
   333         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
       
   334         if eid is None and edef is not None:
       
   335             eid = getattr(edef, 'eid', None)
       
   336         self.eid = eid
       
   337 
       
   338     def check_permission_definitions(self):
       
   339         super(CubicWebEntitySchema, self).check_permission_definitions()
       
   340         for groups in self.permissions.itervalues():
       
   341             for group_or_rqlexpr in groups:
       
   342                 if isinstance(group_or_rqlexpr, RRQLExpression):
       
   343                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   344                     raise BadSchemaDefinition(msg % self.type)
       
   345 
       
   346     def is_subobject(self, strict=False, skiprels=None):
       
   347         if skiprels is None:
       
   348             skiprels = SKIP_COMPOSITE_RELS
       
   349         else:
       
   350             skiprels += SKIP_COMPOSITE_RELS
       
   351         return super(CubicWebEntitySchema, self).is_subobject(strict,
       
   352                                                               skiprels=skiprels)
       
   353 
       
   354     def attribute_definitions(self):
       
   355         """return an iterator on attribute definitions
       
   356 
       
   357         attribute relations are a subset of subject relations where the
       
   358         object's type is a final entity
       
   359 
       
   360         an attribute definition is a 2-uple :
       
   361         * name of the relation
       
   362         * schema of the destination entity type
       
   363         """
       
   364         iter = super(CubicWebEntitySchema, self).attribute_definitions()
       
   365         for rschema, attrschema in iter:
       
   366             if rschema.type == 'has_text':
       
   367                 continue
       
   368             yield rschema, attrschema
       
   369 
       
   370     def main_attribute(self):
       
   371         """convenience method that returns the *main* (i.e. the first non meta)
       
   372         attribute defined in the entity schema
       
   373         """
       
   374         for rschema, _ in self.attribute_definitions():
       
   375             if not (rschema in META_RTYPES
       
   376                     or self.is_metadata(rschema)):
       
   377                 return rschema
       
   378 
       
   379     def add_subject_relation(self, rschema):
       
   380         """register the relation schema as possible subject relation"""
       
   381         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
       
   382         if rschema.final:
       
   383             if self.rdef(rschema).get('fulltextindexed'):
       
   384                 self._update_has_text()
       
   385         elif rschema.fulltext_container:
       
   386             self._update_has_text()
       
   387 
       
   388     def add_object_relation(self, rschema):
       
   389         """register the relation schema as possible object relation"""
       
   390         super(CubicWebEntitySchema, self).add_object_relation(rschema)
       
   391         if rschema.fulltext_container:
       
   392             self._update_has_text()
       
   393 
       
   394     def del_subject_relation(self, rtype):
       
   395         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
       
   396         if 'has_text' in self.subjrels:
       
   397             self._update_has_text(deletion=True)
       
   398 
       
   399     def del_object_relation(self, rtype):
       
   400         super(CubicWebEntitySchema, self).del_object_relation(rtype)
       
   401         if 'has_text' in self.subjrels:
       
   402             self._update_has_text(deletion=True)
       
   403 
       
   404     def _update_has_text(self, deletion=False):
       
   405         may_need_has_text, has_has_text = False, False
       
   406         need_has_text = None
       
   407         for rschema in self.subject_relations():
       
   408             if rschema.final:
       
   409                 if rschema == 'has_text':
       
   410                     has_has_text = True
       
   411                 elif self.rdef(rschema).get('fulltextindexed'):
       
   412                     may_need_has_text = True
       
   413             elif rschema.fulltext_container:
       
   414                 if rschema.fulltext_container == 'subject':
       
   415                     may_need_has_text = True
       
   416                 else:
       
   417                     need_has_text = False
       
   418         for rschema in self.object_relations():
       
   419             if rschema.fulltext_container:
       
   420                 if rschema.fulltext_container == 'object':
       
   421                     may_need_has_text = True
       
   422                 else:
       
   423                     need_has_text = False
       
   424         if need_has_text is None:
       
   425             need_has_text = may_need_has_text
       
   426         if need_has_text and not has_has_text and not deletion:
       
   427             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String',
       
   428                                           __permissions__=RO_ATTR_PERMS)
       
   429             self.schema.add_relation_def(rdef)
       
   430         elif not need_has_text and has_has_text:
       
   431             # use rschema.del_relation_def and not schema.del_relation_def to
       
   432             # avoid deleting the relation type accidentally...
       
   433             self.schema['has_text'].del_relation_def(self, self.schema['String'])
       
   434 
       
   435     def schema_entity(self): # XXX @property for consistency with meta
       
   436         """return True if this entity type is used to build the schema"""
       
   437         return self.type in SCHEMA_TYPES
       
   438 
       
   439     def rql_expression(self, expression, mainvars=None, eid=None):
       
   440         """rql expression factory"""
       
   441         return ERQLExpression(expression, mainvars, eid)
       
   442 
       
   443 
       
   444 class CubicWebRelationSchema(RelationSchema):
       
   445 
       
   446     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
       
   447         if rdef is not None:
       
   448             # if this relation is inlined
       
   449             self.inlined = rdef.inlined
       
   450         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
       
   451         if eid is None and rdef is not None:
       
   452             eid = getattr(rdef, 'eid', None)
       
   453         self.eid = eid
       
   454 
       
   455     @property
       
   456     def meta(self):
       
   457         return self.type in META_RTYPES
       
   458 
       
   459     def schema_relation(self): # XXX @property for consistency with meta
       
   460         """return True if this relation type is used to build the schema"""
       
   461         return self.type in SCHEMA_TYPES
       
   462 
       
   463     def may_have_permission(self, action, req, eschema=None, role=None):
       
   464         if eschema is not None:
       
   465             for tschema in self.targets(eschema, role):
       
   466                 rdef = self.role_rdef(eschema, tschema, role)
       
   467                 if rdef.may_have_permission(action, req):
       
   468                     return True
       
   469         else:
       
   470             for rdef in self.rdefs.itervalues():
       
   471                 if rdef.may_have_permission(action, req):
       
   472                     return True
       
   473         return False
       
   474 
       
   475     def has_perm(self, _cw, action, **kwargs):
       
   476         """return true if the action is granted globaly or localy"""
       
   477         if self.final:
       
   478             assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
       
   479             assert action in ('read', 'update')
       
   480             if 'eid' in kwargs:
       
   481                 subjtype = _cw.describe(kwargs['eid'])[0]
       
   482             else:
       
   483                 subjtype = objtype = None
       
   484         else:
       
   485             assert not 'eid' in kwargs, kwargs
       
   486             assert action in ('read', 'add', 'delete')
       
   487             if 'fromeid' in kwargs:
       
   488                 subjtype = _cw.describe(kwargs['fromeid'])[0]
       
   489             elif 'frometype' in kwargs:
       
   490                 subjtype = kwargs.pop('frometype')
       
   491             else:
       
   492                 subjtype = None
       
   493             if 'toeid' in kwargs:
       
   494                 objtype = _cw.describe(kwargs['toeid'])[0]
       
   495             elif 'toetype' in kwargs:
       
   496                 objtype = kwargs.pop('toetype')
       
   497             else:
       
   498                 objtype = None
       
   499         if objtype and subjtype:
       
   500             return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs)
       
   501         elif subjtype:
       
   502             for tschema in self.targets(subjtype, 'subject'):
       
   503                 rdef = self.rdef(subjtype, tschema)
       
   504                 if not rdef.has_perm(_cw, action, **kwargs):
       
   505                     return False
       
   506         elif objtype:
       
   507             for tschema in self.targets(objtype, 'object'):
       
   508                 rdef = self.rdef(tschema, objtype)
       
   509                 if not rdef.has_perm(_cw, action, **kwargs):
       
   510                     return False
       
   511         else:
       
   512             for rdef in self.rdefs.itervalues():
       
   513                 if not rdef.has_perm(_cw, action, **kwargs):
       
   514                     return False
       
   515         return True
       
   516 
       
   517     @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
       
   518     def cardinality(self, subjtype, objtype, target):
       
   519         return self.rdef(subjtype, objtype).role_cardinality(target)
       
   520 
       
   521 
       
   522 class CubicWebSchema(Schema):
       
   523     """set of entities and relations schema defining the possible data sets
       
   524     used in an application
       
   525 
       
   526     :type name: str
       
   527     :ivar name: name of the schema, usually the instance identifier
       
   528 
       
   529     :type base: str
       
   530     :ivar base: path of the directory where the schema is defined
       
   531     """
       
   532     reading_from_database = False
       
   533     entity_class = CubicWebEntitySchema
       
   534     relation_class = CubicWebRelationSchema
       
   535     no_specialization_inference = ('identity',)
       
   536 
       
   537     def __init__(self, *args, **kwargs):
       
   538         self._eid_index = {}
       
   539         super(CubicWebSchema, self).__init__(*args, **kwargs)
       
   540         ybo.register_base_types(self)
       
   541         rschema = self.add_relation_type(ybo.RelationType('eid'))
       
   542         rschema.final = True
       
   543         rschema = self.add_relation_type(ybo.RelationType('has_text'))
       
   544         rschema.final = True
       
   545         rschema = self.add_relation_type(ybo.RelationType('identity'))
       
   546         rschema.final = False
       
   547 
       
   548     etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[A-Za-z0-9]*$'
       
   549     def add_entity_type(self, edef):
       
   550         edef.name = edef.name.encode()
       
   551         edef.name = bw_normalize_etype(edef.name)
       
   552         if not re.match(self.etype_name_re, edef.name):
       
   553             raise BadSchemaDefinition(
       
   554                 '%r is not a valid name for an entity type. It should start '
       
   555                 'with an upper cased letter and be followed by at least a '
       
   556                 'lower cased letter' % edef.name)
       
   557         eschema = super(CubicWebSchema, self).add_entity_type(edef)
       
   558         if not eschema.final:
       
   559             # automatically add the eid relation to non final entity types
       
   560             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
       
   561                                           cardinality='11', uid=True,
       
   562                                           __permissions__=RO_ATTR_PERMS)
       
   563             self.add_relation_def(rdef)
       
   564             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type,
       
   565                                           __permissions__=RO_REL_PERMS)
       
   566             self.add_relation_def(rdef)
       
   567         self._eid_index[eschema.eid] = eschema
       
   568         return eschema
       
   569 
       
   570     def add_relation_type(self, rdef):
       
   571         if not rdef.name.islower():
       
   572             raise BadSchemaDefinition(
       
   573                 '%r is not a valid name for a relation type. It should be '
       
   574                 'lower cased' % rdef.name)
       
   575         rdef.name = rdef.name.encode()
       
   576         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
       
   577         self._eid_index[rschema.eid] = rschema
       
   578         return rschema
       
   579 
       
   580     def add_relation_def(self, rdef):
       
   581         """build a part of a relation schema
       
   582         (i.e. add a relation between two specific entity's types)
       
   583 
       
   584         :type subject: str
       
   585         :param subject: entity's type that is subject of the relation
       
   586 
       
   587         :type rtype: str
       
   588         :param rtype: the relation's type (i.e. the name of the relation)
       
   589 
       
   590         :type obj: str
       
   591         :param obj: entity's type that is object of the relation
       
   592 
       
   593         :rtype: RelationSchema
       
   594         :param: the newly created or just completed relation schema
       
   595         """
       
   596         rdef.name = rdef.name.lower()
       
   597         rdef.subject = bw_normalize_etype(rdef.subject)
       
   598         rdef.object = bw_normalize_etype(rdef.object)
       
   599         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
       
   600         if rdefs:
       
   601             try:
       
   602                 self._eid_index[rdef.eid] = rdefs
       
   603             except AttributeError:
       
   604                 pass # not a serialized schema
       
   605         return rdefs
       
   606 
       
   607     def del_relation_type(self, rtype):
       
   608         rschema = self.rschema(rtype)
       
   609         self._eid_index.pop(rschema.eid, None)
       
   610         super(CubicWebSchema, self).del_relation_type(rtype)
       
   611 
       
   612     def del_relation_def(self, subjtype, rtype, objtype):
       
   613         for k, v in self._eid_index.items():
       
   614             if not isinstance(v, RelationDefinitionSchema):
       
   615                 continue
       
   616             if v.subject == subjtype and v.rtype == rtype and v.object == objtype:
       
   617                 del self._eid_index[k]
       
   618                 break
       
   619         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
       
   620 
       
   621     def del_entity_type(self, etype):
       
   622         eschema = self.eschema(etype)
       
   623         self._eid_index.pop(eschema.eid, None)
       
   624         # deal with has_text first, else its automatic deletion (see above)
       
   625         # may trigger an error in ancestor's del_entity_type method
       
   626         if 'has_text' in eschema.subject_relations():
       
   627             self.del_relation_def(etype, 'has_text', 'String')
       
   628         super(CubicWebSchema, self).del_entity_type(etype)
       
   629 
       
   630     def schema_by_eid(self, eid):
       
   631         return self._eid_index[eid]
       
   632 
   106 
   633 # Bases for manipulating RQL in schema #########################################
   107 # Bases for manipulating RQL in schema #########################################
   634 
   108 
   635 def guess_rrqlexpr_mainvars(expression):
   109 def guess_rrqlexpr_mainvars(expression):
   636     defined = set(split_expression(expression))
   110     defined = set(split_expression(expression))
   702                              'expression %s', mainvar, self)
   176                              'expression %s', mainvar, self)
   703         # syntax tree used by read security (inserted in queries when necessary)
   177         # syntax tree used by read security (inserted in queries when necessary)
   704         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
   178         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
   705         # graph of links between variables, used by rql rewriter
   179         # graph of links between variables, used by rql rewriter
   706         self.vargraph = vargraph(self.rqlst)
   180         self.vargraph = vargraph(self.rqlst)
       
   181         # useful for some instrumentation, e.g. localperms permcheck command
       
   182         self.package = ybo.PACKAGE
   707 
   183 
   708     def __str__(self):
   184     def __str__(self):
   709         return self.full_rql
   185         return self.full_rql
   710     def __repr__(self):
   186     def __repr__(self):
   711         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
   187         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
   852     @property
   328     @property
   853     def minimal_rql(self):
   329     def minimal_rql(self):
   854         return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
   330         return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
   855                                     self.expression)
   331                                     self.expression)
   856 
   332 
       
   333 
       
   334 
   857 # rql expressions for use in permission definition #############################
   335 # rql expressions for use in permission definition #############################
   858 
   336 
   859 class ERQLExpression(RQLExpression):
   337 class ERQLExpression(RQLExpression):
   860     predefined_variables = 'XU'
   338     predefined_variables = 'XU'
   861 
   339 
   918                 return False
   396                 return False
   919             kwargs['o'] = toeid
   397             kwargs['o'] = toeid
   920         return self._check(_cw, **kwargs)
   398         return self._check(_cw, **kwargs)
   921 
   399 
   922 
   400 
   923 # in yams, default 'update' perm for attributes granted to managers and owners.
   401 # In yams, default 'update' perm for attributes granted to managers and owners.
   924 # Within cw, we want to default to users who may edit the entity holding the
   402 # Within cw, we want to default to users who may edit the entity holding the
   925 # attribute.
   403 # attribute.
   926 ybo.DEFAULT_ATTRPERMS['update'] = (
   404 # These default permissions won't be checked by the security hooks:
   927     'managers', ERQLExpression('U has_update_permission X'))
   405 # since they delegate checking to the entity, we can skip actual checks.
       
   406 ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X'))
       
   407 ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X'))
       
   408 
       
   409 
       
   410 PUB_SYSTEM_ENTITY_PERMS = {
       
   411     'read':   ('managers', 'users', 'guests',),
       
   412     'add':    ('managers',),
       
   413     'delete': ('managers',),
       
   414     'update': ('managers',),
       
   415     }
       
   416 PUB_SYSTEM_REL_PERMS = {
       
   417     'read':   ('managers', 'users', 'guests',),
       
   418     'add':    ('managers',),
       
   419     'delete': ('managers',),
       
   420     }
       
   421 PUB_SYSTEM_ATTR_PERMS = {
       
   422     'read':   ('managers', 'users', 'guests',),
       
   423     'add': ('managers',),
       
   424     'update': ('managers',),
       
   425     }
       
   426 RO_REL_PERMS = {
       
   427     'read':   ('managers', 'users', 'guests',),
       
   428     'add':    (),
       
   429     'delete': (),
       
   430     }
       
   431 RO_ATTR_PERMS = {
       
   432     'read':   ('managers', 'users', 'guests',),
       
   433     'add': ybo.DEFAULT_ATTRPERMS['add'],
       
   434     'update': (),
       
   435     }
       
   436 
       
   437 # XXX same algorithm as in reorder_cubes and probably other place,
       
   438 # may probably extract a generic function
       
   439 def order_eschemas(eschemas):
       
   440     """return entity schemas ordered such that entity types which specializes an
       
   441     other one appears after that one
       
   442     """
       
   443     graph = {}
       
   444     for eschema in eschemas:
       
   445         if eschema.specializes():
       
   446             graph[eschema] = set((eschema.specializes(),))
       
   447         else:
       
   448             graph[eschema] = set()
       
   449     cycles = get_cycles(graph)
       
   450     if cycles:
       
   451         cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
       
   452         raise Exception('cycles in entity schema specialization: %s'
       
   453                         % cycles)
       
   454     eschemas = []
       
   455     while graph:
       
   456         # sorted to get predictable results
       
   457         for eschema, deps in sorted(graph.items()):
       
   458             if not deps:
       
   459                 eschemas.append(eschema)
       
   460                 del graph[eschema]
       
   461                 for deps in graph.itervalues():
       
   462                     try:
       
   463                         deps.remove(eschema)
       
   464                     except KeyError:
       
   465                         continue
       
   466     return eschemas
       
   467 
       
   468 def bw_normalize_etype(etype):
       
   469     if etype in ETYPE_NAME_MAP:
       
   470         msg = '%s has been renamed to %s, please update your code' % (
       
   471             etype, ETYPE_NAME_MAP[etype])
       
   472         warn(msg, DeprecationWarning, stacklevel=4)
       
   473         etype = ETYPE_NAME_MAP[etype]
       
   474     return etype
       
   475 
       
   476 def display_name(req, key, form='', context=None):
       
   477     """return a internationalized string for the key (schema entity or relation
       
   478     name) in a given form
       
   479     """
       
   480     assert form in ('', 'plural', 'subject', 'object')
       
   481     if form == 'subject':
       
   482         form = ''
       
   483     if form:
       
   484         key = key + '_' + form
       
   485     # ensure unicode
       
   486     if context is not None:
       
   487         return unicode(req.pgettext(context, key))
       
   488     else:
       
   489         return unicode(req._(key))
       
   490 
       
   491 
       
   492 # Schema objects definition ###################################################
       
   493 
       
   494 def ERSchema_display_name(self, req, form='', context=None):
       
   495     """return a internationalized string for the entity/relation type name in
       
   496     a given form
       
   497     """
       
   498     return display_name(req, self.type, form, context)
       
   499 ERSchema.display_name = ERSchema_display_name
       
   500 
       
   501 @cached
       
   502 def get_groups(self, action):
       
   503     """return the groups authorized to perform <action> on entities of
       
   504     this type
       
   505 
       
   506     :type action: str
       
   507     :param action: the name of a permission
       
   508 
       
   509     :rtype: tuple
       
   510     :return: names of the groups with the given permission
       
   511     """
       
   512     assert action in self.ACTIONS, action
       
   513     #assert action in self._groups, '%s %s' % (self, action)
       
   514     try:
       
   515         return frozenset(g for g in self.permissions[action] if isinstance(g, basestring))
       
   516     except KeyError:
       
   517         return ()
       
   518 PermissionMixIn.get_groups = get_groups
       
   519 
       
   520 @cached
       
   521 def get_rqlexprs(self, action):
       
   522     """return the rql expressions representing queries to check the user is allowed
       
   523     to perform <action> on entities of this type
       
   524 
       
   525     :type action: str
       
   526     :param action: the name of a permission
       
   527 
       
   528     :rtype: tuple
       
   529     :return: the rql expressions with the given permission
       
   530     """
       
   531     assert action in self.ACTIONS, action
       
   532     #assert action in self._rqlexprs, '%s %s' % (self, action)
       
   533     try:
       
   534         return tuple(g for g in self.permissions[action] if not isinstance(g, basestring))
       
   535     except KeyError:
       
   536         return ()
       
   537 PermissionMixIn.get_rqlexprs = get_rqlexprs
       
   538 
       
   539 orig_set_action_permissions = PermissionMixIn.set_action_permissions
       
   540 def set_action_permissions(self, action, permissions):
       
   541     """set the groups and rql expressions allowing to perform <action> on
       
   542     entities of this type
       
   543 
       
   544     :type action: str
       
   545     :param action: the name of a permission
       
   546 
       
   547     :type permissions: tuple
       
   548     :param permissions: the groups and rql expressions allowing the given action
       
   549     """
       
   550     orig_set_action_permissions(self, action, tuple(permissions))
       
   551     clear_cache(self, 'get_rqlexprs')
       
   552     clear_cache(self, 'get_groups')
       
   553 PermissionMixIn.set_action_permissions = set_action_permissions
       
   554 
       
   555 def has_local_role(self, action):
       
   556     """return true if the action *may* be granted localy (eg either rql
       
   557     expressions or the owners group are used in security definition)
       
   558 
       
   559     XXX this method is only there since we don't know well how to deal with
       
   560     'add' action checking. Also find a better name would be nice.
       
   561     """
       
   562     assert action in self.ACTIONS, action
       
   563     if self.get_rqlexprs(action):
       
   564         return True
       
   565     if action in ('update', 'delete'):
       
   566         return 'owners' in self.get_groups(action)
       
   567     return False
       
   568 PermissionMixIn.has_local_role = has_local_role
       
   569 
       
   570 def may_have_permission(self, action, req):
       
   571     if action != 'read' and not (self.has_local_role('read') or
       
   572                                  self.has_perm(req, 'read')):
       
   573         return False
       
   574     return self.has_local_role(action) or self.has_perm(req, action)
       
   575 PermissionMixIn.may_have_permission = may_have_permission
       
   576 
       
   577 def has_perm(self, _cw, action, **kwargs):
       
   578     """return true if the action is granted globaly or localy"""
       
   579     try:
       
   580         self.check_perm(_cw, action, **kwargs)
       
   581         return True
       
   582     except Unauthorized:
       
   583         return False
       
   584 PermissionMixIn.has_perm = has_perm
       
   585 
       
   586 
       
   587 def check_perm(self, _cw, action, **kwargs):
       
   588     # NB: _cw may be a server transaction or a request object.
       
   589     #
       
   590     # check user is in an allowed group, if so that's enough internal
       
   591     # transactions should always stop there
       
   592     DBG = False
       
   593     if server.DEBUG & server.DBG_SEC:
       
   594         if action in server._SECURITY_CAPS:
       
   595             _self_str = str(self)
       
   596             if server._SECURITY_ITEMS:
       
   597                 if any(item in _self_str for item in server._SECURITY_ITEMS):
       
   598                     DBG = True
       
   599             else:
       
   600                 DBG = True
       
   601     groups = self.get_groups(action)
       
   602     if _cw.user.matching_groups(groups):
       
   603         if DBG:
       
   604             print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups)
       
   605         return
       
   606     # if 'owners' in allowed groups, check if the user actually owns this
       
   607     # object, if so that's enough
       
   608     #
       
   609     # NB: give _cw to user.owns since user is not be bound to a transaction on
       
   610     # the repository side
       
   611     if 'owners' in groups and (
       
   612           kwargs.get('creating')
       
   613           or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
       
   614         if DBG:
       
   615             print ('check_perm: %r %r: user is owner or creation time' %
       
   616                    (action, _self_str))
       
   617         return
       
   618     # else if there is some rql expressions, check them
       
   619     if DBG:
       
   620         print ('check_perm: %r %r %s' %
       
   621                (action, _self_str, [(rqlexpr, kwargs, rqlexpr.check(_cw, **kwargs))
       
   622                                     for rqlexpr in self.get_rqlexprs(action)]))
       
   623     if any(rqlexpr.check(_cw, **kwargs)
       
   624            for rqlexpr in self.get_rqlexprs(action)):
       
   625         return
       
   626     raise Unauthorized(action, str(self))
       
   627 PermissionMixIn.check_perm = check_perm
       
   628 
       
   629 
       
   630 RelationDefinitionSchema._RPROPERTIES['eid'] = None
       
   631 # remember rproperties defined at this point. Others will have to be serialized in
       
   632 # CWAttribute.extra_props
       
   633 KNOWN_RPROPERTIES = RelationDefinitionSchema.ALL_PROPERTIES()
       
   634 
       
   635 def rql_expression(self, expression, mainvars=None, eid=None):
       
   636     """rql expression factory"""
       
   637     if self.rtype.final:
       
   638         return ERQLExpression(expression, mainvars, eid)
       
   639     return RRQLExpression(expression, mainvars, eid)
       
   640 RelationDefinitionSchema.rql_expression = rql_expression
       
   641 
       
   642 orig_check_permission_definitions = RelationDefinitionSchema.check_permission_definitions
       
   643 def check_permission_definitions(self):
       
   644     orig_check_permission_definitions(self)
       
   645     schema = self.subject.schema
       
   646     for action, groups in self.permissions.iteritems():
       
   647         for group_or_rqlexpr in groups:
       
   648             if action == 'read' and \
       
   649                    isinstance(group_or_rqlexpr, RQLExpression):
       
   650                 msg = "can't use rql expression for read permission of %s"
       
   651                 raise BadSchemaDefinition(msg % self)
       
   652             if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   653                 msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   654                 raise BadSchemaDefinition(msg % self)
       
   655             if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
       
   656                 msg = "can't use ERQLExpression on %s, use a RRQLExpression"
       
   657                 raise BadSchemaDefinition(msg % self)
       
   658 RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
       
   659 
       
   660 
       
   661 class CubicWebEntitySchema(EntitySchema):
       
   662     """a entity has a type, a set of subject and or object relations
       
   663     the entity schema defines the possible relations for a given type and some
       
   664     constraints on those relations
       
   665     """
       
   666     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
       
   667         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
       
   668         if eid is None and edef is not None:
       
   669             eid = getattr(edef, 'eid', None)
       
   670         self.eid = eid
       
   671 
       
   672     def check_permission_definitions(self):
       
   673         super(CubicWebEntitySchema, self).check_permission_definitions()
       
   674         for groups in self.permissions.itervalues():
       
   675             for group_or_rqlexpr in groups:
       
   676                 if isinstance(group_or_rqlexpr, RRQLExpression):
       
   677                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   678                     raise BadSchemaDefinition(msg % self.type)
       
   679 
       
   680     def is_subobject(self, strict=False, skiprels=None):
       
   681         if skiprels is None:
       
   682             skiprels = SKIP_COMPOSITE_RELS
       
   683         else:
       
   684             skiprels += SKIP_COMPOSITE_RELS
       
   685         return super(CubicWebEntitySchema, self).is_subobject(strict,
       
   686                                                               skiprels=skiprels)
       
   687 
       
   688     def attribute_definitions(self):
       
   689         """return an iterator on attribute definitions
       
   690 
       
   691         attribute relations are a subset of subject relations where the
       
   692         object's type is a final entity
       
   693 
       
   694         an attribute definition is a 2-uple :
       
   695         * name of the relation
       
   696         * schema of the destination entity type
       
   697         """
       
   698         iter = super(CubicWebEntitySchema, self).attribute_definitions()
       
   699         for rschema, attrschema in iter:
       
   700             if rschema.type == 'has_text':
       
   701                 continue
       
   702             yield rschema, attrschema
       
   703 
       
   704     def main_attribute(self):
       
   705         """convenience method that returns the *main* (i.e. the first non meta)
       
   706         attribute defined in the entity schema
       
   707         """
       
   708         for rschema, _ in self.attribute_definitions():
       
   709             if not (rschema in META_RTYPES
       
   710                     or self.is_metadata(rschema)):
       
   711                 return rschema
       
   712 
       
   713     def add_subject_relation(self, rschema):
       
   714         """register the relation schema as possible subject relation"""
       
   715         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
       
   716         if rschema.final:
       
   717             if self.rdef(rschema).get('fulltextindexed'):
       
   718                 self._update_has_text()
       
   719         elif rschema.fulltext_container:
       
   720             self._update_has_text()
       
   721 
       
   722     def add_object_relation(self, rschema):
       
   723         """register the relation schema as possible object relation"""
       
   724         super(CubicWebEntitySchema, self).add_object_relation(rschema)
       
   725         if rschema.fulltext_container:
       
   726             self._update_has_text()
       
   727 
       
   728     def del_subject_relation(self, rtype):
       
   729         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
       
   730         if 'has_text' in self.subjrels:
       
   731             self._update_has_text(deletion=True)
       
   732 
       
   733     def del_object_relation(self, rtype):
       
   734         super(CubicWebEntitySchema, self).del_object_relation(rtype)
       
   735         if 'has_text' in self.subjrels:
       
   736             self._update_has_text(deletion=True)
       
   737 
       
   738     def _update_has_text(self, deletion=False):
       
   739         may_need_has_text, has_has_text = False, False
       
   740         need_has_text = None
       
   741         for rschema in self.subject_relations():
       
   742             if rschema.final:
       
   743                 if rschema == 'has_text':
       
   744                     has_has_text = True
       
   745                 elif self.rdef(rschema).get('fulltextindexed'):
       
   746                     may_need_has_text = True
       
   747             elif rschema.fulltext_container:
       
   748                 if rschema.fulltext_container == 'subject':
       
   749                     may_need_has_text = True
       
   750                 else:
       
   751                     need_has_text = False
       
   752         for rschema in self.object_relations():
       
   753             if rschema.fulltext_container:
       
   754                 if rschema.fulltext_container == 'object':
       
   755                     may_need_has_text = True
       
   756                 else:
       
   757                     need_has_text = False
       
   758         if need_has_text is None:
       
   759             need_has_text = may_need_has_text
       
   760         if need_has_text and not has_has_text and not deletion:
       
   761             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String',
       
   762                                           __permissions__=RO_ATTR_PERMS)
       
   763             self.schema.add_relation_def(rdef)
       
   764         elif not need_has_text and has_has_text:
       
   765             # use rschema.del_relation_def and not schema.del_relation_def to
       
   766             # avoid deleting the relation type accidentally...
       
   767             self.schema['has_text'].del_relation_def(self, self.schema['String'])
       
   768 
       
   769     def schema_entity(self): # XXX @property for consistency with meta
       
   770         """return True if this entity type is used to build the schema"""
       
   771         return self.type in SCHEMA_TYPES
       
   772 
       
   773     def rql_expression(self, expression, mainvars=None, eid=None):
       
   774         """rql expression factory"""
       
   775         return ERQLExpression(expression, mainvars, eid)
       
   776 
       
   777 
       
   778 class CubicWebRelationSchema(RelationSchema):
       
   779 
       
   780     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
       
   781         if rdef is not None:
       
   782             # if this relation is inlined
       
   783             self.inlined = rdef.inlined
       
   784         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
       
   785         if eid is None and rdef is not None:
       
   786             eid = getattr(rdef, 'eid', None)
       
   787         self.eid = eid
       
   788 
       
   789     @property
       
   790     def meta(self):
       
   791         return self.type in META_RTYPES
       
   792 
       
   793     def schema_relation(self): # XXX @property for consistency with meta
       
   794         """return True if this relation type is used to build the schema"""
       
   795         return self.type in SCHEMA_TYPES
       
   796 
       
   797     def may_have_permission(self, action, req, eschema=None, role=None):
       
   798         if eschema is not None:
       
   799             for tschema in self.targets(eschema, role):
       
   800                 rdef = self.role_rdef(eschema, tschema, role)
       
   801                 if rdef.may_have_permission(action, req):
       
   802                     return True
       
   803         else:
       
   804             for rdef in self.rdefs.itervalues():
       
   805                 if rdef.may_have_permission(action, req):
       
   806                     return True
       
   807         return False
       
   808 
       
   809     def has_perm(self, _cw, action, **kwargs):
       
   810         """return true if the action is granted globaly or localy"""
       
   811         if self.final:
       
   812             assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
       
   813             assert action in ('read', 'update')
       
   814             if 'eid' in kwargs:
       
   815                 subjtype = _cw.describe(kwargs['eid'])[0]
       
   816             else:
       
   817                 subjtype = objtype = None
       
   818         else:
       
   819             assert not 'eid' in kwargs, kwargs
       
   820             assert action in ('read', 'add', 'delete')
       
   821             if 'fromeid' in kwargs:
       
   822                 subjtype = _cw.describe(kwargs['fromeid'])[0]
       
   823             elif 'frometype' in kwargs:
       
   824                 subjtype = kwargs.pop('frometype')
       
   825             else:
       
   826                 subjtype = None
       
   827             if 'toeid' in kwargs:
       
   828                 objtype = _cw.describe(kwargs['toeid'])[0]
       
   829             elif 'toetype' in kwargs:
       
   830                 objtype = kwargs.pop('toetype')
       
   831             else:
       
   832                 objtype = None
       
   833         if objtype and subjtype:
       
   834             return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs)
       
   835         elif subjtype:
       
   836             for tschema in self.targets(subjtype, 'subject'):
       
   837                 rdef = self.rdef(subjtype, tschema)
       
   838                 if not rdef.has_perm(_cw, action, **kwargs):
       
   839                     return False
       
   840         elif objtype:
       
   841             for tschema in self.targets(objtype, 'object'):
       
   842                 rdef = self.rdef(tschema, objtype)
       
   843                 if not rdef.has_perm(_cw, action, **kwargs):
       
   844                     return False
       
   845         else:
       
   846             for rdef in self.rdefs.itervalues():
       
   847                 if not rdef.has_perm(_cw, action, **kwargs):
       
   848                     return False
       
   849         return True
       
   850 
       
   851     @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
       
   852     def cardinality(self, subjtype, objtype, target):
       
   853         return self.rdef(subjtype, objtype).role_cardinality(target)
       
   854 
       
   855 
       
   856 class CubicWebSchema(Schema):
       
   857     """set of entities and relations schema defining the possible data sets
       
   858     used in an application
       
   859 
       
   860     :type name: str
       
   861     :ivar name: name of the schema, usually the instance identifier
       
   862 
       
   863     :type base: str
       
   864     :ivar base: path of the directory where the schema is defined
       
   865     """
       
   866     reading_from_database = False
       
   867     entity_class = CubicWebEntitySchema
       
   868     relation_class = CubicWebRelationSchema
       
   869     no_specialization_inference = ('identity',)
       
   870 
       
   871     def __init__(self, *args, **kwargs):
       
   872         self._eid_index = {}
       
   873         super(CubicWebSchema, self).__init__(*args, **kwargs)
       
   874         ybo.register_base_types(self)
       
   875         rschema = self.add_relation_type(ybo.RelationType('eid'))
       
   876         rschema.final = True
       
   877         rschema = self.add_relation_type(ybo.RelationType('has_text'))
       
   878         rschema.final = True
       
   879         rschema = self.add_relation_type(ybo.RelationType('identity'))
       
   880         rschema.final = False
       
   881 
       
   882     etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[A-Za-z0-9]*$'
       
   883     def add_entity_type(self, edef):
       
   884         edef.name = edef.name.encode()
       
   885         edef.name = bw_normalize_etype(edef.name)
       
   886         if not re.match(self.etype_name_re, edef.name):
       
   887             raise BadSchemaDefinition(
       
   888                 '%r is not a valid name for an entity type. It should start '
       
   889                 'with an upper cased letter and be followed by at least a '
       
   890                 'lower cased letter' % edef.name)
       
   891         eschema = super(CubicWebSchema, self).add_entity_type(edef)
       
   892         if not eschema.final:
       
   893             # automatically add the eid relation to non final entity types
       
   894             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
       
   895                                           cardinality='11', uid=True,
       
   896                                           __permissions__=RO_ATTR_PERMS)
       
   897             self.add_relation_def(rdef)
       
   898             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type,
       
   899                                           __permissions__=RO_REL_PERMS)
       
   900             self.add_relation_def(rdef)
       
   901         self._eid_index[eschema.eid] = eschema
       
   902         return eschema
       
   903 
       
   904     def add_relation_type(self, rdef):
       
   905         if not rdef.name.islower():
       
   906             raise BadSchemaDefinition(
       
   907                 '%r is not a valid name for a relation type. It should be '
       
   908                 'lower cased' % rdef.name)
       
   909         rdef.name = rdef.name.encode()
       
   910         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
       
   911         self._eid_index[rschema.eid] = rschema
       
   912         return rschema
       
   913 
       
   914     def add_relation_def(self, rdef):
       
   915         """build a part of a relation schema
       
   916         (i.e. add a relation between two specific entity's types)
       
   917 
       
   918         :type subject: str
       
   919         :param subject: entity's type that is subject of the relation
       
   920 
       
   921         :type rtype: str
       
   922         :param rtype: the relation's type (i.e. the name of the relation)
       
   923 
       
   924         :type obj: str
       
   925         :param obj: entity's type that is object of the relation
       
   926 
       
   927         :rtype: RelationSchema
       
   928         :param: the newly created or just completed relation schema
       
   929         """
       
   930         rdef.name = rdef.name.lower()
       
   931         rdef.subject = bw_normalize_etype(rdef.subject)
       
   932         rdef.object = bw_normalize_etype(rdef.object)
       
   933         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
       
   934         if rdefs:
       
   935             try:
       
   936                 self._eid_index[rdef.eid] = rdefs
       
   937             except AttributeError:
       
   938                 pass # not a serialized schema
       
   939         return rdefs
       
   940 
       
   941     def del_relation_type(self, rtype):
       
   942         rschema = self.rschema(rtype)
       
   943         self._eid_index.pop(rschema.eid, None)
       
   944         super(CubicWebSchema, self).del_relation_type(rtype)
       
   945 
       
   946     def del_relation_def(self, subjtype, rtype, objtype):
       
   947         for k, v in self._eid_index.items():
       
   948             if not isinstance(v, RelationDefinitionSchema):
       
   949                 continue
       
   950             if v.subject == subjtype and v.rtype == rtype and v.object == objtype:
       
   951                 del self._eid_index[k]
       
   952                 break
       
   953         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
       
   954 
       
   955     def del_entity_type(self, etype):
       
   956         eschema = self.eschema(etype)
       
   957         self._eid_index.pop(eschema.eid, None)
       
   958         # deal with has_text first, else its automatic deletion (see above)
       
   959         # may trigger an error in ancestor's del_entity_type method
       
   960         if 'has_text' in eschema.subject_relations():
       
   961             self.del_relation_def(etype, 'has_text', 'String')
       
   962         super(CubicWebSchema, self).del_entity_type(etype)
       
   963 
       
   964     def schema_by_eid(self, eid):
       
   965         return self._eid_index[eid]
       
   966 
   928 
   967 
   929 # additional cw specific constraints ###########################################
   968 # additional cw specific constraints ###########################################
   930 
   969 
   931 class BaseRQLConstraint(RRQLExpression, BaseConstraint):
   970 class BaseRQLConstraint(RRQLExpression, BaseConstraint):
   932     """base class for rql constraints"""
   971     """base class for rql constraints"""
   933     distinct_query = None
   972     distinct_query = None
   934 
   973 
   935     def serialize(self):
   974     def serialize(self):
   936         # start with a comma for bw compat,see below
   975         # start with a semicolon for bw compat, see below
   937         return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression
   976         return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression
   938 
   977 
   939     @classmethod
   978     @classmethod
   940     def deserialize(cls, value):
   979     def deserialize(cls, value):
   941         # XXX < 3.5.10 bw compat
       
   942         if not value.startswith(';'):
       
   943             return cls(value)
       
   944         _, mainvars, expression = value.split(';', 2)
   980         _, mainvars, expression = value.split(';', 2)
   945         return cls(expression, mainvars)
   981         return cls(expression, mainvars)
   946 
   982 
   947     def check(self, entity, rtype, value):
   983     def check(self, entity, rtype, value):
   948         """return true if the value satisfy the constraint, else false"""
   984         """return true if the value satisfy the constraint, else false"""
   993         # start with a semicolon for bw compat, see below
  1029         # start with a semicolon for bw compat, see below
   994         return ';%s;%s\n%s' % (','.join(sorted(self.mainvars)), self.expression,
  1030         return ';%s;%s\n%s' % (','.join(sorted(self.mainvars)), self.expression,
   995                                self.msg or '')
  1031                                self.msg or '')
   996 
  1032 
   997     def deserialize(cls, value):
  1033     def deserialize(cls, value):
   998         # XXX < 3.5.10 bw compat
       
   999         if not value.startswith(';'):
       
  1000             return cls(value)
       
  1001         value, msg = value.split('\n', 1)
  1034         value, msg = value.split('\n', 1)
  1002         _, mainvars, expression = value.split(';', 2)
  1035         _, mainvars, expression = value.split(';', 2)
  1003         return cls(expression, mainvars, msg)
  1036         return cls(expression, mainvars, msg)
  1004     deserialize = classmethod(deserialize)
  1037     deserialize = classmethod(deserialize)
  1005 
  1038 
  1139 
  1172 
  1140     def _load_definition_files(self, cubes=None):
  1173     def _load_definition_files(self, cubes=None):
  1141         # bootstraping, ignore cubes
  1174         # bootstraping, ignore cubes
  1142         filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
  1175         filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
  1143         self.info('loading %s', filepath)
  1176         self.info('loading %s', filepath)
  1144         self.handle_file(filepath)
  1177         with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here
       
  1178             self.handle_file(filepath)
  1145 
  1179 
  1146     def unhandled_file(self, filepath):
  1180     def unhandled_file(self, filepath):
  1147         """called when a file without handler associated has been found"""
  1181         """called when a file without handler associated has been found"""
  1148         self.warning('ignoring file %r', filepath)
  1182         self.warning('ignoring file %r', filepath)
  1149 
  1183 
  1179         for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'),
  1213         for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'),
  1180                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'),
  1214                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'),
  1181                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'),
  1215                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'),
  1182                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')):
  1216                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')):
  1183             self.info('loading %s', filepath)
  1217             self.info('loading %s', filepath)
  1184             self.handle_file(filepath)
  1218             with tempattr(ybo, 'PACKAGE', 'cubicweb'):
       
  1219                 self.handle_file(filepath)
  1185         for cube in cubes:
  1220         for cube in cubes:
  1186             for filepath in self.get_schema_files(cube):
  1221             for filepath in self.get_schema_files(cube):
  1187                 self.info('loading %s', filepath)
  1222                 with tempattr(ybo, 'PACKAGE', basename(cube)):
  1188                 self.handle_file(filepath)
  1223                     self.handle_file(filepath)
  1189 
  1224 
  1190     # these are overridden by set_log_methods below
  1225     # these are overridden by set_log_methods below
  1191     # only defining here to prevent pylint from complaining
  1226     # only defining here to prevent pylint from complaining
  1192     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
  1227     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
  1193 
  1228