CatalogTool.py 34.8 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 29 30
#
# 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.
#
##############################################################################

from Products.CMFCore.CatalogTool import CatalogTool as CMFCoreCatalogTool
from Products.ZSQLCatalog.ZSQLCatalog import ZCatalog
31
from Products.ZSQLCatalog.SQLCatalog import Query, ComplexQuery
32
from Products.ERP5Type import Permissions
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33 34
from AccessControl import ClassSecurityInfo, getSecurityManager
from Products.CMFCore.CatalogTool import IndexableObjectWrapper as CMFCoreIndexableObjectWrapper
35
from Products.CMFCore.utils import UniqueObject, _checkPermission, _getAuthenticatedUser, getToolByName
36
from Products.CMFCore.utils import _mergedLocalRoles
37
from Globals import InitializeClass, DTMLFile, package_home
Jean-Paul Smets's avatar
Jean-Paul Smets committed
38 39
from Acquisition import aq_base, aq_inner, aq_parent
from DateTime.DateTime import DateTime
40
from Products.CMFActivity.ActiveObject import ActiveObject
41
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42 43 44 45 46 47

from AccessControl.PermissionRole import rolesForPermissionOn

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

50 51
from Products.ERP5Security.ERP5UserManager import SUPER_USER

52
import os, time, urllib, warnings
Aurel's avatar
Aurel committed
53
from zLOG import LOG, PROBLEM, INFO
Jean-Paul Smets's avatar
Jean-Paul Smets committed
54

55
SECURITY_USING_NUX_USER_GROUPS, SECURITY_USING_PAS = range(2)
56
try:
57 58
  from Products.PluggableAuthService import PluggableAuthService
  PAS_meta_type = PluggableAuthService.PluggableAuthService.meta_type
59
except ImportError:
60
  PAS_meta_type = ''
61 62 63
try:
  from Products.ERP5Security import mergedLocalRoles as PAS_mergedLocalRoles
except ImportError:
64
  pass
65 66 67 68 69 70

try:
  from Products.NuxUserGroups import UserFolderWithGroups
  NUG_meta_type = UserFolderWithGroups.meta_type
except ImportError:
  NUG_meta_type = ''
71 72 73 74 75
try:
  from Products.NuxUserGroups.CatalogToolWithGroups import mergedLocalRoles
  from Products.NuxUserGroups.CatalogToolWithGroups import _getAllowedRolesAndUsers
except ImportError:
  pass
76

Aurel's avatar
Aurel committed
77 78 79
from Persistence import Persistent
from Acquisition import Implicit

80 81 82 83 84 85 86 87 88
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
89

Jean-Paul Smets's avatar
Jean-Paul Smets committed
90 91 92 93 94 95 96 97 98 99 100
class IndexableObjectWrapper(CMFCoreIndexableObjectWrapper):

    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):
        """
101 102 103 104 105 106 107 108
        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
109 110
        """
        ob = self.__ob
111 112 113 114
        security_product = getSecurityProduct(ob.acl_users)
        withnuxgroups = security_product == SECURITY_USING_NUX_USER_GROUPS
        withpas = security_product == SECURITY_USING_PAS

Jean-Paul Smets's avatar
Jean-Paul Smets committed
115
        allowed = {}
116
        for r in rolesForPermissionOn('View', ob):
117
          allowed[r] = 1
118
        if withnuxgroups:
119
          localroles = mergedLocalRoles(ob, withgroups=1)
120 121
        elif withpas:
          localroles = PAS_mergedLocalRoles(ob)
122 123 124
        else:
          # CMF
          localroles = _mergedLocalRoles(ob)
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
        # 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'
        new_dict = {}
        for key in localroles.keys():
          new_list = []
          remove_list = []
          for role in localroles[key]:
            if role.startswith('-'):
              if not role[1:] in new_list and not role[1:] in remove_list:
                remove_list.append(role[1:])
            elif not role in remove_list:
              new_list.append(role)
          if len(new_list)>0:
            new_dict[key] = new_list
        localroles = new_dict
Jean-Paul Smets's avatar
Jean-Paul Smets committed
143
        for user, roles in localroles.items():
144 145 146 147 148 149
          for role in roles:
            if allowed.has_key(role):
              if withnuxgroups:
                allowed[user] = 1
              else:
                allowed['user:' + user] = 1
