CatalogTool.py 36.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
from ZODB.POSException import ConflictError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
30 31
from Products.CMFCore.CatalogTool import CatalogTool as CMFCoreCatalogTool
from Products.ZSQLCatalog.ZSQLCatalog import ZCatalog
32
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
33
from Products.ERP5Type import Permissions
34
from Products.ERP5Type.Cache import CachingMethod
Jean-Paul Smets's avatar
Jean-Paul Smets committed
35 36
from AccessControl import ClassSecurityInfo, getSecurityManager
from Products.CMFCore.CatalogTool import IndexableObjectWrapper as CMFCoreIndexableObjectWrapper
37
from Products.CMFCore.utils import UniqueObject, _checkPermission, _getAuthenticatedUser, getToolByName
38
from Products.CMFCore.utils import _mergedLocalRoles
39
from Globals import InitializeClass, DTMLFile, package_home
40
from Acquisition import aq_base, aq_inner, aq_parent, ImplicitAcquisitionWrapper
Jean-Paul Smets's avatar
Jean-Paul Smets committed
41
from DateTime.DateTime import DateTime
42
from Products.CMFActivity.ActiveObject import ActiveObject
43
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Jean-Paul Smets's avatar
Jean-Paul Smets committed
44 45 46 47 48 49

from AccessControl.PermissionRole import rolesForPermissionOn

from Products.PageTemplates.Expressions import SecureModuleImporter
from Products.CMFCore.Expression import Expression
from Products.PageTemplates.Expressions import getEngine
50
from MethodObject import Method
Jean-Paul Smets's avatar
Jean-Paul Smets committed
51

52 53
from Products.ERP5Security.ERP5UserManager import SUPER_USER

54
import os, time, urllib, warnings
55
import sys
56
from zLOG import LOG, PROBLEM, WARNING, INFO
57
import sets
Jean-Paul Smets's avatar
Jean-Paul Smets committed
58

59
SECURITY_USING_NUX_USER_GROUPS, SECURITY_USING_PAS = range(2)
60 61
ACQUIRE_PERMISSION_VALUE = []

62
try:
63 64
  from Products.PluggableAuthService import PluggableAuthService
  PAS_meta_type = PluggableAuthService.PluggableAuthService.meta_type
65
except ImportError:
66
  PAS_meta_type = ''
67 68 69
try:
  from Products.ERP5Security import mergedLocalRoles as PAS_mergedLocalRoles
except ImportError:
70
  pass
71 72 73 74 75 76

try:
  from Products.NuxUserGroups import UserFolderWithGroups
  NUG_meta_type = UserFolderWithGroups.meta_type
except ImportError:
  NUG_meta_type = ''
77 78 79 80 81
try:
  from Products.NuxUserGroups.CatalogToolWithGroups import mergedLocalRoles
  from Products.NuxUserGroups.CatalogToolWithGroups import _getAllowedRolesAndUsers
except ImportError:
  pass
82

Aurel's avatar
Aurel committed
83
from Persistence import Persistent
84
from Acquisition import Implicit
Aurel's avatar
Aurel committed
85

86 87 88 89 90 91 92 93 94
def getSecurityProduct(acl_users):
  """returns the security used by the user folder passed.
  (NuxUserGroup, ERP5Security, or None if anything else).
  """
  if acl_users.meta_type == PAS_meta_type:
    return SECURITY_USING_PAS
  elif acl_users.meta_type == NUG_meta_type:
    return SECURITY_USING_NUX_USER_GROUPS

Aurel's avatar
Aurel committed
95

96
class IndexableObjectWrapper(CMFCoreIndexableObjectWrapper):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
97 98 99 100 101 102 103 104 105 106

    def __setattr__(self, name, value):
      # We need to update the uid during the cataloging process
      if name == 'uid':
        setattr(self.__ob, name, value)
      else:
        self.__dict__[name] = value

    def allowedRolesAndUsers(self):
        """
107 108 109 110 111 112 113 114
        Return a list of roles and users with View permission.
        Used by Portal Catalog to filter out items you're not allowed to see.

        WARNING (XXX): some user base local role association is currently
        being stored (ex. to be determined). This should be prevented or it will
        make the table explode. To analyse the symptoms, look at the
        user_and_roles table. You will find some user:foo values
        which are not necessary.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
115 116
        """
        ob = self.__ob
