CatalogTool.py 44.7 KB
Newer Older
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1 2 3
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
4
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

29
import sys
30
from copy import deepcopy
31
from collections import defaultdict
32
from math import ceil
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33 34
from Products.CMFCore.CatalogTool import CatalogTool as CMFCoreCatalogTool
from Products.ZSQLCatalog.ZSQLCatalog import ZCatalog
35
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery, SimpleQuery
36
from Products.ERP5Type import Permissions
Jean-Paul Smets's avatar
Jean-Paul Smets committed
37
from AccessControl import ClassSecurityInfo, getSecurityManager
38
from AccessControl.User import system as system_user
Aurel's avatar
Aurel committed
39 40
from Products.CMFCore.utils import UniqueObject, _getAuthenticatedUser, getToolByName
from Products.ERP5Type.Globals import InitializeClass, DTMLFile
41
from Acquisition import aq_base, aq_inner, aq_parent, ImplicitAcquisitionWrapper
42
from Products.CMFActivity.ActiveObject import ActiveObject
43
from Products.CMFActivity.ActivityTool import GroupedMessage
44
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Jean-Paul Smets's avatar
Jean-Paul Smets committed
45 46 47

from AccessControl.PermissionRole import rolesForPermissionOn

48
from MethodObject import Method
Jean-Paul Smets's avatar
Jean-Paul Smets committed
49

50
from Products.ERP5Security import mergedLocalRoles
51
from Products.ERP5Security.ERP5UserManager import SUPER_USER
52
from Products.ZSQLCatalog.Utils import sqlquote
53

Aurel's avatar
Aurel committed
54
import warnings
55
from zLOG import LOG, PROBLEM, WARNING, INFO
Jean-Paul Smets's avatar
Jean-Paul Smets committed
56

57
ACQUIRE_PERMISSION_VALUE = []
58
DYNAMIC_METHOD_NAME = 'z_related_'
59
DYNAMIC_METHOD_NAME_LEN = len(DYNAMIC_METHOD_NAME)
60
STRICT_DYNAMIC_METHOD_NAME = DYNAMIC_METHOD_NAME + 'strict_'
61
STRICT_DYNAMIC_METHOD_NAME_LEN = len(STRICT_DYNAMIC_METHOD_NAME)
62
RELATED_DYNAMIC_METHOD_NAME = '_related'
63 64
# Negative as it's used as a slice end offset
RELATED_DYNAMIC_METHOD_NAME_LEN = -len(RELATED_DYNAMIC_METHOD_NAME)
65
ZOPE_SECURITY_SUFFIX = '__roles__'
Aurel's avatar
Aurel committed
66

67
class IndexableObjectWrapper(object):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
68

69
    def __init__(self, ob):
70 71
        self.__ob = ob

72 73 74 75
    def __getattr__(self, name):
        return getattr(self.__ob, name)

    # We need to update the uid during the cataloging process
76
    uid = property(lambda self: self.__ob.getUid(),
77
                   lambda self, value: setattr(self.__ob, 'uid', value))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78

79
    def _getSecurityParameterList(self):
80 81
      result = self.__dict__.get('_cache_result', None)
      if result is None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82
        ob = self.__ob
83 84 85 86 87
        # For each group or user, we have a list of roles, this list
        # give in this order : [roles on object, roles acquired on the parent,
        # roles acquired on the parent of the parent....]
        # So if we have ['-Author','Author'] we should remove the role 'Author'
        # but if we have ['Author','-Author'] we have to keep the role 'Author'
88 89
        localroles = {}
        skip_role_set = set()
90 91
        skip_role = skip_role_set.add
        clear_skip_role = skip_role_set.clear
92
        for key, role_list in mergedLocalRoles(ob).iteritems():
93 94 95 96 97 98 99 100 101
          new_role_list = []
          new_role = new_role_list.append
          clear_skip_role()
          for role in role_list:
            if role[:1] == '-':
              skip_role(role[1:])
            elif role not in skip_role_set:
              new_role(role)
          if len(new_role_list)>0:
102
            localroles[key] = new_role_list
103

104
        portal = ob.getPortalObject()
105 106 107 108
        role_dict = dict(portal.portal_catalog.getSQLCatalog().\
                                              getSQLCatalogRoleKeysList())
        getUserById = portal.acl_users.getUserById

109
        allowed_dict = {}
110

111 112 113 114 115 116
        # For each local role of a user:
        #   If the local role grants View permission, add it.
        # Every addition implies 2 lines:
        #   user:<user_id>
        #   user:<user_id>:<role_id>
        # A line must not be present twice in final result.
117
        allowed_role_set = set(rolesForPermissionOn('View', ob))
118 119 120 121 122
        # XXX the permission name is included by default for verbose
        # logging of security errors, but the catalog does not need to
        # index it. Unfortunately, rolesForPermissionOn does not have
        # an option to disable this behavior at calling time, so
        # discard it explicitly.
123
        allowed_role_set.discard('_View_Permission')
124 125
        # XXX Owner is hardcoded, in order to prevent searching for user on the
        # site root.
126 127 128
        allowed_role_set.discard('Owner')

        # XXX make this a method of base ?
129
        local_roles_group_id_group_id = deepcopy(getattr(ob,
130
          '__ac_local_roles_group_id_dict__', {}))
131 132 133 134 135 136 137 138 139 140

        # If we acquire a permission, then we also want to acquire the local
        # roles group ids
        local_roles_container = ob
        while getattr(local_roles_container, 'isRADContent', 0):
          if local_roles_container._getAcquireLocalRoles():
            local_roles_container = local_roles_container.aq_parent
            for role_definition_group, user_and_role_list in \
                getattr(local_roles_container,
                        '__ac_local_roles_group_id_dict__',
141
                        {}).items():
142 143 144 145
              local_roles_group_id_group_id.setdefault(role_definition_group, set()
                ).update(user_and_role_list)
          else:
            break
146

