1/* SOGoParentFolder.m - this file is part of SOGo
2 *
3 * Copyright (C) 2006-2017 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/NSArray.h>
22#import <Foundation/NSDictionary.h>
23#import <Foundation/NSEnumerator.h>
24#import <Foundation/NSString.h>
25
26#import <NGObjWeb/NSException+HTTP.h>
27#import <NGObjWeb/SoSecurityManager.h>
28#import <NGObjWeb/WOContext+SoObjects.h>
29#import <NGObjWeb/WOMessage.h>
30#import <NGObjWeb/WORequest.h>
31#import <NGExtensions/NSObject+Logs.h>
32#import <GDLContentStore/GCSChannelManager.h>
33#import <GDLContentStore/GCSFolderManager.h>
34#import <GDLContentStore/NSURL+GCS.h>
35#import <GDLAccess/EOAdaptorChannel.h>
36#import <DOM/DOMElement.h>
37#import <DOM/DOMProtocols.h>
38#import <SaxObjC/XMLNamespaces.h>
39#import <SOGo/SOGoUserDefaults.h>
40#import <SOGo/SOGoUserSettings.h>
41
42#import "NSObject+DAV.h"
43#import "SOGoGCSFolder.h"
44#import "SOGoPermissions.h"
45#import "SOGoUser.h"
46#import "SOGoWebDAVAclManager.h"
47
48#import "SOGoParentFolder.h"
49
50static SoSecurityManager *sm = nil;
51
52@implementation SOGoParentFolder
53
54+ (void) initialize
55{
56  if (!sm)
57    sm = [SoSecurityManager sharedSecurityManager];
58}
59
60+ (SOGoWebDAVAclManager *) webdavAclManager
61{
62  static SOGoWebDAVAclManager *aclManager = nil;
63
64  if (!aclManager)
65    {
66      aclManager = [SOGoWebDAVAclManager new];
67      [aclManager registerDAVPermission: davElement (@"read",
68						     XMLNS_WEBDAV)
69		  abstract: YES
70		  withEquivalent: nil
71		  asChildOf: davElement (@"all", XMLNS_WEBDAV)];
72      [aclManager registerDAVPermission:
73		    davElement (@"read-current-user-privilege-set",
74				XMLNS_WEBDAV)
75		  abstract: NO
76		  withEquivalent: SoPerm_WebDAVAccess
77		  asChildOf: davElement (@"read", XMLNS_WEBDAV)];
78      [aclManager registerDAVPermission: davElement (@"write",
79						     XMLNS_WEBDAV)
80		  abstract: YES
81		  withEquivalent: nil
82		  asChildOf: davElement (@"all", XMLNS_WEBDAV)];
83      [aclManager registerDAVPermission: davElement (@"bind",
84						     XMLNS_WEBDAV)
85		  abstract: NO
86		  withEquivalent: SoPerm_AddFolders
87		  asChildOf: davElement (@"write", XMLNS_WEBDAV)];
88      [aclManager registerDAVPermission: davElement (@"unbind",
89						     XMLNS_WEBDAV)
90		  abstract: NO
91		  withEquivalent: SoPerm_DeleteObjects
92		  asChildOf: davElement (@"write", XMLNS_WEBDAV)];
93      [aclManager
94	registerDAVPermission: davElement (@"write-properties", XMLNS_WEBDAV)
95	abstract: YES
96	withEquivalent: nil
97	asChildOf: davElement (@"write", XMLNS_WEBDAV)];
98      [aclManager
99	registerDAVPermission: davElement (@"write-content", XMLNS_WEBDAV)
100	abstract: YES
101	withEquivalent: nil
102	asChildOf: davElement (@"write", XMLNS_WEBDAV)];
103    }
104
105  return aclManager;
106}
107
108- (id) init
109{
110  if ((self = [super init]))
111    {
112      subFolders = nil;
113      OCSPath = nil;
114      subscribedSubFolders = nil;
115      subFolderClass = Nil;
116//       hasSubscribedSources = NO;
117    }
118
119  return self;
120}
121
122- (void) dealloc
123{
124  [subscribedSubFolders release];
125  [subFolders release];
126  [OCSPath release];
127  [super dealloc];
128}
129
130+ (Class) subFolderClass
131{
132  [self subclassResponsibility: _cmd];
133
134  return Nil;
135}
136
137+ (NSString *) gcsFolderType
138{
139  [self subclassResponsibility: _cmd];
140
141  return nil;
142}
143
144- (void) setBaseOCSPath: (NSString *) newOCSPath
145{
146  ASSIGN (OCSPath, newOCSPath);
147}
148
149- (NSString *) defaultFolderName
150{
151  return @"Personal";
152}
153
154- (NSString *) collectedFolderName
155{
156  return @"Collected";
157}
158
159- (void) createSpecialFolder: (SOGoFolderType) folderType
160{
161  NSArray *roles;
162  NSString *folderName;
163  SOGoGCSFolder *folder;
164  SOGoUser *folderOwner;
165  SOGoUserDefaults *ud;
166
167  roles = [[context activeUser] rolesForObject: self inContext: context];
168  folderOwner = [SOGoUser userWithLogin: [self ownerInContext: context]];
169
170
171  // We autocreate the calendars if the user is the owner, a superuser or
172  // if it's a resource as we won't necessarily want to login as a resource
173  // in order to create its database tables.
174  // FolderType is an enum where 0 = Personal and 1 = collected
175  if ([roles containsObject: SoRole_Owner] ||
176      (folderOwner && [folderOwner isResource]))
177  {
178    if (folderType == SOGoPersonalFolder)
179    {
180      folderName = @"personal";
181      folder = [subFolderClass objectWithName: folderName inContainer: self];
182      [folder setDisplayName: [self defaultFolderName]];
183      [folder setOCSPath: [NSString stringWithFormat: @"%@/%@", OCSPath, folderName]];
184
185      if ([folder create])
186      [subFolders setObject: folder forKey: folderName];
187    }
188    else if (folderType == SOGoCollectedFolder)
189    {
190      ud = [[context activeUser] userDefaults];
191      if ([ud mailAddOutgoingAddresses]) {
192        folderName = @"collected";
193        folder = [subFolderClass objectWithName: folderName inContainer: self];
194        [folder setDisplayName: [self collectedFolderName]];
195        [folder setOCSPath: [NSString stringWithFormat: @"%@/%@", OCSPath, folderName]];
196
197        if ([folder create])
198          [subFolders setObject: folder forKey: folderName];
199
200        [ud setSelectedAddressBook:folderName];
201      }
202    }
203  }
204}
205
206- (NSException *) fetchSpecialFolders: (NSString *) sql
207                          withChannel: (EOAdaptorChannel *) fc
208                        andFolderType: (SOGoFolderType) folderType
209{
210  NSArray *attrs;
211  NSDictionary *row;
212  SOGoGCSFolder *folder;
213  NSString *key;
214  NSException *error;
215  SOGoUserDefaults *ud;
216  ud = [[context activeUser] userDefaults];
217
218  if (!subFolderClass)
219    subFolderClass = [[self class] subFolderClass];
220
221  error = [fc evaluateExpressionX: sql];
222  if (!error)
223  {
224    attrs = [fc describeResults: NO];
225    while ((row = [fc fetchAttributes: attrs withZone: NULL]))
226    {
227      key = [row objectForKey: @"c_path4"];
228      if ([key isKindOfClass: [NSString class]])
229      {
230        folder = [subFolderClass objectWithName: key inContainer: self];
231        [folder setOCSPath: [NSString stringWithFormat: @"%@/%@", OCSPath, key]];
232        [subFolders setObject: folder forKey: key];
233      }
234    }
235    if (folderType == SOGoPersonalFolder)
236    {
237      if (![subFolders objectForKey: @"personal"])
238        [self createSpecialFolder: SOGoPersonalFolder];
239    }
240    else if (folderType == SOGoCollectedFolder)
241    {
242      if (![subFolders objectForKey: @"collected"])
243        if ([[ud selectedAddressBook] isEqualToString:@"collected"])
244          [self createSpecialFolder: SOGoCollectedFolder];
245    }
246  }
247  return error;
248}
249
250- (NSException *) appendPersonalSources
251{
252  GCSChannelManager *cm;
253  EOAdaptorChannel *fc;
254  NSURL *folderLocation;
255  NSString *sql, *gcsFolderType;
256  NSException *error;
257
258  cm = [GCSChannelManager defaultChannelManager];
259  folderLocation = [[GCSFolderManager defaultFolderManager] folderInfoLocation];
260  fc = [cm acquireOpenChannelForURL: folderLocation];
261  if ([fc isOpen])
262  {
263    gcsFolderType = [[self class] gcsFolderType];
264
265    sql = [NSString stringWithFormat: (@"SELECT c_path4 FROM %@"
266                                       @" WHERE c_path2 = '%@'"
267                                       @" AND c_folder_type = '%@'"),
268          [folderLocation gcsTableName], owner, gcsFolderType];
269
270    error = [self fetchSpecialFolders: sql withChannel: fc andFolderType: SOGoPersonalFolder];
271
272    [cm releaseChannel: fc];
273  }
274  else
275    error = [NSException exceptionWithName: @"SOGoDBException"
276			 reason: @"database connection could not be open"
277			 userInfo: nil];
278
279  return error;
280}
281
282
283- (NSException *) appendSystemSources
284{
285  return nil;
286}
287
288- (BOOL) _appendSubscribedSource: (NSString *) sourceKey
289{
290  SOGoGCSFolder *subscribedFolder;
291
292  subscribedFolder
293    = [subFolderClass folderWithSubscriptionReference: sourceKey
294		      inContainer: self];
295
296  // We check with -ocsFolderForPath if the folder also exists in the database.
297  // This is important because user A could delete folder X, and user B has subscribed to it.
298  // If the "default roles" are enabled for calendars/address books, -validatePersmission:.. will
299  // work (grabbing the default role) and the deleted resource will be incorrectly returned.
300  if (subscribedFolder
301      && [subscribedFolder ocsFolderForPath: [subscribedFolder ocsPath]]
302      && ![sm validatePermission: SOGoPerm_AccessObject
303			onObject: subscribedFolder
304		       inContext: context])
305    {
306      [subscribedSubFolders setObject: subscribedFolder
307			       forKey: [subscribedFolder nameInContainer]];
308      return YES;
309    }
310
311  return NO;
312}
313
314- (NSException *) appendSubscribedSources
315{
316  NSMutableDictionary *folderDisplayNames;
317  NSMutableArray *subscribedReferences;
318  SOGoUserSettings *settings;
319  NSString *activeUser, *currentKey;
320  SOGoUser *ownerUser;
321  NSException *error;
322  id o;
323  int i;
324  BOOL dirty;
325
326  if (!subscribedSubFolders)
327    subscribedSubFolders = [NSMutableDictionary new];
328
329  if (!subFolderClass)
330    subFolderClass = [[self class] subFolderClass];
331
332  error = nil; /* we ignore non-DB errors at this time... */
333  dirty = NO;
334
335  activeUser = [[context activeUser] login];
336  ownerUser = [SOGoUser userWithLogin: owner];
337  settings = [ownerUser userSettings];
338
339  subscribedReferences = [NSMutableArray arrayWithArray: [[settings objectForKey: nameInContainer]
340							   objectForKey: @"SubscribedFolders"]];
341  o =  [[settings objectForKey: nameInContainer] objectForKey: @"FolderDisplayNames"];
342  if (o)
343    folderDisplayNames = [NSMutableDictionary dictionaryWithDictionary: o];
344  else
345    folderDisplayNames = nil;
346
347  for (i = [subscribedReferences count] - 1; i >= 0; i--)
348    {
349      currentKey = [subscribedReferences objectAtIndex: i];
350      if (![self _appendSubscribedSource: currentKey])
351	{
352	  // We no longer have access to this subscription, let's
353	  // remove it from the current list.
354	  [subscribedReferences removeObject: currentKey];
355	  [folderDisplayNames removeObjectForKey: currentKey];
356          if ([owner isEqualToString: activeUser])
357            // Synchronize settings only if the subscription is owned by the active user
358            dirty = YES;
359	}
360    }
361
362  // If we changed the folder subscribtion list, we must sync it
363  if (dirty)
364    {
365      if (subscribedReferences)
366        [[settings objectForKey: nameInContainer] setObject: subscribedReferences
367                                                     forKey: @"SubscribedFolders"];
368      if (folderDisplayNames)
369        [[settings objectForKey: nameInContainer] setObject: folderDisplayNames
370                                                     forKey: @"FolderDisplayNames"];
371      [settings synchronize];
372    }
373
374  return error;
375}
376
377- (NSException *) newFolderWithName: (NSString *) name
378		 andNameInContainer: (NSString *) newNameInContainer
379{
380  SOGoGCSFolder *newFolder;
381  NSException *error;
382
383  if (!subFolderClass)
384    subFolderClass = [[self class] subFolderClass];
385
386  newFolder = [subFolderClass objectWithName: newNameInContainer
387			      inContainer: self];
388  if ([newFolder isKindOfClass: [NSException class]])
389    error = (NSException *) newFolder;
390  else
391    {
392      [newFolder setDisplayName: name];
393      [newFolder setOCSPath: [NSString stringWithFormat: @"%@/%@",
394                                       OCSPath, newNameInContainer]];
395      if ([newFolder create])
396	{
397	  [subFolders setObject: newFolder forKey: newNameInContainer];
398	  error = nil;
399	}
400      else
401        error = [NSException exceptionWithHTTPStatus: 400
402			     reason: @"The new folder could not be created"];
403    }
404
405  return error;
406}
407
408- (NSException *) newFolderWithName: (NSString *) name
409		    nameInContainer: (NSString **) newNameInContainer
410{
411  NSString *newFolderID;
412  NSException *error;
413
414  newFolderID = *newNameInContainer;
415
416  if (!newFolderID)
417    newFolderID = [self globallyUniqueObjectId];
418
419  error = [self newFolderWithName: name
420		andNameInContainer: newFolderID];
421  if (error)
422    *newNameInContainer = nil;
423  else
424    *newNameInContainer = newFolderID;
425
426  return error;
427}
428
429- (NSException *) initSubFolders
430{
431  NSException *error;
432
433  if (!subFolders)
434    {
435      subFolders = [NSMutableDictionary new];
436      error = [self appendPersonalSources];
437      if (!error)
438        if ([self respondsToSelector:@selector(appendCollectedSources)])
439          error = [self performSelector:@selector(appendCollectedSources)];
440      if (!error)
441        error = [self appendSystemSources]; // TODO : Not really a testcase, see function
442      if (error)
443      {
444        [subFolders release];
445        subFolders = nil;
446      }
447    }
448  else
449    error = nil;
450
451  return error;
452}
453
454- (void) removeSubFolder: (NSString *) subfolderName
455{
456  [subFolders removeObjectForKey: subfolderName];
457}
458
459- (NSException *) initSubscribedSubFolders
460{
461  NSException *error;
462  SOGoUser *currentUser;
463
464  if (!subFolderClass)
465    subFolderClass = [[self class] subFolderClass];
466
467  error = nil; /* we ignore non-DB errors at this time... */
468  currentUser = [context activeUser];
469  if (!subscribedSubFolders
470      && ([[currentUser login] isEqualToString: owner]
471          || [currentUser isSuperUser]))
472    {
473      subscribedSubFolders = [NSMutableDictionary new];
474      error = [self appendSubscribedSources];
475    }
476
477  return error;
478}
479
480- (id) lookupName: (NSString *) name
481        inContext: (WOContext *) lookupContext
482          acquire: (BOOL) acquire
483{
484  id obj;
485  NSException *error;
486
487  /* first check attributes directly bound to the application */
488  obj = [super lookupName: name inContext: lookupContext acquire: NO];
489  if (!obj)
490    {
491      obj = [self lookupPersonalFolder: name
492                        ignoringRights: NO];
493      if (!obj)
494	{
495	  // Lookup in subscribed folders
496	  error = [self initSubscribedSubFolders];
497	  if (error)
498	    {
499	      [self errorWithFormat: @"a database error occured: %@", [error reason]];
500	      obj = [NSException exceptionWithHTTPStatus: 503];
501	    }
502	  else
503	    obj = [subscribedSubFolders objectForKey: name];
504	}
505    }
506
507  return obj;
508}
509
510- (id) lookupPersonalFolder: (NSString *) name
511             ignoringRights: (BOOL) ignoreRights
512{
513  NSException *error;
514  id obj;
515
516  error = [self initSubFolders];
517  if (error)
518    {
519      [self errorWithFormat: @"a database error occured: %@", [error reason]];
520      obj = [NSException exceptionWithHTTPStatus: 503];
521    }
522  else
523    {
524      obj = [subFolders objectForKey: name];
525      if (obj && !ignoreRights && ![self ignoreRights]
526          && [sm validatePermission: SOGoPerm_AccessObject
527                           onObject: obj
528                          inContext: context])
529        obj = nil;
530    }
531
532  return obj;
533}
534
535- (NSArray *) subFolders
536{
537  NSMutableArray *ma;
538  NSException *error;
539  NSString *requestMethod;
540  BOOL isPropfind;
541
542  requestMethod = [[context request] method];
543  isPropfind = [requestMethod isEqualToString: @"PROPFIND"];
544
545  error = [self initSubFolders];
546  if (error && isPropfind)
547    {
548      /* We exceptionnally raise the exception here because doPROPFIND: will
549	 not care for errors in its response from toManyRelationShipKeys,
550	 which may in turn trigger the disappearance of user folders in the
551	 SOGo extensions. */
552      [error raise];
553    }
554
555  error = [self initSubscribedSubFolders];
556  if (error && isPropfind)
557    [error raise];
558
559  ma = [NSMutableArray arrayWithArray: [subFolders allValues]];
560  if ([subscribedSubFolders count])
561    [ma addObjectsFromArray: [subscribedSubFolders allValues]];
562
563  return [ma sortedArrayUsingSelector: @selector (compare:)];
564}
565
566- (BOOL) hasLocalSubFolderNamed: (NSString *) name
567{
568  NSArray *subs;
569  NSString *currentDisplayName;
570  int i, count;
571  BOOL rc;
572
573  rc = NO;
574
575#warning check error here
576  [self initSubFolders];
577
578  subs = [subFolders allValues];
579  count = [subs count];
580  for (i = 0; !rc && i < count; i++)
581    {
582      currentDisplayName = [[subs objectAtIndex: i] displayName];
583      rc = [name isEqualToString: currentDisplayName];
584    }
585
586  return rc;
587}
588
589- (NSArray *) toManyRelationshipKeys
590{
591  NSEnumerator *sortedSubFolders;
592  NSMutableArray *keys;
593  SOGoGCSFolder *currentFolder;
594  BOOL ignoreRights;
595
596  ignoreRights = [self ignoreRights];
597
598  keys = [NSMutableArray array];
599  sortedSubFolders = [[self subFolders] objectEnumerator];
600  while ((currentFolder = [sortedSubFolders nextObject]))
601    {
602      if (ignoreRights
603          || ![sm validatePermission: SOGoPerm_AccessObject
604                            onObject: currentFolder
605                           inContext: context])
606        [keys addObject: [currentFolder nameInContainer]];
607    }
608
609  return keys;
610}
611
612- (NSException *) davCreateCollection: (NSString *) pathInfo
613			    inContext: (WOContext *) localContext
614{
615  id <DOMDocument> document;
616  //
617  // We check if we got a MKCOL with the addressbook resource on the
618  // calendar-homeset collection (/Calendar). If so, we abort the
619  // operation and return the proper error code.
620  //
621  // See http://tools.ietf.org/html/rfc5689 for all details.
622  //
623  document = [[localContext request] contentAsDOMDocument];
624
625  // If a payload was specified, lets get it in order to see
626  // if we must accept or reject the MKCOL operation. If we
627  // don't have any payload (what SOGo Connector / Integrators
628  // sends right now), we proceed as before.
629  if (document)
630    {
631      NSMutableArray *supportedTypes;
632      id <DOMNodeList> children;
633      id <DOMElement> element;
634      NSException *error;
635      NSArray *allTypes;
636      id o;
637
638      BOOL supported;
639      int i;
640
641      error = [self initSubFolders];
642      supported = YES;
643
644      if (error)
645	{
646	  [self errorWithFormat: @"a database error occured: %@", [error reason]];
647	  return [NSException exceptionWithHTTPStatus: 503];
648	}
649
650      // We assume "personal" exists. In fact, if it doesn't, something
651      // is seriously broken.
652      allTypes = [[subFolders objectForKey: @"personal"] davResourceType];
653      supportedTypes = [NSMutableArray array];
654
655      for (i = 0; i < [allTypes count]; i++)
656	{
657	  o = [allTypes objectAtIndex: i];
658	  if ([o isKindOfClass: [NSArray class]])
659	    o = [o objectAtIndex: 0];
660
661	  [supportedTypes addObject: o];
662	}
663
664      children = [[(NSArray *)[[document documentElement] getElementsByTagName: @"resourcetype"]
665			      lastObject] childNodes];
666
667      // We check if all the provided types are supported.
668      // In case one of them is not, we reject the operation.
669      for (i = 0; i < [children length]; i++)
670	{
671	  element = [children objectAtIndex: i];
672
673	  if ([element nodeType] == DOM_ELEMENT_NODE &&
674	      ![supportedTypes containsObject: [element nodeName]])
675	    supported = NO;
676	}
677
678      if (!supported)
679	{
680	  return [NSException exceptionWithHTTPStatus: 403];
681	}
682    }
683
684  return [self newFolderWithName: pathInfo
685	       andNameInContainer: pathInfo];
686}
687
688@end
689