117 118 119 120
        security_product = getSecurityProduct(ob.acl_users)
        withnuxgroups = security_product == SECURITY_USING_NUX_USER_GROUPS
        withpas = security_product == SECURITY_USING_PAS

121
        if withnuxgroups:
122
          localroles = mergedLocalRoles(ob, withgroups=1)
123 124
        elif withpas:
          localroles = PAS_mergedLocalRoles(ob)
125 126 127
        else:
          # CMF
          localroles = _mergedLocalRoles(ob)
128 129 130 131 132
        # 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'
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
        flat_localroles = {}
        skip_role_set = sets.Set()
        skip_role = skip_role_set.add
        clear_skip_role = skip_role_set.clear
        for key, role_list in localroles.iteritems():
          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:
            flat_localroles[key] = new_role_list
        localroles = flat_localroles
149 150 151 152 153 154
        # 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.
155 156 157
        allowed = sets.Set(rolesForPermissionOn('View', ob))
        allowed.discard('Owner')
        add = allowed.add
158
        for user, roles in localroles.iteritems():
159 160 161 162
          if withnuxgroups:
            prefix = user
          else:
            prefix = 'user:' + user
163
          for role in roles:
164 165 166 167
            if role in allowed:
              add(prefix)
              add(prefix + ':' + role)
        return list(allowed)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
168

169 170 171 172
    def __repr__(self):
      return '<Products.ERP5Catalog.CatalogTool.IndexableObjectWrapper'\
          ' for %s>' % ('/'.join(self.__ob.getPhysicalPath()), )

173

174
class RelatedBaseCategory(Method):
175 176
    """A Dynamic Method to act as a related key.
    """
177
    def __init__(self, id,strict_membership=0):
178
      self._id = id
179
      self.strict_membership=strict_membership
180

181
    def __call__(self, instance, table_0, table_1, query_table='catalog', **kw):
182
      """Create the sql code for this related key."""
183 184 185 186
      base_category_uid = instance.portal_categories._getOb(self._id).getUid()
      expression_list = []
      append = expression_list.append
      append('%s.uid = %s.category_uid' % (table_1,table_0))
187 188
      if self.strict_membership:
        append('AND %s.category_strict_membership = 1' % table_0)
189 190 191 192
      append('AND %s.base_category_uid = %s' % (table_0,base_category_uid))
      append('AND %s.uid = %s.uid' % (table_0,query_table))
      return ' '.join(expression_list)

