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