2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSString.h>
25 #import <NGObjWeb/NSException+HTTP.h>
26 #import <NGObjWeb/SoObject+SoDAV.h>
27 #import <NGObjWeb/WOContext.h>
28 #import <NGObjWeb/WORequest.h>
29 #import <NGExtensions/NSObject+Logs.h>
30 #import <EOControl/EOQualifier.h>
31 #import <EOControl/EOSortOrdering.h>
32 #import <GDLContentStore/GCSFolder.h>
33 #import <DOM/DOMProtocols.h>
34 #import <SaxObjC/SaxObjC.h>
35 #import <SaxObjC/XMLNamespaces.h>
37 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
38 #import "SOGoContactGCSEntry.h"
39 #import "SOGoContactGCSFolder.h"
41 #define folderListingFields [NSArray arrayWithObjects: @"c_name", @"c_cn", \
42 @"c_givenname", @"c_sn", @"c_screenname", \
43 @"c_o", @"c_mail", @"c_telephonenumber", \
46 @implementation SOGoContactGCSFolder
50 - (id <SOGoContactObject>) lookupContactWithId: (NSString *) recordId
52 SOGoContactGCSEntry *contact;
54 if ([recordId length] > 0)
55 contact = [SOGoContactGCSEntry objectWithName: recordId
63 - (id) lookupName: (NSString *) _key
64 inContext: (WOContext *) _ctx
71 obj = [super lookupName:_key inContext:_ctx acquire:NO];
74 if ([[[_ctx request] method] isEqualToString: @"PUT"])
76 if ([_key isEqualToString: @"PUT"])
79 obj = [SOGoContactGCSEntry objectWithName: _key
83 obj = [self lookupContactWithId: _key];
85 // if (!(obj || isPut))
86 // obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
89 // if ([[self ocsFolder] versionOfContentWithName:_key])
91 // return [self contactWithName:_key inContext:_ctx];
94 /* return 404 to stop acquisition */
99 - (EOQualifier *) _qualifierForFilter: (NSString *) filter
102 EOQualifier *qualifier;
104 if (filter && [filter length] > 0)
106 #warning why we do not use %%%@%% everywhere?
107 qs = [NSString stringWithFormat:
108 @"(c_sn isCaseInsensitiveLike: '%@%%') OR "
109 @"(c_givenname isCaseInsensitiveLike: '%@%%') OR "
110 @"(c_mail isCaseInsensitiveLike: '%@%%') OR "
111 @"(c_telephonenumber isCaseInsensitiveLike: '%%%@%%')",
112 filter, filter, filter, filter];
113 qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
121 - (NSArray *) _flattenedRecords: (NSArray *) records
123 NSMutableArray *newRecords;
124 NSEnumerator *oldRecords;
125 NSDictionary *oldRecord;
126 NSMutableDictionary *newRecord;
129 newRecords = [NSMutableArray arrayWithCapacity: [records count]];
131 oldRecords = [records objectEnumerator];
132 oldRecord = [oldRecords nextObject];
135 newRecord = [NSMutableDictionary new];
136 [newRecord autorelease];
138 [newRecord setObject: [oldRecord objectForKey: @"c_name"]
140 [newRecord setObject: [oldRecord objectForKey: @"c_name"]
143 data = [oldRecord objectForKey: @"c_cn"];
145 data = [oldRecord keysWithFormat: @"%{c_givenname} %{c_sn}"];
146 [newRecord setObject: data
147 forKey: @"displayName"];
149 data = [oldRecord objectForKey: @"c_mail"];
152 [newRecord setObject: data forKey: @"mail"];
154 data = [oldRecord objectForKey: @"c_screenname"];
157 [newRecord setObject: data forKey: @"screenName"];
159 data = [oldRecord objectForKey: @"c_o"];
162 [newRecord setObject: data forKey: @"org"];
164 data = [oldRecord objectForKey: @"c_telephonenumber"];
167 [newRecord setObject: data forKey: @"phone"];
169 [newRecords addObject: newRecord];
170 oldRecord = [oldRecords nextObject];
176 - (BOOL) _isValidFilter: (NSString *) theString
178 if ([theString caseInsensitiveCompare: @"sn"] == NSOrderedSame)
181 if ([theString caseInsensitiveCompare: @"givenname"] == NSOrderedSame)
184 if ([theString caseInsensitiveCompare: @"mail"] == NSOrderedSame)
187 if ([theString caseInsensitiveCompare: @"telephonenumber"] == NSOrderedSame)
193 - (NSDictionary *) _parseContactFilter: (id <DOMElement>) filterElement
195 NSMutableDictionary *filterData;
196 id <DOMNode> parentNode;
197 id <DOMNodeList> ranges;
198 NSString *componentName;
200 parentNode = [filterElement parentNode];
202 if ([[parentNode tagName] isEqualToString: @"filter"] &&
203 [self _isValidFilter: [filterElement attribute: @"name"]])
205 ranges = [filterElement getElementsByTagName: @"text-match"];
207 if ([ranges count] && [[[ranges objectAtIndex: 0] childNodes] count])
209 filterData = [NSMutableDictionary new];
210 [filterData autorelease];
211 [filterData setObject: [[[[ranges objectAtIndex: 0] childNodes] lastObject] data]
212 forKey: [filterElement attribute: @"name"]];
221 #warning filters is leaked here
222 - (NSArray *) _parseContactFilters: (id <DOMElement>) parentNode
224 NSEnumerator *children;
226 NSMutableArray *filters;
227 NSDictionary *filter;
229 filters = [NSMutableArray new];
231 children = [[parentNode getElementsByTagName: @"prop-filter"]
234 node = [children nextObject];
238 filter = [self _parseContactFilter: node];
240 [filters addObject: filter];
241 node = [children nextObject];
247 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
248 toResponse: (WOResponse *) response
250 unsigned int count, max;
251 NSDictionary *currentFilter, *contact;
252 NSEnumerator *contacts;
255 baseURL = [self baseURLInContext: context];
257 max = [filters count];
258 for (count = 0; count < max; count++)
260 currentFilter = [filters objectAtIndex: count];
261 contacts = [[self lookupContactsWithFilter: [[currentFilter allValues] lastObject]
262 sortBy: @"c_givenname"
263 ordering: NSOrderedDescending]
266 while ((contact = [contacts nextObject]))
268 [self appendObject: contact
270 toREPORTResponse: response];
275 - (void) appendObject: (NSDictionary *) object
276 withBaseURL: (NSString *) baseURL
277 toREPORTResponse: (WOResponse *) r
279 SOGoContactGCSEntry *component;
280 Class componentClass;
281 NSString *name, *etagLine, *contactString;
283 name = [object objectForKey: @"c_name"];
284 componentClass = [SOGoContactGCSEntry class];
286 component = [componentClass objectWithName: name inContainer: self];
288 [r appendContentString: @" <D:response>\r\n"];
289 [r appendContentString: @" <D:href>"];
290 [r appendContentString: baseURL];
291 if (![baseURL hasSuffix: @"/"])
292 [r appendContentString: @"/"];
293 [r appendContentString: name];
294 [r appendContentString: @"</D:href>\r\n"];
296 [r appendContentString: @" <D:propstat>\r\n"];
297 [r appendContentString: @" <D:prop>\r\n"];
298 etagLine = [NSString stringWithFormat: @" <D:getetag>%@</D:getetag>\r\n",
299 [component davEntityTag]];
300 [r appendContentString: etagLine];
301 [r appendContentString: @" </D:prop>\r\n"];
302 [r appendContentString: @" <D:status>HTTP/1.1 200 OK</D:status>\r\n"];
303 [r appendContentString: @" </D:propstat>\r\n"];
304 [r appendContentString: @" <C:addressbook-data>"];
305 contactString = [[component contentAsString] stringByEscapingXMLString];
306 [r appendContentString: contactString];
307 [r appendContentString: @"</C:addressbook-data>\r\n"];
308 [r appendContentString: @" </D:response>\r\n"];
311 - (NSArray *) lookupContactsWithFilter: (NSString *) filter
312 sortBy: (NSString *) sortKey
313 ordering: (NSComparisonResult) sortOrdering
315 NSArray *fields, *dbRecords, *records;
316 EOQualifier *qualifier;
317 EOSortOrdering *ordering;
319 fields = folderListingFields;
320 qualifier = [self _qualifierForFilter: filter];
321 dbRecords = [[self ocsFolder] fetchFields: fields
322 matchingQualifier: qualifier];
324 if ([dbRecords count] > 0)
326 records = [self _flattenedRecords: dbRecords];
328 = [EOSortOrdering sortOrderingWithKey: sortKey
329 selector: ((sortOrdering == NSOrderedDescending)
330 ? EOCompareCaseInsensitiveDescending
331 : EOCompareCaseInsensitiveAscending)];
333 = [records sortedArrayUsingKeyOrderArray:
334 [NSArray arrayWithObject: ordering]];
339 // [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
341 [self debugWithFormat:@"fetched %i records.", [records count]];
345 - (NSArray *) davNamespaces
347 return [NSArray arrayWithObject: @"urn:ietf:params:xml:ns:carddav"];
350 - (id) davAddressbookQuery: (id) queryContext
354 id <DOMDocument> document;
356 r = [context response];
358 [r setContentEncoding: NSUTF8StringEncoding];
359 [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"];
360 [r setHeader: @"no-cache" forKey: @"pragma"];
361 [r setHeader: @"no-cache" forKey: @"cache-control"];
362 [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
363 [r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
364 @" xmlns:C=\"urn:ietf:params:xml:ns:carddav\">\r\n"];
366 document = [[context request] contentAsDOMDocument];
367 filters = [self _parseContactFilters: [document documentElement]];
369 [self _appendComponentsMatchingFilters: filters
371 [r appendContentString:@"</D:multistatus>\r\n"];
376 - (NSArray *) davComplianceClassesInContext: (id)_ctx
378 NSMutableArray *classes;
379 NSArray *primaryClasses;
381 classes = [NSMutableArray new];
382 [classes autorelease];
384 primaryClasses = [super davComplianceClassesInContext: _ctx];
386 [classes addObjectsFromArray: primaryClasses];
387 [classes addObject: @"access-control"];
388 [classes addObject: @"addressbook-access"];
393 - (NSString *) groupDavResourceType
395 return @"vcard-collection";
400 // - (id) GETAction: (id)_ctx
402 // // TODO: I guess this should really be done by SOPE (redirect to
403 // // default method)
407 // uri = [[_ctx request] uri];
408 // if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
409 // uri = [uri stringByAppendingString:@"view"];
411 // r = [_ctx response];
412 // [r setStatus:302 /* moved */];
413 // [r setHeader:uri forKey:@"location"];
418 - (NSComparisonResult) compare: (id) otherFolder
420 NSComparisonResult comparison;
422 if ([NSStringFromClass([otherFolder class])
423 isEqualToString: @"SOGoContactLDAPFolder"])
424 comparison = NSOrderedAscending;
426 comparison = [super compare: otherFolder];
433 - (NSString *) folderType
438 - (NSString *) outlookFolderClass
440 return @"IPF.Contact";
443 @end /* SOGoContactGCSFolder */