193
class CatalogTool (UniqueObject, ZCatalog, CMFCoreCatalogTool, ActiveObject):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
194 195 196 197 198 199 200
    """
    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
201

202
    default_result_limit = 1000
203
    default_count_limit = 1
204
    
Vincent Pelletier's avatar
Vincent Pelletier committed
205
    manage_options = ({ 'label' : 'Overview', 'action' : 'manage_overview' },
Jean-Paul Smets's avatar
Jean-Paul Smets committed
206 207 208 209 210
                     ) + ZCatalog.manage_options

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

211
    # Explicit Inheritance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
212 213 214
    __url = CMFCoreCatalogTool.__url
    manage_catalogFind = CMFCoreCatalogTool.manage_catalogFind

Vincent Pelletier's avatar
Vincent Pelletier committed
215 216 217
    security.declareProtected(Permissions.ManagePortal
                , 'manage_schema')
    manage_schema = DTMLFile('dtml/manageSchema', globals())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
218

Aurel's avatar
Aurel committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
    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
      
Vincent Pelletier's avatar
Vincent Pelletier committed
242
    security.declareProtected('Import/Export objects', 'addDefaultSQLMethods')
243
    def addDefaultSQLMethods(self, config_id='erp5'):
244 245 246
      """
        Add default SQL methods for a given configuration.
      """
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
      # For compatibility.
      if config_id.lower() == 'erp5':
        config_id = 'erp5_mysql'
      elif config_id.lower() == 'cps3':
        config_id = 'cps3_mysql'

      addSQLCatalog = self.manage_addProduct['ZSQLCatalog'].manage_addSQLCatalog
      if config_id not in self.objectIds():
        addSQLCatalog(config_id, '')

      catalog = self.getSQLCatalog(config_id)
      addSQLMethod = catalog.manage_addProduct['ZSQLMethods'].manage_addZSQLMethod
      product_path = package_home(globals())
      zsql_dirs = []

262 263
      # Common methods - for backward compatibility
      # SQL code distribution is supposed to be business template based nowadays
264
      if config_id.lower() == 'erp5_mysql':
265
        zsql_dirs.append(os.path.join(product_path, 'sql', 'common_mysql'))
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
        zsql_dirs.append(os.path.join(product_path, 'sql', 'erp5_mysql'))
      elif config_id.lower() == 'cps3_mysql':
        zsql_dirs.append(os.path.join(product_path, 'sql', 'common_mysql'))
        zsql_dirs.append(os.path.join(product_path, 'sql', 'cps3_mysql'))

      # Iterate over the sql directory. Add all sql methods in that directory.
      for directory in zsql_dirs:
        for entry in os.listdir(directory):
          if entry.endswith('.zsql'):
            id = entry[:-5]
            # Create an empty SQL method first.
            addSQLMethod(id = id, title = '', connection_id = '', arguments = '', template = '')
            #LOG('addDefaultSQLMethods', 0, 'catalog = %r' % (catalog.objectIds(),))
            sql_method = getattr(catalog, id)
            # Set parameters of the SQL method from the contents of a .zsql file.
            sql_method.fromFile(os.path.join(directory, entry))
          elif entry == 'properties.xml':
            # This sets up the attributes. The file should be generated by manage_exportProperties.
            catalog.manage_importProperties(os.path.join(directory, entry))

      # Make this the default.
      self.default_sql_catalog_id = config_id
288
     
Vincent Pelletier's avatar
Vincent Pelletier committed
289
    security.declareProtected('Import/Export objects', 'exportSQLMethods')
290
    def exportSQLMethods(self, sql_catalog_id=None, config_id='erp5'):
291 292 293 294 295 296 297 298
      """
        Export SQL methods for a given configuration.
      """
      # For compatibility.
      if config_id.lower() == 'erp5':
        config_id = 'erp5_mysql'
      elif config_id.lower() == 'cps3':
        config_id = 'cps3_mysql'
Jean-Paul Smets's avatar
Jean-Paul Smets committed
299

300
      catalog = self.getSQLCatalog(sql_catalog_id)
301 302 303
      product_path = package_home(globals())
      common_sql_dir = os.path.join(product_path, 'sql', 'common_mysql')
      config_sql_dir = os.path.join(product_path, 'sql', config_id)
304 305 306 307 308
      common_sql_list = ('z0_drop_record', 'z_read_recorded_object_list', 'z_catalog_paths',
                         'z_record_catalog_object', 'z_clear_reserved', 'z_record_uncatalog_object',
                         'z_create_record', 'z_related_security', 'z_delete_recorded_object_list',
                         'z_reserve_uid', 'z_getitem_by_path', 'z_show_columns', 'z_getitem_by_path',
                         'z_show_tables', 'z_getitem_by_uid', 'z_unique_values', 'z_produce_reserved_uid_list',)
309
    
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
      msg = ''
      for id in catalog.objectIds(spec=('Z SQL Method',)):
        if id in common_sql_list:
          d = common_sql_dir
        else:
          d = config_sql_dir
        sql = catalog._getOb(id)
        # First convert the skin to text
        text = sql.manage_FTPget()
        name = os.path.join(d, '%s.zsql' % (id,))
        msg += 'Writing %s\n' % (name,)
        f = open(name, 'w')
        try:
          f.write(text)
        finally:
          f.close()
326
          
327 328 329 330 331 332 333 334
      properties = self.manage_catalogExportProperties(sql_catalog_id=sql_catalog_id)
      name = os.path.join(config_sql_dir, 'properties.xml')
      msg += 'Writing %s\n' % (name,)
      f = open(name, 'w')
      try:
        f.write(properties)
      finally:
        f.close()
335
        
336
      return msg
337
        
338
    def _listAllowedRolesAndUsers(self, user):
339 340
      security_product = getSecurityProduct(self.acl_users)
      if security_product == SECURITY_USING_PAS:
341
        # We use ERP5Security PAS based authentication
342 343 344
        try:
          # check for proxy role in stack
          eo = getSecurityManager()._context.stack[-1]
345
          proxy_roles = getattr(eo, '_proxy_roles',None)
346 347 348 349 350
        except IndexError:
          proxy_roles = None
        if proxy_roles:
          # apply proxy roles
          user = eo.getOwner()
Vincent Pelletier's avatar
Vincent Pelletier committed
351
          result = list(proxy_roles)
352
        else:
Vincent Pelletier's avatar
Vincent Pelletier committed
353 354 355
          result = list(user.getRoles())
        result.append('Anonymous')
        result.append('user:%s' % user.getId())
356 357 358
        # deal with groups
        getGroups = getattr(user, 'getGroups', None)
        if getGroups is not None:
359
            groups = list(user.getGroups())
360 361 362 363 364 365
            groups.append('role:Anonymous')
            if 'Authenticated' in result:
                groups.append('role:Authenticated')
            for group in groups:
                result.append('user:%s' % group)
        # end groups
366
        return result
367
      elif security_product == SECURITY_USING_NUX_USER_GROUPS:
368
        return _getAllowedRolesAndUsers(user)
369
      else:
370
        return CMFCoreCatalogTool._listAllowedRolesAndUsers(self, user)
371

Jean-Paul Smets's avatar
Jean-Paul Smets committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
    # Schema Management
    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)

    def setColumnList(self, column_list):
      """
      """
      self._sql_schema = column_list

    def getColumnList(self):
      """
      """
      if not hasattr(self, '_sql_schema'): self._sql_schema = []
      return self._sql_schema

    def getColumn(self, column_id):
      """
      """
      for c in self.getColumnList():
        if c.id == column_id:
          return c
      return None

    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)

    def setIndexList(self, index_list):
      """
      """
      self._sql_index = index_list

    def getIndexList(self):
      """
      """
      if not hasattr(self, '_sql_index'): self._sql_index = []
      return self._sql_index

    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
438
    security.declarePublic('getAllowedRolesAndUsers')
Aurel's avatar
Aurel committed
439
    def getAllowedRolesAndUsers(self, sql_catalog_id=None, **kw):
440 441
      """
        Return allowed roles and users.