147
        allowed_by_local_roles_group_id = {}
148 149
        allowed_by_local_roles_group_id[''] = allowed_role_set

150 151
        user_role_dict = {}
        user_view_permission_role_dict = {}
152 153 154
        optimized_role_set = set()
        # First parse optimized roles and build optimized_role_set
        for role_definition_group, user_and_role_list in local_roles_group_id_group_id.items():
155 156
          group_allowed_set = allowed_by_local_roles_group_id.setdefault(
            role_definition_group, set())
157
          for user, role in user_and_role_list:
158 159 160 161
            if role in allowed_role_set:
              prefix = 'user:' + user
              group_allowed_set.update((prefix, '%s:%s' % (prefix, role)))
              optimized_role_set.add((user, role))
162

163
        # Then parse other roles
164
        for user, roles in localroles.iteritems():
165
          prefix = 'user:' + user
166
          for role in roles:
167
            if (role in role_dict) and (getUserById(user) is not None):
168 169
              # If role is monovalued, check if key is a user.
              # If not, continue to index it in roles_and_users table.
170 171
              if (user, role) not in optimized_role_set:
                user_role_dict[role] = user # Only add to user_role_dict if not in optimized_role_set (double check)
172 173 174 175
              if role in allowed_role_set:
                user_view_permission_role_dict[role] = user
            elif role in allowed_role_set:
              for group in local_roles_group_id_group_id.get(user, ('', )):
176 177
                group_allowed_set = allowed_by_local_roles_group_id.setdefault(
                  group, set())
178 179 180
                if (user, role) not in optimized_role_set:
                  # add only if not already added to optimized_role_set to avoid polluting indexation table
                  group_allowed_set.update((prefix, '%s:%s' % (prefix, role)))
181

182
        # sort `allowed` principals
183
        sorted_allowed_by_local_roles_group_id = {}
184 185 186 187 188 189 190
        for local_roles_group_id, allowed in \
                allowed_by_local_roles_group_id.items():
          sorted_allowed_by_local_roles_group_id[local_roles_group_id] = tuple(
            sorted(allowed))

        self._cache_result = result = (sorted_allowed_by_local_roles_group_id,
                                       user_role_dict,
191 192
                                       user_view_permission_role_dict)
      return result
193

