ERP5Conduit.py 44.6 KB
Newer Older
Nicolas Delaby's avatar
Nicolas Delaby committed
1
# -*- coding: utf-8 -*-
Jean-Paul Smets's avatar
Jean-Paul Smets committed
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 29
##############################################################################
#
# 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.
#
##############################################################################

30
from Products.ERP5SyncML.XMLSyncUtils import XMLSyncUtilsMixin
31
from Products.ERP5SyncML.XMLSyncUtils import getXupdateObject
32
from Products.ERP5Type.Utils import deprecated
33
from Products.ERP5Type.XMLExportImport import MARSHALLER_NAMESPACE_URI
Jean-Paul Smets's avatar
Jean-Paul Smets committed
34
from Products.CMFCore.utils import getToolByName
35
from Products.ERP5Type.Base import WorkflowMethod
36
from DateTime.DateTime import DateTime
37 38
from email.mime.base import MIMEBase
from email import encoders
Sebastien Robin's avatar
Sebastien Robin committed
39
from AccessControl import ClassSecurityInfo
40
from Products.ERP5Type import Permissions, interfaces
41
from Products.ERP5Type.Globals import PersistentMapping
42
import pickle
43
from xml.sax.saxutils import unescape
Fabien Morin's avatar
Fabien Morin committed
44
import re
45
from lxml import etree
46
from lxml.etree import Element
47
parser = etree.XMLParser(remove_blank_text=True)
48 49
from xml_marshaller.xml_marshaller import load_tree as unmarshaller
from xupdate_processor import xuproc
50
from zLOG import LOG, INFO, DEBUG
51
from base64 import standard_b64decode
52
from zope.interface import implements
53
from copy import deepcopy
Fabien Morin's avatar
Fabien Morin committed
54

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
from Products.ERP5SyncML.SyncMLConstant import XUPDATE_ELEMENT,\
     XUPDATE_INSERT_OR_ADD_LIST, XUPDATE_DEL, XUPDATE_UPDATE, XUPDATE_INSERT_LIST
# Constant
HISTORY_TAG = 'workflow_action'
XML_OBJECT_TAG = 'object'
LOCAL_ROLE_TAG = 'local_role'
LOCAL_PERMISSION_TAG = 'local_permission'
LOCAL_GROUP_TAG = 'local_group'
LOCAL_ROLE_LIST = (LOCAL_ROLE_TAG, LOCAL_GROUP_TAG,)

ADDABLE_PROPERTY_LIST = LOCAL_ROLE_LIST + (HISTORY_TAG,) + (LOCAL_PERMISSION_TAG,)
NOT_EDITABLE_PROPERTY_LIST = ('id', 'object', 'uid', 'attribute::type',) +\
                             (XUPDATE_ELEMENT,) + ADDABLE_PROPERTY_LIST

FORCE_CONFLICT_LIST = ('layout_and_schema', 'ModificationDate')

from Products.ERP5Type.Accessor.TypeDefinition import list_types
LIST_TYPE_LIST = list_types
TEXT_TYPE_LIST = ('text', 'string',)
NONE_TYPE = 'None'
DATE_TYPE = 'date'
DICT_TYPE = 'dict'
INT_TYPE = 'int'
DATA_TYPE_LIST = ('data', 'object',)
BOOLEAN_TYPE = 'boolean'

HISTORY_EXP = re.compile("/%s\[@id='.*'\]" % HISTORY_TAG)
BAD_HISTORY_EXP = re.compile("/%s\[@id='.*'\]/" % HISTORY_TAG)
EXTRACT_ID_FROM_XPATH = re.compile(
                            "(?P<object_block>(?P<property>[^/]+)\[@"\
                            "(?P<id_of_id>id|gid)='(?P<object_id>[^']+)'\])")

87
class ERP5Conduit(XMLSyncUtilsMixin):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
88 89 90 91 92 93
  """
    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
94
    are eventually missing in a synchronization process)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
95

96
    If an object has be created during a synchronization process,
Jean-Paul Smets's avatar
Jean-Paul Smets committed
97 98 99 100 101 102 103
    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

104
    The first implementation of ERP5 synchronization
Jean-Paul Smets's avatar
Jean-Paul Smets committed
105
    will define a default location to create new objects and
106
    a default class. This will be defined at the level of the synchronization
Jean-Paul Smets's avatar
Jean-Paul Smets committed
107 108
    tool

109 110 111 112 113 114 115 116 117 118 119 120 121 122
    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
123

Fabien Morin's avatar
Fabien Morin committed
124
  # Declarative interfaces
125
  implements( interfaces.IConduit, )
Fabien Morin's avatar
Fabien Morin committed
126

Sebastien Robin's avatar
Sebastien Robin committed
127 128
  # Declarative security
  security = ClassSecurityInfo()
Jean-Paul Smets's avatar
Jean-Paul Smets committed
129

Sebastien Robin's avatar
Sebastien Robin committed
130
  security.declareProtected(Permissions.AccessContentsInformation,'getEncoding')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
131 132 133 134
  def getEncoding(self):
    """
    return the string corresponding to the local encoding
    """
135 136
    #return "iso-8859-1"
    return "utf-8"
Jean-Paul Smets's avatar
Jean-Paul Smets committed
137

138 139 140
  #security.declareProtected(Permissions.ModifyPortalContent, '__init__')
  #def __init__(self):
    #self.args = {}
141

Sebastien Robin's avatar
Sebastien Robin committed
142
  security.declareProtected(Permissions.ModifyPortalContent, 'addNode')
143 144
  def addNode(self, xml=None, object=None, sub_object=None, reset=None,
                                       simulate=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
145 146 147
    """
    A node is added

148 149 150 151 152 153 154 155
    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
156 157
    This fucntion returns conflict_list, wich is of the form,
    [conflict1,conflict2,...] where conclict1 is of the form :