150 151 152
            # Added for ERP5 project by JP Smets
            # The reason why we do not want to keep Owner is because we are
            # trying to reduce the number of security definitions
153 154 155 156 157
            # However, this is a bad idea if we start to use Owner role
            # as a kind of bamed Assignee and if we need it for worklists. Therefore
            # we may sometimes catalog the owner user ID whenever the Owner
            # has view permission (see getAllowedRolesAndUsers bellow
            # as well as getViewPermissionOwner method in Base)
158
            if role != 'Owner': 
159 160 161 162 163 164
              if withnuxgroups:
                allowed[user + ':' + role] = 1
              else:
                allowed['user:' + user + ':' + role] = 1
        if allowed.has_key('Owner'):
          del allowed['Owner']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
165 166
        return list(allowed.keys())

167
class RelatedBaseCategory(Method):
168 169
    """A Dynamic Method to act as a related key.
    """
170
    def __init__(self, id,strict_membership=0):
171
      self._id = id
172
      self.strict_membership=strict_membership
173

174
    def __call__(self, instance, table_0, table_1, query_table='catalog', **kw):
175
      """Create the sql code for this related key."""
176 177 178 179
      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))
180 181
      if self.strict_membership:
        append('AND %s.category_strict_membership = 1' % table_0)
182 183 184 185
      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)

186
class CatalogTool (UniqueObject, ZCatalog, CMFCoreCatalogTool, ActiveObject):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
187 188 189 190 191 192 193
    """
    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
194

195 196
    default_result_limit = 1000
    
Jean-Paul Smets's avatar
Jean-Paul Smets committed
197 198 199 200 201 202 203
    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' },
                     ) + ZCatalog.manage_options


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

204
    # Explicit Inheritance
Jean-Paul Smets's avatar
Jean-Paul Smets committed
205 206 207
    __url = CMFCoreCatalogTool.__url
    manage_catalogFind = CMFCoreCatalogTool.manage_catalogFind

208
    security.declareProtected( Permissions.ManagePortal
Jean-Paul Smets's avatar
Jean-Paul Smets committed
209 210 211
                , 'manage_schema' )
    manage_schema = DTMLFile( 'dtml/manageSchema', globals() )

Aurel's avatar
Aurel committed
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
    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
      
235
    security.declareProtected( 'Import/Export objects', 'addDefaultSQLMethods' )
236
    def addDefaultSQLMethods(self, config_id='erp5'):
237 238 239
      """
        Add default SQL methods for a given configuration.
      """
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
      # 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 = []

255 256
      # Common methods - for backward compatibility
      # SQL code distribution is supposed to be business template based nowadays
257
      if config_id.lower() == 'erp5_mysql':
258
        zsql_dirs.append(os.path.join(product_path, 'sql', 'common_mysql'))
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
        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
281
     
282
    security.declareProtected( 'Import/Export objects', 'exportSQLMethods' )
283
    def exportSQLMethods(self, sql_catalog_id=None, config_id='erp5'):
284 285 286 287 288 289 290 291
      """
        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
292

293
      catalog = self.getSQLCatalog(sql_catalog_id)
294 295 296
      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)
297 298 299 300 301
      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',)
302
    
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
      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()
319
          
320 321 322 323 324 325 326 327
      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()
328
        
329
      return msg
330
        
331
    def _listAllowedRolesAndUsers(self, user):
332 333
      security_product = getSecurityProduct(self.acl_users)
      if security_product == SECURITY_USING_PAS:
334
        # We use ERP5Security PAS based authentication
335 336 337
        try:
          # check for proxy role in stack
          eo = getSecurityManager()._context.stack[-1]
338
          proxy_roles = getattr(eo, '_proxy_roles',None)
339 340 341 342 343 344 345 346
        except IndexError:
          proxy_roles = None
        if proxy_roles:
          # apply proxy roles
          user = eo.getOwner()
          result = list( proxy_roles )
        else:
          result = list( user.getRoles() )
347 348
        result.append( 'Anonymous' )
        result.append( 'user:%s' % user.getId() )
349 350 351
        # deal with groups
        getGroups = getattr(user, 'getGroups', None)
        if getGroups is not None:
