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

29
from Products.ERP5SyncML.XMLSyncUtils import XMLSyncUtilsMixin
Jean-Paul Smets's avatar
Jean-Paul Smets committed
30 31
from Products.ERP5SyncML.Subscription import Conflict
from Products.CMFCore.utils import getToolByName
32
from DateTime.DateTime import DateTime
Jean-Paul Smets's avatar
Jean-Paul Smets committed
33 34
from email.MIMEBase import MIMEBase
from email import Encoders
Sebastien Robin's avatar
Sebastien Robin committed
35
from AccessControl import ClassSecurityInfo
Fabien Morin's avatar
Fabien Morin committed
36
from Products.ERP5Type import Permissions, Interface
Sebastien Robin's avatar
Sebastien Robin committed
37
from Globals import PersistentMapping
38
import pickle
39
from cStringIO import StringIO
40
from xml.sax.saxutils import escape, unescape
Fabien Morin's avatar
Fabien Morin committed
41
import re
42
import cStringIO
43
from lxml import etree
44
parser = etree.XMLParser(remove_blank_text=True)
45
from xml.marshal.generic import loads as unmarshaler
46
from zLOG import LOG, INFO, DEBUG
Fabien Morin's avatar
Fabien Morin committed
47

48
class ERP5Conduit(XMLSyncUtilsMixin):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
  """
    A conduit is a piece of code in charge of

    - updating an object attributes from an XUpdate XML stream

    (Conduits are not in charge of creating new objects which
    are eventually missing in a synchronisation process)

    If an object has be created during a synchronisation process,
    the way to proceed consists in:

    1- creating an empty instance of the appropriate class
      in the appropriate directory

    2- updating that empty instance with the conduit

    The first implementation of ERP5 synchronisation
    will define a default location to create new objects and
    a default class. This will be defined at the level of the synchronisation
    tool

70 71 72 73 74 75 76 77 78 79 80 81 82 83
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
    Look carefully when we are adding elements,
    for example, when we do 'insert-after', with 2 xupdate:element,
    so adding 2 differents objects, actually it adds only XXXX one XXX object
    In this case the getSubObjectDepth(), doesn't have
    too much sence
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    There is also one problem, when we synchronize a conflict, we are not waiting
    the response of the client, so that we are not sure if it take into account,
    we may have CONFLICT_NOT_SYNCHRONIZED AND CONFLICT_SYNCHRONIZED
    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  """
Jean-Paul Smets's avatar
Jean-Paul Smets committed
84

Fabien Morin's avatar
Fabien Morin committed
85 86 87
  # Declarative interfaces
  __implements__ = ( Interface.IConduit, )

Sebastien Robin's avatar
Sebastien Robin committed
88 89
  # Declarative security
  security = ClassSecurityInfo()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
90

Sebastien Robin's avatar
Sebastien Robin committed
91
  security.declareProtected(Permissions.AccessContentsInformation,'getEncoding')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
92 93 94 95
  def getEncoding(self):
    """
    return the string corresponding to the local encoding
    """
96 97
    #return "iso-8859-1"
    return "utf-8"
Jean-Paul Smets's avatar
Jean-Paul Smets committed
98

Sebastien Robin's avatar
Sebastien Robin committed
99
  security.declareProtected(Permissions.ModifyPortalContent, '__init__')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
100 101
  def __init__(self):
    self.args = {}
102

Sebastien Robin's avatar
Sebastien Robin committed
103
  security.declareProtected(Permissions.ModifyPortalContent, 'addNode')