194 195 196
    def getLocalRolesGroupIdDict(self):
      """Returns a mapping of local roles group id to roles and users with View
      permission.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
      """
      return self._getSecurityParameterList()[0]

    def getAssignee(self):
      """Returns the user ID of the user with 'Assignee' local role on this
      document.

      If there is more than one Assignee local role, the result is undefined.
      """
      return self._getSecurityParameterList()[1].get('Assignee', None)

    def getViewPermissionAssignee(self):
      """Returns the user ID of the user with 'Assignee' local role on this
      document, if the Assignee role has View permission.

      If there is more than one Assignee local role, the result is undefined.
      """
      return self._getSecurityParameterList()[2].get('Assignee', None)

    def getViewPermissionAssignor(self):
      """Returns the user ID of the user with 'Assignor' local role on this
      document, if the Assignor role has View permission.

      If there is more than one Assignor local role, the result is undefined.
      """
      return self._getSecurityParameterList()[2].get('Assignor', None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
223

224 225 226 227 228 229 230 231
    def getViewPermissionAssociate(self):
      """Returns the user ID of the user with 'Associate' local role on this
      document, if the Associate role has View permission.

      If there is more than one Associate local role, the result is undefined.
      """
      return self._getSecurityParameterList()[2].get('Associate', None)

232 233 234 235
    def __repr__(self):
      return '<Products.ERP5Catalog.CatalogTool.IndexableObjectWrapper'\
          ' for %s>' % ('/'.join(self.__ob.getPhysicalPath()), )

236

237
class RelatedBaseCategory(Method):
238 239
    """A Dynamic Method to act as a related key.
    """
240
    def __init__(self, id, strict_membership=0, related=0):
241
      self._id = id
242 243 244 245 246 247 248
      if strict_membership:
        strict = 'AND %(category_table)s.category_strict_membership = 1\n'
      else:
        strict = ''
      # From the point of view of query_table, we are looking up objects...
      if related:
        # ... which have a relation toward us
Vincent Pelletier's avatar
Vincent Pelletier committed
249 250 251 252
        # query_table's uid = category table's category_uid
        query_table_side = 'category_uid'
        # category table's uid = foreign_table's uid
        foreign_side = 'uid'
253 254
      else:
        # ... toward which we have a relation
Vincent Pelletier's avatar
Vincent Pelletier committed
255 256 257 258
        # query_table's uid = category table's uid
        query_table_side = 'uid'
        # category table's category_uid = foreign_table's uid
        foreign_side = 'category_uid'
259 260
      self._template = """\
%%(category_table)s.base_category_uid = %%(base_category_uid)s
Vincent Pelletier's avatar
Vincent Pelletier committed
261
%(strict)sAND %%(foreign_catalog)s.uid = %%(category_table)s.%(foreign_side)s
262 263
%%(RELATED_QUERY_SEPARATOR)s
%%(category_table)s.%(query_table_side)s = %%(query_table)s.uid""" % {
264
          'strict': strict,
Vincent Pelletier's avatar
Vincent Pelletier committed
265 266
          'foreign_side': foreign_side,
          'query_table_side': query_table_side,
267
      }
268 269 270 271 272 273
      self._monotable_template = """\
%%(category_table)s.base_category_uid = %%(base_category_uid)s
%(strict)sAND %%(category_table)s.%(query_table_side)s = %%(query_table)s.uid""" % {
          'strict': strict,
          'query_table_side': query_table_side,
      }
274

275
    def __call__(self, instance, table_0, table_1=None, query_table='catalog',
276
        RELATED_QUERY_SEPARATOR=' AND ', **kw):
277
      """Create the sql code for this related key."""
278 279
      # Note: in normal conditions, our category's uid will not change from
      # one invocation to the next.
280 281 282
      return (
        self._monotable_template if table_1 is None else self._template
      ) % {
283 284
        'base_category_uid': instance.getPortalObject().portal_categories.\
          _getOb(self._id).getUid(),
Vincent Pelletier's avatar
Vincent Pelletier committed
285
        'query_table': query_table,
286
        'category_table': table_0,
Vincent Pelletier's avatar
Vincent Pelletier committed
287
        'foreign_catalog': table_1,
288
        'RELATED_QUERY_SEPARATOR': RELATED_QUERY_SEPARATOR,
289
      }
290

291
class CatalogTool (UniqueObject, ZCatalog, CMFCoreCatalogTool, ActiveObject):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
292 293 294 295 296 297 298
    """
    This is a ZSQLCatalog that filters catalog queries.
    It is based on ZSQLCatalog
    """
    id = 'portal_catalog'
    meta_type = 'ERP5 Catalog'
    security = ClassSecurityInfo()
Aurel's avatar
Aurel committed
299

Mame Coumba Sall's avatar
Mame Coumba Sall committed
300
    default_result_limit = None
301
    default_count_limit = 1
Aurel's avatar
Aurel committed
302

Vincent Pelletier's avatar
Vincent Pelletier committed
303
    manage_options = ({ 'label' : 'Overview', 'action' : 'manage_overview' },
Jean-Paul Smets's avatar
Jean-Paul Smets committed
304 305 306 307 308
                     ) + ZCatalog.manage_options

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

309
    # Explicit Inheritance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
310 311 312
    __url = CMFCoreCatalogTool.__url
    manage_catalogFind = CMFCoreCatalogTool.manage_catalogFind

Vincent Pelletier's avatar
Vincent Pelletier committed
313 314 315
    security.declareProtected(Permissions.ManagePortal
                , 'manage_schema')
    manage_schema = DTMLFile('dtml/manageSchema', globals())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
316

317
    security.declarePublic('getPreferredSQLCatalogId')
Aurel's avatar
Aurel committed
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
    def getPreferredSQLCatalogId(self, id=None):
      """
      Get the SQL Catalog from preference.
      """
      if id is None:
        # Check if we want to use an archive
        #if getattr(aq_base(self.portal_preferences), 'uid', None) is not None:
        archive_path = self.portal_preferences.getPreferredArchive(sql_catalog_id=self.default_sql_catalog_id)
        if archive_path not in ('', None):
          try:
            archive = self.restrictedTraverse(archive_path)
          except KeyError:
            # Do not fail if archive object has been removed,
            # but preference is not up to date
            return None
          if archive is not None:
            catalog_id = archive.getCatalogId()
            if catalog_id not in ('', None):
              return catalog_id
        return None
      else:
        return id
340

341
    def _listAllowedRolesAndUsers(self, user):
342
        # We use ERP5Security PAS based authentication
343 344 345
        try:
          # check for proxy role in stack
          eo = getSecurityManager()._context.stack[-1]
346
          proxy_roles = getattr(eo, '_proxy_roles',None)
347 348 349 350 351
        except IndexError:
          proxy_roles = None
        if proxy_roles:
          # apply proxy roles
          user = eo.getOwner()
Vincent Pelletier's avatar
Vincent Pelletier committed
352
          result = list(proxy_roles)
353
        else:
Vincent Pelletier's avatar
Vincent Pelletier committed
354 355 356
          result = list(user.getRoles())
        result.append('Anonymous')
        result.append('user:%s' % user.getId())
357 358 359
        # deal with groups
        getGroups = getattr(user, 'getGroups', None)
        if getGroups is not None:
360
            groups = list(user.getGroups())
361 362 363 364 365 366
            groups.append('role:Anonymous')
            if 'Authenticated' in result:
                groups.append('role:Authenticated')
            for group in groups:
                result.append('user:%s' % group)
        # end groups
367
        return result
368

Jean-Paul Smets's avatar
Jean-Paul Smets committed
369
    # Schema Management
370
    security.declareProtected(Permissions.ManagePortal, 'editColumn')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
371 372 373 374 375 376 377 378 379 380 381 382 383
    def editColumn(self, column_id, sql_definition, method_id, default_value, REQUEST=None, RESPONSE=None):
      """
        Modifies a schema column of the catalog
      """
      new_schema = []
      for c in self.getIndexList():
        if c.id == index_id:
          new_c = {'id': index_id, 'sql_definition': sql_definition, 'method_id': method_id, 'default_value': default_value}
        else:
          new_c = c
        new_schema.append(new_c)
      self.setColumnList(new_schema)

384
    security.declareProtected(Permissions.ManagePortal, 'setColumnList')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
385 386 387 388 389
    def setColumnList(self, column_list):
      """
      """
      self._sql_schema = column_list

390
    security.declarePublic('getColumnList')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
391 392 393 394 395 396
    def getColumnList(self):
      """
      """
      if not hasattr(self, '_sql_schema'): self._sql_schema = []
      return self._sql_schema

397
    security.declarePublic('getColumn')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
398 399 400 401 402 403 404 405
    def getColumn(self, column_id):
      """
      """
      for c in self.getColumnList():
        if c.id == column_id:
          return c
      return None

406
    security.declareProtected(Permissions.ManagePortal, 'editIndex')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
407 408 409 410 411 412 413 414 415 416 417 418 419
    def editIndex(self, index_id, sql_definition, REQUEST=None, RESPONSE=None):
      """
        Modifies the schema of the catalog
      """
      new_index = []
      for c in self.getIndexList():
        if c.id == index_id:
          new_c = {'id': index_id, 'sql_definition': sql_definition}
        else:
          new_c = c
        new_index.append(new_c)
      self.setIndexList(new_index)

420
    security.declareProtected(Permissions.ManagePortal, 'setIndexList')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421 422 423 424 425
    def setIndexList(self, index_list):
      """
      """
      self._sql_index = index_list

426
    security.declarePublic('getIndexList')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
427 428 429 430 431 432
    def getIndexList(self):
      """
      """
      if not hasattr(self, '_sql_index'): self._sql_index = []
      return self._sql_index

433
    security.declarePublic('getIndex')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
434 435 436 437 438 439 440 441 442
    def getIndex(self, index_id):
      """
      """
      for c in self.getIndexList():
        if c.id == index_id:
          return c
      return None


Vincent Pelletier's avatar
Vincent Pelletier committed
443
    security.declarePublic('getAllowedRolesAndUsers')
444
    def getAllowedRolesAndUsers(self, sql_catalog_id=None, local_roles=None):
445 446
      """
        Return allowed roles and users.
447

448
        This is supposed to be used with Z SQL Methods to check permissions
449
        when you list up documents. It is also able to take into account
450
        a parameter named local_roles so that listed documents only include
451 452
        those documents for which the user (or the group) was
        associated one of the given local roles.
Aurel's avatar
Aurel committed
453

454 455
        The use of getAllowedRolesAndUsers is deprecated, you should use
        getSecurityQuery instead
456 457
      """
      user = _getAuthenticatedUser(self)
458
      user_str = str(user)
459 460
      user_is_superuser = (user == system_user) or (user_str == SUPER_USER) or \
          ('Manager' in user.getRoles())
461
      allowedRolesAndUsers = self._listAllowedRolesAndUsers(user)
462
      role_column_dict = {}
463 464 465
      local_role_column_dict = {}
      catalog = self.getSQLCatalog(sql_catalog_id)
      column_map = catalog.getColumnMap()
466

467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
      # We only consider here the Owner role (since it was not indexed)
      # since some objects may only be visible by their owner
      # which was not indexed
      for role, column_id in catalog.getSQLCatalogRoleKeysList():
        # XXX This should be a list
        if not user_is_superuser:
          try:
            # if called by an executable with proxy roles, we don't use
            # owner, but only roles from the proxy.
            eo = getSecurityManager()._context.stack[-1]
            proxy_roles = getattr(eo, '_proxy_roles', None)
            if not proxy_roles:
              role_column_dict[column_id] = user_str
          except IndexError:
            role_column_dict[column_id] = user_str

483 484
      # Patch for ERP5 by JP Smets in order
      # to implement worklists and search of local roles
485
      if local_roles:
486 487
        local_role_dict = dict(catalog.getSQLCatalogLocalRoleKeysList())
        role_dict = dict(catalog.getSQLCatalogRoleKeysList())
488
        # XXX user is not enough - we should also include groups of the user
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
        new_allowedRolesAndUsers = []
        new_role_column_dict = {}
        # Turn it into a list if necessary according to ';' separator
        if isinstance(local_roles, str):
          local_roles = local_roles.split(';')
        # Local roles now has precedence (since it comes from a WorkList)
        for user_or_group in allowedRolesAndUsers:
          for role in local_roles:
            # Performance optimisation
            if local_role_dict.has_key(role):
              # XXX This should be a list
              # If a given role exists as a column in the catalog,
              # then it is considered as single valued and indexed
              # through the catalog.
              if not user_is_superuser:
504
                # XXX This should be a list
505 506 507 508 509 510 511 512 513
                # which also includes all user groups
                column_id = local_role_dict[role]
                local_role_column_dict[column_id] = user_str
            if role_dict.has_key(role):
              # XXX This should be a list
              # If a given role exists as a column in the catalog,
              # then it is considered as single valued and indexed
              # through the catalog.
              if not user_is_superuser:
514
                # XXX This should be a list
515 516 517 518 519 520 521
                # which also includes all user groups
                column_id = role_dict[role]
                new_role_column_dict[column_id] = user_str
            new_allowedRolesAndUsers.append('%s:%s' % (user_or_group, role))
        if local_role_column_dict == {}:
          allowedRolesAndUsers = new_allowedRolesAndUsers
          role_column_dict = new_role_column_dict
522 523

      return allowedRolesAndUsers, role_column_dict, local_role_column_dict
524

525
    security.declarePublic('getSecurityUidDictAndRoleColumnDict')
526
    def getSecurityUidDictAndRoleColumnDict(self, sql_catalog_id=None, local_roles=None):
527
      """
528 529
        Return a dict of local_roles_group_id -> security Uids and a
        dictionnary containing available role columns.
530 531 532 533

        XXX: This method always uses default catalog. This should not break a
        site as long as security uids are considered consistent among all
        catalogs.
534
      """
535
      allowedRolesAndUsers, role_column_dict, local_role_column_dict = \
536 537 538 539
          self.getAllowedRolesAndUsers(
            sql_catalog_id=sql_catalog_id,
            local_roles=local_roles,
          )
Aurel's avatar
Aurel committed
540
      catalog = self.getSQLCatalog(sql_catalog_id)
541
      method = getattr(catalog, catalog.sql_search_security, None)
542
      if allowedRolesAndUsers:
543
        allowedRolesAndUsers.sort()
544
        cache_key = tuple(allowedRolesAndUsers)
545
        tv = getTransactionalVariable()
546
        try:
547
          security_uid_cache = tv['getSecurityUidDictAndRoleColumnDict']
548
        except KeyError:
549
          security_uid_cache = tv['getSecurityUidDictAndRoleColumnDict'] = {}
550
        try:
551
          security_uid_dict = security_uid_cache[cache_key]
552
        except KeyError:
553 554 555 556
          if method is None:
            warnings.warn("The usage of allowedRolesAndUsers is "\
                          "deprecated. Please update your catalog "\
                          "business template.", DeprecationWarning)
557
            security_uid_dict = {None: [x.security_uid for x in \
558 559 560
              self.unrestrictedSearchResults(
                allowedRolesAndUsers=allowedRolesAndUsers,
                select_expression="security_uid",
561
                group_by_expression="security_uid")] }
562 563
          else:
            # XXX: What with this string transformation ?! Souldn't it be done in
564
            # dtml instead ? ... yes, but how to be bw compatible ?
565
            allowedRolesAndUsers = [sqlquote(role) for role in allowedRolesAndUsers]
566

567
            security_uid_dict = defaultdict(list)
568
            for brain in method(security_roles_list=allowedRolesAndUsers):
569 570
              security_uid_dict[getattr(brain, 'local_roles_group_id', '')
                ].append(brain.uid)
571 572

          security_uid_cache[cache_key] = security_uid_dict
573
      else:
574 575
        security_uid_dict = []
      return security_uid_dict, role_column_dict, local_role_column_dict
576

Vincent Pelletier's avatar
Vincent Pelletier committed
577
    security.declarePublic('getSecurityQuery')
578
    def getSecurityQuery(self, query=None, sql_catalog_id=None, local_roles=None, **kw):
579
      """
580 581 582
        Build a query based on allowed roles or on a list of security_uid
        values. The query takes into account the fact that some roles are
        catalogued with columns.
583
      """
584 585
      user = _getAuthenticatedUser(self)
      user_str = str(user)
586 587
      user_is_superuser = (user == system_user) or (user_str == SUPER_USER) or \
          ('Manager' in user.getRoles())
588 589 590
      if user_is_superuser:
        # We need no security check for super user.
        return query
591
      original_query = query
592
      security_uid_dict, role_column_dict, local_role_column_dict = \
593 594 595 596
          self.getSecurityUidDictAndRoleColumnDict(
            sql_catalog_id=sql_catalog_id,
            local_roles=local_roles,
          )
597 598 599

      role_query = None
      security_uid_query = None
600 601 602 603 604
      if role_column_dict:
        query_list = []
        for key, value in role_column_dict.items():
          new_query = Query(**{key : value})
          query_list.append(new_query)
605
        operator_kw = {'operator': 'OR'}
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
        role_query = ComplexQuery(*query_list, **operator_kw)
      if security_uid_dict:
        catalog_security_uid_groups_columns_dict = \
            self.getSQLCatalog().getSQLCatalogSecurityUidGroupsColumnsDict()

        query_list = []
        for local_roles_group_id, security_uid_list in\
                 security_uid_dict.iteritems():
          assert security_uid_list
          query_list.append(Query(**{
            catalog_security_uid_groups_columns_dict[local_roles_group_id]:
                  security_uid_list,
            'operator': 'IN'}))

        security_uid_query = ComplexQuery(*query_list, operator='OR')

      if role_query:
        if security_uid_query:
          # merge
          query = ComplexQuery(security_uid_query, role_query, operator='OR')
        else:
          query = role_query
      elif security_uid_query:
        query = security_uid_query

631
      else:
Aurel's avatar
Aurel committed
632
        # XXX A false query has to be generated.
633 634 635 636 637 638
        # As it is not possible to use SQLKey for now, pass impossible value
        # on uid (which will be detected as False by MySQL, as it is not in the
        # column range)
        # Do not pass security_uid_list as empty in order to prevent useless
        # overhead
        query = Query(uid=-1)
639 640 641 642 643 644 645 646 647 648

      if local_role_column_dict:
        query_list = []
        for key, value in local_role_column_dict.items():
          new_query = Query(**{key : value})
          query_list.append(new_query)
        operator_kw = {'operator': 'AND'}
        local_role_query = ComplexQuery(*query_list, **operator_kw)
        query = ComplexQuery(query, local_role_query, operator='AND')

649 650 651
      if original_query is not None:
        query = ComplexQuery(query, original_query, operator='AND')
      return query
652

Jean-Paul Smets's avatar
Jean-Paul Smets committed
653
    # searchResults has inherited security assertions.
654
    def searchResults(self, query=None, sql_catalog_id=None, local_roles=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
655
        """
656 657
        Calls ZCatalog.searchResults with extra arguments that
        limit the results to what the user is allowed to see.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
658
        """
659 660 661 662 663
        #if not _checkPermission(
        #    Permissions.AccessInactivePortalContent, self):
        #    now = DateTime()
        #    kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
        #    kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
664

665 666 667 668 669 670
        catalog_id = self.getPreferredSQLCatalogId(sql_catalog_id)
        query = self.getSecurityQuery(
          query=query,
          sql_catalog_id=catalog_id,
          local_roles=local_roles,
        )
671 672
        if query is not None:
          kw['query'] = query
673
        kw.setdefault('limit', self.default_result_limit)
Aurel's avatar
Aurel committed
674 675 676
        # get catalog from preference
        #LOG("searchResult", INFO, catalog_id)
        #         LOG("searchResult", INFO, ZCatalog.searchResults(self, query=query, sql_catalog_id=catalog_id, src__=1, **kw))
677
        return ZCatalog.searchResults(self, sql_catalog_id=catalog_id, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
678 679 680

    __call__ = searchResults

681
    security.declarePrivate('unrestrictedSearchResults')
682
    def unrestrictedSearchResults(self, **kw):
683 684
        """Calls ZSQLCatalog.searchResults directly without restrictions.
        """
685
        kw.setdefault('limit', self.default_result_limit)
686
        return ZCatalog.searchResults(self, **kw)
687

688 689
    # We use a string for permissions here due to circular reference in import
    # from ERP5Type.Permissions
690
    security.declareProtected('Search ZCatalog', 'getResultValue')
691
    def getResultValue(self, **kw):
692 693 694 695
        """
        A method to factor common code used to search a single
        object in the database.
        """
696
        kw.setdefault('limit', 1)
697
        result = self.searchResults(**kw)
698 699 700 701
        try:
          return result[0].getObject()
        except IndexError:
          return None
702 703

    security.declarePrivate('unrestrictedGetResultValue')
704
    def unrestrictedGetResultValue(self, **kw):
705 706 707 708 709
        """
        A method to factor common code used to search a single
        object in the database. Same as getResultValue but without
        taking into account security.
        """
710
        kw.setdefault('limit', 1)
711
        result = self.unrestrictedSearchResults(**kw)
712 713 714 715 716
        try:
          return result[0].getObject()
        except IndexError:
          return None

717
    def countResults(self, query=None, sql_catalog_id=None, local_roles=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
718 719 720 721
        """
            Calls ZCatalog.countResults with extra arguments that
            limit the results to what the user is allowed to see.
        """
722
        # XXX This needs to be set again
723
        #if not _checkPermission(
Vincent Pelletier's avatar
Vincent Pelletier committed
724 725
        #    Permissions.AccessInactivePortalContent, self):
        #    base = aq_base(self)
726 727 728
        #    now = DateTime()
        #    #kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
        #    #kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
729 730 731 732 733 734
        catalog_id = self.getPreferredSQLCatalogId(sql_catalog_id)
        query = self.getSecurityQuery(
          query=query,
          sql_catalog_id=catalog_id,
          local_roles=local_roles,
        )
735 736
        if query is not None:
          kw['query'] = query
737
        kw.setdefault('limit', self.default_count_limit)
Aurel's avatar
Aurel committed
738
        # get catalog from preference
739
        return ZCatalog.countResults(self, sql_catalog_id=catalog_id, **kw)
Aurel's avatar
Aurel committed
740

741 742 743 744 745
    security.declarePrivate('unrestrictedCountResults')
    def unrestrictedCountResults(self, REQUEST=None, **kw):
        """Calls ZSQLCatalog.countResults directly without restrictions.
        """
        return ZCatalog.countResults(self, REQUEST, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
746

747 748 749 750 751 752 753 754 755 756
    def wrapObject(self, object, sql_catalog_id=None, **kw):
        """
          Return a wrapped object for reindexing.
        """
        catalog = self.getSQLCatalog(sql_catalog_id)
        if catalog is None:
          # Nothing to do.
          LOG('wrapObject', 0, 'Warning: catalog is not available')
          return (None, None)

757 758 759
        document_object = aq_inner(object)
        w = IndexableObjectWrapper(document_object)

760
        wf = getToolByName(self, 'portal_workflow')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
761
        if wf is not None:
762
          w.__dict__.update(wf.getCatalogVariablesFor(object))
763

764 765 766
        # Find the parent definition for security
        is_acquired = 0
        while getattr(document_object, 'isRADContent', 0):
Aurel's avatar
Aurel committed
767
          # This condition tells which object should acquire
768 769
          # from their parent.
          # XXX Hardcode _View_Permission for a performance point of view
770 771
          if getattr(aq_base(document_object), '_View_Permission', ACQUIRE_PERMISSION_VALUE) == ACQUIRE_PERMISSION_VALUE\
             and document_object._getAcquireLocalRoles():
772
            document_object = document_object.aq_parent
773 774 775 776
            is_acquired = 1
          else:
            break
        if is_acquired:
777
          document_w = IndexableObjectWrapper(document_object)
778 779 780
        else:
          document_w = w

781 782 783 784
        (security_uid_dict, optimised_roles_and_users) = \
              catalog.getSecurityUidDict(document_w)


785
        w.optimised_roles_and_users = optimised_roles_and_users
786 787 788 789 790 791 792 793 794 795 796

        catalog_security_uid_groups_columns_dict = \
            catalog.getSQLCatalogSecurityUidGroupsColumnsDict()
        default_security_uid_column = catalog_security_uid_groups_columns_dict['']
        for local_roles_group_id, security_uid in security_uid_dict.items():
          catalog_column = catalog_security_uid_groups_columns_dict.get(
                local_roles_group_id, default_security_uid_column)
          setattr(w, catalog_column, security_uid)

        # XXX we should build vars begore building the wrapper

797 798
        predicate_property_dict = catalog.getPredicatePropertyDict(object)
        if predicate_property_dict is not None:
799
          w.predicate_property_dict = predicate_property_dict
800
        else:
Aurel's avatar
Aurel committed
801
          w.predicate_property_dict = {}
802

803 804 805
        (subject_set_uid, optimised_subject_list) = catalog.getSubjectSetUid(document_w)
        w.optimised_subject_list = optimised_subject_list
        w.subject_set_uid = subject_set_uid
806 807

        return ImplicitAcquisitionWrapper(w, aq_parent(document_object))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
808 809

    security.declarePrivate('reindexObject')
810
    def reindexObject(self, object, idxs=None, sql_catalog_id=None,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
811 812 813 814
        '''Update catalog after object data has changed.
        The optional idxs argument is a list of specific indexes
        to update (all of them by default).
        '''
815
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
816
        url = self.__url(object)
817
        self.catalog_object(object, url, idxs=idxs, sql_catalog_id=sql_catalog_id,**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
818

819

820 821
    def catalogObjectList(self, object_list, *args, **kw):
        """Catalog a list of objects"""
822
        m = object_list[0]
823 824 825
        if isinstance(m, GroupedMessage):
          tmp_object_list = [x.object for x in object_list]
          super(CatalogTool, self).catalogObjectList(tmp_object_list, **m.kw)
826
          if tmp_object_list:
827 828
            exc_info = sys.exc_info()
          for x in object_list:
829 830
            if x.object in tmp_object_list:
              x.raised(exc_info)
831
            else:
832
              x.result = None
833 834 835
        else:
          super(CatalogTool, self).catalogObjectList(object_list, *args, **kw)

836 837 838
    security.declarePrivate('uncatalogObjectList')
    def uncatalogObjectList(self, message_list):
      """Uncatalog a list of objects"""
839
      # TODO: this is currently only a placeholder for further optimization
840 841
      try:
        for m in message_list:
842
          m.result = self.unindexObject(*m.args, **m.kw)
843
      except Exception:
844
        m.raised()
845

Jean-Paul Smets's avatar
Jean-Paul Smets committed
846
    security.declarePrivate('unindexObject')
847
    def unindexObject(self, object=None, path=None, uid=None,sql_catalog_id=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
848 849 850
        """
          Remove from catalog.
        """
851
        if path is None and uid is None:
852 853
          if object is None:
            raise TypeError, 'One of uid, path and object parameters must not be None'
854
          path = self.__url(object)
855 856
        if uid is None:
          raise TypeError, "unindexObject supports only uid now"
857
        self.uncatalog_object(path=path, uid=uid, sql_catalog_id=sql_catalog_id)
858

Sebastien Robin's avatar
Sebastien Robin committed
859 860 861 862 863 864 865 866 867
    security.declarePrivate('beforeUnindexObject')
    def beforeUnindexObject(self, object, path=None, uid=None,sql_catalog_id=None):
        """
          Remove from catalog.
        """
        if path is None and uid is None:
          path = self.__url(object)
        self.beforeUncatalogObject(path=path,uid=uid, sql_catalog_id=sql_catalog_id)

868 869 870
    security.declarePrivate('getUrl')
    def getUrl(self, object):
      return self.__url(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
871

Jean-Paul Smets's avatar
Jean-Paul Smets committed
872
    security.declarePrivate('moveObject')
873
    def moveObject(self, object, idxs=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
874 875 876 877 878 879
        """
          Reindex in catalog, taking into account
          peculiarities of ERP5Catalog / ZSQLCatalog

          Useless ??? XXX
        """
880
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
881 882
        url = self.__url(object)
        self.catalog_object(object, url, idxs=idxs, is_object_moved=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
883

884 885 886 887 888 889
    security.declarePublic('getPredicatePropertyDict')
    def getPredicatePropertyDict(self, object):
      """
      Construct a dictionnary with a list of properties
      to catalog into the table predicate
      """
890
      if not object.providesIPredicate():
891 892 893
        return None
      object = object.asPredicate()
      if object is None:
894 895 896 897 898 899 900 901 902 903 904
        return None
      property_dict = {}
      identity_criterion = getattr(object,'_identity_criterion',None)
      range_criterion = getattr(object,'_range_criterion',None)
      if identity_criterion is not None:
        for property, value in identity_criterion.items():
          if value is not None:
            property_dict[property] = value
      if range_criterion is not None:
        for property, (min, max) in range_criterion.items():
          if min is not None:
905
            property_dict['%s_range_min' % property] = min
906
          if max is not None:
907
            property_dict['%s_range_max' % property] = max
908
      property_dict['membership_criterion_category_list'] = object.getMembershipCriterionCategoryList()
909 910
      return property_dict

911
    security.declarePrivate('getDynamicRelatedKeyList')
912
    def getDynamicRelatedKeyList(self, key_list, sql_catalog_id=None):
913
      """
914
      Return the list of dynamic related keys.
915 916
      This method will try to automatically generate new related key
      by looking at the category tree.
917 918

      For exemple it will generate:
919 920 921
      destination_title | category,catalog/title/z_related_destination
      default_destination_title | category,catalog/title/z_related_destination
      strict_destination_title | category,catalog/title/z_related_strict_destination
922 923 924

      strict_ related keys only returns documents which are strictly member of
      the category.
925 926
      """
      related_key_list = []
927 928 929 930
      base_cat_id_set = set(
        self.getPortalObject().portal_categories.getBaseCategoryList()
      )
      base_cat_id_set.discard('parent')
931
      default_string = 'default_'
932
      strict_string = 'strict_'
933
      related_string = 'related_'
934
      column_map = self.getSQLCatalog(sql_catalog_id).getColumnMap()
935
      for key in key_list:
936
        prefix = ''
937
        strict = 0
938 939 940
        if key.startswith(default_string):
          key = key[len(default_string):]
          prefix = default_string
941 942 943 944
        if key.startswith(strict_string):
          strict = 1
          key = key[len(strict_string):]
          prefix = prefix + strict_string
945 946 947
        split_key = key.split('_')
        for i in xrange(len(split_key) - 1, 0, -1):
          expected_base_cat_id = '_'.join(split_key[0:i])
948
          if expected_base_cat_id in base_cat_id_set:
949
            # We have found a base_category
950
            end_key = '_'.join(split_key[i:])
951 952 953
            related = end_key.startswith(related_string)
            if related:
              end_key = end_key[len(related_string):]
954
            # XXX: joining with non-catalog tables is not trivial and requires
955 956 957
            # ZSQLCatalog's ColumnMapper cooperation, so only allow catalog
            # columns.
            if 'catalog' in column_map.get(end_key, ()):
958 959 960 961 962 963 964 965 966 967 968 969 970
              is_uid = end_key == 'uid'
              if is_uid:
                end_key = 'uid' if related else 'category_uid'
              related_key_list.append(
                prefix + key + ' | category' +
                ('' if is_uid else ',catalog') +
                '/' +
                end_key +
                '/z_related_' +
                ('strict_' if strict else '') +
                expected_base_cat_id +
                ('_related' if related else '')
              )
971 972 973 974 975 976

      return related_key_list

    def _aq_dynamic(self, name):
      """
      Automatic related key generation.
977
      Will generate z_related_[base_category_id] if possible
978
      """
Vincent Pelletier's avatar
Vincent Pelletier committed
979 980 981
      result = None
      if name.startswith(DYNAMIC_METHOD_NAME) and \
          not name.endswith(ZOPE_SECURITY_SUFFIX):
982
        kw = {}
Vincent Pelletier's avatar
Vincent Pelletier committed
983
        if name.endswith(RELATED_DYNAMIC_METHOD_NAME):
984 985
          end_offset = RELATED_DYNAMIC_METHOD_NAME_LEN
          kw['related'] = 1
986
        else:
987 988 989 990 991 992 993
          end_offset = None
        if name.startswith(STRICT_DYNAMIC_METHOD_NAME):
          start_offset = STRICT_DYNAMIC_METHOD_NAME_LEN
          kw['strict_membership'] = 1
        else:
          start_offset = DYNAMIC_METHOD_NAME_LEN
        method = RelatedBaseCategory(name[start_offset:end_offset], **kw)
Vincent Pelletier's avatar
Vincent Pelletier committed
994
        setattr(self.__class__, name, method)
995 996 997 998 999 1000
        # This getattr has 2 purposes:
        # - wrap in acquisition context
        #   This alone should be explicitly done rather than through getattr.
        # - wrap (if needed) class attribute on the instance
        #   (for the sake of not relying on current implementation details
        #   "too much")
Vincent Pelletier's avatar
Vincent Pelletier committed
1001 1002
        result = getattr(self, name)
      return result
1003

1004
    def _searchAndActivate(self, method_id, method_args=(), method_kw={},
1005
                           activate_kw={}, min_uid=None, group_kw={}, **kw):
1006 1007
      """Search the catalog and run a script by activity on all found objects

1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
      In order to not generate too many activities, this method limits the
      number of rows to fetch from the catalog, and if the catalog would return
      more results, it resumes by calling itself by activity.

      'activate_kw' is for common activate parameters between all generated
      activities and is usually used for priority and dependencies.

      Common usage is to call this method without 'select_method_id'.
      In this case, found objects are processed via a CMFActivity grouping,
      and this can be configured via 'group_kw', for additional parameters to
      pass to CMFActivity (in particular: 'activity' and 'group_method_*').
      A generic grouping method is used if none is given.
      group_method_cost default to 30 objects per packet.

1022 1023 1024 1025
      'select_method_id', if provided, will be called with partial catalog
      results and returned value will be provided to the callable identified by
      'method_id' (which will no longer be invoked in the context of a given
      document returned by catalog) as first positional argument.
1026
      Use 'packet_size' parameter to limit the size of each group (default: 30).
1027

1028 1029 1030 1031 1032
      'activity_count' parameter is deprecated.
      Its value should be hardcoded because CMFActivity can now handle many
      activities efficiently and any tweak should benefit to everyone.
      However, there are still rare cases where one want to limit the number
      of processing nodes, to minimize latency of high-priority activities.
1033
      """
1034
      catalog_kw = kw.copy()
1035
      select_method_id = catalog_kw.pop('select_method_id', None)
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
      if select_method_id:
        packet_size = catalog_kw.pop('packet_size', 30)
        limit = packet_size * catalog_kw.pop('activity_count', 100)
      elif 'packet_size' in catalog_kw: # BBB
        assert not group_kw, (kw, group_kw)
        packet_size = catalog_kw.pop('packet_size')
        group_method_cost = 1. / packet_size
        limit = packet_size * catalog_kw.pop('activity_count', 100)
      else:
        group_method_cost = group_kw.get('group_method_cost', .034) # 30 objects
        limit = catalog_kw.pop('activity_count', None) or \
          100 * int(ceil(1 / group_method_cost))
1048
      if min_uid:
1049 1050
        catalog_kw['min_uid'] = SimpleQuery(uid=min_uid,
                                            comparison_operator='>')
1051 1052 1053 1054 1055 1056 1057 1058
      if catalog_kw.pop('restricted', False):
        search = self
      else:
        search = self.unrestrictedSearchResults
      r = search(sort_on=(('uid','ascending'),), limit=limit, **catalog_kw)
      result_count = len(r)
      if result_count:
        if result_count == limit:
1059 1060
          next_kw = activate_kw.copy()
          next_kw['priority'] = 1 + next_kw.get('priority', 1)
1061
          self.activate(activity='SQLQueue', **next_kw) \
1062
              ._searchAndActivate(method_id,method_args, method_kw,
1063 1064
                                  activate_kw, r[-1].getUid(),
                                  group_kw=group_kw, **kw)
1065 1066 1067 1068
        if select_method_id:
          portal_activities = self.getPortalObject().portal_activities
          active_portal_activities = portal_activities.activate(
            activity='SQLQueue', **activate_kw)
1069 1070
          r = getattr(portal_activities, select_method_id)(r)
          activate = getattr(active_portal_activities, method_id)
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
          for i in xrange(0, result_count, packet_size):
            activate(r[i:i+packet_size], *method_args, **method_kw)
        else:
          kw = activate_kw.copy()
          kw['activity'] = 'SQLQueue'
          if group_method_cost < 1:
            kw['group_method_cost'] = group_method_cost
            kw['group_method_id'] = None
            kw.update(group_kw)
          for r in r:
            getattr(r.activate(**kw), method_id)(*method_args, **method_kw)
1082 1083 1084 1085

    security.declarePublic('searchAndActivate')
    def searchAndActivate(self, *args, **kw):
      """Restricted version of _searchAndActivate"""
Łukasz Nowak's avatar
Łukasz Nowak committed
1086 1087 1088
      if 'uid' in kw:
        raise TypeError("'uid' cannot be used to select documents as it is "
          "used internally")
1089 1090
      return self._searchAndActivate(restricted=True, *args, **kw)

1091 1092
    security.declareProtected(Permissions.ManagePortal, 'upgradeSchema')
    def upgradeSchema(self, sql_catalog_id=None, src__=0):
1093
      """Upgrade all catalog tables, with ALTER or CREATE queries"""
1094 1095 1096
      catalog = self.getSQLCatalog(sql_catalog_id)
      connection_id = catalog.z_create_catalog.connection_id
      src = []
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
      db = self.getPortalObject()[connection_id]()
      with db.lock():
        for clear_method in catalog.sql_clear_catalog:
          r = catalog[clear_method]._upgradeSchema(
            connection_id, create_if_not_exists=1, src__=1)
          if r:
            src.append(r)
        if not src__:
          for r in src:
            db.query(r)
1107 1108 1109
      return src


1110
InitializeClass(CatalogTool)