BusinessTemplate.py 238 KB
Newer Older
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
2 3 4
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
5
#                    Jean-Paul Smets-Solanes <jp@nexedi.com>
Jean-Paul Smets's avatar
Jean-Paul Smets committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# 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.
#
##############################################################################

30
import fnmatch, gc, glob, imp, os, re, shutil, sys, time, tarfile
31
from collections import defaultdict
32
from Shared.DC.ZRDB import Aqueduct
33
from Shared.DC.ZRDB.Connection import Connection as RDBConnection
34
from Products.ERP5Type.DiffUtils import DiffFile
35
from Products.ERP5Type.Globals import Persistent, PersistentMapping
36
from Acquisition import Implicit, aq_base, aq_inner, aq_parent
37
from AccessControl import ClassSecurityInfo, Unauthorized, getSecurityManager
38
from AccessControl.SecurityInfo import ModuleSecurityInfo
Jean-Paul Smets's avatar
Jean-Paul Smets committed
39
from Products.CMFCore.utils import getToolByName
40
from Products.PythonScripts.PythonScript import PythonScript
41
from Products.ERP5Type.Accessor.Constant import PropertyGetter as ConstantGetter
42
from Products.ERP5Type.Cache import transactional_cached
43
from Products.ERP5Type.Message import translateString
Nicolas Dumazet's avatar
Nicolas Dumazet committed
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
from Products.ERP5Type.Utils import readLocalDocument, \
                                    writeLocalDocument, \
                                    importLocalDocument, \
                                    removeLocalDocument
from Products.ERP5Type.Utils import readLocalPropertySheet, \
                                    writeLocalPropertySheet, \
                                    importLocalPropertySheet, \
                                    removeLocalPropertySheet
from Products.ERP5Type.Utils import readLocalConstraint, \
                                    writeLocalConstraint, \
                                    importLocalConstraint, \
                                    removeLocalConstraint
from Products.ERP5Type.Utils import readLocalExtension, \
                                    writeLocalExtension, \
                                    removeLocalExtension
from Products.ERP5Type.Utils import readLocalTest, \
                                    writeLocalTest, \
                                    removeLocalTest
62
from Products.ERP5Type.Utils import convertToUpperCase
63
from Products.ERP5Type import Permissions, PropertySheet, interfaces
Jean-Paul Smets's avatar
Jean-Paul Smets committed
64
from Products.ERP5Type.XMLObject import XMLObject
65
from Products.ERP5Type.dynamic.lazy_class import ERP5BaseBroken
66
from Products.ERP5Type.dynamic.portal_type_class import synchronizeDynamicModules
67
from Products.ERP5Type.Core.PropertySheet import PropertySheet as PropertySheetDocument
68
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
Aurel's avatar
Aurel committed
69
from OFS.Traversable import NotFound
70
from OFS import SimpleItem, XMLExportImport
71
from cStringIO import StringIO
Aurel's avatar
Aurel committed
72
from copy import deepcopy
73
from zExceptions import BadRequest
Aurel's avatar
Aurel committed
74
import OFS.XMLExportImport
75
from Products.ERP5Type.patches.ppml import importXML
Aurel's avatar
Aurel committed
76
customImporters={
77
    XMLExportImport.magic: importXML,
Aurel's avatar
Aurel committed
78
    }
79
from Products.ERP5Type.patches.WorkflowTool import WorkflowHistoryList
80
from zLOG import LOG, WARNING, INFO
81
from warnings import warn
82
from lxml.etree import parse
83
from xml.sax.saxutils import escape
84
from Products.CMFCore.Expression import Expression
85
from urllib import quote, unquote
86
from difflib import unified_diff
87
import posixpath
Julien Muchembled's avatar
Julien Muchembled committed
88
import transaction
89

90
import threading
91
from ZODB.broken import Broken
92 93
from Products.ERP5.genbt5list import BusinessTemplateRevision, \
  item_name_list, item_set
94 95 96 97 98

CACHE_DATABASE_PATH = None
try:
  if int(os.getenv('ERP5_BT5_CACHE', 0)):
    from App.config import getConfiguration
99
    import gdbm
100 101 102 103 104 105
    instancehome = getConfiguration().instancehome
    CACHE_DATABASE_PATH = os.path.join(instancehome, 'bt5cache.db')
except TypeError:
  pass
cache_database = threading.local()

106 107
# those attributes from CatalogMethodTemplateItem are kept for
# backward compatibility
108 109
catalog_method_list = ('_is_catalog_list_method_archive',
                       '_is_uncatalog_method_archive',
110 111
                       '_is_clear_method_archive',
                       '_is_filtered_archive',)
112

113 114
catalog_method_filter_list = ('_filter_expression_archive',
                              '_filter_expression_instance_archive',
115
                              '_filter_expression_cache_key_archive',
116
                              '_filter_type_archive',)
117

118
INSTALLED_BT_FOR_DIFF = 'installed_bt_for_diff'
119
_MARKER = []
120

121 122 123 124 125 126
def _getCatalog(acquisition_context):
  """
    Return the id of the SQLCatalog which correspond to the current BT.
  """
  catalog_method_id_list = acquisition_context.getTemplateCatalogMethodIdList()
  if len(catalog_method_id_list) == 0:
127 128 129 130
    try:
      return acquisition_context.getPortalObject().portal_catalog.objectIds('SQLCatalog')[0]
    except IndexError:
      return None
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
  catalog_method_id = catalog_method_id_list[0]
  return catalog_method_id.split('/')[0]

def _getCatalogValue(acquisition_context):
  """
    Returns the catalog object which correspond to the ZSQLMethods
    stored/to store in the business template.

    NB: acquisition_context must make possible to reach portal object
        and getTemplateCatalogMethodIdList.
  """
  catalog_id = _getCatalog(acquisition_context)
  if catalog_id is None:
    return None
  try:
    return acquisition_context.getPortalObject().portal_catalog[catalog_id]
  except KeyError:
    return None

150 151 152 153 154
def _recursiveRemoveUid(obj):
  """Recusivly set uid to None, to prevent (un)indexing.
  This is used to prevent unindexing real objects when we delete subobjects on
  a copy of this object.
  """
155
  if getattr(aq_base(obj), 'uid', _MARKER) is not _MARKER:
156
    obj.uid = None
157 158 159
  for subobj in obj.objectValues():
    _recursiveRemoveUid(subobj)

160
def removeAll(entry):
161 162 163
  warn('removeAll is deprecated; use shutil.rmtree instead.',
       DeprecationWarning)
  shutil.rmtree(entry, True)
164

165 166 167 168 169 170 171
def getChainByType(context):
  """
  This is used in order to construct the full list
  of mapping between type and list of workflow associated
  This is only useful in order to use
  portal_workflow.manage_changeWorkflows
  """
172
  pw = context.getPortalObject().portal_workflow
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
  cbt = pw._chains_by_type
  ti = pw._listTypeInfo()
  types_info = []
  for t in ti:
    id = t.getId()
    title = t.Title()
    if title == id:
      title = None
    if cbt is not None and cbt.has_key(id):
      chain = ', '.join(cbt[id])
    else:
      chain = '(Default)'
    types_info.append({'id': id,
                      'title': title,
                      'chain': chain})
  new_dict = {}
  for item in types_info:
    new_dict['chain_%s' % item['id']] = item['chain']
  default_chain=', '.join(pw._default_chain)
  return (default_chain, new_dict)