104
  def addNode(self, xml=None, object=None, previous_xml=None,
105
              object_id=None, sub_object=None, force=0, simulate=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
106 107 108
    """
    A node is added

109 110 111 112 113 114 115 116
    xml : the xml wich contains what we want to add

    object : from where we want to add something

    previous_xml : the previous xml of the object, if any

    force : apply updates even if there's a conflict

Jean-Paul Smets's avatar
Jean-Paul Smets committed
117 118
    This fucntion returns conflict_list, wich is of the form,
    [conflict1,conflict2,...] where conclict1 is of the form :
119
    [object.getPath(),keyword,local_and_actual_value,subscriber_value]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
120
    """
121 122
    reset_local_roles = 0
    reset_workflow = 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
123
    conflict_list = []
124
    xml = self.convertToXml(xml)
125
    if xml is None:
126
      return {'conflict_list': conflict_list, 'object': sub_object}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
127
    # In the case where this new node is a object to add
128
    if xml.xpath('name()') in self.XUPDATE_INSERT_OR_ADD and \
Nicolas Delaby's avatar
Nicolas Delaby committed
129
        self.getSubObjectDepth(xml) == 0:
130
      if self.isHistoryAdd(xml) != -1: # bad hack XXX to be removed
Sebastien Robin's avatar
Sebastien Robin committed
131
        for element in self.getXupdateElementList(xml):
132 133 134 135 136 137 138 139 140
            xml = self.getElementFromXupdate(element)
            conflict_list += self.addNode(
                                    xml=xml,
                                    object=object,
                                    previous_xml=previous_xml,
                                    force=force,
                                    simulate=simulate,
                                    **kw)['conflict_list']
    elif xml.tag == 'object':
141
      if object_id is None:
142
        object_id = self.getAttribute(xml, 'id')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
143
      if object_id is not None:
144 145 146 147 148
        if sub_object is None:
          try:
            sub_object = object._getOb(object_id)
          except (AttributeError, KeyError, TypeError):
            sub_object = None
149
        if sub_object is None: # If so, it doesn't exist
Jean-Paul Smets's avatar
Jean-Paul Smets committed
150
          portal_type = ''
151
          if xml.tag == 'object':
Jean-Paul Smets's avatar
Jean-Paul Smets committed
152
            portal_type = self.getObjectType(xml)
153
          elif xml.xpath('name()') in self.XUPDATE_INSERT_OR_ADD: # Deprecated ???
154
            portal_type = self.getXupdateObjectType(xml) # Deprecated ???
155 156 157 158
          sub_object, reset_local_roles, reset_workflow = self.constructContent(
                                                                  object,
                                                                  object_id,
                                                                  portal_type)
159 160 161 162 163 164
        self.newObject(
                  object=sub_object,
                  xml=xml,
                  simulate=simulate,
                  reset_local_roles=reset_local_roles,
                  reset_workflow=reset_workflow)
165 166
    elif xml.xpath('name()') in self.XUPDATE_INSERT_OR_ADD \
         and self.getSubObjectDepth(xml) >= 1:
167 168
      sub_object_id = self.getSubObjectId(xml)
      if previous_xml is not None and sub_object_id is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
169
        # Find the previous xml corresponding to this subobject
170
        sub_previous_xml = self.getSubObjectXml(sub_object_id, previous_xml)
171
        #LOG('addNode', DEBUG,'isSubObjectModification sub_previous_xml: %s' % str(sub_previous_xml))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
172 173 174
        if sub_previous_xml is not None:
          sub_object = None
          try:
175
            sub_object = object._getOb(sub_object_id)
176
          except (AttributeError, KeyError, TypeError):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
177 178
            pass
          if sub_object is not None:
179
            #LOG('addNode', DEBUG, 'subobject.id: %s' % sub_object.id)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
180 181 182
            # Change the xml in order to directly apply
            # modifications to the subobject
            sub_xml = self.getSubObjectXupdate(xml)
183
            #LOG('addNode', DEBUG, 'sub_xml: %s' % str(sub_xml))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
184 185
            # Then do the udpate
            conflict_list += self.addNode(xml=sub_xml,object=sub_object,
186
                            previous_xml=sub_previous_xml, force=force,
187
                            simulate=simulate, **kw)['conflict_list']
188
    elif xml.tag == self.history_tag or self.isHistoryAdd(xml)>0:
189
      conflict_list += self.addWorkflowNode(object, xml, simulate)
190 191 192 193
    #elif xml.tag in self.local_role_list or self.isLocalRole(xml)>0 and not simulate:
    elif xml.tag in self.local_role_list:
      self.addLocalRoleNode(object, xml)
    elif xml.tag in self.local_permission_list:
194
      conflict_list += self.addLocalPermissionNode(object, xml)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
195
    else:
196 197
      conflict_list += self.updateNode(xml=xml,object=object, force=force,
                                       simulate=simulate,  **kw)
198 199
    # We must returns the object created
    return {'conflict_list':conflict_list, 'object': sub_object}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
200

Sebastien Robin's avatar
Sebastien Robin committed
201
  security.declareProtected(Permissions.ModifyPortalContent, 'deleteNode')
202 203
  def deleteNode(self, xml=None, object=None, object_id=None, force=None,
                 simulate=0, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
204 205 206
    """
    A node is deleted
    """
207
    # In the case where we have to delete an object
208
    #LOG('ERP5Conduit.deleteNode', DEBUG, 'deleteNode, object path: %s' % repr(object.getPhysicalPath()))
209
    conflict_list = []
210 211
    if xml is not None:
      xml = self.convertToXml(xml)
212
    if object_id is None:
213
      #LOG('ERP5Conduit.deleteNode', DEBUG, 'deleteNode, SubObjectDepth: %i' % self.getSubObjectDepth(xml))
214
      if xml.tag == self.xml_object_tag:
215
        object_id = self.getAttribute(xml,'id')
Nicolas Delaby's avatar
Nicolas Delaby committed
216
      elif self.getSubObjectDepth(xml) == 1:
217
        object_id = self.getSubObjectId(xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
218
      elif self.getSubObjectDepth(xml) == 2:
219 220
        # we have to call delete node on a subsubobject
        sub_object_id = self.getSubObjectId(xml)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
221
        try:
222 223 224
          sub_object = object._getOb(sub_object_id)
          sub_xml = self.getSubObjectXupdate(xml)
          conflict_list += self.deleteNode(xml=sub_xml,object=sub_object,
225
                                       force=force, simulate=simulate, **kw)
226
        except (KeyError, AttributeError, TypeError):
227
          #LOG('ERP5Conduit.deleteNode', DEBUG, 'deleteNode, Unable to delete SubObject: %s' % str(sub_object_id))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
228
          pass
229
    if object_id is not None: # We do have an object_id
230
      self.deleteObject(object, object_id)
231 232
    # In the case where we have to delete an user role
    # If we are still there, this means the delete is for this node
233
    elif xml.xpath('name()') in self.XUPDATE_DEL:
234
      xml = self.getElementFromXupdate(xml)
235
      if xml.tag in self.local_role_list and not simulate:
236 237
        # We want to del a local role
        user = self.getAttribute(xml,'id')
238
        #LOG('ERP5Conduit.deleteNode local_role: ', DEBUG, 'user: %s' % repr(user))
239
        if xml.tag.find(self.local_role_tag)>=0:
240
          object.manage_delLocalRoles([user])
241
        elif xml.tag.find(self.local_group_tag)>=0:
242
          object.manage_delLocalGroupRoles([user])
243
      if xml.tag in self.local_permission_list and not simulate:
244
        permission = self.getAttribute(xml,'id')
245
        object.manage_setLocalPermissions(permission)
246
    return conflict_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
247

248 249 250 251 252
  security.declareProtected(Permissions.ModifyPortalContent, 'deleteObject')
  def deleteObject(self, object, object_id):
    try:
      object._delObject(object_id)
    except (AttributeError, KeyError):
253
      #LOG('ERP5Conduit.deleteObject', DEBUG, 'Unable to delete: %s' % str(object_id))
254 255
      pass

Sebastien Robin's avatar
Sebastien Robin committed
256
  security.declareProtected(Permissions.ModifyPortalContent, 'updateNode')
257 258
  def updateNode(self, xml=None, object=None, previous_xml=None, force=0,
                 simulate=0,  **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
259 260 261 262 263 264 265 266
    """
    A node is updated with some xupdate
      - xml : the xml corresponding to the update, it should be xupdate
      - object : the object on wich we want to apply the xupdate
      - [previous_xml] : the previous xml of the object, it is mandatory
                         when we have sub objects
    """
    conflict_list = []
267 268
    if xml is None:
      return {'conflict_list':conflict_list, 'object':object}
269
    xml = self.convertToXml(xml)
270
    #LOG('ERP5Conduit.updateNode', DEBUG, 'xml.tag: %s' % xml.tag)
271
    #LOG('ERP5Conduit.updateNode, force: ', DEBUG, force)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
272
    # we have an xupdate xml
273
    if xml.xpath('name()') == 'xupdate:modifications':
274 275 276 277 278
      conflict_list += self.applyXupdate(object=object,
                                         xupdate=xml,
                                         conduit=self,
                                         previous_xml=previous_xml,
                                         force=force,
Nicolas Delaby's avatar
Nicolas Delaby committed
279
                                         simulate=simulate, **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
280 281 282
    # we may have only the part of an xupdate
    else:
      args = {}
283
      if self.isProperty(xml):
284
        keyword = None
285 286 287 288 289 290 291 292 293 294 295
        value = xml.attrib.get('select', None)
        if value is not None:
          select_list = value.split('/') # Something like:
                                         #('','object[1]','sid[1]')
          new_select_list = ()
          for select_item in select_list:
            if select_item.find('[') >= 0:
              select_item = select_item[:select_item.find('[')]
            new_select_list += (select_item,)
          select_list = new_select_list # Something like : ('','object','sid')
          keyword = select_list[len(select_list)-1] # this will be 'sid'
296
        data = None
297 298 299 300
        if xml.xpath('name()') not in self.XUPDATE_INSERT_OR_ADD:
          for subnode in xml:
            if subnode.xpath('name()') in self.XUPDATE_EL:
              keyword = subnode.attrib.get('name', None)
301
              data_xml = subnode
302 303 304 305 306 307 308 309 310
        else:
          #We can call add node
          conflict_list += self.addNode(xml=xml,
                                        object=object,
                                        force=force,
                                        simulate=simulate,
                                        **kw)
          return conflict_list

311
        if xml.xpath('name()') in self.XUPDATE_DEL:
312 313 314 315 316 317
          conflict_list += self.deleteNode(xml=xml,
                                           object=object,
                                           force=force,
                                           simulate=simulate,
                                           **kw)
          return conflict_list
318
        if keyword is None: # This is not a selection, directly the property
319
          keyword = xml.tag
Jean-Paul Smets's avatar
Jean-Paul Smets committed
320 321
        if not (keyword in self.NOT_EDITABLE_PROPERTY):
          # We will look for the data to enter
322
          data_type = object.getPropertyType(keyword)
323
          #LOG('ERP5Conduit.updateNode', DEBUG, 'data_type: %s for keyword: %s' % (str(data_type), keyword))
324
          data = self.convertXmlValue(xml, data_type=data_type)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
325 326 327 328 329
          args[keyword] = data
          args = self.getFormatedArgs(args=args)
          # This is the place where we should look for conflicts
          # For that we need :
          #   - data : the data from the remote box
Fabien Morin's avatar
Fabien Morin committed
330 331
          #   - old_data : the data from this box but at the time of the i
          #last synchronization
Jean-Paul Smets's avatar
Jean-Paul Smets committed
332 333
          #   - current_data : the data actually on this box
          isConflict = 0
334
          if (previous_xml is not None) and (not force):
Fabien Morin's avatar
Fabien Morin committed
335
          # if no previous_xml, no conflict
336
            old_data = self.getObjectProperty(keyword, previous_xml,
337
                                              data_type=data_type)
338 339
            #current_data = object.getProperty(keyword)
            current_data = self.getProperty(object, keyword)
340 341 342
            #LOG('ERP5Conduit.updateNode', DEBUG, 'Conflict data: %s' % str(data))
            #LOG('ERP5Conduit.updateNode', DEBUG, 'Conflict old_data: %s' % str(old_data))
            #LOG('ERP5Conduit.updateNode', DEBUG, 'Conflict current_data: %s' % str(current_data))
343 344
            if (old_data != current_data) and (data != current_data) \
                and keyword not in self.force_conflict_list:
345
              #LOG('ERP5Conduit.updateNode', DEBUG, 'Conflict on : %s' % keyword)
346 347
              # Hack in order to get the synchronization working for demo
              # XXX this have to be removed after
348 349
              #if not (data_type in self.binary_type_list):
              if 1:
350 351
                # This is a conflict
                isConflict = 1
352
                xml_string = etree.tostring(xml, encoding='utf-8')
Sebastien Robin's avatar
Sebastien Robin committed
353 354
                conflict = Conflict(object_path=object.getPhysicalPath(),
                                    keyword=keyword)
355
                conflict.setXupdate(xml_string)
Sebastien Robin's avatar
Sebastien Robin committed
356 357 358
                if not (data_type in self.binary_type_list):
                  conflict.setLocalValue(current_data)
                  conflict.setRemoteValue(data)
359
                conflict_list += [conflict]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
360
          # We will now apply the argument with the method edit
Nicolas Delaby's avatar
Nicolas Delaby committed
361 362
          if args != {} and (isConflict == 0 or force) and (not simulate):
            self.editDocument(object=object, **args)
363
            # It is sometimes required to do something after an edit
Nicolas Delaby's avatar
Nicolas Delaby committed
364
            if getattr(object, 'manage_afterEdit', None) is not None:
365
              object.manage_afterEdit()
366

Jean-Paul Smets's avatar
Jean-Paul Smets committed
367 368
        if keyword == 'object':
          # This is the case where we have to call addNode
369 370 371 372 373
          conflict_list += self.addNode(xml=xml,
                                        object=object,
                                        force=force,
                                        simulate=simulate,
                                        **kw)['conflict_list']
374
        elif keyword == self.history_tag and not simulate:
375
          # This is the case where we have to call addNode
376 377 378
          conflict_list += self.addNode(xml=subnode, object=object,
                                        force=force, simulate=simulate,
                                        **kw)['conflict_list']
379
        elif keyword in (self.local_role_tag, self.local_permission_tag) and not simulate:
380
          # This is the case where we have to update Roles or update permission
381
          #LOG('ERP5Conduit.updateNode', DEBUG, 'we will add a local role')
382 383 384 385
          #user = self.getSubObjectId(xml)
          #roles = self.convertXmlValue(data,data_type='tokens')
          #object.manage_setLocalRoles(user,roles)
          xml = self.getElementFromXupdate(xml)
386 387 388
          conflict_list += self.addNode(xml=xml, object=object,
                                       force=force, simulate=simulate,
                                       **kw)['conflict_list']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
389 390 391
      elif self.isSubObjectModification(xml):
        # We should find the object corresponding to
        # this update, so we have to look in the previous_xml
392
        sub_object_id = self.getSubObjectId(xml)
393
        #LOG('ERP5Conduit.updateNode', DEBUG,'isSubObjectModification sub_object_id: %s' % sub_object_id)
394
        if previous_xml is not None and sub_object_id is not None:
395
          sub_previous_xml = self.getSubObjectXml(sub_object_id, previous_xml)
396
          #LOG('ERP5Conduit.updateNode', DEBUG, 'isSubObjectModification sub_previous_xml: %s' % str(sub_previous_xml))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
397 398 399
          if sub_previous_xml is not None:
            sub_object = None
            try:
400
              sub_object = object._getOb(sub_object_id)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
401 402 403
            except KeyError:
              pass
            if sub_object is not None:
404
              #LOG('ERP5Conduit.updateNode', DEBUG, 'subobject.id: %s' % sub_object.id)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
405 406 407
              # Change the xml in order to directly apply
              # modifications to the subobject
              sub_xml = self.getSubObjectXupdate(xml)
408
              #LOG('ERP5Conduit.updateNode', DEBUG, 'sub_xml: %s' % str(sub_xml))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
409
              # Then do the udpate
410 411 412 413
              conflict_list += self.updateNode(xml=sub_xml, object=sub_object,
                                               force=force,
                                               previous_xml=sub_previous_xml,
                                               simulate=simulate, **kw)
414 415 416 417 418 419 420 421 422 423 424 425 426
        elif previous_xml is None and xml is not None and sub_object_id is not None:
          sub_object = None
          try:
            sub_object = object[sub_object_id]
          except KeyError:
            pass
          if sub_object is not None:
            sub_xml = self.getSubObjectXupdate(xml)
            conflict_list += self.updateNode(xml=sub_xml,
                                             object=sub_object,
                                             force=force,
                                             simulate=simulate,
                                             **kw)
427 428 429 430 431 432 433 434 435 436 437 438 439
        elif previous_xml is None and xml is not None and sub_object_id is not None:
          sub_object = None
          try:
            sub_object = object[sub_object_id]
          except KeyError:
            pass
          if sub_object is not None:
            sub_xml = self.getSubObjectXupdate(xml)
            conflict_list += self.updateNode(xml=sub_xml,
                                             object=sub_object,
                                             force=force,
                                             simulate=simulate,
                                             **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
440 441
    return conflict_list

442 443
  security.declareProtected(Permissions.AccessContentsInformation, 
      'getFormatedArgs')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
444 445 446 447 448 449 450 451
  def getFormatedArgs(self, args=None):
    """
    This lookd inside the args dictionnary and then
    convert any unicode string to string
    """
    new_args = {}
    for keyword in args.keys():
      data = args[keyword]
452
      if isinstance(keyword, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
453
        keyword = keyword.encode(self.getEncoding())
454
      if isinstance(data, (tuple, list)):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
455 456
        new_data = []
        for item in data:
457
          if isinstance(item, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
458
            item = item.encode(self.getEncoding())
459
          new_data.append(item)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
460
        data = new_data
461
      if isinstance(data, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
462 463
        data = data.encode(self.getEncoding())
      if keyword == 'binary_data':
464
        #LOG('ERP5Conduit.getFormatedArgs', DEBUG, 'binary_data keyword: %s' % str(keyword))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
465 466 467 468 469 470 471
        msg = MIMEBase('application','octet-stream')
        Encoders.encode_base64(msg)
        msg.set_payload(data)
        data = msg.get_payload(decode=1)
      new_args[keyword] = data
    return new_args

472
  security.declareProtected(Permissions.AccessContentsInformation, 'isProperty')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
473 474 475
  def isProperty(self, xml):
    """
    Check if it is a simple property
476 477 478 479 480 481 482 483
    not an attribute @type it's a metadata
    """
    bad_list = (self.sub_object_exp, self.history_exp, self.attribute_type_exp,)
    value = xml.attrib.get('select', None)
    if value is not None:
      for bad_string in bad_list:
        if bad_string.search(value) is not None:
          return 0
Jean-Paul Smets's avatar
Jean-Paul Smets committed
484 485
    return 1

486
  security.declareProtected(Permissions.AccessContentsInformation,
487
      'getSubObjectXupdate')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
488 489 490 491 492
  def getSubObjectXupdate(self, xml):
    """
    This will change the xml in order to change the update
    from the object to the subobject
    """
493 494 495
    from copy import deepcopy
    xml_copy = deepcopy(xml)
    self.changeSubObjectSelect(xml_copy)
496
    return xml_copy
Jean-Paul Smets's avatar
Jean-Paul Smets committed
497

498
  security.declareProtected(Permissions.AccessContentsInformation,
499
      'isHistoryAdd')
500
  def isHistoryAdd(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
501
    bad_list = (self.history_exp,)
502 503 504 505 506 507 508 509
    value = xml.attrib.get('select')
    if value is not None:
      for bad_string in bad_list:
        if bad_string.search(value) is not None:
          if self.bad_history_exp.search(value) is None:
            return 1
          else:
            return -1
510 511
    return 0

512 513
  security.declareProtected(Permissions.AccessContentsInformation, 
      'isSubObjectModification')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
514 515 516 517
  def isSubObjectModification(self, xml):
    """
    Check if it is a modification from an subobject
    """
518
    good_list = (self.sub_object_exp,)
519 520 521 522 523
    value = xml.attrib.get('select', None)
    if value is not None:
      for good_string in good_list:
        if good_string.search(value) is not None:
          return 1
Jean-Paul Smets's avatar
Jean-Paul Smets committed
524 525
    return 0

526 527
  security.declareProtected(Permissions.AccessContentsInformation, 
      'getSubObjectDepth')
528 529 530 531 532 533 534
  def getSubObjectDepth(self, xml):
    """
    Give the Depth of a subobject modification
    0 means, no depth
    1 means it is a subobject
    2 means it is more depth than subobject
    """
535 536
    #LOG('getSubObjectDepth',0,'xml.tag: %s' % xml.tag)
    if xml.xpath('name()') in self.XUPDATE_TAG:
537
      i = 0
538
      if xml.xpath('name()') in self.XUPDATE_INSERT:
539
        i = 1
540 541 542 543 544 545 546 547 548 549 550 551 552 553
      #LOG('getSubObjectDepth',0,'xml2.tag: %s' % xml.tag)
      value = xml.attrib.get('select', None)
      if value is not None:
        #LOG('getSubObjectDepth',0,'subnode.nodeValue: %s' % subnode.nodeValue)
        if self.sub_sub_object_exp.search(value) is not None:
          return 2 # This is sure in all cases
        elif self.sub_object_exp.search(value) is not None:
          #new_select = self.getSubObjectSelect(value) # Still needed ???
          #if self.getSubObjectSelect(new_select) != new_select:
          #  return (2 - i)
          #return (1 - i)
          return (2 - i)
        elif self.object_exp.search(value) is not None:
          return (1 - i)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
554 555
    return 0

556 557 558
  security.declareProtected(Permissions.ModifyPortalContent,
      'changeSubObjectSelect')
  def changeSubObjectSelect(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
559
    """
560 561
    Return a string wich is the selection for the subobject
    ex: for "/object[@id='161']/object[@id='default_address']/street_address"
562
    it returns "/object[@id='default_address']/street_address"
Jean-Paul Smets's avatar
Jean-Paul Smets committed
563
    """
564 565
    select = xml.attrib.get('select')
    if self.object_exp.search(select) is not None:
566
      s = '/'
567 568
      if re.search('/.*/', select) is not None: # This means we have more than just object
        new_value = select[select.find(s, select.find(s)+1):]
569 570
      else:
        new_value = '/'
571
      select = new_value
572
    xml.attrib['select'] = select
573

574 575
  security.declareProtected(Permissions.AccessContentsInformation, 
      'getSubObjectId')
576 577 578 579 580
  def getSubObjectId(self, xml):
    """
    Return the id of the subobject in an xupdate modification
    """
    object_id = None
581 582 583 584 585 586 587
    value = xml.attrib.get('select', None)
    if value is not None:
      if self.object_exp.search(value) is not None:
        s = "'"
        first = value.find(s) + 1
        object_id = value[first:value.find(s, first)]
        return object_id
588 589
    return object_id

590 591
  security.declareProtected(Permissions.AccessContentsInformation, 
      'getHistoryIdFromSelect')
592 593 594 595 596
  def getHistoryIdFromSelect(self, xml):
    """
    Return the id of the subobject in an xupdate modification
    """
    object_id = None
597 598 599 600 601 602 603 604
    value = xml.attrib.get('select', None)
    if value is not None:
      if self.history_exp.search(value) is not None:
        s = self.history_tag
        object_id = value[value.find(s):]
        object_id = object_id[object_id.find("'") + 1:]
        object_id = object_id[:object_id.find("'")]
        return object_id
605 606
    return object_id

607 608
  security.declareProtected(Permissions.AccessContentsInformation,
      'getSubObjectXml')
609 610 611 612 613
  def getSubObjectXml(self, object_id, xml):
    """
    Return the xml of the subobject which as the id object_id
    """
    xml = self.convertToXml(xml)
614 615 616
    for subnode in xml:
      if subnode.tag == self.xml_object_tag:
        if object_id == self.getAttribute(subnode, 'id'):
617 618
          return subnode
    return None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
619

Sebastien Robin's avatar
Sebastien Robin committed
620
  security.declareProtected(Permissions.AccessContentsInformation,'getAttribute')
621
  def getAttribute(self, xml, param):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
622
    """
623
    Retrieve the given parameter from the xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
624
    """
625
    return xml.attrib.get(param, None)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
626

Sebastien Robin's avatar
Sebastien Robin committed
627
  security.declareProtected(Permissions.AccessContentsInformation,'getObjectProperty')
628
  def getObjectProperty(self, property, xml, data_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
629 630 631
    """
    Retrieve the given property
    """
632
    xml = self.convertToXml(xml)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
633
    # document, with childNodes[0] a DocumentType and childNodes[1] the Element Node
634 635 636
    for subnode in xml:
      if subnode.tag == property:
        return self.convertXmlValue(subnode)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
637 638
    return None

Sebastien Robin's avatar
Sebastien Robin committed
639
  security.declareProtected(Permissions.AccessContentsInformation,'convertToXml')
640
  def convertToXml(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
641 642 643
    """
    if xml is a string, convert it to a node
    """
644 645 646
    if xml is None: return None
    if isinstance(xml, (str, unicode)):
      if isinstance(xml, unicode):
Sebastien Robin's avatar
Sebastien Robin committed
647
        xml = xml.encode('utf-8')
648
      xml = etree.XML(xml, parser=parser)
649
    # If we have the xml from the node erp5, we just take the subnode
650 651
    if xml.tag == 'erp5':
      xml = xml[0]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
652 653
    return xml

Sebastien Robin's avatar
Sebastien Robin committed
654
  security.declareProtected(Permissions.AccessContentsInformation,'getObjectType')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
655 656 657 658
  def getObjectType(self, xml):
    """
    Retrieve the portal type from an xml
    """
659
    return '%s' % xml.xpath('string(.//@portal_type)')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
660

Sebastien Robin's avatar
Sebastien Robin committed
661
  security.declareProtected(Permissions.AccessContentsInformation,'getPropertyType')
662 663 664 665
  def getPropertyType(self, xml):
    """
    Retrieve the portal type from an xml
    """
666
    return '%s' % xml.xpath('string(.//@type)')
667

Sebastien Robin's avatar
Sebastien Robin committed
668
  security.declareProtected(Permissions.AccessContentsInformation,'getXupdateObjectType')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
669 670 671
  def getXupdateObjectType(self, xml):
    """
    Retrieve the portal type from an xupdate
672
    XXXX  This should not be used any more !!! XXXXXXXXXXX
Jean-Paul Smets's avatar
Jean-Paul Smets committed
673
    """
674
    return xml.xpath('string(.//*[name() == "xupdate:attribute"][@name = "portal_type"])') or None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
675

Sebastien Robin's avatar
Sebastien Robin committed
676
  security.declareProtected(Permissions.ModifyPortalContent, 'newObject')
677 678
  def newObject(self, object=None, xml=None, simulate=0, reset_local_roles=1,
                reset_workflow=1):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
679 680 681 682 683
    """
      modify the object with datas from
      the xml (action section)
    """
    args = {}
684 685
    if simulate:
      return
686
    # Retrieve the list of users with a role and delete default roles
687
    if reset_local_roles:
Nicolas Delaby's avatar
Nicolas Delaby committed
688
      user_role_list = [x[0] for x in object.get_local_roles()]
689
      object.manage_delLocalRoles(user_role_list)
Nicolas Delaby's avatar
Nicolas Delaby committed
690
    if getattr(object, 'workflow_history', None) is not None and reset_workflow:
691
      object.workflow_history = PersistentMapping()
692 693 694 695 696
    if xml.tag.find('xupdate') >= 0:
      xml = xml[0]
    for subnode in xml.xpath('*'):
      #get only Element nodes (not Comments or Processing instructions)
      if subnode.tag not in self.NOT_EDITABLE_PROPERTY:
697
        keyword_type = self.getPropertyType(subnode)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
698
        # This is the case where the property is a list
699 700 701
        keyword = subnode.tag
        args[keyword] = self.convertXmlValue(subnode, keyword_type)
      elif subnode.tag in self.ADDABLE_PROPERTY + (self.xml_object_tag,):
Nicolas Delaby's avatar
Nicolas Delaby committed
702
        self.addNode(object=object, xml=subnode, force=1)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
703 704 705 706
    # We should first edit the object
    args = self.getFormatedArgs(args=args)
    # edit the object with a dictionnary of arguments,
    # like {"telephone_number":"02-5648"}
Nicolas Delaby's avatar
Nicolas Delaby committed
707
    self.editDocument(object=object, **args)
Nicolas Delaby's avatar
Nicolas Delaby committed
708
    if getattr(object, 'manage_afterEdit', None) is not None:
709
      object.manage_afterEdit()
Nicolas Delaby's avatar
Nicolas Delaby committed
710
    self.afterNewObject(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
711

712 713 714 715
    ## Then we may create subobject
    #for subnode in xml:
      #if subnode.tag in (self.xml_object_tag,): #,self.history_tag):
        #self.addNode(object=object, xml=subnode)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
716

717 718 719 720
  security.declareProtected(Permissions.AccessContentsInformation,'afterNewObject')
  def afterNewObject(self, object):
    pass

Sebastien Robin's avatar
Sebastien Robin committed
721
  security.declareProtected(Permissions.AccessContentsInformation,'getStatusFromXml')
722 723 724 725 726
  def getStatusFromXml(self, xml):
    """
    Return a worklow status from xml
    """
    status = {}
727 728
    for subnode in xml:
      keyword = subnode.tag
Nicolas Delaby's avatar
Nicolas Delaby committed
729
      value = self.getObjectProperty(keyword, xml)
730 731 732
      status[keyword] = value
    return status

Sebastien Robin's avatar
Sebastien Robin committed
733
  security.declareProtected(Permissions.AccessContentsInformation,'getXupdateElementList')
734 735 736 737
  def getXupdateElementList(self, xml):
    """
    Retrieve the list of xupdate:element subnodes
    """
738
    return xml.xpath('|'.join(['.//*[name() = "%s"]' % name for name in self.XUPDATE_EL]))
739

Sebastien Robin's avatar
Sebastien Robin committed
740
  security.declareProtected(Permissions.AccessContentsInformation,'getElementFromXupdate')
741 742 743 744
  def getElementFromXupdate(self, xml):
    """
    from a xupdate:element returns the element as xml
    """
745 746 747 748 749 750 751 752
    if xml.xpath('name()') in self.XUPDATE_EL:
      result = '<'
      tag_name = xml.attrib.get('name')
      result += tag_name
      for subnode in xml:
        if subnode.xpath('name()') == 'xupdate:attribute':
          result += ' %s=' % subnode.attrib.get('name')
          result += '"%s"' % subnode.text
753
      result += '>'
754
      # Then dumps the xml and remove what we does'nt want
755
      xml_string = self.nodeToString(xml)
756 757 758
      maxi = max(xml_string.find('>')+1,\
                 xml_string.rfind('</xupdate:attribute>')+len('</xupdate:attribute>'))
      result += xml_string[maxi:xml_string.find('</xupdate:element>')]
759 760 761
      result += '</%s>' % tag_name
      return self.convertToXml(result)
    if xml.xpath('name()') in (self.XUPDATE_UPDATE + self.XUPDATE_DEL):
762
      result = u'<'
763
      attribute = xml.attrib.get('select')
764 765 766 767 768 769 770 771
      s = '[@id='
      s_place = attribute.find(s)
      select_id = None
      if (s_place > 0):
        select_id = attribute[s_place+len(s):]
        select_id = select_id[:select_id.find("'",1)+1]
      else:
        s_place = len(attribute)
772
      property = attribute[:s_place].strip('/')
773 774 775 776
      result += property
      if select_id is not None:
        result += ' id=%s' % select_id
      result +=  '>'
777
      xml_string = self.nodeToString(xml)
778
      maxi = xml_string.find('>')+1
779
      result += xml_string[maxi:xml_string.find('</%s>' % xml.xpath('name()'))]
Nicolas Delaby's avatar
Nicolas Delaby committed
780
      result += '</%s>' % (property)
Nicolas Delaby's avatar
Nicolas Delaby committed
781
      #LOG('getElementFromXupdate, result:',0,repr(result))
782
      return self.convertToXml(result)
783 784
    return xml

Sebastien Robin's avatar
Sebastien Robin committed
785
  security.declareProtected(Permissions.AccessContentsInformation,'getWorkflowActionFromXml')
786 787 788 789 790
  def getWorkflowActionFromXml(self, xml):
    """
    Return the list of workflow actions
    """
    action_list = []
791 792
    if xml.xpath('name()') in self.XUPDATE_EL:
      action_list.append(xml)
793
      return action_list
794 795 796
    for subnode in xml:
      if subnode.tag == self.action_tag:
        action_list.append(subnode)
797 798
    return action_list

Sebastien Robin's avatar
Sebastien Robin committed
799
  security.declareProtected(Permissions.AccessContentsInformation,'convertXmlValue')
800
  def convertXmlValue(self, node, data_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
801 802
    """
    It is possible that the xml change the value, for example
803
    there is some too much '\n' and some spaces. We have to do some extra
804 805
    things so that we convert correctly the value
    XXXNicolas: I'm totally disagree with, so i comment this code
806
    """
807 808 809 810 811 812 813 814
    if node is None: return None
    if data_type is None:
      data_type = self.getPropertyType(node)
    if data_type == self.none_type: return None
    data = node.text
    if data is not None and isinstance(data, unicode):
      data = data.encode('utf-8')
    elif data is None:
815 816
      if data_type in self.list_type_list:
        data = ()
817
      elif data_type in self.text_type_list:
818
        data = ''
819 820 821
      return data
    # We can now convert string in tuple, dict, binary...
    if data_type in self.list_type_list:
822
      data = unmarshaler(node.text)
823
    elif data_type in self.text_type_list:
824
      data = unescape(data)
825
    elif data_type in self.pickle_type_list:
826
      msg = MIMEBase('application', 'octet-stream')
827 828 829
      Encoders.encode_base64(msg)
      msg.set_payload(data)
      data = msg.get_payload(decode=1)
830
      data = pickle.loads(data)
831 832
    elif data_type in self.date_type_list:
      data = DateTime(data)
Sebastien Robin's avatar
Sebastien Robin committed
833 834
    elif data_type in self.int_type_list:
      data = int(data)
Sebastien Robin's avatar
Sebastien Robin committed
835 836 837 838
    elif data_type in self.dict_type_list: # only usefull for CPS, with data = '{fr:1}'
      if data == '{}':
        data = {}
      else:
839
        dict_list = map(lambda x:x.split(':'), data[1:-1].split(','))
Sebastien Robin's avatar
Sebastien Robin committed
840 841
        data = map(lambda (x,y):(x.replace(' ','').replace("'",''),int(y)),dict_list)
        data = dict(data)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
842 843
    return data

844 845
  # XXX is it the right place ? It should be in XupdateUtils, but here we
  # have some specific things to do
Sebastien Robin's avatar
Sebastien Robin committed
846
  security.declareProtected(Permissions.ModifyPortalContent, 'applyXupdate')
847 848
  def applyXupdate(self, object=None, xupdate=None, conduit=None, force=0,
                   simulate=0, **kw):
849 850 851 852
    """
    Parse the xupdate and then it will call the conduit
    """
    conflict_list = []
853
    if isinstance(xupdate, (str, unicode)):
854
      xupdate = etree.XML(xupdate, parser=parser)
855 856 857 858 859
    #When xupdate mix different object, (like object and his subobject) we need to treat them separatly
    if self.isMixedXupdate(xupdate):
      #return to updateNode with only one line
      #del all sub_element
      #clean the node
860
      for subnode in xupdate:
861 862 863 864 865 866 867
        #Create one xupdate:modification per update node
        conflict_list += self.updateNode(xml=subnode,
                                         object=object,
                                         force=force,
                                         simulate=simulate,
                                         **kw)
      return conflict_list
868
    for subnode in xupdate:
869 870
      sub_xupdate = self.getSubObjectXupdate(subnode)
      selection_name = ''
871 872 873 874 875 876 877 878 879 880
      if subnode.xpath('name()') in self.XUPDATE_INSERT_OR_ADD:
        conflict_list += conduit.addNode(xml=sub_xupdate,object=object,
                                         force=force, simulate=simulate,
                                         **kw)['conflict_list']
      elif subnode.xpath('name()') in self.XUPDATE_DEL:
        conflict_list += conduit.deleteNode(xml=sub_xupdate, object=object,
                                            force=force, simulate=simulate, **kw)
      elif subnode.xpath('name()') in self.XUPDATE_UPDATE:
        conflict_list += conduit.updateNode(xml=sub_xupdate, object=object,
                                            force=force, simulate=simulate, **kw)
881 882 883

    return conflict_list

884 885
  def isMixedXupdate(self, xml):
    #If an xupdate:modifications contains modification which concerns different objects
886
    subnode_list = xml
887 888 889
    nb_sub = len(subnode_list)
    comp = 0
    for subnode in subnode_list:
890
      value = self.getAttribute(subnode, 'select')
891
      if self.object_exp.search(value) is not None:
892 893 894 895 896
        comp += 1
    if nb_sub == comp:
      return 0
    return 1

897 898
  def isWorkflowActionAddable(self, object=None, status=None, wf_tool=None,
                              wf_id=None, xml=None):
Sebastien Robin's avatar
Sebastien Robin committed
899 900
    """
    Some checking in order to check if we should add the workfow or not
901 902 903 904
    We should not returns a conflict list as we wanted before, we should
    instead go to a conflict state.
    """
    # We first test if the status in not already inside the workflow_history
905 906
    return 1
    # XXX Disable for now
907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922
    wf_history = object.workflow_history
    if wf_history.has_key(wf_id):
      action_list = wf_history[wf_id]
    else: action_list = []
    addable = 1
    for action in action_list:
      this_one = 1
      for key in action.keys():
        if status[key] != action[key]:
          this_one = 0
          break
      if this_one:
        addable = 0
        break
    return addable

923
  security.declareProtected(Permissions.ModifyPortalContent, 'constructContent')
924
  def constructContent(self, object, object_id, portal_type):
925 926 927 928 929
    """
    This allows to specify how to construct a new content.
    This is really usefull if you want to write your
    own Conduit.
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
930
    #LOG('ERP5Conduit.addNode',0,'portal_type: |%s|' % str(portal_type))
931 932

    object.newContent(portal_type=portal_type, id=object_id)
933
    subobject = object._getOb(object_id)
934
    return subobject, 1, 1
935

936 937 938 939 940
  security.declareProtected(Permissions.ModifyPortalContent, 'addWorkflowNode')
  def addWorkflowNode(self, object, xml, simulate):
    """
    This allows to specify how to handle the workflow informations.
    This is really usefull if you want to write your own Conduit.
941
    """
942 943 944 945 946 947
    conflict_list = []
    # We want to add a workflow action
    wf_tool = getToolByName(object,'portal_workflow')
    wf_id = self.getAttribute(xml,'id')
    if wf_id is None: # History added by xupdate
      wf_id = self.getHistoryIdFromSelect(xml)
948
      xml = xml[0]
949 950
    #for action in self.getWorkflowActionFromXml(xml):
    status = self.getStatusFromXml(xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
951
    #LOG('addNode, status:',0,status)
952 953 954 955 956
    add_action = self.isWorkflowActionAddable(object=object,
                                           status=status,wf_tool=wf_tool,
                                           wf_id=wf_id,xml=xml)
    if add_action and not simulate:
      wf_tool.setStatusOf(wf_id,object,status)
957 958

    # Specific CPS, try to remove duplicate lines in portal_repository._histories
Sebastien Robin's avatar
Sebastien Robin committed
959
    tool = getToolByName(self,'portal_repository',None)
960
    if tool is not None:
Nicolas Delaby's avatar
Nicolas Delaby committed
961
      if getattr(self, 'getDocid', None) is not None:
962 963 964 965 966 967 968 969
        docid = self.getDocid()
        history = tool.getHistory(docid)
        new_history = ()
        for history_line in history:
          if history_line not in new_history:
            new_history += (history_line,)
        tool.setHistory(docid,new_history)

970
    return conflict_list
971

972 973 974 975 976
  security.declareProtected(Permissions.ModifyPortalContent, 'addLocalRoleNode')
  def addLocalRoleNode(self, object, xml):
    """
    This allows to specify how to handle the local role informations.
    This is really usefull if you want to write your own Conduit.
977
    """
978
    # We want to add a local role
979 980
    roles = self.convertXmlValue(xml, data_type='tokens')
    user = self.getAttribute(xml, 'id')
981
    roles = list(roles) # Needed for CPS, or we have a CPS error
982
    #LOG('local_role: ',0,'user: %s roles: %s' % (repr(user),repr(roles)))
983 984
    #user = roles[0]
    #roles = roles[1:]
985 986 987 988
    if xml.tag.find(self.local_role_tag) >= 0:
      object.manage_setLocalRoles(user, roles)
    elif xml.tag.find(self.local_group_tag) >= 0:
      object.manage_setLocalGroupRoles(user, roles)
989

990 991 992 993 994
  security.declareProtected(Permissions.ModifyPortalContent, 'addLocalPermissionNode')
  def addLocalPermissionNode(self, object, xml):
    """
    This allows to specify how to handle the local permision informations.
    This is really usefull if you want to write your own Conduit.
995 996
    """
    conflict_list = []
997
    # We want to add a local role
998
    #LOG('addLocalPermissionNode, xml',0,xml)
999 1000
    if len(xml.text):
      roles = self.convertXmlValue(xml, data_type='tokens')
1001
      roles = list(roles) # Needed for CPS, or we have a CPS error
1002
    else:
1003
      roles = ()
1004
    permission = self.getAttribute(xml, 'id')
1005
    #LOG('local_role: ',0,'permission: %s roles: %s' % (repr(permission),repr(roles)))
1006 1007
    #user = roles[0]
    #roles = roles[1:]
1008 1009
    if xml.tag.find(self.local_permission_tag) >= 0:
      object.manage_setLocalPermissions(permission, roles)
1010 1011
    return conflict_list

1012
  security.declareProtected(Permissions.ModifyPortalContent, 'editDocument')
1013 1014 1015 1016 1017
  def editDocument(self, object=None, **kw):
    """
    This is the default editDocument method. This method
    can easily be overwritten.
    """
1018
    object._edit(**kw)
1019

1020 1021 1022 1023 1024 1025 1026 1027
  security.declareProtected(Permissions.ModifyPortalContent, 'getProperty')
  def getProperty(self, object, kw):
    """
    This is the default getProperty method. This method
    can easily be overwritten.
    """
    return object.getProperty(kw)

1028 1029 1030 1031
  def nodeToString(self, node):
    """
    return an xml string corresponding to the node
    """
1032
    return etree.tostring(node, encoding='utf-8')
1033 1034 1035 1036 1037 1038 1039

  def getGidFromObject(self, object):
    """
    return the Gid composed with the object informations
    """
    return object.getId()

1040 1041 1042 1043 1044
  #def getGidFromXML(self, xml, gid_from_xml_list):
    #"""
    #return the Gid composed with xml informations
    #"""
    #return None
1045