XMLSyncUtils.py 70.5 KB
Newer Older
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 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
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
32
from Products.ERP5SyncML.Signature import Signature
33
from AccessControl.SecurityManagement import newSecurityManager
Nicolas Delaby's avatar
Nicolas Delaby committed
34
from ERP5Diff import ERP5Diff
Nicolas Delaby's avatar
Nicolas Delaby committed
35
from zLOG import LOG, INFO
Nicolas Delaby's avatar
Nicolas Delaby committed
36

37
from lxml import etree
38
from lxml.etree import Element
39 40 41 42
from lxml.builder import ElementMaker
from SyncCode import SYNCML_NAMESPACE
nsmap = {'syncml' : SYNCML_NAMESPACE}
E = ElementMaker(namespace=SYNCML_NAMESPACE, nsmap=nsmap)
43 44
parser = etree.XMLParser(remove_blank_text=True)

45
from xml.dom import minidom
Jean-Paul Smets's avatar
Jean-Paul Smets committed
46

47
from base64 import b16encode, b16decode
48

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

51 52
  def SyncMLHeader(self, session_id, msg_id, target, source, target_name=None,
      source_name=None, dataCred=None, authentication_format='b64',
53
      authentication_type='syncml:auth-basic'):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
54 55 56 57
    """
      Since the Header is always almost the same, this is the
      way to set one quickly.
    """
58
    xml = (E.SyncHdr(
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
59 60
            E.VerDTD('1.2'),
            E.VerProto('SyncML/1.2'),
61 62 63
            E.SessionID('%s' % session_id),
            E.MsgID('%s' % msg_id),
          ))
64 65 66 67 68 69 70 71
    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)
72 73 74 75 76 77
    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
78
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
79

Sebastien Robin's avatar
Sebastien Robin committed
80 81
  def SyncMLAlert(self, cmd_id, sync_code, target, source, last_anchor, 
      next_anchor):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
82 83 84 85
    """
      Since the Alert section is always almost the same, this is the
      way to set one quickly.
    """
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
    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
104
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
105