194 195 196 197 198 199 200 201 202 203 204 205 206 207
def fixZSQLMethod(portal, method):
  """Make sure the ZSQLMethod uses a valid connection.
  """
  if not isinstance(getattr(portal, method.connection_id, None),
                      RDBConnection):
    # if not valid, we assign to the first valid connection found
    sql_connection_list = portal.objectIds(
                          spec=('Z MySQL Database Connection',))
    if (method.connection_id not in sql_connection_list) and \
       (len(sql_connection_list) != 0):
      LOG('BusinessTemplate', WARNING,
          'connection_id for Z SQL Method %s is invalid, using %s' % (
                    method.getId(), sql_connection_list[0]))
      method.connection_id = sql_connection_list[0]
208 209 210
  # recompile the method
  method._arg = Aqueduct.parse(method.arguments_src)
  method.template = method.template_class(method.src)
211

212 213
def registerSkinFolder(skin_tool, skin_folder):
  request = skin_tool.REQUEST
214 215 216
  # XXX: Getting parameter from request instead of dialog is bad
  # XXX: This is even non consistent with rest of parameters selected by user
  #      (like update_translation or update_catalog)
217 218
  register_skin_selection = request.get('your_register_skin_selection', 1)
  reorder_skin_selection = request.get('your_reorder_skin_selection', 1)
219 220
  skin_layer_list = request.get('your_skin_layer_list',
                                skin_tool.getSkinSelections())
221 222 223 224 225

  skin_folder_id = skin_folder.getId()

  try:
    skin_selection_list = skin_folder.getProperty(
226
                 'business_template_registered_skin_selections',
227 228 229 230 231
                 skin_tool.getSkinSelections()
                 )
  except AttributeError:
    skin_selection_list = skin_tool.getSkinSelections()

232 233 234
  if isinstance(skin_selection_list, basestring):
    skin_selection_list = skin_selection_list.split()

235 236 237 238 239 240 241
  def skin_sort_key(skin_folder_id):
    obj = skin_tool._getOb(skin_folder_id, None)
    if obj is None:
      return 0, skin_folder_id
    return -obj.getProperty('business_template_skin_layer_priority',
      obj.meta_type == 'Filesystem Directory View' and -1 or 0), skin_folder_id

242 243 244 245 246
  for skin_name in skin_selection_list:

    if (skin_name not in skin_tool.getSkinSelections()) and \
                                          register_skin_selection:
      createSkinSelection(skin_tool, skin_name)
247
      # add newly created skins to list of skins we care for
248 249 250
      skin_layer_list.append(skin_name)

    selection = skin_tool.getSkinPath(skin_name) or ''
251
    selection_list = selection.split(',')
252
    if (skin_folder_id not in selection_list):
253
      selection_list.insert(0, skin_folder_id)
254
    if reorder_skin_selection:
255 256
      # Sort by skin priority and ID
      selection_list.sort(key=skin_sort_key)
257
    if (skin_name in skin_layer_list):
258 259 260 261 262 263 264 265 266 267
      skin_tool.manage_skinLayers(skinpath=selection_list,
                                  skinname=skin_name, add_skin=1)
      skin_tool.getPortalObject().changeSkin(None)

def createSkinSelection(skin_tool, skin_name):
  # This skin selection does not exist, so we create a new one.
  # We'll initialize it with all skin folders, unless:
  #  - they explictly define a list of
  #    "business_template_registered_skin_selections", and we
  #    are not in this list.
Arnaud Fontaine's avatar
Arnaud Fontaine committed
268
  #  - they are not registered in the default skin selection
269 270 271 272 273 274 275 276 277 278 279
  skin_path = ''
  for skin_folder in skin_tool.objectValues():
    if skin_name in skin_folder.getProperty(
             'business_template_registered_skin_selections',
             (skin_name, )):
      if skin_folder.getId() in \
          skin_tool.getSkinPath(skin_tool.getDefaultSkin()):
        if skin_path:
          skin_path = '%s,%s' % (skin_path, skin_folder.getId())
        else:
          skin_path= skin_folder.getId()
280
  # add newly created skins to list of skins we care for
281 282 283 284 285 286 287 288
  skin_tool.addSkinSelection(skin_name, skin_path)
  skin_tool.getPortalObject().changeSkin(None)

def deleteSkinSelection(skin_tool, skin_name):
  # Do not delete default skin
  if skin_tool.getDefaultSkin() != skin_name:
    for skin_folder in skin_tool.objectValues():
      try:
289 290
        if skin_name in skin_folder.getProperty(
               'business_template_registered_skin_selections', ()):
291 292 293
          break
      except AttributeError:
        pass
294 295
    else:
      skin_tool.manage_skinLayers(chosen=[skin_name], del_skin=1)
296 297
      skin_tool.getPortalObject().changeSkin(None)

298
def unregisterSkinFolderId(skin_tool, skin_folder_id, skin_selection_list):
299 300 301 302 303 304 305 306 307 308
  for skin_selection in skin_selection_list:
    selection = skin_tool.getSkinPath(skin_selection)
    selection = selection.split(',')
    if (skin_folder_id in selection):
      selection.remove(skin_folder_id)
      skin_tool.manage_skinLayers(skinpath=tuple(selection),
                                  skinname=skin_selection, add_skin=1)
      deleteSkinSelection(skin_tool, skin_selection)
      skin_tool.getPortalObject().changeSkin(None)

309
class BusinessTemplateArchive(object):
Aurel's avatar
Aurel committed
310 311
  """
    This is the base class for all Business Template archives
312
  """
313
  def __init__(self, path, **kw):
314
    self.path = path
315
    self.revision = BusinessTemplateRevision()
Aurel's avatar
Aurel committed
316

317
  def addObject(self, obj, name, path=None, ext='.xml'):
318 319 320 321 322 323
    if path:
      name = posixpath.join(path, name)
    # XXX required due to overuse of os.path
    name = name.replace('\\', '/').replace(':', '/')
    name = quote(name + ext)
    path = name.replace('/', os.sep)
324
    try:
325 326 327 328 329
      write = self._writeFile
    except AttributeError:
      if not isinstance(obj, str):
        obj.seek(0)
        obj = obj.read()
330
      self.revision.hash(path, obj)
331 332 333
      self._writeString(obj, path)
    else:
      if isinstance(obj, str):
334
        self.revision.hash(path, obj)
335
        obj = StringIO(obj)
336 337 338
      else:
        obj.seek(0)
        self.revision.hash(path, obj.read())
339
      write(obj, path)
340

341
  def finishCreation(self):
Aurel's avatar
Aurel committed
342 343
    pass

344 345 346
  def getRevision(self):
    return self.revision.digest()

Aurel's avatar
Aurel committed
347 348
class BusinessTemplateFolder(BusinessTemplateArchive):
  """
Christophe Dumez's avatar
Christophe Dumez committed
349
    Class archiving business template into a folder tree
350
  """
351 352 353 354 355 356 357 358 359 360
  def _writeString(self, obj, path):
    object_path = os.path.join(self.path, path)
    path = os.path.dirname(object_path)
    os.path.exists(path) or os.makedirs(path)
    f = open(object_path, 'wb')
    try:
      f.write(obj)
    finally:
      f.close()

