diff --git a/SOPE/GDLContentStore/EOQualifier+GCS.h b/SOPE/GDLContentStore/EOQualifier+GCS.h index 3b2c8f6ca..ac92611a3 100644 --- a/SOPE/GDLContentStore/EOQualifier+GCS.h +++ b/SOPE/GDLContentStore/EOQualifier+GCS.h @@ -25,10 +25,13 @@ #include @class NSMutableString; +@class EOAdaptor; @interface EOQualifier(GCS) -- (void) _gcsAppendToString: (NSMutableString *) _ms; +- (void) appendSQLToString: (NSMutableString *) _ms; +- (void) appendSQLToString: (NSMutableString *) _ms + withAdaptor: (EOAdaptor *) _adaptor; @end diff --git a/SOPE/GDLContentStore/EOQualifier+GCS.m b/SOPE/GDLContentStore/EOQualifier+GCS.m index cc2f6e469..7c3593058 100644 --- a/SOPE/GDLContentStore/EOQualifier+GCS.m +++ b/SOPE/GDLContentStore/EOQualifier+GCS.m @@ -24,6 +24,10 @@ #import #import +#import +#import +#import + #import "EOQualifier+GCS.h" #if (defined(__GNU_LIBOBJC__) && (__GNU_LIBOBJC__ >= 20100911)) || defined(APPLE_RUNTIME) || defined(__GNUSTEP_RUNTIME__) @@ -33,9 +37,9 @@ @implementation EOQualifier(GCS) - (void) _appendAndQualifier: (EOAndQualifier *) _q + withAdaptor: (EOAdaptor *) _adaptor toString: (NSMutableString *) _ms { - // TODO: move to EOQualifier category NSArray *qs; unsigned i, count; @@ -46,14 +50,15 @@ for (i = 0; i < count; i++) { if (i != 0) [_ms appendString:@" AND "]; if (count > 1) [_ms appendString:@"("]; - [[qs objectAtIndex:i] _gcsAppendToString: _ms]; + [[qs objectAtIndex:i] appendSQLToString: _ms + withAdaptor: _adaptor]; if (count > 1) [_ms appendString:@")"]; } } -- (void)_appendOrQualifier: (EOAndQualifier *) _q - toString: (NSMutableString *) _ms +- (void) _appendOrQualifier: (EOAndQualifier *) _q + withAdaptor: (EOAdaptor *) _adaptor + toString: (NSMutableString *) _ms { - // TODO: move to EOQualifier category NSArray *qs; unsigned i, count; @@ -64,23 +69,28 @@ for (i = 0; i < count; i++) { if (i != 0) [_ms appendString:@" OR "]; if (count > 1) [_ms appendString:@"("]; - [[qs objectAtIndex:i] _gcsAppendToString: _ms]; + [[qs objectAtIndex:i] appendSQLToString: _ms + withAdaptor: _adaptor]; if (count > 1) [_ms appendString:@")"]; } } -- (void)_appendNotQualifier: (EONotQualifier *)_q - toString:(NSMutableString *) _ms +- (void) _appendNotQualifier: (EONotQualifier *) _q + withAdaptor: (EOAdaptor *) _adaptor + toString:(NSMutableString *) _ms { [_ms appendString:@" NOT ("]; - [[_q qualifier] _gcsAppendToString: _ms]; + [[_q qualifier] appendSQLToString: _ms + withAdaptor: _adaptor]; [_ms appendString:@")"]; } - (void) _appendKeyValueQualifier: (EOKeyValueQualifier *) _q + withAdaptor: (EOAdaptor *) _adaptor toString: (NSMutableString *) _ms { id val; + EOAttribute *attribute; NSString *qKey, *qOperator, *qValue, *qFormat; BOOL isCI; @@ -119,9 +129,33 @@ qValue = [val stringValue]; else if ([val isKindOfClass: [NSString class]]) { if ([(EOKeyValueQualifier *)self formatted]) - qValue = val; + { + qValue = val; + } else - qValue = [NSString stringWithFormat: @"'%@'", val]; + { + if (_adaptor) + { + // Assume qualifier applies to a varchar column type + attribute = [EOAttribute new]; + [attribute setExternalType: @"varchar"]; + [attribute autorelease]; + + if (sel_isEqual([_q selector], EOQualifierOperatorLike) || + sel_isEqual([_q selector], EOQualifierOperatorCaseInsensitiveLike)) + { + qValue = [[_adaptor expressionClass] sqlPatternFromShellPattern: val]; + qValue = [_adaptor formatValue: qValue + forAttribute: attribute]; + } + else + qValue = [_adaptor formatValue: val + forAttribute: attribute]; + } + else + // No adaptor provided, don't parse value + qValue = [NSString stringWithFormat: @"'%@'", val]; + } } else { qValue = @"NULL"; @@ -155,29 +189,43 @@ } - (void) _appendQualifier: (EOQualifier *) _q + withAdaptor: (EOAdaptor *) _adaptor toString: (NSMutableString *) _ms { if (_q == nil) return; if ([_q isKindOfClass: [EOAndQualifier class]]) [self _appendAndQualifier: (id)_q + withAdaptor: _adaptor toString: _ms]; else if ([_q isKindOfClass: [EOOrQualifier class]]) [self _appendOrQualifier: (id)_q - toString:_ms]; + withAdaptor: _adaptor + toString: _ms]; else if ([_q isKindOfClass: [EOKeyValueQualifier class]]) [self _appendKeyValueQualifier: (id)_q - toString:_ms]; + withAdaptor: _adaptor + toString: _ms]; else if ([_q isKindOfClass: [EONotQualifier class]]) [self _appendNotQualifier: (id)_q - toString:_ms]; + withAdaptor: (EOAdaptor *) _adaptor + toString: _ms]; else [self errorWithFormat:@"unknown qualifier: %@", _q]; } -- (void) _gcsAppendToString: (NSMutableString *) _ms +- (void) appendSQLToString: (NSMutableString *) _ms { [self _appendQualifier: self + withAdaptor: nil + toString: _ms]; +} + +- (void) appendSQLToString: (NSMutableString *) _ms + withAdaptor: (EOAdaptor *) _adaptor +{ + [self _appendQualifier: self + withAdaptor: _adaptor toString: _ms]; } diff --git a/SOPE/GDLContentStore/GCSFolder.h b/SOPE/GDLContentStore/GCSFolder.h index b3e9936ed..c7f79ab31 100644 --- a/SOPE/GDLContentStore/GCSFolder.h +++ b/SOPE/GDLContentStore/GCSFolder.h @@ -159,10 +159,6 @@ /* helpers */ - (EOAttribute *) _attributeForColumn: (NSString *) _field; -- (void) _findQualifiers: (id) qualifier - withAdaptor: (EOAdaptor *) adaptor; -- (void) _formatQualifierValue: (EOKeyValueQualifier *) qualifier - withAdaptor: (EOAdaptor *) adaptor; @end #endif /* __GDLContentStore_GCSFolder_H__ */ diff --git a/SOPE/GDLContentStore/GCSFolder.m b/SOPE/GDLContentStore/GCSFolder.m index 72d39caf6..628aaade3 100644 --- a/SOPE/GDLContentStore/GCSFolder.m +++ b/SOPE/GDLContentStore/GCSFolder.m @@ -1,7 +1,7 @@ /* Copyright (C) 2004-2007 SKYRIX Software AG Copyright (C) 2007 Helge Hess - Copyright (c) 2008-2019 Inverse inc. + Copyright (c) 2008-2022 Inverse inc. This file is part of SOGo. @@ -26,9 +26,11 @@ #import #import +#import #import #import +#import #import #import #import @@ -401,19 +403,9 @@ static GCSStringFormatter *stringFormatter = nil; adaptor = [adaptorCtx adaptor]; } - if ([qualifier isKindOfClass: [EOAndQualifier class]]) - [self _findQualifiers: (id)qualifier withAdaptor: adaptor]; - else if ([qualifier isKindOfClass: [EOOrQualifier class]]) - [self _findQualifiers: (id)qualifier withAdaptor: adaptor]; - else if ([qualifier isKindOfClass: [EOKeyValueQualifier class]]) - [self _formatQualifierValue: (EOKeyValueQualifier *)qualifier withAdaptor: adaptor]; - else if ([qualifier isKindOfClass: [EONotQualifier class]]) - [self _formatQualifierValue: (EOKeyValueQualifier *)[(id)qualifier qualifier] withAdaptor: adaptor]; - else - [self errorWithFormat:@"unknown qualifier: %@", qualifier]; - ms = [NSMutableString stringWithCapacity:32]; - [qualifier _gcsAppendToString: ms]; + [qualifier appendSQLToString: ms + withAdaptor: adaptor]; } else ms = nil; @@ -421,51 +413,6 @@ static GCSStringFormatter *stringFormatter = nil; return ms; } -- (void) _findQualifiers: (id) qualifier - withAdaptor: (EOAdaptor *) adaptor -{ - NSArray *qs; - unsigned i, count; - - if (qualifier == nil) return; - - qs = [qualifier qualifiers]; - if ((count = [qs count]) == 0) - return; - - for (i = 0; i < count; i++) { - id q = [qs objectAtIndex: i]; - if ([q isKindOfClass: [EOAndQualifier class]]) - [self _findQualifiers: q withAdaptor: adaptor]; - else if ([q isKindOfClass:[EOOrQualifier class]]) - [self _findQualifiers: q withAdaptor: adaptor]; - else if ([q isKindOfClass:[EOKeyValueQualifier class]]) - [self _formatQualifierValue: (EOKeyValueQualifier *)q withAdaptor: adaptor]; - else if ([q isKindOfClass:[EONotQualifier class]]) - [self _formatQualifierValue: (EOKeyValueQualifier *)[q qualifier] withAdaptor: adaptor]; - else - [self errorWithFormat:@"unknown qualifier: %@", q]; - } -} - -- (void) _formatQualifierValue: (EOKeyValueQualifier *) qualifier - withAdaptor: (EOAdaptor *) adaptor -{ - NSString *field; - EOAttribute *attribute; - NSString *formattedValue; - - field = [qualifier key]; - attribute = [self _attributeForColumn: field]; - if (attribute && [[qualifier value] isNotNull]) - { - formattedValue = [adaptor formatValue: [qualifier value] - forAttribute: attribute]; - [qualifier setValue: formattedValue]; - [qualifier setFormatted: YES]; - } -} - - (NSString *)_sqlForSortOrderings:(NSArray *)_so { NSMutableString *sql; unsigned i, count; diff --git a/SoObjects/Contacts/SOGoContactFolder.h b/SoObjects/Contacts/SOGoContactFolder.h index 65843f6c9..2843851e0 100644 --- a/SoObjects/Contacts/SOGoContactFolder.h +++ b/SoObjects/Contacts/SOGoContactFolder.h @@ -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 +@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__ */ diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 8e4d86b79..59b55d44f 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -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 diff --git a/SoObjects/Contacts/SOGoContactSourceFolder.m b/SoObjects/Contacts/SOGoContactSourceFolder.m index a264c93f8..5afd887f3 100644 --- a/SoObjects/Contacts/SOGoContactSourceFolder.m +++ b/SoObjects/Contacts/SOGoContactSourceFolder.m @@ -25,6 +25,7 @@ #import #import #import +#import #import #import @@ -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 { diff --git a/SoObjects/Contacts/SOGoFolder+CardDAV.m b/SoObjects/Contacts/SOGoFolder+CardDAV.m index 55dd14a8f..686a779a4 100644 --- a/SoObjects/Contacts/SOGoFolder+CardDAV.m +++ b/SoObjects/Contacts/SOGoFolder+CardDAV.m @@ -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 #import #import +#import +#import #import #import #import #import +#import #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: @"" @""]; [r appendContentString: baseURL]; @@ -64,53 +68,75 @@ [r appendContentString: @"" @"" @""]; - etagLine = [NSString stringWithFormat: @"%@", - [component davEntityTag]]; - [r appendContentString: etagLine]; - [r appendContentString: @""]; - contactString = [[component contentAsString] safeStringByEscapingXMLString]; - [r appendContentString: contactString]; - [r appendContentString: @"" - @""]; - [r appendContentString: contactString]; - [r appendContentString: @"" - @"" - @"HTTP/1.1 200 OK" - @"" - @""]; + + + if ([properties containsObject: @"{DAV:}getetag"]) + { + etagLine = [NSString stringWithFormat: @"%@", + [component davEntityTag]]; + [r appendContentString: etagLine]; + } + + if ([properties containsObject: @"{urn:ietf:params:xml:ns:carddav}address-data"]) + { + [r appendContentString: @""]; + contactString = [[component contentAsString] safeStringByEscapingXMLString]; + [r appendContentString: contactString]; + [r appendContentString: @""]; + } + + if ([properties containsObject: @"{urn:ietf:params:xml:ns:carddav}addressbook-data"]) + { + [r appendContentString: @""]; + if (!contactString) + contactString = [[component contentAsString] safeStringByEscapingXMLString]; + [r appendContentString: contactString]; + [r appendContentString: @""]; + } + + [r appendContentString: @"" + @"HTTP/1.1 200 OK" + @"" + @""]; } } -- (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)self lookupContactsWithQualifier: qualifier + andSortOrdering: sort + inDomain: domain] objectEnumerator]; + + i = 0; + pool = [[NSAutoreleasePool alloc] init]; + while ((contact = [contacts nextObject])) { - currentFilter = [filters objectAtIndex: count]; - contacts = - [[(id)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 ) filterElement -{ - NSMutableDictionary *filterData; - id parentNode; - id 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 ) parentNode -{ - NSEnumerator *children; - id 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 ) filterElement // a prop-filter element +{ + NSMutableArray *qualifiers; + NSMutableArray *criteria; + NSString *name, *test; + NGDOMElement *match; + EOQualifier *qualifier; + id parentNode; + id 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)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 ) parentNode +{ + EOQualifier *qualifier, *currentQualifier; + NSEnumerator *children; + id 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 + - - - foo + + + foo */ - (id) davAddressbookQuery: (id) queryContext { + EOQualifier *qualifier; WOResponse *r; - NSArray *filters; + NSArray *properties; id document; + id 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: @""]; return r; diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index fdc57d1de..2768ab832 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -919,6 +919,29 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses return [EOQualifier qualifierWithQualifierFormat: qs]; } +- (void) addVCardProperty: (NSString *) property + toCriteria: (NSMutableArray *) criteria +{ + static NSDictionary *vCardLDAPFieldsTable = nil; + NSString *field; + + if (!vCardLDAPFieldsTable) + vCardLDAPFieldsTable = [[NSDictionary alloc] initWithObjectsAndKeys: + @"cn", @"fn", + @"cn", @"n", + @"mail", @"email", + @"telephonenumber", @"tel", + @"o", @"org", + @"l", @"adr", + nil]; + + field = [vCardLDAPFieldsTable objectForKey: property]; + if (field) + { + [criteria addObjectUniquely: field]; + } +} + /* - (NSArray *) _constraintsFields { @@ -1326,8 +1349,7 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses pool = [NSAutoreleasePool new]; while ((currentEntry = [entries nextObject])) { - [contacts addObject: - [self _convertLDAPEntryToContact: currentEntry]]; + [contacts addObject: [self _convertLDAPEntryToContact: currentEntry]]; i++; if (i % 10 == 0) { @@ -1524,6 +1546,55 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses andValue: theEmail]; } +- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier + andSortOrdering: (EOSortOrdering *) ordering + inDomain: (NSString *) domain +{ + NSAutoreleasePool *pool; + NGLdapConnection *ldapConnection; + NGLdapEntry *currentEntry; + NSEnumerator *entries; + NSMutableArray *contacts; + unsigned int i; + + contacts = [NSMutableArray array]; + + if ([qualifier count] > 0 || !_listRequiresDot) + { + ldapConnection = [self _ldapConnection]; + + // Perform the query if the qualifier is defined or if no critera was defined + if ([_scope caseInsensitiveCompare: @"BASE"] == NSOrderedSame) + entries = [ldapConnection baseSearchAtBaseDN: _baseDN + qualifier: qualifier + attributes: _lookupFields]; + else if ([_scope caseInsensitiveCompare: @"ONE"] == NSOrderedSame) + entries = [ldapConnection flatSearchAtBaseDN: _baseDN + qualifier: qualifier + attributes: _lookupFields]; + else /* we do it like before */ + entries = [ldapConnection deepSearchAtBaseDN: _baseDN + qualifier: qualifier + attributes: _lookupFields]; + + i = 0; + pool = [NSAutoreleasePool new]; + while ((currentEntry = [entries nextObject])) + { + [contacts addObject: [self _convertLDAPEntryToContact: currentEntry]]; + i++; + if (i % 10 == 0) + { + [pool release]; + pool = [NSAutoreleasePool new]; + } + } + [pool release]; + } + + return contacts; +} + - (void) setSourceID: (NSString *) newSourceID { ASSIGN(_sourceID, newSourceID); diff --git a/SoObjects/SOGo/SOGoSource.h b/SoObjects/SOGo/SOGoSource.h index d6ac6d785..c29f186a6 100644 --- a/SoObjects/SOGo/SOGoSource.h +++ b/SoObjects/SOGo/SOGoSource.h @@ -24,6 +24,8 @@ #import "SOGoConstants.h" +@class EOQualifier; +@class EOSortOrdering; @class NSDictionary; @class NSException; @class NSString; @@ -70,6 +72,9 @@ - (NSArray *) fetchContactsMatching: (NSString *) filter withCriteria: (NSArray *) criteria inDomain: (NSString *) domain; +- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier + andSortOrdering: (EOSortOrdering *) ordering + inDomain: (NSString *) domain; - (void) setSourceID: (NSString *) newSourceID; - (NSString *) sourceID; @@ -88,6 +93,9 @@ - (NSException *) updateContactEntry: (NSDictionary *) ldifRecord; - (NSException *) removeContactEntryWithID: (NSString *) aId; +- (void) addVCardProperty: (NSString *) property + toCriteria: (NSMutableArray *) criteria; + /* user address books */ - (NSArray *) addressBookSourcesForUser: (NSString *) user; diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index 7221afd89..6b167161a 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -30,7 +30,12 @@ #import #import #import +#import #import +#import +#import +#import +#import #import @@ -290,7 +295,7 @@ nil]; [qualifier autorelease]; } - [qualifier _gcsAppendToString: sql]; + [qualifier appendSQLToString: sql]; ex = [channel evaluateExpressionX: sql]; if (!ex) @@ -535,7 +540,7 @@ qualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers]; if (domainQualifier) qualifier = [[EOAndQualifier alloc] initWithQualifiers: domainQualifier, qualifier, nil]; - [qualifier _gcsAppendToString: sql]; + [qualifier appendSQLToString: sql]; ex = [channel evaluateExpressionX: sql]; if (!ex) @@ -623,7 +628,7 @@ qualifier = [[EOAndQualifier alloc] initWithQualifiers: q_uid, q_auth, nil]; [qualifier autorelease]; - [qualifier _gcsAppendToString: sql]; + [qualifier appendSQLToString: sql]; ex = [channel evaluateExpressionX: sql]; if (!ex) @@ -727,6 +732,29 @@ return [self _lookupContactEntry: entryID considerEmail: YES inDomain: domain]; } +- (void) addVCardProperty: (NSString *) property + toCriteria: (NSMutableArray *) criteria +{ + static NSDictionary *vCardSQLFieldsTable = nil; + NSString *field; + + if (!vCardSQLFieldsTable) + vCardSQLFieldsTable = [[NSDictionary alloc] initWithObjectsAndKeys: + @"c_cn", @"fn", + @"c_cn", @"n", + @"mail", @"email", + @"c_telephonenumber", @"tel", + @"c_o", @"org", + @"c_l", @"adr", + nil]; + + field = [vCardSQLFieldsTable objectForKey: property]; + if (field) + { + [criteria addObjectUniquely: field]; + } +} + /* Returns an EOQualifier of the following form: * (_domainField = domain OR _domainField = visibleDomain1 [...]) * Should only be called on SQL sources using _domainField name. @@ -802,7 +830,7 @@ if (domainQualifier) { [sql appendString: @" WHERE "]; - [domainQualifier _gcsAppendToString: sql]; + [domainQualifier appendSQLToString: sql]; } } else @@ -911,7 +939,7 @@ if (domainQualifier) { [sql appendFormat: @" AND ("]; - [domainQualifier _gcsAppendToString: sql]; + [domainQualifier appendSQLToString: sql]; [sql appendFormat: @")"]; } } @@ -948,6 +976,85 @@ return results; } +- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier + andSortOrdering: (EOSortOrdering *) ordering + inDomain: (NSString *) domain +{ + static EOAdaptor *adaptor = nil; + NSException *ex; + NSMutableArray *results; + NSMutableString *sql; + EOAdaptorChannel *channel; + GCSChannelManager *cm; + + results = [NSMutableArray array]; + + if (qualifier || !_listRequiresDot) + { + cm = [GCSChannelManager defaultChannelManager]; + channel = [cm acquireOpenChannelForURL: _viewURL]; + if (channel) + { + if (!adaptor) + { + EOAdaptorContext *adaptorCtx; + adaptorCtx = [channel adaptorContext]; + adaptor = [adaptorCtx adaptor]; + } + sql = [NSMutableString stringWithFormat: @"SELECT c_uid FROM %@ WHERE (", [_viewURL gcsTableName]]; + + if (qualifier) + [qualifier appendSQLToString: sql + withAdaptor: adaptor]; + else + [sql appendString: @"1 = 1"]; + [sql appendString: @")"]; + + if (_domainField) + { + if ([domain length]) + { + EOQualifier *domainQualifier; + domainQualifier = [self visibleDomainsQualifierFromDomain: domain]; + if (domainQualifier) + { + [sql appendFormat: @" AND ("]; + [domainQualifier appendSQLToString: sql]; + [sql appendFormat: @")"]; + } + } + else + [sql appendFormat: @" AND %@ IS NULL", _domainField]; + } + + ex = [channel evaluateExpressionX: sql]; + if (!ex) + { + NSDictionary *row; + NSMutableDictionary *mutableRow; + NSArray *attrs; + + attrs = [channel describeResults: NO]; + + while ((row = [channel fetchAttributes: attrs withZone: NULL])) + { + mutableRow = [row mutableCopy]; + // [mutableRow setObject: self forKey: @"source"]; + [results addObject: mutableRow]; + [mutableRow release]; + } + } + else + [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex]; + [cm releaseChannel: channel]; + } + [self errorWithFormat:@"failed to acquire channel for URL: %@", + [_viewURL absoluteString]]; + } + + return results; +} + - (void) setSourceID: (NSString *) newSourceID { } diff --git a/Tests/spec/CardDAVSpec.js b/Tests/spec/CardDAVSpec.js index f0aa342bf..45dc1ae77 100644 --- a/Tests/spec/CardDAVSpec.js +++ b/Tests/spec/CardDAVSpec.js @@ -27,8 +27,8 @@ TEL;TYPE=cell:portable TEL;TYPE=fax:fax TEL;TYPE=pager:pager X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com +EMAIL;TYPE=work:address.email1@domaine.ca +EMAIL;TYPE=home:address.email1@domaine2.com URL;TYPE=home:web perso TITLE:fonction URL;TYPE=work:page soc @@ -49,14 +49,14 @@ ORG:societe;service NICKNAME:surnom ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso -TEL;TYPE=work:+1 514 123-3372 +TEL;TYPE=work:+1 555 222-2222 TEL;TYPE=home:tel dom TEL;TYPE=cell:portable TEL;TYPE=fax:fax TEL;TYPE=pager:pager X-MOZILLA-HTML:FALSE -EMAIL;TYPE=work:address.email@domaine.ca -EMAIL;TYPE=home:address.email@domaine2.com +EMAIL;TYPE=work:address.email2@domaine.ca +EMAIL;TYPE=home:address.email2@domaine2.com URL;TYPE=home:web perso TITLE:fonction URL;TYPE=work:page soc @@ -96,6 +96,137 @@ describe('CardDAV extensions', function() { await webdav_su.deleteObject(resource) }) + // CARDDAV:addressbook-query Report + // https://datatracker.ietf.org/doc/html/rfc6352#section-8.6 + it("supports for addressbook-query on GCS folder", async function() { + const name = Object.keys(cards)[1] + const ns = DAVNamespaceShorthandMap[DAVNamespace.CARDDAV] + const response = await davRequest({ + url: webdav.serverUrl + resource, + init: { + method: 'REPORT', + namespace: ns, + headers: { ...webdav.headers, depth: '1' }, + body: { + 'addressbook-query': { + _attributes: getDAVAttribute([ + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'address-data', namespace: DAVNamespace.CARDDAV }]), + filter: { + _attributes: { test: 'anyof' }, + 'prop-filter': [ + { + _attributes: { name: 'FN', test: 'anyof' }, + 'text-match': [ + { + _attributes: { collation: 'i;unicasemap', 'match-type': 'starts-with' }, + _text: 'Carte modifiee' // should match the second card + }, + { + _attributes: { collation: 'i;unicasemap', 'match-type': 'contains' }, + _text: 'No match' // should not match any card + } + ] + }, + { + _attributes: { name: 'EMAIL', test: 'allof' }, + 'text-match': { + _attributes: { collation: 'i;unicasemap', 'match-type': 'starts-with' }, + _text: 'email2' // should match the second card + } + } + ] + } + } + }, + elementNameFn: (name) => { + if (!/^.+:.+/.test(name)) { + return `${ns}:${name}` + } + return name + } + } + }) + expect(response.length) + .withContext(`Number of results from addressbook-query`) + .toBe(1) + expect(response[0].status) + .withContext(`HTTP status code of addressbook-query`) + .toEqual(207) + expect(utility.componentsAreEqual(response[0].props.addressData, cards[name])) + .withContext(`Returned vCard matches ${name}`) + .toBe(true) + }) + + // CARDDAV:addressbook-query Report + // https://datatracker.ietf.org/doc/html/rfc6352#section-8.6 + it("supports for addressbook-query on source folder", async function() { + let vcard, emails + const ns = DAVNamespaceShorthandMap[DAVNamespace.CARDDAV] + const response = await davRequest({ + url: webdav.serverUrl + `/SOGo/dav/${config.username}/Contacts/public/`, + init: { + method: 'REPORT', + namespace: ns, + headers: { ...webdav.headers, depth: '1' }, + body: { + 'addressbook-query': { + _attributes: getDAVAttribute([ + DAVNamespace.CARDDAV, + DAVNamespace.DAV + ]), + [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'address-data', namespace: DAVNamespace.CARDDAV }]), + filter: { + _attributes: { test: 'anyof' }, + 'prop-filter': [ + { + _attributes: { name: 'FN', test: 'allof' }, + 'text-match': [ + { + _attributes: { collation: 'i;unicasemap', 'match-type': 'contains' }, + _text: 'No match' // should not match any card + } + ] + }, + { + _attributes: { name: 'EMAIL', test: 'allof' }, + 'text-match': { + _attributes: { collation: 'i;unicasemap', 'match-type': 'starts-with' }, + _text: `${config.attendee1}` + } + } + ] + } + } + }, + elementNameFn: (name) => { + if (!/^.+:.+/.test(name)) { + return `${ns}:${name}` + } + return name + } + } + }) + expect(response.length) + .withContext(`Number of results from addressbook-query`) + .toBe(1) + expect(response[0].status) + .withContext(`HTTP status code of addressbook-query`) + .toEqual(207) + + vcard = ICAL.Component.fromString(response[0].props.addressData.toString()) + emails = [] + for (const prop of vcard.getAllProperties('email')) { + emails.push(prop.getFirstValue()) + } + expect(emails) + .withContext(`Returned vCard has email of ${config.attendee1_username} (${config.attendee1})`) + .toContain(config.attendee1) + }) + + // CARDDAV:addressbook-multiget Report // https://datatracker.ietf.org/doc/html/rfc6352#section-8.7 it("supports for addressbook-multiget", async function() { const hrefs = Object.keys(cards).map(c => `${resource}${c}`)