mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-02-17 07:33:57 +00:00
335 lines
11 KiB
Objective-C
335 lines
11 KiB
Objective-C
/* 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 <Foundation/NSAutoreleasePool.h>
|
|
#import <Foundation/NSDictionary.h>
|
|
|
|
#import <NGObjWeb/WOContext+SoObjects.h>
|
|
#import <NGObjWeb/WORequest.h>
|
|
#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) _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: @"<D:response>"
|
|
@"<D:href>"];
|
|
[r appendContentString: baseURL];
|
|
if (![baseURL hasSuffix: @"/"])
|
|
[r appendContentString: @"/"];
|
|
[r appendContentString: name];
|
|
[r appendContentString: @"</D:href>"
|
|
@"<D:propstat>"
|
|
@"<D:prop>"];
|
|
|
|
|
|
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) _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<SOGoContactFolder>)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 <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 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 *properties;
|
|
id <DOMDocument> document;
|
|
id <DOMElement> documentElement, propElement;
|
|
|
|
r = [queryContext response];
|
|
[r prepareDAVResponse];
|
|
[r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
|
|
@" xmlns:C=\"urn:ietf:params:xml:ns:carddav\">"];
|
|
|
|
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: @"</D:multistatus>"];
|
|
|
|
return r;
|
|
}
|
|
|
|
@end
|