############################################################################## # # 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. # ############################################################################## from Globals import PersistentMapping from time import gmtime, strftime # for anchors from SyncCode import SyncCode from AccessControl import ClassSecurityInfo from Products.CMFCore.utils import getToolByName from Acquisition import Implicit, aq_base from Products.ERP5Type.Core.Folder import Folder from Products.ERP5Type.Base import Base from Products.ERP5Type import Permissions from Products.ERP5Type import PropertySheet from DateTime import DateTime from zLOG import LOG, DEBUG, INFO import md5 from base64 import b64encode, b64decode, b16encode, b16decode def addSubscription( self, id, title='', REQUEST=None ): """ Add a new Subscription """ o = Subscription(id, '', '', '', '', '', '') self._setObject(id, o) if REQUEST is not None: return self.manage_main(self, REQUEST, update_menu=1) return o #class Subscription(SyncCode, Implicit): #class Subscription(Folder, SyncCode, Implicit, Folder, Impli): from XMLSyncUtils import XMLSyncUtils class Subscription(Folder, XMLSyncUtils): """ Subscription hold the definition of a master ODB from/to which a selection of objects will be synchronised Subscription defined by:: publication_url -- a URI to a publication subscription_url -- URL of ourselves destination_path -- the place where objects are stored query -- a query which defines a local set of documents which are going to be synchronised xml_mapping -- a PageTemplate to map documents to XML gpg_key -- the name of a gpg key to use Subscription also holds private data to manage the synchronisation. We choose to keep an MD5 value for all documents which belong to the synchronisation process:: signatures -- a dictionnary which contains the signature of documents at the time they were synchronized session_id -- it defines the id of the session with the server. last_anchor - it defines the id of the last synchronisation next_anchor - it defines the id of the current synchronisation """ meta_type = 'ERP5 Subscription' portal_type = 'SyncML Subscription' # may be useful in the future... isPortalContent = 1 isRADContent = 1 icon = None isIndexable = 0 user = None # Declarative properties property_sheets = ( PropertySheet.Base , PropertySheet.SimpleItem ) allowed_types = ( 'Signatures',) # Declarative constructors constructors = (addSubscription,) # Declarative security security = ClassSecurityInfo() security.declareProtected(Permissions.ManagePortal, 'manage_editProperties', 'manage_changeProperties', 'manage_propertiesForm', ) # Constructor def __init__(self, id, title, publication_url, subscription_url, destination_path, source_uri, target_uri, query, xml_mapping, conduit, gpg_key, id_generator, media_type, login, password, activity_enabled, alert_code, synchronize_with_erp5_sites, sync_content_type): """ We need to create a dictionnary of signatures of documents which belong to the synchronisation process """ self.id = id self.setAlertCode(alert_code) self.setActivityEnabled(activity_enabled) self.publication_url = (publication_url) self.subscription_url = str(subscription_url) self.destination_path = str(destination_path) self.setSourceURI(source_uri) self.setTargetURI(target_uri) self.setQuery(query) self.setXMLMapping(xml_mapping) self.anchor = None self.session_id = 0 #self.signatures = PersistentMapping() self.last_anchor = '00000000T000000Z' self.next_anchor = '00000000T000000Z' self.setMediaType(media_type) self.login = login self.password = password self.domain_type = self.SUB self.gpg_key = gpg_key self.setSynchronizationIdGenerator(id_generator) self.setConduit(conduit) Folder.__init__(self, id) self.title = title self.setSyncContentType(sync_content_type) self.setSynchronizeWithERP5Sites(synchronize_with_erp5_sites) def getAlertCodeList(self): return self.CODE_LIST def getAlertCode(self): return getattr(self, 'alert_code', 200) def setAlertCode(self, value): self.alert_code = int(value) def isOneWayFromServer(self): return self.getDomainType() == self.SUB and \ self.getAlertCode() == self.ONE_WAY_FROM_SERVER def getActivityEnabled(self): """ return true if we are using activity, false otherwise """ return getattr(self, 'activity_enabled', None) def setActivityEnabled(self, activity_enabled): """ set if we are using activity or not """ self.activity_enabled = activity_enabled def getTitle(self): """ getter for title """ return getattr(self, 'title', None) def setTitle(self, value): """ setter for title """ self.title = value def setSourceURI(self, value): """ setter for source_uri """ self.source_uri = value def getSourceURI(self): """ getter for the source_uri (the local path of the subscription data base) """ return getattr(self, 'source_uri', None) def setTargetURI(self, value): """ setter for target_uri """ self.target_uri = value def getTargetURI(self): """ getter for the target_uri (the distant Publication data base we want to synchronize with) """ return getattr(self, 'target_uri', None) def setSyncContentType(self, sync_content_type): """ content type used by the subscriber """ self.sync_content_type = sync_content_type # the varible name is sync_content_type instead of content_type because # content_type seems to be a function name already used def getSyncContentType(self): """ getter of the subscriber sync_content_type """ return getattr(self, 'sync_content_type', 'application/vnd.syncml+xml') def getSynchronizationType(self, default=None): """ """ # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX # XXX for debugging only, to be removed #dict_sign = {} #for o in self.getSignatureList(): #dict_sign[o.getId()] = o.getStatus() # LOG('getSignature', DEBUG, 'signatures_status: %s' % str(dict_sign)) # XXXXXXXXXXXXXXXXXXXXXXXXXXXXX code = self.SLOW_SYNC if len(self.getSignatureList()[:1]) > 0: code = self.getAlertCode() if default is not None: code = default #LOG('Subscription', DEBUG, 'getSynchronizationType: %s' % code) return code def setXMLMapping(self, value): """ this the name of the method used in order to set the xml """ if value == '': value = None self.xml_mapping = value def setSynchronizeWithERP5Sites(self, synchronize_with_erp5_sites): """ if the synchronisation is made with another ERP5 site, synchronize_with_erp5_sites is True, False in other case XXX in the future, the method used to sendHttpResponse will be the same in all cases, so this method will be useless """ self.synchronize_with_erp5_sites = synchronize_with_erp5_sites def getSynchronizeWithERP5Sites(self): """ return True if the synchronisation is between two erp5 sites """ return getattr(self, 'synchronize_with_erp5_sites', True) def checkCorrectRemoteSessionId(self, session_id): """ We will see if the last session id was the same wich means that the same message was sent again return True if the session id was not seen, False if already seen """ last_session_id = getattr(self, 'last_session_id', None) if last_session_id == session_id: return False self.last_session_id = session_id return True def checkCorrectRemoteMessageId(self, message_id): """ We will see if the last message id was the same wich means that the same message was sent again return True if the message id was not seen, False if already seen """ last_message_id = getattr(self, 'last_message_id', None) if last_message_id == message_id: return False self.last_message_id = message_id return True def initLastMessageId(self, last_message_id=0): """ set the last message id to 0 """ self.last_message_id = last_message_id def getLastSentMessage(self): """ This is the getter for the last message we have sent """ return getattr(self, 'last_sent_message', '') def setLastSentMessage(self, xml): """ This is the setter for the last message we have sent """ self.last_sent_message = xml def getDomainType(self): """ return the ID """ return self.domain_type def getId(self): """ return the ID """ return self.id def setId(self, id): """ set the ID """ self.id = id def setConduit(self, value): """ set the Conduit """ self.conduit = value def getConduit(self): """ get the Conduit """ return getattr(self, 'conduit', None) def getQuery(self): """ return the query """ return self.query def getGPGKey(self): """ return the gnupg key name """ return getattr(self, 'gpg_key', '') def setGPGKey(self, value): """ setter for the gnupg key name """ self.gpg_key = value def setQuery(self, query): """ set the query """ if query == '': query = None self.query = query def getPublicationUrl(self): """ return the publication url """ return self.publication_url def getLocalUrl(self): """ return the publication url """ return self.publication_url def setPublicationUrl(self, publication_url): """ set the publication url """ self.publication_url = publication_url def getXMLMapping(self, force=0): """ return the xml mapping """ if self.isOneWayFromServer() and force == 0: return None xml_mapping = getattr(self, 'xml_mapping', None) return xml_mapping def getMediaType(self): """ This method return the type of media used in this session, for example, it could be "text/vcard" or "xml/text",... """ return getattr(self, 'media_type', self.MEDIA_TYPE['TEXT_XML']) def setMediaType(self, media_type): """ set the type of media used """ if media_type in (None, ''): media_type = self.MEDIA_TYPE['TEXT_XML'] self.media_type = media_type def getLogin(self): """ This method return the login of this subscription """ return getattr(self, 'login', '') def setLogin(self, new_login): """ set the login at new_login """ self.login = new_login def getPassword(self): """ This method return the password of this subscription """ return getattr(self, 'password', '') def setPassword(self, new_password): """ set the password at new_password """ self.password = new_password def getZopeUser(self): """ This method return the zope user who begin the synchronization session """ return getattr(self, 'zope_user_name', None) def setZopeUser(self, user_name): """ This method set the zope user_name """ self.zope_user_name = user_name def getAuthenticationFormat(self): """ return the format of authentication """ return getattr(self, 'authentication_format', 'b64') def getAuthenticationType(self): """ return the type of authentication """ return getattr(self, 'authentication_type', 'syncml:auth-basic') def setAuthenticationFormat(self, authentication_format): """ set the format of authentication """ if authentication_format in (None, ''): self.authentication_format = 'b64' else: self.authentication_format = authentication_format def setAuthenticationType(self, authentication_type): """ set the type of authentication """ if authentication_type in (None, ''): self.authentication_type = 'syncml:auth-basic' else: self.authentication_type = authentication_type def getGidFromObject(self, object): """ Returns the object gid """ o_base = aq_base(object) o_gid = None conduit_name = self.getConduit() conduit = self.getConduitByName(conduit_name) gid_gen = getattr(conduit, 'getGidFromObject', None) if callable(gid_gen): o_gid = gid_gen(object) else: raise ValueError, "The conduit "+conduit_name+"seems to not have a \ getGidFromObject method and it must" o_gid = b16encode(o_gid) #LOG('getGidFromObject returning', DEBUG, o_gid) return o_gid def getObjectFromGid(self, gid): """ This tries to get the object with the given gid This uses the query if it exist """ if len(gid)%2 != 0: #something encode in base 16 is always a even number of number #if not, b16decode will failed return None signature = self.getSignatureFromGid(gid) # First look if we do already have the mapping between # the id and the gid destination = self.getDestination() if signature is not None and signature.getPath() is not None: o = None try: o = destination.getPortalObject().restrictedTraverse(signature.getPath()) except (AttributeError, KeyError, TypeError): pass o_id = signature.getObjectId() #try with id param too, because gid is not catalogged object_list = self.getObjectList(gid=b16decode(gid), id=o_id) if o is not None and o in object_list: return o #LOG('entering in the slow loop of getObjectFromGid !!!',0,'') object_list = self.getObjectList(gid=b16decode(gid)) for o in object_list: o_gid = self.getGidFromObject(o) if o_gid == gid: return o #LOG('getObjectFromGid', DEBUG, 'returning None') return None def getObjectFromId(self, id): """ return the object corresponding to the id """ object_list = self.getObjectList(id=id) o = None for object in object_list: if object.getId() == id: o = object break return o def getObjectList(self, **kw): """ This returns the list of sub-object corresponding to the query """ destination = self.getDestinationPath() query = self.getQuery() if query is not None and isinstance(query, str): count = query.count("/") if count > 0: # There is a query path different than destination path change # destination path for this query cut_query = query.split('/') if destination.endswith('/'): destination = "%s%s" % (destination, "/".join(cut_query[:count])) else: destination = "%s/s" % (destination, "/".join(cut_query[:count])) query = cut_query[count] query_list = [] destination = self.unrestrictedTraverse(destination) query_method = getattr(destination, query, None) if query_method is not None: query_list = query_method(**kw) else: raise KeyError, 'This Subscriber %s provide no Query: %s'\ % (self.getTitle(), query) elif callable(query): # used in the test destination = self.unrestrictedTraverse(destination) query_list = query(destination) else: raise KeyError, 'This Subscriber %s provide no Query with id: %s'\ % (self.getTitle(), query) return [x for x in query_list if not getattr(x, '_conflict_resolution', False)] def generateNewIdWithGenerator(self, object=None, gid=None): """ This tries to generate a new Id """ id_generator = self.getSynchronizationIdGenerator() if id_generator is not None: o_base = aq_base(object) new_id = None if callable(id_generator): new_id = id_generator(object, gid=gid) elif getattr(o_base, id_generator, None) is not None: generator = getattr(object, id_generator) new_id = generator() else: # This is probably a python script generator = getattr(object, id_generator) new_id = generator(object=object, gid=gid) #LOG('generateNewIdWithGenerator, new_id: ', DEBUG, new_id) return new_id return None def setSynchronizationIdGenerator(self, method): """ This set the method name wich allows to generate a new id """ if method in ('', 'None'): method = None self.synchronization_id_generator = method def getSynchronizationIdGenerator(self): """ This get the method name wich allows to generate a new id """ return getattr(self, 'synchronization_id_generator', None) def getSubscriptionUrl(self): """ return the subscription url """ return self.subscription_url def setSubscriptionUrl(self, subscription_url): """ set the subscription url """ self.subscription_url = subscription_url def getDestinationPath(self): """ return the destination path """ return self.destination_path def getDestination(self): """ return the destination object itself """ return self.unrestrictedTraverse(self.getDestinationPath()) def setDestinationPath(self, destination_path): """ set the destination path """ self.destination_path = destination_path def getSubscription(self): """ return the current subscription """ return self def setSessionId(self, session_id): """ set the session id """ self.session_id = session_id def getSessionId(self): """ return the session id """ #self.session_id += 1 #to be commented return self.session_id def incrementSessionId(self): """ increment and return the session id """ self.session_id += 1 self.resetMessageId() # for a new session, the message Id must be reset return self.session_id def incrementMessageId(self): """ return the message id """ value = getattr(self, 'message_id', 0) self.message_id = value +1 return self.message_id def getMessageId(self): """ increment and return the message id """ return self.message_id def resetMessageId(self): """ set the message id to 0 """ self.message_id = 0 def setMessageId(self, message_id): """ set the message id to message_id """ self.message_id = message_id def getLastAnchor(self): """ return the id of the last synchronisation """ return self.last_anchor def getNextAnchor(self): """ return the id of the current synchronisation """ return self.next_anchor def setLastAnchor(self, last_anchor): """ set the value last anchor """ self.last_anchor = last_anchor def setNextAnchor(self, next_anchor): """ set the value next anchor """ # We store the old next anchor as the new last one self.last_anchor = self.next_anchor self.next_anchor = next_anchor def NewAnchor(self): """ set a new anchor """ self.last_anchor = self.next_anchor self.next_anchor = strftime("%Y%m%dT%H%M%SZ", gmtime()) def resetAnchors(self): """ reset both last and next anchors """ self.last_anchor = self.NULL_ANCHOR self.next_anchor = self.NULL_ANCHOR def addSignature(self, signature): """ add a Signature to the subscription """ if self.getSignatureFromGid(signature.getGid()) is not None: self.delSignature(signature.getGid()) self._setObject(signature.getGid(), aq_base(signature)) def delSignature(self, gid): """ del a Signature of the subscription """ self._delObject(gid) def getSignatureFromObjectId(self, id): """ return the signature corresponding to the id """ o = None # XXX very slow for signature in self.getSignatureList(): if id == signature.getObjectId(): o = signature break return o def getSignatureFromGid(self, gid): """ return the signature corresponding to the gid """ return getattr(self, gid, None) def getSignatureFromRid(self, rid): """ return the signature corresponding to the rid """ o = None # XXX very slow for signature in self.getSignatureList(): if rid == signature.getRid(): o = signature break return o def getGidList(self): """ Returns the list of gids from signature """ return [id for id in self.getObjectIds()] def getSignatureList(self): """ Returns the list of Signatures """ return self.objectValues() def hasSignature(self, gid): """ Check if there's a signature with this uid """ return self.getSignatureFromGid(gid) is not None def resetAllSignatures(self): """ Reset all signatures in activities """ object_id_list = [id for id in self.getObjectIds()] object_list_len = len(object_id_list) for i in xrange(0, object_list_len, self.MAX_OBJECTS): current_id_list = object_id_list[i:i+self.MAX_OBJECTS] self.activate(activity='SQLQueue', tag=self.getId(), priority=self.PRIORITY).manage_delObjects(current_id_list) def getConflictList(self): """ Return the list of all conflicts from all signatures """ conflict_list = [] for signature in self.getSignatureList(): conflict_list.extend(signature.getConflictList()) return conflict_list def getRemainingObjectPathList(self): """ We should now wich objects should still synchronize """ return getattr(self, 'remaining_object_path_list', None) def setRemainingObjectPathList(self, value): """ We should now wich objects should still synchronize """ setattr(self, 'remaining_object_path_list', value) def removeRemainingObjectPath(self, object_path): """ We should now wich objects should still synchronize """ remaining_object_list = self.getRemainingObjectPathList() if remaining_object_list is not None: new_list = [] new_list.extend(remaining_object_list) while object_path in new_list: new_list.remove(object_path) self.setRemainingObjectPathList(new_list) def startSynchronization(self): """ Set the status of every object as NOT_SYNCHRONIZED """ for s in self.getSignatureList(): # Change the status only if we are not in a conflict mode if s.getStatus() not in (self.CONFLICT, self.PUB_CONFLICT_MERGE, self.PUB_CONFLICT_CLIENT_WIN): s.setStatus(self.NOT_SYNCHRONIZED) s.setPartialXML(None) s.setTempXML(None) self.setRemainingObjectPathList(None) def isAuthenticated(self): """ return True if the subscriber is authenticated for this session, False in other case """ return getattr(self, 'is_authenticated', None) def setAuthenticated(self, value): """ set at True or False the value of is_authenticated is the subscriber is authenticated for this session or not """ self.is_authenticated = value def encode(self, format, string_to_encode): """ return the string_to_encode encoded with format format """ if format in ('', None): return string_to_encode if format == 'b64': return b64encode(string_to_encode) #elif format is .... put here the other formats else:#if there is no format corresponding with format, raise an error LOG('encode : unknown or not implemented format : ', INFO, format) raise ValueError, "Sorry, the server ask for the format %s but \ it's unknow or not implemented" % format def decode(self, format, string_to_decode): """ return the string_to_decode decoded with format format """ string_to_decode = string_to_decode.encode('utf-8') if format in ('', None): return string_to_decode if format == 'b64': return b64decode(string_to_decode) #elif format is .... put here the other formats else:#if there is no format corresponding with format, raise an error LOG('decode : unknown or not implemented format :', INFO, format) raise ValueError, "Sorry, the format %s is unknow or \ not implemented" % format def isDecodeEncodeTheSame(self, string_encoded, string_decoded, format): """ return True if the string_encoded is equal to string_decoded encoded in format """ return self.encode(format, string_decoded) == string_encoded def setUser(self, user): """ save the user logged in to log him on each transaction """ self.user = user def getUser(self): """ retrun the user logged in """ return getattr(self, 'user', None) 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 #XXX backwards compatibility from Products.ERP5SyncML import Signature, Conflict Signature = Signature.Signature Conflict = Conflict.Conflict