Nicolas Delaby's avatar
Nicolas Delaby committed
106 107
  def SyncMLStatus(self, remote_xml, data_code, cmd_id, next_anchor,
                   subscription=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
108
    """
109 110
    return a status bloc with all status corresponding to the syncml
    commands in remote_xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
111
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
112
    namespace = self.getNamespace(remote_xml.nsmap)
113
    #list of element in the SyncBody bloc
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
114
    sub_syncbody_element_list = remote_xml.xpath('/syncml:SyncML/syncml:SyncBody/*')
115
    message_id = self.getMessageIdFromXml(remote_xml)
Nicolas Delaby's avatar
Nicolas Delaby committed
116
    status_list = []
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
117 118 119 120
    target_uri = '%s' %\
    remote_xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Target/syncml:LocURI)')
    source_uri = '%s' %\
    remote_xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Source/syncml:LocURI)')
Nicolas Delaby's avatar
Nicolas Delaby committed
121 122 123 124 125 126 127 128 129 130
    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),
               ))
131
      cmd_id += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
132
      status_list.append(xml)
133
    for sub_syncbody_element in sub_syncbody_element_list:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
134
      if sub_syncbody_element.xpath('local-name()') not in ('Status', 'Final', 'Get'):
Nicolas Delaby's avatar
Nicolas Delaby committed
135 136 137
        xml = (E.Status(
                 E.CmdID('%s' % cmd_id),
                 E.MsgRef('%s' % message_id),
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
138 139 140
                 E.CmdRef('%s' %\
                 sub_syncbody_element.xpath('string(.//syncml:CmdID)')),
                 E.Cmd('%s' % sub_syncbody_element.xpath('name()'))
Nicolas Delaby's avatar
Nicolas Delaby committed
141
                 ))
142
        cmd_id += 1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
143
        target_ref = sub_syncbody_element.xpath('string(.//syncml:Target/syncml:LocURI)')
144
        if target_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
145
          xml.append(E.TargetRef('%s' % target_ref))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
146
        source_ref = sub_syncbody_element.xpath('string(.//syncml:Source/syncml:LocURI)')
147
        if source_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
148
          xml.append(E.SourceRef('%s' % source_ref))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
149
        if sub_syncbody_element.xpath('local-name()') == 'Add':
Nicolas Delaby's avatar
Nicolas Delaby committed
150
          xml.append(E.Data('%s' % self.ITEM_ADDED))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
151 152
        elif sub_syncbody_element.xpath('local-name()') == 'Alert' and \
            sub_syncbody_element.xpath('string(.//syncml:Data)') == \
153
            str(self.SLOW_SYNC):
Nicolas Delaby's avatar
Nicolas Delaby committed
154
          xml.append(E.Data('%s' % self.REFRESH_REQUIRED))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
155
        elif sub_syncbody_element.xpath('local-name()') == 'Alert':
Nicolas Delaby's avatar
Nicolas Delaby committed
156
          xml.append(E.Item(E.Data(E.Anchor(E.Next(next_anchor)))))
157
        else:
Nicolas Delaby's avatar
Nicolas Delaby committed
158 159
          xml.append(E.Data('%s' % self.SUCCESS))
        status_list.append(xml)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
160 161 162 163
      #FIXME to do a test for Get
      if sub_syncbody_element.xpath('local-name()') == 'Get'\
          and subscription is not None:
        cmd_ref = '%s' % sub_syncbody_element.xpath('string(.//syncml:CmdID)')
164 165 166 167 168
        syncml_result = self.SyncMLPut(
                                  cmd_id,
                                  subscription,
                                  markup='Results',
                                  cmd_ref=cmd_ref,
169
                                  message_id=message_id)
Nicolas Delaby's avatar
Nicolas Delaby committed
170 171
        if syncml_result is not None:
          status_list.append(syncml_result)
172
        cmd_id += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
173 174

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

176 177
  def SyncMLConfirmation(self, cmd_id=None, target_ref=None, cmd=None,
      sync_code=None, msg_ref=None, cmd_ref=None, source_ref=None,
178
      remote_xml=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
179
    """
Sebastien Robin's avatar
Sebastien Robin committed
180
    This is used in order to confirm that an object was correctly
Jean-Paul Smets's avatar
Jean-Paul Smets committed
181 182
    synchronized
    """
Nicolas Delaby's avatar
Nicolas Delaby committed
183
    if remote_xml is not None:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
184 185 186 187 188 189
      namespace = self.getNamespace(remote_xml.nsmap)
      msg_ref = '%s' %\
      remote_xml.xpath("string(/syncml:SyncML/syncml:SyncHdr/syncml:MsgID)")
      cmd_ref = '%s' % remote_xml.xpath("string(.//syncml:CmdID)")
      target_ref = '%s' % remote_xml.xpath("string(.//syncml:Target/syncml:LocURI)")
      source_ref = '%s' % remote_xml.xpath("string(.//syncml:Source/syncml:LocURI)")
190
    xml = E.Status()
191 192 193 194 195 196 197 198 199 200 201 202 203 204
    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
205
    return xml
206 207

  def SyncMLChal(self, cmd_id, cmd, target_ref, source_ref, auth_format,
Nicolas Delaby's avatar
Nicolas Delaby committed
208
      auth_type, auth_code):
Sebastien Robin's avatar
Sebastien Robin committed
209 210 211
    """
    This is used in order to ask crendentials
    """
212 213 214 215 216 217 218 219 220 221 222 223 224
    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
225
            E.Data('%s' % auth_code)
226
            ))
Nicolas Delaby's avatar
Nicolas Delaby committed
227
    return xml
Jean-Paul Smets's avatar
Jean-Paul Smets committed
228

229
  def SyncMLPut(self, cmd_id, subscription, markup='Put', cmd_ref=None,
230
      message_id=None):
231 232
    """
    this is used to inform the server of the CTType version supported
233 234
    but if the server use it to respond to a Get request, it's a <Result> markup
    instead of <Put>
235 236
    """
    conduit_name = subscription.getConduit()
237
    conduit = self.getConduitByName(conduit_name)
Nicolas Delaby's avatar
Nicolas Delaby committed
238
    xml = None
239
    #if the conduit support the SyncMLPut :
240 241 242
    if getattr(conduit, 'getCapabilitiesCTTypeList', None) is not None and \
       getattr(conduit, 'getCapabilitiesVerCTList', None) is not None and \
       getattr(conduit, 'getPreferedCapabilitieVerCT', None) is not None:
243
      xml = Element('{%s}%s' % (SYNCML_NAMESPACE, markup))
Nicolas Delaby's avatar
Nicolas Delaby committed
244
      xml.append(E.CmdID('%s' % cmd_id))
245
      if message_id:
Nicolas Delaby's avatar
Nicolas Delaby committed
246
        xml.append(E.MsgRef('%s' % message_id))
247
      if cmd_ref:
Nicolas Delaby's avatar
Nicolas Delaby committed
248
        xml.append(E.CmdRef('%s' % cmd_ref))
249
      xml.extend((E.Meta(E.Type('application/vnd.syncml-devinf+xml')),
Nicolas Delaby's avatar
Nicolas Delaby committed
250 251 252 253 254 255 256 257 258 259
                 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(),
260
                   E.DataStore(E.SourceRef(subscription.getSourceURI()))
Nicolas Delaby's avatar
Nicolas Delaby committed
261 262
                   )
                 )
263
               )))
264
      data_store = xml.find('{%(ns)s}Item/{%(ns)s}Data/{%(ns)s}DevInf/{%(ns)s}DataStore' % {'ns': SYNCML_NAMESPACE})
Nicolas Delaby's avatar
Nicolas Delaby committed
265 266
      tx_element_list = []
      rx_element_list = []
267 268
      for type in conduit.getCapabilitiesCTTypeList():
        if type != self.MEDIA_TYPE['TEXT_XML']:
269 270 271
          for x_version in conduit.getCapabilitiesVerCTList(type):
            rx_element_list.append(E.Rx(E.CTType(type), E.VerCT(x_version)))
            tx_element_list.append(E.Tx(E.CTType(type), E.VerCT(x_version)))
272
      rx_pref = Element('{%s}Rx-Pref' % SYNCML_NAMESPACE)
273 274 275
      rx_pref.extend((E.CTType(conduit.getPreferedCapabilitieCTType()),
                      E.VerCT(conduit.getPreferedCapabilitieVerCT())))
      data_store.append(rx_pref)
Nicolas Delaby's avatar
Nicolas Delaby committed
276
      data_store.extend(rx_element_list)
277
      tx_pref = Element('{%s}Tx-Pref' % SYNCML_NAMESPACE)
278 279 280
      tx_pref.extend((E.CTType(conduit.getPreferedCapabilitieCTType()),
                      E.VerCT(conduit.getPreferedCapabilitieVerCT())))
      data_store.append(tx_pref)
Nicolas Delaby's avatar
Nicolas Delaby committed
281 282 283 284 285 286 287 288
      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
289

Jean-Paul Smets's avatar
Jean-Paul Smets committed
290 291 292 293 294 295 296 297 298
  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
299
    #LOG('SubSendMail', DEBUG,'from: %s, to: %s' % (fromaddr,toaddr))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
300 301 302
    server = smtplib.SMTP('localhost')
    server.sendmail(fromaddr, toaddr, msg)
    # if we want to send the email to someone else (debugging)
303
    #server.sendmail(fromaddr, "seb@localhost", msg)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
304 305
    server.quit()

306
  def getNamespace(self, nsmap):
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
307
    """
308 309 310
      Set the namespace prefix, check if argument is conform
      and return the full namespace updated for syncml
      nsmap -- The namespace of the received xml
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
311 312 313
    """
    #search urn compatible in the namespaces of nsmap
    urns = filter(lambda v: v.upper() in self.URN_LIST, nsmap.values())
314
    if urns:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
315
      namespace = etree.FunctionNamespace(urns[0])
316
      namespace.prefix = 'syncml'
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
317
      return namespace
318 319
    else:
      raise ValueError, "Sorry, the given namespace is not supported"
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
320

321
  def addXMLObject(self, cmd_id=0, object=None, xml_string=None,
322
                  more_data=0, gid=None, media_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
323 324 325
    """
      Add an object with the SyncML protocol
    """
326
    data_node = E.Data()
327
    if media_type == self.MEDIA_TYPE['TEXT_XML'] and isinstance(xml_string, str):
328
      data_node.append(etree.XML(xml_string, parser=parser))
329 330 331 332
    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)
333
    else:
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
      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:
349 350
      item_node = xml.find('{%s}Item' % SYNCML_NAMESPACE)
      item_node.append(E.MoreData())
Nicolas Delaby's avatar
Nicolas Delaby committed
351
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
352

353
  def deleteXMLObject(self, cmd_id=0, object_gid=None, rid=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
354
    """
Sebastien Robin's avatar
Sebastien Robin committed
355 356
      Delete an object with the SyncML protocol
    """
357 358
    if rid:
      elem_to_append = E.Target(E.LocURI('%s' % rid))
359
    else:
360 361 362 363 364 365 366
      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
367
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
368

369
  def replaceXMLObject(self, cmd_id=0, object=None, xml_string=None,
370
                       more_data=0, gid=None, rid=None, media_type=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
371
    """
Sebastien Robin's avatar
Sebastien Robin committed
372 373
      Replace an object with the SyncML protocol
    """
374 375
    if rid:
      elem_to_append = E.Target(E.LocURI('%s' % rid))
376
    else:
377
      elem_to_append = E.Source(E.LocURI('%s' % gid))
378
    data_node = E.Data()
379 380 381
    if not isinstance(xml_string, (str, unicode)):
      data_node.append(xml_string)
    else:
382
      data_node.append(etree.XML(xml_string, parser=parser))
383 384 385 386 387 388 389 390 391 392 393
    xml = (E.Replace(
             E.CmdID('%s' % cmd_id),
             E.Meta(
               E.Type(media_type)
               ),
             E.Item(
               elem_to_append,
               data_node
               )
             ))
    if more_data:
394 395
      item_node = xml.find('{%s}Item' % SYNCML_NAMESPACE)
      item_node.append(E.MoreData())
Nicolas Delaby's avatar
Nicolas Delaby committed
396
    return etree.tostring(xml, encoding='utf-8', pretty_print=True)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
397

Nicolas Delaby's avatar
Nicolas Delaby committed
398
  def getXupdateObject(self, object_xml=None, old_xml=None):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
399 400
    """
    Generate the xupdate with the new object and the old xml
Nicolas Delaby's avatar
Nicolas Delaby committed
401 402 403
    """
    erp5diff = ERP5Diff()
    erp5diff.compare(old_xml, object_xml)
404 405 406 407
    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')
408 409
      attr_version.value = '1.0'
      erp5diff._result.documentElement.setAttributeNodeNS(attr_version)
410 411
      attr_ns = erp5diff._result.createAttributeNS(None, 'xmlns:xupdate')
      attr_ns.value = 'http://www.xmldb.org/xupdate'
412 413 414 415 416
      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
417
    #omit xml declaration
Jean-Paul Smets's avatar
Jean-Paul Smets committed
418 419 420
    xupdate = xupdate[xupdate.find('<xupdate:modifications'):]
    return xupdate

421
  def getSessionIdFromXml(self, xml):
422 423 424
    """
    We will retrieve the session id of the message
    """
425 426
    return int(xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:SessionID)',
                         namespaces=xml.nsmap))
427

428
  def getMessageIdFromXml(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
429 430 431
    """
    We will retrieve the message id of the message
    """
432 433
    return int(xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:MsgID)',
                         namespaces=xml.nsmap))
Sebastien Robin's avatar
Sebastien Robin committed
434 435 436 437 438

  def getTarget(self, xml):
    """
    return the target in the SyncHdr section
    """
439 440 441
    return '%s' %\
      xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Target/syncml:LocURI)',
                namespaces=xml.nsmap)
Sebastien Robin's avatar
Sebastien Robin committed
442

443
  def getAlertLastAnchor(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
444 445 446 447
    """
      Return the value of the last anchor, in the
      alert section of the xml_stream
    """
448 449 450
    return '%s' %\
      xml.xpath('string(.//syncml:Alert/syncml:Item/syncml:Meta/syncml:Anchor/syncml:Last)',
                namespaces=xml.nsmap)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
451

452
  def getAlertNextAnchor(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
453 454 455 456
    """
      Return the value of the next anchor, in the
      alert section of the xml_stream
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
457
    return '%s' %\
458 459
      xml.xpath('string(.//syncml:Alert/syncml:Item/syncml:Meta/syncml:Anchor/syncml:Next)',
                namespaces=xml.nsmap)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
460

461
  def getSourceURI(self, xml):
462 463 464
    """
    return the source uri of the data base
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
465
    return '%s' %\
466 467
      xml.xpath('string(//syncml:SyncBody/syncml:Alert/syncml:Item/syncml:Source/syncml:LocURI)',
                namespaces=xml.nsmap)
468

469 470 471 472
  def getTargetURI(self, xml):
    """
    return the target uri of the data base
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
473
    return '%s' %\
474 475
      xml.xpath('string(//syncml:SyncBody/syncml:Alert/syncml:Item/syncml:Target/syncml:LocURI)',
                namespaces=xml.nsmap)
476

477
  def getSubscriptionUrlFromXML(self, xml):
478 479 480
    """
    return the source URI of the syncml header
    """
481 482
    return '%s' % xml.xpath('string(//syncml:SyncHdr/syncml:Source/syncml:LocURI)',
                            namespaces=xml.nsmap)
483

Jean-Paul Smets's avatar
Jean-Paul Smets committed
484 485 486 487
  def getStatusTarget(self, xml):
    """
      Return the value of the alert code inside the xml_stream
    """
488
    return '%s' % xml.xpath('string(syncml:TargetRef)', namespaces=xml.nsmap)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
489 490 491 492 493

  def getStatusCode(self, xml):
    """
      Return the value of the alert code inside the xml_stream
    """
494
    status_code = '%s' % xml.xpath('string(syncml:Data)', namespaces=xml.nsmap)
495
    if status_code:
496
      return int(status_code)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
497 498
    return None

499 500 501 502
  def getStatusCommand(self, xml):
    """
      Return the value of the command inside the xml_stream
    """
503
    cmd = None
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
504
    if xml.xpath('local-name()') == 'Status':
505
      cmd = '%s' % xml.xpath('string(syncml:Cmd)', namespaces=xml.nsmap)
506
    return cmd
507

508 509 510
  def getCred(self, xml):
    """
      return the credential information : type, format and data
Nicolas Delaby's avatar
Nicolas Delaby committed
511
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
512
    format = '%s' %\
513 514
    xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Cred/syncml:Meta/*[local-name() = "Format"])',
              namespaces=xml.nsmap)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
515
    type = '%s' %\
516 517 518 519
    xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Cred/syncml:Meta/*[local-name() = "Type"])',
              namespaces=xml.nsmap)
    data = '%s' % xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Cred/syncml:Data)',
                            namespaces=xml.nsmap)
520 521 522

    return (format, type, data)

523
  def checkCred(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
524
    """
525
      Check if there's a Cred section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
526
    """
527
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncHdr/syncml:Cred)', namespaces=xml.nsmap))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
528

529
  def getChal(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
530
    """
531
      return the chalenge information : format and type
532
    """
533 534
    format = '%s' % xml.xpath('string(//*[local-name() = "Format"])', namespaces=xml.nsmap)
    type = '%s' % xml.xpath('string(//*[local-name() = "Type"])', namespaces=xml.nsmap)
535 536
    return (format, type)

537
  def checkChal(self, xml):
Sebastien Robin's avatar
Sebastien Robin committed
538
    """
539 540
      Check if there's a Chal section in the xml_stream
    """
541 542
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Status/syncml:Chal)',
                          namespaces=xml.nsmap))
543

544
  def checkMap(self, xml):
545 546 547
    """
      Check if there's a Map section in the xml_stream
    """
548
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Map)', namespaces=xml.nsmap))
549

550
  def setRidWithMap(self, xml, subscriber):
551 552 553 554
    """
      get all the local objects of the given target id and set them the rid with
      the given source id (in the Map section)
    """
555 556
    item_list = xml.xpath('/syncml:SyncML/syncml:SyncBody/syncml:Map/syncml:MapItem',
                          namespaces=xml.nsmap)
557
    for map_item in item_list:
558
      gid = '%s' % map_item.xpath('string(.//syncml:Target/syncml:LocURI)', namespaces=xml.nsmap)
559
      signature = subscriber.getSignatureFromGid(gid)
560
      rid = '%s' % map_item.xpath('string(.//syncml:Source/syncml:LocURI)', namespaces=xml.nsmap)
561
      signature.setRid(rid)
562

563
  def getAlertCodeFromXML(self, xml):
564 565 566
    """
      Return the value of the alert code inside the full syncml message
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
567
    alert_code = '%s' %\
568 569
    xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Alert/syncml:Data)',
              namespaces=xml.nsmap)
