schema.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """classes to define schemas for CubicWeb"""
       
    19 from __future__ import print_function
       
    20 
       
    21 __docformat__ = "restructuredtext en"
       
    22 
       
    23 import re
       
    24 from os.path import join, basename
       
    25 from logging import getLogger
       
    26 from warnings import warn
       
    27 
       
    28 from six import PY2, text_type, string_types, add_metaclass
       
    29 from six.moves import range
       
    30 
       
    31 from logilab.common import tempattr
       
    32 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
       
    33 from logilab.common.logging_ext import set_log_methods
       
    34 from logilab.common.deprecation import deprecated, class_moved, moved
       
    35 from logilab.common.textutils import splitstrip
       
    36 from logilab.common.graph import get_cycles
       
    37 
       
    38 import yams
       
    39 from yams import BadSchemaDefinition, buildobjs as ybo
       
    40 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema, \
       
    41      RelationDefinitionSchema, PermissionMixIn, role_name
       
    42 from yams.constraints import (BaseConstraint, FormatConstraint, BoundaryConstraint,
       
    43                               IntervalBoundConstraint, StaticVocabularyConstraint)
       
    44 from yams.reader import (CONSTRAINTS, PyFileReader, SchemaLoader,
       
    45                          cleanup_sys_modules, fill_schema_from_namespace)
       
    46 
       
    47 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
       
    48 from rql.analyze import ETypeResolver
       
    49 
       
    50 import cubicweb
       
    51 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized, _
       
    52 
       
    53 try:
       
    54     from cubicweb import server
       
    55 except ImportError:
       
    56     # We need to lookup DEBUG from there,
       
    57     # however a pure dbapi client may not have it.
       
    58     class server(object): pass
       
    59     server.DEBUG = False
       
    60 
       
    61 
       
    62 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
       
    63 VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',))
       
    64 
       
    65 # set of meta-relations available for every entity types
       
    66 META_RTYPES = set((
       
    67     'owned_by', 'created_by', 'is', 'is_instance_of', 'identity',
       
    68     'eid', 'creation_date', 'cw_source', 'modification_date', 'has_text', 'cwuri',
       
    69     ))
       
    70 WORKFLOW_RTYPES = set(('custom_workflow', 'in_state', 'wf_info_for'))
       
    71 WORKFLOW_DEF_RTYPES = set(('workflow_of', 'state_of', 'transition_of',
       
    72                            'initial_state', 'default_workflow',
       
    73                            'allowed_transition', 'destination_state',
       
    74                            'from_state', 'to_state', 'condition',
       
    75                            'subworkflow', 'subworkflow_state', 'subworkflow_exit',
       
    76                            'by_transition',
       
    77                            ))
       
    78 SYSTEM_RTYPES = set(('in_group', 'require_group',
       
    79                      # cwproperty
       
    80                      'for_user',
       
    81                      'cw_schema', 'cw_import_of', 'cw_for_source',
       
    82                      'cw_host_config_of',
       
    83                      )) | WORKFLOW_RTYPES
       
    84 NO_I18NCONTEXT = META_RTYPES | WORKFLOW_RTYPES
       
    85 
       
    86 SKIP_COMPOSITE_RELS = [('cw_source', 'subject')]
       
    87 
       
    88 # set of entity and relation types used to build the schema
       
    89 SCHEMA_TYPES = set((
       
    90     'CWEType', 'CWRType', 'CWComputedRType', 'CWAttribute', 'CWRelation',
       
    91     'CWConstraint', 'CWConstraintType', 'CWUniqueTogetherConstraint',
       
    92     'RQLExpression',
       
    93     'specializes',
       
    94     'relation_type', 'from_entity', 'to_entity',
       
    95     'constrained_by', 'cstrtype',
       
    96     'constraint_of', 'relations',
       
    97     'read_permission', 'add_permission',
       
    98     'delete_permission', 'update_permission',
       
    99     ))
       
   100 
       
   101 WORKFLOW_TYPES = set(('Transition', 'State', 'TrInfo', 'Workflow',
       
   102                       'WorkflowTransition', 'BaseTransition',
       
   103                       'SubWorkflowExitPoint'))
       
   104 
       
   105 INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport',
       
   106                       'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig'))
       
   107 
       
   108 UNIQUE_CONSTRAINTS = ('SizeConstraint', 'FormatConstraint',
       
   109                       'StaticVocabularyConstraint',
       
   110                       'RQLVocabularyConstraint')
       
   111 
       
   112 _LOGGER = getLogger('cubicweb.schemaloader')
       
   113 
       
   114 # entity and relation schema created from serialized schema have an eid
       
   115 ybo.ETYPE_PROPERTIES += ('eid',)
       
   116 ybo.RTYPE_PROPERTIES += ('eid',)
       
   117 
       
   118 def build_schema_from_namespace(items):
       
   119     schema = CubicWebSchema('noname')
       
   120     fill_schema_from_namespace(schema, items, register_base_types=False)
       
   121     return schema
       
   122 
       
   123 # Bases for manipulating RQL in schema #########################################
       
   124 
       
   125 def guess_rrqlexpr_mainvars(expression):
       
   126     defined = set(split_expression(expression))
       
   127     mainvars = set()
       
   128     if 'S' in defined:
       
   129         mainvars.add('S')
       
   130     if 'O' in defined:
       
   131         mainvars.add('O')
       
   132     if 'U' in defined:
       
   133         mainvars.add('U')
       
   134     if not mainvars:
       
   135         raise BadSchemaDefinition('unable to guess selection variables in %r'
       
   136                                   % expression)
       
   137     return mainvars
       
   138 
       
   139 def split_expression(rqlstring):
       
   140     for expr in rqlstring.split(','):
       
   141         for noparen1 in expr.split('('):
       
   142             for noparen2 in noparen1.split(')'):
       
   143                 for word in noparen2.split():
       
   144                     yield word
       
   145 
       
   146 def normalize_expression(rqlstring):
       
   147     """normalize an rql expression to ease schema synchronization (avoid
       
   148     suppressing and reinserting an expression if only a space has been
       
   149     added/removed for instance)
       
   150     """
       
   151     union = parse(u'Any 1 WHERE %s' % rqlstring).as_string()
       
   152     if PY2 and isinstance(union, str):
       
   153         union = union.decode('utf-8')
       
   154     return union.split(' WHERE ', 1)[1]
       
   155 
       
   156 
       
   157 def _check_valid_formula(rdef, formula_rqlst):
       
   158     """Check the formula is a valid RQL query with some restriction (no union,
       
   159     single selected node, etc.), raise BadSchemaDefinition if not
       
   160     """
       
   161     if len(formula_rqlst.children) != 1:
       
   162         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
       
   163                                   'can not use UNION in formula %(form)r' %
       
   164                                   {'attr' : rdef.rtype,
       
   165                                    'etype' : rdef.subject.type,
       
   166                                    'form' : rdef.formula})
       
   167     select = formula_rqlst.children[0]
       
   168     if len(select.selection) != 1:
       
   169         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
       
   170                                   'can only select one term in formula %(form)r' %
       
   171                                   {'attr' : rdef.rtype,
       
   172                                    'etype' : rdef.subject.type,
       
   173                                    'form' : rdef.formula})
       
   174     term = select.selection[0]
       
   175     types = set(term.get_type(sol) for sol in select.solutions)
       
   176     if len(types) != 1:
       
   177         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
       
   178                                   'multiple possible types (%(types)s) for formula %(form)r' %
       
   179                                   {'attr' : rdef.rtype,
       
   180                                    'etype' : rdef.subject.type,
       
   181                                    'types' : list(types),
       
   182                                    'form' : rdef.formula})
       
   183     computed_type = types.pop()
       
   184     expected_type = rdef.object.type
       
   185     if computed_type != expected_type:
       
   186         raise BadSchemaDefinition('computed attribute %(attr)s on %(etype)s: '
       
   187                                   'computed attribute type (%(comp_type)s) mismatch with '
       
   188                                   'specified type (%(attr_type)s)' %
       
   189                                   {'attr' : rdef.rtype,
       
   190                                    'etype' : rdef.subject.type,
       
   191                                    'comp_type' : computed_type,
       
   192                                    'attr_type' : expected_type})
       
   193 
       
   194 
       
   195 class RQLExpression(object):
       
   196     """Base class for RQL expression used in schema (constraints and
       
   197     permissions)
       
   198     """
       
   199     # these are overridden by set_log_methods below
       
   200     # only defining here to prevent pylint from complaining
       
   201     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
   202     # to be defined in concrete classes
       
   203     rqlst = None
       
   204     predefined_variables = None
       
   205     full_rql = None
       
   206 
       
   207     def __init__(self, expression, mainvars, eid):
       
   208         """
       
   209         :type mainvars: sequence of RQL variables' names. Can be provided as a
       
   210                         comma separated string.
       
   211         :param mainvars: names of the variables being selected.
       
   212 
       
   213         """
       
   214         self.eid = eid # eid of the entity representing this rql expression
       
   215         assert mainvars, 'bad mainvars %s' % mainvars
       
   216         if isinstance(mainvars, string_types):
       
   217             mainvars = set(splitstrip(mainvars))
       
   218         elif not isinstance(mainvars, set):
       
   219             mainvars = set(mainvars)
       
   220         self.mainvars = mainvars
       
   221         self.expression = normalize_expression(expression)
       
   222         try:
       
   223             self.full_rql = self.rqlst.as_string()
       
   224         except RQLSyntaxError:
       
   225             raise RQLSyntaxError(expression)
       
   226         for mainvar in mainvars:
       
   227             # if variable is predefined, an extra reference is inserted
       
   228             # automatically (`VAR eid %(v)s`)
       
   229             if mainvar in self.predefined_variables:
       
   230                 min_refs = 3
       
   231             else:
       
   232                 min_refs = 2
       
   233             if len(self.rqlst.defined_vars[mainvar].references()) < min_refs:
       
   234                 _LOGGER.warn('You did not use the %s variable in your RQL '
       
   235                              'expression %s', mainvar, self)
       
   236         # syntax tree used by read security (inserted in queries when necessary)
       
   237         self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
       
   238         # graph of links between variables, used by rql rewriter
       
   239         self.vargraph = vargraph(self.rqlst)
       
   240         # useful for some instrumentation, e.g. localperms permcheck command
       
   241         self.package = ybo.PACKAGE
       
   242 
       
   243     def __str__(self):
       
   244         return self.full_rql
       
   245     def __repr__(self):
       
   246         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
       
   247 
       
   248     def __lt__(self, other):
       
   249         if hasattr(other, 'expression'):
       
   250             return self.expression < other.expression
       
   251         return True
       
   252 
       
   253     def __eq__(self, other):
       
   254         if hasattr(other, 'expression'):
       
   255             return self.expression == other.expression
       
   256         return False
       
   257 
       
   258     def __ne__(self, other):
       
   259         return not (self == other)
       
   260 
       
   261     def __hash__(self):
       
   262         return hash(self.expression)
       
   263 
       
   264     def __deepcopy__(self, memo):
       
   265         return self.__class__(self.expression, self.mainvars)
       
   266     def __getstate__(self):
       
   267         return (self.expression, self.mainvars)
       
   268     def __setstate__(self, state):
       
   269         self.__init__(*state)
       
   270 
       
   271     @cachedproperty
       
   272     def rqlst(self):
       
   273         select = parse(self.minimal_rql, print_errors=False).children[0]
       
   274         defined = set(split_expression(self.expression))
       
   275         for varname in self.predefined_variables:
       
   276             if varname in defined:
       
   277                 select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute')
       
   278         return select
       
   279 
       
   280     # permission rql expression specific stuff #################################
       
   281 
       
   282     @cached
       
   283     def transform_has_permission(self):
       
   284         found = None
       
   285         rqlst = self.rqlst
       
   286         for var in rqlst.defined_vars.values():
       
   287             for varref in var.references():
       
   288                 rel = varref.relation()
       
   289                 if rel is None:
       
   290                     continue
       
   291                 try:
       
   292                     prefix, action, suffix = rel.r_type.split('_')
       
   293                 except ValueError:
       
   294                     continue
       
   295                 if prefix != 'has' or suffix != 'permission' or \
       
   296                        not action in ('add', 'delete', 'update', 'read'):
       
   297                     continue
       
   298                 if found is None:
       
   299                     found = []
       
   300                     rqlst.save_state()
       
   301                 assert rel.children[0].name == 'U'
       
   302                 objvar = rel.children[1].children[0].variable
       
   303                 rqlst.remove_node(rel)
       
   304                 selected = [v.name for v in rqlst.get_selected_variables()]
       
   305                 if objvar.name not in selected:
       
   306                     colindex = len(selected)
       
   307                     rqlst.add_selected(objvar)
       
   308                 else:
       
   309                     colindex = selected.index(objvar.name)
       
   310                 found.append((action, colindex))
       
   311                 # remove U eid %(u)s if U is not used in any other relation
       
   312                 uvrefs = rqlst.defined_vars['U'].references()
       
   313                 if len(uvrefs) == 1:
       
   314                     rqlst.remove_node(uvrefs[0].relation())
       
   315         if found is not None:
       
   316             rql = rqlst.as_string()
       
   317             if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation):
       
   318                 # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql
       
   319                 keyarg = rqlst.selection[0].name.lower()
       
   320             else:
       
   321                 keyarg = None
       
   322             rqlst.recover()
       
   323             return rql, found, keyarg
       
   324         return rqlst.as_string(), None, None
       
   325 
       
   326     def _check(self, _cw, **kwargs):
       
   327         """return True if the rql expression is matching the given relation
       
   328         between fromeid and toeid
       
   329 
       
   330         _cw may be a request or a server side transaction
       
   331         """
       
   332         creating = kwargs.get('creating')
       
   333         if not creating and self.eid is not None:
       
   334             key = (self.eid, tuple(sorted(kwargs.items())))
       
   335             try:
       
   336                 return _cw.local_perm_cache[key]
       
   337             except KeyError:
       
   338                 pass
       
   339         rql, has_perm_defs, keyarg = self.transform_has_permission()
       
   340         # when creating an entity, expression related to X satisfied
       
   341         if creating and 'X' in self.rqlst.defined_vars:
       
   342             return True
       
   343         if keyarg is None:
       
   344             kwargs.setdefault('u', _cw.user.eid)
       
   345             try:
       
   346                 rset = _cw.execute(rql, kwargs, build_descr=True)
       
   347             except NotImplementedError:
       
   348                 self.critical('cant check rql expression, unsupported rql %s', rql)
       
   349                 if self.eid is not None:
       
   350                     _cw.local_perm_cache[key] = False
       
   351                 return False
       
   352             except TypeResolverException as ex:
       
   353                 # some expression may not be resolvable with current kwargs
       
   354                 # (type conflict)
       
   355                 self.warning('%s: %s', rql, str(ex))
       
   356                 if self.eid is not None:
       
   357                     _cw.local_perm_cache[key] = False
       
   358                 return False
       
   359             except Unauthorized as ex:
       
   360                 self.debug('unauthorized %s: %s', rql, str(ex))
       
   361                 if self.eid is not None:
       
   362                     _cw.local_perm_cache[key] = False
       
   363                 return False
       
   364         else:
       
   365             rset = _cw.eid_rset(kwargs[keyarg])
       
   366         # if no special has_*_permission relation in the rql expression, just
       
   367         # check the result set contains something
       
   368         if has_perm_defs is None:
       
   369             if rset:
       
   370                 if self.eid is not None:
       
   371                     _cw.local_perm_cache[key] = True
       
   372                 return True
       
   373         elif rset:
       
   374             # check every special has_*_permission relation is satisfied
       
   375             get_eschema = _cw.vreg.schema.eschema
       
   376             try:
       
   377                 for eaction, col in has_perm_defs:
       
   378                     for i in range(len(rset)):
       
   379                         eschema = get_eschema(rset.description[i][col])
       
   380                         eschema.check_perm(_cw, eaction, eid=rset[i][col])
       
   381                 if self.eid is not None:
       
   382                     _cw.local_perm_cache[key] = True
       
   383                 return True
       
   384             except Unauthorized:
       
   385                 pass
       
   386         if self.eid is not None:
       
   387             _cw.local_perm_cache[key] = False
       
   388         return False
       
   389 
       
   390     @property
       
   391     def minimal_rql(self):
       
   392         return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)),
       
   393                                     self.expression)
       
   394 
       
   395 
       
   396 
       
   397 # rql expressions for use in permission definition #############################
       
   398 
       
   399 class ERQLExpression(RQLExpression):
       
   400     predefined_variables = 'XU'
       
   401 
       
   402     def __init__(self, expression, mainvars=None, eid=None):
       
   403         RQLExpression.__init__(self, expression, mainvars or 'X', eid)
       
   404 
       
   405     def check(self, _cw, eid=None, creating=False, **kwargs):
       
   406         if 'X' in self.rqlst.defined_vars:
       
   407             if eid is None:
       
   408                 if creating:
       
   409                     return self._check(_cw, creating=True, **kwargs)
       
   410                 return False
       
   411             assert creating == False
       
   412             return self._check(_cw, x=eid, **kwargs)
       
   413         return self._check(_cw, **kwargs)
       
   414 
       
   415 
       
   416 class CubicWebRelationDefinitionSchema(RelationDefinitionSchema):
       
   417     def constraint_by_eid(self, eid):
       
   418         for cstr in self.constraints:
       
   419             if cstr.eid == eid:
       
   420                 return cstr
       
   421         raise ValueError('No constraint with eid %d' % eid)
       
   422 
       
   423     def rql_expression(self, expression, mainvars=None, eid=None):
       
   424         """rql expression factory"""
       
   425         if self.rtype.final:
       
   426             return ERQLExpression(expression, mainvars, eid)
       
   427         return RRQLExpression(expression, mainvars, eid)
       
   428 
       
   429     def check_permission_definitions(self):
       
   430         super(CubicWebRelationDefinitionSchema, self).check_permission_definitions()
       
   431         schema = self.subject.schema
       
   432         for action, groups in self.permissions.items():
       
   433             for group_or_rqlexpr in groups:
       
   434                 if action == 'read' and \
       
   435                        isinstance(group_or_rqlexpr, RQLExpression):
       
   436                     msg = "can't use rql expression for read permission of %s"
       
   437                     raise BadSchemaDefinition(msg % self)
       
   438                 if self.final and isinstance(group_or_rqlexpr, RRQLExpression):
       
   439                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   440                     raise BadSchemaDefinition(msg % self)
       
   441                 if not self.final and isinstance(group_or_rqlexpr, ERQLExpression):
       
   442                     msg = "can't use ERQLExpression on %s, use a RRQLExpression"
       
   443                     raise BadSchemaDefinition(msg % self)
       
   444 
       
   445 def vargraph(rqlst):
       
   446     """ builds an adjacency graph of variables from the rql syntax tree, e.g:
       
   447     Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF
       
   448     => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']}
       
   449     """
       
   450     vargraph = {}
       
   451     for relation in rqlst.get_nodes(nodes.Relation):
       
   452         try:
       
   453             rhsvarname = relation.children[1].children[0].variable.name
       
   454             lhsvarname = relation.children[0].name
       
   455         except AttributeError:
       
   456             pass
       
   457         else:
       
   458             vargraph.setdefault(lhsvarname, []).append(rhsvarname)
       
   459             vargraph.setdefault(rhsvarname, []).append(lhsvarname)
       
   460             #vargraph[(lhsvarname, rhsvarname)] = relation.r_type
       
   461     return vargraph
       
   462 
       
   463 
       
   464 class GeneratedConstraint(object):
       
   465     def __init__(self, rqlst, mainvars):
       
   466         self.snippet_rqlst = rqlst
       
   467         self.mainvars = mainvars
       
   468         self.vargraph = vargraph(rqlst)
       
   469 
       
   470 
       
   471 class RRQLExpression(RQLExpression):
       
   472     predefined_variables = 'SOU'
       
   473 
       
   474     def __init__(self, expression, mainvars=None, eid=None):
       
   475         if mainvars is None:
       
   476             mainvars = guess_rrqlexpr_mainvars(expression)
       
   477         RQLExpression.__init__(self, expression, mainvars, eid)
       
   478 
       
   479     def check(self, _cw, fromeid=None, toeid=None):
       
   480         kwargs = {}
       
   481         if 'S' in self.rqlst.defined_vars:
       
   482             if fromeid is None:
       
   483                 return False
       
   484             kwargs['s'] = fromeid
       
   485         if 'O' in self.rqlst.defined_vars:
       
   486             if toeid is None:
       
   487                 return False
       
   488             kwargs['o'] = toeid
       
   489         return self._check(_cw, **kwargs)
       
   490 
       
   491 
       
   492 # In yams, default 'update' perm for attributes granted to managers and owners.
       
   493 # Within cw, we want to default to users who may edit the entity holding the
       
   494 # attribute.
       
   495 # These default permissions won't be checked by the security hooks:
       
   496 # since they delegate checking to the entity, we can skip actual checks.
       
   497 ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X'))
       
   498 ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X'))
       
   499 
       
   500 # we don't want 'add' or 'delete' permissions on computed relation types
       
   501 # (they're hardcoded to '()' on computed relation definitions)
       
   502 if 'add' in yams.DEFAULT_COMPUTED_RELPERMS:
       
   503     del yams.DEFAULT_COMPUTED_RELPERMS['add']
       
   504 if 'delete' in yams.DEFAULT_COMPUTED_RELPERMS:
       
   505     del yams.DEFAULT_COMPUTED_RELPERMS['delete']
       
   506 
       
   507 
       
   508 PUB_SYSTEM_ENTITY_PERMS = {
       
   509     'read':   ('managers', 'users', 'guests',),
       
   510     'add':    ('managers',),
       
   511     'delete': ('managers',),
       
   512     'update': ('managers',),
       
   513     }
       
   514 PUB_SYSTEM_REL_PERMS = {
       
   515     'read':   ('managers', 'users', 'guests',),
       
   516     'add':    ('managers',),
       
   517     'delete': ('managers',),
       
   518     }
       
   519 PUB_SYSTEM_ATTR_PERMS = {
       
   520     'read':   ('managers', 'users', 'guests',),
       
   521     'add': ('managers',),
       
   522     'update': ('managers',),
       
   523     }
       
   524 RO_REL_PERMS = {
       
   525     'read':   ('managers', 'users', 'guests',),
       
   526     'add':    (),
       
   527     'delete': (),
       
   528     }
       
   529 RO_ATTR_PERMS = {
       
   530     'read':   ('managers', 'users', 'guests',),
       
   531     'add': ybo.DEFAULT_ATTRPERMS['add'],
       
   532     'update': (),
       
   533     }
       
   534 
       
   535 # XXX same algorithm as in reorder_cubes and probably other place,
       
   536 # may probably extract a generic function
       
   537 def order_eschemas(eschemas):
       
   538     """return entity schemas ordered such that entity types which specializes an
       
   539     other one appears after that one
       
   540     """
       
   541     graph = {}
       
   542     for eschema in eschemas:
       
   543         if eschema.specializes():
       
   544             graph[eschema] = set((eschema.specializes(),))
       
   545         else:
       
   546             graph[eschema] = set()
       
   547     cycles = get_cycles(graph)
       
   548     if cycles:
       
   549         cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
       
   550         raise Exception('cycles in entity schema specialization: %s'
       
   551                         % cycles)
       
   552     eschemas = []
       
   553     while graph:
       
   554         # sorted to get predictable results
       
   555         for eschema, deps in sorted(graph.items()):
       
   556             if not deps:
       
   557                 eschemas.append(eschema)
       
   558                 del graph[eschema]
       
   559                 for deps in graph.values():
       
   560                     try:
       
   561                         deps.remove(eschema)
       
   562                     except KeyError:
       
   563                         continue
       
   564     return eschemas
       
   565 
       
   566 def bw_normalize_etype(etype):
       
   567     if etype in ETYPE_NAME_MAP:
       
   568         msg = '%s has been renamed to %s, please update your code' % (
       
   569             etype, ETYPE_NAME_MAP[etype])
       
   570         warn(msg, DeprecationWarning, stacklevel=4)
       
   571         etype = ETYPE_NAME_MAP[etype]
       
   572     return etype
       
   573 
       
   574 def display_name(req, key, form='', context=None):
       
   575     """return a internationalized string for the key (schema entity or relation
       
   576     name) in a given form
       
   577     """
       
   578     assert form in ('', 'plural', 'subject', 'object')
       
   579     if form == 'subject':
       
   580         form = ''
       
   581     if form:
       
   582         key = key + '_' + form
       
   583     # ensure unicode
       
   584     if context is not None:
       
   585         return text_type(req.pgettext(context, key))
       
   586     else:
       
   587         return text_type(req._(key))
       
   588 
       
   589 
       
   590 # Schema objects definition ###################################################
       
   591 
       
   592 def ERSchema_display_name(self, req, form='', context=None):
       
   593     """return a internationalized string for the entity/relation type name in
       
   594     a given form
       
   595     """
       
   596     return display_name(req, self.type, form, context)
       
   597 ERSchema.display_name = ERSchema_display_name
       
   598 
       
   599 @cached
       
   600 def get_groups(self, action):
       
   601     """return the groups authorized to perform <action> on entities of
       
   602     this type
       
   603 
       
   604     :type action: str
       
   605     :param action: the name of a permission
       
   606 
       
   607     :rtype: tuple
       
   608     :return: names of the groups with the given permission
       
   609     """
       
   610     assert action in self.ACTIONS, action
       
   611     #assert action in self._groups, '%s %s' % (self, action)
       
   612     try:
       
   613         return frozenset(g for g in self.permissions[action] if isinstance(g, string_types))
       
   614     except KeyError:
       
   615         return ()
       
   616 PermissionMixIn.get_groups = get_groups
       
   617 
       
   618 @cached
       
   619 def get_rqlexprs(self, action):
       
   620     """return the rql expressions representing queries to check the user is allowed
       
   621     to perform <action> on entities of this type
       
   622 
       
   623     :type action: str
       
   624     :param action: the name of a permission
       
   625 
       
   626     :rtype: tuple
       
   627     :return: the rql expressions with the given permission
       
   628     """
       
   629     assert action in self.ACTIONS, action
       
   630     #assert action in self._rqlexprs, '%s %s' % (self, action)
       
   631     try:
       
   632         return tuple(g for g in self.permissions[action] if not isinstance(g, string_types))
       
   633     except KeyError:
       
   634         return ()
       
   635 PermissionMixIn.get_rqlexprs = get_rqlexprs
       
   636 
       
   637 orig_set_action_permissions = PermissionMixIn.set_action_permissions
       
   638 def set_action_permissions(self, action, permissions):
       
   639     """set the groups and rql expressions allowing to perform <action> on
       
   640     entities of this type
       
   641 
       
   642     :type action: str
       
   643     :param action: the name of a permission
       
   644 
       
   645     :type permissions: tuple
       
   646     :param permissions: the groups and rql expressions allowing the given action
       
   647     """
       
   648     orig_set_action_permissions(self, action, tuple(permissions))
       
   649     clear_cache(self, 'get_rqlexprs')
       
   650     clear_cache(self, 'get_groups')
       
   651 PermissionMixIn.set_action_permissions = set_action_permissions
       
   652 
       
   653 def has_local_role(self, action):
       
   654     """return true if the action *may* be granted locally (i.e. either rql
       
   655     expressions or the owners group are used in security definition)
       
   656 
       
   657     XXX this method is only there since we don't know well how to deal with
       
   658     'add' action checking. Also find a better name would be nice.
       
   659     """
       
   660     assert action in self.ACTIONS, action
       
   661     if self.get_rqlexprs(action):
       
   662         return True
       
   663     if action in ('update', 'delete'):
       
   664         return 'owners' in self.get_groups(action)
       
   665     return False
       
   666 PermissionMixIn.has_local_role = has_local_role
       
   667 
       
   668 def may_have_permission(self, action, req):
       
   669     if action != 'read' and not (self.has_local_role('read') or
       
   670                                  self.has_perm(req, 'read')):
       
   671         return False
       
   672     return self.has_local_role(action) or self.has_perm(req, action)
       
   673 PermissionMixIn.may_have_permission = may_have_permission
       
   674 
       
   675 def has_perm(self, _cw, action, **kwargs):
       
   676     """return true if the action is granted globally or locally"""
       
   677     try:
       
   678         self.check_perm(_cw, action, **kwargs)
       
   679         return True
       
   680     except Unauthorized:
       
   681         return False
       
   682 PermissionMixIn.has_perm = has_perm
       
   683 
       
   684 
       
   685 def check_perm(self, _cw, action, **kwargs):
       
   686     # NB: _cw may be a server transaction or a request object.
       
   687     #
       
   688     # check user is in an allowed group, if so that's enough internal
       
   689     # transactions should always stop there
       
   690     DBG = False
       
   691     if server.DEBUG & server.DBG_SEC:
       
   692         if action in server._SECURITY_CAPS:
       
   693             _self_str = str(self)
       
   694             if server._SECURITY_ITEMS:
       
   695                 if any(item in _self_str for item in server._SECURITY_ITEMS):
       
   696                     DBG = True
       
   697             else:
       
   698                 DBG = True
       
   699     groups = self.get_groups(action)
       
   700     if _cw.user.matching_groups(groups):
       
   701         if DBG:
       
   702             print('check_perm: %r %r: user matches %s' % (action, _self_str, groups))
       
   703         return
       
   704     # if 'owners' in allowed groups, check if the user actually owns this
       
   705     # object, if so that's enough
       
   706     #
       
   707     # NB: give _cw to user.owns since user is not be bound to a transaction on
       
   708     # the repository side
       
   709     if 'owners' in groups and (
       
   710           kwargs.get('creating')
       
   711           or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
       
   712         if DBG:
       
   713             print('check_perm: %r %r: user is owner or creation time' %
       
   714                   (action, _self_str))
       
   715         return
       
   716     # else if there is some rql expressions, check them
       
   717     if DBG:
       
   718         print('check_perm: %r %r %s' %
       
   719               (action, _self_str, [(rqlexpr, kwargs, rqlexpr.check(_cw, **kwargs))
       
   720                                    for rqlexpr in self.get_rqlexprs(action)]))
       
   721     if any(rqlexpr.check(_cw, **kwargs)
       
   722            for rqlexpr in self.get_rqlexprs(action)):
       
   723         return
       
   724     raise Unauthorized(action, str(self))
       
   725 PermissionMixIn.check_perm = check_perm
       
   726 
       
   727 
       
   728 CubicWebRelationDefinitionSchema._RPROPERTIES['eid'] = None
       
   729 # remember rproperties defined at this point. Others will have to be serialized in
       
   730 # CWAttribute.extra_props
       
   731 KNOWN_RPROPERTIES = CubicWebRelationDefinitionSchema.ALL_PROPERTIES()
       
   732 
       
   733 
       
   734 class CubicWebEntitySchema(EntitySchema):
       
   735     """a entity has a type, a set of subject and or object relations
       
   736     the entity schema defines the possible relations for a given type and some
       
   737     constraints on those relations
       
   738     """
       
   739     def __init__(self, schema=None, edef=None, eid=None, **kwargs):
       
   740         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
       
   741         if eid is None and edef is not None:
       
   742             eid = getattr(edef, 'eid', None)
       
   743         self.eid = eid
       
   744 
       
   745     def targets(self, role):
       
   746         assert role in ('subject', 'object')
       
   747         if role == 'subject':
       
   748             return self.subjrels.values()
       
   749         return self.objrels.values()
       
   750 
       
   751     @cachedproperty
       
   752     def composite_rdef_roles(self):
       
   753         """Return all relation definitions that define the current entity
       
   754         type as a composite.
       
   755         """
       
   756         rdef_roles = []
       
   757         for role in ('subject', 'object'):
       
   758             for rschema in self.targets(role):
       
   759                 if rschema.final:
       
   760                     continue
       
   761                 for rdef in rschema.rdefs.values():
       
   762                     if (role == 'subject' and rdef.subject == self) or \
       
   763                             (role == 'object' and rdef.object == self):
       
   764                         crole = rdef.composite
       
   765                         if crole == role:
       
   766                             rdef_roles.append((rdef, role))
       
   767         return rdef_roles
       
   768 
       
   769     @cachedproperty
       
   770     def is_composite(self):
       
   771         return bool(len(self.composite_rdef_roles))
       
   772 
       
   773     def check_permission_definitions(self):
       
   774         super(CubicWebEntitySchema, self).check_permission_definitions()
       
   775         for groups in self.permissions.values():
       
   776             for group_or_rqlexpr in groups:
       
   777                 if isinstance(group_or_rqlexpr, RRQLExpression):
       
   778                     msg = "can't use RRQLExpression on %s, use an ERQLExpression"
       
   779                     raise BadSchemaDefinition(msg % self.type)
       
   780 
       
   781     def is_subobject(self, strict=False, skiprels=None):
       
   782         if skiprels is None:
       
   783             skiprels = SKIP_COMPOSITE_RELS
       
   784         else:
       
   785             skiprels += SKIP_COMPOSITE_RELS
       
   786         return super(CubicWebEntitySchema, self).is_subobject(strict,
       
   787                                                               skiprels=skiprels)
       
   788 
       
   789     def attribute_definitions(self):
       
   790         """return an iterator on attribute definitions
       
   791 
       
   792         attribute relations are a subset of subject relations where the
       
   793         object's type is a final entity
       
   794 
       
   795         an attribute definition is a 2-uple :
       
   796         * name of the relation
       
   797         * schema of the destination entity type
       
   798         """
       
   799         iter = super(CubicWebEntitySchema, self).attribute_definitions()
       
   800         for rschema, attrschema in iter:
       
   801             if rschema.type == 'has_text':
       
   802                 continue
       
   803             yield rschema, attrschema
       
   804 
       
   805     def main_attribute(self):
       
   806         """convenience method that returns the *main* (i.e. the first non meta)
       
   807         attribute defined in the entity schema
       
   808         """
       
   809         for rschema, _ in self.attribute_definitions():
       
   810             if not (rschema in META_RTYPES
       
   811                     or self.is_metadata(rschema)):
       
   812                 return rschema
       
   813 
       
   814     def add_subject_relation(self, rschema):
       
   815         """register the relation schema as possible subject relation"""
       
   816         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
       
   817         if rschema.final:
       
   818             if self.rdef(rschema).get('fulltextindexed'):
       
   819                 self._update_has_text()
       
   820         elif rschema.fulltext_container:
       
   821             self._update_has_text()
       
   822 
       
   823     def add_object_relation(self, rschema):
       
   824         """register the relation schema as possible object relation"""
       
   825         super(CubicWebEntitySchema, self).add_object_relation(rschema)
       
   826         if rschema.fulltext_container:
       
   827             self._update_has_text()
       
   828 
       
   829     def del_subject_relation(self, rtype):
       
   830         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
       
   831         if 'has_text' in self.subjrels:
       
   832             self._update_has_text(deletion=True)
       
   833 
       
   834     def del_object_relation(self, rtype):
       
   835         super(CubicWebEntitySchema, self).del_object_relation(rtype)
       
   836         if 'has_text' in self.subjrels:
       
   837             self._update_has_text(deletion=True)
       
   838 
       
   839     def _update_has_text(self, deletion=False):
       
   840         may_need_has_text, has_has_text = False, False
       
   841         need_has_text = None
       
   842         for rschema in self.subject_relations():
       
   843             if rschema.final:
       
   844                 if rschema == 'has_text':
       
   845                     has_has_text = True
       
   846                 elif self.rdef(rschema).get('fulltextindexed'):
       
   847                     may_need_has_text = True
       
   848             elif rschema.fulltext_container:
       
   849                 if rschema.fulltext_container == 'subject':
       
   850                     may_need_has_text = True
       
   851                 else:
       
   852                     need_has_text = False
       
   853         for rschema in self.object_relations():
       
   854             if rschema.fulltext_container:
       
   855                 if rschema.fulltext_container == 'object':
       
   856                     may_need_has_text = True
       
   857                 else:
       
   858                     need_has_text = False
       
   859         if need_has_text is None:
       
   860             need_has_text = may_need_has_text
       
   861         if need_has_text and not has_has_text and not deletion:
       
   862             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String',
       
   863                                           __permissions__=RO_ATTR_PERMS)
       
   864             self.schema.add_relation_def(rdef)
       
   865         elif not need_has_text and has_has_text:
       
   866             # use rschema.del_relation_def and not schema.del_relation_def to
       
   867             # avoid deleting the relation type accidentally...
       
   868             self.schema['has_text'].del_relation_def(self, self.schema['String'])
       
   869 
       
   870     def schema_entity(self): # XXX @property for consistency with meta
       
   871         """return True if this entity type is used to build the schema"""
       
   872         return self.type in SCHEMA_TYPES
       
   873 
       
   874     def rql_expression(self, expression, mainvars=None, eid=None):
       
   875         """rql expression factory"""
       
   876         return ERQLExpression(expression, mainvars, eid)
       
   877 
       
   878 
       
   879 class CubicWebRelationSchema(PermissionMixIn, RelationSchema):
       
   880     permissions = {}
       
   881     ACTIONS = ()
       
   882     rdef_class = CubicWebRelationDefinitionSchema
       
   883 
       
   884     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
       
   885         if rdef is not None:
       
   886             # if this relation is inlined
       
   887             self.inlined = rdef.inlined
       
   888         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
       
   889         if eid is None and rdef is not None:
       
   890             eid = getattr(rdef, 'eid', None)
       
   891         self.eid = eid
       
   892 
       
   893     def init_computed_relation(self, rdef):
       
   894         self.ACTIONS = ('read',)
       
   895         super(CubicWebRelationSchema, self).init_computed_relation(rdef)
       
   896 
       
   897     def advertise_new_add_permission(self):
       
   898         pass
       
   899 
       
   900     def check_permission_definitions(self):
       
   901         RelationSchema.check_permission_definitions(self)
       
   902         PermissionMixIn.check_permission_definitions(self)
       
   903 
       
   904     @property
       
   905     def meta(self):
       
   906         return self.type in META_RTYPES
       
   907 
       
   908     def schema_relation(self): # XXX @property for consistency with meta
       
   909         """return True if this relation type is used to build the schema"""
       
   910         return self.type in SCHEMA_TYPES
       
   911 
       
   912     def may_have_permission(self, action, req, eschema=None, role=None):
       
   913         if eschema is not None:
       
   914             for tschema in self.targets(eschema, role):
       
   915                 rdef = self.role_rdef(eschema, tschema, role)
       
   916                 if rdef.may_have_permission(action, req):
       
   917                     return True
       
   918         else:
       
   919             for rdef in self.rdefs.values():
       
   920                 if rdef.may_have_permission(action, req):
       
   921                     return True
       
   922         return False
       
   923 
       
   924     def has_perm(self, _cw, action, **kwargs):
       
   925         """return true if the action is granted globally or locally"""
       
   926         if self.final:
       
   927             assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
       
   928             assert action in ('read', 'update')
       
   929             if 'eid' in kwargs:
       
   930                 subjtype = _cw.entity_metas(kwargs['eid'])['type']
       
   931             else:
       
   932                 subjtype = objtype = None
       
   933         else:
       
   934             assert not 'eid' in kwargs, kwargs
       
   935             assert action in ('read', 'add', 'delete')
       
   936             if 'fromeid' in kwargs:
       
   937                 subjtype = _cw.entity_metas(kwargs['fromeid'])['type']
       
   938             elif 'frometype' in kwargs:
       
   939                 subjtype = kwargs.pop('frometype')
       
   940             else:
       
   941                 subjtype = None
       
   942             if 'toeid' in kwargs:
       
   943                 objtype = _cw.entity_metas(kwargs['toeid'])['type']
       
   944             elif 'toetype' in kwargs:
       
   945                 objtype = kwargs.pop('toetype')
       
   946             else:
       
   947                 objtype = None
       
   948         if objtype and subjtype:
       
   949             return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs)
       
   950         elif subjtype:
       
   951             for tschema in self.targets(subjtype, 'subject'):
       
   952                 rdef = self.rdef(subjtype, tschema)
       
   953                 if not rdef.has_perm(_cw, action, **kwargs):
       
   954                     return False
       
   955         elif objtype:
       
   956             for tschema in self.targets(objtype, 'object'):
       
   957                 rdef = self.rdef(tschema, objtype)
       
   958                 if not rdef.has_perm(_cw, action, **kwargs):
       
   959                     return False
       
   960         else:
       
   961             for rdef in self.rdefs.values():
       
   962                 if not rdef.has_perm(_cw, action, **kwargs):
       
   963                     return False
       
   964         return True
       
   965 
       
   966     @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
       
   967     def cardinality(self, subjtype, objtype, target):
       
   968         return self.rdef(subjtype, objtype).role_cardinality(target)
       
   969 
       
   970 
       
   971 class CubicWebSchema(Schema):
       
   972     """set of entities and relations schema defining the possible data sets
       
   973     used in an application
       
   974 
       
   975     :type name: str
       
   976     :ivar name: name of the schema, usually the instance identifier
       
   977 
       
   978     :type base: str
       
   979     :ivar base: path of the directory where the schema is defined
       
   980     """
       
   981     reading_from_database = False
       
   982     entity_class = CubicWebEntitySchema
       
   983     relation_class = CubicWebRelationSchema
       
   984     no_specialization_inference = ('identity',)
       
   985 
       
   986     def __init__(self, *args, **kwargs):
       
   987         self._eid_index = {}
       
   988         super(CubicWebSchema, self).__init__(*args, **kwargs)
       
   989         ybo.register_base_types(self)
       
   990         rschema = self.add_relation_type(ybo.RelationType('eid'))
       
   991         rschema.final = True
       
   992         rschema = self.add_relation_type(ybo.RelationType('has_text'))
       
   993         rschema.final = True
       
   994         rschema = self.add_relation_type(ybo.RelationType('identity'))
       
   995         rschema.final = False
       
   996 
       
   997     etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[A-Za-z0-9]*$'
       
   998     def add_entity_type(self, edef):
       
   999         edef.name = str(edef.name)
       
  1000         edef.name = bw_normalize_etype(edef.name)
       
  1001         if not re.match(self.etype_name_re, edef.name):
       
  1002             raise BadSchemaDefinition(
       
  1003                 '%r is not a valid name for an entity type. It should start '
       
  1004                 'with an upper cased letter and be followed by at least a '
       
  1005                 'lower cased letter' % edef.name)
       
  1006         eschema = super(CubicWebSchema, self).add_entity_type(edef)
       
  1007         if not eschema.final:
       
  1008             # automatically add the eid relation to non final entity types
       
  1009             rdef = ybo.RelationDefinition(eschema.type, 'eid', 'Int',
       
  1010                                           cardinality='11', uid=True,
       
  1011                                           __permissions__=RO_ATTR_PERMS)
       
  1012             self.add_relation_def(rdef)
       
  1013             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type,
       
  1014                                           __permissions__=RO_REL_PERMS)
       
  1015             self.add_relation_def(rdef)
       
  1016         self._eid_index[eschema.eid] = eschema
       
  1017         return eschema
       
  1018 
       
  1019     def add_relation_type(self, rdef):
       
  1020         if not rdef.name.islower():
       
  1021             raise BadSchemaDefinition(
       
  1022                 '%r is not a valid name for a relation type. It should be '
       
  1023                 'lower cased' % rdef.name)
       
  1024         rdef.name = str(rdef.name)
       
  1025         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
       
  1026         self._eid_index[rschema.eid] = rschema
       
  1027         return rschema
       
  1028 
       
  1029     def add_relation_def(self, rdef):
       
  1030         """build a part of a relation schema
       
  1031         (i.e. add a relation between two specific entity's types)
       
  1032 
       
  1033         :type subject: str
       
  1034         :param subject: entity's type that is subject of the relation
       
  1035 
       
  1036         :type rtype: str
       
  1037         :param rtype: the relation's type (i.e. the name of the relation)
       
  1038 
       
  1039         :type obj: str
       
  1040         :param obj: entity's type that is object of the relation
       
  1041 
       
  1042         :rtype: RelationSchema
       
  1043         :param: the newly created or just completed relation schema
       
  1044         """
       
  1045         rdef.name = rdef.name.lower()
       
  1046         rdef.subject = bw_normalize_etype(rdef.subject)
       
  1047         rdef.object = bw_normalize_etype(rdef.object)
       
  1048         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
       
  1049         if rdefs:
       
  1050             try:
       
  1051                 self._eid_index[rdef.eid] = rdefs
       
  1052             except AttributeError:
       
  1053                 pass # not a serialized schema
       
  1054         return rdefs
       
  1055 
       
  1056     def del_relation_type(self, rtype):
       
  1057         rschema = self.rschema(rtype)
       
  1058         self._eid_index.pop(rschema.eid, None)
       
  1059         super(CubicWebSchema, self).del_relation_type(rtype)
       
  1060 
       
  1061     def del_relation_def(self, subjtype, rtype, objtype):
       
  1062         for k, v in self._eid_index.items():
       
  1063             if not isinstance(v, RelationDefinitionSchema):
       
  1064                 continue
       
  1065             if v.subject == subjtype and v.rtype == rtype and v.object == objtype:
       
  1066                 del self._eid_index[k]
       
  1067                 break
       
  1068         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
       
  1069 
       
  1070     def del_entity_type(self, etype):
       
  1071         eschema = self.eschema(etype)
       
  1072         self._eid_index.pop(eschema.eid, None)
       
  1073         # deal with has_text first, else its automatic deletion (see above)
       
  1074         # may trigger an error in ancestor's del_entity_type method
       
  1075         if 'has_text' in eschema.subject_relations():
       
  1076             self.del_relation_def(etype, 'has_text', 'String')
       
  1077         super(CubicWebSchema, self).del_entity_type(etype)
       
  1078 
       
  1079     def schema_by_eid(self, eid):
       
  1080         return self._eid_index[eid]
       
  1081 
       
  1082     def iter_computed_attributes(self):
       
  1083         for relation in self.relations():
       
  1084             for rdef in relation.rdefs.values():
       
  1085                 if rdef.final and rdef.formula is not None:
       
  1086                     yield rdef
       
  1087 
       
  1088     def iter_computed_relations(self):
       
  1089         for relation in self.relations():
       
  1090             if relation.rule:
       
  1091                 yield relation
       
  1092 
       
  1093     def finalize(self):
       
  1094         super(CubicWebSchema, self).finalize()
       
  1095         self.finalize_computed_attributes()
       
  1096         self.finalize_computed_relations()
       
  1097 
       
  1098     def finalize_computed_attributes(self):
       
  1099         """Check computed attributes validity (if any), else raise
       
  1100         `BadSchemaDefinition`
       
  1101         """
       
  1102         analyzer = ETypeResolver(self)
       
  1103         for rdef in self.iter_computed_attributes():
       
  1104             rqlst = parse(rdef.formula)
       
  1105             select = rqlst.children[0]
       
  1106             select.add_type_restriction(select.defined_vars['X'], str(rdef.subject))
       
  1107             analyzer.visit(select)
       
  1108             _check_valid_formula(rdef, rqlst)
       
  1109             rdef.formula_select = select # avoid later recomputation
       
  1110 
       
  1111 
       
  1112     def finalize_computed_relations(self):
       
  1113         """Build relation definitions for computed relations
       
  1114 
       
  1115         The subject and object types are infered using rql analyzer.
       
  1116         """
       
  1117         analyzer = ETypeResolver(self)
       
  1118         for rschema in self.iter_computed_relations():
       
  1119             # XXX rule is valid if both S and O are defined and not in an exists
       
  1120             rqlexpr = RRQLExpression(rschema.rule)
       
  1121             rqlst = rqlexpr.snippet_rqlst
       
  1122             analyzer.visit(rqlst)
       
  1123             couples = set((sol['S'], sol['O']) for sol in rqlst.solutions)
       
  1124             for subjtype, objtype in couples:
       
  1125                 if self[objtype].final:
       
  1126                     raise BadSchemaDefinition('computed relations cannot be final')
       
  1127                 rdef = ybo.RelationDefinition(
       
  1128                     subjtype, rschema.type, objtype,
       
  1129                     __permissions__={'add': (),
       
  1130                                      'delete': (),
       
  1131                                      'read': rschema.permissions['read']})
       
  1132                 rdef.infered = True
       
  1133                 self.add_relation_def(rdef)
       
  1134 
       
  1135     def rebuild_infered_relations(self):
       
  1136         super(CubicWebSchema, self).rebuild_infered_relations()
       
  1137         self.finalize_computed_attributes()
       
  1138         self.finalize_computed_relations()
       
  1139 
       
  1140 
       
  1141 # additional cw specific constraints ###########################################
       
  1142 
       
  1143 # these are implemented as CHECK constraints in sql, don't do the work
       
  1144 # twice
       
  1145 StaticVocabularyConstraint.check = lambda *args: True
       
  1146 IntervalBoundConstraint.check = lambda *args: True
       
  1147 BoundaryConstraint.check = lambda *args: True
       
  1148 
       
  1149 class BaseRQLConstraint(RRQLExpression, BaseConstraint):
       
  1150     """base class for rql constraints"""
       
  1151     distinct_query = None
       
  1152 
       
  1153     def serialize(self):
       
  1154         # start with a semicolon for bw compat, see below
       
  1155         return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression
       
  1156 
       
  1157     @classmethod
       
  1158     def deserialize(cls, value):
       
  1159         _, mainvars, expression = value.split(';', 2)
       
  1160         return cls(expression, mainvars)
       
  1161 
       
  1162     def check(self, entity, rtype, value):
       
  1163         """return true if the value satisfy the constraint, else false"""
       
  1164         # implemented as a hook in the repository
       
  1165         return 1
       
  1166 
       
  1167     def __str__(self):
       
  1168         if self.distinct_query:
       
  1169             selop = 'Any'
       
  1170         else:
       
  1171             selop = 'DISTINCT Any'
       
  1172         return '%s(%s %s WHERE %s)' % (self.__class__.__name__, selop,
       
  1173                                        ','.join(sorted(self.mainvars)),
       
  1174                                        self.expression)
       
  1175 
       
  1176     def __repr__(self):
       
  1177         return '<%s @%#x>' % (self.__str__(), id(self))
       
  1178 
       
  1179 
       
  1180 class RQLVocabularyConstraint(BaseRQLConstraint):
       
  1181     """the rql vocabulary constraint:
       
  1182 
       
  1183     limits the proposed values to a set of entities returned by an rql query,
       
  1184     but this is not enforced at the repository level
       
  1185 
       
  1186     `expression` is an additional rql restriction that will be added to
       
  1187     a predefined query, where the S and O variables respectively represent
       
  1188     the subject and the object of the relation
       
  1189 
       
  1190     `mainvars` is a set of variables that should be used as selection variables
       
  1191     (i.e. `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
       
  1192     made to guess it based on the variables used in the expression.
       
  1193     """
       
  1194 
       
  1195     def repo_check(self, session, eidfrom, rtype, eidto):
       
  1196         """raise ValidationError if the relation doesn't satisfy the constraint
       
  1197         """
       
  1198         pass # this is a vocabulary constraint, not enforced
       
  1199 
       
  1200 
       
  1201 class RepoEnforcedRQLConstraintMixIn(object):
       
  1202 
       
  1203     def __init__(self, expression, mainvars=None, msg=None):
       
  1204         super(RepoEnforcedRQLConstraintMixIn, self).__init__(expression, mainvars)
       
  1205         self.msg = msg
       
  1206 
       
  1207     def serialize(self):
       
  1208         # start with a semicolon for bw compat, see below
       
  1209         return ';%s;%s\n%s' % (','.join(sorted(self.mainvars)), self.expression,
       
  1210                                self.msg or '')
       
  1211 
       
  1212     @classmethod
       
  1213     def deserialize(cls, value):
       
  1214         value, msg = value.split('\n', 1)
       
  1215         _, mainvars, expression = value.split(';', 2)
       
  1216         return cls(expression, mainvars, msg)
       
  1217 
       
  1218     def repo_check(self, session, eidfrom, rtype, eidto=None):
       
  1219         """raise ValidationError if the relation doesn't satisfy the constraint
       
  1220         """
       
  1221         if not self.match_condition(session, eidfrom, eidto):
       
  1222             # XXX at this point if both or neither of S and O are in mainvar we
       
  1223             # dunno if the validation error `occurred` on eidfrom or eidto (from
       
  1224             # user interface point of view)
       
  1225             #
       
  1226             # possible enhancement: check entity being created, it's probably
       
  1227             # the main eid unless this is a composite relation
       
  1228             if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars:
       
  1229                 maineid = eidfrom
       
  1230                 qname = role_name(rtype, 'subject')
       
  1231             else:
       
  1232                 maineid = eidto
       
  1233                 qname = role_name(rtype, 'object')
       
  1234             if self.msg:
       
  1235                 msg = session._(self.msg)
       
  1236             else:
       
  1237                 msg = '%(constraint)s %(expression)s failed' % {
       
  1238                     'constraint':  session._(self.type()),
       
  1239                     'expression': self.expression}
       
  1240             raise ValidationError(maineid, {qname: msg})
       
  1241 
       
  1242     def exec_query(self, _cw, eidfrom, eidto):
       
  1243         if eidto is None:
       
  1244             # checking constraint for an attribute relation
       
  1245             expression = 'S eid %(s)s, ' + self.expression
       
  1246             args = {'s': eidfrom}
       
  1247         else:
       
  1248             expression = 'S eid %(s)s, O eid %(o)s, ' + self.expression
       
  1249             args = {'s': eidfrom, 'o': eidto}
       
  1250         if 'U' in self.rqlst.defined_vars:
       
  1251             expression = 'U eid %(u)s, ' + expression
       
  1252             args['u'] = _cw.user.eid
       
  1253         rql = 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), expression)
       
  1254         if self.distinct_query:
       
  1255             rql = 'DISTINCT ' + rql
       
  1256         return _cw.execute(rql, args, build_descr=False)
       
  1257 
       
  1258 
       
  1259 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
       
  1260     """the rql constraint is similar to the RQLVocabularyConstraint but
       
  1261     are also enforced at the repository level
       
  1262     """
       
  1263     distinct_query = False
       
  1264 
       
  1265     def match_condition(self, session, eidfrom, eidto):
       
  1266         return self.exec_query(session, eidfrom, eidto)
       
  1267 
       
  1268 
       
  1269 class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
       
  1270     """the unique rql constraint check that the result of the query isn't
       
  1271     greater than one.
       
  1272 
       
  1273     You *must* specify `mainvars` when instantiating the constraint since there
       
  1274     is no way to guess it correctly (e.g. if using S,O or U the constraint will
       
  1275     always be satisfied because we've to use a DISTINCT query).
       
  1276     """
       
  1277     # XXX turns mainvars into a required argument in __init__
       
  1278     distinct_query = True
       
  1279 
       
  1280     def match_condition(self, session, eidfrom, eidto):
       
  1281         return len(self.exec_query(session, eidfrom, eidto)) <= 1
       
  1282 
       
  1283 
       
  1284 # workflow extensions #########################################################
       
  1285 
       
  1286 from yams.buildobjs import _add_relation as yams_add_relation
       
  1287 
       
  1288 class workflowable_definition(ybo.metadefinition):
       
  1289     """extends default EntityType's metaclass to add workflow relations
       
  1290     (i.e. in_state, wf_info_for and custom_workflow). This is the default
       
  1291     metaclass for WorkflowableEntityType.
       
  1292     """
       
  1293     def __new__(mcs, name, bases, classdict):
       
  1294         abstract = classdict.pop('__abstract__', False)
       
  1295         cls = super(workflowable_definition, mcs).__new__(mcs, name, bases,
       
  1296                                                           classdict)
       
  1297         if not abstract:
       
  1298             make_workflowable(cls)
       
  1299         return cls
       
  1300 
       
  1301 
       
  1302 @add_metaclass(workflowable_definition)
       
  1303 class WorkflowableEntityType(ybo.EntityType):
       
  1304     """Use this base class instead of :class:`EntityType` to have workflow
       
  1305     relations (i.e. `in_state`, `wf_info_for` and `custom_workflow`) on your
       
  1306     entity type.
       
  1307     """
       
  1308     __abstract__ = True
       
  1309 
       
  1310 
       
  1311 def make_workflowable(cls, in_state_descr=None):
       
  1312     """Adds workflow relations as :class:`WorkflowableEntityType`, but usable on
       
  1313     existing classes which are not using that base class.
       
  1314     """
       
  1315     existing_rels = set(rdef.name for rdef in cls.__relations__)
       
  1316     # let relation types defined in cw.schemas.workflow carrying
       
  1317     # cardinality, constraints and other relation definition properties
       
  1318     etype = getattr(cls, 'name', cls.__name__)
       
  1319     if 'custom_workflow' not in existing_rels:
       
  1320         rdef = ybo.RelationDefinition(etype, 'custom_workflow', 'Workflow')
       
  1321         yams_add_relation(cls.__relations__, rdef)
       
  1322     if 'in_state' not in existing_rels:
       
  1323         rdef = ybo.RelationDefinition(etype, 'in_state', 'State',
       
  1324                                       description=in_state_descr)
       
  1325         yams_add_relation(cls.__relations__, rdef)
       
  1326     if 'wf_info_for' not in existing_rels:
       
  1327         rdef = ybo.RelationDefinition('TrInfo', 'wf_info_for', etype)
       
  1328         yams_add_relation(cls.__relations__, rdef)
       
  1329 
       
  1330 
       
  1331 # schema loading ##############################################################
       
  1332 
       
  1333 CONSTRAINTS['RQLConstraint'] = RQLConstraint
       
  1334 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
       
  1335 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
       
  1336 CONSTRAINTS.pop('MultipleStaticVocabularyConstraint', None) # don't want this in cw yams schema
       
  1337 PyFileReader.context.update(CONSTRAINTS)
       
  1338 
       
  1339 
       
  1340 class BootstrapSchemaLoader(SchemaLoader):
       
  1341     """cubicweb specific schema loader, loading only schema necessary to read
       
  1342     the persistent schema
       
  1343     """
       
  1344     schemacls = CubicWebSchema
       
  1345 
       
  1346     def load(self, config, path=(), **kwargs):
       
  1347         """return a Schema instance from the schema definition read
       
  1348         from <directory>
       
  1349         """
       
  1350         return super(BootstrapSchemaLoader, self).load(
       
  1351             path, config.appid, register_base_types=False, **kwargs)
       
  1352 
       
  1353     def _load_definition_files(self, cubes=None):
       
  1354         # bootstraping, ignore cubes
       
  1355         filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py')
       
  1356         self.info('loading %s', filepath)
       
  1357         with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here
       
  1358             self.handle_file(filepath)
       
  1359 
       
  1360     def unhandled_file(self, filepath):
       
  1361         """called when a file without handler associated has been found"""
       
  1362         self.warning('ignoring file %r', filepath)
       
  1363 
       
  1364     # these are overridden by set_log_methods below
       
  1365     # only defining here to prevent pylint from complaining
       
  1366     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
  1367 
       
  1368 class CubicWebSchemaLoader(BootstrapSchemaLoader):
       
  1369     """cubicweb specific schema loader, automatically adding metadata to the
       
  1370     instance's schema
       
  1371     """
       
  1372 
       
  1373     def load(self, config, **kwargs):
       
  1374         """return a Schema instance from the schema definition read
       
  1375         from <directory>
       
  1376         """
       
  1377         self.info('loading %s schemas', ', '.join(config.cubes()))
       
  1378         self.extrapath = {}
       
  1379         for cubesdir in config.cubes_search_path():
       
  1380             if cubesdir != config.CUBES_DIR:
       
  1381                 self.extrapath[cubesdir] = 'cubes'
       
  1382         if config.apphome:
       
  1383             path = tuple(reversed([config.apphome] + config.cubes_path()))
       
  1384         else:
       
  1385             path = tuple(reversed(config.cubes_path()))
       
  1386         try:
       
  1387             return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs)
       
  1388         finally:
       
  1389             # we've to cleanup modules imported from cubicweb.schemas as well
       
  1390             cleanup_sys_modules([join(cubicweb.CW_SOFTWARE_ROOT, 'schemas')])
       
  1391 
       
  1392     def _load_definition_files(self, cubes):
       
  1393         for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'),
       
  1394                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'),
       
  1395                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'),
       
  1396                          join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')):
       
  1397             self.info('loading %s', filepath)
       
  1398             with tempattr(ybo, 'PACKAGE', 'cubicweb'):
       
  1399                 self.handle_file(filepath)
       
  1400         for cube in cubes:
       
  1401             for filepath in self.get_schema_files(cube):
       
  1402                 with tempattr(ybo, 'PACKAGE', basename(cube)):
       
  1403                     self.handle_file(filepath)
       
  1404 
       
  1405     # these are overridden by set_log_methods below
       
  1406     # only defining here to prevent pylint from complaining
       
  1407     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
  1408 
       
  1409 
       
  1410 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
       
  1411 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
       
  1412 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
       
  1413 
       
  1414 # _() is just there to add messages to the catalog, don't care about actual
       
  1415 # translation
       
  1416 MAY_USE_TEMPLATE_FORMAT = set(('managers',))
       
  1417 NEED_PERM_FORMATS = [_('text/cubicweb-page-template')]
       
  1418 
       
  1419 @monkeypatch(FormatConstraint)
       
  1420 def vocabulary(self, entity=None, form=None):
       
  1421     cw = None
       
  1422     if form is None and entity is not None:
       
  1423         cw = entity._cw
       
  1424     elif form is not None:
       
  1425         cw = form._cw
       
  1426     if cw is not None:
       
  1427         if hasattr(cw, 'write_security'): # test it's a session and not a request
       
  1428             # cw is a server session
       
  1429             hasperm = not cw.write_security or \
       
  1430                       not cw.is_hook_category_activated('integrity') or \
       
  1431                       cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT)
       
  1432         else:
       
  1433             hasperm = cw.user.matching_groups(MAY_USE_TEMPLATE_FORMAT)
       
  1434         if hasperm:
       
  1435             return self.regular_formats + tuple(NEED_PERM_FORMATS)
       
  1436     return self.regular_formats
       
  1437 
       
  1438 # XXX itou for some Statement methods
       
  1439 from rql import stmts
       
  1440 orig_get_etype = stmts.ScopeNode.get_etype
       
  1441 def bw_get_etype(self, name):
       
  1442     return orig_get_etype(self, bw_normalize_etype(name))
       
  1443 stmts.ScopeNode.get_etype = bw_get_etype
       
  1444 
       
  1445 orig_add_main_variable_delete = stmts.Delete.add_main_variable
       
  1446 def bw_add_main_variable_delete(self, etype, vref):
       
  1447     return orig_add_main_variable_delete(self, bw_normalize_etype(etype), vref)
       
  1448 stmts.Delete.add_main_variable = bw_add_main_variable_delete
       
  1449 
       
  1450 orig_add_main_variable_insert = stmts.Insert.add_main_variable
       
  1451 def bw_add_main_variable_insert(self, etype, vref):
       
  1452     return orig_add_main_variable_insert(self, bw_normalize_etype(etype), vref)
       
  1453 stmts.Insert.add_main_variable = bw_add_main_variable_insert
       
  1454 
       
  1455 orig_set_statement_type = stmts.Select.set_statement_type
       
  1456 def bw_set_statement_type(self, etype):
       
  1457     return orig_set_statement_type(self, bw_normalize_etype(etype))
       
  1458 stmts.Select.set_statement_type = bw_set_statement_type