442

443
        This is supposed to be used with Z SQL Methods to check permissions
444
        when you list up documents. It is also able to take into account
445
        a parameter named local_roles so that listed documents only include
446 447
        those documents for which the user (or the group) was
        associated one of the given local roles.
448 449 450
      
        The use of getAllowedRolesAndUsers is deprecated, you should use
        getSecurityQuery instead
451 452
      """
      user = _getAuthenticatedUser(self)
453
      user_str = str(user)
454
      user_is_superuser = (user_str == SUPER_USER)
455
      allowedRolesAndUsers = self._listAllowedRolesAndUsers(user)
456
      role_column_dict = {}
457 458 459
      local_role_column_dict = {}
      catalog = self.getSQLCatalog(sql_catalog_id)
      column_map = catalog.getColumnMap()
460 461 462 463

      # Patch for ERP5 by JP Smets in order
      # to implement worklists and search of local roles
      if kw.has_key('local_roles'):
464
        local_roles = kw['local_roles']
465 466
        local_role_dict = dict(catalog.getSQLCatalogLocalRoleKeysList())
        role_dict = dict(catalog.getSQLCatalogRoleKeysList())
467
        # XXX user is not enough - we should also include groups of the user
468
        # Only consider local_roles if it is not empty
469
        if local_roles not in (None, '', []): # XXX: Maybe "if local_roles:" is enough.
470
          new_allowedRolesAndUsers = []
471
          # Turn it into a list if necessary according to ';' separator
472
          if isinstance(local_roles, str):
473 474
            local_roles = local_roles.split(';')
          # Local roles now has precedence (since it comes from a WorkList)
475
          for user_or_group in allowedRolesAndUsers:
476
            for role in local_roles:
477
              # Performance optimisation
478 479
              if local_role_dict.has_key(role):
                # XXX This should be a list
480 481 482
                # If a given role exists as a column in the catalog,
                # then it is considered as single valued and indexed
                # through the catalog.
483
                if not user_is_superuser:
484 485 486 487 488 489 490 491 492 493 494 495 496 497
                  # XXX This should be a list
                  # 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:
                  # XXX This should be a list
                  # which also includes all user groups
                  column_id = role_dict[role]
                  role_column_dict[column_id] = user_str
498
              else:
499
                # Else, we use the standard approach
500
                new_allowedRolesAndUsers.append('%s:%s' % (user_or_group, role))
501 502 503
          if local_role_column_dict == {}:
            allowedRolesAndUsers = new_allowedRolesAndUsers

504 505 506 507
      else:
        # 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
508 509
        for role, column_id in catalog.getSQLCatalogRoleKeysList():
          # XXX This should be a list
510
          if not user_is_superuser:
511 512 513 514
            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]
515 516
              proxy_roles = getattr(eo, '_proxy_roles', None)
              if not proxy_roles:
517
                role_column_dict[column_id] = user_str
518
            except IndexError:
519
              role_column_dict[column_id] = user_str
520

521
      return allowedRolesAndUsers, role_column_dict, local_role_column_dict
522

Aurel's avatar
Aurel committed
523
    def getSecurityUidListAndRoleColumnDict(self, sql_catalog_id=None, **kw):
524
      """
