XMLSyncUtils.py 70.2 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')
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()
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()
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
980
      partial_data = '%s' % action.xpath('string(.//syncml:Item/syncml:Data/syncml:Partial)')
981
      if not self.checkActionMoreData(action):
Jean-Paul Smets's avatar
Jean-Paul Smets committed
982
        data_subnode = None
983
        if partial_data:
984 985 986
          if signature.hasPartialXML():
            signature.appendPartialXML(partial_data)
            data_subnode = signature.getPartialXML()
987 988
          else:
            data_subnode = partial_data
989
          #LOG('applyActionList', DEBUG, 'data_subnode: %s' % data_subnode)
990
          if subscriber.getMediaType() == self.MEDIA_TYPE['TEXT_XML']:
991
            data_subnode = etree.XML(data_subnode, parser=parser)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
992
        else:
993
          if subscriber.getMediaType() != self.MEDIA_TYPE['TEXT_XML']:
994
            data_subnode = self.getDataText(action)
995
          else:
996
            data_subnode = self.getDataSubNode(action)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
997
        if action.xpath('local-name()') == 'Add':
Jean-Paul Smets's avatar
Jean-Paul Smets committed
998
          # Then store the xml of this new subobject
999
          reset = 0
1000
          if object is None:
1001 1002 1003
            add_data = conduit.addNode(xml=data_subnode,
                                       object=destination,
                                       object_id=object_id)
1004
            conflict_list.extend(add_data['conflict_list'])
1005 1006
            # Retrieve directly the object from addNode
            object = add_data['object']
Nicolas Delaby's avatar
Nicolas Delaby committed
1007 1008
            if object is not None:
              signature.setPath(object.getPhysicalPath())
1009
              signature.setObjectId(object.getId())
1010
          else:
1011
            reset = 1
1012 1013
            # Object was retrieve but need to be updated without recreated
            # usefull when an object is only deleted by workflow.
1014
            if data_subnode is not None:
1015 1016
              actual_xml = conduit.getXMLFromObjectWithId(object,
                           xml_mapping=domain.getXMLMapping(force=1))
1017 1018
              actual_xml = etree.XML(actual_xml, parser=parser)
              xml_string_gid = conduit.replaceIdFromXML(data_subnode, gid)
1019 1020 1021
              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
1022
            conflict_list.extend(conduit.updateNode(
1023 1024 1025 1026
                                        xml=data_subnode,
                                        object=object,
                                        previous_xml=signature.getXML(),
                                        force=force,
1027 1028
                                        simulate=simulate,
                                        reset=reset))
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1029 1030
            xml_object = conduit.getXMLFromObjectWithId(object,\
                         xml_mapping=domain.getXMLMapping()) 
1031
            signature.setTempXML(xml_object)
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1032
          if object is not None:
1033
            #LOG('applyActionList', DEBUG, 'addNode, found the object')
1034 1035 1036
            if reset:
              #After a reset we want copy the LAST XML view on Signature.
              #this implementation is not sufficient, need to be improved.
1037 1038
              if not isinstance(xml_object, str):
                xml_object = etree.tostring(xml_object, encoding='utf-8',
Nicolas Delaby's avatar
Nicolas Delaby committed
1039
                                            pretty_print=True)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1040 1041 1042
            else: 
              xml_object = conduit.getXMLFromObjectWithId(object,\
                           xml_mapping=domain.getXMLMapping()) 
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1043
            signature.setStatus(self.SYNCHRONIZED)
1044
            #signature.setId(object.getId())
1045
            signature.setPath(object.getPhysicalPath())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1046
            signature.setXML(xml_object)
Nicolas Delaby's avatar
Nicolas Delaby committed
1047 1048 1049 1050 1051
            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
1052
            cmd_id +=1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1053
        elif action.xpath('local-name()') == 'Replace':
1054
          #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % str(object))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1055
          if object is not None:
1056
            #LOG('applyActionList', DEBUG, 'object: %s will be updated...' % object.id)
1057
            signature = subscriber.getSignatureFromGid(gid)
1058
            #LOG('applyActionList', DEBUG, 'previous signature: %s' % str(signature))
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1059
            previous_xml = signature.getXML()
1060 1061 1062
            conflict_list += conduit.updateNode(
                                        xml=data_subnode,
                                        object=object,
1063
                                        previous_xml=previous_xml,
1064 1065
                                        force=force,
                                        simulate=simulate)
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1066 1067
            xml_object = conduit.getXMLFromObjectWithId(object,\
                         xml_mapping=domain.getXMLMapping())
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1068
            signature.setTempXML(xml_object)
1069
            if conflict_list:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1070 1071
              status_code = self.CONFLICT
              signature.setStatus(self.CONFLICT)
1072 1073
              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
1074
              signature.setPartialXML(data_subnode_string)
1075
            elif not simulate:
Jean-Paul Smets's avatar
Jean-Paul Smets committed
1076
              signature.setStatus(self.SYNCHRONIZED)
Nicolas Delaby's avatar
Nicolas Delaby committed
1077 1078 1079 1080 1081
            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
1082
            cmd_id +=1
1083
            if simulate:
Sebastien Robin's avatar
Sebastien Robin committed
1084
              # This means we are on the publisher side and we want to store
1085 1086
              # the xupdate from the subscriber and we also want to generate
              # the current xupdate from the last synchronization
1087
              data_subnode_string = etree.tostring(data_subnode, encoding='utf-8')
1088
              #LOG('applyActionList, subscriber_xupdate:', TRACE, data_subnode_string)
1089 1090
              signature.setSubscriberXupdate(data_subnode_string)

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

Nicolas Delaby's avatar
Nicolas Delaby committed
1126
    return (xml_confirmation_list, has_next_action, cmd_id)
1127

1128
  def applyStatusList(self, subscriber=None, remote_xml=None):
1129 1130 1131 1132
    """
    This read a list of status list (ie syncml confirmations).
    This method have to change status codes on signatures
    """
1133
    status_list = self.getSyncBodyStatusList(remote_xml)
1134 1135
    has_status_list = 0
    destination_waiting_more_data = 0
1136 1137 1138 1139 1140 1141 1142 1143
    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
1144 1145 1146 1147 1148
      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)
1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
      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())
1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187
    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

1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206
  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

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

    subscriber = domain # If we are the client, this is fine
1225
    simulate = 0 # used by applyActionList, should be 0 for client
1226
    if domain.domain_type == self.PUB:
1227
      simulate = 1
1228
      subscription_url = self.getSubscriptionUrlFromXML(xml_header)
1229
      subscriber = domain.getSubscriber(subscription_url)
1230

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

1263
    # First apply the list of status codes
Nicolas Delaby's avatar
Nicolas Delaby committed
1264
    (destination_waiting_more_data, has_status_list) = self.applyStatusList(
1265 1266
                                                      subscriber=subscriber,
                                                      remote_xml=remote_xml)
1267

1268
    alert_code = self.getAlertCodeFromXML(remote_xml)
Kevin Deldycke's avatar
Kevin Deldycke committed
1269
    # Import the conduit and get it
1270
    conduit = self.getConduitByName(subscriber.getConduit())
1271
    # Then apply the list of actions
Nicolas Delaby's avatar
Nicolas Delaby committed
1272
    (xml_confirmation_list, has_next_action, cmd_id) = self.applyActionList(
1273 1274 1275 1276 1277
                                          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
1278

1279
    xml = E.SyncML()
1280

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

    # syncml body
1295 1296
    sync_body = E.SyncBody()
    xml.append(sync_body)
1297

1298 1299 1300 1301 1302
    xml_status, cmd_id = self.SyncMLStatus(
                                    remote_xml,
                                    self.SUCCESS,
                                    cmd_id,
                                    subscriber.getNextAnchor(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1303 1304
                                    subscription=subscriber)
    sync_body.extend(xml_status)
1305

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

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

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

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

Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1449
    self.sync_finished = 0 
1450
    if domain.domain_type == self.PUB: # We always reply
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1451 1452 1453 1454
      # 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):
1455
        sync_body.append(E.Final())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1456 1457
        self.sync_finished = 1
      xml_string = etree.tostring(xml_tree, encoding='utf-8', pretty_print=True)
Nicolas Delaby's avatar
Nicolas Delaby committed
1458
      subscriber.setLastSentMessage(xml_string)
1459 1460 1461 1462
      self.sendResponse(
                from_url=domain.publication_url,
                to_url=subscriber.subscription_url,
                sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1463
                xml=xml_string,
1464 1465
                domain=domain,
                content_type=domain.getSyncContentType())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1466
      if self.sync_finished == 1:
Nicolas Delaby's avatar
Nicolas Delaby committed
1467
        LOG('this is the end of the synchronisation session from PUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1468 1469
        subscriber.setAuthenticated(False)
        domain.setAuthenticated(False)
1470 1471
      has_response = 1
    elif domain.domain_type == self.SUB:
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1472 1473 1474 1475
      # 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:
1476
        sync_body.append(E.Final())
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1477 1478 1479
        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
1480
        subscriber.setLastSentMessage(xml_string)
1481 1482 1483 1484
        self.sendResponse(
                  from_url=domain.subscription_url,
                  to_url=domain.publication_url,
                  sync_id=domain.getTitle(),
Nicolas Delaby's avatar
Nicolas Delaby committed
1485
                  xml=xml_string, domain=domain,
1486
                  content_type=domain.getSyncContentType())
1487
        has_response = 1
Danièle Vanbaelinghem's avatar
Danièle Vanbaelinghem committed
1488
      #When the receive the final element and the sub finished synchronization
Fabien Morin's avatar
Fabien Morin committed
1489
      else:
1490 1491
        if domain.isOneWayFromServer():
          self.deleteRemainObjectList(domain, subscriber)
Nicolas Delaby's avatar
Nicolas Delaby committed
1492
        LOG('this is the end of the synchronisation session from SUB !!!', INFO, domain.getId())
Fabien Morin's avatar
Fabien Morin committed
1493
        domain.setAuthenticated(False)
Nicolas Delaby's avatar
Nicolas Delaby committed
1494
    return {'has_response':has_response, 'xml':xml_string}
1495 1496 1497 1498 1499

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

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

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