1/* SOGo+DAV.m - this file is part of SOGo
2 *
3 * Copyright (C) 2010-2013 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#import <Foundation/NSDictionary.h>
22#import <Foundation/NSEnumerator.h>
23
24#import <NGObjWeb/NSException+HTTP.h>
25#import <NGObjWeb/SoObject+SoDAV.h>
26#import <NGObjWeb/WOContext+SoObjects.h>
27#import <NGObjWeb/WORequest.h>
28#import <NGExtensions/NSObject+Logs.h>
29
30#import <DOM/DOMDocument.h>
31#import <DOM/DOMNode.h>
32#import <DOM/DOMProtocols.h>
33#import <SaxObjC/XMLNamespaces.h>
34
35#import <SOGo/NSArray+Utilities.h>
36#import <SOGo/NSObject+DAV.h>
37#import <SOGo/NSObject+Utilities.h>
38#import <SOGo/NSString+DAV.h>
39#import <SOGo/WORequest+SOGo.h>
40#import <SOGo/WOResponse+SOGo.h>
41#import <SOGo/SOGoSource.h>
42#import <SOGo/SOGoPermissions.h>
43#import <SOGo/SOGoUser.h>
44#import <SOGo/SOGoUserFolder.h>
45#import <SOGo/SOGoUserManager.h>
46
47#import "SOGo+DAV.h"
48
49@implementation SOGo (SOGoWebDAVExtensions)
50
51- (NSArray *) davPrincipalURL
52{
53  NSArray *principalURL;
54  NSString *classes;
55  WOContext *context;
56
57  context = [self context];
58  if ([[context request] isICal4])
59    {
60      classes = [[self davComplianceClassesInContext: context]
61                  componentsJoinedByString: @", "];
62      [[context response] setHeader: classes forKey: @"DAV"];
63    }
64
65  principalURL = [NSArray arrayWithObjects: @"href", @"DAV:", @"D",
66                          [self davURLAsString], nil];
67
68  return [NSArray arrayWithObject: principalURL];
69}
70
71- (WOResponse *) davPrincipalSearchPropertySet: (WOContext *) localContext
72{
73  static NSDictionary *davResponse = nil;
74  NSMutableArray *propertySet;
75  NSDictionary *property, *prop, *description;
76  NSArray *currentValue, *properties, *descriptions, *namespaces;
77  int count, max;
78  WOResponse *r;
79
80  if (!davResponse)
81    {
82      properties = [NSArray arrayWithObjects:
83                              @"calendar-user-type",
84                            @"calendar-user-address-set",
85                            @"calendar-user-type",
86                            @"displayname",
87                            @"first-name",
88                            @"last-name",
89                            @"email-address-set",
90                            nil];
91      descriptions
92        = [properties resultsOfSelector: @selector (asDAVPropertyDescription)];
93      namespaces = [NSArray arrayWithObjects:
94                              XMLNS_CALDAV,
95                            XMLNS_CALDAV,
96                            XMLNS_CALDAV,
97                            XMLNS_WEBDAV,
98                            XMLNS_CalendarServerOrg,
99                            XMLNS_CalendarServerOrg,
100                            XMLNS_CalendarServerOrg,
101                            nil];
102
103      max = [properties count];
104      propertySet = [NSMutableArray arrayWithCapacity: max];
105      for (count = 0; count < max; count++)
106        {
107          property = davElement([properties objectAtIndex: count],
108                                [namespaces objectAtIndex: count]);
109          prop = davElementWithContent(@"prop", @"DAV:",
110                                       property);
111          description
112            = davElementWithContent(@"description", @"DAV:",
113                                    [descriptions objectAtIndex: count]);
114          currentValue = [NSArray arrayWithObjects: prop, description, nil];
115          [propertySet
116            addObject: davElementWithContent(@"principal-search-property",
117                                             @"DAV:",
118                                             currentValue)];
119        }
120      davResponse = davElementWithContent (@"principal-search-property-set",
121                                           @"DAV:",
122                                           propertySet);
123      [davResponse retain];
124    }
125
126  r = [localContext response];
127  [r prepareDAVResponse];
128  [r appendContentString: [davResponse asWebDavStringWithNamespaces: nil]];
129
130  return r;
131}
132
133#warning all REPORT method should be standardized...
134- (NSDictionary *) _formalizePrincipalMatchResponse: (NSArray *) hrefs
135{
136  NSDictionary *multiStatus;
137  NSEnumerator *hrefList;
138  NSString *currentHref;
139  NSMutableArray *responses;
140  NSArray *responseElements;
141
142  responses = [NSMutableArray array];
143
144  hrefList = [hrefs objectEnumerator];
145  while ((currentHref = [hrefList nextObject]))
146    {
147      responseElements
148	= [NSArray arrayWithObjects: davElementWithContent (@"href",
149                                                            XMLNS_WEBDAV,
150							    currentHref),
151		   davElementWithContent (@"status", XMLNS_WEBDAV,
152					  @"HTTP/1.1 200 OK"),
153		   nil];
154      [responses addObject: davElementWithContent (@"response", XMLNS_WEBDAV,
155						   responseElements)];
156    }
157
158  multiStatus = davElementWithContent (@"multistatus", XMLNS_WEBDAV,
159                                       responses);
160
161  return multiStatus;
162}
163
164- (NSDictionary *) _handlePrincipalMatchSelfInContext: (WOContext *) localContext
165{
166  NSString *davURL, *userLogin;
167  NSArray *principalURL;
168
169  davURL = [self davURLAsString];
170  userLogin = [[localContext activeUser] login];
171  principalURL
172    = [NSArray arrayWithObject: [NSString stringWithFormat: @"%@%@/", davURL,
173					  userLogin]];
174  return [self _formalizePrincipalMatchResponse: principalURL];
175}
176
177- (void) _fillArrayWithPrincipalsOwnedBySelf: (NSMutableArray *) hrefs
178                                   inContext: (WOContext *) localContext
179{
180  NSArray *roles;
181
182  roles = [[localContext activeUser] rolesForObject: self
183                                          inContext: localContext];
184  if ([roles containsObject: SoRole_Owner])
185    [hrefs addObject: [self davURLAsString]];
186}
187
188- (NSDictionary *)
189   _handlePrincipalMatchPrincipalProperty: (id <DOMElement>) child
190                                inContext: (WOContext *) localContext
191{
192  NSMutableArray *hrefs;
193  NSDictionary *response;
194
195  hrefs = [NSMutableArray array];
196  [self _fillArrayWithPrincipalsOwnedBySelf: hrefs
197                                  inContext: localContext];
198
199  response = [self _formalizePrincipalMatchResponse: hrefs];
200
201  return response;
202}
203
204- (NSDictionary *) _handlePrincipalMatchReport: (id <DOMDocument>) document
205                                     inContext: (WOContext *) localContext
206{
207  NSDictionary *response;
208  id <DOMElement> documentElement, queryChild;
209  NSArray *children;
210  NSString *queryTag, *error;
211
212  documentElement = [document documentElement];
213  children = [self          domNode: documentElement
214                getChildNodesByType: DOM_ELEMENT_NODE];
215  if ([children count] == 1)
216    {
217      queryChild = [children objectAtIndex: 0];
218      queryTag = [queryChild tagName];
219      if ([queryTag isEqualToString: @"self"])
220	response = [self _handlePrincipalMatchSelfInContext: localContext];
221      else if ([queryTag isEqualToString: @"principal-property"])
222	response = [self _handlePrincipalMatchPrincipalProperty: queryChild
223                                                      inContext: localContext];
224      else
225        {
226          error = (@"Query element must be either '{DAV:}principal-property'"
227                   @"  or '{DAV:}self'");
228          response = [NSException exceptionWithHTTPStatus: 400
229                                                   reason: error];
230        }
231    }
232  else
233    {
234      error = (@"Query must have one element: '{DAV:}principal-property'"
235               @" or '{DAV:}self'");
236      response = [NSException exceptionWithHTTPStatus: 400
237                                               reason: error];
238    }
239
240  return response;
241}
242
243- (WOResponse *) davPrincipalMatch: (WOContext *) localContext
244{
245  WOResponse *r;
246  id <DOMDocument> document;
247  NSDictionary *xmlResponse;
248
249  r = [localContext response];
250
251  document = [[localContext request] contentAsDOMDocument];
252  xmlResponse = [self _handlePrincipalMatchReport: document
253                                        inContext: localContext];
254  if ([xmlResponse isKindOfClass: [NSException class]])
255    r = (WOResponse *) xmlResponse;
256  else
257    {
258      [r prepareDAVResponse];
259      [r appendContentString: [xmlResponse asWebDavStringWithNamespaces: nil]];
260    }
261
262  return r;
263}
264
265- (void) _fillMatches: (NSMutableDictionary *) matches
266	  fromElement: (NSObject <DOMElement> *) searchElement
267{
268  NSObject <DOMNodeList> *list;
269  NSObject <DOMNode> *valueNode;
270  NSArray *elements;
271  NSString *property=nil, *match=nil;
272
273  list = [searchElement getElementsByTagName: @"prop"];
274  if ([list length])
275    {
276      elements = [self domNode: [list objectAtIndex: 0]
277		       getChildNodesByType: DOM_ELEMENT_NODE];
278      if ([elements count])
279	{
280	  valueNode = [elements objectAtIndex: 0];
281	  property = [NSString stringWithFormat: @"{%@}%@",
282			       [valueNode namespaceURI],
283			       [valueNode nodeName]];
284	}
285    }
286  list = [searchElement getElementsByTagName: @"match"];
287  if ([list length])
288    {
289      valueNode = [[list objectAtIndex: 0] firstChild];
290      match = [valueNode nodeValue];
291    }
292
293  [matches setObject: match forKey: property];
294}
295
296- (void) _fillProperties: (NSMutableArray *) properties
297	     fromElement: (NSObject <DOMElement> *) propElement
298{
299  NSEnumerator *elements;
300  NSObject <DOMElement> *propNode;
301  NSString *property;
302
303  elements = [[self domNode: propElement
304		    getChildNodesByType: DOM_ELEMENT_NODE]
305	       objectEnumerator];
306  while ((propNode = [elements nextObject]))
307    {
308      property = [NSString stringWithFormat: @"{%@}%@",
309			   [propNode namespaceURI],
310			   [propNode nodeName]];
311      [properties addObject: property];
312    }
313}
314
315- (void) _fillPrincipalMatches: (NSMutableDictionary *) matches
316		 andProperties: (NSMutableArray *) properties
317		   fromElement: (NSObject <DOMElement> *) documentElement
318{
319  NSEnumerator *children;
320  NSObject <DOMElement> *currentElement;
321  NSString *tag;
322
323  children = [[self domNode: documentElement
324		    getChildNodesByType: DOM_ELEMENT_NODE]
325	       objectEnumerator];
326  while ((currentElement = [children nextObject]))
327    {
328      tag = [currentElement tagName];
329      if ([tag isEqualToString: @"property-search"])
330	[self _fillMatches: matches fromElement: currentElement];
331      else if  ([tag isEqualToString: @"prop"])
332	[self _fillProperties: properties fromElement: currentElement];
333      else
334	[self errorWithFormat: @"principal-property-search: unknown tag '%@'",
335	      tag];
336    }
337}
338
339#warning this is a bit ugly, as usual
340- (void)       _fillCollections: (NSMutableArray *) collections
341    withCalendarHomeSetMatching: (NSString *) value
342                      inContext: (WOContext *) localContext
343{
344  SOGoUserFolder *collection;
345  NSRange substringRange;
346
347  substringRange = [value rangeOfString: @"/SOGo/dav/"];
348
349  if (substringRange.location == NSNotFound)
350    return;
351
352  value = [value substringFromIndex: NSMaxRange (substringRange)];
353  substringRange = [value rangeOfString: @"/Calendar"];
354
355  if (substringRange.location == NSNotFound)
356    return;
357
358  value = [value substringToIndex: substringRange.location];
359  collection = [[SOGoUser userWithLogin: value]
360		 homeFolderInContext: localContext];
361  if (collection)
362    [collections addObject: collection];
363}
364
365- (void)       _fillCollections: (NSMutableArray *) collections
366    withEmailAddressSetMatching: (NSString *) value
367                      inContext: (WOContext *) localContext
368{
369  SOGoUserManager *um;
370  NSArray *records;
371  NSUInteger count, max;
372  NSString *uid;
373  SOGoUserFolder *collection;
374
375  um = [SOGoUserManager sharedUserManager];
376  records = [um fetchUsersMatching: value
377			  inDomain: [[localContext activeUser] domain]];
378
379  max = [records count];
380  for (count = 0; count < max; count++)
381    {
382      uid = [[records objectAtIndex: count] objectForKey: @"c_uid"];
383      if ([uid length] > 0)
384	{
385	  collection = [[SOGoUser userWithLogin: uid]
386                             homeFolderInContext: localContext];
387	  [collections addObject: collection];
388	}
389    }
390}
391
392- (void) _fillCollections: (NSMutableArray *) collections
393                    where: (NSString *) key
394                  matches: (NSString *) value
395                inContext: (WOContext *) localContext
396{
397  if ([key
398	isEqualToString: @"{urn:ietf:params:xml:ns:caldav}calendar-home-set"])
399    [self _fillCollections: collections withCalendarHomeSetMatching: value
400                 inContext: localContext];
401  else if ([key isEqualToString: @"{http://calendarserver.org/ns/}email-address-set"])
402    [self _fillCollections: collections withEmailAddressSetMatching: value
403                 inContext: localContext];
404  else
405    [self warnWithFormat: @"principal-property-search: unhandled key '%@'",
406	  key];
407}
408
409- (NSArray *) _principalCollectionsMatching: (NSDictionary *) matches
410                                  inContext: (WOContext *) localContext
411{
412  NSMutableArray *collections;
413  NSEnumerator *allKeys;
414  NSString *currentKey;
415
416  collections = [NSMutableArray array];
417
418  allKeys = [[matches allKeys] objectEnumerator];
419  while ((currentKey = [allKeys nextObject]))
420    [self _fillCollections: collections
421                     where: currentKey
422                   matches: [matches objectForKey: currentKey]
423                 inContext: localContext];
424
425  return collections;
426}
427
428/*   <D:principal-property-search xmlns:D="DAV:">
429     <D:property-search>
430       <D:prop>
431         <D:displayname/>
432       </D:prop>
433       <D:match>doE</D:match>
434     </D:property-search>
435     <D:property-search>
436       <D:prop xmlns:B="http://www.example.com/ns/">
437         <B:title/>
438       </D:prop>
439       <D:match>Sales</D:match>
440     </D:property-search>
441     <D:prop xmlns:B="http://www.example.com/ns/">
442       <D:displayname/>
443       <B:department/>
444       <B:phone/>
445       <B:office/>
446       <B:salary/>
447     </D:prop>
448     </D:principal-property-search> */
449
450- (void) _appendProperties: (NSArray *) properties
451	      ofCollection: (SOGoUserFolder *) collection
452	       toResponses: (NSMutableArray *) responses
453{
454  unsigned int count, max;
455  SEL methodSel;
456  NSString *currentProperty;
457  id currentValue;
458  NSMutableArray *response, *props, *propstat;
459  NSDictionary *keyTuple;
460
461  response = [NSMutableArray array];
462  [response addObject: davElementWithContent (@"href", XMLNS_WEBDAV,
463					      [collection davURLAsString])];
464  props = [NSMutableArray array];
465
466  max = [properties count];
467  for (count = 0; count < max; count++)
468    {
469      currentProperty = [properties objectAtIndex: count];
470      methodSel = SOGoSelectorForPropertyGetter (currentProperty);
471      if (methodSel && [collection respondsToSelector: methodSel])
472	{
473	  currentValue = [collection performSelector: methodSel];
474	  #warning evil eVIL EVIl!
475	  if ([currentValue isKindOfClass: [NSArray class]])
476	    {
477	      currentValue = [currentValue objectAtIndex: 0];
478	      currentValue
479		= davElementWithContent ([currentValue objectAtIndex: 0],
480					 [currentValue objectAtIndex: 1],
481					 [currentValue objectAtIndex: 3]);
482	    }
483	  keyTuple = [currentProperty asWebDAVTuple];
484	  [props addObject: davElementWithContent ([keyTuple objectForKey: @"method"],
485						   [keyTuple objectForKey: @"ns"],
486						   currentValue)];
487	}
488    }
489
490  propstat = [NSMutableArray array];
491  [propstat addObject: davElementWithContent (@"status", XMLNS_WEBDAV,
492                                              @"HTTP/1.1 200 OK")];
493  [propstat addObject: davElementWithContent (@"prop", XMLNS_WEBDAV,
494                                              props)];
495  [response addObject: davElementWithContent (@"propstat", XMLNS_WEBDAV, propstat)];
496  [responses addObject: davElementWithContent (@"response", XMLNS_WEBDAV,
497					       response)];
498}
499
500- (void) _appendProperties: (NSArray *) properties
501	     ofCollections: (NSArray *) collections
502		toResponse: (WOResponse *) response
503{
504  NSDictionary *mStatus;
505  NSMutableArray *responses;
506  unsigned int count, max;
507
508  max = [collections count];
509  responses = [NSMutableArray arrayWithCapacity: max];
510  for (count = 0; count < max; count++)
511    [self _appendProperties: properties
512               ofCollection: [collections objectAtIndex: count]
513                toResponses: responses];
514  mStatus = davElementWithContent (@"multistatus", XMLNS_WEBDAV, responses);
515  [response appendContentString: [mStatus asWebDavStringWithNamespaces: nil]];
516}
517
518/* rfc3744, should be moved into SOGoUserFolder */
519- (WOResponse *) davPrincipalPropertySearch: (WOContext *) localContext
520{
521  NSObject <DOMDocument> *document;
522  NSObject <DOMElement> *documentElement;
523  NSArray *collections;
524  NSMutableDictionary *matches;
525  NSMutableArray *properties;
526  WOResponse *r;
527
528  document = [[localContext request] contentAsDOMDocument];
529  documentElement = [document documentElement];
530
531  matches = [NSMutableDictionary dictionary];
532  properties = [NSMutableArray array];
533  [self _fillPrincipalMatches: matches andProperties: properties
534                  fromElement: documentElement];
535  collections = [self _principalCollectionsMatching: matches
536                                          inContext: localContext];
537  r = [localContext response];
538  [r prepareDAVResponse];
539  [self _appendProperties: properties ofCollections: collections
540               toResponse: r];
541
542  return r;
543}
544
545- (SOGoWebDAVValue *) davCurrentUserPrincipal
546{
547  NSDictionary *userHREF;
548  NSString *usersUrl, *login;
549  SOGoUser *activeUser;
550  SOGoWebDAVValue *davCurrentUserPrincipal;
551
552  activeUser = [[self context] activeUser];
553  login = [activeUser login];
554  if ([login isEqualToString: @"anonymous"])
555    davCurrentUserPrincipal = nil;
556  else
557    {
558      usersUrl = [NSString stringWithFormat: @"%@%@/",
559                           [self davURLAsString], login];
560      userHREF = davElementWithContent (@"href", XMLNS_WEBDAV, usersUrl);
561      davCurrentUserPrincipal
562        = [davElementWithContent (@"current-user-principal",
563                                  XMLNS_WEBDAV,
564                                  userHREF)
565                                 asWebDAVValue];
566    }
567
568  return davCurrentUserPrincipal;
569}
570
571- (NSArray *) davComplianceClassesInContext: (WOContext *) localContext
572{
573  static NSMutableArray *newClasses = nil;
574  NSArray *selfClasses;
575
576  if (!newClasses)
577    {
578      newClasses
579        = [[super davComplianceClassesInContext: localContext] mutableCopy];
580      selfClasses = [NSArray arrayWithObjects: @"access-control",
581                             @"addressbook", @"calendar-access",
582                             @"calendar-schedule", @"calendar-auto-schedule",
583                             @"calendar-proxy",
584                             @"calendar-query-extended",
585                             @"extended-mkcol",
586                             @"calendarserver-principal-property-search",
587                             nil];
588      [newClasses addObjectsFromArray: selfClasses];
589    }
590
591  return newClasses;
592}
593
594- (SOGoWebDAVValue *) davPrincipalCollectionSet
595{
596  NSString *davURL;
597  NSDictionary *collectionHREF;
598  NSString *classes;
599  WOContext *context;
600
601  context = [self context];
602  if ([[context request] isICal4])
603    {
604      classes = [[self davComplianceClassesInContext: context]
605                  componentsJoinedByString: @", "];
606      [[context response] setHeader: classes forKey: @"dav"];
607    }
608
609  /* WOApplication has no support for the DAV methods we define here so we
610     use the user's principal object as a reference */
611  davURL = [self davURLAsString];
612  collectionHREF = davElementWithContent (@"href", XMLNS_WEBDAV, davURL);
613
614  return [davElementWithContent (@"principal-collection-set",
615				 XMLNS_WEBDAV,
616				 [NSArray arrayWithObject: collectionHREF])
617				asWebDAVValue];
618}
619
620@end
621