]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Contacts/SOGoContactGCSFolder.m
26e0e974cd12c59a63deb8ba5850e6f047f4d78d
[scalable-opengroupware.org] / SoObjects / Contacts / SOGoContactGCSFolder.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSString.h>
24
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>
38
39 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
40 #import "SOGoContactGCSEntry.h"
41 #import "SOGoContactGCSFolder.h"
42
43 #define folderListingFields [NSArray arrayWithObjects: @"c_name", @"c_cn", \
44                                      @"c_givenname", @"c_sn", @"c_screenname", \
45                                      @"c_o", @"c_mail", @"c_telephonenumber", \
46                                      nil]
47
48 @implementation SOGoContactGCSFolder
49
50 /* name lookup */
51
52 - (id <SOGoContactObject>) lookupContactWithId: (NSString *) recordId
53 {
54   SOGoContactGCSEntry *contact;
55
56   if ([recordId length] > 0)
57     contact = [SOGoContactGCSEntry objectWithName: recordId
58                                    inContainer: self];
59   else
60     contact = nil;
61
62   return contact;
63 }
64
65 - (id) lookupName: (NSString *) _key
66         inContext: (WOContext *) _ctx
67           acquire: (BOOL) _flag
68 {
69   id obj;
70   BOOL isPut;
71
72   isPut = NO;
73   obj = [super lookupName:_key inContext:_ctx acquire:NO];
74   if (!obj)
75     {
76       if ([[[_ctx request] method] isEqualToString: @"PUT"])
77         {
78           if ([_key isEqualToString: @"PUT"])
79             isPut = YES;
80           else
81             obj = [SOGoContactGCSEntry objectWithName: _key
82                                        inContainer: self];
83         }
84       else
85         obj = [self lookupContactWithId: _key];
86     }
87 //   if (!(obj || isPut))
88 //     obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
89
90 // #if 0
91 //     if ([[self ocsFolder] versionOfContentWithName:_key])
92 // #endif
93 //       return [self contactWithName:_key inContext:_ctx];
94 //   }
95
96   /* return 404 to stop acquisition */
97   return obj;
98 }
99
100 /* fetching */
101 - (EOQualifier *) _qualifierForFilter: (NSString *) filter
102 {
103   NSString *qs;
104   EOQualifier *qualifier;
105
106   if (filter && [filter length] > 0)
107     {
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];
115     }
116   else
117     qualifier = nil;
118
119   return qualifier;
120 }
121
122 - (NSArray *) _flattenedRecords: (NSArray *) records
123 {
124   NSMutableArray *newRecords;
125   NSEnumerator *oldRecords;
126   NSDictionary *oldRecord;
127   NSMutableDictionary *newRecord;
128   NSString *data;
129   
130   newRecords = [NSMutableArray arrayWithCapacity: [records count]];
131
132   oldRecords = [records objectEnumerator];
133   oldRecord = [oldRecords nextObject];
134   while (oldRecord)
135     {
136       newRecord = [NSMutableDictionary new];
137       [newRecord autorelease];
138
139       [newRecord setObject: [oldRecord objectForKey: @"c_name"]
140                  forKey: @"c_uid"];
141       [newRecord setObject: [oldRecord objectForKey: @"c_name"]
142                  forKey: @"c_name"];
143
144       data = [oldRecord objectForKey: @"c_cn"];
145       if (![data length])
146         data = [oldRecord keysWithFormat: @"%{c_givenname} %{c_sn}"];
147       [newRecord setObject: data
148                  forKey: @"displayName"];
149
150       data = [oldRecord objectForKey: @"c_mail"];
151       if (!data)
152         data = @"";
153       [newRecord setObject: data forKey: @"mail"];
154
155       data = [oldRecord objectForKey: @"c_screenname"];
156       if (!data)
157         data = @"";
158       [newRecord setObject: data forKey: @"screenName"];
159
160       data = [oldRecord objectForKey: @"c_o"];
161       if (!data)
162         data = @"";
163       [newRecord setObject: data forKey: @"org"];
164
165       data = [oldRecord objectForKey: @"c_telephonenumber"];
166       if (![data length])
167         data = @"";
168       [newRecord setObject: data forKey: @"phone"];
169
170       [newRecords addObject: newRecord];
171       oldRecord = [oldRecords nextObject];
172     }
173
174   return newRecords;
175 }
176
177 - (BOOL) _isValidFilter: (NSString *) theString
178 {
179   if ([theString caseInsensitiveCompare: @"sn"] == NSOrderedSame)
180     return YES;
181
182   if ([theString caseInsensitiveCompare: @"givenname"] == NSOrderedSame)
183     return YES;
184
185   if ([theString caseInsensitiveCompare: @"mail"] == NSOrderedSame)
186     return YES;
187
188   if ([theString caseInsensitiveCompare: @"telephonenumber"] == NSOrderedSame)
189     return YES;
190
191   return NO;
192 }
193
194 - (NSDictionary *) _parseContactFilter: (id <DOMElement>) filterElement
195 {
196   NSMutableDictionary *filterData;
197   id <DOMNode> parentNode;
198   id <DOMNodeList> ranges;
199
200   parentNode = [filterElement parentNode];
201
202   if ([[parentNode tagName] isEqualToString: @"filter"] &&
203       [self _isValidFilter: [filterElement attribute: @"name"]])
204     {
205       ranges = [filterElement getElementsByTagName: @"text-match"];
206      
207       if ([ranges count] && [[[ranges objectAtIndex: 0] childNodes] count])
208         {
209           filterData = [NSMutableDictionary new];
210           [filterData autorelease];
211           [filterData setObject: [[[[ranges objectAtIndex: 0] childNodes] lastObject] data]
212                       forKey: [filterElement attribute: @"name"]];
213         }
214     }
215   else
216     filterData = nil;
217
218   return filterData;
219 }
220
221 #warning filters is leaked here
222 - (NSArray *) _parseContactFilters: (id <DOMElement>) parentNode
223 {
224   NSEnumerator *children;
225   id<DOMElement> node;
226   NSMutableArray *filters;
227   NSDictionary *filter;
228
229   filters = [NSMutableArray new];
230
231   children = [[parentNode getElementsByTagName: @"prop-filter"]
232                objectEnumerator];
233
234   node = [children nextObject];
235
236   while (node)
237     {
238       filter = [self _parseContactFilter: node];
239       if (filter)
240         [filters addObject: filter];
241       node = [children nextObject];
242     }
243
244   return filters;
245 }
246
247 - (void) appendObject: (NSDictionary *) object
248           withBaseURL: (NSString *) baseURL
249      toREPORTResponse: (WOResponse *) r
250 {
251   SOGoContactGCSEntry *component;
252   Class componentClass;
253   NSString *name, *etagLine, *contactString;
254
255   name = [object objectForKey: @"c_name"];
256   componentClass = [SOGoContactGCSEntry class];
257
258   component = [componentClass objectWithName: name inContainer: self];
259
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"];
267
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"];
281 }
282
283 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
284                                toResponse: (WOResponse *) response
285 {
286   unsigned int count, max;
287   NSDictionary *currentFilter, *contact;
288   NSEnumerator *contacts;
289   NSString *baseURL;
290
291   baseURL = [self baseURLInContext: context];
292
293   max = [filters count];
294   for (count = 0; count < max; count++)
295     {
296       currentFilter = [filters objectAtIndex: count];
297       contacts = [[self lookupContactsWithFilter: [[currentFilter allValues] lastObject]
298                         sortBy: @"c_givenname"
299                         ordering: NSOrderedDescending]
300                    objectEnumerator];
301       
302       while ((contact = [contacts nextObject]))
303       {
304           [self appendObject: contact
305                 withBaseURL: baseURL
306                 toREPORTResponse: response];
307         }
308     }
309 }
310
311 - (NSArray *) lookupContactsWithFilter: (NSString *) filter
312                                 sortBy: (NSString *) sortKey
313                               ordering: (NSComparisonResult) sortOrdering
314 {
315   NSArray *fields, *dbRecords, *records;
316   EOQualifier *qualifier;
317   EOSortOrdering *ordering;
318
319   fields = folderListingFields;
320   qualifier = [self _qualifierForFilter: filter];
321   dbRecords = [[self ocsFolder] fetchFields: fields
322                                 matchingQualifier: qualifier];
323
324   if ([dbRecords count] > 0)
325     {
326       records = [self _flattenedRecords: dbRecords];
327       ordering
328         = [EOSortOrdering sortOrderingWithKey: sortKey
329                           selector: ((sortOrdering == NSOrderedDescending)
330                                      ? EOCompareCaseInsensitiveDescending
331                                      : EOCompareCaseInsensitiveAscending)];
332       records
333         = [records sortedArrayUsingKeyOrderArray:
334                      [NSArray arrayWithObject: ordering]];
335     }
336   else
337     records = nil;
338 //   else
339 //     [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
340
341   [self debugWithFormat:@"fetched %i records.", [records count]];
342   return records;
343 }
344
345 - (NSArray *) davNamespaces
346 {
347   return [NSArray arrayWithObject: @"urn:ietf:params:xml:ns:carddav"];
348 }
349
350 - (id) davAddressbookQuery: (id) queryContext
351 {
352   WOResponse *r;
353   NSArray *filters;
354   id <DOMDocument> document;
355
356   r = [context response];
357   [r setStatus: 207];
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"];
365
366   document = [[context request] contentAsDOMDocument];
367   filters = [self _parseContactFilters: [document documentElement]];
368
369   [self _appendComponentsMatchingFilters: filters
370         toResponse: r];
371   [r appendContentString:@"</D:multistatus>\r\n"];
372
373   return r;
374 }
375
376 - (NSArray *) davComplianceClassesInContext: (id)_ctx
377 {
378   NSMutableArray *classes;
379   NSArray *primaryClasses;
380
381   classes = [NSMutableArray new];
382   [classes autorelease];
383
384   primaryClasses = [super davComplianceClassesInContext: _ctx];
385   if (primaryClasses)
386     [classes addObjectsFromArray: primaryClasses];
387   [classes addObject: @"access-control"];
388   [classes addObject: @"addressbook-access"];
389
390   return classes;
391 }
392
393 - (NSString *) groupDavResourceType
394 {
395   return @"vcard-collection";
396 }
397
398 // /* GET */
399
400 // - (id) GETAction: (id)_ctx
401 // {
402 //   // TODO: I guess this should really be done by SOPE (redirect to
403 //   //       default method)
404 //   WOResponse *r;
405 //   NSString *uri;
406
407 //   uri = [[_ctx request] uri];
408 //   if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
409 //   uri = [uri stringByAppendingString:@"view"];
410   
411 //   r = [_ctx response];
412 //   [r setStatus:302 /* moved */];
413 //   [r setHeader:uri forKey:@"location"];
414 //   return r;
415 // }
416
417 /* sorting */
418 - (NSComparisonResult) compare: (id) otherFolder
419 {
420   NSComparisonResult comparison;
421
422   if ([NSStringFromClass([otherFolder class])
423                         isEqualToString: @"SOGoContactLDAPFolder"])
424     comparison = NSOrderedAscending;
425   else
426     comparison = [super compare: otherFolder];
427
428   return comparison;
429 }
430
431 /* folder type */
432
433 - (NSString *) folderType
434 {
435   return @"Contact";
436 }
437
438 - (NSString *) outlookFolderClass
439 {
440   return @"IPF.Contact";
441 }
442
443 @end /* SOGoContactGCSFolder */