/* NSObject+CardDAV.m - this file is part of SOGo * * 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 * 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 "SOGoContactFolder.h" #import "SOGoContactGCSEntry.h" @implementation SOGoFolder (CardDAV) - (void) _appendProperties: (NSArray *) properties forObject: (NSDictionary *) object withBaseURL: (NSString *) baseURL toREPORTResponse: (WOResponse *) r { id component; NSString *name, *etagLine, *contactString; 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.", name]; return; } [r appendContentString: @"" @""]; [r appendContentString: baseURL]; if (![baseURL hasSuffix: @"/"]) [r appendContentString: @"/"]; [r appendContentString: name]; [r appendContentString: @"" @"" @""]; 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) _appendComponentsProperties: (NSArray *) properties matchingQualifier: (EOQualifier *) qualifier toResponse: (WOResponse *) response context: (id) localContext { EOSortOrdering *sort; NSAutoreleasePool *pool; NSDictionary *contact; NSMutableArray *names; NSEnumerator *contacts; 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]; contacts = [[(id)self lookupContactsWithQualifier: qualifier andSortOrdering: sort inDomain: domain] objectEnumerator]; i = 0; pool = [[NSAutoreleasePool alloc] init]; while ((contact = [contacts nextObject])) { // Don't append suspected duplicates name = [contact objectForKey: @"c_name"]; // primary key of contacts if (![names containsObject: name]) { [self _appendProperties: properties forObject: contact withBaseURL: baseURL toREPORTResponse: response]; [names addObject: name]; if (i % 10 == 0) { RELEASE(pool); pool = [[NSAutoreleasePool alloc] init]; } i++; } } 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: @"mail"] || [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 */ - (id) davAddressbookQuery: (id) queryContext { EOQualifier *qualifier; WOResponse *r; NSArray *properties; id document; id documentElement, propElement; r = [queryContext response]; [r prepareDAVResponse]; [r appendContentString: @""]; document = [[queryContext request] contentAsDOMDocument]; documentElement = [document documentElement]; propElement = [(NGDOMNodeWithChildren *) documentElement firstElementWithTag: @"prop" inNamespace: @"DAV:"]; properties = [(NGDOMNodeWithChildren *) propElement flatPropertyNameOfSubElements]; qualifier = [self _parseContactFilters: documentElement]; [self _appendComponentsProperties: properties matchingQualifier: qualifier toResponse: r context: queryContext]; [r appendContentString: @""]; return r; } @end