570
    if alert_code:
571 572 573
      return int(alert_code)
    else:
      return None
Sebastien Robin's avatar
Sebastien Robin committed
574

575
  def checkAlert(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
576
    """
Sebastien Robin's avatar
Sebastien Robin committed
577
      Check if there's an Alert section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
578
    """
579 580
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Alert)',
                namespaces=xml.nsmap))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
581

582
  def checkSync(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
583
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
584
      Check if there's an Sync section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
585
    """
586 587 588 589
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Sync)',
                namespaces=xml.nsmap))

  def checkFinal(self, xml):
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
590 591 592 593
    """
      Check if there's an Final section in the xml_stream
      The end sections (inizialisation, modification) have this tag
    """
594 595 596 597
    return  bool(xml.xpath('/syncml:SyncML/syncml:SyncBody/syncml:Final',
                 namespaces=xml.nsmap))

  def checkStatus(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
598
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
599
      Check if there's a Status section in the xml_stream
Jean-Paul Smets's avatar
Jean-Paul Smets committed
600
    """
601 602
    return bool(xml.xpath('string(/syncml:SyncML/syncml:SyncBody/syncml:Status)',
                namespaces=xml.nsmap))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
603

604
  def getSyncActionList(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
605
    """
606
    return the list of the action (could be "add", "replace", "delete").
Jean-Paul Smets's avatar
Jean-Paul Smets committed
607
    """
608 609
    return xml.xpath('//syncml:Add|//syncml:Delete|//syncml:Replace',
                     namespaces=xml.nsmap)
610

611
  def getSyncBodyStatusList(self, xml):
612 613 614 615 616
    """
    return the list of dictionary corredponding to the data of each status bloc
    the data are : cmd, code and source
    """
    status_list = []
617
    status_node_list = xml.xpath('//syncml:Status', namespaces=xml.nsmap)
618
    for status in status_node_list:
619
      tmp_dict = {}
620 621 622 623 624 625 626 627
      tmp_dict['cmd'] = '%s' % status.xpath('string(./syncml:Cmd)',
                                            namespaces=xml.nsmap)
      tmp_dict['code'] = '%s' % status.xpath('string(./syncml:Data)',
                                             namespaces=xml.nsmap)
      tmp_dict['source'] = '%s' % status.xpath('string(./syncml:SourceRef)',
                                               namespaces=xml.nsmap)
      tmp_dict['target'] = '%s' % status.xpath('string(./syncml:TargetRef)',
                                               namespaces=xml.nsmap)
628 629
      status_list.append(tmp_dict)
    return status_list
Jean-Paul Smets's avatar
Jean-Paul Smets committed
630

631
  def getDataText(self, xml):
632 633 634
    """
    return the section data in text form, it's usefull for the VCardConduit
    """
635 636
    return '%s' % xml.xpath('string(.//syncml:Item/syncml:Data)',
                            namespaces=xml.nsmap)
637

638
  def getDataSubNode(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
639 640 641
    """
      Return the node starting with <object....> of the action
    """
642 643
    object_node_list = xml.xpath('.//syncml:Item/syncml:Data/*[1]',
                                 namespaces=xml.nsmap)
644 645
    if object_node_list:
      return object_node_list[0]
Jean-Paul Smets's avatar
Jean-Paul Smets committed
646 647
    return None

648
  def getActionId(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
649 650 651
    """
      Return the rid of the object described by the action
    """
652 653
    id = '%s' % xml.xpath('string(.//syncml:Item/syncml:Source/syncml:LocURI)',
                          namespaces=xml.nsmap)
654
    if not id:
655 656
      id = '%s' % xml.xpath('string(.//syncml:Item/syncml:Target/syncml:LocURI)',
                            namespaces=xml.nsmap)
657
    return id
Jean-Paul Smets's avatar
Jean-Paul Smets committed
658

659
  def checkActionMoreData(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
660 661 662
    """
      Return the rid of the object described by the action
    """
663 664
    return bool(xml.xpath('.//syncml:Item/syncml:MoreData',
                          namespaces=xml.nsmap))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
665

666
  def getActionType(self, xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
667 668 669
    """
      Return the type of the object described by the action
    """
670 671
    return '%s' % xml.xpath('string(.//syncml:Meta/syncml:Type)',
                            namespaces=xml.nsmap)
672

673
  def cutXML(self, xml_string):
674
    """
675
    Sliced a xml tree a return two fragment
676
    """
677 678 679
    line_list = xml_string.split('\n')
    short_string = '\n'.join(line_list[:self.MAX_LINES])
    rest_string = '\n'.join(line_list[self.MAX_LINES:])
680 681 682
    xml_tree = E.Partial()
    xml_tree.text = etree.CDATA(short_string.decode('utf-8'))
    return xml_tree, rest_string
683

684
  def getSyncMLData(self, domain=None, remote_xml=None, cmd_id=0,
Nicolas Delaby's avatar
Nicolas Delaby committed
685
                    subscriber=None, xml_confirmation_list=None, conduit=None,
Nicolas Delaby's avatar
Nicolas Delaby committed
686
                    max=None, **kw):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
687
    """
688 689
    This generate the syncml data message. This returns a string
    with all modification made locally (ie replace, add ,delete...)
690 691 692

    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
693
    """
694
    #LOG('getSyncMLData starting...', DEBUG, domain.getId())
Sebastien Robin's avatar
Sebastien Robin committed
695 696
    if isinstance(conduit, str):
      conduit = self.getConduitByName(conduit)
Nicolas Delaby's avatar
Nicolas Delaby committed
697 698
    if xml_confirmation_list is None:
      xml_confirmation_list = []
699
    local_gid_list = []
Nicolas Delaby's avatar
Nicolas Delaby committed
700
    syncml_data_list = kw.get('syncml_data_list', [])
Nicolas Delaby's avatar
Nicolas Delaby committed
701
    result = {'finished':1}
702
    if isinstance(remote_xml, (str, unicode)):
703
      remote_xml = etree.XML(remote_xml, parser=parser)
704
    if domain.isOneWayFromServer():
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
705
      #Do not set object_path_list, subscriber send nothing it's a client
706 707
      subscriber.setRemainingObjectPathList([])
    elif subscriber.getRemainingObjectPathList() is None:
708
      object_list = domain.getObjectList()
709
      object_path_list = [x.getPhysicalPath() for x in object_list]
710
      subscriber.setRemainingObjectPathList(object_path_list)
711 712 713 714 715 716
      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:
717
          #LOG('getSyncMLData :', DEBUG, 'object:%s,  objectTitle:%s, local_gid_list:%s' % (object, object.getTitle(), local_gid_list))
718 719 720
          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)])
721 722
            if number:
              gid = '%s__%s' %  (gid, str(number+1))
723 724
          gid_not_encoded_list.append(gid)
          local_gid_list.append(b16encode(gid))
725
          #LOG('getSyncMLData :', DEBUG,'gid_not_encoded_list:%s, local_gid_list:%s, gid:%s' % (gid_not_encoded_list, local_gid_list, gid))
726
      else:
727
        local_gid_list = [domain.getGidFromObject(x) for x in object_list]
728
      # Objects to remove
729
      #LOG('getSyncMLData remove object to remove ...', DEBUG, '')
730
      for object_gid in subscriber.getGidList():
Nicolas Delaby's avatar
Nicolas Delaby committed
731
        if object_gid not in local_gid_list:
732
          # This is an object to remove
733
          signature = subscriber.getSignatureFromGid(object_gid)
734 735
          if signature.getStatus() != self.PARTIAL:
            # If partial, then we have a signature but no local object
736 737 738 739 740
            rid = signature.getRid()
            syncml_data_list.append(self.deleteXMLObject(object_gid=object_gid,
                                                         rid=rid,
                                                         cmd_id=cmd_id))
            cmd_id += 1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
741
          # Delete Signature if object does not exist anymore
742 743
          subscriber.delSignature(object_gid)

744
    local_gid_list = []
745
    loop = 0
746
    for object_path in subscriber.getRemainingObjectPathList():
Nicolas Delaby's avatar
Nicolas Delaby committed
747 748 749
      if max is not None and loop >= max:
        result['finished'] = 0
        break
750
      #LOG('getSyncMLData object_path', INFO, object_path)
751
      object = self.unrestrictedTraverse(object_path)
752
      status = self.SENT
753
      object_gid = domain.getGidFromObject(object)
754
      if not object_gid:
Nicolas Delaby's avatar
Nicolas Delaby committed
755
        continue
756
      local_gid_list += [object_gid]
757
      force = 0
Nicolas Delaby's avatar
Nicolas Delaby committed
758
      if ''.join(syncml_data_list).count('\n') < self.MAX_LINES and not \
759 760
          object.id.startswith('.'):
        # If not we have to cut
761 762 763 764 765 766 767
        #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))
768

Nicolas Delaby's avatar
Nicolas Delaby committed
769 770
        signature = subscriber.getSignatureFromGid(object_gid)
        ## Here we first check if the object was modified or not by looking at dates
771
        status = self.SENT
772
        more_data = 0
773
        # For the case it was never synchronized, we have to send everything
Nicolas Delaby's avatar
Nicolas Delaby committed
774
        if signature is not None and signature.getXMLMapping() is None:
775
          pass
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
776
        elif signature is None or\
777
            (not signature.hasXML() and\
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
778
            signature.getStatus() != self.PARTIAL) or\
779
            self.getAlertCodeFromXML(remote_xml) == self.SLOW_SYNC:
780
          #LOG('getSyncMLData', DEBUG, 'Current object.getPath: %s' % object.getPath())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
781 782
          xml_string = conduit.getXMLFromObjectWithId(object,\
                       xml_mapping=domain.getXMLMapping())
783
          gid = subscriber.getGidFromObject(object)
784
          signature = Signature(id=gid, object=object).__of__(subscriber)
785
          signature.setTempXML(xml_string)
786
          if xml_string.count('\n') > self.MAX_LINES:
787
            more_data = 1
788
            xml_string, rest_string = self.cutXML(xml_string)
789
            signature.setPartialXML(rest_string)
790
            status = self.PARTIAL
791
            signature.setAction('Add')
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
792
          #in first, we try with rid if there is one
793
          gid = signature.getRid() or signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
794
          syncml_data_list.append(self.addXMLObject(
795 796 797 798 799
                                  cmd_id=cmd_id,
                                  object=object,
                                  gid=gid,
                                  xml_string=xml_string,
                                  more_data=more_data,
Nicolas Delaby's avatar
Nicolas Delaby committed
800
                                  media_type=subscriber.getMediaType()))
801 802 803
          cmd_id += 1
          signature.setStatus(status)
          subscriber.addSignature(signature)
804 805
        elif signature.getStatus() in (self.NOT_SYNCHRONIZED,
                                       self.PUB_CONFLICT_MERGE,):
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
806 807 808
          # We don't have synchronized this object yet but it has a signature
          xml_object = conduit.getXMLFromObjectWithId(object,\
                       xml_mapping=domain.getXMLMapping()) 
809 810
          #LOG('getSyncMLData', DEBUG, 'checkMD5: %s' % str(signature.checkMD5(xml_object)))
          #LOG('getSyncMLData', DEBUG, 'getStatus: %s' % str(signature.getStatus()))
811
          if signature.getStatus() == self.PUB_CONFLICT_MERGE:
Nicolas Delaby's avatar
Nicolas Delaby committed
812 813 814 815 816
            xml_confirmation_list.append(self.SyncMLConfirmation(
                                    cmd_id=cmd_id,
                                    source_ref=signature.getGid(),
                                    sync_code=self.CONFLICT_MERGE,
                                    cmd='Replace'))
817
          set_synchronized = 1
818
          if not signature.checkMD5(xml_object):
819
            set_synchronized = 0
820
            if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
821
              # If there is no xml, we re-send all the objects
822
              xml_string = xml_object
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
823 824 825 826 827 828
            else:
             # This object has changed on this side, we have to generate some xmldiff
              xml_string = self.getXupdateObject(xml_object, signature.getXML())
              if xml_string.count('\n') > self.MAX_LINES:
                # This make comment fails, so we need to replace
                more_data = 1
829
                xml_string, rest_string = self.cutXML(xml_string)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
830 831 832 833 834
                signature.setPartialXML(rest_string)
                status = self.PARTIAL
                signature.setAction('Replace')
                signature.setStatus(status)
            rid = signature.getRid()#in first, we try with rid if there is
835
            gid = signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
836
            syncml_data_list.append(self.replaceXMLObject(
837 838 839 840
                                        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
841
                                        media_type=subscriber.getMediaType()))
842 843
            cmd_id += 1
            signature.setTempXML(xml_object)
844 845
          # Now we can apply the xupdate from the subscriber
          subscriber_xupdate = signature.getSubscriberXupdate()
846
          #LOG('getSyncMLData subscriber_xupdate', DEBUG, subscriber_xupdate)
847
          if subscriber_xupdate is not None:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
848 849
            # The modification in the xml from signature is compare and update
            # with xml_xupdate from subscriber
850
            old_xml = signature.getXML()
851 852 853 854 855 856
            conduit.updateNode(
                        xml=subscriber_xupdate,
                        object=object,
                        previous_xml=old_xml,
                        force=(domain.getDomainType() == self.SUB),
                        simulate=0)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
857 858
            xml_object = conduit.getXMLFromObjectWithId(object,\
                         xml_mapping=domain.getXMLMapping()) 
859 860 861
            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
862
            signature.setStatus(self.SYNCHRONIZED)
863
        elif signature.getStatus() == self.PUB_CONFLICT_CLIENT_WIN:
864
          # We have decided to apply the update
865
          # XXX previous_xml will be geXML instead of getTempXML because
866 867
          # some modification was already made and the update
          # may not apply correctly
868
          xml_update = signature.getPartialXML()
869
          conduit.updateNode(
870
                      xml=xml_update,
871 872 873
                      object=object,
                      previous_xml=signature.getXML(),
                      force=1)
Nicolas Delaby's avatar
Nicolas Delaby committed
874 875 876 877 878
          xml_confirmation_list.append(self.SyncMLConfirmation(
                                  cmd_id=cmd_id,
                                  target_ref=object_gid,
                                  sync_code=self.CONFLICT_CLIENT_WIN,
                                  cmd='Replace'))
879
          signature.setStatus(self.SYNCHRONIZED)
880
        elif signature.getStatus() == self.PARTIAL:
881
          xml_string = signature.getPartialXML(default='')
882
          if(subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']):
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
883 884
            xml_to_send = conduit.getXMLFromObjectWithId(object,\
                                  xml_mapping=domain.getXMLMapping()) 
885 886
          elif xml_string.count('\n') > self.MAX_LINES:
            more_data = 1
887 888
            # Receive the chunk of partial xml
            short_string = signature.getFirstChunkPdata(self.MAX_LINES)
889
            xml_to_send = E.Partial()
890
            xml_to_send.text = etree.CDATA(short_string.decode('utf-8'))
891
            status = self.PARTIAL
892
          else:
893
            xml_to_send = E.Partial()
894
            xml_to_send.text = etree.CDATA(xml_string.decode('utf-8'))
895
          signature.setStatus(status)
896
          if signature.getAction() == 'Replace':
897 898
            rid = signature.getRid()
            # In first, we try with rid if there is one
899
            gid = signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
900
            syncml_data_list.append(self.replaceXMLObject(
901 902 903 904
                                       cmd_id=cmd_id,
                                       object=object,
                                       gid=gid,
                                       rid=rid,
905
                                       xml_string=xml_to_send,
906
                                       more_data=more_data,
Nicolas Delaby's avatar
Nicolas Delaby committed
907
                                       media_type=subscriber.getMediaType()))
908
          elif signature.getAction() == 'Add':
909 910
            #in fisrt, we try with rid if there is one
            gid = signature.getRid() or signature.getGid()
Nicolas Delaby's avatar
Nicolas Delaby committed
911 912 913 914 915 916 917
            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
918 919
        if not more_data:
          subscriber.removeRemainingObjectPath(object_path)
920
      else:
Nicolas Delaby's avatar
Nicolas Delaby committed
921
        result['finished'] = 1
922
        break
923
      loop += 1
Nicolas Delaby's avatar
Nicolas Delaby committed
924 925
    result['syncml_data_list'] = syncml_data_list
    result['xml_confirmation_list'] = xml_confirmation_list
926 927
    result['cmd_id'] = cmd_id
    return result
928

929
  def applyActionList(self, domain=None, subscriber=None, cmd_id=0,
930
                      remote_xml=None, conduit=None, simulate=0):
931 932 933
    """
    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
934
    """
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
935
    namespace = self.getNamespace(remote_xml.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
936
    xml_confirmation_list = []
Jean-Paul Smets's avatar
Jean-Paul Smets committed
937
    has_next_action = 0
938
    gid_from_xml_list = []
939
    destination = self.unrestrictedTraverse(domain.getDestinationPath())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
940 941 942
    #LOG('applyActionList args', DEBUG, 'domain : %s\n subscriber : %s\n cmd_id: %s'\ 
    #% (domain.getPath(), subscriber.getPath(), cmd_id))
    #LOG('XMLSyncUtils applyActionList', DEBUG, self.getSyncActionList(remote_xml))
943
    for action in self.getSyncActionList(remote_xml):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
944 945 946
      conflict_list = []
      status_code = self.SUCCESS
      # Thirst we have to check the kind of action it is
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
947 948 949 950 951 952 953 954 955 956 957 958

      # The rid is the Temporary GUID (SYNCML Protocol). the rid sent by the
      # client unlike gid. The rid is in MapItem for each Action Map it's the LocURI in
      # the action. 
      gid = rid = self.getActionId(action)
      #The action delete hasn't need a gid and retrieve the gid of conduit for
      #object.
      if action.xpath('local-name()') != 'Delete':
        data_action = self.getDataSubNode(action)
        if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
          #data in unicode
          data_action = self.getDataText(action)
959
        if getattr(conduit, 'getGidFromXML', None) is not None and \
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
960 961
          conduit.getGidFromXML(data_action, namespace, gid_from_xml_list):
          gid = conduit.getGidFromXML(data_action, namespace, gid_from_xml_list)
962 963
          gid_from_xml_list.append(gid)
          gid = b16encode(gid)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
964 965
      #the rid unlike gid, it's the rid or gid (if rid == gid) will use for
      #retrieve object and send response to client
966 967
      signature = subscriber.getSignatureFromGid(gid)
      object = subscriber.getObjectFromGid(gid)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
968 969 970 971 972 973 974 975
      object_id = domain.generateNewIdWithGenerator(object=destination, gid=gid)
      if rid == gid:
        #We can't use our own gid and the temporary GUID is useless
        rid = None
      if signature is not None:
        signature.setRid(rid)
      else:
        signature = Signature(id=gid, rid=rid, status=self.NOT_SYNCHRONIZED,
976
                                object=object).__of__(subscriber)
977
        signature.setObjectId(object_id)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
978 979
        subscriber.addSignature(signature)
      force = signature.getForce()
980 981
      partial_node = action.find('.//%(uri)sItem/%(uri)sData/%(uri)sPartial' % {'uri': '{SYNCML:SYNCML1.2}'})
      partial_data = partial_node is not None and partial_node.text or ''
982
      if not self.checkActionMoreData(action):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
983
        data_subnode = None
984 985 986 987 988
        if partial_node is not None:
          if not partial_data:
            data_subnode = signature.getPartialXML(default='')
            signature.setPartialXML(None)
          elif signature.hasPartialXML():
989 990
            signature.appendPartialXML(partial_data)
            data_subnode = signature.getPartialXML()
991
            signature.setPartialXML(None)
992 993
          else:
            data_subnode = partial_data
994
          #LOG('applyActionList', DEBUG, 'data_subnode: %s' % data_subnode)
995
          if subscriber.getMediaType() == self.MEDIA_TYPE['TEXT_XML']:
996
            data_subnode = etree.XML(data_subnode, parser=parser)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
997
        else:
998
          if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
999
            data_subnode = self.getDataText(action)
1000
          else:
1001
            data_subnode = self.getDataSubNode(action)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1002
        if action.xpath('local-name()') == 'Add':
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1003
          # Then store the xml of this new subobject
1004
          reset = 0
1005
          if object is None:
1006 1007 1008
            add_data = conduit.addNode(xml=data_subnode,
                                       object=destination,
                                       object_id=object_id)
1009
            conflict_list.extend(add_data['conflict_list'])
1010 1011
            # Retrieve directly the object from addNode
            object = add_data['object']
Nicolas Delaby's avatar
Nicolas Delaby committed
1012 1013
            if object is not None:
              signature.setPath(object.getPhysicalPath())
1014
              signature.setObjectId(object.getId())
1015
          else:
1016
            reset = 1
1017 1018
            # Object was retrieve but need to be updated without recreated
            # usefull when an object is only deleted by workflow.
1019
            if data_subnode is not None:
1020 1021
              actual_xml = conduit.getXMLFromObjectWithId(object,
                           xml_mapping=domain.getXMLMapping(force=1))
1022 1023
              actual_xml = etree.XML(actual_xml, parser=parser)
              xml_string_gid = conduit.replaceIdFromXML(data_subnode, gid)
1024 1025 1026
              actual_xml_gid = conduit.replaceIdFromXML(actual_xml, gid)
              # use gid as compare key because their ids can be different
              data_subnode = self.getXupdateObject(xml_string_gid, actual_xml_gid)
Nicolas Delaby's avatar
Nicolas Delaby committed
1027
            conflict_list.extend(conduit.updateNode(
1028 1029 1030 1031
                                        xml=data_subnode,
                                        object=object,
                                        previous_xml=signature.getXML(),
                                        force=force,
1032 1033
                                        simulate=simulate,
                                        reset=reset))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1034 1035
            xml_object = conduit.getXMLFromObjectWithId(object,\
                         xml_mapping=domain.getXMLMapping()) 
1036
            signature.setTempXML(xml_object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1037
          if object is not None:
1038
            #LOG('applyActionList', DEBUG, 'addNode, found the object')
1039 1040 1041
            if reset:
              #After a reset we want copy the LAST XML view on Signature.
              #this implementation is not sufficient, need to be improved.
1042 1043
              if not isinstance(xml_object, str):
                xml_object = etree.tostring(xml_object, encoding='utf-8',
Nicolas Delaby's avatar
Nicolas Delaby committed
1044
                                            pretty_print=True)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1045 1046 1047
            else: 
              xml_object = conduit.getXMLFromObjectWithId(object,\
                           xml_mapping=domain.getXMLMapping()) 
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1048
            signature.setStatus(self.SYNCHRONIZED)
1049
            #signature.setId(object.getId())
1050
            signature.setPath(object.getPhysicalPath())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1051
            signature.setXML(xml_object)
Nicolas Delaby's avatar
Nicolas Delaby committed
1052 1053 1054 1055 1056
            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
1057
            cmd_id +=1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1058
        elif action.xpath('local-name()') == 'Replace':
1059
          #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % str(object))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1060
          if object is not None:
1061
            #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % object.id)
1062
            signature = subscriber.getSignatureFromGid(gid)
1063
            #LOG('applyActionList', DEBUG, 'previous signature: %s' % str(signature))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1064
            previous_xml = signature.getXML()
1065 1066 1067
            conflict_list += conduit.updateNode(
                                        xml=data_subnode,
                                        object=object,
1068
                                        previous_xml=previous_xml,
1069 1070
                                        force=force,
                                        simulate=simulate)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1071 1072
            xml_object = conduit.getXMLFromObjectWithId(object,\
                         xml_mapping=domain.getXMLMapping())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1073
            signature.setTempXML(xml_object)
1074
            if conflict_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1075 1076
              status_code = self.CONFLICT
              signature.setStatus(self.CONFLICT)
1077 1078
              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
1079
              signature.setPartialXML(data_subnode_string)
1080
            elif not simulate:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1081
              signature.setStatus(self.SYNCHRONIZED)
Nicolas Delaby's avatar
Nicolas Delaby committed
1082 1083 1084 1085 1086
            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
1087
            cmd_id +=1
1088
            if simulate:
Sebastien Robin's avatar
Sebastien Robin committed
1089
              # This means we are on the publisher side and we want to store
1090 1091
              # the xupdate from the subscriber and we also want to generate
              # the current xupdate from the last synchronization
1092
              data_subnode_string = etree.tostring(data_subnode, encoding='utf-8')
1093
              #LOG('applyActionList, subscriber_xupdate:', TRACE, data_subnode_string)
1094 1095
              signature.setSubscriberXupdate(data_subnode_string)

Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1096 1097
        elif action.xpath('local-name()') == 'Delete':
          LOG("applyactionlist delete",INFO,"")
1098
          object_id = signature.getId()
1099
          #LOG('applyActionList Delete on : ', DEBUG, (signature.getId(), subscriber.getObjectFromGid(object_id)))
1100
          if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
1101
            data_subnode = self.getDataText(action)
1102
          else:
1103
            data_subnode = self.getDataSubNode(action)
1104
          #LOG('applyActionList, object gid to delete :', 0, subscriber.getObjectFromGid(object_id))
1105
          if subscriber.getObjectFromGid(object_id) is not None:
1106
          #if the object exist:
1107 1108 1109 1110
            conduit.deleteNode(
                        xml=data_subnode,
                        object=destination,
                        object_id=subscriber.getObjectFromGid(object_id).getId())
1111
            subscriber.delSignature(gid)
Nicolas Delaby's avatar
Nicolas Delaby committed
1112 1113 1114 1115 1116
          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
1117 1118
      else: # We want to retrieve more data
        signature.setStatus(self.PARTIAL)
1119
        signature.appendPartialXML(partial_data) 
1120 1121
        #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
1122 1123
        xml_confirmation_list.append(self.SyncMLConfirmation(
                                cmd_id=cmd_id,
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1124
                                cmd= "%s" % action.xpath('name()'),
Nicolas Delaby's avatar
Nicolas Delaby committed
1125 1126
                                sync_code=self.WAITING_DATA,
                                remote_xml=action))
1127
      if conflict_list and signature is not None:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1128 1129
        # We had a conflict
        signature.setStatus(self.CONFLICT)
1130

Nicolas Delaby's avatar
Nicolas Delaby committed
1131
    return (xml_confirmation_list, has_next_action, cmd_id)
1132

1133
  def applyStatusList(self, subscriber=None, remote_xml=None):
1134 1135 1136 1137
    """
    This read a list of status list (ie syncml confirmations).
    This method have to change status codes on signatures
    """
1138
    status_list = self.getSyncBodyStatusList(remote_xml)
1139 1140
    has_status_list = 0
    destination_waiting_more_data = 0
1141 1142 1143 1144 1145 1146 1147 1148
    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'])
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1149 1150 1151 1152 1153
      signature = subscriber.getSignatureFromGid(object_gid)
      if signature is None and\
      not(subscriber.getSynchronizeWithERP5Sites()):
        #the client give his id but not the gid
        signature = subscriber.getSignatureFromRid(object_gid)
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175
      if status_cmd in ('Add', 'Replace',):
        has_status_list = 1
        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:
          if signature is not None:
            subscriber.delSignature(signature.getGid())
1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192
    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

1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211
  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

1212 1213 1214 1215
  def SyncModif(self, domain, remote_xml):
    """
    Modification Message, this is used after the first
    message in order to send modifications.
1216 1217
    Send the server modification, this happens after the Synchronization
    initialization
1218
    """
1219
    has_response = 0 #check if syncmodif replies to this messages
1220
    cmd_id = 1 # specifies a SyncML message-unique command identifier
1221
    #LOG('SyncModif', DEBUG, 'Starting... domain: %s' % domain.getId())
1222
    # Get informations from the header
1223
    xml_header = remote_xml[0]
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1224 1225
    #FIXME to apply a DTD or schema
    if xml_header.xpath('local-name()') != "SyncHdr":
1226
      LOG('SyncModif', INFO, 'This is not a SyncML Header')
Sebastien Robin's avatar
Sebastien Robin committed
1227
      raise ValueError, "Sorry, This is not a SyncML Header"
1228 1229

    subscriber = domain # If we are the client, this is fine
1230
    simulate = 0 # used by applyActionList, should be 0 for client
1231
    if domain.domain_type == self.PUB:
1232
      simulate = 1
1233
      subscription_url = self.getSubscriptionUrlFromXML(xml_header)
1234
      subscriber = domain.getSubscriber(subscription_url)
1235

1236 1237
    # We have to check if this message was not already, this can be dangerous
    # to update two times the same object
1238
    message_id = self.getMessageIdFromXml(remote_xml)
Sebastien Robin's avatar
Sebastien Robin committed
1239 1240
    correct_message = subscriber.checkCorrectRemoteMessageId(message_id)
    if not correct_message: # We need to send again the message
1241
      LOG('SyncModif, no correct message:', INFO, "sending again...")
1242
      last_xml = subscriber.getLastSentMessage()
1243
      LOG("SyncModif last_xml :", INFO, last_xml)
1244 1245 1246
      remote_xml = etree.tostring(remote_xml, encoding='utf-8',
                                  xml_declaration=True,
                                  pretty_print=True)
Nicolas Delaby's avatar
Nicolas Delaby committed
1247
      LOG("SyncModif remote_xml :", INFO, remote_xml)
1248
      if last_xml:
1249 1250
        has_response = 1
        if domain.domain_type == self.PUB: # We always reply
1251 1252 1253 1254
          self.sendResponse(
                    from_url=domain.publication_url,
                    to_url=subscriber.subscription_url,
                    sync_id=domain.getTitle(),
1255
                    xml=last_xml, domain=domain,
1256
                    content_type=domain.getSyncContentType())
1257
        elif domain.domain_type == self.SUB:
1258 1259 1260 1261 1262 1263 1264 1265
          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}
1266 1267
    subscriber.setLastSentMessage('')

1268
    # First apply the list of status codes
Nicolas Delaby's avatar
Nicolas Delaby committed
1269
    (destination_waiting_more_data, has_status_list) = self.applyStatusList(
1270 1271
                                                      subscriber=subscriber,
                                                      remote_xml=remote_xml)
1272

1273
    alert_code = self.getAlertCodeFromXML(remote_xml)
Kevin Deldycke's avatar
Kevin Deldycke committed
1274
    # Import the conduit and get it
1275
    conduit = self.getConduitByName(subscriber.getConduit())
1276
    # Then apply the list of actions
Nicolas Delaby's avatar
Nicolas Delaby committed
1277
    (xml_confirmation_list, has_next_action, cmd_id) = self.applyActionList(
1278 1279 1280 1281 1282
                                          cmd_id=cmd_id,
                                          domain=domain,
                                          subscriber=subscriber,
                                          remote_xml=remote_xml,
                                          conduit=conduit, simulate=simulate)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1283

1284
    xml = E.SyncML()
1285

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1286 1287
    # syncml header
    if domain.domain_type == self.PUB:
Nicolas Delaby's avatar
Nicolas Delaby committed
1288
      xml.append(self.SyncMLHeader(
1289 1290 1291 1292
                  subscriber.getSessionId(),
                  subscriber.incrementMessageId(),
                  subscriber.getSubscriptionUrl(),
                  domain.getPublicationUrl()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1293
    elif domain.domain_type == self.SUB:
Nicolas Delaby's avatar
Nicolas Delaby committed
1294
      xml.append(self.SyncMLHeader(
1295 1296 1297
                  domain.getSessionId(), domain.incrementMessageId(),
                  domain.getPublicationUrl(),
                  domain.getSubscriptionUrl()))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1298 1299

    # syncml body
1300 1301
    sync_body = E.SyncBody()
    xml.append(sync_body)
1302

1303 1304 1305 1306 1307
    xml_status, cmd_id = self.SyncMLStatus(
                                    remote_xml,
                                    self.SUCCESS,
                                    cmd_id,
                                    subscriber.getNextAnchor(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1308 1309
                                    subscription=subscriber)
    sync_body.extend(xml_status)
1310

Jean-Paul Smets's avatar
Jean-Paul Smets committed
1311 1312
    destination_url = ''
    # alert message if we want more data
1313
    if destination_waiting_more_data:
Nicolas Delaby's avatar
Nicolas Delaby committed
1314 1315 1316 1317 1318 1319 1320
      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
1321
    # Now we should send confirmations
1322
    cmd_id_before_getsyncmldata = cmd_id
1323
    cmd_id = cmd_id+1
1324
    if domain.getActivityEnabled():
1325
      #use activities to get SyncML data.
Nicolas Delaby's avatar
Nicolas Delaby committed
1326
      remote_xml = etree.tostring(remote_xml, encoding='utf-8',
1327
                                    xml_declaration=True, pretty_print=False)
Nicolas Delaby's avatar
Nicolas Delaby committed
1328 1329
      xml_tree = etree.tostring(xml, encoding='utf-8', xml_declaration=True,
                                pretty_print=False)
1330 1331 1332 1333
      xml_confirmation_list = [etree.tostring(xml, encoding='utf-8',\
                                              xml_declaration=True,\
                                              pretty_print=False) for xml in \
                                              xml_confirmation_list]
1334
      domain.activate(activity='SQLQueue',
Nicolas Delaby's avatar
Nicolas Delaby committed
1335 1336
                      tag=domain.getId(),
                      priority=self.PRIORITY).activateSyncModif(
1337
                      domain_relative_url=domain.getRelativeUrl(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1338
                      remote_xml=remote_xml,
Nicolas Delaby's avatar
Nicolas Delaby committed
1339
                      xml_tree=xml_tree,
Nicolas Delaby's avatar
Nicolas Delaby committed
1340 1341
                      subscriber_relative_url=subscriber.getRelativeUrl(),
                      cmd_id=cmd_id,
Nicolas Delaby's avatar
Nicolas Delaby committed
1342 1343
                      xml_confirmation_list=xml_confirmation_list,
                      syncml_data_list=[],
Nicolas Delaby's avatar
Nicolas Delaby committed
1344 1345 1346
                      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
1347
      return {'has_response':1, 'xml':''}
1348 1349
    else:
      result = self.getSyncMLData(domain=domain,
1350 1351
                             remote_xml=remote_xml,
                             subscriber=subscriber,
1352
                             cmd_id=cmd_id,
Nicolas Delaby's avatar
Nicolas Delaby committed
1353
                             xml_confirmation_list=xml_confirmation_list,
1354
                             conduit=conduit,
1355
                             max=None)
Nicolas Delaby's avatar
Nicolas Delaby committed
1356 1357
      syncml_data_list = result['syncml_data_list']
      xml_confirmation_list = result['xml_confirmation_list']
1358
      cmd_id = result['cmd_id']
Nicolas Delaby's avatar
Nicolas Delaby committed
1359 1360 1361
      return self.sendSyncModif(syncml_data_list, cmd_id_before_getsyncmldata,
                                subscriber, domain, xml_confirmation_list,
                                remote_xml, xml, has_status_list,
1362
                                has_response)
1363

1364 1365 1366 1367 1368 1369 1370
  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]
1371 1372 1373 1374 1375 1376
    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
1377 1378
                    tag=domain.getId(),
                    priority=self.PRIORITY).activateDeleteRemainObjectList(domain_path,
1379 1380 1381 1382 1383 1384 1385 1386 1387
                                                                       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)
1388 1389 1390 1391
    destination = self.unrestrictedTraverse(domain.getDestinationPath())
    conduit_name = subscriber.getConduit()
    conduit = self.getConduitByName(conduit_name)
    for gid in gid_list:
1392
      if subscriber.getSignatureFromGid(gid) is None:
1393 1394 1395
        object_id = b16decode(gid)
        conduit.deleteObject(object=destination, object_id=object_id)

1396
  def activateSyncModif(self, **kw):
1397 1398 1399
    domain = self.unrestrictedTraverse(kw['domain_relative_url'])
    subscriber = self.unrestrictedTraverse(kw['subscriber_relative_url'])
    conduit = subscriber.getConduit()
1400 1401
    result = self.getSyncMLData(domain=domain, subscriber=subscriber,
                                conduit=conduit, max=self.MAX_OBJECTS, **kw)
Nicolas Delaby's avatar
Nicolas Delaby committed
1402
    syncml_data_list = result['syncml_data_list']
1403
    cmd_id = result['cmd_id']
Nicolas Delaby's avatar
Nicolas Delaby committed
1404
    kw['syncml_data_list'] = syncml_data_list
1405
    kw['cmd_id'] = cmd_id
1406 1407
    finished = result['finished']
    if not finished:
1408
      domain.activate(activity='SQLQueue',
Nicolas Delaby's avatar
Nicolas Delaby committed
1409 1410
                      tag=domain.getId(),
                      priority=self.PRIORITY).activateSyncModif(**kw)
1411 1412 1413
    else:
      cmd_id = result['cmd_id']
      cmd_id_before_getsyncmldata = kw['cmd_id_before_getsyncmldata']
1414
      remote_xml = etree.XML(kw['remote_xml'], parser=parser)
Nicolas Delaby's avatar
Nicolas Delaby committed
1415 1416
      xml_tree = etree.XML(kw['xml_tree'], parser=parser)
      xml_confirmation_list = kw['xml_confirmation_list']
1417 1418
      has_status_list = kw['has_status_list']
      has_response = kw['has_response']
1419
      return self.sendSyncModif(
Nicolas Delaby's avatar
Nicolas Delaby committed
1420
                        syncml_data_list,
1421 1422 1423
                        cmd_id_before_getsyncmldata,
                        subscriber,
                        domain,
Nicolas Delaby's avatar
Nicolas Delaby committed
1424
                        xml_confirmation_list,
1425
                        remote_xml,
Nicolas Delaby's avatar
Nicolas Delaby committed
1426
                        xml_tree,
1427 1428
                        has_status_list,
                        has_response)
1429

Nicolas Delaby's avatar
Nicolas Delaby committed
1430 1431 1432
  def sendSyncModif(self, syncml_data_list, cmd_id_before_getsyncmldata,
                    subscriber, domain, xml_confirmation_list, remote_xml,
                    xml_tree, has_status_list, has_response):
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1433
    # XXX the better is a namespace for all
1434
    namespace = self.getNamespace(xml_tree.nsmap)
Nicolas Delaby's avatar
Nicolas Delaby committed
1435
    sync_body = xml_tree.find('SyncBody')
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1436 1437
    if sync_body is None:
      sync_body = xml_tree.xpath('syncml:SyncBody')[0]
Nicolas Delaby's avatar
Nicolas Delaby committed
1438
    if syncml_data_list:
1439 1440
      sync_node = E.Sync(E.CmdID('%s' % cmd_id_before_getsyncmldata))
      sync_body.append(sync_node)
Nicolas Delaby's avatar
Nicolas Delaby committed
1441 1442 1443 1444 1445 1446 1447 1448
      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))
1449
    for xml_confirmation in xml_confirmation_list:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1450 1451 1452
      if isinstance(xml_confirmation, str):
        xml_confirmation = etree.XML(xml_confirmation, parser=parser)
      sync_body.append(xml_confirmation)
1453

Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1454
    self.sync_finished = 0 
1455
    if domain.domain_type == self.PUB: # We always reply
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1456 1457 1458 1459
      # When the publication recieved the response Final and the modification 
      # data is finished so the publication send the tag "Final"
      if not self.checkSync(remote_xml) and not xml_confirmation_list\
        and not syncml_data_list and self.checkFinal(remote_xml):
1460
        sync_body.append(E.Final())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1461 1462
        self.sync_finished = 1
      xml_string = etree.tostring(xml_tree, encoding='utf-8', pretty_print=True)
Nicolas Delaby's avatar
Nicolas Delaby committed
1463
      subscriber.setLastSentMessage(xml_string)
1464 1465 1466 1467
      self.sendResponse(
                from_url=domain.publication_url,
                to_url=subscriber.subscription_url,
                sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1468
                xml=xml_string,
1469 1470
                domain=domain,
                content_type=domain.getSyncContentType())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1471
      if self.sync_finished == 1:
Nicolas Delaby's avatar
Nicolas Delaby committed
1472
        LOG('this is the end of the synchronisation session from PUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1473 1474
        subscriber.setAuthenticated(False)
        domain.setAuthenticated(False)
1475 1476
      has_response = 1
    elif domain.domain_type == self.SUB:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1477 1478 1479 1480
      # the modification data is finished on the subscription so the tag
      # "Final" sent to the publication
      if not self.checkAlert(remote_xml) and not xml_confirmation_list\
        and not syncml_data_list:
1481
        sync_body.append(E.Final())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1482 1483 1484
        self.sync_finished = 1
      xml_string = etree.tostring(xml_tree, encoding='utf-8', pretty_print=True)
      if not self.sync_finished or not self.checkFinal(remote_xml):
Nicolas Delaby's avatar
Nicolas Delaby committed
1485
        subscriber.setLastSentMessage(xml_string)
1486 1487 1488 1489
        self.sendResponse(
                  from_url=domain.subscription_url,
                  to_url=domain.publication_url,
                  sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1490
                  xml=xml_string, domain=domain,
1491
                  content_type=domain.getSyncContentType())
1492
        has_response = 1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1493
      #When the receive the final element and the sub finished synchronization
Fabien Morin's avatar
Fabien Morin committed
1494
      else:
1495 1496
        if domain.isOneWayFromServer():
          self.deleteRemainObjectList(domain, subscriber)
Nicolas Delaby's avatar
Nicolas Delaby committed
1497
        LOG('this is the end of the synchronisation session from SUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1498
        domain.setAuthenticated(False)
Nicolas Delaby's avatar
Nicolas Delaby committed
1499
    return {'has_response':has_response, 'xml':xml_string}
1500 1501 1502 1503 1504

  def xml2wbxml(self, xml):
    """
    convert xml string to wbxml using a temporary file
    """
1505
    #LOG('xml2wbxml starting ...', DEBUG, '')
1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519
    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
    """
1520
    #LOG('wbxml2xml starting ...', DEBUG, '')
1521 1522 1523 1524 1525 1526 1527 1528 1529
    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
1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544

  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:
1545
      if isinstance(xml_client, (str, unicode)):
1546
        xml_client = etree.XML(xml_client, parser=parser)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1547 1548
      #FIXME to apply a DTD or schema
      if xml_client.xpath('local-name()') != "SyncML":
1549 1550 1551 1552
        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
1553
      client_header = xml_client[0]
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1554 1555
      #FIXME to apply a DTD or schema
      if client_header.xpath('local-name()') != "SyncHdr":
1556 1557 1558 1559 1560
        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)
1561
      if subscriber is None:
Nicolas Delaby's avatar
Nicolas Delaby committed
1562
        subscriber = Subscriber(publication.generateNewId(), subscription_url)
1563 1564 1565
        subscriber.setXMLMapping(publication.getXMLMapping())
        subscriber.setConduit(publication.getConduit())
        publication.addSubscriber(subscriber)
1566
        subscriber = subscriber.__of__(publication)
1567
        # first synchronization
Nicolas Delaby's avatar
Nicolas Delaby committed
1568 1569 1570 1571
        result = self.PubSyncInit(publication=publication,
                                  xml_client=xml_client,
                                  subscriber=subscriber,
                                  sync_type=self.SLOW_SYNC)
1572 1573 1574 1575 1576
      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
1577 1578 1579 1580
        result = self.PubSyncInit(publication=publication,
                                  xml_client=xml_client,
                                  subscriber=subscriber,
                                  sync_type=alert_code)
1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594
      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
1595 1596 1597 1598
      result = self.PubSyncInit(publication=publication,
                                xml_client=None,
                                subscriber=subscriber,
                                sync_type=self.TWO_WAY)
1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609
    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)
1610
    if msg is None and (subscription.getSubscriptionUrl()).find('file') >= 0:
1611
      msg = self.readResponse(sync_id=subscription.getSubscriptionUrl(),
1612
                              from_url=subscription.getSubscriptionUrl())
1613
    if msg is None:
1614 1615 1616
      response = self.SubSyncInit(subscription)
    else:
      xml_client = msg
1617
      if isinstance(xml_client, (str, unicode)):
1618
        xml_client = etree.XML(xml_client, parser=parser)
1619
        status_list = self.getSyncBodyStatusList(xml_client)
1620
        if status_list:
1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644
          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:
1645
          response = self.SubSyncModif(subscription, xml_client)
1646 1647 1648 1649 1650 1651

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