525 526
        Return a list of security Uids and a dictionnary containing available
        role columns.
527 528 529 530

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

Vincent Pelletier's avatar
Vincent Pelletier committed
566
    security.declarePublic('getSecurityQuery')
Aurel's avatar
Aurel committed
567
    def getSecurityQuery(self, query=None, sql_catalog_id=None, **kw):
568
      """
569 570 571
        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.
572
      """
573
      original_query = query
574 575 576
      security_uid_list, role_column_dict, local_role_column_dict = \
          self.getSecurityUidListAndRoleColumnDict(
              sql_catalog_id=sql_catalog_id, **kw)
577 578 579 580 581 582 583 584 585 586 587 588 589
      if role_column_dict:
        query_list = []
        for key, value in role_column_dict.items():
          new_query = Query(**{key : value})
          query_list.append(new_query)
        operator_kw = {'operator': 'AND'}
        query = ComplexQuery(*query_list, **operator_kw)
        # If security_uid_list is empty, adding it to criterions will only
        # result in "false or [...]", so avoid useless overhead by not
        # adding it at all.
        if security_uid_list:
          query = ComplexQuery(Query(security_uid=security_uid_list, operator='IN'),
                               query, operator='OR')
590
      else:
591
        query = Query(security_uid=security_uid_list, operator='IN')
592 593 594 595 596 597 598 599 600 601

      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')

602 603 604
      if original_query is not None:
        query = ComplexQuery(query, original_query, operator='AND')
      return query
605

Jean-Paul Smets's avatar
Jean-Paul Smets committed
606
    # searchResults has inherited security assertions.
