fix(addressbook(dav)): improve handling of addressbook-query

This commit is contained in:
Francis Lachapelle
2022-02-28 17:07:39 -05:00
parent 3f93e6ebc4
commit 7faab9117a
12 changed files with 744 additions and 209 deletions
+21 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright (C) 2006-2017 Inverse inc.
Copyright (C) 2006-2022 Inverse inc.
This file is part of SOGo.
@@ -34,6 +34,8 @@
#import <SOGo/SOGoFolder.h>
@class EOQualifier;
@class EOSortOrdering;
@class NSArray;
@class NSDictionary;
@class NSString;
@@ -46,8 +48,26 @@
sortBy: (NSString *) sortKey
ordering: (NSComparisonResult) sortOrdering
inDomain: (NSString *) domain;
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
andSortOrdering: (EOSortOrdering *) ordering
inDomain: (NSString *) domain;
- (NSDictionary *) lookupContactWithName: (NSString *) aName;
/**
Map a vCard property to a source field name.
Possible vCard properties are:
- EMAIL
- FN
- N
- ORG
- ADR
- TEL
*/
- (void) addVCardProperty: (NSString *) property
toCriteria: (NSMutableArray *) criteria;
@end
#endif /* __Contacts_SOGoContactFolder_H__ */
+72
View File
@@ -80,6 +80,19 @@ static NSArray *folderListingFields = nil;
[super dealloc];
}
- (NSArray *) nameFields
{
static NSArray *nameFields = nil;
if (!nameFields)
{
nameFields = [NSArray arrayWithObjects: @"c_sn", @"c_givenname", @"c_cn", nil];
[nameFields retain];
}
return nameFields;
}
- (NSArray *) searchFields
{
static NSArray *searchFields = nil;
@@ -438,6 +451,61 @@ static NSArray *folderListingFields = nil;
return records;
}
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
andSortOrdering: (EOSortOrdering *) ordering
inDomain: (NSString *) domain
{
NSArray *dbRecords, *records;
EOFetchSpecification *spec;
spec = [EOFetchSpecification fetchSpecificationWithEntityName: [[self ocsFolder] folderName]
qualifier: qualifier
sortOrderings: [NSArray arrayWithObject: ordering]];
dbRecords = [[self ocsFolder] fetchFields: folderListingFields
fetchSpecification: spec
ignoreDeleted: YES];
if ([dbRecords count] > 0)
records = [self _flattenedRecords: dbRecords];
else
records = dbRecords;
[self debugWithFormat:@"fetched %i records.", [records count]];
return records;
}
- (void) addVCardProperty: (NSString *) property
toCriteria: (NSMutableArray *) criteria
{
static NSDictionary *vCardSQLFieldsTable = nil;
NSEnumerator *fields;
id field;
if (!vCardSQLFieldsTable)
vCardSQLFieldsTable = [[NSDictionary alloc] initWithObjectsAndKeys:
[self nameFields], @"fn",
[self nameFields], @"n",
@"c_mail", @"email",
@"c_telephonenumber", @"tel",
@"c_o", @"org",
@"c_l", @"adr",
nil];
field = [vCardSQLFieldsTable objectForKey: property];
if (field)
{
if ([field isKindOfClass: [NSArray class]])
{
fields = [(NSArray *)field objectEnumerator];
while ((field = [fields nextObject]))
[criteria addObjectUniquely: field];
}
else
[criteria addObjectUniquely: field];
}
}
- (NSDictionary *) davSQLFieldsTable
{
static NSMutableDictionary *davSQLFieldsTable = nil;
@@ -470,6 +538,10 @@ static NSArray *folderListingFields = nil;
return resourceType;
}
/**
CARDDAV:addressbook-multiget Report
https://datatracker.ietf.org/doc/html/rfc6352#section-8.6
*/
- (id) davAddressbookMultiget: (id) queryContext
{
return [self performMultigetInContext: queryContext
@@ -25,6 +25,7 @@
#import <NGObjWeb/WOContext+SoObjects.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <EOControl/EOQualifier.h>
#import <EOControl/EOSortOrdering.h>
#import <SaxObjC/XMLNamespaces.h>
@@ -451,6 +452,33 @@
return result;
}
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
andSortOrdering: (EOSortOrdering *) ordering
inDomain: (NSString *) domain
{
NSArray *records;
records = nil;
if ([qualifier count] > 0 || ![source listRequiresDot])
{
records = [source lookupContactsWithQualifier: qualifier
andSortOrdering: ordering
inDomain: domain];
records = [self _flattenedRecords: records];
}
return records;
}
- (void) addVCardProperty: (NSString *) property
toCriteria: (NSMutableArray *) criteria
{
[source addVCardProperty: property
toCriteria: criteria];
}
- (NSString *) _deduceObjectNameFromURL: (NSString *) url
fromBaseURL: (NSString *) baseURL
{
+222 -118
View File
@@ -1,6 +1,6 @@
/* NSObject+CardDAV.m - this file is part of SOGo
*
* Copyright (C) 2007-2015 Inverse inc.
* Copyright (C) 2007-2022 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
@@ -26,20 +26,24 @@
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <DOM/DOMNode.h>
#import <EOControl/EOQualifier.h>
#import <EOControl/EOSortOrdering.h>
#import <SaxObjC/SaxObjC.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/WOResponse+SOGo.h>
#import <SOGo/DOMNode+SOGo.h>
#import "SOGoContactFolder.h"
#import "SOGoContactGCSEntry.h"
@implementation SOGoFolder (CardDAV)
- (void) _appendObject: (NSDictionary *) object
withBaseURL: (NSString *) baseURL
toREPORTResponse: (WOResponse *) r
- (void) _appendProperties: (NSArray *) properties
forObject: (NSDictionary *) object
withBaseURL: (NSString *) baseURL
toREPORTResponse: (WOResponse *) r
{
id component;
NSString *name, *etagLine, *contactString;
@@ -47,14 +51,14 @@
name = [object objectForKey: @"c_name"];
if ([name length])
{
contactString = nil;
component = [self lookupName: name inContext: context acquire: NO];
if ([component isKindOfClass: [NSException class]])
{
[self logWithFormat: @"Object with name '%@' not found. You likely have a LDAP configuration issue.", name];
[self logWithFormat: @"Object with name '%@' not found.", name];
return;
}
#warning we provide both "address-data" and "addressbook-data" for compatibility reasons, we should actually check which one has been queried
[r appendContentString: @"<D:response>"
@"<D:href>"];
[r appendContentString: baseURL];
@@ -64,53 +68,75 @@
[r appendContentString: @"</D:href>"
@"<D:propstat>"
@"<D:prop>"];
etagLine = [NSString stringWithFormat: @"<D:getetag>%@</D:getetag>",
[component davEntityTag]];
[r appendContentString: etagLine];
[r appendContentString: @"<C:address-data>"];
contactString = [[component contentAsString] safeStringByEscapingXMLString];
[r appendContentString: contactString];
[r appendContentString: @"</C:address-data>"
@"<C:addressbook-data>"];
[r appendContentString: contactString];
[r appendContentString: @"</C:addressbook-data>"
@"</D:prop>"
@"<D:status>HTTP/1.1 200 OK</D:status>"
@"</D:propstat>"
@"</D:response>"];
if ([properties containsObject: @"{DAV:}getetag"])
{
etagLine = [NSString stringWithFormat: @"<D:getetag>%@</D:getetag>",
[component davEntityTag]];
[r appendContentString: etagLine];
}
if ([properties containsObject: @"{urn:ietf:params:xml:ns:carddav}address-data"])
{
[r appendContentString: @"<C:address-data>"];
contactString = [[component contentAsString] safeStringByEscapingXMLString];
[r appendContentString: contactString];
[r appendContentString: @"</C:address-data>"];
}
if ([properties containsObject: @"{urn:ietf:params:xml:ns:carddav}addressbook-data"])
{
[r appendContentString: @"<C:addressbook-data>"];
if (!contactString)
contactString = [[component contentAsString] safeStringByEscapingXMLString];
[r appendContentString: contactString];
[r appendContentString: @"</C:addressbook-data>"];
}
[r appendContentString: @"</D:prop>"
@"<D:status>HTTP/1.1 200 OK</D:status>"
@"</D:propstat>"
@"</D:response>"];
}
}
- (void) _appendComponentsMatchingFilters: (NSArray *) filters
toResponse: (WOResponse *) response
context: (id) localContext
- (void) _appendComponentsProperties: (NSArray *) properties
matchingQualifier: (EOQualifier *) qualifier
toResponse: (WOResponse *) response
context: (id) localContext
{
unsigned int count,i , max;
EOSortOrdering *sort;
NSAutoreleasePool *pool;
NSDictionary *currentFilter, *contact;
NSDictionary *contact;
NSMutableArray *names;
NSEnumerator *contacts;
NSString *baseURL, *domain;
NSString *baseURL, *domain, *name;
unsigned int i;
baseURL = [self baseURLInContext: localContext];
domain = [[localContext activeUser] domain];
sort = [EOSortOrdering sortOrderingWithKey: @"c_cn"
selector: EOCompareCaseInsensitiveAscending];
names = [NSMutableArray array];
max = [filters count];
for (count = 0; count < max; count++)
contacts = [[(id<SOGoContactFolder>)self lookupContactsWithQualifier: qualifier
andSortOrdering: sort
inDomain: domain] objectEnumerator];
i = 0;
pool = [[NSAutoreleasePool alloc] init];
while ((contact = [contacts nextObject]))
{
currentFilter = [filters objectAtIndex: count];
contacts =
[[(id<SOGoContactFolder>)self lookupContactsWithFilter: [[currentFilter allValues] lastObject]
onCriteria: nil
sortBy: @"c_givenname"
ordering: NSOrderedDescending
inDomain: domain] objectEnumerator];
pool = [[NSAutoreleasePool alloc] init];
i = 0;
while ((contact = [contacts nextObject]))
// Don't append suspected duplicates
name = [contact objectForKey: @"c_name"]; // primary key of contacts
if (![names containsObject: name])
{
[self _appendObject: contact withBaseURL: baseURL
toREPORTResponse: response];
[self _appendProperties: properties
forObject: contact
withBaseURL: baseURL
toREPORTResponse: response];
[names addObject: name];
if (i % 10 == 0)
{
RELEASE(pool);
@@ -118,97 +144,170 @@
}
i++;
}
RELEASE(pool);
}
}
- (BOOL) _isValidFilter: (NSString *) theString
{
NSString *newString;
newString = [theString lowercaseString];
return ([newString isEqualToString: @"sn"]
|| [newString isEqualToString: @"givenname"]
|| [newString isEqualToString: @"email"]
|| [newString isEqualToString: @"mail"]
|| [newString isEqualToString: @"telephonenumber"]);
}
- (NSDictionary *) _parseContactFilter: (id <DOMElement>) filterElement
{
NSMutableDictionary *filterData;
id <DOMNode> parentNode;
id <DOMNodeList> ranges;
filterData = nil;
parentNode = [filterElement parentNode];
if ([[(id)parentNode tagName] isEqualToString: @"filter"]
&& [self _isValidFilter: [filterElement attribute: @"name"]])
{
ranges = [filterElement getElementsByTagName: @"text-match"];
if ([(NSArray *) ranges count]
&& [(NSArray *) [[ranges objectAtIndex: 0] childNodes] count])
{
filterData = [NSMutableDictionary dictionary];
[filterData setObject: [(NGDOMNode *)[ranges objectAtIndex: 0] textValue]
forKey: [filterElement attribute: @"name"]];
}
}
return filterData;
}
- (NSArray *) _parseContactFilters: (id <DOMElement>) parentNode
{
NSEnumerator *children;
id <DOMElement> node;
NSMutableArray *filters;
NSDictionary *filter;
filters = [NSMutableArray array];
children = [(NSArray *)[parentNode getElementsByTagName: @"prop-filter"]
objectEnumerator];
while ((node = [children nextObject]))
{
filter = [self _parseContactFilter: node];
if (filter)
[filters addObject: filter];
}
// If no filters are provided, we return everything.
if (![filters count])
{
[filters addObject: [NSDictionary dictionaryWithObject: @"." forKey: @"email"]];
[filters addObject: [NSDictionary dictionaryWithObject: @"%" forKey: @"name"]];
}
return filters;
RELEASE(pool);
}
/**
Validate the prop-filter name of the addressbook-query. Must match the supported vCard
properties of all SOGoContactFolder classes.
@see [SOGoContactFolder addVCardProperty:toCriteria:]
@see [SOGoContactGCSFolder addVCardProperty:toCriteria:]
@see [LDAPSource addVCardProperty:toCriteria:]
@see [SQLSource addVCardProperty:toCriteria:]
*/
- (BOOL) _isValidFilter: (NSString *) theString
{
NSString *newString;
BOOL isValid;
newString = [theString lowercaseString];
isValid = ([newString isEqualToString: @"fn"]
|| [newString isEqualToString: @"n"]
|| [newString isEqualToString: @"email"]
|| [newString isEqualToString: @"tel"]
|| [newString isEqualToString: @"org"]
|| [newString isEqualToString: @"adr"]);
if (!isValid)
[self warnWithFormat: @"Unsupported prop-filter name '%@'", theString];
return isValid;
}
- (EOQualifier *) _parseContactFilter: (id <DOMElement>) filterElement // a prop-filter element
{
NSMutableArray *qualifiers;
NSMutableArray *criteria;
NSString *name, *test;
NGDOMElement *match;
EOQualifier *qualifier;
id <DOMNode> parentNode;
id <DOMNodeList> ranges;
unsigned int i;
qualifier = nil;
parentNode = [filterElement parentNode];
name = [[filterElement attribute: @"name"] lowercaseString];
if ([[(id)parentNode tagName] isEqualToString: @"filter"]
&& [self _isValidFilter: name])
{
qualifiers = [NSMutableArray array];
criteria = [NSMutableArray array];
test = [[filterElement attribute: @"test"] lowercaseString];
ranges = [filterElement getElementsByTagName: @"text-match"];
[(id<SOGoContactFolder>)self addVCardProperty: name
toCriteria: criteria];
for (i = 0; i < [ranges length]; i++)
{
match = (NGDOMElement *)[ranges objectAtIndex: i];
if ([(NSArray *)[match childNodes] count])
{
SEL currentOperator;
EOQualifier *currentQualifier;
NSString *currentMatchType, *currentMatch;
currentMatch = [match textValue];
currentMatchType = [[match attribute: @"match-type"] lowercaseString];
if ([currentMatchType isEqualToString: @"equals"])
currentOperator = EOQualifierOperatorEqual;
else // contains, starts-with, ends-with
{
currentOperator = EOQualifierOperatorCaseInsensitiveLike;
currentMatch = [NSString stringWithFormat: @"*%@*", currentMatch];
}
currentQualifier = [[EOKeyValueQualifier alloc] initWithKey: [criteria objectAtIndex: 0]
operatorSelector: currentOperator
value: currentMatch];
[currentQualifier autorelease];
[qualifiers addObject: currentQualifier];
}
}
if ([qualifiers count] > 1)
{
if ([test isEqualToString: @"allof"])
qualifier = [[EOAndQualifier alloc] initWithQualifierArray: qualifiers];
else // anyof
qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
[qualifier autorelease];
}
else if ([qualifiers count])
qualifier = [qualifiers objectAtIndex: 0];
}
return qualifier;
}
- (EOQualifier *) _parseContactFilters: (id <DOMElement>) parentNode
{
EOQualifier *qualifier, *currentQualifier;
NSEnumerator *children;
id <DOMElement> filterElement, node;
NSMutableArray *qualifiers;
NSString *test;
qualifier = nil;
filterElement = [(NGDOMNodeWithChildren *) parentNode firstElementWithTag: @"filter"
inNamespace: @"urn:ietf:params:xml:ns:carddav"];
if (filterElement)
{
qualifiers = [NSMutableArray array];
test = [[filterElement attribute: @"test"] lowercaseString];
children = [(NSArray *)[parentNode getElementsByTagName: @"prop-filter"] objectEnumerator];
while ((node = [children nextObject]))
{
currentQualifier = [self _parseContactFilter: node];
if (currentQualifier)
[qualifiers addObject: currentQualifier];
}
if ([qualifiers count] > 1)
{
if ([test isEqualToString: @"allof"])
qualifier = [[EOAndQualifier alloc] initWithQualifierArray: qualifiers];
else
qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
[qualifier autorelease];
}
else if ([qualifiers count])
qualifier = [qualifiers objectAtIndex: 0];
}
return qualifier;
}
/**
CARDDAV:addressbook-query Report
https://datatracker.ietf.org/doc/html/rfc6352#section-8.6
<?xml version="1.0" encoding="UTF-8"?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data/>
</D:prop>
<C:filter>
<C:prop-filter name="mail">
<C:text-match collation="i;unicasemap" match-type="starts-with">foo</C:text-match>
<C:filter test="anyof">
<C:prop-filter name="EMAIL" test="allof">
<C:text-match collation="i;unicode-casemap" match-type="starts-with">foo</C:text-match>
</C:prop-filter>
</C:filter>
</C:addressbook-query>
*/
- (id) davAddressbookQuery: (id) queryContext
{
EOQualifier *qualifier;
WOResponse *r;
NSArray *filters;
NSArray *properties;
id <DOMDocument> document;
id <DOMElement> documentElement, propElement;
r = [queryContext response];
[r prepareDAVResponse];
@@ -216,11 +315,16 @@
@" xmlns:C=\"urn:ietf:params:xml:ns:carddav\">"];
document = [[queryContext request] contentAsDOMDocument];
filters = [self _parseContactFilters: [document documentElement]];
documentElement = [document documentElement];
propElement = [(NGDOMNodeWithChildren *) documentElement firstElementWithTag: @"prop"
inNamespace: @"DAV:"];
properties = [(NGDOMNodeWithChildren *) propElement flatPropertyNameOfSubElements];
qualifier = [self _parseContactFilters: documentElement];
[self _appendComponentsMatchingFilters: filters
toResponse: r
context: queryContext];
[self _appendComponentsProperties: properties
matchingQualifier: qualifier
toResponse: r
context: queryContext];
[r appendContentString: @"</D:multistatus>"];
return r;