361
  def importFiles(self, item):
Aurel's avatar
Aurel committed
362 363 364
    """
      Import file from a local folder
    """
365
    join = os.path.join
366 367
    item_name = item.__class__.__name__
    root = join(os.path.normpath(self.path), item_name, '')
368
    root_path_len = len(root)
369 370 371 372 373 374
    if CACHE_DATABASE_PATH:
      try:
        cache_database.db = gdbm.open(CACHE_DATABASE_PATH, 'cf')
      except gdbm.error:
        cache_database.db = gdbm.open(CACHE_DATABASE_PATH, 'nf')
    try:
375 376 377 378
      for root, dirs, files in os.walk(root):
        for file_name in files:
          file_name = join(root, file_name)
          with open(file_name, 'rb') as f:
379
            file_name = posixpath.normpath(file_name[root_path_len:])
380 381
            if '%' in file_name:
              file_name = unquote(file_name)
382 383 384 385
            elif item_name == 'bt' and file_name == 'revision':
              continue
            self.revision.hash(item_name + '/' + file_name, f.read())
            f.seek(0)
386
            item._importFile(file_name, f)
387 388 389 390
    finally:
      if hasattr(cache_database, 'db'):
        cache_database.db.close()
        del cache_database.db
391

Aurel's avatar
Aurel committed
392 393 394 395 396
class BusinessTemplateTarball(BusinessTemplateArchive):
  """
    Class archiving businnes template into a tarball file
  """

397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
  def __init__(self, path, creation=0, importing=0, **kw):
    super(BusinessTemplateTarball, self).__init__(path, **kw)
    if creation:
      self.fobj = StringIO()
      self.tar = tarfile.open('', 'w:gz', self.fobj)
      self.time = time.time()
    elif importing:
      self.tar = tarfile.open(path, 'r:gz')
      self.item_dict = item_dict = defaultdict(list)
      for info in self.tar.getmembers():
        if info.isreg():
          path = info.name.split('/')
          if path[0] == '.':
            del path[0]
          item_dict[path[1]].append(('/'.join(path[2:]), info))
412 413 414 415 416 417 418 419 420 421 422 423

  def _writeFile(self, obj, path):
    if self.path:
      path = posixpath.join(self.path, path)
    info = tarfile.TarInfo(path)
    info.mtime = self.time
    obj.seek(0, 2)
    info.size = obj.tell()
    obj.seek(0)
    self.tar.addfile(info, obj)

  def finishCreation(self):
Aurel's avatar
Aurel committed
424 425 426
    self.tar.close()
    return self.fobj

427
  def importFiles(self, item):
Aurel's avatar
Aurel committed
428 429
    """
      Import all file from the archive to the site
430
    """
431
    extractfile = self.tar.extractfile
432 433
    item_name = item.__class__.__name__
    for file_name, info in self.item_dict.get(item_name, ()):
434 435
      if '%' in file_name:
        file_name = unquote(file_name)
436 437 438 439 440 441
      elif item_name == 'bt' and file_name == 'revision':
        continue
      f = extractfile(info)
      self.revision.hash(item_name + '/' + file_name, f.read())
      f.seek(0)
      item._importFile(file_name, f)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
442

443
class TemplateConditionError(Exception): pass
444
class TemplateConflictError(Exception): pass
445
class BusinessTemplateMissingDependency(Exception): pass
446

447 448 449
ModuleSecurityInfo(__name__).declarePublic('BusinessTemplateMissingDependency',
  'TemplateConditionError', 'TemplateConflictError')

450
class BaseTemplateItem(Implicit, Persistent):
451
  """
452
    This class is the base class for all template items.
453
    is_bt_for_diff means This BT is used to compare self temporary BT with installed BT
454
  """
455
  is_bt_for_diff = None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
456

