2 Copyright (C) 2002-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE 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
11 SOPE 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.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "OFSFolder.h"
24 #include "OFSFactoryContext.h"
25 #include "OFSFactoryRegistry.h"
26 #include "OFSResourceManager.h"
27 #include "OFSFolderClassDescription.h"
28 #include "OFSFolderDataSource.h"
29 #include <NGObjWeb/WOResponse.h>
32 @implementation OFSFolder
34 static BOOL factoryDebugOn = NO;
35 static BOOL debugLookup = NO;
36 static BOOL debugRestore = NO;
37 static BOOL debugNegotiate = NO;
38 static BOOL debugAuthLookup = NO;
41 return [super version] + 1 /* v2 */;
44 static BOOL didInit = NO;
46 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
48 NSAssert2([super version] == 1,
49 @"invalid superclass (%@) version %i !",
50 NSStringFromClass([self superclass]), [super version]);
52 debugLookup = [ud boolForKey:@"SoDebugKeyLookup"];
53 factoryDebugOn = [ud boolForKey:@"SoOFSDebugFactory"];
54 debugRestore = [ud boolForKey:@"SoOFSDebugRestore"];
55 debugNegotiate = [ud boolForKey:@"SoOFSDebugNegotiate"];
56 debugAuthLookup = [ud boolForKey:@"SoOFSDebugAuthLookup"];
61 [(OFSResourceManager *)self->resourceManager invalidate];
62 [self->resourceManager release];
64 [[self->children allValues]
65 makeObjectsPerformSelector:@selector(detachFromContainer)];
67 [self->childNames release];
68 [self->props release];
69 [self->children release];
75 - (NSString *)propertyFilename {
76 return @".props.plist";
79 - (BOOL)isCollection {
83 return [self->childNames count] > 0 ? YES : NO;
86 - (NSArray *)allKeys {
87 return self->childNames;
90 - (BOOL)hasKey:(NSString *)_key {
91 return [self->childNames containsObject:_key];
94 - (id)objectForKey:(NSString *)_key {
95 OFSFactoryContext *ctx;
96 NSDictionary *fileAttrs;
97 NSString *fileType, *mimeType;
102 if ((child = [self->children objectForKey:_key]))
104 return [child isNotNull] ? child : nil;
106 if ([_key hasPrefix:@"."])
107 /* do not consider keys starting with a point ... */
110 if (self->flags.didLoadAll)
111 /* everything is cached, should be in there .. */
114 if (![self->childNames containsObject:_key])
115 /* not a storage key anyway */
118 /* find out filetype */
120 childPath = [self storagePathForChildKey:_key];
121 fileAttrs = [[self fileManager] fileAttributesAtPath:childPath
123 fileType = [fileAttrs objectForKey:NSFileType];
124 mimeType = [fileAttrs objectForKey:@"NSFileMimeType"];
127 [self logWithFormat:@"got no file type for child %@ ...", _key];
129 /* create factory context */
131 ctx = [OFSFactoryContext contextForChild:_key
132 storagePath:childPath
134 ctx->fileType = [fileType copy];
135 ctx->mimeType = [mimeType copy];
139 if ((factory = [self restorationFactoryForContext:ctx]) == nil) {
140 [self logWithFormat:@"found no factory for key '%@' (%@, mime=%@)",
141 _key, fileType, mimeType];
142 return [NSException exceptionWithHTTPStatus:500
143 reason:@"found no factory for object !"];
147 [self debugWithFormat:@"selected factory %@ for key %@", factory, _key];
149 /* instantiate and register */
151 if (self->children == nil) {
153 [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
156 if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
157 [self logWithFormat:@"factory did not instantiate object for key '%@'",
159 child = [NSException exceptionWithHTTPStatus:500
160 reason:@"instantiation of object failed !"];
163 [self->children setObject:child forKey:_key];
165 /* awake object, handle possible replacement result */
167 if (![child isKindOfClass:[NSException class]]) {
170 replacement = [child awakeFromFetchInContext:ctx];
171 if (replacement != child) {
172 if (replacement == nil)
173 [self->children removeObjectForKey:_key];
175 [self->children setObject:replacement forKey:_key];
182 - (NSArray *)allValues {
186 if (self->flags.didLoadAll)
187 return [self->children allValues];
189 /* query each key to load it into the children cache */
191 keys = [self->childNames objectEnumerator];
192 while ((key = [keys nextObject]))
193 [self objectForKey:key];
195 self->flags.didLoadAll = 1;
196 return [self->children allValues];
199 - (NSEnumerator *)keyEnumerator {
200 return [self->childNames objectEnumerator];
202 - (NSEnumerator *)objectEnumerator {
203 return [[self allValues] objectEnumerator];
206 - (BOOL)isValidKey:(NSString *)_key {
208 Check whether key is usable for storage (extract some FS sensitive or
212 if ([_key length] == 0) return NO;
213 c = [_key characterAtIndex:0];
214 if (c == '.') return NO;
215 if (c == '~') return NO;
216 if (c == '%') return NO;
217 if (c == '/') return NO;
218 if ([_key rangeOfString:@"/"].length > 0)
219 // TBD: we should allow '/' in filenames
221 if ([_key isEqualToString:[self propertyFilename]])
228 - (EODataSource *)contentDataSource {
229 return [OFSFolderDataSource dataSourceOnFolder:self];
237 - (NSString *)storagePathForChildKey:(NSString *)_name {
238 if (![self isValidKey:_name]) return nil;
239 return [[self storagePath] stringByAppendingPathComponent:_name];
242 - (OFSFactoryRegistry *)factoryRegistry {
243 return [OFSFactoryRegistry sharedFactoryRegistry];
246 - (id)restorationFactoryForContext:(OFSFactoryContext *)_ctx {
247 return [[self factoryRegistry] restorationFactoryForContext:_ctx];
249 - (id)creationFactoryForContext:(OFSFactoryContext *)_ctx {
250 return [[self factoryRegistry] creationFactoryForContext:_ctx];
255 - (NSClassDescription *)soClassDescription {
256 // TODO: cache class description ?
257 return [[[OFSFolderClassDescription alloc] initWithFolder:self] autorelease];
259 - (NSArray *)attributeKeys {
260 return [self->props allKeys];
262 - (NSArray *)toOneRelationshipKeys {
263 return [self allKeys];
266 - (void)filterChildNameArray:(NSMutableArray *)p {
269 [p removeObject:[self propertyFilename]];
271 for (i = 0; i < [p count];) {
275 k = [p objectAtIndex:i];
277 if (kl == 3 && !self->flags.hasCVS && [k isEqualToString:@"CVS"]) {
278 self->flags.hasCVS = 1;
279 [p removeObjectAtIndex:i];
281 else if (kl == 4 && !self->flags.hasSvn && [k isEqualToString:@".svn"]) {
282 self->flags.hasSvn = 1;
283 [p removeObjectAtIndex:i];
285 else if ([k hasPrefix:@"."])
286 [p removeObjectAtIndex:i];
290 self->flags.checkedVersionSpecials = 1;
293 - (id)awakeFromFetchInContext:(OFSFactoryContext *)_ctx {
298 [self debugWithFormat:@"-awakeFromContext:%@", _ctx];
300 if ((p = [super awakeFromFetchInContext:_ctx]) != self) {
302 [self debugWithFormat:@" parent replaced object with: %@", p];
306 sp = [_ctx storagePath];
308 [self debugWithFormat:@" restore path: '%@'", sp];
310 /* load the dictionary properties */
312 p = [sp stringByAppendingPathComponent:[self propertyFilename]];
313 self->props = [[NSDictionary alloc] initWithContentsOfFile:p];
316 [self debugWithFormat:@" restored %i properties: %@",
318 [[self->props allKeys] componentsJoinedByString:@","]];
321 /* load the collection children names */
323 p = [[[_ctx fileManager] directoryContentsAtPath:sp] mutableCopy];
325 [self debugWithFormat:@"couldn't get child names at path '%@'.", p];
329 [self debugWithFormat:@" storage child names at '%@': %@", sp, p];
331 [self filterChildNameArray:p];
332 [p sortUsingSelector:@selector(compare:)];
334 self->childNames = [p copy];
338 [self debugWithFormat:@" restored child names: %@",
339 [self->childNames componentsJoinedByString:@","]];
345 - (void)flushChildCache {
346 [[self->children allValues]
347 makeObjectsPerformSelector:@selector(detachFromContainer)];
348 [self->children removeAllObjects];
349 self->flags.didLoadAll = 0;
352 - (NSException *)reload {
353 // TODO: reload folder !
354 [self flushChildCache];
360 - (id)valueForKey:(NSString *)_name {
361 /* map out some very private keys */
366 if ((v = [self->props objectForKey:_name]))
369 if ((nl = [_name length]) == 0)
372 c = [_name characterAtIndex:0];
375 return [super valueForKey:_name];
380 - (BOOL)allowRecursiveDeleteInContext:(id)_ctx {
384 - (NSString *)defaultMethodNameInContext:(id)_ctx {
387 - (id)lookupDefaultMethod {
390 ctx = [[WOApplication application] context];
391 return [self lookupName:[self defaultMethodNameInContext:ctx]
396 - (id)GETAction:(id)_ctx {
397 WOResponse *r = [(id <WOPageGenerationContext>)_ctx response];
398 NSString *uri, *qs, *method;
401 if (![[_ctx soRequestType] isEqualToString:@"METHOD"])
404 if ((method = [self defaultMethodNameInContext:_ctx]) == nil)
405 /* no default method */
410 uri = [[(id <WOPageGenerationContext>)_ctx request] uri];
411 ra = [uri rangeOfString:@"?"];
413 qs = [uri substringFromIndex:ra.location];
414 uri = [uri substringToIndex:ra.location];
418 uri = [uri stringByAppendingPathComponent:method];
419 if (qs) uri = [uri stringByAppendingString:qs];
421 [r setStatus:302 /* moved */];
422 [r setHeader:uri forKey:@"location"];
426 - (id)DELETEAction:(id)_ctx {
429 if ((e = [self validateForDelete]))
432 if ([self hasChildren]) {
433 if (![self allowRecursiveDeleteInContext:_ctx]) {
434 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
435 reason:@"tried to delete a filled folder"];
438 return [super DELETEAction:_ctx];
441 - (id)PUTAction:(id)_ctx {
442 OFSFactoryContext *ctx;
449 pathInfo = [_ctx pathInfo];
450 /* TODO: NEED TO REWRITE path info to a key (eg strip .vcf) ! */
452 // TODO: return conflict, on attempt to create subfolder
453 if ([pathInfo length] == 0) {
454 [self debugWithFormat:@"attempt to PUT to an OFSFolder !"];
455 [self debugWithFormat:@"body:\n%@", [[(id <WOPageGenerationContext>)_ctx request] contentAsString]];
457 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
458 reason:@"HTTP PUT not allowed on a folder resource"];
461 if ([self->childNames containsObject:pathInfo]) {
463 Explained: PUT can be and is used to overwrite existing resources. But
464 if PUT was issued on an existing resource, the SoObject for this resource
465 will receive the PUT action, not it's contained.
466 So: the container (folder) only receives a PUT action with a PATH_INFO if
467 the resource to be PUT is new.
469 [self debugWithFormat:
470 @"internal inconsistency, tried to create an existing resource !"];
471 return [NSException exceptionWithHTTPStatus:500 /* method not allowed */
472 reason:@"tried to create an existing resource"];
475 if ((childPath = [self storagePathForChildKey:pathInfo]) == nil) {
476 [self debugWithFormat:@"invalid name for child !"];
477 return [NSException exceptionWithHTTPStatus:400 /* bad request */
478 reason:@"the name for the child creation was invalid"];
481 /* create factory context */
483 ctx = [OFSFactoryContext contextForNewChild:pathInfo
484 storagePath:childPath
486 ctx->fileType = [NSFileTypeRegular retain];
487 ctx->mimeType = [[[(id <WOPageGenerationContext>)_ctx request] headerForKey:@"content-type"] copy];
491 if ((factory = [self creationFactoryForContext:ctx]) == nil) {
492 [self logWithFormat:@"found no factory for new key '%@' (%@, mime=%@)",
493 pathInfo, ctx->fileType, [ctx mimeType]];
494 return [NSException exceptionWithHTTPStatus:500
495 reason:@"found no factory for new object !"];
498 if (factoryDebugOn) {
499 [self debugWithFormat:@"selected factory %@ for new child named %@",
503 /* instantiate and register */
505 if (self->children == nil) {
507 [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
510 if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
512 @"factory did not instantiate new object for key '%@'",
514 return [NSException exceptionWithHTTPStatus:500
515 reason:@"instantiation of object failed !"];
517 if ([child isKindOfClass:[NSException class]])
520 childPutMethod = [child lookupName:@"PUT" inContext:_ctx acquire:NO];
521 if (childPutMethod == nil) {
522 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
523 reason:@"new child does not support HTTP PUT."];
526 [self->children setObject:child forKey:pathInfo];
528 /* awake object, handle possible replacement result */
533 replacement = [child awakeFromInsertionInContext:ctx];
534 if (replacement != child) {
535 if ([replacement isKindOfClass:[NSException class]]) {
536 replacement = [replacement retain];
537 [self->children removeObjectForKey:pathInfo];
538 return [replacement autorelease];
541 if (replacement == nil) {
542 [self->children removeObjectForKey:pathInfo];
543 return [NSException exceptionWithHTTPStatus:500
544 reason:@"awake failed, reason unknown"];
548 [replacement lookupName:@"PUT" inContext:_ctx acquire:NO];
550 if (childPutMethod == nil) {
551 [self->children removeObjectForKey:pathInfo];
552 return [NSException exceptionWithHTTPStatus:405 /* not allowed */
553 reason:@"new child does not support HTTP PUT."];
556 [self->children setObject:replacement forKey:pathInfo];
562 /* now forward the PUT to the child */
564 result = [[childPutMethod bindToObject:child inContext:_ctx]
565 callOnObject:child inContext:_ctx];
567 /* check whether put was successful */
569 if ([result isKindOfClass:[NSException class]]) {
570 /* creation failed, unregister from childlist */
571 [child detachFromContainer];
572 [self->children removeObjectForKey:pathInfo];
578 - (id)MKCOLAction:(id)_ctx {
581 pathInfo = [_ctx pathInfo];
582 pathInfo = [pathInfo stringByUnescapingURL];
584 if ([pathInfo length] == 0) {
585 [self debugWithFormat:@"attempt to MKCOL an existint OFSFolder !"];
586 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
587 reason:@"tried MKCOL an an existing resource"];
590 // TBD: create new child
591 // TBD: return conflict, on attempt to create subfolder
592 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
593 reason:@"creating collections is forbidden"];
598 - (NSString *)normalizeKey:(NSString *)_name inContext:(id)_ctx {
599 /* useful for content-negotiation */
603 - (NSString *)selectBestMatchForName:(NSString *)_name
604 fromChildNames:(NSArray *)_matches
610 if ((count = [_matches count]) == 0)
613 storeName = [_matches objectAtIndex:0];
615 // TODO: some real negotiation based on "accept", "language", ..
616 storeName = [_matches objectAtIndex:0];
617 [self logWithFormat:@"negotiate: selected '%@' from: %@.", storeName,
618 [_matches componentsJoinedByString:@","]];
620 if (debugNegotiate) [self logWithFormat:@"negotiated: '%@'", storeName];
624 - (NSString *)negotiateName:(NSString *)_name inContext:(id)_ctx {
625 /* returns a "storeName", one which can be resolved in the store */
626 NSMutableArray *matches;
627 NSString *askedExt, *normName, *storeName;
631 if (debugNegotiate) [self logWithFormat:@"negotiate: %@", _name];
633 availKeys = [self allKeys];
634 if ((count = [availKeys count]) == 0)
635 return nil; /* no content */
636 if ([availKeys containsObject:_name])
637 return _name; /* exact match */
639 /* some hard-coded content negotiation */
641 askedExt = [_name pathExtension];
642 normName = [_name stringByDeletingPathExtension];
644 for (i = 0, matches = nil; i < count; i++) {
645 NSString *storeName, *childNormName;
647 storeName = [availKeys objectAtIndex:i];
648 childNormName = [storeName stringByDeletingPathExtension];
650 if (debugNegotiate) [self logWithFormat:@" check: %@", storeName];
652 if (![normName isEqualToString:childNormName])
656 if (matches == nil) matches = [[NSMutableArray alloc] initWithCapacity:16];
657 [matches addObject:storeName];
661 return nil; /* no matches */
663 storeName = [self selectBestMatchForName:normName
664 fromChildNames:matches
666 storeName = [[storeName copy] autorelease];
671 - (BOOL)hasName:(NSString *)_name inContext:(id)_ctx {
672 _name = [self normalizeKey:_name inContext:_ctx];
674 if ([self hasKey:_name])
675 /* is a stored key ! */
678 /* queried something else */
679 return [super hasName:_name inContext:_ctx];
682 - (NSException *)validateName:(NSString *)_name inContext:(id)_ctx {
683 return [super validateName:[self normalizeKey:_name inContext:_ctx] inContext:_ctx];
686 - (id)lookupStoredName:(NSString *)_name inContext:(id)_ctx {
689 if ((storeName = [self negotiateName:_name inContext:_ctx]) == nil)
692 /* is a stored key ! */
693 return [self objectForKey:storeName];
696 - (id)handleMissingName:(NSString *)_name inContext:(id)_ctx {
697 // TODO: object autocreation support (aka "create on access")
699 [self debugWithFormat:@" found no matching value for key: %@", _name];
703 - (id)lookupName:(NSString *)_name inContext:(id)_ctx acquire:(BOOL)_flag {
706 if (debugLookup) [self debugWithFormat:@"lookup key '%@'", _name];
710 _name = [self normalizeKey:_name inContext:_ctx];
712 [self debugWithFormat:@" normalized '%@'", _name];
714 /* lookup in folder storage */
716 if ((value = [self lookupStoredName:_name inContext:_ctx])) {
717 /* found an SoOFS child in storage */
718 if (debugLookup) [self debugWithFormat:@" stored value: %@", value];
723 [self debugWithFormat:@" not a collection child: %@",
724 [[self allKeys] componentsJoinedByString:@","]];
727 /* queried something else */
728 if ((value = [super lookupName:_name inContext:_ctx acquire:_flag])) {
730 [self debugWithFormat:@" value from superclass: %@", value];
734 return [self handleMissingName:_name inContext:_ctx];
739 - (id)lookupAuthenticatorNamed:(NSString *)_name inContext:(id)_ctx {
740 /* look for a "user-folder" (an authentication database) */
743 if ((auth = [self lookupName:_name inContext:_ctx acquire:NO])==nil)
747 [self logWithFormat:@"use '%@' user-folder: %@", _name, auth];
750 [self logWithFormat:@" auth recursion detected: %@", auth];
754 res = [auth authenticatorInContext:_ctx];
756 [self logWithFormat:@" got authenticator: %@", res];
758 if (debugAuthLookup) {
760 @" recursion detected (%@ returned folder): %@, auth: %@",
765 else if (res == auth) {
766 if (debugAuthLookup) {
768 @" recursion detected (%@ returned auth): %@",
776 - (id)authenticatorInContext:(id)_ctx {
777 /* look for a "user-folder" (an authentication database) */
780 /* the following are flawed and can lead to recursions */
781 if ((auth = [self lookupAuthenticatorNamed:@"htpasswd" inContext:_ctx]))
783 if ((auth = [self lookupAuthenticatorNamed:@"acl_users" inContext:_ctx]))
786 // TODO: check children for extensions
789 [self logWithFormat:@"no user-folder in folder ..."];
791 return [super authenticatorInContext:_ctx];
794 - (NSString *)ownerInContext:(id)_ctx {
797 if ((owner = [self->props objectForKey:@"SoOwner"]))
800 /* let parent handle my owner */
801 return [[self container] ownerOfChild:self inContext:_ctx];
804 - (NSString *)ownerOfChild:(id)_child inContext:(id)_ctx {
805 NSDictionary *childOwners;
808 if ((childOwners = [self->props objectForKey:@"SoChildOwners"])) {
809 if ((owner = [childOwners objectForKey:[_ctx nameInContainer]]))
813 /* let child inherit owner of container */
814 return [self ownerInContext:_ctx];
819 - (WOResourceManager *)resourceManagerInContext:(id)_ctx {
820 if (self->resourceManager == nil) {
821 self->resourceManager =
822 [[OFSResourceManager alloc] initWithBaseObject:self inContext:_ctx];
824 return self->resourceManager;
827 /* version control */
829 - (void)checkVersionControlSpecials {
830 id<NGFileManager> fm;
834 if ((fm = [self fileManager]) == nil) return;
835 if ((sp = [self storagePath]) == nil) return;
837 p = [sp stringByAppendingPathComponent:@"CVS"];
838 self->flags.hasCVS = [fm fileExistsAtPath:p isDirectory:&isDir]
842 p = [sp stringByAppendingPathComponent:@".svn"];
843 self->flags.hasSvn = [fm fileExistsAtPath:p isDirectory:&isDir]
847 self->flags.checkedVersionSpecials = 1;
849 - (BOOL)isCvsControlled {
850 if (!self->flags.checkedVersionSpecials)
851 [self checkVersionControlSpecials];
852 return self->flags.hasCVS;
854 - (BOOL)isSvnControlled {
855 if (!self->flags.checkedVersionSpecials)
856 [self checkVersionControlSpecials];
857 return self->flags.hasSvn;
862 @implementation OFSFolder(Factory)
864 + (id)instantiateInFactoryContext:(OFSFactoryContext *)_ctx {
865 /* look into plist for class */
872 plistPath = [[_ctx storagePath]
873 stringByAppendingPathComponent:@".props.plist"];
874 content = [[_ctx fileManager] contentsAtPath:plistPath];
875 if (content == nil) {
877 clazz = [self soClass];
880 /* parse the existing plist file */
884 string = [[NSString alloc] initWithData:content
885 encoding:[NSString defaultCStringEncoding]];
887 [self logWithFormat:@"could not make string for stored data."];
888 return [NSException exceptionWithHTTPStatus:500
889 reason:@"stored property list is corrupted"];
892 if ((plist = [string propertyList]) == nil) {
894 [self logWithFormat:@"could not make plist for stored data."];
895 return [NSException exceptionWithHTTPStatus:500
897 @"stored property list is corrupted "
898 @"(not in plist format)"];
902 /* lookup the classname in plist */
904 className = [plist objectForKey:@"SoClassName"];
905 if ([className length] == 0) {
906 if ((className = [plist objectForKey:@"SoFolderClassName"]))
908 @"%@: SoFolderClassName is deprecated (use SoClassName) !",
912 if ([className length] == 0) {
913 /* no special class assigned, use default */
914 clazz = [self soClass];
917 clazz = [[SoClassRegistry sharedClassRegistry]
918 soClassWithName:className];
920 [self logWithFormat:@"did not find SoClass: %@", className];
928 if (factoryDebugOn) {
929 [self debugWithFormat:@"instantiate child %@ from class %@",
930 [_ctx nameInContainer], clazz];
933 object = [clazz instantiateObject];
934 [object takeStorageInfoFromContext:_ctx];
938 @end /* OFSFolder(Factory) */