607
    def searchResults(self, query=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
608
        """
609 610
        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
611
        """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
612
        if not _checkPermission(
Vincent Pelletier's avatar
Vincent Pelletier committed
613
            Permissions.AccessInactivePortalContent, self):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
614 615 616
            now = DateTime()
            kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
            kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
617

Aurel's avatar
Aurel committed
618 619
        catalog_id = self.getPreferredSQLCatalogId(kw.pop("sql_catalog_id", None))
        query = self.getSecurityQuery(query=query, sql_catalog_id=catalog_id, **kw)
620
        kw.setdefault('limit', self.default_result_limit)
Aurel's avatar
Aurel committed
621 622 623 624
        # 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))
        return ZCatalog.searchResults(self, query=query, sql_catalog_id=catalog_id, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
625 626 627

    __call__ = searchResults

628
    security.declarePrivate('beforeCatalogClear')
629 630 631 632 633
    def beforeCatalogClear(self):
      """
      Clears the catalog by calling a list of methods
      """
      id_tool = self.getPortalObject().portal_ids
634 635 636 637 638 639 640 641 642 643 644 645 646
      try:
        # Call generate new id in order to store the last id into
        # the zodb
        id_tool.generateNewLengthId(id_group='portal_activity')
        id_tool.generateNewLengthId(id_group='portal_activity_queue')
      except ConflictError:
        raise
      except:
        # Swallow exceptions to allow catalog clear to happen.
        # For example, is portal_ids table does not exist and exception will
        # be thrown by portal_id methods.
        LOG('ERP5Catalog.beforeCatalogClear', WARNING,
            'beforeCatalogClear failed', error=sys.exc_info())
647

648 649 650 651
    security.declarePrivate('unrestrictedSearchResults')
    def unrestrictedSearchResults(self, REQUEST=None, **kw):
        """Calls ZSQLCatalog.searchResults directly without restrictions.
        """
652
        kw.setdefault('limit', self.default_result_limit)
653 654
        return ZCatalog.searchResults(self, REQUEST, **kw)

655 656
    # We use a string for permissions here due to circular reference in import
    # from ERP5Type.Permissions
657 658
    security.declareProtected('Search ZCatalog', 'getResultValue')
    def getResultValue(self, query=None, **kw):
659 660 661 662 663 664 665 666 667
        """
        A method to factor common code used to search a single
        object in the database.
        """
        result = self.searchResults(query=query, **kw)
        try:
          return result[0].getObject()
        except IndexError:
          return None
668 669 670 671 672 673 674 675 676 677 678 679 680 681

    security.declarePrivate('unrestrictedGetResultValue')
    def unrestrictedGetResultValue(self, query=None, **kw):
        """
        A method to factor common code used to search a single
        object in the database. Same as getResultValue but without
        taking into account security.
        """
        result = self.unrestrictedSearchResults(query=query, **kw)
        try:
          return result[0].getObject()
        except IndexError:
          return None

682
    def countResults(self, query=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
683 684 685 686
        """
            Calls ZCatalog.countResults with extra arguments that
            limit the results to what the user is allowed to see.
        """
687
        # XXX This needs to be set again
688
        #if not _checkPermission(
Vincent Pelletier's avatar
Vincent Pelletier committed
689 690
        #    Permissions.AccessInactivePortalContent, self):
        #    base = aq_base(self)
691 692 693
        #    now = DateTime()
        #    #kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
        #    #kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
Aurel's avatar
Aurel committed
694 695
        catalog_id = self.getPreferredSQLCatalogId(kw.pop("sql_catalog_id", None))        
        query = self.getSecurityQuery(query=query, sql_catalog_id=catalog_id, **kw)
696
        kw.setdefault('limit', self.default_count_limit)
Aurel's avatar
Aurel committed
697 698
        # get catalog from preference
        return ZCatalog.countResults(self, query=query, sql_catalog_id=catalog_id, **kw)
699
    
700 701 702 703 704
    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
705

706 707 708 709 710 711 712 713 714 715
    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)

716
        wf = getToolByName(self, 'portal_workflow')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
717
        if wf is not None:
718
          vars = wf.getCatalogVariablesFor(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
719
        else:
720 721
          vars = {}
        #LOG('catalog_object vars', 0, str(vars))
722

723 724 725 726 727
        # Find the parent definition for security
        document_object = aq_inner(object)
        is_acquired = 0
        w = IndexableObjectWrapper(vars, document_object)
        while getattr(document_object, 'isRADContent', 0):
728 729 730
          # This condition tells which object should acquire 
          # from their parent.
          # XXX Hardcode _View_Permission for a performance point of view
731 732
          if getattr(aq_base(document_object), '_View_Permission', ACQUIRE_PERMISSION_VALUE) == ACQUIRE_PERMISSION_VALUE\
             and document_object._getAcquireLocalRoles():
733
            document_object = document_object.aq_parent
734 735 736 737
            is_acquired = 1
          else:
            break
        if is_acquired:
738 739 740 741 742
          document_w = IndexableObjectWrapper({}, document_object)
        else:
          document_w = w

        (security_uid, optimised_roles_and_users) = catalog.getSecurityUid(document_w)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
743
        #LOG('catalog_object optimised_roles_and_users', 0, str(optimised_roles_and_users))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
744
        # XXX we should build vars begore building the wrapper
Jean-Paul Smets's avatar
Jean-Paul Smets committed
745 746
        if optimised_roles_and_users is not None:
          vars['optimised_roles_and_users'] = optimised_roles_and_users
747
        else:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
748
          vars['optimised_roles_and_users'] = None
749 750 751
        predicate_property_dict = catalog.getPredicatePropertyDict(object)
        if predicate_property_dict is not None:
          vars['predicate_property_dict'] = predicate_property_dict
752
        vars['security_uid'] = security_uid
753 754

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

    security.declarePrivate('reindexObject')
757
    def reindexObject(self, object, idxs=None, sql_catalog_id=None,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
758 759 760 761
        '''Update catalog after object data has changed.
        The optional idxs argument is a list of specific indexes
        to update (all of them by default).
        '''
762
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
763
        url = self.__url(object)
764
        self.catalog_object(object, url, idxs=idxs, sql_catalog_id=sql_catalog_id,**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
765

766

Jean-Paul Smets's avatar
Jean-Paul Smets committed
767
    security.declarePrivate('unindexObject')
768
    def unindexObject(self, object=None, path=None, uid=None,sql_catalog_id=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
769 770 771
        """
          Remove from catalog.
        """
772
        if path is None and uid is None:
773 774
          if object is None:
            raise TypeError, 'One of uid, path and object parameters must not be None'
775
          path = self.__url(object)
776 777
        if uid is None:
          raise TypeError, "unindexObject supports only uid now"
778
        self.uncatalog_object(path=path, uid=uid, sql_catalog_id=sql_catalog_id)
779

Sebastien Robin's avatar
Sebastien Robin committed
780 781 782 783 784 785 786 787 788
    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)

789 790 791
    security.declarePrivate('getUrl')
    def getUrl(self, object):
      return self.__url(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
792

Jean-Paul Smets's avatar
Jean-Paul Smets committed
793
    security.declarePrivate('moveObject')
794
    def moveObject(self, object, idxs=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
795 796 797 798 799 800
        """
          Reindex in catalog, taking into account
          peculiarities of ERP5Catalog / ZSQLCatalog

          Useless ??? XXX
        """
801
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
802 803
        url = self.__url(object)
        self.catalog_object(object, url, idxs=idxs, is_object_moved=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
804

805 806 807 808 809 810
    security.declarePublic('getPredicatePropertyDict')
    def getPredicatePropertyDict(self, object):
      """
      Construct a dictionnary with a list of properties
      to catalog into the table predicate
      """
811 812 813 814
      if not getattr(object,'isPredicate',None):
        return None
      object = object.asPredicate()
      if object is None:
815 816 817 818 819 820 821 822 823 824 825
        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:
826
            property_dict['%s_range_min' % property] = min
827
          if max is not None:
828
            property_dict['%s_range_max' % property] = max
829
      property_dict['membership_criterion_category_list'] = object.getMembershipCriterionCategoryList()
830 831
      return property_dict

832
    security.declarePrivate('getDynamicRelatedKeyList')
833
    def getDynamicRelatedKeyList(self, key_list, sql_catalog_id=None):
834
      """
835
      Return the list of dynamic related keys.
836 837
      This method will try to automatically generate new related key
      by looking at the category tree.
838 839 840 841

      For exemple it will generate:
      destination_title | category,catalog/title/z_related_destination
      default_destination_title | category,catalog/title/z_related_destination
842 843 844 845
      strict_destination_title | category,catalog/title/z_related_strict_destination

      strict_ related keys only returns documents which are strictly member of
      the category.
846 847
      """
      related_key_list = []
848
      base_cat_id_list = self.portal_categories.getBaseCategoryDict()
849
      default_string = 'default_'
850
      strict_string = 'strict_'
851
      for key in key_list:
852
        prefix = ''
853
        strict = 0
854 855 856
        if key.startswith(default_string):
          key = key[len(default_string):]
          prefix = default_string
857 858 859 860
        if key.startswith(strict_string):
          strict = 1
          key = key[len(strict_string):]
          prefix = prefix + strict_string
861
        splitted_key = key.split('_')
862 863
        # look from the end of the key from the beginning if we
        # can find 'title', or 'portal_type'...
864 865
        for i in range(1,len(splitted_key))[::-1]:
          expected_base_cat_id = '_'.join(splitted_key[0:i])
866
          if expected_base_cat_id != 'parent' and \
867 868 869
             expected_base_cat_id in base_cat_id_list:
            # We have found a base_category
            end_key = '_'.join(splitted_key[i:])
870
            # accept only some catalog columns
871
            if end_key in ('title', 'uid', 'description', 'reference',
872
                           'relative_url', 'id', 'portal_type'):
873 874 875 876 877 878
              if strict:
                related_key_list.append(
                      '%s%s | category,catalog/%s/z_related_strict_%s' %
                      (prefix, key, end_key, expected_base_cat_id))
              else:
                related_key_list.append(
879 880
                      '%s%s | category,catalog/%s/z_related_%s' %
                      (prefix, key, end_key, expected_base_cat_id))
881 882 883 884 885 886

      return related_key_list

    def _aq_dynamic(self, name):
      """
      Automatic related key generation.
887
      Will generate z_related_[base_category_id] if possible
888 889 890 891
      """
      aq_base_name = getattr(aq_base(self), name, None)
      if aq_base_name == None:
        DYNAMIC_METHOD_NAME = 'z_related_'
892
        STRICT_DYNAMIC_METHOD_NAME = 'z_related_strict_'
893 894 895 896
        method_name_length = len(DYNAMIC_METHOD_NAME)
        zope_security = '__roles__'
        if (name.startswith(DYNAMIC_METHOD_NAME) and \
          (not name.endswith(zope_security))):
897 898
          if name.startswith(STRICT_DYNAMIC_METHOD_NAME):
            base_category_id = name[len(STRICT_DYNAMIC_METHOD_NAME):]
899
            method = RelatedBaseCategory(base_category_id, strict_membership=1)
900 901 902
          else:
            base_category_id = name[len(DYNAMIC_METHOD_NAME):]
            method = RelatedBaseCategory(base_category_id)
903
          setattr(self.__class__, name, method)
904 905 906 907 908
          klass = aq_base(self).__class__
          if hasattr(klass, 'security'):
            from Products.ERP5Type import Permissions as ERP5Permissions
            klass.security.declareProtected(ERP5Permissions.View, name)
          else:
909 910
            LOG('ERP5Catalog', PROBLEM,
                'Security not defined on %s' % klass.__name__)
911 912 913 914
          return getattr(self, name)
        else:
          return aq_base_name
      return aq_base_name
915

916
InitializeClass(CatalogTool)