352
            groups = list(user.getGroups())
353 354 355 356 357 358
            groups.append('role:Anonymous')
            if 'Authenticated' in result:
                groups.append('role:Authenticated')
            for group in groups:
                result.append('user:%s' % group)
        # end groups
359
        return result
360
      elif security_product == SECURITY_USING_NUX_USER_GROUPS:
361
        return _getAllowedRolesAndUsers(user)
362
      else:
363
        return CMFCoreCatalogTool._listAllowedRolesAndUsers(self, user)
364

Jean-Paul Smets's avatar
Jean-Paul Smets committed
365 366 367 368 369 370 371 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
    # 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


431
    security.declarePublic( 'getAllowedRolesAndUsers' )
Aurel's avatar
Aurel committed
432
    def getAllowedRolesAndUsers(self, sql_catalog_id=None, **kw):
433 434
      """
        Return allowed roles and users.
435

436
        This is supposed to be used with Z SQL Methods to check permissions
437
        when you list up documents. It is also able to take into account
438
        a parameter named local_roles so that listed documents only include
439 440
        those documents for which the user (or the group) was
        associated one of the given local roles.
441 442 443
      
        The use of getAllowedRolesAndUsers is deprecated, you should use
        getSecurityQuery instead
444 445
      """
      user = _getAuthenticatedUser(self)
446
      user_str = str(user)
447
      user_is_superuser = (user_str == SUPER_USER)
448
      allowedRolesAndUsers = self._listAllowedRolesAndUsers(user)
449
      role_column_dict = {}
Aurel's avatar
Aurel committed
450
      column_map = self.getSQLCatalog(sql_catalog_id).getColumnMap()
451 452 453 454

      # Patch for ERP5 by JP Smets in order
      # to implement worklists and search of local roles
      if kw.has_key('local_roles'):
455
        local_roles = kw['local_roles']
456
        # XXX user is not enough - we should also include groups of the user
457
        # Only consider local_roles if it is not empty
458
        if local_roles not in (None, '', []): # XXX: Maybe "if local_roles:" is enough.
459
          new_allowedRolesAndUsers = []
460
          # Turn it into a list if necessary according to ';' separator
461
          if isinstance(local_roles, str):
462
            local_roles = local_roles.split(';')
463
          local_roles = [x.lower() for x in local_roles]
464
          # Local roles now has precedence (since it comes from a WorkList)
465
          for user_or_group in allowedRolesAndUsers:
466
            for role in local_roles:
467
              # Performance optimisation
468
              if role in column_map:
469 470 471
                # If a given role exists as a column in the catalog,
                # then it is considered as single valued and indexed
                # through the catalog.
472 473 474
                if not user_is_superuser:
                  role_column_dict[role] = user_str  # XXX This should be a list
                                                     # which also includes all user groups
475
              else:
476
                # Else, we use the standard approach
477
                new_allowedRolesAndUsers.append('%s:%s' % (user_or_group, role))
478
          allowedRolesAndUsers = new_allowedRolesAndUsers
479 480 481 482
      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
483 484
        if 'owner' in column_map:
          if not user_is_superuser:
485 486 487 488 489 490 491
            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)
            except IndexError:
              role_column_dict['owner'] = user_str
492

493
      return allowedRolesAndUsers, role_column_dict
494

Aurel's avatar
Aurel committed
495
    def getSecurityUidListAndRoleColumnDict(self, sql_catalog_id=None, **kw):
496
      """
497 498
        Return a list of security Uids and a dictionnary containing available
        role columns.
499 500 501 502

        XXX: This method always uses default catalog. This should not break a
        site as long as security uids are considered consistent among all
        catalogs.
503 504
      """
      allowedRolesAndUsers, role_column_dict = self.getAllowedRolesAndUsers(**kw)
Aurel's avatar
Aurel committed
505
      catalog = self.getSQLCatalog(sql_catalog_id)
506 507 508 509 510
      method = getattr(catalog, catalog.sql_search_security, None)
      if method is None:
        raise DeprecationWarning, "The usage of allowedRolesAndUsers is "\
                                  "deprecated. Please update your catalog "\
                                  "business template."
511
      if allowedRolesAndUsers:
512
        allowedRolesAndUsers.sort()
513 514 515 516 517 518 519 520 521
        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:
522 523 524 525
          # 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)]
526
          security_uid_cache[cache_key] = security_uid_list
527 528
      else:
        security_uid_list = []
529
      return security_uid_list, role_column_dict
530

531
    security.declarePublic( 'getSecurityQuery' )
Aurel's avatar
Aurel committed
532
    def getSecurityQuery(self, query=None, sql_catalog_id=None, **kw):
533
      """
534 535 536
        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.
537
      """
538
      original_query = query
539
      try:
Aurel's avatar
Aurel committed
540
        security_uid_list, role_column_dict = self.getSecurityUidListAndRoleColumnDict(sql_catalog_id=sql_catalog_id, **kw)
541 542
      except DeprecationWarning, message:
        warnings.warn(message, DeprecationWarning)
Aurel's avatar
Aurel committed
543
        allowedRolesAndUsers, role_column_dict = self.getAllowedRolesAndUsers(sql_catalog_id=sql_catalog_id, **kw)
544 545 546
        if role_column_dict:
          query_list = []
          for key, value in role_column_dict.items():
547
            new_query = Query(**{key : value})
548 549 550 551 552 553 554 555
            query_list.append(new_query)
          operator_kw = {'operator': 'AND'} 
          query = ComplexQuery(*query_list, **operator_kw)
          if allowedRolesAndUsers:
            query = ComplexQuery(Query(allowedRolesAndUsers=allowedRolesAndUsers),
                                 query, operator='OR')
        else:
          query = Query(allowedRolesAndUsers=allowedRolesAndUsers)
556
      else:
557 558 559
        if role_column_dict:
          query_list = []
          for key, value in role_column_dict.items():
560
            new_query = Query(**{key : value})
561 562 563
            query_list.append(new_query)
          operator_kw = {'operator': 'AND'}
          query = ComplexQuery(*query_list, **operator_kw)
564 565 566 567 568 569
          # 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')
570
        else:
Vincent Pelletier's avatar
Vincent Pelletier committed
571
          query = Query(security_uid=security_uid_list, operator='IN')
572 573 574
      if original_query is not None:
        query = ComplexQuery(query, original_query, operator='AND')
      return query
575

Jean-Paul Smets's avatar
Jean-Paul Smets committed
576
    # searchResults has inherited security assertions.
