schema.py
changeset 0 b97547f5f1fa
child 372 a8a975a88368
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """classes to define schemas for CubicWeb
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 import warnings
       
    10 import re
       
    11 from logging import getLogger
       
    12 
       
    13 from logilab.common.decorators import cached, clear_cache
       
    14 from logilab.common.compat import any
       
    15 
       
    16 from yams import BadSchemaDefinition, buildobjs as ybo
       
    17 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema
       
    18 from yams.constraints import BaseConstraint, StaticVocabularyConstraint
       
    19 from yams.reader import (CONSTRAINTS, RelationFileReader, PyFileReader,
       
    20                          SchemaLoader)
       
    21 
       
    22 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
       
    23 
       
    24 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
       
    25 
       
    26 _ = unicode
       
    27 
       
    28 BASEGROUPS = ('managers', 'users', 'guests', 'owners')
       
    29 
       
    30 LOGGER = getLogger('cubicweb.schemaloader')
       
    31 
       
    32 # schema entities created from serialized schema have an eid rproperty
       
    33 ybo.ETYPE_PROPERTIES += ('eid',)
       
    34 ybo.RTYPE_PROPERTIES += ('eid',)
       
    35 ybo.RDEF_PROPERTIES += ('eid',)
       
    36 
       
    37 def bw_normalize_etype(etype):
       
    38     if etype in ETYPE_NAME_MAP:
       
    39         from warnings import warn
       
    40         msg = '%s has been renamed to %s, please update your code' % (
       
    41             etype, ETYPE_NAME_MAP[etype])            
       
    42         warn(msg, DeprecationWarning, stacklevel=4)
       
    43         etype = ETYPE_NAME_MAP[etype]
       
    44     return etype
       
    45 
       
    46 # monkey path yams.builder.RelationDefinition to support a new wildcard type '@'
       
    47 # corresponding to system entity (ie meta but not schema)
       
    48 def _actual_types(self, schema, etype):
       
    49     # two bits of error checking & reporting :
       
    50     if type(etype) not in (str, list, tuple):
       
    51         raise RuntimeError, ('Entity types must not be instances but strings or'
       
    52                              ' list/tuples thereof. Ex. (bad, good) : '
       
    53                              'SubjectRelation(Foo), SubjectRelation("Foo"). '
       
    54                              'Hence, %r is not acceptable.' % etype)
       
    55     # real work :
       
    56     if etype == '**':
       
    57         return self._pow_etypes(schema)
       
    58     if isinstance(etype, (tuple, list)):
       
    59         return etype
       
    60     if '*' in etype or '@' in etype:
       
    61         assert len(etype) in (1, 2)
       
    62         etypes = ()
       
    63         if '*' in etype:
       
    64             etypes += tuple(self._wildcard_etypes(schema))
       
    65         if '@' in etype:
       
    66             etypes += tuple(system_etypes(schema))
       
    67         return etypes
       
    68     return (etype,)
       
    69 ybo.RelationDefinition._actual_types = _actual_types
       
    70 
       
    71 def display_name(req, key, form=''):
       
    72     """return a internationalized string for the key (schema entity or relation
       
    73     name) in a given form
       
    74     """
       
    75     assert form in ('', 'plural', 'subject', 'object')
       
    76     if form == 'subject':
       
    77         form = ''
       
    78     if form:
       
    79         key = key + '_' + form
       
    80     # ensure unicode
       
    81     # added .lower() in case no translation are available
       
    82     return unicode(req._(key)).lower()
       
    83 __builtins__['display_name'] = display_name
       
    84 
       
    85 def ERSchema_display_name(self, req, form=''):
       
    86     """return a internationalized string for the entity/relation type name in
       
    87     a given form
       
    88     """
       
    89     return display_name(req, self.type, form)
       
    90 ERSchema.display_name = ERSchema_display_name
       
    91 
       
    92 @cached
       
    93 def ERSchema_get_groups(self, action):
       
    94     """return the groups authorized to perform <action> on entities of
       
    95     this type
       
    96 
       
    97     :type action: str
       
    98     :param action: the name of a permission
       
    99 
       
   100     :rtype: tuple
       
   101     :return: names of the groups with the given permission
       
   102     """
       
   103     assert action in self.ACTIONS, action
       
   104     #assert action in self._groups, '%s %s' % (self, action)
       
   105     try:
       
   106         return frozenset(g for g in self._groups[action] if isinstance(g, basestring))
       
   107     except KeyError:
       
   108         return ()
       
   109 ERSchema.get_groups = ERSchema_get_groups
       
   110 
       
   111 def ERSchema_set_groups(self, action, groups):
       
   112     """set the groups allowed to perform <action> on entities of this type. Don't
       
   113     change rql expressions for the same action.
       
   114 
       
   115     :type action: str
       
   116     :param action: the name of a permission
       
   117 
       
   118     :type groups: list or tuple
       
   119     :param groups: names of the groups granted to do the given action
       
   120     """
       
   121     assert action in self.ACTIONS, action
       
   122     clear_cache(self, 'ERSchema_get_groups')
       
   123     self._groups[action] = tuple(groups) + self.get_rqlexprs(action)
       
   124 ERSchema.set_groups = ERSchema_set_groups
       
   125 
       
   126 @cached
       
   127 def ERSchema_get_rqlexprs(self, action):
       
   128     """return the rql expressions representing queries to check the user is allowed
       
   129     to perform <action> on entities of this type
       
   130 
       
   131     :type action: str
       
   132     :param action: the name of a permission
       
   133 
       
   134     :rtype: tuple
       
   135     :return: the rql expressions with the given permission
       
   136     """
       
   137     assert action in self.ACTIONS, action
       
   138     #assert action in self._rqlexprs, '%s %s' % (self, action)
       
   139     try:
       
   140         return tuple(g for g in self._groups[action] if not isinstance(g, basestring))
       
   141     except KeyError:
       
   142         return ()
       
   143 ERSchema.get_rqlexprs = ERSchema_get_rqlexprs
       
   144 
       
   145 def ERSchema_set_rqlexprs(self, action, rqlexprs):
       
   146     """set the rql expression allowing to perform <action> on entities of this type. Don't
       
   147     change groups for the same action.
       
   148 
       
   149     :type action: str
       
   150     :param action: the name of a permission
       
   151 
       
   152     :type rqlexprs: list or tuple
       
   153     :param rqlexprs: the rql expressions allowing the given action
       
   154     """
       
   155     assert action in self.ACTIONS, action
       
   156     clear_cache(self, 'ERSchema_get_rqlexprs')
       
   157     self._groups[action] = tuple(self.get_groups(action)) + tuple(rqlexprs)
       
   158 ERSchema.set_rqlexprs = ERSchema_set_rqlexprs
       
   159 
       
   160 def ERSchema_set_permissions(self, action, permissions):
       
   161     """set the groups and rql expressions allowing to perform <action> on
       
   162     entities of this type
       
   163 
       
   164     :type action: str
       
   165     :param action: the name of a permission
       
   166 
       
   167     :type permissions: tuple
       
   168     :param permissions: the groups and rql expressions allowing the given action
       
   169     """
       
   170     assert action in self.ACTIONS, action
       
   171     clear_cache(self, 'ERSchema_get_rqlexprs')
       
   172     clear_cache(self, 'ERSchema_get_groups')
       
   173     self._groups[action] = tuple(permissions)
       
   174 ERSchema.set_permissions = ERSchema_set_permissions
       
   175 
       
   176 def ERSchema_has_perm(self, session, action, *args, **kwargs):
       
   177     """return true if the action is granted globaly or localy"""
       
   178     try:
       
   179         self.check_perm(session, action, *args, **kwargs)
       
   180         return True
       
   181     except Unauthorized:
       
   182         return False
       
   183 ERSchema.has_perm = ERSchema_has_perm
       
   184 
       
   185 def ERSchema_has_local_role(self, action):
       
   186     """return true if the action *may* be granted localy (eg either rql
       
   187     expressions or the owners group are used in security definition)
       
   188 
       
   189     XXX this method is only there since we don't know well how to deal with
       
   190     'add' action checking. Also find a better name would be nice.
       
   191     """
       
   192     assert action in self.ACTIONS, action
       
   193     if self.get_rqlexprs(action):
       
   194         return True
       
   195     if action in ('update', 'delete'):
       
   196         return self.has_group(action, 'owners')
       
   197     return False
       
   198 ERSchema.has_local_role = ERSchema_has_local_role
       
   199 
       
   200 
       
   201 def system_etypes(schema):
       
   202     """return system entity types only: skip final, schema and application entities
       
   203     """
       
   204     for eschema in schema.entities():
       
   205         if eschema.is_final() or eschema.schema_entity() or not eschema.meta:
       
   206             continue
       
   207         yield eschema.type
       
   208 
       
   209 # Schema objects definition ###################################################
       
   210 
       
   211 class CubicWebEntitySchema(EntitySchema):
       
   212     """a entity has a type, a set of subject and or object relations
       
   213     the entity schema defines the possible relations for a given type and some
       
   214     constraints on those relations
       
   215     """
       
   216     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
       
   217         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
       
   218         if eid is None and edef is not None:
       
   219             eid = getattr(edef, 'eid', None)
       
   220         self.eid = eid
       
   221         # take care: no _groups attribute when deep-copying
       
   222         if getattr(self, '_groups', None): 
       
   223             for groups in self._groups.itervalues():
       
   224                 for group_or_rqlexpr in groups:
       
   225                     if isinstance(group_or_rqlexpr, RRQLExpression):
       
   226                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
       
   227                         raise BadSchemaDefinition(msg % self.type)
       
   228             
       
   229     def attribute_definitions(self):
       
   230         """return an iterator on attribute definitions
       
   231         
       
   232         attribute relations are a subset of subject relations where the
       
   233         object's type is a final entity
       
   234         
       
   235         an attribute definition is a 2-uple :
       
   236         * name of the relation
       
   237         * schema of the destination entity type
       
   238         """
       
   239         iter = super(CubicWebEntitySchema, self).attribute_definitions()
       
   240         for rschema, attrschema in iter:
       
   241             if rschema.type == 'has_text':
       
   242                 continue
       
   243             yield rschema, attrschema
       
   244             
       
   245     def add_subject_relation(self, rschema):
       
   246         """register the relation schema as possible subject relation"""
       
   247         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
       
   248         self._update_has_text()
       
   249 
       
   250     def del_subject_relation(self, rtype):
       
   251         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
       
   252         self._update_has_text(False)
       
   253         
       
   254     def _update_has_text(self, need_has_text=None):
       
   255         may_need_has_text, has_has_text = False, False
       
   256         for rschema in self.subject_relations():
       
   257             if rschema.is_final():
       
   258                 if rschema == 'has_text':
       
   259                     has_has_text = True
       
   260                 elif self.rproperty(rschema, 'fulltextindexed'):
       
   261                     may_need_has_text = True
       
   262             elif rschema.fulltext_container:
       
   263                 if rschema.fulltext_container == 'subject':
       
   264                     may_need_has_text = True
       
   265                 else:
       
   266                     need_has_text = False
       
   267         for rschema in self.object_relations():
       
   268             if rschema.fulltext_container:
       
   269                 if rschema.fulltext_container == 'object':
       
   270                     may_need_has_text = True
       
   271                 else:
       
   272                     need_has_text = False
       
   273                     break
       
   274         if need_has_text is None:
       
   275             need_has_text = may_need_has_text
       
   276         if need_has_text and not has_has_text:
       
   277             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String')
       
   278             self.schema.add_relation_def(rdef)
       
   279         elif not need_has_text and has_has_text:
       
   280             self.schema.del_relation_def(self.type, 'has_text', 'String')
       
   281             
       
   282     def schema_entity(self):
       
   283         """return True if this entity type is used to build the schema"""
       
   284         return self.type in self.schema.schema_entity_types()
       
   285 
       
   286     def rich_text_fields(self):
       
   287         """return an iterator on (attribute, format attribute) of rich text field
       
   288 
       
   289         (the first tuple element containing the text and the second the text format)
       
   290         """
       
   291         for rschema, _ in self.attribute_definitions():
       
   292             if rschema.type.endswith('_format'):
       
   293                 for constraint in self.constraints(rschema):
       
   294                     if isinstance(constraint, FormatConstraint):
       
   295                         yield self.subject_relation(rschema.type[:-7]), rschema
       
   296                         break
       
   297                     
       
   298     def check_perm(self, session, action, eid=None):
       
   299         # NB: session may be a server session or a request object
       
   300         user = session.user
       
   301         # check user is in an allowed group, if so that's enough
       
   302         # internal sessions should always stop there
       
   303         if user.matching_groups(self.get_groups(action)):
       
   304             return
       
   305         # if 'owners' in allowed groups, check if the user actually owns this
       
   306         # object, if so that's enough
       
   307         if eid is not None and 'owners' in self.get_groups(action) and \
       
   308                user.owns(eid):
       
   309             return
       
   310         # else if there is some rql expressions, check them
       
   311         if any(rqlexpr.check(session, eid)
       
   312                for rqlexpr in self.get_rqlexprs(action)):
       
   313             return        
       
   314         raise Unauthorized(action, str(self))
       
   315 
       
   316     def rql_expression(self, expression, mainvars=None, eid=None):
       
   317         """rql expression factory"""
       
   318         return ERQLExpression(expression, mainvars, eid)
       
   319     
       
   320 class CubicWebRelationSchema(RelationSchema):
       
   321     RelationSchema._RPROPERTIES['eid'] = None
       
   322     _perms_checked = False
       
   323     
       
   324     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
       
   325         if rdef is not None:
       
   326             # if this relation is inlined
       
   327             self.inlined = rdef.inlined
       
   328         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
       
   329         if eid is None and rdef is not None:
       
   330             eid = getattr(rdef, 'eid', None)
       
   331         self.eid = eid
       
   332                     
       
   333         
       
   334     def update(self, subjschema, objschema, rdef):
       
   335         super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
       
   336         if not self._perms_checked and self._groups:
       
   337             for action, groups in self._groups.iteritems():
       
   338                 for group_or_rqlexpr in groups:
       
   339                     if action == 'read' and \
       
   340                            isinstance(group_or_rqlexpr, RQLExpression):
       
   341                         msg = "can't use rql expression for read permission of "\
       
   342                               "a relation type (%s)"
       
   343                         raise BadSchemaDefinition(msg % self.type)
       
   344                     elif self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   345                         if self.schema.reading_from_database:
       
   346                             # we didn't have final relation earlier, so turn
       
   347                             # RRQLExpression into ERQLExpression now
       
   348                             rqlexpr = group_or_rqlexpr
       
   349                             newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr]
       
   350                             newrqlexprs.append(ERQLExpression(rqlexpr.expression,
       
   351                                                               rqlexpr.mainvars,
       
   352                                                               rqlexpr.eid))
       
   353                             self.set_rqlexprs(action, newrqlexprs) 
       
   354                         else:
       
   355                             msg = "can't use RRQLExpression on a final relation "\
       
   356                                   "type (eg attribute relation), use an ERQLExpression (%s)"
       
   357                             raise BadSchemaDefinition(msg % self.type)
       
   358                     elif not self.final and \
       
   359                              isinstance(group_or_rqlexpr, ERQLExpression):
       
   360                         msg = "can't use ERQLExpression on a relation type, use "\
       
   361                               "a RRQLExpression (%s)"
       
   362                         raise BadSchemaDefinition(msg % self.type)
       
   363             self._perms_checked = True
       
   364             
       
   365     def cardinality(self, subjtype, objtype, target):
       
   366         card = self.rproperty(subjtype, objtype, 'cardinality')
       
   367         return (target == 'subject' and card[0]) or \
       
   368                (target == 'object' and card[1])
       
   369     
       
   370     def schema_relation(self):
       
   371         return self.type in ('relation_type', 'from_entity', 'to_entity',
       
   372                              'constrained_by', 'cstrtype')
       
   373     
       
   374     def physical_mode(self):
       
   375         """return an appropriate mode for physical storage of this relation type:
       
   376         * 'subjectinline' if every possible subject cardinalities are 1 or ?
       
   377         * 'objectinline' if 'subjectinline' mode is not possible but every
       
   378           possible object cardinalities are 1 or ?
       
   379         * None if neither 'subjectinline' and 'objectinline'
       
   380         """
       
   381         assert not self.final
       
   382         return self.inlined and 'subjectinline' or None
       
   383 
       
   384     def check_perm(self, session, action, *args, **kwargs):
       
   385         # NB: session may be a server session or a request object check user is
       
   386         # in an allowed group, if so that's enough internal sessions should
       
   387         # always stop there
       
   388         if session.user.matching_groups(self.get_groups(action)):
       
   389             return 
       
   390         # else if there is some rql expressions, check them
       
   391         if any(rqlexpr.check(session, *args, **kwargs)
       
   392                for rqlexpr in self.get_rqlexprs(action)):
       
   393             return
       
   394         raise Unauthorized(action, str(self))
       
   395 
       
   396     def rql_expression(self, expression, mainvars=None, eid=None):
       
   397         """rql expression factory"""
       
   398         if self.is_final():
       
   399             return ERQLExpression(expression, mainvars, eid)
       
   400         return RRQLExpression(expression, mainvars, eid)
       
   401 
       
   402     
       
   403 class CubicWebSchema(Schema):
       
   404     """set of entities and relations schema defining the possible data sets
       
   405     used in an application
       
   406 
       
   407 
       
   408     :type name: str
       
   409     :ivar name: name of the schema, usually the application identifier
       
   410     
       
   411     :type base: str
       
   412     :ivar base: path of the directory where the schema is defined
       
   413     """
       
   414     reading_from_database = False    
       
   415     entity_class = CubicWebEntitySchema
       
   416     relation_class = CubicWebRelationSchema
       
   417 
       
   418     def __init__(self, *args, **kwargs):
       
   419         self._eid_index = {}
       
   420         super(CubicWebSchema, self).__init__(*args, **kwargs)
       
   421         ybo.register_base_types(self)
       
   422         rschema = self.add_relation_type(ybo.RelationType('eid', meta=True))
       
   423         rschema.final = True
       
   424         rschema.set_default_groups()
       
   425         rschema = self.add_relation_type(ybo.RelationType('has_text', meta=True))
       
   426         rschema.final = True
       
   427         rschema.set_default_groups()
       
   428         rschema = self.add_relation_type(ybo.RelationType('identity', meta=True))
       
   429         rschema.final = False
       
   430         rschema.set_default_groups()
       
   431         
       
   432     def schema_entity_types(self):
       
   433         """return the list of entity types used to build the schema"""
       
   434         return frozenset(('EEType', 'ERType', 'EFRDef', 'ENFRDef',
       
   435                           'EConstraint', 'EConstraintType', 'RQLExpression',
       
   436                           # XXX those are not really "schema" entity types
       
   437                           #     but we usually don't want them as @* targets
       
   438                           'EProperty', 'EPermission', 'State', 'Transition'))
       
   439         
       
   440     def add_entity_type(self, edef):
       
   441         edef.name = edef.name.encode()
       
   442         edef.name = bw_normalize_etype(edef.name)
       
   443         assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
       
   444         eschema = super(CubicWebSchema, self).add_entity_type(edef)
       
   445         if not eschema.is_final():
       
   446             # automatically add the eid relation to non final entity types
       
   447             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
       
   448                                           cardinality='11', uid=True)
       
   449             self.add_relation_def(rdef)
       
   450             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type)
       
   451             self.add_relation_def(rdef)
       
   452         self._eid_index[eschema.eid] = eschema
       
   453         return eschema
       
   454         
       
   455     def add_relation_type(self, rdef):
       
   456         rdef.name = rdef.name.lower().encode()
       
   457         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
       
   458         self._eid_index[rschema.eid] = rschema
       
   459         return rschema
       
   460     
       
   461     def add_relation_def(self, rdef):
       
   462         """build a part of a relation schema
       
   463         (i.e. add a relation between two specific entity's types)
       
   464 
       
   465         :type subject: str
       
   466         :param subject: entity's type that is subject of the relation
       
   467 
       
   468         :type rtype: str
       
   469         :param rtype: the relation's type (i.e. the name of the relation)
       
   470 
       
   471         :type obj: str
       
   472         :param obj: entity's type that is object of the relation
       
   473 
       
   474         :rtype: RelationSchema
       
   475         :param: the newly created or just completed relation schema
       
   476         """
       
   477         rdef.name = rdef.name.lower()
       
   478         rdef.subject = bw_normalize_etype(rdef.subject)
       
   479         rdef.object = bw_normalize_etype(rdef.object)
       
   480         super(CubicWebSchema, self).add_relation_def(rdef)
       
   481         try:
       
   482             self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
       
   483                                          self.rschema(rdef.name),
       
   484                                          self.eschema(rdef.object))
       
   485         except AttributeError:
       
   486             pass # not a serialized schema
       
   487     
       
   488     def del_relation_type(self, rtype):
       
   489         rschema = self.rschema(rtype)
       
   490         self._eid_index.pop(rschema.eid, None)
       
   491         super(CubicWebSchema, self).del_relation_type(rtype)
       
   492     
       
   493     def del_relation_def(self, subjtype, rtype, objtype):
       
   494         for k, v in self._eid_index.items():
       
   495             if v == (subjtype, rtype, objtype):
       
   496                 del self._eid_index[k]
       
   497         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
       
   498         
       
   499     def del_entity_type(self, etype):
       
   500         eschema = self.eschema(etype)
       
   501         self._eid_index.pop(eschema.eid, None)
       
   502         # deal with has_text first, else its automatic deletion (see above)
       
   503         # may trigger an error in ancestor's del_entity_type method
       
   504         if 'has_text' in eschema.subject_relations():
       
   505             self.del_relation_def(etype, 'has_text', 'String')
       
   506         super(CubicWebSchema, self).del_entity_type(etype)
       
   507         
       
   508     def schema_by_eid(self, eid):
       
   509         return self._eid_index[eid]
       
   510 
       
   511 
       
   512 # Possible constraints ########################################################
       
   513 
       
   514 class RQLVocabularyConstraint(BaseConstraint):
       
   515     """the rql vocabulary constraint :
       
   516 
       
   517     limit the proposed values to a set of entities returned by a rql query,
       
   518     but this is not enforced at the repository level
       
   519     
       
   520      restriction is additional rql restriction that will be added to
       
   521      a predefined query, where the S and O variables respectivly represent
       
   522      the subject and the object of the relation
       
   523     """
       
   524     
       
   525     def __init__(self, restriction):
       
   526         self.restriction = restriction
       
   527 
       
   528     def serialize(self):
       
   529         return self.restriction
       
   530     
       
   531     def deserialize(cls, value):
       
   532         return cls(value)
       
   533     deserialize = classmethod(deserialize)
       
   534     
       
   535     def check(self, entity, rtype, value):
       
   536         """return true if the value satisfy the constraint, else false"""
       
   537         # implemented as a hook in the repository
       
   538         return 1
       
   539 
       
   540     def repo_check(self, session, eidfrom, rtype, eidto):
       
   541         """raise ValidationError if the relation doesn't satisfy the constraint
       
   542         """
       
   543         pass # this is a vocabulary constraint, not enforce
       
   544     
       
   545     def __str__(self):
       
   546         return self.restriction
       
   547 
       
   548     def __repr__(self):
       
   549         return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction))
       
   550 
       
   551 
       
   552 class RQLConstraint(RQLVocabularyConstraint):
       
   553     """the rql constraint is similar to the RQLVocabularyConstraint but
       
   554     are also enforced at the repository level
       
   555     """
       
   556     def exec_query(self, session, eidfrom, eidto):
       
   557         rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction
       
   558         return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto},
       
   559                                       ('s', 'o'), build_descr=False)
       
   560     def error(self, eid, rtype, msg):
       
   561         raise ValidationError(eid, {rtype: msg})
       
   562         
       
   563     def repo_check(self, session, eidfrom, rtype, eidto):
       
   564         """raise ValidationError if the relation doesn't satisfy the constraint
       
   565         """
       
   566         if not self.exec_query(session, eidfrom, eidto):
       
   567             # XXX at this point dunno if the validation error `occured` on
       
   568             #     eidfrom or eidto (from user interface point of view)
       
   569             self.error(eidfrom, rtype, 'constraint %s failed' % self)
       
   570 
       
   571 
       
   572 class RQLUniqueConstraint(RQLConstraint):
       
   573     """the unique rql constraint check that the result of the query isn't
       
   574     greater than one
       
   575     """
       
   576     def repo_check(self, session, eidfrom, rtype, eidto):
       
   577         """raise ValidationError if the relation doesn't satisfy the constraint
       
   578         """
       
   579         if len(self.exec_query(session, eidfrom, eidto)) > 1:
       
   580             # XXX at this point dunno if the validation error `occured` on
       
   581             #     eidfrom or eidto (from user interface point of view)
       
   582             self.error(eidfrom, rtype, 'unique constraint %s failed' % self)
       
   583 
       
   584     
       
   585 def split_expression(rqlstring):
       
   586     for expr in rqlstring.split(','):
       
   587         for word in expr.split():
       
   588             yield word
       
   589             
       
   590 def normalize_expression(rqlstring):
       
   591     """normalize an rql expression to ease schema synchronization (avoid
       
   592     suppressing and reinserting an expression if only a space has been added/removed
       
   593     for instance)
       
   594     """
       
   595     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
       
   596 
       
   597 
       
   598 class RQLExpression(object):
       
   599     def __init__(self, expression, mainvars, eid):
       
   600         self.eid = eid # eid of the entity representing this rql expression
       
   601         if not isinstance(mainvars, unicode):
       
   602             mainvars = unicode(mainvars)
       
   603         self.mainvars = mainvars
       
   604         self.expression = normalize_expression(expression)
       
   605         try:
       
   606             self.rqlst = parse(self.full_rql, print_errors=False).children[0]
       
   607         except RQLSyntaxError:
       
   608             raise RQLSyntaxError(expression)
       
   609         for mainvar in mainvars.split(','):
       
   610             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
       
   611                 LOGGER.warn('You did not use the %s variable in your RQL expression %s',
       
   612                             mainvar, self)
       
   613     
       
   614     def __str__(self):
       
   615         return self.full_rql
       
   616     def __repr__(self):
       
   617         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
       
   618         
       
   619     def __deepcopy__(self, memo):
       
   620         return self.__class__(self.expression, self.mainvars)
       
   621     def __getstate__(self):
       
   622         return (self.expression, self.mainvars)
       
   623     def __setstate__(self, state):
       
   624         self.__init__(*state)
       
   625         
       
   626     @cached
       
   627     def transform_has_permission(self):
       
   628         found = None
       
   629         rqlst = self.rqlst
       
   630         for var in rqlst.defined_vars.itervalues():
       
   631             for varref in var.references():
       
   632                 rel = varref.relation()
       
   633                 if rel is None:
       
   634                     continue
       
   635                 try:
       
   636                     prefix, action, suffix = rel.r_type.split('_')
       
   637                 except ValueError:
       
   638                     continue
       
   639                 if prefix != 'has' or suffix != 'permission' or \
       
   640                        not action in ('add', 'delete', 'update', 'read'):
       
   641                     continue
       
   642                 if found is None:
       
   643                     found = []
       
   644                     rqlst.save_state()
       
   645                 assert rel.children[0].name == 'U'
       
   646                 objvar = rel.children[1].children[0].variable
       
   647                 rqlst.remove_node(rel)
       
   648                 selected = [v.name for v in rqlst.get_selected_variables()]
       
   649                 if objvar.name not in selected:
       
   650                     colindex = len(selected)
       
   651                     rqlst.add_selected(objvar)
       
   652                 else:
       
   653                     colindex = selected.index(objvar.name)
       
   654                 found.append((action, objvar, colindex))
       
   655                 # remove U eid %(u)s if U is not used in any other relation
       
   656                 uvrefs = rqlst.defined_vars['U'].references()
       
   657                 if len(uvrefs) == 1:
       
   658                     rqlst.remove_node(uvrefs[0].relation())
       
   659         if found is not None:
       
   660             rql = rqlst.as_string()
       
   661             if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation):
       
   662                 # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql
       
   663                 keyarg = rqlst.selection[0].name.lower()
       
   664             else:
       
   665                 keyarg = None
       
   666             rqlst.recover()
       
   667             return rql, found, keyarg
       
   668         return rqlst.as_string(), None, None
       
   669         
       
   670     def _check(self, session, **kwargs):
       
   671         """return True if the rql expression is matching the given relation
       
   672         between fromeid and toeid
       
   673 
       
   674         session may actually be a request as well
       
   675         """
       
   676         if self.eid is not None:
       
   677             key = (self.eid, tuple(sorted(kwargs.iteritems())))
       
   678             try:
       
   679                 return session.local_perm_cache[key]
       
   680             except KeyError:
       
   681                 pass
       
   682         rql, has_perm_defs, keyarg = self.transform_has_permission()
       
   683         if keyarg is None:
       
   684             # on the server side, use unsafe_execute, but this is not available
       
   685             # on the client side (session is actually a request)
       
   686             execute = getattr(session, 'unsafe_execute', session.execute)
       
   687             # XXX what if 'u' in kwargs
       
   688             cachekey = kwargs.keys()
       
   689             kwargs['u'] = session.user.eid
       
   690             try:
       
   691                 rset = execute(rql, kwargs, cachekey, build_descr=True)
       
   692             except NotImplementedError:
       
   693                 self.critical('cant check rql expression, unsupported rql %s', rql)
       
   694                 if self.eid is not None:
       
   695                     session.local_perm_cache[key] = False
       
   696                 return False
       
   697             except TypeResolverException, ex:
       
   698                 # some expression may not be resolvable with current kwargs
       
   699                 # (type conflict)
       
   700                 self.warning('%s: %s', rql, str(ex))
       
   701                 if self.eid is not None:
       
   702                     session.local_perm_cache[key] = False
       
   703                 return False
       
   704         else:
       
   705             rset = session.eid_rset(kwargs[keyarg])
       
   706         # if no special has_*_permission relation in the rql expression, just
       
   707         # check the result set contains something
       
   708         if has_perm_defs is None:
       
   709             if rset:
       
   710                 if self.eid is not None:
       
   711                     session.local_perm_cache[key] = True
       
   712                 return True
       
   713         elif rset:
       
   714             # check every special has_*_permission relation is satisfied
       
   715             get_eschema = session.vreg.schema.eschema
       
   716             try:
       
   717                 for eaction, var, col in has_perm_defs:
       
   718                     for i in xrange(len(rset)):
       
   719                         eschema = get_eschema(rset.description[i][col])
       
   720                         eschema.check_perm(session, eaction, rset[i][col])
       
   721                 if self.eid is not None:
       
   722                     session.local_perm_cache[key] = True
       
   723                 return True
       
   724             except Unauthorized:
       
   725                 pass
       
   726         if self.eid is not None:
       
   727             session.local_perm_cache[key] = False
       
   728         return False
       
   729     
       
   730     @property
       
   731     def minimal_rql(self):
       
   732         return 'Any %s WHERE %s' % (self.mainvars, self.expression)
       
   733 
       
   734 
       
   735 class ERQLExpression(RQLExpression):
       
   736     def __init__(self, expression, mainvars=None, eid=None):
       
   737         RQLExpression.__init__(self, expression, mainvars or 'X', eid)
       
   738         # syntax tree used by read security (inserted in queries when necessary
       
   739         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
       
   740 
       
   741     @property
       
   742     def full_rql(self):
       
   743         rql = self.minimal_rql
       
   744         rqlst = getattr(self, 'rqlst', None) # may be not set yet
       
   745         if rqlst is not None:
       
   746             defined = rqlst.defined_vars
       
   747         else:
       
   748             defined = set(split_expression(self.expression))
       
   749         if 'X' in defined:
       
   750             rql += ', X eid %(x)s'
       
   751         if 'U' in defined:
       
   752             rql += ', U eid %(u)s'
       
   753         return rql
       
   754     
       
   755     def check(self, session, eid=None):
       
   756         if 'X' in self.rqlst.defined_vars:
       
   757             if eid is None:
       
   758                 return False
       
   759             return self._check(session, x=eid)
       
   760         return self._check(session)
       
   761     
       
   762 PyFileReader.context['ERQLExpression'] = ERQLExpression
       
   763         
       
   764 class RRQLExpression(RQLExpression):
       
   765     def __init__(self, expression, mainvars=None, eid=None):
       
   766         if mainvars is None:
       
   767             defined = set(split_expression(expression))
       
   768             mainvars = []
       
   769             if 'S' in defined:
       
   770                 mainvars.append('S')
       
   771             if 'O' in defined:
       
   772                 mainvars.append('O')
       
   773             if not mainvars:
       
   774                 raise Exception('unable to guess selection variables')
       
   775             mainvars = ','.join(mainvars)
       
   776         RQLExpression.__init__(self, expression, mainvars, eid)
       
   777 
       
   778     @property
       
   779     def full_rql(self):
       
   780         rql = self.minimal_rql
       
   781         rqlst = getattr(self, 'rqlst', None) # may be not set yet
       
   782         if rqlst is not None:
       
   783             defined = rqlst.defined_vars
       
   784         else:
       
   785             defined = set(split_expression(self.expression))
       
   786         if 'S' in defined:
       
   787             rql += ', S eid %(s)s'
       
   788         if 'O' in defined:
       
   789             rql += ', O eid %(o)s'
       
   790         if 'U' in defined:
       
   791             rql += ', U eid %(u)s'
       
   792         return rql
       
   793     
       
   794     def check(self, session, fromeid=None, toeid=None):
       
   795         kwargs = {}
       
   796         if 'S' in self.rqlst.defined_vars:
       
   797             if fromeid is None:
       
   798                 return False
       
   799             kwargs['s'] = fromeid
       
   800         if 'O' in self.rqlst.defined_vars:
       
   801             if toeid is None:
       
   802                 return False
       
   803             kwargs['o'] = toeid
       
   804         return self._check(session, **kwargs)
       
   805         
       
   806 PyFileReader.context['RRQLExpression'] = RRQLExpression
       
   807 
       
   808         
       
   809 # schema loading ##############################################################
       
   810 
       
   811 class CubicWebRelationFileReader(RelationFileReader):
       
   812     """cubicweb specific relation file reader, handling additional RQL
       
   813     constraints on a relation definition
       
   814     """
       
   815     
       
   816     def handle_constraint(self, rdef, constraint_text):
       
   817         """arbitrary constraint is an rql expression for cubicweb"""
       
   818         if not rdef.constraints:
       
   819             rdef.constraints = []
       
   820         rdef.constraints.append(RQLVocabularyConstraint(constraint_text))
       
   821 
       
   822     def process_properties(self, rdef, relation_def):
       
   823         if 'inline' in relation_def:
       
   824             rdef.inlined = True
       
   825         RelationFileReader.process_properties(self, rdef, relation_def)
       
   826 
       
   827         
       
   828 CONSTRAINTS['RQLConstraint'] = RQLConstraint
       
   829 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
       
   830 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
       
   831 PyFileReader.context.update(CONSTRAINTS)
       
   832 
       
   833 
       
   834 class BootstrapSchemaLoader(SchemaLoader):
       
   835     """cubicweb specific schema loader, loading only schema necessary to read
       
   836     the persistent schema
       
   837     """
       
   838     schemacls = CubicWebSchema
       
   839     SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader,
       
   840                                        })
       
   841 
       
   842     def load(self, config, path=()):
       
   843         """return a Schema instance from the schema definition read
       
   844         from <directory>
       
   845         """
       
   846         self.lib_directory = config.schemas_lib_dir()
       
   847         return super(BootstrapSchemaLoader, self).load(
       
   848             path, config.appid, register_base_types=False)
       
   849     
       
   850     def _load_definition_files(self, cubes=None):
       
   851         # bootstraping, ignore cubes
       
   852         for filepath in self.include_schema_files('bootstrap'):
       
   853             self.info('loading %s', filepath)
       
   854             self.handle_file(filepath)
       
   855         
       
   856     def unhandled_file(self, filepath):
       
   857         """called when a file without handler associated has been found"""
       
   858         self.warning('ignoring file %r', filepath)
       
   859 
       
   860 
       
   861 class CubicWebSchemaLoader(BootstrapSchemaLoader):
       
   862     """cubicweb specific schema loader, automatically adding metadata to the
       
   863     application's schema
       
   864     """
       
   865 
       
   866     def load(self, config):
       
   867         """return a Schema instance from the schema definition read
       
   868         from <directory>
       
   869         """
       
   870         self.info('loading %s schemas', ', '.join(config.cubes()))
       
   871         path = reversed([config.apphome] + config.cubes_path())
       
   872         return super(CubicWebSchemaLoader, self).load(config, path=path)
       
   873 
       
   874     def _load_definition_files(self, cubes):
       
   875         for filepath in (self.include_schema_files('bootstrap')
       
   876                          + self.include_schema_files('base')
       
   877                          + self.include_schema_files('Bookmark')
       
   878                          + self.include_schema_files('Card')):
       
   879             self.info('loading %s', filepath)
       
   880             self.handle_file(filepath)
       
   881         for cube in cubes:
       
   882             for filepath in self.get_schema_files(cube):
       
   883                 self.info('loading %s', filepath)
       
   884                 self.handle_file(filepath)
       
   885 
       
   886 
       
   887 # _() is just there to add messages to the catalog, don't care about actual
       
   888 # translation
       
   889 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
       
   890 
       
   891 class FormatConstraint(StaticVocabularyConstraint):
       
   892     need_perm_formats = (_('text/cubicweb-page-template'),
       
   893                          )
       
   894     regular_formats = (_('text/rest'),
       
   895                        _('text/html'),
       
   896                        _('text/plain'),
       
   897                        )
       
   898     def __init__(self):
       
   899         pass
       
   900     def serialize(self):
       
   901         """called to make persistent valuable data of a constraint"""
       
   902         return None
       
   903 
       
   904     @classmethod
       
   905     def deserialize(cls, value):
       
   906         """called to restore serialized data of a constraint. Should return
       
   907         a `cls` instance
       
   908         """
       
   909         return cls()
       
   910     
       
   911     def vocabulary(self, entity=None):
       
   912         if entity and entity.req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
       
   913             return self.regular_formats + self.need_perm_formats
       
   914         return self.regular_formats
       
   915     
       
   916     def __str__(self):
       
   917         return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary())
       
   918     
       
   919     
       
   920 format_constraint = FormatConstraint()
       
   921 CONSTRAINTS['FormatConstraint'] = FormatConstraint
       
   922 PyFileReader.context['format_constraint'] = format_constraint
       
   923 
       
   924 from logging import getLogger
       
   925 from cubicweb import set_log_methods
       
   926 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
       
   927 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
       
   928 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
       
   929 
       
   930 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
       
   931 # necessary
       
   932 orig_import_erschema = PyFileReader.import_erschema
       
   933 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
       
   934     return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
       
   935 PyFileReader.import_erschema = bw_import_erschema
       
   936     
       
   937 # XXX itou for some Statement methods
       
   938 from rql import stmts
       
   939 orig_get_etype = stmts.ScopeNode.get_etype
       
   940 def bw_get_etype(self, name):
       
   941     return orig_get_etype(self, bw_normalize_etype(name))
       
   942 stmts.ScopeNode.get_etype = bw_get_etype
       
   943 
       
   944 orig_add_main_variable_delete = stmts.Delete.add_main_variable
       
   945 def bw_add_main_variable_delete(self, etype, vref):
       
   946     return orig_add_main_variable_delete(self, bw_normalize_etype(etype), vref)
       
   947 stmts.Delete.add_main_variable = bw_add_main_variable_delete
       
   948 
       
   949 orig_add_main_variable_insert = stmts.Insert.add_main_variable
       
   950 def bw_add_main_variable_insert(self, etype, vref):
       
   951     return orig_add_main_variable_insert(self, bw_normalize_etype(etype), vref)
       
   952 stmts.Insert.add_main_variable = bw_add_main_variable_insert
       
   953 
       
   954 orig_set_statement_type = stmts.Select.set_statement_type
       
   955 def bw_set_statement_type(self, etype):
       
   956     return orig_set_statement_type(self, bw_normalize_etype(etype))
       
   957 stmts.Select.set_statement_type = bw_set_statement_type