sogo/SoObjects/Contacts/SOGoContactSourceFolder.m
Francis Lachapelle eb90760b39 Use address books search fields in Contacts module
Searches can now be scoped to one or multiple fields. Those fields are
now dynamic and can be defined using SearchFieldNames in external
contacts sources (SQL and LDAP).
2017-11-21 15:56:16 -05:00

833 lines
24 KiB
Objective-C

/* 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 <Foundation/NSValue.h>
#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <EOControl/EOSortOrdering.h>
#import <SaxObjC/XMLNamespaces.h>
#import <SOGo/DOMNode+SOGo.h>
#import <SOGo/LDAPSource.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/NSObject+DAV.h>
#import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/WORequest+SOGo.h>
#import <SOGo/WOResponse+SOGo.h>
#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 <SOGoSource>) newSource
{
ASSIGN (source, newSource);
}
- (id <SOGoSource>) 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 <SOGoSource> *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"];
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"];
// o => org
data = [oldRecord objectForKey: @"o"];
if (data)
[newRecord setObject: data forKey: @"org"];
else
data = @"";
[newRecord setObject: data forKey: @"c_o"];
// telephonenumber || cellphone || homephone => phones[]
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"];
// Custom attribute for group-lookups. See LDAPSource.m where
// it's set.
data = [oldRecord objectForKey: @"isGroup"];
if (data)
{
[newRecord setObject: data forKey: @"isGroup"];
[newRecord setObject: @"vlist" forKey: @"c_component"];
}
else
{
[newRecord setObject: @"vcard" forKey: @"c_component"];
}
// c_info => note
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 <SOGoDNSource>*) 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];
if ([source isKindOfClass: [LDAPSource class]])
[(LDAPSource *)source applyContactMappingToResult: oldRecord];
[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: @"<D:propstat><D:prop>"];
properties = [propstat objectForKey: @"properties"];
max = [properties count];
for (count = 0; count < max; count++)
[r appendString: [properties objectAtIndex: count]];
[r appendString: @"</D:prop><D:status>"];
[r appendString: [propstat objectForKey: @"status"]];
[r appendString: @"</D:status></D:propstat>"];
}
- (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: @"<D:response><D:href>"];
[r appendString: baseURL];
[r appendString: [[object objectForKey: @"c_name"] stringByEscapingURL]];
[r appendString: @"</D:href>"];
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: @"</D:response>"];
}
- (void) appendMissingObjectRef: (NSString *) href
toBuffer: (NSMutableString *) r
{
[r appendString: @"<D:response><D:href>"];
[r appendString: href];
[r appendString: @"</D:href><D:status>HTTP/1.1 404 Not Found</D:status></D:response>"];
}
- (void) _appendComponentProperties: (NSArray *) properties
matchingURLs: (id <DOMNodeList>) refs
toResponse: (WOResponse *) response
{
NSObject <DOMElement> *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 <DOMDocument> document;
id <DOMElement> documentElement, propElement;
r = [context response];
[r prepareDAVResponse];
[r appendContentString:
[NSString stringWithFormat: @"<D:multistatus xmlns:D=\"DAV:\""
@" xmlns:C=\"%@\">", 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:@"</D:multistatus>"];
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