/* Copyright (c) 2014, Inverse inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Inverse inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "SOGoActiveSyncDispatcher+Sync.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #include "iCalEvent+ActiveSync.h" #include "iCalToDo+ActiveSync.h" #include "NGDOMElement+ActiveSync.h" #include "NGVCard+ActiveSync.h" #include "NSCalendarDate+ActiveSync.h" #include "NSDate+ActiveSync.h" #include "NSData+ActiveSync.h" #include "NSString+ActiveSync.h" #include "SOGoActiveSyncConstants.h" #include "SOGoMailObject+ActiveSync.h" @implementation SOGoActiveSyncDispatcher (Sync) - (id) collectionFromId: (NSString *) theCollectionId type: (SOGoMicrosoftActiveSyncFolderType) theFolderType { id collection; collection = nil; switch (theFolderType) { case ActiveSyncContactFolder: { collection = [[context activeUser] personalContactsFolderInContext: context]; } break; case ActiveSyncEventFolder: case ActiveSyncTaskFolder: { collection = [[context activeUser] personalCalendarFolderInContext: context]; } break; case ActiveSyncMailFolder: default: { SOGoMailAccounts *accountsFolder; SOGoMailFolder *currentFolder; SOGoUserFolder *userFolder; userFolder = [[context activeUser] homeFolderInContext: context]; accountsFolder = [userFolder lookupName: @"Mail" inContext: context acquire: NO]; currentFolder = [accountsFolder lookupName: @"0" inContext: context acquire: NO]; collection = [currentFolder lookupName: [NSString stringWithFormat: @"folder%@", theCollectionId] inContext: context acquire: NO]; } } return collection; } // // // // // // // 1388757902 // vcard/personal // // 25 // // // 1 // 32768 // // // // // 16 // // // 1 // // // Goo Inc. // annie@broccoli.com // Broccoli, Annie // Annie // Broccoli // // // // // // // // - (void) processSyncAddCommand: (id ) theDocumentElement inCollection: (id) theCollection withType: (SOGoMicrosoftActiveSyncFolderType) theFolderType inBuffer: (NSMutableString *) theBuffer { NSMutableDictionary *allValues; NSString *clientId, *serverId; NSArray *additions; id anAddition, sogoObject, o; int i; additions = (id)[theDocumentElement getElementsByTagName: @"Add"]; if ([additions count]) { for (i = 0; i < [additions count]; i++) { anAddition = [additions objectAtIndex: i]; clientId = [[(id)[anAddition getElementsByTagName: @"ClientId"] lastObject] textValue]; allValues = [NSMutableDictionary dictionaryWithDictionary: [[(id)[anAddition getElementsByTagName: @"ApplicationData"] lastObject] applicationData]]; switch (theFolderType) { case ActiveSyncContactFolder: { serverId = [NSString stringWithFormat: @"%@.vcf", [theCollection globallyUniqueObjectId]]; sogoObject = [[SOGoContactGCSEntry alloc] initWithName: serverId inContainer: theCollection]; o = [sogoObject vCard]; } break; case ActiveSyncEventFolder: { serverId = [NSString stringWithFormat: @"%@.ics", [theCollection globallyUniqueObjectId]]; sogoObject = [[SOGoAppointmentObject alloc] initWithName: serverId inContainer: theCollection]; [allValues setObject: [[[context activeUser] userDefaults] timeZone] forKey: @"SOGoUserTimeZone"]; o = [sogoObject component: YES secure: NO]; } break; case ActiveSyncTaskFolder: { serverId = [NSString stringWithFormat: @"%@.ics", [theCollection globallyUniqueObjectId]]; sogoObject = [[SOGoTaskObject alloc] initWithName: serverId inContainer: theCollection]; [allValues setObject: [[[context activeUser] userDefaults] timeZone] forKey: @"SOGoUserTimeZone"]; o = [sogoObject component: YES secure: NO]; } break; case ActiveSyncMailFolder: default: { // FIXME continue; } } [o takeActiveSyncValues: allValues]; [sogoObject setIsNew: YES]; [sogoObject saveComponent: o]; // Everything is fine, lets generate our response [theBuffer appendString: @""]; [theBuffer appendFormat: @"%@", clientId]; [theBuffer appendFormat: @"%@", serverId]; [theBuffer appendFormat: @"%d", 1]; [theBuffer appendString: @""]; } } } // // // // // // // 1387546048 // vtodo/personal // // 25 // // // 1 // 32768 // // // // // 36C5-52B36280-1-27B38F40.ics // // // 1 // // // foobar1 // 1 // 0 // 0 // 0 // // // // // // // - (void) processSyncChangeCommand: (id ) theDocumentElement inCollection: (id) theCollection withType: (SOGoMicrosoftActiveSyncFolderType) theFolderType inBuffer: (NSMutableString *) theBuffer { NSDictionary *allChanges; NSString *serverId; NSArray *changes; id aChange, o, sogoObject; int i; changes = (id)[theDocumentElement getElementsByTagName: @"Change"]; if ([changes count]) { for (i = 0; i < [changes count]; i++) { aChange = [changes objectAtIndex: i]; serverId = [[(id)[aChange getElementsByTagName: @"ServerId"] lastObject] textValue]; allChanges = [[(id)[aChange getElementsByTagName: @"ApplicationData"] lastObject] applicationData]; // Fetch the object and apply the changes sogoObject = [theCollection lookupName: serverId inContext: context acquire: NO]; // Object was removed inbetween sync/commands? if ([sogoObject isKindOfClass: [NSException class]]) { // FIXME - return status == 8 continue; } switch (theFolderType) { case ActiveSyncContactFolder: { o = [sogoObject vCard]; [o takeActiveSyncValues: allChanges]; [sogoObject saveComponent: o]; } break; case ActiveSyncEventFolder: case ActiveSyncTaskFolder: { o = [sogoObject component: NO secure: NO]; [o takeActiveSyncValues: allChanges]; [sogoObject saveComponent: o]; } break; case ActiveSyncMailFolder: default: { [sogoObject takeActiveSyncValues: allChanges]; } } } } } // // // // // // // 1388764784 // vtodo/personal // // 25 // // // 1 // 32768 // // // // // 2CB5-52B36080-1-1C1D0240.ics // // // // // // - (void) processSyncDeleteCommand: (id ) theDocumentElement inCollection: (id) theCollection withType: (SOGoMicrosoftActiveSyncFolderType) theFolderType inBuffer: (NSMutableString *) theBuffer { NSArray *deletions; NSString *serverId; id aDelete, sogoObject; int i; deletions = (id)[theDocumentElement getElementsByTagName: @"Delete"]; if ([deletions count]) { for (i = 0; i < [deletions count]; i++) { aDelete = [deletions objectAtIndex: i]; serverId = [[(id)[aDelete getElementsByTagName: @"ServerId"] lastObject] textValue]; sogoObject = [theCollection lookupName: serverId inContext: context acquire: NO]; [sogoObject delete]; } } } // // // 91 // // - (void) processSyncFetchCommand: (id ) theDocumentElement inCollection: (id) theCollection withType: (SOGoMicrosoftActiveSyncFolderType) theFolderType inBuffer: (NSMutableString *) theBuffer { NSString *serverId; id o; serverId = [[(id)[theDocumentElement getElementsByTagName: @"ServerId"] lastObject] textValue]; o = [theCollection lookupName: serverId inContext: context acquire: NO]; // FIXME - error handling [theBuffer appendString: @""]; [theBuffer appendFormat: @"%@", serverId]; [theBuffer appendFormat: @"%d", 1]; [theBuffer appendString: @""]; [theBuffer appendString: [o activeSyncRepresentation]]; [theBuffer appendString: @""]; [theBuffer appendString: @""]; } // // The method handles // - (void) processSyncGetChanges: (id ) theDocumentElement inCollection: (id) theCollection withSyncKey: (NSString *) theSyncKey withFolderType: (SOGoMicrosoftActiveSyncFolderType) theFolderType withFilterType: (NSCalendarDate *) theFilterType inBuffer: (NSMutableString *) theBuffer { NSMutableString *s; int i; // // No changes in the collection - 2.2.2.19.1.1 Empty Sync Request. // We check this and we don't generate any commands if we don't have to. // if ([theSyncKey isEqualToString: [theCollection davCollectionTag]]) return; s = [NSMutableString string]; switch (theFolderType) { // Handle all the GCS components case ActiveSyncContactFolder: case ActiveSyncEventFolder: case ActiveSyncTaskFolder: { id sogoObject, componentObject; NSString *uid, *component_name; NSDictionary *component; NSArray *allComponents; BOOL updated; int deleted; if (theFolderType == ActiveSyncContactFolder) component_name = @"vcard"; else if (theFolderType == ActiveSyncEventFolder) component_name = @"vevent"; else component_name = @"vtodo"; allComponents = [theCollection syncTokenFieldsWithProperties: nil matchingSyncToken: theSyncKey fromDate: theFilterType]; for (i = 0; i < [allComponents count]; i++) { component = [allComponents objectAtIndex: i]; deleted = [[component objectForKey: @"c_deleted"] intValue]; if (!deleted && ![[component objectForKey: @"c_component"] isEqualToString: component_name]) continue; uid = [component objectForKey: @"c_name"]; if (deleted) { [s appendString: @""]; [s appendFormat: @"%@", uid]; [s appendString: @""]; } else { updated = YES; if ([[component objectForKey: @"c_creationdate"] intValue] > [theSyncKey intValue]) updated = NO; if (updated) [s appendString: @""]; else [s appendString: @""]; [s appendFormat: @"%@", uid]; [s appendString: @""]; sogoObject = [theCollection lookupName: uid inContext: context acquire: 0]; if (theFolderType == ActiveSyncContactFolder) componentObject = [sogoObject vCard]; else componentObject = [sogoObject component: NO secure: NO]; [s appendString: [componentObject activeSyncRepresentation]]; [s appendString: @""]; if (updated) [s appendString: @""]; else [s appendString: @""]; } } // for ... } break; case ActiveSyncMailFolder: default: { SOGoMailObject *mailObject; NSString *uid, *command; NSDictionary *aMessage; NSArray *allMessages; allMessages = [theCollection syncTokenFieldsWithProperties: nil matchingSyncToken: theSyncKey fromDate: theFilterType]; for (i = 0; i < [allMessages count]; i++) { aMessage = [allMessages objectAtIndex: i]; uid = [[[aMessage allKeys] lastObject] stringValue]; command = [[aMessage allValues] lastObject]; if ([command isEqualToString: @"deleted"]) { [s appendString: @""]; [s appendFormat: @"%@", uid]; [s appendString: @""]; } else { if ([command isEqualToString: @"added"]) [s appendString: @""]; else [s appendString: @""]; mailObject = [theCollection lookupName: uid inContext: context acquire: 0]; [s appendFormat: @"%@", uid]; [s appendString: @""]; [s appendString: [mailObject activeSyncRepresentation]]; [s appendString: @""]; if ([command isEqualToString: @"added"]) [s appendString: @""]; else [s appendString: @""]; } } } break; } // switch (folderType) ... if ([s length]) { [theBuffer appendString: @""]; [theBuffer appendString: s]; [theBuffer appendString: @""]; } } // // We have something like this: // // // // 91 // // // - (void) processSyncCommands: (id ) theDocumentElement inCollection: (id) theCollection withType: (SOGoMicrosoftActiveSyncFolderType) theFolderType inBuffer: (NSMutableString *) theBuffer processed: (BOOL *) processed { id aCommandDetails; id aCommand, element; NSArray *allCommands; int i, j; allCommands = (id)[theDocumentElement getElementsByTagName: @"Commands"]; for (i = 0; i < [allCommands count]; i++) { aCommand = [allCommands objectAtIndex: i]; aCommandDetails = [aCommand childNodes]; for (j = 0; j < [(id)aCommandDetails count]; j++) { element = [aCommandDetails objectAtIndex: j]; if ([element nodeType] == DOM_ELEMENT_NODE) { if ([[element tagName] isEqualToString: @"Add"]) { // Add [self processSyncAddCommand: aCommand inCollection: theCollection withType: theFolderType inBuffer: theBuffer]; *processed = YES; } else if ([[element tagName] isEqualToString: @"Change"]) { // Change [self processSyncChangeCommand: aCommand inCollection: theCollection withType: theFolderType inBuffer: theBuffer]; *processed = YES; } else if ([[element tagName] isEqualToString: @"Delete"]) { // Delete [self processSyncDeleteCommand: aCommand inCollection: theCollection withType: theFolderType inBuffer: theBuffer]; } else if ([[element tagName] isEqualToString: @"Fetch"]) { // Fetch [self processSyncFetchCommand: aCommand inCollection: theCollection withType: theFolderType inBuffer: theBuffer]; *processed = YES; } } } } } // // // - (void) processSyncCollection: (id ) theDocumentElement inBuffer: (NSMutableString *) theBuffer { NSString *collectionId, *realCollectionId, *syncKey, *davCollectionTag, *bodyPreferenceType; SOGoMicrosoftActiveSyncFolderType folderType; id collection, value; BOOL getChanges, first_sync; collectionId = [[(id)[theDocumentElement getElementsByTagName: @"CollectionId"] lastObject] textValue]; realCollectionId = [collectionId realCollectionIdWithFolderType: &folderType]; collection = [self collectionFromId: realCollectionId type: folderType]; syncKey = [[(id)[theDocumentElement getElementsByTagName: @"SyncKey"] lastObject] textValue]; davCollectionTag = [collection davCollectionTag]; // From the documention, if GetChanges is missing, we must assume it's a YES. // See http://msdn.microsoft.com/en-us/library/gg675447(v=exchg.80).aspx for all details. value = [theDocumentElement getElementsByTagName: @"GetChanges"]; getChanges = YES; if ([value count] && [[[value lastObject] textValue] length]) getChanges = [[[value lastObject] textValue] boolValue]; first_sync = NO; if ([syncKey isEqualToString: @"0"]) { davCollectionTag = @"-1"; first_sync = YES; } // We check our sync preferences and we stash them bodyPreferenceType = [[(id)[[(id)[theDocumentElement getElementsByTagName: @"BodyPreference"] lastObject] getElementsByTagName: @"Type"] lastObject] textValue]; if (!bodyPreferenceType) bodyPreferenceType = @"1"; [context setObject: bodyPreferenceType forKey: @"BodyPreferenceType"]; [theBuffer appendString: @""]; if (folderType == ActiveSyncMailFolder) [theBuffer appendString: @"Email"]; else if (folderType == ActiveSyncContactFolder) [theBuffer appendString: @"Contacts"]; else if (folderType == ActiveSyncEventFolder) [theBuffer appendString: @"Calendar"]; else if (folderType == ActiveSyncTaskFolder) [theBuffer appendString: @"Tasks"]; [theBuffer appendFormat: @"%@", davCollectionTag]; [theBuffer appendFormat: @"%@", collectionId]; [theBuffer appendFormat: @"%d", 1]; // We generate the commands, if any, for the response. We might also have // generated some in processSyncCommand:inResponse: as we could have // received a Fetch command if (getChanges && !first_sync) { [self processSyncGetChanges: theDocumentElement inCollection: collection withSyncKey: syncKey withFolderType: folderType withFilterType: [NSCalendarDate dateFromFilterType: [[(id)[theDocumentElement getElementsByTagName: @"FilterType"] lastObject] textValue]] inBuffer: theBuffer]; } // // We process the commands from the request // if (!first_sync) { NSMutableString *s; BOOL processed; s = [NSMutableString string]; processed = NO; [self processSyncCommands: theDocumentElement inCollection: collection withType: folderType inBuffer: s processed: &processed]; if (processed) [theBuffer appendFormat: @"%@", s]; else [theBuffer appendString: s]; } [theBuffer appendString: @""]; } // // Initial folder sync: // // // // // // // 0 // folderINBOX // // // // // // Following this will be a GetItemEstimate call. Following our response to the GetItemEstimate, we'll // have a new Sync call like this: // // // // // // // 1 // folderINBOX // 1 // // 50 // // 5 -- http://msdn.microsoft.com/en-us/library/gg709713(v=exchg.80).aspx // -- http://msdn.microsoft.com/en-us/library/ee218197(v=exchg.80).aspx // 2 -- // 51200 // // // 4 // // // // // // // // // When adding a new task, we might have something like this: // // // // // // // 1 // personal // // -- http://msdn.microsoft.com/en-us/library/gg675447(v=exchg.80).aspx // 5 -- http://msdn.microsoft.com/en-us/library/gg650865(v=exchg.80).aspx // // -- http://msdn.microsoft.com/en-us/library/ee218197(v=exchg.80).aspx // 1 // 400000 // // // // // new_task_1386614771261 // // // 1 // 6 // tomate // // test 1 // 1 // 2013-12-09T19:00:00.000Z // 0 // 0 // 2013-12-09T19:00:00.000Z // // // // // // // // The algorithm here is pretty simple: // // 1. extract the list of collections // 2. for each collection // 2.1. extract the metadata (id, synckey, etc.) // 2.2. extract the list of commands // 2.3. for each command // 2.3.1 process the command (add/change/delete/fetch) // 2.3.2 build a response during the processsing // // - (void) processSync: (id ) theDocumentElement inResponse: (WOResponse *) theResponse { id aCollection; NSArray *allCollections; NSMutableString *s; NSData *d; int i; // We initialize our output buffer s = [NSMutableString string]; [s appendString: @""]; [s appendString: @""]; [s appendString: @""]; allCollections = (id)[theDocumentElement getElementsByTagName: @"Collection"]; for (i = 0; i < [allCollections count]; i++) { aCollection = [allCollections objectAtIndex: i]; [self processSyncCollection: aCollection inBuffer: s]; } [s appendString: @""]; d = [[s dataUsingEncoding: NSUTF8StringEncoding] xml2wbxml]; [theResponse setContent: d]; } @end