XMLSyncUtils.py 65.7 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 29 30 31
##############################################################################
#
# 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.
#
##############################################################################

import smtplib
from Products.ERP5SyncML.SyncCode import SyncCode
from Products.ERP5SyncML.Subscription import Signature
32
from AccessControl.SecurityManagement import newSecurityManager
Nicolas Delaby's avatar
Nicolas Delaby committed
33
from ERP5Diff import ERP5Diff
Nicolas Delaby's avatar
Nicolas Delaby committed
34
from zLOG import LOG, INFO
Nicolas Delaby's avatar
Nicolas Delaby committed
35

36
from lxml import etree
Nicolas Delaby's avatar
Nicolas Delaby committed
37
from lxml.etree import Element, SubElement
38
from lxml.builder import E
39 40
parser = etree.XMLParser(remove_blank_text=True)

41
from xml.dom import minidom
Jean-Paul Smets's avatar
Jean-Paul Smets committed
42

43
try:
44
  from base64 import b16encode, b16decode
45
except ImportError:
46
  from base64 import encodestring as b16encode, decodestring as b16decode
47

48
class XMLSyncUtilsMixin(SyncCode):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
49

50 51
  def SyncMLHeader(self, session_id, msg_id, target, source, target_name=None,
      source_name=None, dataCred=None, authentication_format='b64',
52
      authentication_type='syncml:auth-basic'):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
53 54 55 56
    """
      Since the Header is always almost the same, this is the
      way to set one quickly.
    """
57 58 59 60 61 62
    xml = (E.SyncHdr(
            E.VerDTD('1.1'),
            E.VerProto('SyncML/1.1'),
            E.SessionID('%s' % session_id),
            E.MsgID('%s' % msg_id),
          ))
63 64 65 66 67 68 69 70
    target_node = E.Target(E.LocURI(target))
    if target_name:
      target_node.append(E.LocName(target_name.decode('utf-8')))
    xml.append(target_node)
    source_node = E.Source(E.LocURI(source))
    if source_name:
      source_node.append(E.LocName(source_name.decode('utf-8')))
    xml.append(source_node)
71 72 73 74 75 76
    if dataCred:
      xml.append(E.Cred(
                  E.Meta(E.Format(authentication_format, xmlns='syncml:metinf'),
                  E.Type(authentication_type, xmlns='syncml:metinf'),),
                  E.Data(dataCred)
                  ))
Nicolas Delaby's avatar
Nicolas Delaby committed
77
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
78

Sebastien Robin's avatar
Sebastien Robin committed
79 80
  def SyncMLAlert(self, cmd_id, sync_code, target, source, last_anchor, 
      next_anchor):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
81 82 83 84
    """
      Since the Alert section is always almost the same, this is the
      way to set one quickly.
    """
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    xml = (E.Alert(
            E.CmdID('%s' % cmd_id),
            E.Data('%s' % sync_code),
            E.Item(
              E.Target(
                E.LocURI(target)
                ),
              E.Source(
                E.LocURI(source)
                ),
              E.Meta(
                E.Anchor(
                  E.Last(last_anchor),
                  E.Next(next_anchor)
                  )
                )
              )
            ))
Nicolas Delaby's avatar
Nicolas Delaby committed
103
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
104