158
    [object.getPath(),keyword,local_and_actual_value,subscriber_value]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
159
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
160 161
    reset_local_roles = False
    reset_workflow = False
Jean-Paul Smets's avatar
Jean-Paul Smets committed
162
    conflict_list = []
163
    xml = self.convertToXml(xml)
164
    #LOG('ERP5Conduit.addNode', INFO, 'object path:%r' % object.getPath())
165
    #LOG('ERP5Conduit.addNode', INFO, '\n%s' % etree.tostring(xml, pretty_print=True))
166
    if xml is None:
167
      return {'conflict_list': conflict_list, 'object': sub_object}
Jean-Paul Smets's avatar
Jean-Paul Smets committed
168
    # In the case where this new node is a object to add
169
    xpath_expression = xml.get('select')
170
    if xml.xpath('local-name()') == HISTORY_TAG and not reset:
171
      conflict_list += self.addWorkflowNode(object, xml, simulate)
172
    elif xml.xpath('name()') in XUPDATE_INSERT_OR_ADD_LIST and\
173 174
                            MARSHALLER_NAMESPACE_URI not in xml.nsmap.values():
      # change the context according select expression
175
      get_target_parent = xml.xpath('name()') in XUPDATE_INSERT_LIST
176 177
      context = self.getContextFromXpath(object, xpath_expression,
                                         get_target_parent=get_target_parent)
178 179 180 181
      for element in xml.findall('{%s}element' % xml.nsmap['xupdate']):
        xml = self.getElementFromXupdate(element)
        conflict_list += self.addNode(xml=xml, object=context, **kw)\
                                                              ['conflict_list']
182
    elif xml.xpath('local-name()') == XML_OBJECT_TAG:
183 184 185 186 187
      sub_object = self._createContent(xml=xml,
                                      object=object,
                                      sub_object=sub_object,
                                      reset_local_roles=reset_local_roles,
                                      reset_workflow=reset_workflow,
188
                                      reset=reset,
189 190
                                      simulate=simulate,
                                      **kw)
191
    elif xml.xpath('local-name()') in LOCAL_ROLE_LIST:
192
      self.addLocalRoleNode(object, xml)