457
  def __init__(self, id_list, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
458
    self.__dict__.update(kw)
459
    self._archive = PersistentMapping()
Aurel's avatar
Aurel committed
460
    self._objects = PersistentMapping()
461
    for id in id_list:
462 463
      if id is not None and id != '':
        self._archive[id] = None
464 465 466 467

  def build(self, context, **kw):
    pass

468
  def preinstall(self, context, installed_item, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
469 470
    """
      Build a list of added/removed/changed files between the BusinessTemplate
471
      being installed (self) and the installed one (installed_item).
Vincent Pelletier's avatar
Vincent Pelletier committed
472 473 474 475 476
      Note : we compare files between BTs, *not* between the installed BT and
      the objects in the DataFS.

      XXX: -12 used here is -len('TemplateItem')
    """
Yoshinori Okuji's avatar
Yoshinori Okuji committed
477
    modified_object_list = {}
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492
    for path in self._objects:
      if installed_item._objects.has_key(path):
        # compare objects to see it there are changes
        new_obj_xml = self.generateXml(path=path)
        old_obj_xml = installed_item.generateXml(path=path)
        if new_obj_xml != old_obj_xml:
          modified_object_list.update({path : ['Modified', self.__class__.__name__[:-12]]})
        # else, compared versions are identical, don't overwrite the old one
      else: # new object
        modified_object_list.update({path : ['New', self.__class__.__name__[:-12]]})
    # list removed objects
    old_keys = installed_item._objects.keys()
    for path in old_keys:
      if path not in self._objects:
        modified_object_list.update({path : ['Removed', self.__class__.__name__[:-12]]})
493 494 495
    return modified_object_list

  def install(self, context, trashbin, **kw):
496
    pass
497 498 499

  def uninstall(self, context, **kw):
    pass
500

501
  def remove(self, context, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
502 503 504 505 506 507
    """
      If 'remove' is chosen on an object containing subobjects, all the
      subobjects will be removed too, even if 'backup' or 'keep' was chosen for
      the subobjects.
      Likewise, for 'save_and_remove' : subobjects will get saved too.
    """
508 509 510
    remove_dict = kw.get('remove_object_dict', {})
    keys = self._objects.keys()
    keys.sort()
511
    keys.reverse()
512 513 514 515 516 517 518 519 520 521 522
    # if you choose remove, the object and all its subobjects will be removed
    # even if you choose backup or keep for subobjects
    # it is same behaviour for backup_and_remove, all we be save
    for path in keys:
      if remove_dict.has_key(path):
        action = remove_dict[path]
        if action == 'save_and_remove':
          # like trash
          self.uninstall(context, trash=1, object_path=path, **kw)
        elif action == 'remove':
          self.uninstall(context, trash=0, object_path=path, **kw)
523 524 525 526
        else:
          # As the list of available actions is not strictly defined,
          # prevent mistake if an action is not handled
          raise ValueError, 'Unknown action "%s"' % action
527

528

529 530 531 532
  def trash(self, context, new_item, **kw):
    # trash is quite similar to uninstall.
    return self.uninstall(context, new_item=new_item, trash=1, **kw)

Aurel's avatar
Aurel committed
533
  def export(self, context, bta, **kw):
534
    pass
Aurel's avatar
Aurel committed
535

536 537
  def getKeys(self):
    return self._objects.keys()
538

Aurel's avatar
Aurel committed
539
  def importFile(self, bta, **kw):
540
    bta.importFiles(self)
541

542
  def _removeAllButLastWorkflowHistory(self, obj):
543 544 545 546 547 548
    workflow_history = getattr(obj, 'workflow_history', None)
    if workflow_history is None:
      return
    for workflow_id in workflow_history.keys():
      workflow_history[workflow_id] = WorkflowHistoryList(
        [workflow_history[workflow_id][-1]])
549 550 551 552 553 554

  def removeProperties(self,
                       obj,
                       export,
                       keep_workflow_history=False,
                       keep_workflow_history_last_history_only=False):
555 556
    """
    Remove unneeded properties for export
557
    """
558
    obj._p_activate()
559 560
    klass = obj.__class__
    classname = klass.__name__
561

562 563 564
    attr_set = {'_dav_writelocks', '_filepath', '_owner', '_related_index',
                'last_id', 'uid',
                '__ac_local_roles__', '__ac_local_roles_group_id_dict__'}
565
    if export:
566 567 568
      if keep_workflow_history_last_history_only:
        self._removeAllButLastWorkflowHistory(obj)
      elif not keep_workflow_history:
569
        attr_set.add('workflow_history')
570 571
      # PythonScript covers both Zope Python scripts
      # and ERP5 Python Scripts
572
      if isinstance(obj, PythonScript):
573
        attr_set.update(('func_code', 'func_defaults', '_code',
574
                         '_lazy_compilation', 'Python_magic'))
575 576 577
        for attr in 'errors', 'warnings', '_proxy_roles':
          if not obj.__dict__.get(attr, 1):
            delattr(obj, attr)
578 579
      elif classname == 'SQL' and klass.__module__== 'Products.ZSQLMethods.SQL':
        attr_set.update(('_arg', 'template'))
580
      elif interfaces.IIdGenerator.providedBy(obj):
581
        attr_set.update(('last_max_id_dict', 'last_id_dict'))
582 583
      elif classname == 'Types Tool' and klass.__module__ == 'erp5.portal_type':
        attr_set.add('type_provider_list')
584

585 586
    for attr in obj.__dict__.keys():
      if attr in attr_set or attr.startswith('_cache_cookie_'):
587 588
        delattr(obj, attr)

589
    if classname == 'PDFForm':
590
      if not obj.getProperty('business_template_include_content', 1):
591
        obj.deletePdfContent()
592 593
    return obj

594 595
  def getTemplateTypeName(self):
    """
596
     Get a meaningfull class Name without 'TemplateItem'. Used to
597 598 599 600 601 602
     present to the user.

     XXX: -12 used here is -len('TemplateItem')
    """
    return self.__class__.__name__[:-12]

603
  def restrictedResolveValue(self, context=None, path='', default=_MARKER):
604 605 606 607
    """
      Get the value with checking the security.
      This method does not acquire the parent.
    """
608 609
    return self.unrestrictedResolveValue(context, path, default=default,
                                         restricted=1)
610

611 612
  def unrestrictedResolveValue(self, context=None, path='', default=_MARKER,
                               restricted=0):
613 614 615 616 617 618 619 620 621 622 623
    """
      Get the value without checking the security.
      This method does not acquire the parent.
    """
    if isinstance(path, basestring):
      stack = path.split('/')
    else:
      stack = list(path)
    stack.reverse()
    if stack:
      if context is None:
624
        portal = aq_inner(self.getPortalObject())
625 626 627 628
        container = portal
      else:
        container = context

629 630 631 632
      if restricted:
        validate = getSecurityManager().validate

      while stack:
633
        key = stack.pop()
634 635 636
        try:
          value = container[key]
        except KeyError:
637
          LOG('BusinessTemplate', WARNING,
638 639 640 641
              'Could not access object %s' % (path,))
          if default is _MARKER:
            raise
          return default
642

643 644 645 646 647 648 649 650 651 652 653 654
        if restricted:
          try:
            if not validate(container, container, key, value):
              raise Unauthorized('unauthorized access to element %s' % key)
          except Unauthorized:
            LOG('BusinessTemplate', WARNING,
                'access to %s is forbidden' % (path,))
          if default is _MARKER:
            raise
          return default

        container = value
655

656 657 658
      return value
    else:
      return context
659

660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
  def _resetDynamicModules(self):
    # before any import, flush all ZODB caches to force a DB reload
    # otherwise we could have objects trying to get commited while
    # holding reference to a class that is no longer the same one as
    # the class in its import location and pickle doesn't tolerate it.
    # First we do a savepoint to dump dirty objects to temporary
    # storage, so that all references to them can be freed.
    transaction.savepoint(optimistic=True)
    # Then we need to flush from all caches, not only the one from this
    # connection
    portal = self.getPortalObject()
    portal._p_jar.db().cacheMinimize()
    synchronizeDynamicModules(portal, force=True)
    gc.collect()

675 676 677
class ObjectTemplateItem(BaseTemplateItem):
  """
    This class is used for generic objects and as a subclass.
678
  """
679

680 681 682 683
  def __init__(self, id_list, tool_id=None, **kw):
    BaseTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
    if tool_id is not None:
      id_list = self._archive.keys()
684
      self._archive.clear()
685 686 687
      for id in id_list :
        if id != '':
          self._archive["%s/%s" % (tool_id, id)] = None
688

689
  def export(self, context, bta, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
690 691 692 693
    """
      Export the business template : fill the BusinessTemplateArchive with
      objects exported as XML, hierarchicaly organised.
    """
694 695
    if len(self._objects.keys()) == 0:
      return
696
    path = self.__class__.__name__
Vincent Pelletier's avatar
Vincent Pelletier committed
697
    for key, obj in self._objects.iteritems():
698
      # export object in xml
699
      f = StringIO()
700
      XMLExportImport.exportXML(obj._p_jar, obj._p_oid, f)
701
      bta.addObject(f, key, path=path)
702

Aurel's avatar
Aurel committed
703
  def build_sub_objects(self, context, id_list, url, **kw):
Vincent Pelletier's avatar
Vincent Pelletier committed
704
    # XXX duplicates code from build
Aurel's avatar
Aurel committed
705 706
    p = context.getPortalObject()
    for id in id_list:
707
      relative_url = '/'.join([url,id])
708 709
      obj = p.unrestrictedTraverse(relative_url)
      obj = obj._getCopy(context)
710 711 712
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
Vincent Pelletier's avatar
Vincent Pelletier committed
713 714 715
      id_list = obj.objectIds() # FIXME duplicated variable name
      if hasattr(aq_base(obj), 'groups'): # XXX should check metatype instead
        # we must keep groups because they are deleted along with subobjects
716
        groups = deepcopy(obj.groups)
717
      if id_list:
Aurel's avatar
Aurel committed
718
        self.build_sub_objects(context, id_list, relative_url)
719
        for id_ in list(id_list):
720
          obj._delObject(id_)
721
      if hasattr(aq_base(obj), 'groups'):
722 723 724
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
Aurel's avatar
Aurel committed
725

726 727 728 729
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
    for relative_url in self._archive.keys():
730 731 732 733
      try:
        obj = p.unrestrictedTraverse(relative_url)
      except ValueError:
        raise ValueError, "Can not access to %s" % relative_url
734 735 736 737
      try:
        obj = obj._getCopy(context)
      except AttributeError:
        raise AttributeError, "Could not find object '%s' during business template processing." % relative_url
738
      _recursiveRemoveUid(obj)
739 740 741
      obj = self.removeProperties(obj, 1,
                                  self.isKeepWorkflowObject(relative_url),
                                  self.isKeepWorkflowObjectLastHistoryOnly(relative_url))
742
      id_list = obj.objectIds()
Vincent Pelletier's avatar
Vincent Pelletier committed
743 744
      if hasattr(aq_base(obj), 'groups'): # XXX should check metatype instead
        # we must keep groups because they are deleted along with subobjects
745
        groups = deepcopy(obj.groups)
Aurel's avatar
Aurel committed
746 747
      if len(id_list) > 0:
        self.build_sub_objects(context, id_list, relative_url)
748
        for id_ in list(id_list):
749
          obj._delObject(id_)
750
      if hasattr(aq_base(obj), 'groups'):
751 752 753
        obj.groups = groups
      self._objects[relative_url] = obj
      obj.wl_clearLocks()
754

755
  def _compileXML(self, file):
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
    # This method converts XML to ZEXP. Because the conversion
    # is quite heavy, a persistent cache database is used to
    # store ZEXP, so the second run wouldn't have to re-generate
    # identical data again.
    #
    # For now, a pair of the path to an XML file and its modification time
    # are used as a unique key. In theory, a checksum of the content could
    # be used instead, and it could be more reliable, as modification time
    # might not be updated in some insane filesystems correctly. However,
    # in practice, checksums consume a lot of CPU time, so when the cache
    # does not hit, the increased overhead is significant. In addition, it
    # does rarely happen that two XML files in Business Templates contain
    # the same data, so it may not be expected to have more cache hits
    # with this approach.
    #
    # The disadvantage is that this wouldn't work with the archive format,
    # because each entry in an archive does not have a mtime in itself.
    # However, the plan is to have an archive to retain ZEXP directly
    # instead of XML, so the idea of caching would be completely useless
    # with the archive format.
    name = file.name
    mtime = os.path.getmtime(file.name)
    key = '%s:%s' % (name, mtime)

    try:
      return StringIO(cache_database.db[key])
    except:
      pass

    #LOG('Business Template', 0, 'Compiling %s...' % (name,))
    from Shared.DC.xml import ppml
    from OFS.XMLExportImport import start_zopedata, save_record, save_zopedata
    import xml.parsers.expat
    outfile=StringIO()
    try:
      data=file.read()
      F=ppml.xmlPickler()
      F.end_handlers['record'] = save_record
      F.end_handlers['ZopeData'] = save_zopedata
      F.start_handlers['ZopeData'] = start_zopedata
      F.binary=1
      F.file=outfile
      p=xml.parsers.expat.ParserCreate('utf-8')
      p.returns_unicode = False
      p.CharacterDataHandler=F.handle_data
      p.StartElementHandler=F.unknown_starttag
      p.EndElementHandler=F.unknown_endtag
803
      p.Parse(data)
804 805 806 807 808 809 810 811 812 813 814

      try:
        cache_database.db[key] = outfile.getvalue()
      except:
        pass

      outfile.seek(0)
      return outfile
    except:
      outfile.close()
      raise
815

816 817 818 819 820 821 822
  def getConnection(self, obj):
    while True:
      connection = obj._p_jar
      if connection is not None:
        return connection
      obj = obj.aq_parent

823
  def _importFile(self, file_name, file_obj):
824
    # import xml file
825
    if not file_name.endswith('.xml'):
826
      LOG('Business Template', 0, 'Skipping file "%s"' % (file_name, ))
827
      return
828
    connection = self.getConnection(self.aq_parent)
829
    __traceback_info__ = 'Importing %s' % file_name
830 831
    if hasattr(cache_database, 'db') and isinstance(file_obj, file):
      obj = connection.importFile(self._compileXML(file_obj))
832
    else:
833 834
      # FIXME: Why not use the importXML function directly? Are there any BT5s
      # with actual .zexp files on the wild?
835
      obj = connection.importFile(file_obj, customImporters=customImporters)
836 837
    self._objects[file_name[:-4]] = obj

838
  def preinstall(self, context, installed_item, **kw):
Yoshinori Okuji's avatar
Yoshinori Okuji committed
839
    modified_object_list = {}
840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
    upgrade_list = []
    type_name = self.__class__.__name__.split('TemplateItem')[-2]
    for path, obj in self._objects.iteritems():
      if installed_item._objects.has_key(path):
        upgrade_list.append((path, installed_item._objects[path]))
      else: # new object
        modified_object_list[path] = 'New', type_name

    # update _p_jar property of objects cleaned by removeProperties
    transaction.savepoint(optimistic=True)
    for path, old_object in upgrade_list:
      # compare object to see it there is changes
      new_object = self._objects[path]
      new_io = StringIO()
      old_io = StringIO()
      OFS.XMLExportImport.exportXML(new_object._p_jar, new_object._p_oid, new_io)
      new_obj_xml = new_io.getvalue()
      try:
        OFS.XMLExportImport.exportXML(old_object._p_jar, old_object._p_oid, old_io)
        old_obj_xml = old_io.getvalue()
      except (ImportError, UnicodeDecodeError), e: # module is already
                                                   # removed etc.
        old_obj_xml = '(%s: %s)' % (e.__class__.__name__, e)
      new_io.close()
      old_io.close()
      if new_obj_xml != old_obj_xml:
866
        if context.isKeepObject(path):
867
          modified_object_list[path] = 'Modified but should be kept', type_name
868
        else:
869 870 871 872 873 874 875
          modified_object_list[path] = 'Modified', type_name
    # get removed object
    for path in set(installed_item._objects) - set(self._objects):
      if context.isKeepObject(path):
        modified_object_list[path] = 'Removed but should be kept', type_name
      else:
        modified_object_list[path] = 'Removed', type_name
876 877
    return modified_object_list

878
  def _backupObject(self, action, trashbin, container_path, object_id, **kw):
879 880 881
    """
      Backup the object in portal trash if necessery and return its subobjects
    """
882 883 884 885
    if "portal_integrations" in container_path and "module" in object_id:
      # XXX It is impossible to backup integration module as
      # it will call the request and try to get remote data
      return
886
    p = self.getPortalObject()
887
    if trashbin is None: # must return subobjects
888 889 890 891 892 893 894
      subobject_dict = {}
      obj = p.unrestrictedTraverse(container_path)[object_id]
      for subobject_id in obj.objectIds():
        subobject = obj[subobject_id]
        subobject_dict[subobject_id] = subobject._p_jar.exportFile(
            subobject._p_oid, StringIO())
      return subobject_dict
895
    # XXX btsave is for backward compatibility
896
    if action in ('backup', 'btsave', 'save_and_remove',):
897
      save = 1
898
    elif action in ('install', 'remove'):
899
      save = 0
900 901 902
    else:
      # As the list of available actions is not strictly defined,
      # prevent mistake if an action is not handled
903
      raise NotImplementedError, 'Unknown action "%s"' % action
904 905 906
    return p.portal_trash.backupObject(trashbin, container_path, object_id,
                                       save=save, **kw)

907

908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
  def beforeInstall(self):
    """
      Installation hook.
      Called right at the begining of "install" method.
      Can be overridden by subclasses.
    """
    pass

  def afterInstall(self):
    """
      Installation hook.
      Called right before returning in "install" method.
      Can be overridden by subclasses.
    """
    pass

924
  def onNewObject(self, obj):
925 926 927 928
    """
      Installation hook.
      Called when installation process determined that object to install is
      new on current site (it's not replacing an existing object).
929
      `obj` parameter is the newly created object in its acquisition context.
930 931 932 933
      Can be overridden by subclasses.
    """
    pass

934 935 936 937 938 939 940 941
  def setSafeReindexationMode(self, context):
    """
      Postpone indexations after unindexations.
      This avoids alarming error messages about a single uid being used
      by "deleted" path and reindexed object. This can happen here for
      objects on which the uid was restored: previous object was deleted,
      hence the "deleted" path, and new object does have the same uid.
    """
942 943 944 945 946 947 948
    kw = context.getPlacelessDefaultReindexParameters()
    if kw is None:
      kw = {}
    context.setPlacelessDefaultReindexParameters(**dict(kw,
      activate_kw=dict(kw.get('activate_kw', ()),
                       after_method_id='unindexObject')))
    return kw
949

950 951 952 953 954 955
  def _getObjectKeyList(self):
    # sort to add objects before their subobjects
    keys = self._objects.keys()
    keys.sort()
    return keys

956
  def unindexBrokenObject(self, item_path):
957 958 959 960 961 962 963 964
    """
      Unindex broken objects.

      Corresponding catalog record is not unindexed even after a broken object
      is removed, since the broken object does not implement 'CopySupport'.
      This situation triggers a FATAL problem on SQLCatalog.catalogObjectList
      when upgrading a broken path by ObjectTemplateItem with BusinessTemplate.
      We often get this problem when we are upgrading a quite old ERP5 site
965
      to new one, as several old classes may be already removed/replaced
966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
      in the file system, thus several objects tend to be broken.

      Keyword arguments:
      item_path -- the path specified by the ObjectTemplateItem
    """
    def flushActivity(obj, invoke=0, **kw):
      try:
        activity_tool = self.getPortalObject().portal_activities
      except AttributeError:
        return # Do nothing if no portal_activities
      # flush all activities related to this object
      activity_tool.flush(obj, invoke=invoke, **kw)

    class fakeobject:
       def __init__(self, path):
981
         self._physical_path = tuple(path.split('/'))
982 983
       def getPhysicalPath(self):
         return self._physical_path
984

985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
    def recursiveUnindex(catalog, item_path, root_document_path):
      # search the object + sub-objects
      result = catalog(relative_url=(item_path,
                                     item_path.replace('_', r'\_') + '/%'))
      for x in result:
        uid = x.uid
        path = x.path
        unindex(root_document_path, path, uid)

    def unindex(root_document_path, path, uid):
      LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
          'Unindex Broken object at %r.' % (path,))
      # Make sure there is not activity for this object
      flushActivity(fakeobject(path))
      # Set the path as deleted without lock
      catalog.beforeUnindexObject(None,path=path,uid=uid)
      # Then start activity in order to remove lines in catalog,
      # sql wich generate locks
      catalog.activate(activity='SQLQueue',
                       tag='%s' % uid,
                       group_method_id='portal_catalog/uncatalogObjectList',
                       serialization_tag=root_document_path
                       ).unindexObject(uid=uid)

1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
    portal = self.getPortalObject()
    try:
      catalog = portal.portal_catalog
    except AttributeError:
      pass
    else:
      # given item_path is a relative_url in reality
      root_path = "/".join(item_path.split('/')[:2])
      root_document_path = '/%s/%s' % (portal.getId(), root_path)
      recursiveUnindex(catalog, item_path, root_document_path)
1019

1020 1021 1022 1023
  def fixBrokenObject(self, obj):
    if isinstance(obj, ERP5BaseBroken):
      self._resetDynamicModules()

1024
  def install(self, context, trashbin, **kw):
1025
    self.beforeInstall()
1026 1027
    update_dict = kw.get('object_to_update')
    force = kw.get('force')
1028

1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
    def recurse(hook, document, prefix=''):
      my_prefix = '%s/%s' % (prefix, document.id)
      if (hook(document, my_prefix)):
        for subdocument in document.objectValues():
          recurse(hook, subdocument, my_prefix)
    def saveHook(document, prefix):
      uid = getattr(aq_base(document), 'uid', None)
      if uid is None:
        return 0
      else:
        saved_uid_dict[prefix] = uid
        return 1
    def restoreHook(document, prefix):
      uid = saved_uid_dict.get(prefix)
      if uid is None:
        return 0
      else:
        document.uid = uid
        return 1
    groups = {}
    old_groups = {}
    portal = context.getPortalObject()
    # set safe activities execution order
    original_reindex_parameters = self.setSafeReindexationMode(context)
    object_key_list = self._getObjectKeyList()
    for path in object_key_list:
      __traceback_info__ = path
      # We do not need to perform any backup because the object was
      # created during the Business Template installation
      if update_dict.get(path) == 'migrate':
        continue

      if update_dict.has_key(path) or force:
        # get action for the oject
        action = 'backup'
        if not force:
          action = update_dict[path]
          if action == 'nothing':
            continue
        # get subobjects in path
        path_list = path.split('/')
        container_path = path_list[:-1]
        object_id = path_list[-1]
        try:
          container = self.unrestrictedResolveValue(portal, container_path)
        except KeyError:
          # parent object can be set to nothing, in this case just go on
          container_url = '/'.join(container_path)
          if update_dict.get(container_url) == 'nothing':
            continue
          # If container's container is portal_catalog,
          # then automatically create the container.
          elif len(container_path) > 1 and container_path[-2] == 'portal_catalog':
            # The id match, but better double check with the meta type
            # while avoiding the impact of systematic check
            container_container = portal.unrestrictedTraverse(container_path[:-1])
            if container_container.meta_type == 'ERP5 Catalog':
              container_container.manage_addProduct['ZSQLCatalog'].manage_addSQLCatalog(id=container_path[-1], title='')
              if len(container_container.objectIds()) == 1:
                container_container.default_sql_catalog_id = container_path[-1]
              container = portal.unrestrictedTraverse(container_path)
          else:
            raise
        saved_uid_dict = {}
        subobjects_dict = {}
        portal_type_dict = {}
        old_obj = container._getOb(object_id, None)
        object_existed = old_obj is not None
        if object_existed:
          if context.isKeepObject(path) and force:
            # do nothing if the object is specified in keep list in
            # force mode.
            continue
          # Object already exists
          recurse(saveHook, old_obj)
          if getattr(aq_base(old_obj), 'groups', None) is not None:
1105
            # we must keep original order groups
1106 1107 1108 1109
            # from old form in case we keep some
            # old widget, thus we can readd them in
            # the right order group
            old_groups[path] = deepcopy(old_obj.groups)
1110 1111
          # we force backup since it was an existing object
          subobjects_dict = self._backupObject('backup', trashbin,
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131
                                               container_path, object_id)
          # in case of portal types, we want to keep some properties
          if interfaces.ITypeProvider.providedBy(container):
            for attr in ('allowed_content_types',
                         'hidden_content_type_list',
                         'property_sheet_list',
                         'base_category_list'):
              portal_type_dict[attr] = getattr(old_obj, attr, ())
            portal_type_dict['workflow_chain'] = \
              getChainByType(context)[1].get('chain_' + object_id, '')
          container.manage_delObjects([object_id])
          # unindex here when it is a broken object
          if isinstance(old_obj, Broken):
            new_obj = self._objects[path]
            # check isIndexable with new one, because the old one is broken
            if new_obj.isIndexable():
              self.unindexBrokenObject(path)

        # install object
        obj = self._objects[path]
1132
        self.fixBrokenObject(obj)
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
        # XXX Following code make Python Scripts compile twice, because
        #     _getCopy returns a copy without the result of the compilation.
        #     A solution could be to add a specific _getCopy method to
        #     Python Scripts.
        if getattr(aq_base(obj), 'groups', None) is not None:
          # we must keep original order groups
          # because they change when we add subobjects
          groups[path] = deepcopy(obj.groups)
        # copy the object
        if (getattr(aq_base(obj), '_mt_index', None) is not None and
            obj._count() == 0):
          # some btrees were exported in a corrupted state. They're empty but
          # their metadata-index (._mt_index) contains entries which in
          # Zope 2.12 are used for .objectIds(), .objectValues() and
1147
          # .objectItems(). In these cases, force the
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
          LOG('Products.ERP5.Document.BusinessTemplate', WARNING,
              'Cleaning corrupted BTreeFolder2 object at %r.' % (path,))
          obj._initBTrees()
        obj = obj._getCopy(container)
        self.removeProperties(obj, 0)
        __traceback_info__ = (container, object_id, obj)
        container._setObject(object_id, obj)
        obj = container._getOb(object_id)

        if not object_existed:
          # A new object was added, call the hook
          self.onNewObject(obj)

        # mark a business template installation so in 'PortalType_afterClone' scripts
        # we can implement logical for reseting or not attributes (i.e reference).
        self.REQUEST.set('is_business_template_installation', 1)
        # We set isIndexable to 0 before calling
        # manage_afterClone in order to not call recursiveReindex, this is
        # useless because we will already reindex every created object, so
        # we avoid duplication of reindexation
        obj.isIndexable = ConstantGetter('isIndexable', value=False)
        # START:part of ERP5Type.CopySupport.manage_afterClone
        # * reset uid
        # * reset owner
        # * do not reset workflow
        # * do not call recursively
        # * do not call type-based afterClone script
        #
        # Change uid attribute so that Catalog thinks object was not yet catalogued
        aq_base(obj).uid = portal.portal_catalog.newUid()
        # Give the Owner local role to the current user, zope only does this if no
        # local role has been defined on the object, which breaks ERP5Security
        if getattr(aq_base(obj), '__ac_local_roles__', None) is not None:
          user=getSecurityManager().getUser()
          if user is not None:
            userid=user.getId()
            if userid is not None:
              #remove previous owners
              local_role_dict = obj.__ac_local_roles__
              removable_role_key_list = []
              for key, value in local_role_dict.items():
                if 'Owner' in value:
                  value.remove('Owner')
                if len(value) == 0:
                  removable_role_key_list.append(key)
              # there is no need to keep emptied keys after cloning, it makes
              # unstable local roles -- if object is cloned it can be different when
              # after being just added
              for key in removable_role_key_list:
                local_role_dict.pop(key)
              #add new owner
              l=local_role_dict.setdefault(userid, [])
              l.append('Owner')
        # END:part of ERP5Type.CopySupport.manage_afterClone
        del obj.isIndexable
        if getattr(aq_base(obj), 'reindexObject', None) is not None:
          obj.reindexObject()
        obj.wl_clearLocks()
        if portal_type_dict:
          # set workflow chain
          wf_chain = portal_type_dict.pop('workflow_chain')
          chain_dict = getChainByType(context)[1]
          default_chain = ''
          chain_dict['chain_%s' % (object_id)] = wf_chain
          context.portal_workflow.manage_changeWorkflows(default_chain, props=chain_dict)
          # restore some other properties
          obj.__dict__.update(portal_type_dict)
        # import sub objects if there is
        if subobjects_dict:
          # get a jar
          connection = self.getConnection(obj)
          # import subobjects
          for subobject_id, subobject_data in subobjects_dict.iteritems():
            try:
              if obj._getOb(subobject_id, None) is None:
                subobject_data.seek(0)
                subobject = connection.importFile(subobject_data)
                obj._setObject(subobject_id, subobject)
            except AttributeError:
              # XXX this may happen when an object which can contain
              # sub-objects (e.g. ERP5 Form) has been replaced with
              # an object which cannot (e.g. External Method).
              LOG('BusinessTemplate', WARNING,
                  'could not restore %r in %r' % (subobject_id, obj))
        if obj.meta_type in ('Z SQL Method',):
          fixZSQLMethod(portal, obj)
        # portal transforms specific initialization
        elif obj.meta_type in ('Transform', 'TransformsChain'):
          assert container.meta_type == 'Portal Transforms'
          # skip transforms that couldn't have been initialized
          if obj.title != 'BROKEN':
            container._mapTransform(obj)
        elif obj.meta_type in ('ERP5 Ram Cache',
                               'ERP5 Distributed Ram Cache',):
          assert container.meta_type in ('ERP5 Cache Factory',
                                         'ERP5 Cache Bag')
          container.getParentValue().updateCache()
        elif (container.meta_type == 'CMF Skins Tool') and \
            (old_obj is not None):
          # Keep compatibility with previous export format of
          # business_template_registered_skin_selections
          # and do not modify exported value
1250
          if obj.getProperty('business_template_registered_skin_selections',
1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
                             None) is None:
            # Keep previous value of register skin selection for skin folder
            skin_selection_list = old_obj.getProperty(
                'business_template_registered_skin_selections', None)
            if skin_selection_list is not None:
              if isinstance(skin_selection_list, basestring):
                skin_selection_list = skin_selection_list.split(' ')
              obj._setProperty(
                  'business_template_registered_skin_selections',
                  skin_selection_list, type='tokens')
        # in case the portal ids, we want keep the property dict
        elif interfaces.IIdGenerator.providedBy(obj) and \
          old_obj is not None:
          for dict_name in ('last_max_id_dict', 'last_id_dict'):
            # Keep previous last id dict
            if getattr(old_obj, dict_name, None) is not None:
              old_dict = getattr(old_obj, dict_name, None)
              setattr(obj, dict_name, old_dict)

        recurse(restoreHook, obj)
    # now put original order group
    # we remove object not added in forms
    # we put old objects we have kept
    for path, new_groups_dict in groups.iteritems():
      if not old_groups.has_key(path):
        # installation of a new form
        obj = portal.unrestrictedTraverse(path)
        obj.groups = new_groups_dict
      else:
        # upgrade of a form
        old_groups_dict = old_groups[path]
        obj = portal.unrestrictedTraverse(path)
        # first check that all widgets are in new order
        # excetp the one that had to be removed
        widget_id_list = obj.objectIds()
        for widget_id in widget_id_list:
          widget_path = path+'/'+widget_id
          if update_dict.has_key(widget_path) and update_dict[widget_path] in ('remove', 'save_and_remove'):
            continue
          widget_in_form = 0
1291
          for group_id, group_value_list in new_groups_dict.iteritems():
1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305
            if widget_id in group_value_list:
              widget_in_form = 1
              break
          # if not, add it in the same groups
          # defined on the former form
          previous_group_id = None
          if not widget_in_form:
            for old_group_id, old_group_values in old_groups_dict.iteritems():
              if widget_id in old_group_values:
                previous_group_id = old_group_id
            # if we find same group in new one, add widget to it
            if previous_group_id is not None and new_groups_dict.has_key(previous_group_id):
              new_groups_dict[previous_group_id].append(widget_id)
            # otherwise use a specific group
1306
            else:
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322
              if new_groups_dict.has_key('not_assigned'):
                new_groups_dict['not_assigned'].append(widget_id)
              else:
                new_groups_dict['not_assigned'] = [widget_id,]
                obj.group_list = list(obj.group_list) + ['not_assigned']
        # second check all widget_id in order are in form
        for group_id, group_value_list in new_groups_dict.iteritems():
          for widget_id in tuple(group_value_list):
            if widget_id not in widget_id_list:
              # if we don't find the widget id in the form
              # remove it fro the group
              group_value_list.remove(widget_id)
        # now set new group object
        obj.groups = new_groups_dict
    # restore previous activities execution order
    context.setPlacelessDefaultReindexParameters(**original_reindex_parameters)
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334
    to_delete_dict = {}
    # XXX: it is not clear why update_dict would contain subojects of any
    # element of object_key_list, and not just these objects themselves.
    # XXX: why does update_dict contain the path of documents not managed
    # by current instance ?
    for path, action in update_dict.iteritems():
      if action not in ('remove', 'save_and_remove'):
        continue
      path_match = path + '/'
      for object_key in object_key_list:
        if path_match.startswith(object_key + '/'):
          to_delete_dict[path] = action
1335 1336
    # Sort by path so that, for example, form is created before its fields.
    for path, action in sorted(to_delete_dict.iteritems()):
1337 1338 1339 1340 1341 1342 1343 1344 1345 1346
      document = self.unrestrictedResolveValue(portal, path, None)
      if document is None:
        continue
      if getattr(aq_base(document), 'getParentValue', None) is None:
        parent = document.aq_parent
      else:
        parent = document.getParentValue()
      document_id = document.getId()
      self._backupObject(action, trashbin, path.split('/')[:-1],
                         document_id)
1347 1348 1349 1350
      try:
        parent.manage_delObjects([document_id])
      except BadRequest:
        pass # removed manually
1351

1352
    self.afterInstall()
1353 1354 1355

  def uninstall(self, context, **kw):
    portal = context.getPortalObject()
1356
    trash = kw.get('trash', 0)
1357 1358 1359 1360 1361
    trashbin = kw.get('trashbin', None)
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
1362
      object_keys = self._archive.keys()
1363
    for relative_url in object_keys:
1364 1365
      container_path = relative_url.split('/')[0:-1]
      object_id = relative_url.split('/')[-1]
1366
      try:
1367
        container = self.unrestrictedResolveValue(portal, container_path)
1368
        container._getOb(object_id) # We force access to the object to be sure
1369 1370
                                        # that appropriate exception is thrown
                                        # in case object is already backup and/or removed
1371
        if trash and trashbin is not None:
1372
          self.portal_trash.backupObject(trashbin, container_path, object_id, save=1, keep_subobjects=1)
1373
        if container.meta_type == 'CMF Skins Tool':
1374
          # we are removing a skin folder, check and
1375
          # remove if registered skin selection
1376
          unregisterSkinFolderId(container, object_id,
1377 1378
              container.getSkinSelections())

1379
        container.manage_delObjects([object_id])
1380
        if container.aq_parent.meta_type == 'ERP5 Catalog' and not len(container):
1381
          # We are removing a ZSQLMethod, remove the SQLCatalog if empty
1382
          container.getParentValue().manage_delObjects([container.id])
1383
      except (NotFound, KeyError, BadRequest, AttributeError):
1384
        # object is already backup and/or removed
1385
        pass
1386 1387
    BaseTemplateItem.uninstall(self, context, **kw)

1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
class PathTemplateItem(ObjectTemplateItem):
  """
    This class is used to store objects with wildcards supported.
  """
  def __init__(self, id_list, tool_id=None, **kw):
    BaseTemplateItem.__init__(self, id_list, tool_id=tool_id, **kw)
    id_list = self._archive.keys()
    self._archive.clear()
    self._path_archive = PersistentMapping()
    for id in id_list:
      self._path_archive[id] = None

1400 1401 1402 1403 1404 1405 1406 1407 1408
  def uninstall(self, context, **kw):
    portal = context.getPortalObject()
    trash = kw.get('trash', 0)
    trashbin = kw.get('trashbin', None)
    object_path = kw.get('object_path', None)
    if object_path is not None:
      object_keys = [object_path]
    else:
      object_keys = self._path_archive.keys()
1409 1410
    object_keys.sort()
    object_keys.reverse()
1411 1412
    p = context.getPortalObject()
    for path in object_keys:
1413 1414 1415 1416 1417
      try:
        path_list = self._resolvePath(p, [], path.split('/'))
      except AttributeError:
        # path seems to not exist anymore
        continue
1418 1419 1420
      path_list.sort()
      path_list.reverse()
      for relative_url in path_list:
1421
        try:
Aurel's avatar
Aurel committed
1422 1423
          container_path = relative_url.split('/')[0:-1]
          object_id = relative_url.split('/')[-1]
1424
          container = self.unrestrictedResolveValue(portal, container_path)
1425
          if trash and trashbin is not None:
1426 1427
            self.portal_trash.backupObject(trashbin, container_path,
                                           object_id, save=1,
1428
                                           keep_subobjects=1)
1429 1430 1431 1432 1433 1434
          container.manage_delObjects([object_id])
        except (NotFound, KeyError):
          # object is already backup and/or removed
          pass
    BaseTemplateItem.uninstall(self, context, **kw)

1435 1436 1437
  def _resolvePath(self, folder, relative_url_list, id_list):
    """
      This method calls itself recursively.
1438

1439 1440 1441 1442 1443 1444 1445 1446 1447
      The folder is the current object which contains sub-objects.
      The list of ids are path components. If the list is empty,
      the current folder is valid.
    """
    if len(id_list) == 0:
      return ['/'.join(relative_url_list)]
    id = id_list[0]
    if re.search('[\*\?\[\]]', id) is None:
      # If the id has no meta character, do not have to check all objects.
1448 1449 1450
      obj = folder._getOb(id, None)
      if obj is None:
        raise AttributeError, "Could not resolve '%s' during business template processing." % id
1451
      return self._resolvePath(obj, relative_url_list + [id], id_list[1:])
1452 1453
    path_list = []
    for object_id in fnmatch.filter(folder.objectIds(), id):
1454
      if object_id != "":
1455
        path_list.extend(self._resolvePath(
1456
            folder._getOb(object_id),
1457
            relative_url_list + [object_id], id_list[1:]))
1458
    return path_list
Aurel's avatar
Aurel committed
1459

1460 1461 1462
  def build(self, context, **kw):
    BaseTemplateItem.build(self, context, **kw)
    p = context.getPortalObject()
Aurel's avatar
Aurel committed
1463
    keys = self._path_archive.keys()
1464
    keys.sort()
Aurel's avatar