]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Contacts/SOGoContactGCSFolder.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1185 d1b88da0-ebda-0310...
[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/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>
36
37 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
38 #import "SOGoContactGCSEntry.h"
39 #import "SOGoContactGCSFolder.h"
40
41 #define folderListingFields [NSArray arrayWithObjects: @"c_name", @"c_cn", \
42                                      @"c_givenname", @"c_sn", @"c_screenname", \
43                                      @"c_o", @"c_mail", @"c_telephonenumber", \
44                                      nil]
45
46 @implementation SOGoContactGCSFolder
47
48 /* name lookup */
49
50 - (id <SOGoContactObject>) lookupContactWithId: (NSString *) recordId
51 {
52   SOGoContactGCSEntry *contact;
53
54   if ([recordId length] > 0)
55     contact = [SOGoContactGCSEntry objectWithName: recordId
56                                    inContainer: self];
57   else
58     contact = nil;
59
60   return contact;
61 }
62
63 - (id) lookupName: (NSString *) _key
64         inContext: (WOContext *) _ctx
65           acquire: (BOOL) _flag
66 {
67   id obj;
68   BOOL isPut;
69
70   isPut = NO;
71   obj = [super lookupName:_key inContext:_ctx acquire:NO];
72   if (!obj)
73     {
74       if ([[[_ctx request] method] isEqualToString: @"PUT"])
75         {
76           if ([_key isEqualToString: @"PUT"])
77             isPut = YES;
78           else
79             obj = [SOGoContactGCSEntry objectWithName: _key
80                                        inContainer: self];
81         }
82       else
83         obj = [self lookupContactWithId: _key];
84     }
85 //   if (!(obj || isPut))
86 //     obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
87
88 // #if 0
89 //     if ([[self ocsFolder] versionOfContentWithName:_key])
90 // #endif
91 //       return [self contactWithName:_key inContext:_ctx];
92 //   }
93
94   /* return 404 to stop acquisition */
95   return obj;
96 }
97
98 /* fetching */
99 - (EOQualifier *) _qualifierForFilter: (NSString *) filter
100 {
101   NSString *qs;
102   EOQualifier *qualifier;
103
104   if (filter && [filter length] > 0)
105     {
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];
114     }
115   else
116     qualifier = nil;
117
118   return qualifier;
119 }
120
121 - (NSArray *) _flattenedRecords: (NSArray *) records
122 {
123   NSMutableArray *newRecords;
124   NSEnumerator *oldRecords;
125   NSDictionary *oldRecord;
126   NSMutableDictionary *newRecord;
127   NSString *data;
128   
129   newRecords = [NSMutableArray arrayWithCapacity: [records count]];
130
131   oldRecords = [records objectEnumerator];
132   oldRecord = [oldRecords nextObject];
133   while (oldRecord)
134     {
135       newRecord = [NSMutableDictionary new];
136       [newRecord autorelease];
137
138       [newRecord setObject: [oldRecord objectForKey: @"c_name"]
139                  forKey: @"c_uid"];
140       [newRecord setObject: [oldRecord objectForKey: @"c_name"]
141                  forKey: @"c_name"];
142
143       data = [oldRecord objectForKey: @"c_cn"];
144       if (![data length])
145         data = [oldRecord keysWithFormat: @"%{c_givenname} %{c_sn}"];
146       [newRecord setObject: data
147                  forKey: @"displayName"];
148
149       data = [oldRecord objectForKey: @"c_mail"];
150       if (!data)
151         data = @"";
152       [newRecord setObject: data forKey: @"mail"];
153
154       data = [oldRecord objectForKey: @"c_screenname"];
155       if (!data)
156         data = @"";
157       [newRecord setObject: data forKey: @"screenName"];
158
159       data = [oldRecord objectForKey: @"c_o"];
160       if (!data)
161         data = @"";
162       [newRecord setObject: data forKey: @"org"];
163
164       data = [oldRecord objectForKey: @"c_telephonenumber"];
165       if (![data length])
166         data = @"";
167       [newRecord setObject: data forKey: @"phone"];
168
169       [newRecords addObject: newRecord];
170       oldRecord = [oldRecords nextObject];
171     }
172
173   return newRecords;
174 }
175
176 - (BOOL) _isValidFilter: (NSString *) theString
177 {
178   if ([theString caseInsensitiveCompare: @"sn"] == NSOrderedSame)
179     return YES;
180
181   if ([theString caseInsensitiveCompare: @"givenname"] == NSOrderedSame)
182     return YES;
183
184   if ([theString caseInsensitiveCompare: @"mail"] == NSOrderedSame)
185     return YES;
186
187   if ([theString caseInsensitiveCompare: @"telephonenumber"] == NSOrderedSame)
188     return YES;
189
190   return NO;
191 }
192
193 - (NSDictionary *) _parseContactFilter: (id <DOMElement>) filterElement
194 {
195   NSMutableDictionary *filterData;
196   id <DOMNode> parentNode;
197   id <DOMNodeList> ranges;
198   NSString *componentName;
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) _appendComponentsMatchingFilters: (NSArray *) filters
248                                toResponse: (WOResponse *) response
249 {
250   unsigned int count, max;
251   NSDictionary *currentFilter, *contact;
252   NSEnumerator *contacts;
253   NSString *baseURL;
254
255   baseURL = [self baseURLInContext: context];
256
257   max = [filters count];
258   for (count = 0; count < max; count++)
259     {
260       currentFilter = [filters objectAtIndex: count];
261       contacts = [[self lookupContactsWithFilter: [[currentFilter allValues] lastObject]
262                         sortBy: @"c_givenname"
263                         ordering: NSOrderedDescending]
264                    objectEnumerator];
265       
266       while ((contact = [contacts nextObject]))
267       {
268           [self appendObject: contact
269                 withBaseURL: baseURL
270                 toREPORTResponse: response];
271         }
272     }
273 }
274
275 - (void) appendObject: (NSDictionary *) object
276           withBaseURL: (NSString *) baseURL
277      toREPORTResponse: (WOResponse *) r
278 {
279   SOGoContactGCSEntry *component;
280   Class componentClass;
281   NSString *name, *etagLine, *contactString;
282
283   name = [object objectForKey: @"c_name"];
284   componentClass = [SOGoContactGCSEntry class];
285
286   component = [componentClass objectWithName: name inContainer: self];
287
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"];
295
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"];
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 */