193
    elif xml.xpath('local-name()') == LOCAL_PERMISSION_TAG:
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, reset=reset,
                                                       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
  def deleteNode(self, xml=None, object=None, object_id=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
203 204 205
    """
    A node is deleted
    """
206 207 208
    #LOG('ERP5Conduit.deleteNode', INFO, 'object path:%s' % object.getPath())
    #LOG('ERP5Conduit deleteNode', INFO, 'object_id:%r' % object_id)
    if object_id is not None:
Aurel's avatar
typo  
Aurel committed
209
      self._deleteContent(object=object, object_id=object_id, **kw)
210 211 212 213
      return []
    xml = self.convertToXml(xml)
    #LOG('ERP5Conduit deleteNode', INFO, etree.tostring(xml, pretty_print=True))
    xpath_expression = xml.get('select')
214
    #LOG('ERP5Conduit xpath_expression', INFO, xpath_expression)
215
    context_to_delete = self.getContextFromXpath(object, xpath_expression)
216 217 218 219 220
    if 'workflow_action' in xpath_expression:
      # Something like /erp5/object[@gid='313730']/object[@id='170']/workflow_action[@id='edit_workflow'][2]
      # we can not edit or erase workflow history
      pass
    elif context_to_delete != object:
221
      self._deleteContent(object=context_to_delete.getParentValue(),
222
                                           object_id=context_to_delete.getId(),
Aurel's avatar
typo  
Aurel committed
223
                          **kw)
224 225
    else:
      #same context
226 227
      if [role for role in LOCAL_ROLE_LIST if role in xpath_expression]:
        user = EXTRACT_ID_FROM_XPATH.findall(xpath_expression)[-1][3]
228
        #LOG('ERP5Conduit.deleteNode local_role: ', INFO, 'user: %r' % user)
229
        if LOCAL_ROLE_TAG in xpath_expression:
230
          object.manage_delLocalRoles([user])
231
        elif LOCAL_GROUP_TAG in xpath_expression:
232
          object.manage_delLocalGroupRoles([user])
233 234
      if LOCAL_PERMISSION_TAG in xpath_expression:
        permission = EXTRACT_ID_FROM_XPATH.findall(xpath_expression)[-1][3]
235 236
        #LOG('ERP5Conduit.deleteNode permission: ', INFO,
                                                 #'permission: %r' % permission)
237
        object.manage_setLocalPermissions(permission)
238
    return []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
239

240
  security.declareProtected(Permissions.ModifyPortalContent, 'deleteObject')
241
  def deleteObject(self, object, object_id, **kw):
242 243 244
    try:
      object._delObject(object_id)
    except (AttributeError, KeyError):
245
      #LOG('ERP5Conduit.deleteObject', DEBUG, 'Unable to delete: %s' % str(object_id))
246 247
      pass

Sebastien Robin's avatar
Sebastien Robin committed
248
  security.declareProtected(Permissions.ModifyPortalContent, 'updateNode')
249 250
  def updateNode(self, xml=None, object=None, previous_xml=None, force=False,
                 simulate=False, reset=False, xpath_expression=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
251 252 253 254 255 256 257 258
    """
    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 = []
259 260
    if xml is None:
      return {'conflict_list':conflict_list, 'object':object}
261
    xml = self.convertToXml(xml)
262 263 264 265
    #LOG('ERP5Conduit.updateNode, force: ', INFO, force)
    #LOG('ERP5Conduit updateNode', INFO, object.getPath())
    #LOG('ERP5Conduit updateNode', INFO, '\n%s' % etree.tostring(xml, pretty_print=True))
    if xml.tag == '{%s}modifications' % xml.nsmap.get('xupdate'):
266 267 268 269
      conflict_list += self.applyXupdate(object=object,
                                         xupdate=xml,
                                         previous_xml=previous_xml,
                                         force=force,
270 271 272
                                         simulate=simulate,
                                         reset=reset,
                                         **kw)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
273 274 275
    # we may have only the part of an xupdate
    else:
      args = {}
276
      if self.isProperty(xml):
277
        keyword = None
278
        value = xml.get('select')
279 280 281
        if value is not None:
          select_list = value.split('/') # Something like:
                                         #('','object[1]','sid[1]')
282
          new_select_list = []
283 284 285
          for select_item in select_list:
            if select_item.find('[') >= 0:
              select_item = select_item[:select_item.find('[')]
286
            new_select_list.append(select_item)
287
          select_list = new_select_list # Something like : ('','object','sid')
288
          keyword = select_list[-1] # this will be 'sid'
289
        data = None
290
        if xml.xpath('name()') not in XUPDATE_INSERT_OR_ADD_LIST:
291
          for subnode in xml:
292
            if subnode.xpath('name()') == XUPDATE_ELEMENT:
293
              keyword = subnode.get('name')
294
              data_xml = subnode
295
        else:
296
          #XXX find something better than hardcoded prefix
297
          # We can call add node
298 299 300 301
          conflict_list += self.addNode(xml=xml,
                                        object=object,
                                        force=force,
                                        simulate=simulate,
302
                                        reset=reset,
303 304
                                        **kw)
          return conflict_list
305
        if xml.xpath('name()') in XUPDATE_DEL:
306 307 308 309
          conflict_list += self.deleteNode(xml=xml,
                                           object=object,
                                           force=force,
                                           simulate=simulate,
310
                                           reset=reset,
311 312
                                           **kw)
          return conflict_list
313
        if keyword is None: # This is not a selection, directly the property
314
          keyword = xml.xpath('name()')
315
        if keyword not in NOT_EDITABLE_PROPERTY_LIST:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
316
          # We will look for the data to enter
317
          xpath_expression = xml.get('select', xpath_expression)
318
          get_target_parent = xml.xpath('name()') in XUPDATE_INSERT_LIST
319 320 321
          context = self.getContextFromXpath(object,
                                           xpath_expression,
                                           get_target_parent=get_target_parent)
322 323
          data_type = context.getPropertyType(keyword)
          #LOG('ERP5Conduit.updateNode', INFO, 'data_type:%r for keyword: %s' % (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
          #   - current_data : the data actually on this box
Nicolas Delaby's avatar
Nicolas Delaby committed
333
          isConflict = False
334
          if previous_xml and not force:
Fabien Morin's avatar
Fabien Morin committed
335
          # if no previous_xml, no conflict
336 337 338 339 340 341 342 343 344 345 346 347 348 349
            #old_data = self.getObjectProperty(keyword, previous_xml,
                                              #data_type=data_type)
            previous_xml_tree = self.convertToXml(previous_xml)
            old_result = previous_xml_tree.xpath(xpath_expression)
            if old_result:
              old_data = self.convertXmlValue(old_result[0])
            else:
              raise ValueError('Xpath expression does not apply on previous'\
                                                  ' xml:%r' % xpath_expression)
            current_data = self.getProperty(context, keyword)
            #LOG('ERP5Conduit.updateNode', INFO, 'Conflict keyword: %s' % keyword)
            #LOG('ERP5Conduit.updateNode', INFO, 'Conflict data: %s' % str(data))
            #LOG('ERP5Conduit.updateNode', INFO, 'Conflict old_data: %s' % str(old_data))
            #LOG('ERP5Conduit.updateNode', INFO, 'Conflict current_data: %s' % str(current_data))
350 351
            if (old_data != current_data) and (data != current_data) and\
                                            keyword not in FORCE_CONFLICT_LIST:
352 353 354 355
              #LOG('ERP5Conduit.updateNode', INFO, 'Conflict on : %s' % keyword)
              # This is a conflict
              isConflict = True
              xml_string = etree.tostring(xml, encoding='utf-8')
356 357 358 359 360 361 362 363 364 365
              # unattach diff from its parent once copy has been
              # stored on signature
              xml.getparent().remove(xml)
              conflict = kw['signature'].newContent(portal_type='SyncML Conflict',
                                                    origin_value=context,
                                                    property_id=keyword,
                                                    diff_chunk=xml_string)
              if data_type not in DATA_TYPE_LIST:
                conflict.edit(local_value=current_data,
                              remote_value=data)
366
              conflict_list += [conflict]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
367
          # We will now apply the argument with the method edit
368
          if args and (not isConflict or force) and (not simulate or reset):
369
            self._updateContent(object=context, **args)
370
            # It is sometimes required to do something after an edit
371 372
            if getattr(context, 'manage_afterEdit', None) is not None:
              context.manage_afterEdit()
373

Jean-Paul Smets's avatar
Jean-Paul Smets committed
374 375
        if keyword == 'object':
          # This is the case where we have to call addNode
376 377 378 379
          conflict_list += self.addNode(xml=xml,
                                        object=object,
                                        force=force,
                                        simulate=simulate,
380
                                        reset=reset,
381
                                        **kw)['conflict_list']
382
        elif keyword == HISTORY_TAG and not simulate:
383
          # This is the case where we have to call addNode
384 385
          conflict_list += self.addNode(xml=subnode, object=object,
                                        force=force, simulate=simulate,
386
                                        reset=reset, **kw)['conflict_list']
387
        elif keyword in (LOCAL_ROLE_TAG, LOCAL_PERMISSION_TAG) and not simulate:
388
          # This is the case where we have to update Roles or update permission
389
          #LOG('ERP5Conduit.updateNode', DEBUG, 'we will add a local role')
390 391 392
          #user = self.getSubObjectId(xml)
          #roles = self.convertXmlValue(data,data_type='tokens')
          #object.manage_setLocalRoles(user,roles)
393 394
          conflict_list += self.addNode(xml=xml, object=object,
                                       force=force, simulate=simulate,
395
                                       reset=reset, **kw)['conflict_list']
Jean-Paul Smets's avatar
Jean-Paul Smets committed
396 397
    return conflict_list

398
  security.declareProtected(Permissions.AccessContentsInformation,
399
      'getFormatedArgs')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
400 401 402 403 404 405 406 407
  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]
408
      if isinstance(keyword, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
409
        keyword = keyword.encode(self.getEncoding())
410
      if isinstance(data, (tuple, list)):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
411 412
        new_data = []
        for item in data:
413
          if isinstance(item, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
414
            item = item.encode(self.getEncoding())
415
          new_data.append(item)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
416
        data = new_data
417
      if isinstance(data, unicode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
418 419
        data = data.encode(self.getEncoding())
      if keyword == 'binary_data':
420
        #LOG('ERP5Conduit.getFormatedArgs', DEBUG, 'binary_data keyword: %s' % str(keyword))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
421
        msg = MIMEBase('application','octet-stream')
422
        encoders.encode_base64(msg)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
423
        msg.set_payload(data)
Nicolas Delaby's avatar
Nicolas Delaby committed
424
        data = msg.get_payload(decode=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
425 426 427
      new_args[keyword] = data
    return new_args

428
  security.declareProtected(Permissions.AccessContentsInformation, 'isProperty')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
429 430 431
  def isProperty(self, xml):
    """
    Check if it is a simple property
432 433
    not an attribute @type it's a metadata
    """
434
    bad_list = (HISTORY_EXP,)
435
    value = xml.get('select')
436 437 438
    if value is not None:
      for bad_string in bad_list:
        if bad_string.search(value) is not None:
Nicolas Delaby's avatar
Nicolas Delaby committed
439 440
          return False
    return True
Jean-Paul Smets's avatar
Jean-Paul Smets committed
441

442
  def getContextFromXpath(self, context, xpath, get_target_parent=False):
443 444 445 446 447
    """Return the last object from xpath expression
    /object[@gid='foo']/object[@id='bar']/object[@id='freak']/property
    will return object.getId() == 'freak'
    - We ignore the first object_block /object[@gid='foo'] intentionaly
    because the targeted context is already actual context.
448 449 450
      context: object in acquisition context
      xpath: string which is xpath expression to fetch the object
      get_target_parent: boolean to get the parent of targetted object
451 452 453
    """
    if xpath is None:
      return context
454
    result_list = EXTRACT_ID_FROM_XPATH.findall(xpath)
455 456
    if get_target_parent:
      result_list = result_list[:-1]
457 458 459 460
    first_object = True
    while result_list:
      object_block = result_list[0][0]
      sub_context_id = result_list[0][3]
461 462 463 464 465
      _get_ob_accessor = getattr(context, '_getOb', None)
      if _get_ob_accessor is not None:
        # Some ERP5 objects doe not implement Folder
        # like coordinates objects
        sub_context = _get_ob_accessor(sub_context_id, None)
466 467 468 469 470 471
      if first_object:
        first_object = False
      elif sub_context is not None:
        context = sub_context
      else:
        # Ignore non existing objects
472 473 474
        pass
        #LOG('ERP5Conduit', INFO, 'sub document of %s not found with id:%r'%\
                                         #(context.getPath(), sub_context_id))
475
      xpath = xpath.replace(object_block, '', 1)
476
      result_list = EXTRACT_ID_FROM_XPATH.findall(xpath)
477 478
      if get_target_parent:
        result_list = result_list[:-1]
479 480
    return context

481
  security.declareProtected(Permissions.AccessContentsInformation,
482 483
                                                         'getSubObjectXupdate')
  @deprecated
Jean-Paul Smets's avatar
Jean-Paul Smets committed
484 485 486 487 488
  def getSubObjectXupdate(self, xml):
    """
    This will change the xml in order to change the update
    from the object to the subobject
    """
489 490
    xml_copy = deepcopy(xml)
    self.changeSubObjectSelect(xml_copy)
491
    return xml_copy
Jean-Paul Smets's avatar
Jean-Paul Smets committed
492

493
  security.declareProtected(Permissions.AccessContentsInformation,
494
      'isHistoryAdd')
495
  def isHistoryAdd(self, xml):
496
    bad_list = (HISTROY_EXP,)
497
    value = xml.get('select')
498 499 500 501 502 503 504
    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
505 506
    return 0

507
  security.declareProtected(Permissions.AccessContentsInformation,
508
      'getHistoryIdFromSelect')
509 510 511 512 513
  def getHistoryIdFromSelect(self, xml):
    """
    Return the id of the subobject in an xupdate modification
    """
    object_id = None
514
    value = xml.get('select')
515
    if value is not None:
516 517
      if HISTORY_EXP.search(value) is not None:
        s = HISTORY_TAG
518 519 520 521
        object_id = value[value.find(s):]
        object_id = object_id[object_id.find("'") + 1:]
        object_id = object_id[:object_id.find("'")]
        return object_id
522 523
    return object_id

524 525
  security.declareProtected(Permissions.AccessContentsInformation,
      'getSubObjectXml')
526 527 528 529 530
  def getSubObjectXml(self, object_id, xml):
    """
    Return the xml of the subobject which as the id object_id
    """
    xml = self.convertToXml(xml)
531
    for subnode in xml:
532
      if subnode.xpath('local-name()') == XML_OBJECT_TAG:
533
        if object_id == subnode.get('id'):
534 535
          return subnode
    return None
Jean-Paul Smets's avatar
Jean-Paul Smets committed
536

537 538
  security.declareProtected(Permissions.ModifyPortalContent,
                            'replaceIdFromXML')
539
  def replaceIdFromXML(self, xml, attribute_name, new_id, as_string=True):
540 541 542
    """XXX argument old_attribute_name is missing
    XXX name of method is not good, because content is not necessarily XML
    return a xml with id replaced by a new id
543
    """
544 545 546
    if isinstance(xml, str):
      xml = etree.XML(xml, parser=parser)
    else:
547
      # copy of xml object for modification
548 549
      xml = deepcopy(xml)
    object_element = xml.find('object')
550 551 552 553
    if attribute_name == 'id':
      del object_element.attrib['gid']
    else:
      del object_element.attrib['id']
554 555
    object_element.attrib[attribute_name] = new_id
    if as_string:
556
      return etree.tostring(xml, encoding="utf-8")
557
    return xml
558

559 560 561
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getXMLFromObjectWithId')
  def getXMLFromObjectWithId(self, object, xml_mapping, context_document=None):
562 563 564 565 566 567 568 569
    """
      return the xml with Id of Object
    """
    xml = ''
    if xml_mapping is None:
      return xml
    func = getattr(object, xml_mapping, None)
    if func is not None:
570 571 572 573 574
      try:
        xml = func(context_document=context_document)
      except TypeError:
        # The method does not accept parameters
        xml = func()
575 576
    return xml

577 578 579
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getXMLFromObjectWithGid')
  def getXMLFromObjectWithGid(self, object, gid, xml_mapping, as_string=True, context_document=None):
580 581 582
    """
      return the xml with Gid of Object
    """
583
    xml_with_id = self.getXMLFromObjectWithId(object, xml_mapping, context_document=context_document)
584 585
    return self.replaceIdFromXML(xml_with_id, 'gid', gid, as_string=as_string)

586

587 588 589
  security.declareProtected(Permissions.AccessContentsInformation,
                            'getXMLFromObjectWithRid')
  def getXMLFromObjectWithRid(self, object, rid, xml_mapping, as_string=True, context_document=None):
590 591 592
    """
      return the xml with Rid of Object
    """
593
    xml_id = self.getXMLFromObjectWithId(object, xml_mapping, context_document=context_document)
594
    xml_rid = self.replaceIdFromXML(xml_id, 'rid', rid, as_string=as_string)
595
    return xml_rid
Jean-Paul Smets's avatar
Jean-Paul Smets committed
596

Sebastien Robin's avatar
Sebastien Robin committed
597
  security.declareProtected(Permissions.AccessContentsInformation,'convertToXml')
598
  def convertToXml(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
599 600 601
    """
    if xml is a string, convert it to a node
    """
602 603 604
    if xml is None: return None
    if isinstance(xml, (str, unicode)):
      if isinstance(xml, unicode):
Sebastien Robin's avatar
Sebastien Robin committed
605
        xml = xml.encode('utf-8')
606
      xml = etree.XML(xml, parser=parser)
607
    # If we have the xml from the node erp5, we just take the subnode
608
    if xml.xpath('local-name()') == 'erp5':
609
      xml = xml[0]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
610 611
    return xml

Nicolas Delaby's avatar
Nicolas Delaby committed
612 613
  security.declareProtected(Permissions.AccessContentsInformation,
                                                               'getObjectType')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
614 615 616 617
  def getObjectType(self, xml):
    """
    Retrieve the portal type from an xml
    """
618
    return xml.get('portal_type')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
619

Nicolas Delaby's avatar
Nicolas Delaby committed
620 621
  security.declareProtected(Permissions.AccessContentsInformation,
                                                             'getPropertyType')
622 623 624 625
  def getPropertyType(self, xml):
    """
    Retrieve the portal type from an xml
    """
626
    return xml.get('type')
627

Sebastien Robin's avatar
Sebastien Robin committed
628
  security.declareProtected(Permissions.ModifyPortalContent, 'newObject')
Nicolas Delaby's avatar
Nicolas Delaby committed
629 630
  def newObject(self, object=None, xml=None, simulate=False,
                reset_local_roles=True, reset_workflow=True):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
631 632 633 634 635
    """
      modify the object with datas from
      the xml (action section)
    """
    args = {}
636 637
    if simulate:
      return
638
    # Retrieve the list of users with a role and delete default roles
639
    if reset_local_roles:
Nicolas Delaby's avatar
Nicolas Delaby committed
640
      user_role_list = [x[0] for x in object.get_local_roles()]
641
      object.manage_delLocalRoles(user_role_list)
Nicolas Delaby's avatar
Nicolas Delaby committed
642
    if getattr(object, 'workflow_history', None) is not None and reset_workflow:
643
      object.workflow_history = PersistentMapping()
644
    if xml.prefix == 'xupdate':
645 646 647
      xml = xml[0]
    for subnode in xml.xpath('*'):
      #get only Element nodes (not Comments or Processing instructions)
648
      if subnode.xpath('name()') not in NOT_EDITABLE_PROPERTY_LIST:
649
        keyword_type = self.getPropertyType(subnode)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
650
        # This is the case where the property is a list
651
        keyword = subnode.xpath('name()')
652
        args[keyword] = self.convertXmlValue(subnode, keyword_type)
653 654
      elif subnode.xpath('local-name()') in ADDABLE_PROPERTY_LIST\
                                                           + (XML_OBJECT_TAG,):
Nicolas Delaby's avatar
Nicolas Delaby committed
655
        self.addNode(object=object, xml=subnode, force=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
656 657 658 659
    # 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
660
    self.editDocument(object=object, **args)
Nicolas Delaby's avatar
Nicolas Delaby committed
661
    if getattr(object, 'manage_afterEdit', None) is not None:
662
      object.manage_afterEdit()
Nicolas Delaby's avatar
Nicolas Delaby committed
663
    self.afterNewObject(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
664

Nicolas Delaby's avatar
Nicolas Delaby committed
665 666
  security.declareProtected(Permissions.AccessContentsInformation,
                                                              'afterNewObject')
667
  def afterNewObject(self, object):
Nicolas Delaby's avatar
Nicolas Delaby committed
668 669
    """Overloadable method
    """
670 671
    pass

Nicolas Delaby's avatar
Nicolas Delaby committed
672 673
  security.declareProtected(Permissions.AccessContentsInformation,
                                                            'getStatusFromXml')
674 675 676 677 678
  def getStatusFromXml(self, xml):
    """
    Return a worklow status from xml
    """
    status = {}
679
    for subnode in xml:
680 681
      keyword = subnode.tag
      value = self.convertXmlValue(xml.find(keyword))
682 683 684
      status[keyword] = value
    return status

Nicolas Delaby's avatar
Nicolas Delaby committed
685 686
  security.declareProtected(Permissions.AccessContentsInformation,
                                                       'getElementFromXupdate')
687 688
  def getElementFromXupdate(self, xml):
    """
689
    return a fragment node with applied xupdate
Nicolas Delaby's avatar
Nicolas Delaby committed
690 691
    This method simulate an xupdate transformation on given XML.
    It transform the xupdate into node handleable by Conduit
692
    """
693
    if xml.xpath('name()') == XUPDATE_ELEMENT:
694 695 696 697 698 699 700
      new_node = Element(xml.get('name'), nsmap=xml.nsmap)
      for subnode in xml.findall('{%s}attribute' % xml.nsmap['xupdate']):
        new_node.attrib.update({subnode.get('name'): subnode.text})
      ## Then dumps the xml and remove xupdate:attribute nodes
      new_node.extend(deepcopy(child) for child in\
                                 xml.xpath('*[name() != "xupdate:attribute"]'))
      new_node.text = xml.text
701 702
      new_node.tail = xml.tail
      return new_node
703
    if xml.xpath('name()') == XUPDATE_UPDATE:
Nicolas Delaby's avatar
Nicolas Delaby committed
704 705
      # This condition seems not used anymore and not efficient
      # Usage of xupdate_processor is recommanded
706
      result = u'<'
707
      attribute = xml.attrib.get('select')
708 709 710 711 712 713 714 715
      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)
716
      property = attribute[:s_place].strip('/')
717 718 719 720
      result += property
      if select_id is not None:
        result += ' id=%s' % select_id
      result +=  '>'
721
      xml_string = self.nodeToString(xml)
722
      maxi = xml_string.find('>')+1
723
      result += xml_string[maxi:xml_string.find('</%s>' % xml.xpath('name()'))]
Nicolas Delaby's avatar
Nicolas Delaby committed
724
      result += '</%s>' % (property)
Nicolas Delaby's avatar
Nicolas Delaby committed
725
      #LOG('getElementFromXupdate, result:',0,repr(result))
726
      return self.convertToXml(result)
727 728
    return xml

Nicolas Delaby's avatar
Nicolas Delaby committed
729 730
  security.declareProtected(Permissions.AccessContentsInformation,
                                                    'getWorkflowActionFromXml')
731 732 733 734 735
  def getWorkflowActionFromXml(self, xml):
    """
    Return the list of workflow actions
    """
    action_list = []
736
    if xml.xpath('name()') == XUPDATE_ELEMENT:
737
      action_list.append(xml)
738
      return action_list
739
    # XXX not sure code bellow is still used ?
740
    for subnode in xml:
741
      if subnode.xpath('local-name()') == self.action_tag:
742
        action_list.append(subnode)
743 744
    return action_list

745 746
  security.declareProtected(Permissions.AccessContentsInformation,
                                                             'convertXmlValue')
747
  def convertXmlValue(self, node, data_type=None):
748
    """Cast xml information into appropriate python type
Jean-Paul Smets's avatar
Jean-Paul Smets committed
749
    """
750 751
    if node is None:
      return None
752 753
    if data_type is None:
      data_type = self.getPropertyType(node)
754
    if data_type == NONE_TYPE:
755
      return None
756 757 758
    data = node.text
    if data is not None and isinstance(data, unicode):
      data = data.encode('utf-8')
759
    elif data is None and data_type in TEXT_TYPE_LIST:
760
      return ''
761
    # We can now convert string in tuple, dict, binary...
762
    if data_type in LIST_TYPE_LIST:
763
      data = unmarshaller(node[0])
764
    elif data_type in TEXT_TYPE_LIST:
765
      data = unescape(data)
766 767 768 769 770 771 772 773
    elif data_type == BOOLEAN_TYPE:
      if data == 'False':
        data = False
      elif data == 'True':
        data = True
      else:
        raise TypeError('Boolean type not expected:%r' % (data,))
    elif data_type in DATA_TYPE_LIST:
774
      if data is None:
775 776 777
        # data is splitted inside  block_data nodes
        data = ''.join([standard_b64decode(block.text) for\
                                                 block in node.iterchildren()])
778
    elif data_type == DATE_TYPE:
779
      data = DateTime(data)
780
    elif data_type == INT_TYPE :
Sebastien Robin's avatar
Sebastien Robin committed
781
      data = int(data)
782
    elif data_type == BOOLEAN_TYPE:
783 784 785 786 787 788
      if data == 'False':
        data = False
      elif data == 'True':
        data = True
      else:
        raise NotImplementedError
Jean-Paul Smets's avatar
Jean-Paul Smets committed
789 790
    return data

791

Sebastien Robin's avatar
Sebastien Robin committed
792
  security.declareProtected(Permissions.ModifyPortalContent, 'applyXupdate')
793
  def applyXupdate(self, object=None, xupdate=None, previous_xml=None, **kw):
794 795 796 797
    """
    Parse the xupdate and then it will call the conduit
    """
    conflict_list = []
798
    if isinstance(xupdate, (str, unicode)):
799
      xupdate = etree.XML(xupdate, parser=parser)
800 801 802
    #LOG("applyXupdate", INFO, etree.tostring(xupdate, pretty_print=True))
    xupdate_builded = False
    xpath_expression_update_dict = {}
803
    for subnode in xupdate:
804
      selection_name = ''
805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
      original_xpath_expression = subnode.get('select', '')
      if not xupdate_builded and\
                            MARSHALLER_NAMESPACE_URI in subnode.nsmap.values()\
                                  or 'block_data' in original_xpath_expression:
        # It means that the xpath expression is targetting
        # marshalled values or data nodes. We need to rebuild the original xml
        # in its own context in order to retrieve original value

        # We are insde a loop build the XUpdated tree only once
        xupdate_builded = True

        # Find the prefix used by marshaller.
        for prefix, namespace_uri in subnode.nsmap.iteritems():
          if namespace_uri == MARSHALLER_NAMESPACE_URI:
            break
        # TODO add support of etree objects for xuproc to avoid
        # serializing tree into string
        if not isinstance(previous_xml, str):
          previous_xml = etree.tostring(previous_xml)
        xupdated_tree = xuproc.applyXUpdate(xml_xu_string=etree.tostring(xupdate),
                                            xml_doc_string=previous_xml)
      if MARSHALLER_NAMESPACE_URI in subnode.nsmap.values():
        xpath_expression = original_xpath_expression
828
        get_target_parent = subnode.xpath('name()') in XUPDATE_INSERT_LIST
829 830
        context = self.getContextFromXpath(object, xpath_expression,
                                           get_target_parent=get_target_parent)
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
        base_xpath_expression = xpath_expression\
                                            [:xpath_expression.index(prefix)-1]
        xupdated_node_list = xupdated_tree.xpath(base_xpath_expression)
        if xupdated_node_list:
          xupdated_node = xupdated_node_list[0]
        else:
          ValueError('Wrong xpath expression:%r' % base_xpath_expression)
        if base_xpath_expression not in xpath_expression_update_dict:
          xpath_expression_update_dict[base_xpath_expression] = \
                                   dict(xml=xupdated_node,
                                        object=context,
                                        xpath_expression=base_xpath_expression)
      elif 'block_data' in original_xpath_expression:
        """XXX Use Qualified Names for block_data nodes
        to avoid ambiguity
        """
        xpath_expression = original_xpath_expression
848
        get_target_parent = subnode.xpath('name()') in XUPDATE_INSERT_LIST
849 850
        context = self.getContextFromXpath(object, xpath_expression,
                                           get_target_parent=get_target_parent)
851 852 853 854 855 856 857 858 859 860 861 862
        base_xpath_expression = xpath_expression\
                                            [:xpath_expression.index('block_data')-1]
        xupdated_node_list = xupdated_tree.xpath(base_xpath_expression)
        if xupdated_node_list:
          xupdated_node = xupdated_node_list[0]
        else:
          ValueError('Wrong xpath expression:%r' % base_xpath_expression)
        if base_xpath_expression not in xpath_expression_update_dict:
          xpath_expression_update_dict[base_xpath_expression] = \
                                   dict(xml=xupdated_node,
                                        object=context,
                                        xpath_expression=base_xpath_expression)
863
      elif subnode.xpath('name()') in XUPDATE_INSERT_OR_ADD_LIST:
864 865 866
        conflict_list += self.addNode(xml=subnode, object=object,
                                      previous_xml=previous_xml,
                                      **kw)['conflict_list']
867
      elif subnode.xpath('name()') == XUPDATE_DEL:
868 869
        conflict_list += self.deleteNode(xml=subnode, object=object,
                                         previous_xml=previous_xml, **kw)
870
      elif subnode.xpath('name()') == XUPDATE_UPDATE:
871 872 873 874 875 876 877 878
        conflict_list += self.updateNode(xml=subnode, object=object,
                                         previous_xml=previous_xml, **kw)

    # Now apply collected xupdated_node
    for update_dict in xpath_expression_update_dict.itervalues():
      update_dict.update(kw)
      conflict_list += self.updateNode(previous_xml=previous_xml,
                                       **update_dict)
879 880
    return conflict_list

881 882
  def isWorkflowActionAddable(self, object=None, status=None, wf_tool=None,
                              wf_id=None, xml=None):
Sebastien Robin's avatar
Sebastien Robin committed
883 884
    """
    Some checking in order to check if we should add the workfow or not
885 886 887 888 889
    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
    wf_history = object.workflow_history
890
    if wf_id in wf_history:
891
      action_list = wf_history[wf_id]
892 893
    else:
      return True # addable
Nicolas Delaby's avatar
Nicolas Delaby committed
894
    addable = True
895
    time = status.get('time')
896
    for action in action_list:
Nicolas Delaby's avatar
Nicolas Delaby committed
897
      this_one = True
898
      if time <= action.get('time'):
899 900
        # action in the past are not append
        addable = False
901 902
      for key in action.keys():
        if status[key] != action[key]:
Nicolas Delaby's avatar
Nicolas Delaby committed
903
          this_one = False
904 905
          break
      if this_one:
Nicolas Delaby's avatar
Nicolas Delaby committed
906
        addable = False
907 908 909
        break
    return addable

910
  security.declareProtected(Permissions.ModifyPortalContent, 'constructContent')
911
  def constructContent(self, object, object_id, portal_type):
912 913 914 915 916
    """
    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
917
    #LOG('ERP5Conduit.addNode',0,'portal_type: |%s|' % str(portal_type))
918
    subobject = object.newContent(portal_type=portal_type, id=object_id)
Nicolas Delaby's avatar
Nicolas Delaby committed
919
    return subobject, True, True
920

921 922 923 924 925
  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.
926
    """
927 928
    conflict_list = []
    # We want to add a workflow action
Nicolas Delaby's avatar
Nicolas Delaby committed
929 930
    wf_tool = getToolByName(object.getPortalObject(), 'portal_workflow')
    wf_id = xml.get('id')
931 932
    if wf_id is None: # History added by xupdate
      wf_id = self.getHistoryIdFromSelect(xml)
933
      xml = xml[0]
934 935
    #for action in self.getWorkflowActionFromXml(xml):
    status = self.getStatusFromXml(xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
936
    #LOG('addNode, status:',0,status)
937
    add_action = self.isWorkflowActionAddable(object=object,
Nicolas Delaby's avatar
Nicolas Delaby committed
938 939
                                              status=status,wf_tool=wf_tool,
                                              wf_id=wf_id,xml=xml)
940
    if add_action and not simulate:
Nicolas Delaby's avatar
Nicolas Delaby committed
941
      wf_tool.setStatusOf(wf_id, object, status)
942

943
    return conflict_list
944

945 946 947 948 949
  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.
950
    """
951
    # We want to add a local role
952
    roles = self.convertXmlValue(xml, data_type='tokens')
Nicolas Delaby's avatar
Nicolas Delaby committed
953 954 955
    user = xml.get('id')
    #LOG('local_role: %s' % object.getPath(), INFO,
                                          #'user:%r | roles:%r' % (user, roles))
956 957
    #user = roles[0]
    #roles = roles[1:]
958
    if xml.xpath('local-name()') == LOCAL_ROLE_TAG:
959
      object.manage_setLocalRoles(user, roles)
960
    elif xml.xpath('local-name()') == LOCAL_GROUP_TAG:
961
      object.manage_setLocalGroupRoles(user, roles)
962

963 964 965 966 967
  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.
968 969
    """
    conflict_list = []
970
    # We want to add a local role
971
    #LOG('addLocalPermissionNode, xml',0,xml)
972 973 974
    roles = self.convertXmlValue(xml, data_type='tokens')

    permission = xml.get('id')
975
    #LOG('local_role: ',0,'permission: %s roles: %s' % (repr(permission),repr(roles)))
976 977
    #user = roles[0]
    #roles = roles[1:]
978
    if xml.xpath('local-name()') == LOCAL_PERMISSION_TAG:
979
      object.manage_setLocalPermissions(permission, roles)
980 981
    return conflict_list

982
  security.declareProtected(Permissions.ModifyPortalContent, 'editDocument')
983 984 985 986 987 988 989 990

  # XXX Ugly hack to avoid calling interaction workflow when synchronizing
  # objects with ERP5SyncML as it leads to unwanted side-effects on the object
  # being synchronized, such as undesirable workflow history being added (for
  # example edit_workflow) and double conversion for OOo documents (for
  # example document_conversion_interaction_workflow defined for _setData())
  # making the source and destination XML representation different.
  @WorkflowMethod.disable
991 992 993 994 995
  def editDocument(self, object=None, **kw):
    """
    This is the default editDocument method. This method
    can easily be overwritten.
    """
996
    object._edit(**kw)
997

998 999 1000 1001 1002 1003 1004 1005
  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)

1006 1007 1008 1009
  def nodeToString(self, node):
    """
    return an xml string corresponding to the node
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
1010
    return etree.tostring(node, encoding='utf-8', pretty_print=True)
1011 1012 1013 1014 1015 1016 1017

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

1018 1019 1020
  def _createContent(self, xml=None, object=None, object_id=None,
                     sub_object=None, reset_local_roles=False,
                     reset_workflow=False, simulate=False, **kw):
1021 1022 1023
    """
      This is the method calling to create an object
    """
1024
    # XXX We can not find an object with remote id
1025
    if object_id is None:
1026
      object_id = xml.get('id')
1027 1028
    if object_id is not None:
      if sub_object is None:
1029
        sub_object = object._getOb(object_id, None)
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042
    if sub_object is None: # If so, it doesn't exist
      portal_type = ''
      if xml.xpath('local-name()') == XML_OBJECT_TAG:
        portal_type = self.getObjectType(xml)
      sub_object, reset_local_roles, reset_workflow = self.constructContent(
                                                      object,
                                                      object_id,
                                                      portal_type)
    self.newObject(object=sub_object,
                    xml=xml,
                    simulate=simulate,
                    reset_local_roles=reset_local_roles,
                    reset_workflow=reset_workflow)
1043 1044 1045 1046 1047 1048 1049 1050
    return sub_object

  def _updateContent(self, object=None, **args):
    """
      This is the method for update the object
    """
    return self.editDocument(object=object, **args)

1051
  def _deleteContent(self, object=None, object_id=None, **kw):
1052 1053 1054
    """
      This is the method for delete the object
    """
1055
    return self.deleteObject(object, object_id, **kw)
1056

1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074
  def getContentType(self):
    """Content-Type of binded data
    """
    return 'text/xml'

  def generateDiff(self, old_data, new_data):
    """return xupdate node
    """
    return getXupdateObject(old_data, new_data)

  def applyDiff(self, original_data, diff):
    """Use xuproc for computing patched content
    """
    # XXX xuproc does not support passing
    # etree objetcs
    diff = etree.tostring(diff)
    return etree.tostring(xuproc.applyXUpdate(xml_xu_string=diff,
                                              xml_doc_string=original_data), encoding='utf-8')
1075

1076 1077 1078 1079 1080 1081 1082 1083
#  def getGidFromXML(self, xml, namespace, gid_from_xml_list):
#    """
#    return the Gid composed with xml informations
#    """
#    gid = xml.xpath('string(.//syncml:id)')
#    if gid in gid_from_xml_list or gid == ' ':
#      return False
#    return gid