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/WOResponse.h>
29 #import <NGObjWeb/WORequest.h>
30 #import <NGExtensions/NSObject+Logs.h>
31 #import <NGExtensions/NSString+misc.h>
32 #import <EOControl/EOQualifier.h>
33 #import <EOControl/EOSortOrdering.h>
34 #import <GDLContentStore/GCSFolder.h>
35 #import <DOM/DOMProtocols.h>
36 #import <SaxObjC/SaxObjC.h>
37 #import <SaxObjC/XMLNamespaces.h>
39 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
40 #import "SOGoContactGCSEntry.h"
41 #import "SOGoContactGCSFolder.h"
43 #define folderListingFields [NSArray arrayWithObjects: @"c_name", @"c_cn", \
44 @"c_givenname", @"c_sn", @"c_screenname", \
45 @"c_o", @"c_mail", @"c_telephonenumber", \
48 @implementation SOGoContactGCSFolder
52 - (id <SOGoContactObject>) lookupContactWithId: (NSString *) recordId
54 SOGoContactGCSEntry *contact;
56 if ([recordId length] > 0)
57 contact = [SOGoContactGCSEntry objectWithName: recordId
65 - (id) lookupName: (NSString *) _key
66 inContext: (WOContext *) _ctx
73 obj = [super lookupName:_key inContext:_ctx acquire:NO];
76 if ([[[_ctx request] method] isEqualToString: @"PUT"])
78 if ([_key isEqualToString: @"PUT"])
81 obj = [SOGoContactGCSEntry objectWithName: _key
85 obj = [self lookupContactWithId: _key];
87 // if (!(obj || isPut))
88 // obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
91 // if ([[self ocsFolder] versionOfContentWithName:_key])
93 // return [self contactWithName:_key inContext:_ctx];
96 /* return 404 to stop acquisition */
101 - (EOQualifier *) _qualifierForFilter: (NSString *) filter
104 EOQualifier *qualifier;
106 if (filter && [filter length] > 0)
108 qs = [NSString stringWithFormat:
109 @"(c_sn isCaseInsensitiveLike: '%@%%') OR "
110 @"(c_givenname isCaseInsensitiveLike: '%@%%') OR "
111 @"(c_mail isCaseInsensitiveLike: '%@%%') OR "
112 @"(c_telephonenumber isCaseInsensitiveLike: '%%%@%%')",
113 filter, filter, filter, filter];
114 qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
122 - (NSArray *) _flattenedRecords: (NSArray *) records
124 NSMutableArray *newRecords;
125 NSEnumerator *oldRecords;
126 NSDictionary *oldRecord;
127 NSMutableDictionary *newRecord;
130 newRecords = [NSMutableArray arrayWithCapacity: [records count]];
132 oldRecords = [records objectEnumerator];
133 oldRecord = [oldRecords nextObject];
136 newRecord = [NSMutableDictionary new];
137 [newRecord autorelease];
139 [newRecord setObject: [oldRecord objectForKey: @"c_name"]
141 [newRecord setObject: [oldRecord objectForKey: @"c_name"]
144 data = [oldRecord objectForKey: @"c_cn"];
146 data = [oldRecord keysWithFormat: @"%{c_givenname} %{c_sn}"];
147 [newRecord setObject: data
148 forKey: @"displayName"];
150 data = [oldRecord objectForKey: @"c_mail"];
153 [newRecord setObject: data forKey: @"mail"];
155 data = [oldRecord objectForKey: @"c_screenname"];
158 [newRecord setObject: data forKey: @"screenName"];
160 data = [oldRecord objectForKey: @"c_o"];
163 [newRecord setObject: data forKey: @"org"];
165 data = [oldRecord objectForKey: @"c_telephonenumber"];
168 [newRecord setObject: data forKey: @"phone"];
170 [newRecords addObject: newRecord];
171 oldRecord = [oldRecords nextObject];
177 - (BOOL) _isValidFilter: (NSString *) theString
179 if ([theString caseInsensitiveCompare: @"sn"] == NSOrderedSame)
182 if ([theString caseInsensitiveCompare: @"givenname"] == NSOrderedSame)
185 if ([theString caseInsensitiveCompare: @"mail"] == NSOrderedSame)
188 if ([theString caseInsensitiveCompare: @"telephonenumber"] == NSOrderedSame)
194 - (NSDictionary *) _parseContactFilter: (id <DOMElement>) filterElement
196 NSMutableDictionary *filterData;
197 id <DOMNode> parentNode;
198 id <DOMNodeList> ranges;
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) appendObject: (NSDictionary *) object
248 withBaseURL: (NSString *) baseURL
249 toREPORTResponse: (WOResponse *) r
251 SOGoContactGCSEntry *component;
252 Class componentClass;
253 NSString *name, *etagLine, *contactString;
255 name = [object objectForKey: @"c_name"];
256 componentClass = [SOGoContactGCSEntry class];
258 component = [componentClass objectWithName: name inContainer: self];
260 [r appendContentString: @" <D:response>\r\n"];
261 [r appendContentString: @" <D:href>"];
262 [r appendContentString: baseURL];
263 if (![baseURL hasSuffix: @"/"])
264 [r appendContentString: @"/"];
265 [r appendContentString: name];
266 [r appendContentString: @"</D:href>\r\n"];
268 [r appendContentString: @" <D:propstat>\r\n"];
269 [r appendContentString: @" <D:prop>\r\n"];
270 etagLine = [NSString stringWithFormat: @" <D:getetag>%@</D:getetag>\r\n",
271 [component davEntityTag]];
272 [r appendContentString: etagLine];
273 [r appendContentString: @" </D:prop>\r\n"];
274 [r appendContentString: @" <D:status>HTTP/1.1 200 OK</D:status>\r\n"];
275 [r appendContentString: @" </D:propstat>\r\n"];
276 [r appendContentString: @" <C:addressbook-data>"];
277 contactString = [[component contentAsString] stringByEscapingXMLString];
278 [r appendContentString: contactString];
279 [r appendContentString: @"</C:addressbook-data>\r\n"];
280 [r appendContentString: @" </D:response>\r\n"];
283 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
284 toResponse: (WOResponse *) response
286 unsigned int count, max;
287 NSDictionary *currentFilter, *contact;
288 NSEnumerator *contacts;
291 baseURL = [self baseURLInContext: context];
293 max = [filters count];
294 for (count = 0; count < max; count++)
296 currentFilter = [filters objectAtIndex: count];
297 contacts = [[self lookupContactsWithFilter: [[currentFilter allValues] lastObject]
298 sortBy: @"c_givenname"
299 ordering: NSOrderedDescending]
302 while ((contact = [contacts nextObject]))
304 [self appendObject: contact
306 toREPORTResponse: response];
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 */