577
    def searchResults(self, query=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
578
        """
579 580
        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
581
        """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
582
        if not _checkPermission(
583
            Permissions.AccessInactivePortalContent, self ):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
584 585 586
            now = DateTime()
            kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
            kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
Jean-Paul Smets's avatar
Jean-Paul Smets committed
587

Aurel's avatar
Aurel committed
588 589
        catalog_id = self.getPreferredSQLCatalogId(kw.pop("sql_catalog_id", None))
        query = self.getSecurityQuery(query=query, sql_catalog_id=catalog_id, **kw)
590
        kw.setdefault('limit', self.default_result_limit)
Aurel's avatar
Aurel committed
591 592 593 594
        # 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
595 596 597

    __call__ = searchResults

598 599 600 601
    security.declarePrivate('unrestrictedSearchResults')
    def unrestrictedSearchResults(self, REQUEST=None, **kw):
        """Calls ZSQLCatalog.searchResults directly without restrictions.
        """
602
        kw.setdefault('limit', self.default_result_limit)
603 604
        return ZCatalog.searchResults(self, REQUEST, **kw)

605 606
    # We use a string for permissions here due to circular reference in import
    # from ERP5Type.Permissions
607 608
    security.declareProtected('Search ZCatalog', 'getResultValue')
    def getResultValue(self, query=None, **kw):
609 610 611 612 613 614 615 616 617
        """
        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
618 619 620 621 622 623 624 625 626 627 628 629 630 631

    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

632
    def countResults(self, query=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
633 634 635 636
        """
            Calls ZCatalog.countResults with extra arguments that
            limit the results to what the user is allowed to see.
        """
637
        # XXX This needs to be set again
638
        #if not _checkPermission(
639
        #    Permissions.AccessInactivePortalContent, self ):
640 641 642 643
        #    base = aq_base( self )
        #    now = DateTime()
        #    #kw[ 'effective' ] = { 'query' : now, 'range' : 'max' }
        #    #kw[ 'expires'   ] = { 'query' : now, 'range' : 'min' }
Aurel's avatar
Aurel committed
644 645
        catalog_id = self.getPreferredSQLCatalogId(kw.pop("sql_catalog_id", None))        
        query = self.getSecurityQuery(query=query, sql_catalog_id=catalog_id, **kw)
646
        kw.setdefault('limit', self.default_result_limit)
Aurel's avatar
Aurel committed
647 648
        # get catalog from preference
        return ZCatalog.countResults(self, query=query, sql_catalog_id=catalog_id, **kw)
649
    
650 651 652 653 654
    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
655

656 657 658 659 660 661 662 663 664 665
    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)

666
        wf = getToolByName(self, 'portal_workflow')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
667
        if wf is not None:
668
          vars = wf.getCatalogVariablesFor(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
669
        else:
670 671
          vars = {}
        #LOG('catalog_object vars', 0, str(vars))
672

Jean-Paul Smets's avatar
Jean-Paul Smets committed
673
        w = IndexableObjectWrapper(vars, object)
674 675 676 677 678 679 680 681 682 683 684 685 686 687 688

        object_path = object.getPhysicalPath()
        portal_path = object.portal_url.getPortalObject().getPhysicalPath()
        if len(object_path) > len(portal_path) + 2 and getattr(object, 'isRADContent', 0):
          # This only applied to ERP5 Contents (not CPS)
          # We are now in the case of a subobject of a root document
          # We want to return single security information
          document_object = aq_inner(object)
          for i in range(0, len(object_path) - len(portal_path) - 2):
            document_object = document_object.aq_parent
          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
689
        #LOG('catalog_object optimised_roles_and_users', 0, str(optimised_roles_and_users))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
690
        # XXX we should build vars begore building the wrapper
Jean-Paul Smets's avatar
Jean-Paul Smets committed
691 692
        if optimised_roles_and_users is not None:
          vars['optimised_roles_and_users'] = optimised_roles_and_users
693
        else:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
694
          vars['optimised_roles_and_users'] = None
695 696 697
        predicate_property_dict = catalog.getPredicatePropertyDict(object)
        if predicate_property_dict is not None:
          vars['predicate_property_dict'] = predicate_property_dict
698
        vars['security_uid'] = security_uid
699 700

        return w
Jean-Paul Smets's avatar
Jean-Paul Smets committed
701 702

    security.declarePrivate('reindexObject')
703
    def reindexObject(self, object, idxs=None, sql_catalog_id=None,**kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
704 705 706 707
        '''Update catalog after object data has changed.
        The optional idxs argument is a list of specific indexes
        to update (all of them by default).
        '''
708
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
709
        url = self.__url(object)
710
        self.catalog_object(object, url, idxs=idxs, sql_catalog_id=sql_catalog_id,**kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
711

712

Jean-Paul Smets's avatar
Jean-Paul Smets committed
713
    security.declarePrivate('unindexObject')
714
    def unindexObject(self, object=None, path=None, uid=None,sql_catalog_id=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
715 716 717
        """
          Remove from catalog.
        """
718 719
        if path is None and uid is None:
          path = self.__url(object)
Vincent Pelletier's avatar
Vincent Pelletier committed
720
        elif object is None:
Vincent Pelletier's avatar
Vincent Pelletier committed
721
          raise TypeError, 'One of uid, path and object parameters must not be None'
722 723
        self.uncatalog_object(path=path,uid=uid, sql_catalog_id=sql_catalog_id)

Sebastien Robin's avatar
Sebastien Robin committed
724 725 726 727 728 729 730 731 732
    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)

733 734 735
    security.declarePrivate('getUrl')
    def getUrl(self, object):
      return self.__url(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
736

Jean-Paul Smets's avatar
Jean-Paul Smets committed
737
    security.declarePrivate('moveObject')
738
    def moveObject(self, object, idxs=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
739 740 741 742 743 744
        """
          Reindex in catalog, taking into account
          peculiarities of ERP5Catalog / ZSQLCatalog

          Useless ??? XXX
        """
745
        if idxs is None: idxs = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
746 747
        url = self.__url(object)
        self.catalog_object(object, url, idxs=idxs, is_object_moved=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
748

749 750 751 752 753 754
    security.declarePublic('getPredicatePropertyDict')
    def getPredicatePropertyDict(self, object):
      """
      Construct a dictionnary with a list of properties
      to catalog into the table predicate
      """
755 756 757 758
      if not getattr(object,'isPredicate',None):
        return None
      object = object.asPredicate()
      if object is None:
759 760 761 762 763 764 765 766 767 768 769
        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:
770
            property_dict['%s_range_min' % property] = min
771
          if max is not None:
772
            property_dict['%s_range_max' % property] = max
773
      property_dict['membership_criterion_category_list'] = object.getMembershipCriterionCategoryList()
774 775
      return property_dict

776
    security.declarePrivate('getDynamicRelatedKeyList')
777
    def getDynamicRelatedKeyList(self, key_list, sql_catalog_id=None):
778
      """
779
      Return the list of dynamic related keys.
780 781
      This method will try to automatically generate new related key
      by looking at the category tree.
782 783 784 785

      For exemple it will generate:
      destination_title | category,catalog/title/z_related_destination
      default_destination_title | category,catalog/title/z_related_destination
786 787 788 789
      strict_destination_title | category,catalog/title/z_related_strict_destination

      strict_ related keys only returns documents which are strictly member of
      the category.
790 791
      """
      related_key_list = []
792
      base_cat_id_list = self.portal_categories.getBaseCategoryDict()
793
      default_string = 'default_'
794
      strict_string = 'strict_'
795
      for key in key_list:
796
        prefix = ''
797
        strict = 0
798 799 800
        if key.startswith(default_string):
          key = key[len(default_string):]
          prefix = default_string
801 802 803 804
        if key.startswith(strict_string):
          strict = 1
          key = key[len(strict_string):]
          prefix = prefix + strict_string
805
        splitted_key = key.split('_')
806 807
        # look from the end of the key from the beginning if we
        # can find 'title', or 'portal_type'...
808 809
        for i in range(1,len(splitted_key))[::-1]:
          expected_base_cat_id = '_'.join(splitted_key[0:i])
810
          if expected_base_cat_id != 'parent' and \
811 812 813
             expected_base_cat_id in base_cat_id_list:
            # We have found a base_category
            end_key = '_'.join(splitted_key[i:])
814
            # accept only some catalog columns
815 816
            if end_key in ('title', 'uid', 'description',
                           'relative_url', 'id', 'portal_type'):
817 818 819 820 821 822
              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(
823 824
                      '%s%s | category,catalog/%s/z_related_%s' %
                      (prefix, key, end_key, expected_base_cat_id))
825 826 827 828 829 830

      return related_key_list

    def _aq_dynamic(self, name):
      """
      Automatic related key generation.
831
      Will generate z_related_[base_category_id] if possible
832 833 834 835
      """
      aq_base_name = getattr(aq_base(self), name, None)
      if aq_base_name == None:
        DYNAMIC_METHOD_NAME = 'z_related_'
836
        STRICT_DYNAMIC_METHOD_NAME = 'z_related_strict_'
837 838 839 840
        method_name_length = len(DYNAMIC_METHOD_NAME)
        zope_security = '__roles__'
        if (name.startswith(DYNAMIC_METHOD_NAME) and \
          (not name.endswith(zope_security))):
841 842
          if name.startswith(STRICT_DYNAMIC_METHOD_NAME):
            base_category_id = name[len(STRICT_DYNAMIC_METHOD_NAME):]
843
            method = RelatedBaseCategory(base_category_id, strict_membership=1)
844 845 846
          else:
            base_category_id = name[len(DYNAMIC_METHOD_NAME):]
            method = RelatedBaseCategory(base_category_id)
847
          setattr(self.__class__, name, method)
848 849 850 851 852
          klass = aq_base(self).__class__
          if hasattr(klass, 'security'):
            from Products.ERP5Type import Permissions as ERP5Permissions
            klass.security.declareProtected(ERP5Permissions.View, name)
          else:
853 854
            LOG('ERP5Catalog', PROBLEM,
                'Security not defined on %s' % klass.__name__)
855 856 857 858
          return getattr(self, name)
        else:
          return aq_base_name
      return aq_base_name
859 860 861



Aurel's avatar
Aurel committed
862 863


864
InitializeClass(CatalogTool)