sogo/SoObjects/Contacts/SOGoContactGCSFolder.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

562 lines
15 KiB
Objective-C

/*
Copyright (C) 2006-2016 Inverse inc.
This file is part of SOGo.
SOGo is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 2, or (at your option) any
later version.
SOGo 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 Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public
License along with SOGo; see the file COPYING. If not, write to the
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/SoObject+SoDAV.h>
#import <NGObjWeb/WOContext.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <SaxObjC/XMLNamespaces.h>
#import <EOControl/EOFetchSpecification.h>
#import <EOControl/EOQualifier.h>
#import <EOControl/EOSortOrdering.h>
#import <NGCards/CardGroup.h>
#import <GDLContentStore/GCSFolder.h>
#import <SOGo/DOMNode+SOGo.h>
#import <SOGo/SOGoCache.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/NSObject+DAV.h>
#import <SOGo/WORequest+SOGo.h>
#import "SOGoContactGCSEntry.h"
#import "SOGoContactGCSList.h"
#import "SOGoContactGCSFolder.h"
static NSArray *folderListingFields = nil;
@implementation SOGoContactGCSFolder
+ (void) initialize
{
if (!folderListingFields)
folderListingFields = [[NSArray alloc] initWithObjects: @"c_name",
@"c_cn", @"c_givenname", @"c_sn",
@"c_screenname", @"c_o",
@"c_mail", @"c_telephonenumber",
@"c_categories",
@"c_component", nil];
}
- (id) init
{
if ((self = [super init]))
{
baseCardDAVURL = nil;
basePublicCardDAVURL = nil;
}
return self;
}
- (void) dealloc
{
[baseCardDAVURL release];
[basePublicCardDAVURL release];
[super dealloc];
}
- (NSArray *) searchFields
{
static NSArray *searchFields = nil;
if (!searchFields)
{
// "name" expands to c_sn, c_givenname and c_cn
searchFields = [NSArray arrayWithObjects: @"name", @"c_mail", @"c_categories", @"c_o", nil];
[searchFields retain];
}
return searchFields;
}
- (Class) objectClassForContent: (NSString *) content
{
CardGroup *cardEntry;
NSString *firstTag;
Class objectClass;
objectClass = Nil;
cardEntry = [CardGroup parseSingleFromSource: content];
if (cardEntry)
{
firstTag = [[cardEntry tag] uppercaseString];
if ([firstTag isEqualToString: @"VCARD"])
objectClass = [SOGoContactGCSEntry class];
else if ([firstTag isEqualToString: @"VLIST"])
objectClass = [SOGoContactGCSList class];
}
return objectClass;
}
- (Class) objectClassForComponentName: (NSString *) componentName
{
Class objectClass;
if ([componentName isEqualToString: @"vcard"])
objectClass = [SOGoContactGCSEntry class];
else if ([componentName isEqualToString: @"vlist"])
objectClass = [SOGoContactGCSList class];
else
objectClass = Nil;
return objectClass;
}
- (Class) objectClassForResourceNamed: (NSString *) name
{
EOQualifier *qualifier;
NSArray *records;
NSString *component;
Class objectClass;
qualifier = [EOQualifier qualifierWithQualifierFormat: @"c_name = %@", [name asSafeSQLString]];
records = [[self ocsFolder] fetchFields: [NSArray arrayWithObject: @"c_component"]
matchingQualifier: qualifier];
if ([records count])
{
component = [[records objectAtIndex: 0] valueForKey: @"c_component"];
objectClass = [self objectClassForComponentName: component];
}
else
objectClass = Nil;
return objectClass;
}
- (BOOL) requestNamedIsHandledLater: (NSString *) name
{
return [name isEqualToString: @"OPTIONS"];
}
- (id) lookupName: (NSString *)_key
inContext: (id)_ctx
acquire: (BOOL)_flag
{
id obj;
NSString *url;
BOOL handledLater;
/* first check attributes directly bound to the application */
handledLater = [self requestNamedIsHandledLater: _key];
if (handledLater)
obj = nil;
else
{
obj = [super lookupName:_key inContext:_ctx acquire:NO];
if (!obj)
{
if ([self isValidContentName: _key])
{
url = [[[_ctx request] uri] urlWithoutParameters];
if ([url hasSuffix: @"AsContact"])
obj = [SOGoContactGCSEntry objectWithName: _key
inContainer: self];
else if ([url hasSuffix: @"AsList"])
obj = [SOGoContactGCSList objectWithName: _key
inContainer: self];
[obj setIsNew: YES];
}
}
if (!obj || ([obj isKindOfClass: [SOGoContactGCSList class]] && [[_ctx request] isMacOSXAddressBookApp]))
obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
}
if (obj)
[[SOGoCache sharedCache] registerObject: obj
withName: _key
inContainer: container];
return obj;
}
- (EOQualifier *) qualifierForFilter: (NSString *) filter
onCriteria: (NSArray *) criteria
{
NSEnumerator *criteriaList;
NSMutableArray *filters;
NSString *filterFormat, *currentCriteria, *qs;
EOQualifier *qualifier;
qualifier = nil;
if ([filter length] > 0)
{
filter = [filter asSafeSQLString];
filters = [NSMutableArray array];
filterFormat = [NSString stringWithFormat: @"(%%@ isCaseInsensitiveLike: '%%%%%@%%%%')", filter];
if (criteria)
criteriaList = [criteria objectEnumerator];
else
criteriaList = [[self searchFields] objectEnumerator];
while (( currentCriteria = [criteriaList nextObject] ))
{
if ([currentCriteria isEqualToString: @"name"])
{
[filters addObject: @"c_sn"];
[filters addObject: @"c_givenname"];
[filters addObject: @"c_cn"];
}
else if ([[self searchFields] containsObject: currentCriteria])
[filters addObject: currentCriteria];
}
if ([filters count])
{
qs = [[[filters uniqueObjects] stringsWithFormat: filterFormat] componentsJoinedByString: @" OR "];
qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
}
}
return qualifier;
}
/**
* Normalize keys of dictionary representing a contact.
* @param contactRecord a dictionary with pairs from the quick table
* @see [UIxContactView dataAction]
*/
- (void) fixupContactRecord: (NSMutableDictionary *) contactRecord
{
NSString *data;
// c_categories => categories
data = [contactRecord objectForKey: @"c_categories"];
if ([data length])
[contactRecord setObject: data forKey: @"categories"];
// c_name => id
data = [contactRecord objectForKey: @"c_name"];
if ([data length])
[contactRecord setObject: data forKey: @"id"];
// c_cn
data = [contactRecord objectForKey: @"c_cn"];
if (![data length])
{
data = [contactRecord keysWithFormat: @"%{c_givenname} %{c_sn}"];
if ([data length] > 1)
{
[contactRecord setObject: data forKey: @"c_cn"];
}
else
{
data = [contactRecord objectForKey: @"c_o"];
[contactRecord setObject: data forKey: @"c_cn"];
}
}
// c_screenname
if (![contactRecord objectForKey: @"c_screenname"])
[contactRecord setObject: @"" forKey: @"c_screenname"];
// c_mail => emails[]
data = [contactRecord objectForKey: @"c_mail"];
if ([data length])
{
NSArray *values;
NSDictionary *email;
NSMutableArray *emails;
NSString *type, *value;
int i, max;
values = [data componentsSeparatedByString: @","];
max = [values count];
emails = [NSMutableArray arrayWithCapacity: max];
for (i = 0; i < max; i++)
{
type = (i == 0)? @"pref" : @"home";
value = [values objectAtIndex: i];
email = [NSDictionary dictionaryWithObjectsAndKeys: type, @"type", value, @"value", nil];
[emails addObject: email];
}
[contactRecord setObject: emails forKey: @"emails"];
}
else
{
[contactRecord setObject: @"" forKey: @"c_mail"];
[contactRecord setObject: [NSArray array] forKey: @"emails"];
}
// c_telephonenumber => phones[]
data = [contactRecord objectForKey: @"c_telephonenumber"];
if ([data length])
{
NSDictionary *phonenumber;
phonenumber = [NSDictionary dictionaryWithObjectsAndKeys: @"pref", @"type", data, @"value", nil];
[contactRecord setObject: [NSArray arrayWithObject: phonenumber] forKey: @"phones"];
}
else
{
[contactRecord setObject: @"" forKey: @"c_telephonenumber"];
[contactRecord setObject: [NSArray array] forKey: @"phones"];
}
}
- (NSArray *) _flattenedRecords: (NSArray *) records
{
NSMutableArray *newRecords;
NSEnumerator *oldRecords;
NSDictionary *oldRecord;
NSMutableDictionary *newRecord;
newRecords = [NSMutableArray arrayWithCapacity: [records count]];
oldRecords = [records objectEnumerator];
oldRecord = [oldRecords nextObject];
while (oldRecord)
{
newRecord = [NSMutableDictionary dictionaryWithDictionary: oldRecord];
[self fixupContactRecord: newRecord];
[newRecords addObject: newRecord];
oldRecord = [oldRecords nextObject];
}
return newRecords;
}
/* This method returns the quick entry corresponding to the name passed as
parameter. */
- (NSDictionary *) lookupContactWithName: (NSString *) aName
{
NSArray *dbRecords;
NSMutableDictionary *record;
EOQualifier *qualifier;
NSString *qs;
record = nil;
if (aName && [aName length] > 0)
{
aName = [aName asSafeSQLString];
qs = [NSString stringWithFormat: @"(c_name='%@')", aName];
qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
dbRecords = [[self ocsFolder] fetchFields: folderListingFields
matchingQualifier: qualifier];
if ([dbRecords count] > 0)
{
record = [dbRecords objectAtIndex: 0];
[self fixupContactRecord: record];
}
}
return record;
}
/*
* GCS folder are personal folders and are not associated to a domain.
* The domain is therefore ignored.
*/
- (NSArray *) lookupContactsWithFilter: (NSString *) filter
onCriteria: (NSArray *) criteria
sortBy: (NSString *) sortKey
ordering: (NSComparisonResult) sortOrdering
inDomain: (NSString *) domain
{
NSArray *dbRecords, *records;
EOQualifier *qualifier;
EOSortOrdering *ordering;
qualifier = [self qualifierForFilter: filter onCriteria: criteria];
dbRecords = [[self ocsFolder] fetchFields: folderListingFields
matchingQualifier: qualifier];
if ([dbRecords count] > 0)
{
records = [self _flattenedRecords: dbRecords];
ordering
= [EOSortOrdering sortOrderingWithKey: sortKey
selector: ((sortOrdering == NSOrderedDescending)
? EOCompareCaseInsensitiveDescending
: EOCompareCaseInsensitiveAscending)];
records
= [records sortedArrayUsingKeyOrderArray:
[NSArray arrayWithObject: ordering]];
}
else
records = nil;
[self debugWithFormat:@"fetched %i records.", [records count]];
return records;
}
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
{
return [self lookupContactsFields: folderListingFields
withQualifier: qualifier
andOrderings: nil];
}
- (NSArray *) lookupContactsFields: (NSArray *) fields
withQualifier: (EOQualifier *) qualifier
andOrderings: (NSArray *) orderings
{
NSArray *dbRecords, *records;
EOFetchSpecification *spec;
spec = [EOFetchSpecification fetchSpecificationWithEntityName: [[self ocsFolder] folderName]
qualifier: qualifier
sortOrderings: orderings];
dbRecords = [[self ocsFolder] fetchFields: fields
fetchSpecification: spec
ignoreDeleted: YES];
if ([dbRecords count] > 0 && fields == folderListingFields)
records = [self _flattenedRecords: dbRecords];
else
records = dbRecords;
[self debugWithFormat:@"fetched %i records.", [records count]];
return records;
}
- (NSDictionary *) davSQLFieldsTable
{
static NSMutableDictionary *davSQLFieldsTable = nil;
if (!davSQLFieldsTable)
{
davSQLFieldsTable = [[super davSQLFieldsTable] mutableCopy];
[davSQLFieldsTable setObject: @"c_content" forKey: @"{urn:ietf:params:xml:ns:carddav}address-data"];
}
return davSQLFieldsTable;
}
- (NSString *) groupDavResourceType
{
return @"vcard-collection";
}
- (NSArray *) davResourceType
{
NSMutableArray *resourceType;
NSArray *cardDavCollection;
cardDavCollection
= [NSArray arrayWithObjects: @"addressbook",
@"urn:ietf:params:xml:ns:carddav", nil];
resourceType = [NSMutableArray arrayWithArray: [super davResourceType]];
[resourceType addObject: cardDavCollection];
return resourceType;
}
- (id) davAddressbookMultiget: (id) queryContext
{
return [self performMultigetInContext: queryContext
inNamespace: @"urn:ietf:params:xml:ns:carddav"];
}
/* sorting */
- (NSComparisonResult) compare: (id) otherFolder
{
NSComparisonResult comparison;
if ([NSStringFromClass([otherFolder class])
isEqualToString: @"SOGoContactSourceFolder"])
comparison = NSOrderedAscending;
else
comparison = [super compare: otherFolder];
return comparison;
}
/* folder type */
- (NSString *) folderType
{
return @"Contact";
}
/* 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 *) _baseCardDAVURL
{
NSString *davURL;
if (!baseCardDAVURL)
{
davURL = [[self realDavURL] absoluteString];
if ([davURL hasSuffix: @"/"])
baseCardDAVURL = [davURL substringToIndex: [davURL length] - 1];
else
baseCardDAVURL = davURL;
[baseCardDAVURL retain];
}
return baseCardDAVURL;
}
- (NSString *) cardDavURL
{
return [NSString stringWithFormat: @"%@/", [self _baseCardDAVURL]];
}
- (NSString *) _basePublicCardDAVURL
{
NSString *davURL;
if (!basePublicCardDAVURL)
{
davURL = [[self publicDavURL] absoluteString];
if ([davURL hasSuffix: @"/"])
basePublicCardDAVURL = [davURL substringToIndex: [davURL length] - 1];
else
basePublicCardDAVURL = davURL;
[basePublicCardDAVURL retain];
}
return basePublicCardDAVURL;
}
- (NSString *) publicCardDavURL
{
return [NSString stringWithFormat: @"%@/", [self _basePublicCardDAVURL]];
}
@end /* SOGoContactGCSFolder */