/* SOGoContactSourceFolder.m - this file is part of SOGo * * Copyright (C) 2006-2016 Inverse inc. * * This file 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, or (at your option) * any later version. * * This file 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "NSArray+Contacts.h" #import "SOGoContactFolders.h" #import "SOGoContactGCSFolder.h" #import "SOGoContactLDIFEntry.h" #import "SOGoContactSourceFolder.h" @class WOContext; @implementation SOGoContactSourceFolder + (id) folderWithName: (NSString *) aName andDisplayName: (NSString *) aDisplayName inContainer: (id) aContainer { id folder; folder = [[self alloc] initWithName: aName andDisplayName: aDisplayName inContainer: aContainer]; [folder autorelease]; return folder; } - (id) init { if ((self = [super init])) { childRecords = [NSMutableDictionary new]; source = nil; } return self; } - (id) initWithName: (NSString *) newName andDisplayName: (NSString *) newDisplayName inContainer: (id) newContainer { if ((self = [self initWithName: newName inContainer: newContainer])) { if (![newDisplayName length]) newDisplayName = newName; ASSIGN (displayName, (NSMutableString *)newDisplayName); } return self; } - (void) dealloc { [childRecords release]; [source release]; [super dealloc]; } - (void) setSource: (id ) newSource { ASSIGN (source, newSource); } - (id ) source { return source; } - (void) setIsPersonalSource: (BOOL) isPersonal { isPersonalSource = isPersonal; } - (BOOL) isPersonalSource { return isPersonalSource; } - (NSArray *) searchFields { return [source searchFields]; } - (BOOL) listRequiresDot { return [source listRequiresDot]; } - (NSString *) groupDavResourceType { return @"vcard-collection"; } - (NSArray *) davResourceType { NSMutableArray *resourceType; NSArray *type; resourceType = [NSMutableArray arrayWithArray: [super davResourceType]]; type = [NSArray arrayWithObjects: @"addressbook", XMLNS_CARDDAV, nil]; [resourceType addObject: type]; type = [NSArray arrayWithObjects: @"directory", XMLNS_CARDDAV, nil]; [resourceType addObject: type]; return resourceType; } - (id) lookupName: (NSString *) objectName inContext: (WOContext *) lookupContext acquire: (BOOL) acquire { NSDictionary *ldifEntry; SOGoContactLDIFEntry *obj; NSString *url; BOOL isNew = NO; NSArray *baseClasses; /* first check attributes directly bound to the application */ obj = [super lookupName: objectName inContext: lookupContext acquire: NO]; if (!obj) { ldifEntry = [childRecords objectForKey: objectName]; if (!ldifEntry) { ldifEntry = [source lookupContactEntry: objectName inDomain: [[context activeUser] domain]]; if (ldifEntry) [childRecords setObject: ldifEntry forKey: objectName]; else if ([self isValidContentName: objectName]) { url = [[[lookupContext request] uri] urlWithoutParameters]; if ([url hasSuffix: @"AsContact"]) { baseClasses = [NSArray arrayWithObjects: @"inetorgperson", @"mozillaabpersonalpha", nil]; ldifEntry = [NSMutableDictionary dictionaryWithObject: baseClasses forKey: @"objectclass"]; isNew = YES; } } } if (ldifEntry) { obj = [SOGoContactLDIFEntry contactEntryWithName: objectName withLDIFEntry: ldifEntry inContainer: self]; if (isNew) [obj setIsNew: YES]; } else obj = [NSException exceptionWithHTTPStatus: 404]; } return obj; } - (NSArray *) toOneRelationshipKeys { NSString *userDomain; userDomain = [[context activeUser] domain]; return [source allEntryIDsVisibleFromDomain: userDomain]; } - (NSException *) saveLDIFEntry: (SOGoContactLDIFEntry *) ldifEntry { return (([ldifEntry isNew]) ? [source addContactEntry: [ldifEntry ldifRecord] withID: [ldifEntry nameInContainer]] : [source updateContactEntry: [ldifEntry ldifRecord]]); } - (NSException *) deleteLDIFEntry: (SOGoContactLDIFEntry *) ldifEntry { return [source removeContactEntryWithID: [ldifEntry nameInContainer]]; } /** * Normalize keys of dictionary representing a contact. * @param oldRecord a dictionary with pairs from the source folder (LDAP or SQL) * @see [SOGoContactGCSFolder _fixupContactRecord] */ - (NSDictionary *) _flattenedRecord: (NSDictionary *) oldRecord { NSMutableDictionary *newRecord; id data; NSObject *recordSource; newRecord = [NSMutableDictionary dictionaryWithCapacity: 8]; [newRecord setObject: [oldRecord objectForKey: @"c_uid"] forKey: @"c_uid"]; // c_name => id [newRecord setObject: [oldRecord objectForKey: @"c_name"] forKey: @"c_name"]; [newRecord setObject: [oldRecord objectForKey: @"c_name"] forKey: @"id"]; // displayname || c_cn => fn data = [oldRecord objectForKey: @"displayname"]; if (!data) data = [oldRecord objectForKey: @"c_cn"]; if (data) [newRecord setObject: data forKey: @"fn"]; else data = @""; [newRecord setObject: data forKey: @"c_cn"]; // sn => c_sn data = [oldRecord objectForKey: @"sn"]; if (!data) data = @""; [newRecord setObject: data forKey: @"c_sn"]; // for sorting // givenname => c_givenname data = [oldRecord objectForKey: @"givenname"]; if (!data) data = @""; [newRecord setObject: data forKey: @"c_givenname"]; if ([[SOGoSystemDefaults sharedSystemDefaults] enableDomainBasedUID]) { data = [oldRecord objectForKey: @"c_domain"]; if (data) [newRecord setObject: data forKey: @"c_domain"]; } // mail => emails[] data = [oldRecord objectForKey: @"c_emails"]; if (!data) data = [oldRecord objectForKey: @"mail"]; if (data) { if ([data isKindOfClass: [NSArray class]]) { if ([data count] > 0) { NSEnumerator *emails; NSMutableArray *recordEmails; NSString *email; emails = [(NSArray *)data objectEnumerator]; recordEmails = [NSMutableArray arrayWithCapacity: [data count]]; while ((email = [emails nextObject])) { [recordEmails addObject: [NSDictionary dictionaryWithObject: email forKey: @"value"]]; } [newRecord setObject: recordEmails forKey: @"emails"]; } } else if (data) { NSDictionary *email; email = [NSDictionary dictionaryWithObjectsAndKeys: @"pref", @"type", data, @"value", nil]; [newRecord setObject: [NSArray arrayWithObject: email] forKey: @"emails"]; } else data = @""; } else data = @""; [newRecord setObject: data forKey: @"c_mail"]; data = [oldRecord objectForKey: @"mozillanickname"]; if (![data length]) data = [oldRecord objectForKey: @"nsaimid"]; if (![data length]) data = [oldRecord objectForKey: @"nscpaimscreenname"]; if (![data length]) data = @""; [newRecord setObject: data forKey: @"c_screenname"]; // for sorting // o => org data = [oldRecord objectForKey: @"o"]; if (data) [newRecord setObject: data forKey: @"org"]; else data = @""; [newRecord setObject: data forKey: @"c_o"]; // for sorting // telephonenumber || cellphone || homephone => phones[] & c_telephonenumber data = [oldRecord objectForKey: @"telephonenumber"]; if (![data length]) data = [oldRecord objectForKey: @"cellphone"]; if (![data length]) data = [oldRecord objectForKey: @"homephone"]; if (data) { NSDictionary *phonenumber; phonenumber = [NSDictionary dictionaryWithObjectsAndKeys: @"pref", @"type", data, @"value", nil]; [newRecord setObject: [NSArray arrayWithObject: phonenumber] forKey: @"phones"]; } else data = @""; [newRecord setObject: data forKey: @"c_telephonenumber"]; // for sorting // Custom attribute for group lookups. See LDAPSource.m. data = [oldRecord objectForKey: @"isGroup"]; if (data) { [newRecord setObject: data forKey: @"isGroup"]; [newRecord setObject: @"vlist" forKey: @"c_component"]; } else { [newRecord setObject: @"vcard" forKey: @"c_component"]; } // Custom attribute for resource lookups. See LDAPSource.m. data = [oldRecord objectForKey: @"isResource"]; if (data) { [newRecord setObject: data forKey: @"isResource"]; } // c_info => note + contactInfo data = [oldRecord objectForKey: @"c_info"]; if ([data length] > 0) { [newRecord setObject: data forKey: @"note"]; [newRecord setObject: data forKey: @"contactInfo"]; } // photo => hasPhoto data = [oldRecord objectForKey: @"photo"]; [newRecord setObject: [NSNumber numberWithInt: [data length] ? 1 : 0] forKey: @"hasPhoto"]; recordSource = [oldRecord objectForKey: @"source"]; if ([recordSource conformsToProtocol: @protocol (SOGoDNSource)] && [[(NSObject *) recordSource MSExchangeHostname] length]) [newRecord setObject: [NSNumber numberWithInt: 1] forKey: @"isMSExchange"]; return newRecord; } - (NSArray *) _flattenedRecords: (NSArray *) records { NSMutableDictionary *oldRecord; NSMutableArray *newRecords; NSEnumerator *oldRecords; NSDictionary *o; newRecords = [NSMutableArray arrayWithCapacity: [records count]]; oldRecords = [records objectEnumerator]; while ((o = [oldRecords nextObject])) { oldRecord = [NSMutableDictionary dictionary]; [oldRecord addEntriesFromDictionary: o]; [newRecords addObject: [self _flattenedRecord: oldRecord]]; } return newRecords; } /* This method returns the entry corresponding to the name passed as parameter. */ - (NSDictionary *) lookupContactWithName: (NSString *) aName { NSDictionary *record; if (aName && [aName length] > 0) { record = [source lookupContactEntry: aName inDomain: [[context activeUser] domain]]; record = [self _flattenedRecord: record]; } else record = nil; return record; } - (NSArray *) lookupContactsWithFilter: (NSString *) filter onCriteria: (NSArray *) criteria sortBy: (NSString *) sortKey ordering: (NSComparisonResult) sortOrdering inDomain: (NSString *) domain { NSArray *records, *result; EOSortOrdering *ordering; result = nil; if ([filter length] > 0 || ![source listRequiresDot]) { records = [source fetchContactsMatching: filter withCriteria: criteria inDomain: domain]; [childRecords setObjects: records forKeys: [records objectsForKey: @"c_name" notFoundMarker: nil]]; records = [self _flattenedRecords: records]; ordering = [EOSortOrdering sortOrderingWithKey: sortKey selector: ((sortOrdering == NSOrderedDescending) ? EOCompareCaseInsensitiveDescending : EOCompareCaseInsensitiveAscending)]; result = [records sortedArrayUsingKeyOrderArray: [NSArray arrayWithObject: ordering]]; } return result; } - (NSString *) _deduceObjectNameFromURL: (NSString *) url fromBaseURL: (NSString *) baseURL { NSRange urlRange; NSString *name; urlRange = [url rangeOfString: baseURL]; if (urlRange.location != NSNotFound) { name = [url substringFromIndex: NSMaxRange (urlRange)]; if ([name hasPrefix: @"/"]) name = [name substringFromIndex: 1]; } else name = nil; return name; } /* TODO: multiget reorg */ - (NSString *) _nodeTagForProperty: (NSString *) property { NSString *namespace, *nodeName, *nsRep; NSRange nsEnd; nsEnd = [property rangeOfString: @"}"]; namespace = [property substringFromRange: NSMakeRange (1, nsEnd.location - 1)]; nodeName = [property substringFromIndex: nsEnd.location + 1]; if ([namespace isEqualToString: XMLNS_CARDDAV]) nsRep = @"C"; else nsRep = @"D"; return [NSString stringWithFormat: @"%@:%@", nsRep, nodeName]; } - (NSString *) _nodeTag: (NSString *) property { static NSMutableDictionary *tags = nil; NSString *nodeTag; if (!tags) tags = [NSMutableDictionary new]; nodeTag = [tags objectForKey: property]; if (!nodeTag) { nodeTag = [self _nodeTagForProperty: property]; [tags setObject: nodeTag forKey: property]; } return nodeTag; } - (NSString **) _properties: (NSString **) properties count: (unsigned int) propertiesCount ofObject: (NSDictionary *) object { SOGoContactLDIFEntry *ldifEntry; NSString **currentProperty; NSString **values, **currentValue; SEL methodSel; // NSLog (@"_properties:ofObject:: %@", [NSDate date]); values = NSZoneMalloc (NULL, (propertiesCount + 1) * sizeof (NSString *)); *(values + propertiesCount) = nil; ldifEntry = [SOGoContactLDIFEntry contactEntryWithName: [object objectForKey: @"c_name"] withLDIFEntry: object inContainer: self]; currentProperty = properties; currentValue = values; while (*currentProperty) { methodSel = SOGoSelectorForPropertyGetter (*currentProperty); if (methodSel && [ldifEntry respondsToSelector: methodSel]) *currentValue = [[ldifEntry performSelector: methodSel] safeStringByEscapingXMLString]; currentProperty++; currentValue++; } // NSLog (@"/_properties:ofObject:: %@", [NSDate date]); return values; } - (NSArray *) _propstats: (NSString **) properties count: (unsigned int) propertiesCount ofObject: (NSDictionary *) object { NSMutableArray *propstats, *properties200, *properties404, *propDict; NSString **property, **values, **currentValue; NSString *propertyValue, *nodeTag; // NSLog (@"_propstats:ofObject:: %@", [NSDate date]); propstats = [NSMutableArray array]; properties200 = [NSMutableArray array]; properties404 = [NSMutableArray array]; values = [self _properties: properties count: propertiesCount ofObject: object]; currentValue = values; property = properties; while (*property) { nodeTag = [self _nodeTag: *property]; if (*currentValue) { propertyValue = [NSString stringWithFormat: @"<%@>%@", nodeTag, *currentValue, nodeTag]; propDict = properties200; } else { propertyValue = [NSString stringWithFormat: @"<%@/>", nodeTag]; propDict = properties404; } [propDict addObject: propertyValue]; property++; currentValue++; } free (values); if ([properties200 count]) [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys: properties200, @"properties", @"HTTP/1.1 200 OK", @"status", nil]]; if ([properties404 count]) [propstats addObject: [NSDictionary dictionaryWithObjectsAndKeys: properties404, @"properties", @"HTTP/1.1 404 Not Found", @"status", nil]]; // NSLog (@"/_propstats:ofObject:: %@", [NSDate date]); return propstats; } - (void) _appendPropstat: (NSDictionary *) propstat toBuffer: (NSMutableString *) r { NSArray *properties; unsigned int count, max; [r appendString: @""]; properties = [propstat objectForKey: @"properties"]; max = [properties count]; for (count = 0; count < max; count++) [r appendString: [properties objectAtIndex: count]]; [r appendString: @""]; [r appendString: [propstat objectForKey: @"status"]]; [r appendString: @""]; } - (void) appendObject: (NSDictionary *) object properties: (NSString **) properties count: (unsigned int) propertiesCount withBaseURL: (NSString *) baseURL toBuffer: (NSMutableString *) r { NSArray *propstats; unsigned int count, max; [r appendFormat: @""]; [r appendString: baseURL]; [r appendString: [[object objectForKey: @"c_name"] stringByEscapingURL]]; [r appendString: @""]; propstats = [self _propstats: properties count: propertiesCount ofObject: object]; max = [propstats count]; for (count = 0; count < max; count++) [self _appendPropstat: [propstats objectAtIndex: count] toBuffer: r]; [r appendString: @""]; } - (void) appendMissingObjectRef: (NSString *) href toBuffer: (NSMutableString *) r { [r appendString: @""]; [r appendString: href]; [r appendString: @"HTTP/1.1 404 Not Found"]; } - (void) _appendComponentProperties: (NSArray *) properties matchingURLs: (id ) refs toResponse: (WOResponse *) response { NSObject *element; NSString *url, *baseURL, *cname, *domain; NSString **propertiesArray; NSMutableString *buffer; NSDictionary *object; unsigned int count, max, propertiesCount; baseURL = [self davURLAsString]; #warning review this when fixing http://www.scalableogo.org/bugs/view.php?id=276 if (![baseURL hasSuffix: @"/"]) baseURL = [NSString stringWithFormat: @"%@/", baseURL]; propertiesArray = [properties asPointersOfObjects]; propertiesCount = [properties count]; max = [refs length]; buffer = [NSMutableString stringWithCapacity: max*512]; domain = [[context activeUser] domain]; for (count = 0; count < max; count++) { element = [refs objectAtIndex: count]; url = [[[element firstChild] nodeValue] stringByUnescapingURL]; cname = [self _deduceObjectNameFromURL: url fromBaseURL: baseURL]; object = [source lookupContactEntry: cname inDomain: domain]; if (object) [self appendObject: object properties: propertiesArray count: propertiesCount withBaseURL: baseURL toBuffer: buffer]; else [self appendMissingObjectRef: url toBuffer: buffer]; } [response appendContentString: buffer]; // NSLog (@"/adding properties with url"); NSZoneFree (NULL, propertiesArray); } - (WOResponse *) performMultigetInContext: (WOContext *) queryContext inNamespace: (NSString *) namespace { WOResponse *r; id document; id documentElement, propElement; r = [context response]; [r prepareDAVResponse]; [r appendContentString: [NSString stringWithFormat: @"", namespace]]; document = [[queryContext request] contentAsDOMDocument]; documentElement = [document documentElement]; propElement = [(NGDOMNodeWithChildren *) documentElement firstElementWithTag: @"prop" inNamespace: @"DAV:"]; [self _appendComponentProperties: [(NGDOMNodeWithChildren *) propElement flatPropertyNameOfSubElements] matchingURLs: [documentElement getElementsByTagName: @"href"] toResponse: r]; [r appendContentString:@""]; return r; } - (id) davAddressbookMultiget: (id) queryContext { return [self performMultigetInContext: queryContext inNamespace: XMLNS_CARDDAV]; } - (NSString *) davDisplayName { return displayName; } - (BOOL) isFolderish { return YES; } /* folder type */ - (NSString *) folderType { return @"Contact"; } /* sorting */ - (NSComparisonResult) compare: (id) otherFolder { NSComparisonResult comparison; BOOL otherIsPersonal; otherIsPersonal = ([otherFolder isKindOfClass: [SOGoContactGCSFolder class]] || ([otherFolder isKindOfClass: object_getClass(self)] && [otherFolder isPersonalSource])); if (isPersonalSource) { if (otherIsPersonal && ![nameInContainer isEqualToString: @"personal"]) { if ([[otherFolder nameInContainer] isEqualToString: @"personal"]) comparison = NSOrderedDescending; else comparison = [[self displayName] localizedCaseInsensitiveCompare: [otherFolder displayName]]; } else comparison = NSOrderedAscending; } else { if (otherIsPersonal) comparison = NSOrderedDescending; else comparison = [[self displayName] localizedCaseInsensitiveCompare: [otherFolder displayName]]; } return comparison; } /* common methods */ - (NSException *) delete { NSException *error; if (isPersonalSource) { error = [(SOGoContactFolders *) container removeLDAPAddressBook: nameInContainer]; if (!error && [[context request] handledByDefaultHandler]) [self sendFolderAdvisoryTemplate: @"Removal"]; } else error = [NSException exceptionWithHTTPStatus: 501 /* not implemented */ reason: @"delete not available on system sources"]; return error; } - (void) renameTo: (NSString *) newName { NSException *error; if (isPersonalSource) { if (![[source displayName] isEqualToString: newName]) { error = [(SOGoContactFolders *) container renameLDAPAddressBook: nameInContainer withDisplayName: newName]; if (!error) [self setDisplayName: newName]; } } /* If public source then method is ignored, maybe we should return an NSException instead... */ } /* acls */ - (NSString *) ownerInContext: (WOContext *) noContext { NSString *sourceOwner; if (isPersonalSource) sourceOwner = [[source modifiers] objectAtIndex: 0]; else sourceOwner = @"nobody"; return sourceOwner; } - (NSArray *) subscriptionRoles { return [NSArray arrayWithObject: SoRole_Authenticated]; } - (NSArray *) aclsForUser: (NSString *) uid { NSArray *acls, *modifiers; static NSArray *modifierRoles = nil; if (!modifierRoles) modifierRoles = [[NSArray alloc] initWithObjects: @"Owner", @"ObjectViewer", @"ObjectEditor", @"ObjectCreator", @"ObjectEraser", nil]; modifiers = [source modifiers]; if ([modifiers containsObject: uid]) acls = [modifierRoles copy]; else acls = [NSArray new]; [acls autorelease]; return acls; } @end