Nicolas Delaby's avatar
Nicolas Delaby committed
105 106
  def SyncMLStatus(self, remote_xml, data_code, cmd_id, next_anchor,
                   subscription=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
107
    """
108 109
    return a status bloc with all status corresponding to the syncml
    commands in remote_xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
110
    """
111 112

    #list of element in the SyncBody bloc
113 114
    sub_syncbody_element_list = remote_xml.xpath('/SyncML/SyncBody/*')
    message_id = self.getMessageIdFromXml(remote_xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
115 116 117 118 119 120 121 122 123 124 125 126 127
    status_list = []
    target_uri = '%s' % remote_xml.xpath('string(/SyncML/SyncHdr/Target/LocURI)')
    source_uri = '%s' % remote_xml.xpath('string(/SyncML/SyncHdr/Source/LocURI)')
    if data_code != self.AUTH_REQUIRED:
      xml = (E.Status(
               E.CmdID('%s' % cmd_id),
               E.MsgRef('%s' % message_id),
               E.CmdRef('0'),
               E.Cmd('SyncHdr'),
               E.TargetRef(target_uri),
               E.SourceRef(source_uri),
               E.Data('%s' % data_code),
               ))
128
      cmd_id += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
129
      status_list.append(xml)
130 131
    for sub_syncbody_element in sub_syncbody_element_list:
      if sub_syncbody_element.tag not in ('Status', 'Final', 'Get'):
Nicolas Delaby's avatar
Nicolas Delaby committed
132 133 134 135 136 137
        xml = (E.Status(
                 E.CmdID('%s' % cmd_id),
                 E.MsgRef('%s' % message_id),
                 E.CmdRef('%s' % sub_syncbody_element.xpath('string(.//CmdID)')),
                 E.Cmd(sub_syncbody_element.tag)
                 ))
138
        cmd_id += 1
139 140
        target_ref = sub_syncbody_element.xpath('string(.//Target/LocURI)')
        if target_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
141
          xml.append(E.TargetRef('%s' % target_ref))
142 143
        source_ref = sub_syncbody_element.xpath('string(.//Source/LocURI)')
        if source_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
144
          xml.append(E.SourceRef('%s' % source_ref))
145
        if sub_syncbody_element.tag == 'Add':
Nicolas Delaby's avatar
Nicolas Delaby committed
146
          xml.append(E.Data('%s' % self.ITEM_ADDED))
147 148
        elif sub_syncbody_element.tag == 'Alert' and \
            sub_syncbody_element.xpath('string(.//Data)') == \
149
            str(self.SLOW_SYNC):
Nicolas Delaby's avatar
Nicolas Delaby committed
150
          xml.append(E.Data('%s' % self.REFRESH_REQUIRED))
151
        elif sub_syncbody_element.tag == 'Alert':
Nicolas Delaby's avatar
Nicolas Delaby committed
152
          xml.append(E.Item(E.Data(E.Anchor(E.Next(next_anchor)))))
153
        else:
Nicolas Delaby's avatar
Nicolas Delaby committed
154 155
          xml.append(E.Data('%s' % self.SUCCESS))
        status_list.append(xml)
156

157 158
      if sub_syncbody_element.tag == 'Get' and subscription is not None:
        cmd_ref = '%s' % sub_syncbody_element.xpath('string(.//CmdID)')
159 160 161 162 163
        syncml_result = self.SyncMLPut(
                                  cmd_id,
                                  subscription,
                                  markup='Results',
                                  cmd_ref=cmd_ref,
164
                                  message_id=message_id)
Nicolas Delaby's avatar
Nicolas Delaby committed
165 166
        if syncml_result is not None:
          status_list.append(syncml_result)
167
        cmd_id += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
168 169

    return status_list, cmd_id
Jean-Paul Smets's avatar
Jean-Paul Smets committed
170

171 172
  def SyncMLConfirmation(self, cmd_id=None, target_ref=None, cmd=None,
      sync_code=None, msg_ref=None, cmd_ref=None, source_ref=None,
173
      remote_xml=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
174
    """
Sebastien Robin's avatar
Sebastien Robin committed
175
    This is used in order to confirm that an object was correctly
Jean-Paul Smets's avatar
Jean-Paul Smets committed
176 177
    synchronized
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
178
    if remote_xml is not None:
179 180 181 182
      msg_ref = '%s' % remote_xml.xpath("string(/SyncML/SyncHdr/MsgID)")
      cmd_ref = '%s' % remote_xml.xpath("string(.//CmdID)")
      target_ref = '%s' % remote_xml.xpath("string(.//Target/LocURI)")
      source_ref = '%s' % remote_xml.xpath("string(.//Source/LocURI)")
Nicolas Delaby's avatar
Nicolas Delaby committed
183
    xml = Element('Status')
184 185 186 187 188 189 190 191 192 193 194 195 196 197
    if cmd_id:
      xml.append(E.CmdID('%s' % cmd_id))
    if msg_ref:
      xml.append(E.MsgRef(msg_ref))
    if cmd_ref:
      xml.append(E.CmdRef(cmd_ref))
    if cmd:
      xml.append(E.Cmd(cmd))
    if target_ref:
      xml.append(E.TargetRef(target_ref))
    if source_ref:
      xml.append(E.SourceRef(source_ref))
    if sync_code:
      xml.append(E.Data('%s'% sync_code))
Nicolas Delaby's avatar
Nicolas Delaby committed
198
    return xml
199 200

  def SyncMLChal(self, cmd_id, cmd, target_ref, source_ref, auth_format,
Nicolas Delaby's avatar
Nicolas Delaby committed
201
      auth_type, auth_code):
Sebastien Robin's avatar
Sebastien Robin committed
202 203 204
    """
    This is used in order to ask crendentials
    """
205 206 207 208 209 210 211 212 213 214 215 216 217
    xml = (E.Status(
             E.CmdID('%s' % cmd_id),
             E.MsgRef('1'),
             E.CmdRef('0'),
             E.Cmd(cmd),
             E.TargetRef(target_ref),
             E.SourceRef(source_ref),
             E.Chal(
               E.Meta(
                 E.Format(auth_format, xmlns='syncml:metinf'),
                 E.Type(auth_type, xmlns='syncml:metinf')
                 )
               ),
Nicolas Delaby's avatar
Nicolas Delaby committed
218
            E.Data('%s' % auth_code)
219
            ))
Nicolas Delaby's avatar
Nicolas Delaby committed
220
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
221

222
  def SyncMLPut(self, cmd_id, subscription, markup='Put', cmd_ref=None,
223
      message_id=None):
224 225
    """
    this is used to inform the server of the CTType version supported
226 227
    but if the server use it to respond to a Get request, it's a <Result> markup
    instead of <Put>
228 229
    """
    conduit_name = subscription.getConduit()
230
    conduit = self.getConduitByName(conduit_name)
Nicolas Delaby's avatar
Nicolas Delaby committed
231
    xml = None
232
    #if the conduit support the SyncMLPut :
233 234 235
    if getattr(conduit, 'getCapabilitiesCTTypeList', None) is not None and \
       getattr(conduit, 'getCapabilitiesVerCTList', None) is not None and \
       getattr(conduit, 'getPreferedCapabilitieVerCT', None) is not None:
Nicolas Delaby's avatar
Nicolas Delaby committed
236 237
      xml = Element(markup)
      xml.append(E.CmdID('%s' % cmd_id))
238
      if message_id:
Nicolas Delaby's avatar
Nicolas Delaby committed
239
        xml.append(E.MsgRef('%s' % message_id))
240
      if cmd_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
        xml.append(E.CmdRef('%s' % cmd_ref))
      xml.append(E.Meta(E.Type('application/vnd.syncml-devinf+xml')),
                 E.Item(E.Source(E.LocURI('./devinf11')),
                 E.Data(E.DevInf(
                   E.VerDTD('1.1'),
                   E.Man('Nexedi'),
                   E.Mod('ERP5SyncML'),
                   E.OEM('Open Source'),
                   E.SwV('0.1'),
                   E.DevID(subscription.getSubscriptionUrl()),
                   E.DevTyp('workstation'),
                   E.UTC(),
                   E.DataStore(
                     E.SourceRef(subscription.getSourceURI()),
                     Element('E.Rx-Pref').append(
                       (E.CTType(conduit.getPreferedCapabilitieCTType()),
                        E.VerCT(conduit.getPreferedCapabilitieVerCT()))
                       )
                     )
                   )
                 )
               ))
      data_store = xml.find('DataStore')
      tx_element_list = []
      rx_element_list = []
266 267 268
      for type in conduit.getCapabilitiesCTTypeList():
        if type != self.MEDIA_TYPE['TEXT_XML']:
          for tx_version in conduit.getCapabilitiesVerCTList(type):
Nicolas Delaby's avatar
Nicolas Delaby committed
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
            rx_element_list.append(E.Rx(E.CTType(type), E.VerCT(rx_version)))
            tx_element_list.append(E.Tx(E.CTType(type), E.VerCT(rx_version)))
      data_store.extend(rx_element_list)
      data_store.append(Element('Tx-Pref').extend(
                           (E.CTType(conduit.getPreferedCapabilitieCTType()),
                            E.VerCT(conduit.getPreferedCapabilitieVerCT()))
                            ))
      data_store.extend(tx_element_list)
      data_store.append(E.SyncCap(
                          E.SyncType('2'),
                          E.SyncType('1'),
                          E.SyncType('4'),
                          E.SyncType('6')
                          ))
    return xml
284

Jean-Paul Smets's avatar
Jean-Paul Smets committed
285 286 287 288 289 290 291 292 293
  def sendMail(self, fromaddr, toaddr, id_sync, msg):
    """
      Send a message via email
      - sync_object : this is a publication or a subscription
      - message : what we want to send
    """
    header = "Subject: %s\n" % id_sync
    header += "To: %s\n\n" % toaddr
    msg = header + msg
294
    #LOG('SubSendMail', DEBUG,'from: %s, to: %s' % (fromaddr,toaddr))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
295 296 297
    server = smtplib.SMTP('localhost')
    server.sendmail(fromaddr, toaddr, msg)
    # if we want to send the email to someone else (debugging)
298
    #server.sendmail(fromaddr, "seb@localhost", msg)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
299 300
    server.quit()

301
  def addXMLObject(self, cmd_id=0, object=None, xml_string=None,
302
                  more_data=0, gid=None, media_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
303 304 305
    """
      Add an object with the SyncML protocol
    """
306 307
    data_node = Element('Data')
    if media_type == self.MEDIA_TYPE['TEXT_XML'] and isinstance(xml_string, str):
308
      data_node.append(etree.XML(xml_string, parser=parser))
309 310 311 312
    elif media_type == self.MEDIA_TYPE['TEXT_XML'] and \
         not isinstance(xml_string, str):
      #xml_string could be Partial element if partial XML
      data_node.append(xml_string)
313
    else:
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
      cdata = etree.CDATA(xml_string.decode('utf-8'))
      data_node.text = cdata
    xml = (E.Add(
            E.CmdID('%s' % cmd_id),
            E.Meta(
              E.Type(media_type)
              ),
            E.Item(
              E.Source(
                E.LocURI(gid)
                ),
              data_node
              )
            ))
    if more_data:
      item_node = xml.find('Item')
      item_node.append(Element('MoreData'))
Nicolas Delaby's avatar
Nicolas Delaby committed
331
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
332

333
  def deleteXMLObject(self, cmd_id=0, object_gid=None, rid=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
334
    """
Sebastien Robin's avatar
Sebastien Robin committed
335 336
      Delete an object with the SyncML protocol
    """
337 338
    if rid:
      elem_to_append = E.Target(E.LocURI('%s' % rid))
339
    else:
340 341 342 343 344 345 346
      elem_to_append = E.Source(E.LocURI('%s' % object_gid))
    xml = (E.Delete(
             E.CmdID('%s' % cmd_id),
             E.Item(
               elem_to_append
               )
             ))
Nicolas Delaby's avatar
Nicolas Delaby committed
347
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
348

349
  def replaceXMLObject(self, cmd_id=0, object=None, xml_string=None,
350
                       more_data=0, gid=None, rid=None, media_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
351
    """
Sebastien Robin's avatar
Sebastien Robin committed
352 353
      Replace an object with the SyncML protocol
    """
354 355
    if rid:
      elem_to_append = E.Target(E.LocURI('%s' % rid))
356
    else:
357 358 359 360 361
      elem_to_append = E.Source(E.LocURI('%s' % gid))
    data_node = Element('Data')
    if not isinstance(xml_string, (str, unicode)):
      data_node.append(xml_string)
    else:
362
      data_node.append(etree.XML(xml_string, parser=parser))
363 364 365 366 367 368 369 370 371 372 373 374 375
    xml = (E.Replace(
             E.CmdID('%s' % cmd_id),
             E.Meta(
               E.Type(media_type)
               ),
             E.Item(
               elem_to_append,
               data_node
               )
             ))
    if more_data:
      item_node = xml.find('Item')
      item_node.append(Element('MoreData'))
Nicolas Delaby's avatar
Nicolas Delaby committed
376
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
377

Nicolas Delaby's avatar
Nicolas Delaby committed
378
  def getXupdateObject(self, object_xml=None, old_xml=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
379 380
    """
    Generate the xupdate with the new object and the old xml
Nicolas Delaby's avatar
Nicolas Delaby committed
381 382 383
    """
    erp5diff = ERP5Diff()
    erp5diff.compare(old_xml, object_xml)
384 385 386 387
    if isinstance(erp5diff._result, minidom.Document):
      #XXX While ERP5Diff use minidom, this part needs to be keeped.
      #minidom is buggy, add namespace declaration, and version attributes
      attr_version = erp5diff._result.createAttributeNS(None, 'version')
388 389
      attr_version.value = '1.0'
      erp5diff._result.documentElement.setAttributeNodeNS(attr_version)
390 391
      attr_ns = erp5diff._result.createAttributeNS(None, 'xmlns:xupdate')
      attr_ns.value = 'http://www.xmldb.org/xupdate'
392 393 394 395 396
      erp5diff._result.documentElement.setAttributeNodeNS(attr_ns)
      xupdate = erp5diff._result.toxml('utf-8')
    else:
      #Upper version of ERP5Diff produce valid XML.
      xupdate = erp5diff.outputString()
Nicolas Delaby's avatar
Nicolas Delaby committed
397
    #omit xml declaration
Jean-Paul Smets's avatar
Jean-Paul Smets committed
398 399 400
    xupdate = xupdate[xupdate.find('<xupdate:modifications'):]
    return xupdate

401
  def getSessionIdFromXml(self, xml):
402 403 404
    """
    We will retrieve the session id of the message
    """
405
    return int(xml.xpath('string(/SyncML/SyncHdr/SessionID)'))
406

407
  def getMessageIdFromXml(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
408 409 410
    """
    We will retrieve the message id of the message
    """
411
    return int(xml.xpath('string(/SyncML/SyncHdr/MsgID)'))
Sebastien Robin's avatar
Sebastien Robin committed
412 413 414 415 416

  def getTarget(self, xml):
    """
    return the target in the SyncHdr section
    """
417
    return '%s' % xml.xpath('string(/SyncML/SyncHdr/Target/LocURI)')
Sebastien Robin's avatar
Sebastien Robin committed
418

Jean-Paul Smets's avatar
Jean-Paul Smets committed
419 420 421 422 423
  def getAlertLastAnchor(self, xml_stream):
    """
      Return the value of the last anchor, in the
      alert section of the xml_stream
    """
424
    return '%s' % xml_stream.xpath('string(.//Alert/Item/Meta/Anchor/Last)')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
425 426 427 428 429 430

  def getAlertNextAnchor(self, xml_stream):
    """
      Return the value of the next anchor, in the
      alert section of the xml_stream
    """
431
    return '%s' % xml_stream.xpath('string(.//Alert/Item/Meta/Anchor/Next)')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
432

433
  def getSourceURI(self, xml):
434 435 436
    """
    return the source uri of the data base
    """
437
    return '%s' % xml.xpath('string(//SyncBody/Alert/Item/Source/LocURI)')
438

439 440 441 442
  def getTargetURI(self, xml):
    """
    return the target uri of the data base
    """
443
    return '%s' % xml.xpath('string(//SyncBody/Alert/Item/Target/LocURI)')
444

445
  def getSubscriptionUrlFromXML(self, xml):
446 447 448
    """
    return the source URI of the syncml header
    """
449
    return '%s' % xml.xpath('string(//SyncHdr/Source/LocURI)')
450

Jean-Paul Smets's avatar
Jean-Paul Smets committed
451 452 453 454
  def getStatusTarget(self, xml):
    """
      Return the value of the alert code inside the xml_stream
    """
455
    return '%s' % xml.xpath('string(TargetRef)')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
456 457 458 459 460

  def getStatusCode(self, xml):
    """
      Return the value of the alert code inside the xml_stream
    """
461 462
    status_code = '%s' % xml.xpath('string(Data)')
    if status_code:
463
      return int(status_code)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
464 465
    return None

466 467 468 469
  def getStatusCommand(self, xml):
    """
      Return the value of the command inside the xml_stream
    """
470
    cmd = None
471
    if xml.tag == 'Status':
Nicolas Delaby's avatar
Nicolas Delaby committed
472
      cmd = '%s' % xml.xpath('string(Cmd)')
473
    return cmd
474

475 476 477
  def getCred(self, xml):
    """
      return the credential information : type, format and data
Nicolas Delaby's avatar
Nicolas Delaby committed
478
    """
479 480 481
    format = '%s' % xml.xpath("string(/SyncML/SyncHdr/Cred/Meta/*[local-name() = 'Format'])")
    type = '%s' % xml.xpath("string(/SyncML/SyncHdr/Cred/Meta/*[local-name() = 'Type'])")
    data = '%s' % xml.xpath('string(/SyncML/SyncHdr/Cred/Data)')
482 483 484

    return (format, type, data)

485
  def checkCred(self, xml_stream):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
486
    """
487
      Check if there's a Cred section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
488
    """
489
    return bool(xml_stream.xpath('string(/SyncML/SyncHdr/Cred)'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
490

491
  def getChal(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
492
    """
493
      return the chalenge information : format and type
494
    """
495 496
    format = '%s' % xml.xpath("string(//*[local-name() = 'Format'])")
    type = '%s' % xml.xpath("string(//*[local-name() = 'Type'])")
497 498 499
    return (format, type)

  def checkChal(self, xml_stream):
Sebastien Robin's avatar
Sebastien Robin committed
500
    """
501 502
      Check if there's a Chal section in the xml_stream
    """
503
    return bool(xml_stream.xpath('string(/SyncML/SyncBody/Status/Chal)'))
504

505 506 507 508
  def checkMap(self, xml_stream):
    """
      Check if there's a Map section in the xml_stream
    """
509
    return bool(xml_stream.xpath('string(/SyncML/SyncBody/Map)'))
510 511 512 513 514 515

  def setRidWithMap(self, xml_stream, subscriber):
    """
      get all the local objects of the given target id and set them the rid with
      the given source id (in the Map section)
    """
516
    item_list = xml_stream.xpath('/SyncML/SyncBody/Map/MapItem')
517
    for map_item in item_list:
518
      gid = '%s' % map_item.xpath('string(.//Target/LocURI)')
519
      signature = subscriber.getSignatureFromGid(gid)
520
      rid = '%s' % map_item.xpath('string(.//Source/LocURI)')
521
      signature.setRid(rid)
522

523
  def getAlertCodeFromXML(self, xml_stream):
524 525 526
    """
      Return the value of the alert code inside the full syncml message
    """
527 528
    alert_code = '%s' % xml_stream.xpath('string(/SyncML/SyncBody/Alert/Data)')
    if alert_code:
529 530 531
      return int(alert_code)
    else:
      return None
Sebastien Robin's avatar
Sebastien Robin committed
532

Jean-Paul Smets's avatar
Jean-Paul Smets committed
533 534
  def checkAlert(self, xml_stream):
    """
Sebastien Robin's avatar
Sebastien Robin committed
535
      Check if there's an Alert section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
536
    """
537
    return bool(xml_stream.xpath('string(/SyncML/SyncBody/Alert)'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
538 539 540 541 542

  def checkSync(self, xml_stream):
    """
      Check if there's an Sync section in the xml_xtream
    """
543
    return bool(xml_stream.xpath('string(/SyncML/SyncBody/Sync)'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
544

545
  def checkStatus(self, xml_stream):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
546 547 548
    """
      Check if there's a Status section in the xml_xtream
    """
549
    return bool(xml_stream.xpath('string(/SyncML/SyncBody/Status)'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
550

551
  def getSyncActionList(self, xml_stream):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
552
    """
553
    return the list of the action (could be "add", "replace", "delete").
Jean-Paul Smets's avatar
Jean-Paul Smets committed
554
    """
555 556 557 558 559 560 561 562
    return xml_stream.xpath('//Add|//Delete|//Replace')

  def getSyncBodyStatusList(self, xml_stream):
    """
    return the list of dictionary corredponding to the data of each status bloc
    the data are : cmd, code and source
    """
    status_list = []
563 564
    status_node_list = xml_stream.xpath('//Status')
    for status in status_node_list:
565
      tmp_dict = {}
566 567 568 569
      tmp_dict['cmd'] = '%s' % status.xpath('string(./Cmd)')
      tmp_dict['code'] = '%s' % status.xpath('string(./Data)')
      tmp_dict['source'] = '%s' % status.xpath('string(./SourceRef)')
      tmp_dict['target'] = '%s' % status.xpath('string(./TargetRef)')
570 571
      status_list.append(tmp_dict)
    return status_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
572

573 574 575 576
  def getDataText(self, action):
    """
    return the section data in text form, it's usefull for the VCardConduit
    """
577
    return '%s' % action.xpath('string(.//Item/Data)')
578

Jean-Paul Smets's avatar
Jean-Paul Smets committed
579 580 581 582
  def getDataSubNode(self, action):
    """
      Return the node starting with <object....> of the action
    """
583 584 585
    object_node_list = action.xpath('.//Item/Data/*[1]')
    if object_node_list:
      return object_node_list[0]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
586 587
    return None

588
  def getActionId(self, action):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
589 590 591
    """
      Return the rid of the object described by the action
    """
592 593 594
    id = '%s' % action.xpath('string(.//Item/Source/LocURI)')
    if not id:
      id = '%s' % action.xpath('string(.//Item/Target/LocURI)')
595
    return id
Jean-Paul Smets's avatar
Jean-Paul Smets committed
596 597 598 599 600

  def checkActionMoreData(self, action):
    """
      Return the rid of the object described by the action
    """
601
    return bool(action.xpath('.//Item/MoreData'))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
602 603 604 605 606

  def getActionType(self, action):
    """
      Return the type of the object described by the action
    """
607
    return '%s' % action.xpath('string(.//Meta/Type)')
608

609
  def cutXML(self, xml_string):
610
    """
611
    Sliced a xml tree a return two fragment
612
    """
613 614 615 616 617 618
    line_list = xml_string.split('\n')
    short_string = '\n'.join(line_list[:self.MAX_LINES])
    rest_string = '\n'.join(line_list[self.MAX_LINES:])
    xml_string = etree.Element('Partial')
    xml_string.text = etree.CDATA(short_string.decode('utf-8'))
    return xml_string, rest_string
619

620
  def getSyncMLData(self, domain=None, remote_xml=None, cmd_id=0,
Nicolas Delaby's avatar
Nicolas Delaby committed
621
                    subscriber=None, xml_confirmation_list=None, conduit=None,
Nicolas Delaby's avatar
Nicolas Delaby committed
622
                    max=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
623
    """
624 625
    This generate the syncml data message. This returns a string
    with all modification made locally (ie replace, add ,delete...)
626 627 628

    if object is not None, this usually means we want to set the
    actual xupdate on the signature.
Jean-Paul Smets's avatar
Jean-Paul Smets committed
629
    """
630
    #LOG('getSyncMLData starting...', DEBUG, domain.getId())
Sebastien Robin's avatar
Sebastien Robin committed
631 632
    if isinstance(conduit, str):
      conduit = self.getConduitByName(conduit)
Nicolas Delaby's avatar
Nicolas Delaby committed
633 634
    if xml_confirmation_list is None:
      xml_confirmation_list = []
635
    local_gid_list = []
Nicolas Delaby's avatar
Nicolas Delaby committed
636
    syncml_data_list = kw.get('syncml_data_list', [])
Nicolas Delaby's avatar
Nicolas Delaby committed
637
    result = {'finished':1}
638
    if isinstance(remote_xml, (str, unicode)):
639
      remote_xml = etree.XML(remote_xml, parser=parser)
640 641 642 643
    if domain.isOneWayFromServer():
      #Do not set object_path_list, subscriber send nothing
      subscriber.setRemainingObjectPathList([])
    elif subscriber.getRemainingObjectPathList() is None:
644
      object_list = domain.getObjectList()
645
      object_path_list = [x.getPhysicalPath() for x in object_list]
646
      subscriber.setRemainingObjectPathList(object_path_list)
647 648 649 650 651 652
      if subscriber.getMediaType() == self.MEDIA_TYPE['TEXT_VCARD']:
        #here the method getGidFromObject don't return the good gid because
        #the conduit use the catalog to determine it and object are not yet
        #cataloged so if there is many times the same gid, we must count it
        gid_not_encoded_list = []
        for object in object_list:
653
          #LOG('getSyncMLData :', DEBUG, 'object:%s,  objectTitle:%s, local_gid_list:%s' % (object, object.getTitle(), local_gid_list))
654 655 656
          gid = b16decode(domain.getGidFromObject(object))
          if gid in gid_not_encoded_list:
            number = len([item for item in gid_not_encoded_list if item.startswith(gid)])
657 658
            if number:
              gid = '%s__%s' %  (gid, str(number+1))
659 660
          gid_not_encoded_list.append(gid)
          local_gid_list.append(b16encode(gid))
661
          #LOG('getSyncMLData :', DEBUG,'gid_not_encoded_list:%s, local_gid_list:%s, gid:%s' % (gid_not_encoded_list, local_gid_list, gid))
662
      else:
663
        local_gid_list = [domain.getGidFromObject(x) for x in object_list]
664 665

      # Objects to remove
666
      #LOG('getSyncMLData remove object to remove ...', DEBUG, '')
667
      for object_gid in subscriber.getGidList():
Nicolas Delaby's avatar
Nicolas Delaby committed
668
        if object_gid not in local_gid_list:
669
          # This is an object to remove
670
          signature = subscriber.getSignatureFromGid(object_gid)
671 672
          if signature.getStatus() != self.PARTIAL:
            # If partial, then we have a signature but no local object
673 674 675 676 677 678 679 680
            rid = signature.getRid()
            syncml_data_list.append(self.deleteXMLObject(object_gid=object_gid,
                                                         rid=rid,
                                                         cmd_id=cmd_id))
            cmd_id += 1
          #delete Signature if object does not exist anymore
          subscriber.delSignature(object_gid)

681
    local_gid_list = []
682
    loop = 0
683
    for object_path in subscriber.getRemainingObjectPathList():
Nicolas Delaby's avatar
Nicolas Delaby committed
684 685 686
      if max is not None and loop >= max:
        result['finished'] = 0
        break
687
      #LOG('getSyncMLData object_path', INFO, object_path)
688
      object = self.unrestrictedTraverse(object_path)
689
      status = self.SENT
690
      object_gid = domain.getGidFromObject(object)
691
      if not object_gid:
Nicolas Delaby's avatar
Nicolas Delaby committed
692
        continue
693
      local_gid_list += [object_gid]
694
      force = 0
Nicolas Delaby's avatar
Nicolas Delaby committed
695
      if ''.join(syncml_data_list).count('\n') < self.MAX_LINES and not \
696 697
          object.id.startswith('.'):
        # If not we have to cut
698 699 700 701 702 703 704
        #LOG('getSyncMLData', 0, 'object_path: %s' % '/'.join(object_path))
        #LOG('getSyncMLData', 0, 'xml_mapping: %s' % str(domain.getXMLMapping()))
        #LOG('getSyncMLData', 0, 'code: %s' % str(self.getAlertCodeFromXML(remote_xml)))
        #LOG('getSyncMLData', 0, 'gid_list: %s' % str(local_gid_list))
        #LOG('getSyncMLData', 0, 'subscriber.getGidList: %s' % subscriber.getGidList())
        #LOG('getSyncMLData', 0, 'hasSignature: %s' % str(subscriber.hasSignature(object_gid)))
        #LOG('getSyncMLData', 0, 'alert_code == slowsync: %s' % str(self.getAlertCodeFromXML(remote_xml) == self.SLOW_SYNC))
705

Nicolas Delaby's avatar
Nicolas Delaby committed
706 707 708
        signature = subscriber.getSignatureFromGid(object_gid)
        ## Here we first check if the object was modified or not by looking at dates
        #if signature is not None:
709
          #LOG('getSyncMLData', DEBUG, 'signature.getStatus: %s' % signature.getStatus())
710
        status = self.SENT
711
        more_data = 0
712
        # For the case it was never synchronized, we have to send everything
Nicolas Delaby's avatar
Nicolas Delaby committed
713
        if signature is not None and signature.getXMLMapping() is None:
714
          pass
Nicolas Delaby's avatar
Nicolas Delaby committed
715
        elif signature is None or (signature.getXML() is None and \
716
            signature.getStatus() != self.PARTIAL) or \
717
            self.getAlertCodeFromXML(remote_xml) == self.SLOW_SYNC:
718
          #LOG('getSyncMLData', DEBUG, 'Current object.getPath: %s' % object.getPath())
719
          xml_string = domain.getXMLFromObject(object)
720
          gid = subscriber.getGidFromObject(object)
721
          signature = Signature(id=gid, object=object).__of__(subscriber)
722
          signature.setTempXML(xml_string)
723
          if xml_string.count('\n') > self.MAX_LINES:
724 725
            xml_string, rest_string = self.cutXML(xml_string)
            more_data = 1
726
            signature.setPartialXML(rest_string)
727
            status = self.PARTIAL
728
            signature.setAction('Add')
729 730
          #in fisrt, we try with rid if there is one
          gid = signature.getRid() or signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
731
          syncml_data_list.append(self.addXMLObject(
732 733 734 735 736
                                  cmd_id=cmd_id,
                                  object=object,
                                  gid=gid,
                                  xml_string=xml_string,
                                  more_data=more_data,
Nicolas Delaby's avatar
Nicolas Delaby committed
737
                                  media_type=subscriber.getMediaType()))
738 739 740
          cmd_id += 1
          signature.setStatus(status)
          subscriber.addSignature(signature)
741 742
        elif signature.getStatus() in (self.NOT_SYNCHRONIZED,
                                       self.PUB_CONFLICT_MERGE,):
743
          # We don't have synchronized this object yet
744
          xml_object = domain.getXMLFromObject(object)
745 746
          #LOG('getSyncMLData', DEBUG, 'checkMD5: %s' % str(signature.checkMD5(xml_object)))
          #LOG('getSyncMLData', DEBUG, 'getStatus: %s' % str(signature.getStatus()))
747
          if signature.getStatus() == self.PUB_CONFLICT_MERGE:
Nicolas Delaby's avatar
Nicolas Delaby committed
748 749 750 751 752
            xml_confirmation_list.append(self.SyncMLConfirmation(
                                    cmd_id=cmd_id,
                                    source_ref=signature.getGid(),
                                    sync_code=self.CONFLICT_MERGE,
                                    cmd='Replace'))
753
          set_synchronized = 1
754
          if not signature.checkMD5(xml_object):
755
            set_synchronized = 0
756
            # This object has changed on this side, we have to generate some xmldiff
757
            if subscriber.getMediaType() == self.MEDIA_TYPE['TEXT_XML']:
758
              xml_string = self.getXupdateObject(xml_object, signature.getXML())
759
            else: #if there is no xml, we re-send all the object
760
              xml_string = xml_object
761 762 763
            if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
              xml_string = xml_object
            elif xml_string.count('\n') > self.MAX_LINES:
764
              # This make comment fails, so we need to replace
765
              xml_string, rest_string = self.cutXML(xml_string)
766
              more_data = 1
767 768 769
              signature.setPartialXML(rest_string)
              status = self.PARTIAL
              signature.setAction('Replace')
770
              signature.setStatus(status)
771 772
            rid = signature.getRid()#in fisrt, we try with rid if there is one
            gid = signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
773
            syncml_data_list.append(self.replaceXMLObject(
774 775 776 777
                                        cmd_id=cmd_id, object=object,
                                        gid=gid, rid=rid,
                                        xml_string=xml_string,
                                        more_data=more_data,
Nicolas Delaby's avatar
Nicolas Delaby committed
778
                                        media_type=subscriber.getMediaType()))
779 780
            cmd_id += 1
            signature.setTempXML(xml_object)
781 782
          # Now we can apply the xupdate from the subscriber
          subscriber_xupdate = signature.getSubscriberXupdate()
783
          #LOG('getSyncMLData subscriber_xupdate', DEBUG, subscriber_xupdate)
784 785
          if subscriber_xupdate is not None:
            old_xml = signature.getXML()
786 787 788 789 790 791
            conduit.updateNode(
                        xml=subscriber_xupdate,
                        object=object,
                        previous_xml=old_xml,
                        force=(domain.getDomainType() == self.SUB),
                        simulate=0)
792
            xml_object = domain.getXMLFromObject(object)
793 794 795
            signature.setTempXML(xml_object)
          if set_synchronized: # We have to do that after this previous update
            # We should not have this case when we are in CONFLICT_MERGE
796
            signature.setStatus(self.SYNCHRONIZED)
797
        elif signature.getStatus() == self.PUB_CONFLICT_CLIENT_WIN:
798
          # We have decided to apply the update
799
          # XXX previous_xml will be geXML instead of getTempXML because
800 801
          # some modification was already made and the update
          # may not apply correctly
802
          xml_update = signature.getPartialXML()
803 804 805 806 807
          conduit.updateNode(
                      xml=signature.getPartialXML(),
                      object=object,
                      previous_xml=signature.getXML(),
                      force=1)
Nicolas Delaby's avatar
Nicolas Delaby committed
808 809 810 811 812
          xml_confirmation_list.append(self.SyncMLConfirmation(
                                  cmd_id=cmd_id,
                                  target_ref=object_gid,
                                  sync_code=self.CONFLICT_CLIENT_WIN,
                                  cmd='Replace'))
813
          signature.setStatus(self.SYNCHRONIZED)
814
        elif signature.getStatus() == self.PARTIAL:
815
          xml_string = signature.getPartialXML()
816 817 818 819 820 821 822
          xml_to_send = Element('Partial')
          xml_to_send.text = etree.CDATA(xml_string.decode('utf-8'))
          if(subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']):
            xml_to_send = domain.getXMLFromObject(object)
          elif xml_string.count('\n') > self.MAX_LINES:
            xml_to_send, rest_string = self.cutXML(xml_string)
            more_data = 1
823 824 825
            signature.setPartialXML(rest_string)
            status = self.PARTIAL
          signature.setStatus(status)
826
          if signature.getAction() == 'Replace':
827 828
            rid = signature.getRid()#in fisrt, we try with rid if there is one
            gid = signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
829
            syncml_data_list.append(self.replaceXMLObject(
830 831 832 833
                                       cmd_id=cmd_id,
                                       object=object,
                                       gid=gid,
                                       rid=rid,
834
                                       xml_string=xml_to_send,
835
                                       more_data=more_data,
Nicolas Delaby's avatar
Nicolas Delaby committed
836
                                       media_type=subscriber.getMediaType()))
837
          elif signature.getAction() == 'Add':
838 839
            #in fisrt, we try with rid if there is one
            gid = signature.getRid() or signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
840 841 842 843 844 845 846
            syncml_data_list.append(self.addXMLObject(
                                        cmd_id=cmd_id,
                                        object=object,
                                        gid=gid,
                                        xml_string=xml_to_send,
                                        more_data=more_data,
                                        media_type=subscriber.getMediaType()))
Sebastien Robin's avatar
Sebastien Robin committed
847 848
        if not more_data:
          subscriber.removeRemainingObjectPath(object_path)
849
      else:
Nicolas Delaby's avatar
Nicolas Delaby committed
850
        result['finished'] = 1
851
        break
852
      loop += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
853 854
    result['syncml_data_list'] = syncml_data_list
    result['xml_confirmation_list'] = xml_confirmation_list
855 856
    result['cmd_id'] = cmd_id
    return result
857

858
  def applyActionList(self, domain=None, subscriber=None, cmd_id=0,
859
                      remote_xml=None, conduit=None, simulate=0):
860 861 862
    """
    This just look to a list of action to do, then id applies
    each action one by one, thanks to a conduit
Jean-Paul Smets's avatar
Jean-Paul Smets committed
863
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
864
    xml_confirmation_list = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
865
    has_next_action = 0
866
    gid_from_xml_list = []
867
    destination = self.unrestrictedTraverse(domain.getDestinationPath())
868 869
    #LOG('applyActionList args', DEBUG, 'domain : %s\n subscriber : %s\n cmd_id : %s' % (domain.getPath(), subscriber.getPath(), cmd_id))
    #LOG('applyActionList', DEBUG, self.getSyncActionList(remote_xml))
870
    for action in self.getSyncActionList(remote_xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
871 872 873
      conflict_list = []
      status_code = self.SUCCESS
      # Thirst we have to check the kind of action it is
874
      partial_data = '%s' % action.xpath('string(.//Item/Data/Partial)')
875
      rid = self.getActionId(action)
876 877 878
      if action.tag != 'Delete':
        if getattr(conduit, 'getGidFromXML', None) is not None and \
           conduit.getGidFromXML(self.getDataText(action), gid_from_xml_list):
879
          gid = conduit.getGidFromXML(self.getDataText(action),
880
                                      gid_from_xml_list)
881 882
          gid_from_xml_list.append(gid)
          gid = b16encode(gid)
883
        else:
884
          gid = rid
885
      else:
886
        gid = rid
887
      object_id = domain.generateNewIdWithGenerator(object=destination, gid=gid)
888
      signature = subscriber.getSignatureFromGid(gid)
Nicolas Delaby's avatar
Nicolas Delaby committed
889
      if signature is not None and rid != gid:
890 891 892
        #in this case, the object was created on another subscriber than erp5
        # and we should save it's remote id
        signature.setRid(rid)
893
      #LOG('gid == rid ?', DEBUG, 'gid=%s, rid=%s' % (gid, rid))
894
      object = subscriber.getObjectFromGid(gid)
Nicolas Delaby's avatar
Nicolas Delaby committed
895
      if object is None and not domain.getSynchronizeWithERP5Sites():
896 897 898 899 900
        #if the object is None, that could mean two things :
        # - the object to synchronize don't exists
        # - the id is not a gid but a rid
        #here we try to find an object with the rid
        #LOG('applyActionList, try to find an object with rid', DEBUG, '')
901
        object = subscriber.getObjectFromRid(rid)
902
        signature = subscriber.getSignatureFromRid(rid)
903
        if signature is not None:
904
          gid = signature.getId()
905
      #LOG('applyActionList subscriber.getObjectFromGid %s' % gid, DEBUG, object)
Nicolas Delaby's avatar
Nicolas Delaby committed
906
      if signature is None:
907
        #LOG('applyActionList, signature is None', DEBUG, signature)
908
        if gid == rid:
Nicolas Delaby's avatar
Nicolas Delaby committed
909
          signature = Signature(id=gid, status=self.NOT_SYNCHRONIZED,
910
                                object=object).__of__(subscriber)
911 912
        else:
          signature = Signature(rid=rid, id=gid, status=self.NOT_SYNCHRONIZED,
913
                                object=object).__of__(subscriber)
914
        signature.setObjectId(object_id)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
915 916
        subscriber.addSignature(signature)
      force = signature.getForce()
917
      if not self.checkActionMoreData(action):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
918
        data_subnode = None
919
        if partial_data:
920
          signature_partial_xml = signature.getPartialXML()
921 922
          if signature_partial_xml:
            data_subnode = signature_partial_xml + partial_data
923 924
          else:
            data_subnode = partial_data
925
          #LOG('applyActionList', DEBUG, 'data_subnode: %s' % data_subnode)
926
          if subscriber.getMediaType() == self.MEDIA_TYPE['TEXT_XML']:
927
            data_subnode = etree.XML(data_subnode, parser=parser)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
928
        else:
929
          if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
930
            data_subnode = self.getDataText(action)
931
          else:
932
            data_subnode = self.getDataSubNode(action)
933
        if action.tag == 'Add':
Jean-Paul Smets's avatar
Jean-Paul Smets committed
934
          # Then store the xml of this new subobject
935
          reset = 0
936
          if object is None:
937 938 939
            add_data = conduit.addNode(xml=data_subnode,
                                       object=destination,
                                       object_id=object_id)
940
            conflict_list.extend(add_data['conflict_list'])
941 942
            # Retrieve directly the object from addNode
            object = add_data['object']
Nicolas Delaby's avatar
Nicolas Delaby committed
943 944
            if object is not None:
              signature.setPath(object.getPhysicalPath())
945
              signature.setObjectId(object.getId())
946
          else:
947
            reset = 1
948 949
            #Object was retrieve but need to be updated without recreated
            #usefull when an object is only deleted by workflow.
950
            if data_subnode is not None:
951 952 953
              if not isinstance(data_subnode, str):
                xml_string = etree.tostring(data_subnode, encoding='utf-8')
              actual_xml = subscriber.getXMLFromObject(object=object, force=1)
954
              data_subnode = self.getXupdateObject(xml_string, actual_xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
955
            conflict_list.extend(conduit.updateNode(
956 957 958 959
                                        xml=data_subnode,
                                        object=object,
                                        previous_xml=signature.getXML(),
                                        force=force,
Nicolas Delaby's avatar
Nicolas Delaby committed
960
                                        simulate=simulate))
961 962
            xml_object = domain.getXMLFromObject(object)
            signature.setTempXML(xml_object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
963
          if object is not None:
964
            #LOG('applyActionList', DEBUG, 'addNode, found the object')
965 966 967
            if reset:
              #After a reset we want copy the LAST XML view on Signature.
              #this implementation is not sufficient, need to be improved.
968
              if not isinstance(data_subnode, str):
Nicolas Delaby's avatar
Nicolas Delaby committed
969 970
                xml_object = etree.tostring(data_subnode, encoding='utf-8',
                                            pretty_print=True)
971 972
              else:
                xml_object = data_subnode
973 974
            else:
              xml_object = domain.getXMLFromObject(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
975
            signature.setStatus(self.SYNCHRONIZED)
976
            #signature.setId(object.getId())
977
            signature.setPath(object.getPhysicalPath())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
978
            signature.setXML(xml_object)
Nicolas Delaby's avatar
Nicolas Delaby committed
979 980 981 982 983
            xml_confirmation_list.append(self.SyncMLConfirmation(
                                    cmd_id=cmd_id,
                                    cmd='Add',
                                    sync_code=self.ITEM_ADDED,
                                    remote_xml=action))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
984
            cmd_id +=1
985
        elif action.tag == 'Replace':
986
          #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % str(object))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
987
          if object is not None:
988
            #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % object.id)
989
            signature = subscriber.getSignatureFromGid(gid)
990 991
            if signature is None:
              signature = subscriber.getSignatureFromRid(gid)
992
            #LOG('applyActionList', DEBUG, 'previous signature: %s' % str(signature))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
993
            previous_xml = signature.getXML()
994 995 996 997 998 999
            conflict_list += conduit.updateNode(
                                        xml=data_subnode,
                                        object=object,
                                        previous_xml=signature.getXML(),
                                        force=force,
                                        simulate=simulate)
1000
            xml_object = domain.getXMLFromObject(object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1001
            signature.setTempXML(xml_object)
1002
            if conflict_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1003 1004
              status_code = self.CONFLICT
              signature.setStatus(self.CONFLICT)
1005 1006
              signature.setConflictList(signature.getConflictList()+conflict_list)
              data_subnode_string = etree.tostring(data_subnode, encoding='utf-8')
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1007
              signature.setPartialXML(data_subnode_string)
1008
            elif not simulate:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1009
              signature.setStatus(self.SYNCHRONIZED)
Nicolas Delaby's avatar
Nicolas Delaby committed
1010 1011 1012 1013 1014
            xml_confirmation_list.append(self.SyncMLConfirmation(
                                    cmd_id=cmd_id,
                                    cmd='Replace',
                                    sync_code=status_code,
                                    remote_xml=action))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1015
            cmd_id +=1
1016
            if simulate:
Sebastien Robin's avatar
Sebastien Robin committed
1017
              # This means we are on the publisher side and we want to store
1018 1019
              # the xupdate from the subscriber and we also want to generate
              # the current xupdate from the last synchronization
1020
              data_subnode_string = etree.tostring(data_subnode, encoding='utf-8')
1021
              #LOG('applyActionList, subscriber_xupdate:', TRACE, data_subnode_string)
1022 1023
              signature.setSubscriberXupdate(data_subnode_string)

1024
        elif action.tag == 'Delete':
1025
          object_id = signature.getId()
1026
          #LOG('applyActionList Delete on : ', DEBUG, (signature.getId(), subscriber.getObjectFromGid(object_id)))
1027
          if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
1028
            data_subnode = self.getDataText(action)
1029
          else:
1030
            data_subnode = self.getDataSubNode(action)
1031
          #LOG('applyActionList, object gid to delete :', 0, subscriber.getObjectFromGid(object_id))
1032
          if subscriber.getObjectFromGid(object_id) is not None:
1033
          #if the object exist:
1034 1035 1036 1037
            conduit.deleteNode(
                        xml=data_subnode,
                        object=destination,
                        object_id=subscriber.getObjectFromGid(object_id).getId())
1038
            subscriber.delSignature(gid)
Nicolas Delaby's avatar
Nicolas Delaby committed
1039 1040 1041 1042 1043
          xml_confirmation_list.append(self.SyncMLConfirmation(
                                  cmd_id=cmd_id,
                                  cmd='Delete',
                                  sync_code=status_code,
                                  remote_xml=action))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1044 1045 1046
      else: # We want to retrieve more data
        signature.setStatus(self.PARTIAL)
        previous_partial = signature.getPartialXML() or ''
1047
        previous_partial += partial_data
1048
        #LOG('applyActionList', DEBUG, 'setPartialXML: %s' % str(previous_partial))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1049
        signature.setPartialXML(previous_partial)
1050 1051
        #LOG('applyActionList', DEBUG, 'previous_partial: %s' % str(previous_partial))
        #LOG('applyActionList', DEBUG, 'waiting more data for :%s' % signature.getId())
Nicolas Delaby's avatar
Nicolas Delaby committed
1052 1053 1054 1055 1056
        xml_confirmation_list.append(self.SyncMLConfirmation(
                                cmd_id=cmd_id,
                                cmd=action.tag,
                                sync_code=self.WAITING_DATA,
                                remote_xml=action))
1057
      if conflict_list and signature is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1058 1059
        # We had a conflict
        signature.setStatus(self.CONFLICT)
1060

Nicolas Delaby's avatar
Nicolas Delaby committed
1061
    return (xml_confirmation_list, has_next_action, cmd_id)
1062

1063
  def applyStatusList(self, subscriber=None, remote_xml=None):
1064 1065 1066 1067
    """
    This read a list of status list (ie syncml confirmations).
    This method have to change status codes on signatures
    """
1068
    status_list = self.getSyncBodyStatusList(remote_xml)
1069 1070
    has_status_list = 0
    destination_waiting_more_data = 0
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101
    for status in status_list:
      if not status['code']:
        continue
      status_cmd = status['cmd']
      object_gid = status['source']
      if not object_gid:
        object_gid = status['target']
      status_code = int(status['code'])
      if status_cmd in ('Add', 'Replace',):
        has_status_list = 1
        signature = subscriber.getSignatureFromGid(object_gid)
        if signature is None:
          signature = subscriber.getSignatureFromRid(object_gid)
        if status_code == self.CHUNK_OK:
          destination_waiting_more_data = 1
          signature.setStatus(self.PARTIAL)
        elif status_code == self.CONFLICT:
          signature.setStatus(self.CONFLICT)
        elif status_code == self.CONFLICT_MERGE:
          # We will have to apply the update, and we should not care 
          # about conflicts, so we have to force the update
          signature.setStatus(self.NOT_SYNCHRONIZED)
          signature.setForce(1)
        elif status_code == self.CONFLICT_CLIENT_WIN:
          # The server was agree to apply our updates, nothing to do
          signature.setStatus(self.SYNCHRONIZED)
        elif status_code in (self.SUCCESS, self.ITEM_ADDED):
          signature.setStatus(self.SYNCHRONIZED)
      elif status_cmd == 'Delete':
        has_status_list = 1
        if status_code == self.SUCCESS:
1102
          signature = subscriber.getSignatureFromGid(object_gid)
1103 1104
          if signature is None and \
          not(subscriber.getSynchronizeWithERP5Sites()):
1105
            signature = subscriber.getSignatureFromRid(object_gid)
1106 1107
          if signature is not None:
            subscriber.delSignature(signature.getGid())
1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
    return (destination_waiting_more_data, has_status_list)


class XMLSyncUtils(XMLSyncUtilsMixin):

  def Sync(self, id, msg=None, RESPONSE=None):
    """
    This is the main method for synchronization
    """
    pass

  def SyncInit(self, domain):
    """
    Initialization of a synchronization, this is
    used for the first message of every synchronization
    """
    pass

1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
  def getConduitByName(self, conduit_name):
    """
    Get Conduit Object by given name.
    The Conduit can be located in Any Products according to naming Convention
    Products.<Product Name>.Conduit.<Conduit Module> ,if conduit_name equal module's name.
    By default Conduit must be defined in Products.ERP5SyncML.Conduit.<Conduit Module>
    """
    from Products.ERP5SyncML import Conduit
    if conduit_name.startswith('Products'):
      path = conduit_name
      conduit_name = conduit_name.split('.')[-1]
      conduit_module = __import__(path, globals(), locals(), [''])
      conduit = getattr(conduit_module, conduit_name)()
    else:
      conduit_module = __import__('.'.join([Conduit.__name__, conduit_name]),
                                  globals(), locals(), [''])
      conduit = getattr(conduit_module, conduit_name)()
    return conduit

1145 1146 1147 1148
  def SyncModif(self, domain, remote_xml):
    """
    Modification Message, this is used after the first
    message in order to send modifications.
1149 1150
    Send the server modification, this happens after the Synchronization
    initialization
1151
    """
1152
    has_response = 0 #check if syncmodif replies to this messages
1153
    cmd_id = 1 # specifies a SyncML message-unique command identifier
1154
    #LOG('SyncModif', DEBUG, 'Starting... domain: %s' % domain.getId())
1155
    # Get informations from the header
1156 1157
    xml_header = remote_xml[0]
    if xml_header.tag != "SyncHdr":
1158
      LOG('SyncModif', INFO, 'This is not a SyncML Header')
Sebastien Robin's avatar
Sebastien Robin committed
1159
      raise ValueError, "Sorry, This is not a SyncML Header"
1160 1161

    subscriber = domain # If we are the client, this is fine
1162
    simulate = 0 # used by applyActionList, should be 0 for client
1163
    if domain.domain_type == self.PUB:
1164
      simulate = 1
1165
      subscription_url = self.getSubscriptionUrlFromXML(xml_header)
1166
      subscriber = domain.getSubscriber(subscription_url)
1167

1168 1169
    # We have to check if this message was not already, this can be dangerous
    # to update two times the same object
1170
    message_id = self.getMessageIdFromXml(remote_xml)
Sebastien Robin's avatar
Sebastien Robin committed
1171 1172
    correct_message = subscriber.checkCorrectRemoteMessageId(message_id)
    if not correct_message: # We need to send again the message
1173
      LOG('SyncModif, no correct message:', INFO, "sending again...")
1174
      last_xml = subscriber.getLastSentMessage()
1175
      LOG("SyncModif last_xml :", INFO, last_xml)
1176 1177 1178
      remote_xml = etree.tostring(remote_xml, encoding='utf-8',
                                  xml_declaration=True,
                                  pretty_print=True)
Nicolas Delaby's avatar
Nicolas Delaby committed
1179
      LOG("SyncModif remote_xml :", INFO, remote_xml)
1180
      if last_xml:
1181 1182
        has_response = 1
        if domain.domain_type == self.PUB: # We always reply
1183 1184 1185 1186
          self.sendResponse(
                    from_url=domain.publication_url,
                    to_url=subscriber.subscription_url,
                    sync_id=domain.getTitle(),
1187
                    xml=last_xml, domain=domain,
1188
                    content_type=domain.getSyncContentType())
1189
        elif domain.domain_type == self.SUB:
1190 1191 1192 1193 1194 1195 1196 1197
          self.sendResponse(
                    from_url=domain.subscription_url,
                    to_url=domain.publication_url,
                    sync_id=domain.getTitle(),
                    xml=last_xml,
                    domain=domain,
                    content_type=domain.getSyncContentType())
      return {'has_response':has_response, 'xml':last_xml}
1198 1199
    subscriber.setLastSentMessage('')

1200
    # First apply the list of status codes
Nicolas Delaby's avatar
Nicolas Delaby committed
1201
    (destination_waiting_more_data, has_status_list) = self.applyStatusList(
1202 1203
                                                      subscriber=subscriber,
                                                      remote_xml=remote_xml)
1204

1205
    alert_code = self.getAlertCodeFromXML(remote_xml)
Kevin Deldycke's avatar
Kevin Deldycke committed
1206
    # Import the conduit and get it
1207
    conduit = self.getConduitByName(subscriber.getConduit())
1208
    # Then apply the list of actions
Nicolas Delaby's avatar
Nicolas Delaby committed
1209
    (xml_confirmation_list, has_next_action, cmd_id) = self.applyActionList(
1210 1211 1212 1213 1214
                                          cmd_id=cmd_id,
                                          domain=domain,
                                          subscriber=subscriber,
                                          remote_xml=remote_xml,
                                          conduit=conduit, simulate=simulate)
Nicolas Delaby's avatar
Nicolas Delaby committed
1215
    xml = Element('SyncML')
1216

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1217 1218
    # syncml header
    if domain.domain_type == self.PUB:
Nicolas Delaby's avatar
Nicolas Delaby committed
1219
      xml.append(self.SyncMLHeader(
1220 1221 1222 1223
                  subscriber.getSessionId(),
                  subscriber.incrementMessageId(),
                  subscriber.getSubscriptionUrl(),
                  domain.getPublicationUrl()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1224
    elif domain.domain_type == self.SUB:
Nicolas Delaby's avatar
Nicolas Delaby committed
1225
      xml.append(self.SyncMLHeader(
1226 1227 1228
                  domain.getSessionId(), domain.incrementMessageId(),
                  domain.getPublicationUrl(),
                  domain.getSubscriptionUrl()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1229 1230

    # syncml body
Nicolas Delaby's avatar
Nicolas Delaby committed
1231
    sync_body = SubElement(xml, 'SyncBody')
1232

1233 1234 1235 1236 1237
    xml_status, cmd_id = self.SyncMLStatus(
                                    remote_xml,
                                    self.SUCCESS,
                                    cmd_id,
                                    subscriber.getNextAnchor(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1238 1239
                                    subscription=subscriber)
    sync_body.extend(xml_status)
1240

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1241 1242
    destination_url = ''
    # alert message if we want more data
1243
    if destination_waiting_more_data:
Nicolas Delaby's avatar
Nicolas Delaby committed
1244 1245 1246 1247 1248 1249 1250
      sync_body.append(self.SyncMLAlert(
                        cmd_id,
                        self.WAITING_DATA,
                        subscriber.getTargetURI(),
                        subscriber.getSourceURI(),
                        subscriber.getLastAnchor(),
                        subscriber.getNextAnchor()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1251
    # Now we should send confirmations
1252
    cmd_id_before_getsyncmldata = cmd_id
1253
    cmd_id = cmd_id+1
1254
    if domain.getActivityEnabled():
1255
      #use activities to get SyncML data.
Nicolas Delaby's avatar
Nicolas Delaby committed
1256
      remote_xml = etree.tostring(remote_xml, encoding='utf-8',
1257
                                    xml_declaration=True, pretty_print=False)
Nicolas Delaby's avatar
Nicolas Delaby committed
1258 1259
      xml_tree = etree.tostring(xml, encoding='utf-8', xml_declaration=True,
                                pretty_print=False)
1260 1261 1262 1263
      xml_confirmation_list = [etree.tostring(xml, encoding='utf-8',\
                                              xml_declaration=True,\
                                              pretty_print=False) for xml in \
                                              xml_confirmation_list]
1264
      domain.activate(activity='SQLQueue',
Nicolas Delaby's avatar
Nicolas Delaby committed
1265 1266
                      tag=domain.getId(),
                      priority=self.PRIORITY).activateSyncModif(
1267
                      domain_relative_url=domain.getRelativeUrl(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1268
                      remote_xml=remote_xml,
Nicolas Delaby's avatar
Nicolas Delaby committed
1269
                      xml_tree=xml_tree,
Nicolas Delaby's avatar
Nicolas Delaby committed
1270 1271
                      subscriber_relative_url=subscriber.getRelativeUrl(),
                      cmd_id=cmd_id,
Nicolas Delaby's avatar
Nicolas Delaby committed
1272 1273
                      xml_confirmation_list=xml_confirmation_list,
                      syncml_data_list=[],
Nicolas Delaby's avatar
Nicolas Delaby committed
1274 1275 1276
                      cmd_id_before_getsyncmldata=cmd_id_before_getsyncmldata,
                      has_status_list=has_status_list,
                      has_response=has_response )
Nicolas Delaby's avatar
Nicolas Delaby committed
1277
      return {'has_response':1, 'xml':''}
1278 1279
    else:
      result = self.getSyncMLData(domain=domain,
1280 1281
                             remote_xml=remote_xml,
                             subscriber=subscriber,
1282
                             cmd_id=cmd_id,
Nicolas Delaby's avatar
Nicolas Delaby committed
1283
                             xml_confirmation_list=xml_confirmation_list,
1284
                             conduit=conduit,
1285
                             max=None)
Nicolas Delaby's avatar
Nicolas Delaby committed
1286 1287
      syncml_data_list = result['syncml_data_list']
      xml_confirmation_list = result['xml_confirmation_list']
1288
      cmd_id = result['cmd_id']
Nicolas Delaby's avatar
Nicolas Delaby committed
1289 1290 1291
      return self.sendSyncModif(syncml_data_list, cmd_id_before_getsyncmldata,
                                subscriber, domain, xml_confirmation_list,
                                remote_xml, xml, has_status_list,
1292
                                has_response)
1293

1294 1295 1296 1297 1298 1299 1300
  def deleteRemainObjectList(self, domain, subscriber):
    """
    This method allow deletion on not synchronised Objects at the end of Synchronisation session.
    Usefull only after reseting in One Way Sync
    """
    object_list = domain.getObjectList()
    gid_list = [domain.getGidFromObject(x) for x in object_list]
1301 1302 1303 1304 1305 1306
    domain_path = domain.getPath()
    subscriber_path = subscriber.getPath()
    while len(gid_list):
      sliced_gid_list = [gid_list.pop() for i in gid_list[:self.MAX_OBJECTS]]
      #Split List Processing in activities
      self.activate(activity='SQLQueue',
Nicolas Delaby's avatar
Nicolas Delaby committed
1307 1308
                    tag=domain.getId(),
                    priority=self.PRIORITY).activateDeleteRemainObjectList(domain_path,
1309 1310 1311 1312 1313 1314 1315 1316 1317
                                                                       subscriber_path,
                                                                       sliced_gid_list)

  def activateDeleteRemainObjectList(self, domain_path, subscriber_path, gid_list):
    """
    Execute Deletion in Activities
    """
    domain = self.unrestrictedTraverse(domain_path)
    subscriber = self.unrestrictedTraverse(subscriber_path)
1318 1319 1320 1321
    destination = self.unrestrictedTraverse(domain.getDestinationPath())
    conduit_name = subscriber.getConduit()
    conduit = self.getConduitByName(conduit_name)
    for gid in gid_list:
1322
      if subscriber.getSignatureFromGid(gid) is None:
1323 1324 1325
        object_id = b16decode(gid)
        conduit.deleteObject(object=destination, object_id=object_id)

1326
  def activateSyncModif(self, **kw):
1327 1328 1329
    domain = self.unrestrictedTraverse(kw['domain_relative_url'])
    subscriber = self.unrestrictedTraverse(kw['subscriber_relative_url'])
    conduit = subscriber.getConduit()
1330 1331
    result = self.getSyncMLData(domain=domain, subscriber=subscriber,
                                conduit=conduit, max=self.MAX_OBJECTS, **kw)
Nicolas Delaby's avatar
Nicolas Delaby committed
1332
    syncml_data_list = result['syncml_data_list']
1333
    cmd_id = result['cmd_id']
Nicolas Delaby's avatar
Nicolas Delaby committed
1334
    kw['syncml_data_list'] = syncml_data_list
1335
    kw['cmd_id'] = cmd_id
1336 1337
    finished = result['finished']
    if not finished:
1338
      domain.activate(activity='SQLQueue',
Nicolas Delaby's avatar
Nicolas Delaby committed
1339 1340
                      tag=domain.getId(),
                      priority=self.PRIORITY).activateSyncModif(**kw)
1341 1342 1343
    else:
      cmd_id = result['cmd_id']
      cmd_id_before_getsyncmldata = kw['cmd_id_before_getsyncmldata']
1344
      remote_xml = etree.XML(kw['remote_xml'], parser=parser)
Nicolas Delaby's avatar
Nicolas Delaby committed
1345 1346
      xml_tree = etree.XML(kw['xml_tree'], parser=parser)
      xml_confirmation_list = kw['xml_confirmation_list']
1347 1348
      has_status_list = kw['has_status_list']
      has_response = kw['has_response']
1349
      return self.sendSyncModif(
Nicolas Delaby's avatar
Nicolas Delaby committed
1350
                        syncml_data_list,
1351 1352 1353
                        cmd_id_before_getsyncmldata,
                        subscriber,
                        domain,
Nicolas Delaby's avatar
Nicolas Delaby committed
1354
                        xml_confirmation_list,
1355
                        remote_xml,
Nicolas Delaby's avatar
Nicolas Delaby committed
1356
                        xml_tree,
1357 1358
                        has_status_list,
                        has_response)
1359

Nicolas Delaby's avatar
Nicolas Delaby committed
1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375
  def sendSyncModif(self, syncml_data_list, cmd_id_before_getsyncmldata,
                    subscriber, domain, xml_confirmation_list, remote_xml,
                    xml_tree, has_status_list, has_response):
    sync_body = xml_tree.find('SyncBody')
    if syncml_data_list:
      sync_node = SubElement(sync_body, 'Sync')
      cmd_id_node = SubElement(sync_node, 'CmdID')
      cmd_id_node.text = '%s' % cmd_id_before_getsyncmldata
      target_uri = subscriber.getTargetURI()
      if target_uri:
        sync_node.append(E.Target(E.LocURI(target_uri)))
      source_uri = subscriber.getSourceURI()
      if source_uri:
        sync_node.append(E.Source(E.LocURI(source_uri)))
      for syncml_data in syncml_data_list:
        sync_node.append(etree.XML(syncml_data, parser=parser))
1376 1377 1378 1379 1380
    for xml_confirmation in xml_confirmation_list:
      if not isinstance(xml_confirmation, str):
        sync_body.append(xml_confirmation)
      else:
        sync_body.append(etree.XML(xml_confirmation, parser=parser))
Nicolas Delaby's avatar
Nicolas Delaby committed
1381 1382
    sync_body.append(Element('Final'))
    xml_string = etree.tostring(xml_tree, encoding='utf-8', pretty_print=True)
1383

1384
    if domain.domain_type == self.PUB: # We always reply
Nicolas Delaby's avatar
Nicolas Delaby committed
1385
      subscriber.setLastSentMessage(xml_string)
1386 1387 1388 1389
      self.sendResponse(
                from_url=domain.publication_url,
                to_url=subscriber.subscription_url,
                sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1390
                xml=xml_string,
1391 1392
                domain=domain,
                content_type=domain.getSyncContentType())
Nicolas Delaby's avatar
Nicolas Delaby committed
1393 1394
      if not syncml_data_list:
        LOG('this is the end of the synchronisation session from PUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1395 1396
        subscriber.setAuthenticated(False)
        domain.setAuthenticated(False)
1397 1398
      has_response = 1
    elif domain.domain_type == self.SUB:
Nicolas Delaby's avatar
Nicolas Delaby committed
1399 1400
      if self.checkAlert(remote_xml) or xml_confirmation_list or syncml_data_list:
        subscriber.setLastSentMessage(xml_string)
1401 1402 1403 1404
        self.sendResponse(
                  from_url=domain.subscription_url,
                  to_url=domain.publication_url,
                  sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1405
                  xml=xml_string, domain=domain,
1406
                  content_type=domain.getSyncContentType())
1407
        has_response = 1
Fabien Morin's avatar
Fabien Morin committed
1408
      else:
1409 1410
        if domain.isOneWayFromServer():
          self.deleteRemainObjectList(domain, subscriber)
Nicolas Delaby's avatar
Nicolas Delaby committed
1411
        LOG('this is the end of the synchronisation session from SUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1412
        domain.setAuthenticated(False)
Nicolas Delaby's avatar
Nicolas Delaby committed
1413
    return {'has_response':has_response, 'xml':xml_string}
1414 1415 1416 1417 1418

  def xml2wbxml(self, xml):
    """
    convert xml string to wbxml using a temporary file
    """
1419
    #LOG('xml2wbxml starting ...', DEBUG, '')
1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433
    import os
    f = open('/tmp/xml2wbxml', 'w')
    f.write(xml)
    f.close()
    os.system('/usr/bin/xml2wbxml -o /tmp/xml2wbxml /tmp/xml2wbxml')
    f = open('/tmp/xml2wbxml', 'r')
    wbxml = f.read()
    f.close()
    return wbxml

  def wbxml2xml(self, wbxml):
    """
    convert wbxml string to xml using a temporary file
    """
1434
    #LOG('wbxml2xml starting ...', DEBUG, '')
1435 1436 1437 1438 1439 1440 1441 1442 1443
    import os
    f = open('/tmp/wbxml2xml', 'w')
    f.write(wbxml)
    f.close()
    os.system('/usr/bin/wbxml2xml -o /tmp/wbxml2xml /tmp/wbxml2xml')
    f = open('/tmp/wbxml2xml', 'r')
    xml = f.read()
    f.close()
    return xml
1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459

  def PubSync(self, publication_path, msg=None, RESPONSE=None, subscriber=None):
    """
      This is the synchronization method for the server
    """
    from Products.ERP5SyncML.Publication import Subscriber
    #LOG('PubSync', DEBUG, 'Starting... publication: %s' % (publication_path))
    # Read the request from the client
    publication = self.unrestrictedTraverse(publication_path)
    xml_client = msg
    if xml_client is None:
      xml_client = self.readResponse(from_url=publication.getPublicationUrl())
    #LOG('PubSync', DEBUG, 'Starting... msg: %s' % str(xml_client))
    result = None

    if xml_client is not None:
1460
      if isinstance(xml_client, (str, unicode)):
1461
        xml_client = etree.XML(xml_client, parser=parser)
1462
      if xml_client.tag != "SyncML":
1463 1464 1465 1466
        LOG('PubSync', INFO, 'This is not a SyncML Message')
        raise ValueError, "Sorry, This is not a SyncML Message"
      alert_code = self.getAlertCodeFromXML(xml_client)
      # Get informations from the header
1467 1468
      client_header = xml_client[0]
      if client_header.tag != "SyncHdr":
1469 1470 1471 1472 1473
        LOG('PubSync', INFO, 'This is not a SyncML Header')
        raise ValueError, "Sorry, This is not a SyncML Header"
      subscription_url = self.getSubscriptionUrlFromXML(client_header)
      # Get the subscriber or create it if not already in the list
      subscriber = publication.getSubscriber(subscription_url)
1474
      if subscriber is None:
Nicolas Delaby's avatar
Nicolas Delaby committed
1475
        subscriber = Subscriber(publication.generateNewId(), subscription_url)
1476 1477 1478
        subscriber.setXMLMapping(publication.getXMLMapping())
        subscriber.setConduit(publication.getConduit())
        publication.addSubscriber(subscriber)
1479
        subscriber = subscriber.__of__(publication)
1480
        # first synchronization
Nicolas Delaby's avatar
Nicolas Delaby committed
1481 1482 1483 1484
        result = self.PubSyncInit(publication=publication,
                                  xml_client=xml_client,
                                  subscriber=subscriber,
                                  sync_type=self.SLOW_SYNC)
1485 1486 1487 1488 1489
      elif self.checkAlert(xml_client) and \
          alert_code in (self.TWO_WAY, self.SLOW_SYNC, \
          self.ONE_WAY_FROM_SERVER):
        subscriber.setXMLMapping(publication.getXMLMapping())
        subscriber.setConduit(publication.getConduit())
Nicolas Delaby's avatar
Nicolas Delaby committed
1490 1491 1492 1493
        result = self.PubSyncInit(publication=publication,
                                  xml_client=xml_client,
                                  subscriber=subscriber,
                                  sync_type=alert_code)
1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507
      else:
        #we log the user authenticated to do the synchronization with him
        if self.checkMap(xml_client) :
          self.setRidWithMap(xml_client, subscriber)
        if subscriber.isAuthenticated():
            uf = self.getPortalObject().acl_users
            user = uf.getUserById(subscriber.getUser()).__of__(uf)
            newSecurityManager(None, user)
            result = self.PubSyncModif(publication, xml_client)
        else:
          result = self.PubSyncModif(publication, xml_client)
    elif subscriber is not None:
      # This looks like we are starting a synchronization after
      # a conflict resolution by the user
Nicolas Delaby's avatar
Nicolas Delaby committed
1508 1509 1510 1511
      result = self.PubSyncInit(publication=publication,
                                xml_client=None,
                                subscriber=subscriber,
                                sync_type=self.TWO_WAY)
1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522
    if RESPONSE is not None:
      RESPONSE.redirect('managePublications')
    elif result is not None:
      return result

  def SubSync(self, subscription_path, msg=None, RESPONSE=None):
    """
      This is the synchronization method for the client
    """
    response = None #check if subsync replies to this messages
    subscription = self.unrestrictedTraverse(subscription_path)
1523
    if msg is None and (subscription.getSubscriptionUrl()).find('file') >= 0:
1524
      msg = self.readResponse(sync_id=subscription.getSubscriptionUrl(),
1525
                              from_url=subscription.getSubscriptionUrl())
1526
    if msg is None:
1527 1528 1529
      response = self.SubSyncInit(subscription)
    else:
      xml_client = msg
1530
      if isinstance(xml_client, (str, unicode)):
1531
        xml_client = etree.XML(xml_client, parser=parser)
1532
        status_list = self.getSyncBodyStatusList(xml_client)
1533
        if status_list:
1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557
          status_code_syncHdr = status_list[0]['code']
          if status_code_syncHdr.isdigit():
            status_code_syncHdr = int(status_code_syncHdr)
          #LOG('SubSync status code : ', DEBUG, status_code_syncHdr)
          if status_code_syncHdr == self.AUTH_REQUIRED:
            if self.checkChal(xml_client):
              authentication_format, authentication_type = self.getChal(xml_client)
              #LOG('SubSync auth_required :', DEBUG, 'format:%s, type:%s' % (authentication_format, authentication_type))
              if authentication_format is not None and \
                  authentication_type is not None:
                subscription.setAuthenticationFormat(authentication_format)
                subscription.setAuthenticationType(authentication_type)
            else:
              raise ValueError, "Sorry, the server chalenge for an \
                  authentication, but the authentication format is not find"

            LOG('SubSync', INFO, 'Authentication required')
            response = self.SubSyncCred(subscription, xml_client)
          elif status_code_syncHdr == self.UNAUTHORIZED:
            LOG('SubSync', INFO, 'Bad authentication')
            return {'has_response':0, 'xml':xml_client}
          else:
            response = self.SubSyncModif(subscription, xml_client)
        else:
1558
          response = self.SubSyncModif(subscription, xml_client)
1559 1560 1561 1562 1563 1564 1565 1566

    if RESPONSE is not None:
      RESPONSE.redirect('manageSubscriptions')
    else:
      return response

  def getActivityType(self, domain):
    if domain.getActivityEnabled():
1567
      return 'SQLQueue'
1568
    return 'RAMQueue'