2 Copyright (C) 2000-2003 SKYRIX Software AG
4 This file is part of OGo
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
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.
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
21 // $Id: OFSFolder.m 4 2004-08-20 17:04:31Z helge $
23 #include "OFSFolder.h"
25 #include "OFSFactoryContext.h"
26 #include "OFSFactoryRegistry.h"
27 #include "OFSResourceManager.h"
28 #include "OFSFolderClassDescription.h"
29 #include "OFSFolderDataSource.h"
30 #include <NGObjWeb/WOResponse.h>
33 @implementation OFSFolder
35 static BOOL factoryDebugOn = NO;
36 static BOOL debugLookup = NO;
37 static BOOL debugRestore = NO;
38 static BOOL debugNegotiate = NO;
39 static BOOL debugAuthLookup = NO;
42 return [super version] + 1 /* v2 */;
45 static BOOL didInit = NO;
47 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
49 NSAssert2([super version] == 1,
50 @"invalid superclass (%@) version %i !",
51 NSStringFromClass([self superclass]), [super version]);
53 debugLookup = [ud boolForKey:@"SoDebugKeyLookup"];
54 factoryDebugOn = [ud boolForKey:@"SoOFSDebugFactory"];
55 debugRestore = [ud boolForKey:@"SoOFSDebugRestore"];
56 debugNegotiate = [ud boolForKey:@"SoOFSDebugNegotiate"];
57 debugAuthLookup = [ud boolForKey:@"SoOFSDebugAuthLookup"];
62 [(OFSResourceManager *)self->resourceManager invalidate];
63 [self->resourceManager release];
65 [[self->children allValues]
66 makeObjectsPerformSelector:@selector(detachFromContainer)];
68 [self->childNames release];
69 [self->props release];
70 [self->children release];
76 - (NSString *)propertyFilename {
77 return @".props.plist";
80 - (BOOL)isCollection {
84 return [self->childNames count] > 0 ? YES : NO;
87 - (NSArray *)allKeys {
88 return self->childNames;
91 - (BOOL)hasKey:(NSString *)_key {
92 return [self->childNames containsObject:_key];
95 - (id)objectForKey:(NSString *)_key {
96 OFSFactoryContext *ctx;
97 NSDictionary *fileAttrs;
98 NSString *fileType, *mimeType;
103 if ((child = [self->children objectForKey:_key]))
105 return [child isNotNull] ? child : nil;
107 if ([_key hasPrefix:@"."])
108 /* do not consider keys starting with a point ... */
111 if (self->flags.didLoadAll)
112 /* everything is cached, should be in there .. */
115 if (![self->childNames containsObject:_key])
116 /* not a storage key anyway */
119 /* find out filetype */
121 childPath = [self storagePathForChildKey:_key];
122 fileAttrs = [[self fileManager] fileAttributesAtPath:childPath
124 fileType = [fileAttrs objectForKey:NSFileType];
125 mimeType = [fileAttrs objectForKey:@"NSFileMimeType"];
128 [self logWithFormat:@"got no file type for child %@ ...", _key];
130 /* create factory context */
132 ctx = [OFSFactoryContext contextForChild:_key
133 storagePath:childPath
135 ctx->fileType = [fileType copy];
136 ctx->mimeType = [mimeType copy];
140 if ((factory = [self restorationFactoryForContext:ctx]) == nil) {
141 [self logWithFormat:@"found no factory for key '%@' (%@, mime=%@)",
142 _key, fileType, mimeType];
143 return [NSException exceptionWithHTTPStatus:500
144 reason:@"found no factory for object !"];
148 [self debugWithFormat:@"selected factory %@ for key %@", factory, _key];
150 /* instantiate and register */
152 if (self->children == nil) {
154 [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
157 if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
158 [self logWithFormat:@"factory did not instantiate object for key '%@'",
160 child = [NSException exceptionWithHTTPStatus:500
161 reason:@"instantiation of object failed !"];
164 [self->children setObject:child forKey:_key];
166 /* awake object, handle possible replacement result */
168 if (![child isKindOfClass:[NSException class]]) {
171 replacement = [child awakeFromFetchInContext:ctx];
172 if (replacement != child) {
173 if (replacement == nil)
174 [self->children removeObjectForKey:_key];
176 [self->children setObject:replacement forKey:_key];
183 - (NSArray *)allValues {
187 if (self->flags.didLoadAll)
188 return [self->children allValues];
190 /* query each key to load it into the children cache */
192 keys = [self->childNames objectEnumerator];
193 while ((key = [keys nextObject]))
194 [self objectForKey:key];
196 self->flags.didLoadAll = 1;
197 return [self->children allValues];
200 - (NSEnumerator *)keyEnumerator {
201 return [self->childNames objectEnumerator];
203 - (NSEnumerator *)objectEnumerator {
204 return [[self allValues] objectEnumerator];
207 - (BOOL)isValidKey:(NSString *)_key {
209 Check whether key is usable for storage (extract some FS sensitive or
213 if ([_key length] == 0) return NO;
214 c = [_key characterAtIndex:0];
215 if (c == '.') return NO;
216 if (c == '~') return NO;
217 if (c == '%') return NO;
218 if (c == '/') return NO;
219 if ([_key rangeOfString:@"/"].length > 0)
220 // TBD: we should allow '/' in filenames
222 if ([_key isEqualToString:[self propertyFilename]])
229 - (EODataSource *)contentDataSource {
230 return [OFSFolderDataSource dataSourceOnFolder:self];
238 - (NSString *)storagePathForChildKey:(NSString *)_name {
239 if (![self isValidKey:_name]) return nil;
240 return [[self storagePath] stringByAppendingPathComponent:_name];
243 - (OFSFactoryRegistry *)factoryRegistry {
244 return [OFSFactoryRegistry sharedFactoryRegistry];
247 - (id)restorationFactoryForContext:(OFSFactoryContext *)_ctx {
248 return [[self factoryRegistry] restorationFactoryForContext:_ctx];
250 - (id)creationFactoryForContext:(OFSFactoryContext *)_ctx {
251 return [[self factoryRegistry] creationFactoryForContext:_ctx];
256 - (NSClassDescription *)soClassDescription {
257 // TODO: cache class description ?
258 return [[[OFSFolderClassDescription alloc] initWithFolder:self] autorelease];
260 - (NSArray *)attributeKeys {
261 return [self->props allKeys];
263 - (NSArray *)toOneRelationshipKeys {
264 return [self allKeys];
267 - (void)filterChildNameArray:(NSMutableArray *)p {
270 [p removeObject:[self propertyFilename]];
272 for (i = 0; i < [p count];) {
276 k = [p objectAtIndex:i];
278 if (kl == 3 && !self->flags.hasCVS && [k isEqualToString:@"CVS"]) {
279 self->flags.hasCVS = 1;
280 [p removeObjectAtIndex:i];
282 else if (kl == 4 && !self->flags.hasSvn && [k isEqualToString:@".svn"]) {
283 self->flags.hasSvn = 1;
284 [p removeObjectAtIndex:i];
286 else if ([k hasPrefix:@"."])
287 [p removeObjectAtIndex:i];
291 self->flags.checkedVersionSpecials = 1;
294 - (id)awakeFromFetchInContext:(OFSFactoryContext *)_ctx {
299 [self debugWithFormat:@"-awakeFromContext:%@", _ctx];
301 if ((p = [super awakeFromFetchInContext:_ctx]) != self) {
303 [self debugWithFormat:@" parent replaced object with: %@", p];
307 sp = [_ctx storagePath];
309 [self debugWithFormat:@" restore path: '%@'", sp];
311 /* load the dictionary properties */
313 p = [sp stringByAppendingPathComponent:[self propertyFilename]];
314 self->props = [[NSDictionary alloc] initWithContentsOfFile:p];
317 [self debugWithFormat:@" restored %i properties: %@",
319 [[self->props allKeys] componentsJoinedByString:@","]];
322 /* load the collection children names */
324 p = [[[_ctx fileManager] directoryContentsAtPath:sp] mutableCopy];
326 [self debugWithFormat:@"couldn't get child names at path '%@'.", p];
330 [self debugWithFormat:@" storage child names at '%@': %@", sp, p];
332 [self filterChildNameArray:p];
333 [p sortUsingSelector:@selector(compare:)];
335 self->childNames = [p copy];
339 [self debugWithFormat:@" restored child names: %@",
340 [self->childNames componentsJoinedByString:@","]];
346 - (void)flushChildCache {
347 [[self->children allValues]
348 makeObjectsPerformSelector:@selector(detachFromContainer)];
349 [self->children removeAllObjects];
350 self->flags.didLoadAll = 0;
353 - (NSException *)reload {
354 // TODO: reload folder !
355 [self flushChildCache];
361 - (id)valueForKey:(NSString *)_name {
362 /* map out some very private keys */
367 if ((v = [self->props objectForKey:_name]))
370 if ((nl = [_name length]) == 0)
373 c = [_name characterAtIndex:0];
376 return [super valueForKey:_name];
381 - (BOOL)allowRecursiveDeleteInContext:(id)_ctx {
385 - (NSString *)defaultMethodNameInContext:(id)_ctx {
388 - (id)lookupDefaultMethod {
391 ctx = [[WOApplication application] context];
392 return [self lookupName:[self defaultMethodNameInContext:ctx]
397 - (id)GETAction:(id)_ctx {
398 WOResponse *r = [(id <WOPageGenerationContext>)_ctx response];
399 NSString *uri, *qs, *method;
402 if (![[_ctx soRequestType] isEqualToString:@"METHOD"])
405 if ((method = [self defaultMethodNameInContext:_ctx]) == nil)
406 /* no default method */
411 uri = [[(id <WOPageGenerationContext>)_ctx request] uri];
412 ra = [uri rangeOfString:@"?"];
414 qs = [uri substringFromIndex:ra.location];
415 uri = [uri substringToIndex:ra.location];
419 uri = [uri stringByAppendingPathComponent:method];
420 if (qs) uri = [uri stringByAppendingString:qs];
422 [r setStatus:302 /* moved */];
423 [r setHeader:uri forKey:@"location"];
427 - (id)DELETEAction:(id)_ctx {
430 if ((e = [self validateForDelete]))
433 if ([self hasChildren]) {
434 if (![self allowRecursiveDeleteInContext:_ctx]) {
435 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
436 reason:@"tried to delete a filled folder"];
439 return [super DELETEAction:_ctx];
442 - (id)PUTAction:(id)_ctx {
443 OFSFactoryContext *ctx;
450 pathInfo = [_ctx pathInfo];
451 /* TODO: NEED TO REWRITE path info to a key (eg strip .vcf) ! */
453 // TODO: return conflict, on attempt to create subfolder
454 if ([pathInfo length] == 0) {
455 [self debugWithFormat:@"attempt to PUT to an OFSFolder !"];
456 [self debugWithFormat:@"body:\n%@", [[(id <WOPageGenerationContext>)_ctx request] contentAsString]];
458 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
459 reason:@"HTTP PUT not allowed on a folder resource"];
462 if ([self->childNames containsObject:pathInfo]) {
464 Explained: PUT can be and is used to overwrite existing resources. But
465 if PUT was issued on an existing resource, the SoObject for this resource
466 will receive the PUT action, not it's contained.
467 So: the container (folder) only receives a PUT action with a PATH_INFO if
468 the resource to be PUT is new.
470 [self debugWithFormat:
471 @"internal inconsistency, tried to create an existing resource !"];
472 return [NSException exceptionWithHTTPStatus:500 /* method not allowed */
473 reason:@"tried to create an existing resource"];
476 if ((childPath = [self storagePathForChildKey:pathInfo]) == nil) {
477 [self debugWithFormat:@"invalid name for child !"];
478 return [NSException exceptionWithHTTPStatus:400 /* bad request */
479 reason:@"the name for the child creation was invalid"];
482 /* create factory context */
484 ctx = [OFSFactoryContext contextForNewChild:pathInfo
485 storagePath:childPath
487 ctx->fileType = [NSFileTypeRegular retain];
488 ctx->mimeType = [[[(id <WOPageGenerationContext>)_ctx request] headerForKey:@"content-type"] copy];
492 if ((factory = [self creationFactoryForContext:ctx]) == nil) {
493 [self logWithFormat:@"found no factory for new key '%@' (%@, mime=%@)",
494 pathInfo, ctx->fileType, [ctx mimeType]];
495 return [NSException exceptionWithHTTPStatus:500
496 reason:@"found no factory for new object !"];
499 if (factoryDebugOn) {
500 [self debugWithFormat:@"selected factory %@ for new child named %@",
504 /* instantiate and register */
506 if (self->children == nil) {
508 [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
511 if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
513 @"factory did not instantiate new object for key '%@'",
515 return [NSException exceptionWithHTTPStatus:500
516 reason:@"instantiation of object failed !"];
518 if ([child isKindOfClass:[NSException class]])
521 childPutMethod = [child lookupName:@"PUT" inContext:_ctx acquire:NO];
522 if (childPutMethod == nil) {
523 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
524 reason:@"new child does not support HTTP PUT."];
527 [self->children setObject:child forKey:pathInfo];
529 /* awake object, handle possible replacement result */
534 replacement = [child awakeFromInsertionInContext:ctx];
535 if (replacement != child) {
536 if ([replacement isKindOfClass:[NSException class]]) {
537 replacement = [replacement retain];
538 [self->children removeObjectForKey:pathInfo];
539 return [replacement autorelease];
542 if (replacement == nil) {
543 [self->children removeObjectForKey:pathInfo];
544 return [NSException exceptionWithHTTPStatus:500
545 reason:@"awake failed, reason unknown"];
549 [replacement lookupName:@"PUT" inContext:_ctx acquire:NO];
551 if (childPutMethod == nil) {
552 [self->children removeObjectForKey:pathInfo];
553 return [NSException exceptionWithHTTPStatus:405 /* not allowed */
554 reason:@"new child does not support HTTP PUT."];
557 [self->children setObject:replacement forKey:pathInfo];
563 /* now forward the PUT to the child */
565 result = [[childPutMethod bindToObject:child inContext:_ctx]
566 callOnObject:child inContext:_ctx];
568 /* check whether put was successful */
570 if ([result isKindOfClass:[NSException class]]) {
571 /* creation failed, unregister from childlist */
572 [child detachFromContainer];
573 [self->children removeObjectForKey:pathInfo];
579 - (id)MKCOLAction:(id)_ctx {
582 pathInfo = [_ctx pathInfo];
583 pathInfo = [pathInfo stringByUnescapingURL];
585 if ([pathInfo length] == 0) {
586 [self debugWithFormat:@"attempt to MKCOL an existint OFSFolder !"];
587 return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
588 reason:@"tried MKCOL an an existing resource"];
591 // TBD: create new child
592 // TBD: return conflict, on attempt to create subfolder
593 return [NSException exceptionWithHTTPStatus:403 /* forbidden */
594 reason:@"creating collections is forbidden"];
599 - (NSString *)normalizeKey:(NSString *)_name inContext:(id)_ctx {
600 /* useful for content-negotiation */
604 - (NSString *)selectBestMatchForName:(NSString *)_name
605 fromChildNames:(NSArray *)_matches
611 if ((count = [_matches count]) == 0)
614 storeName = [_matches objectAtIndex:0];
616 // TODO: some real negotiation based on "accept", "language", ..
617 storeName = [_matches objectAtIndex:0];
618 [self logWithFormat:@"negotiate: selected '%@' from: %@.", storeName,
619 [_matches componentsJoinedByString:@","]];
621 if (debugNegotiate) [self logWithFormat:@"negotiated: '%@'", storeName];
625 - (NSString *)negotiateName:(NSString *)_name inContext:(id)_ctx {
626 /* returns a "storeName", one which can be resolved in the store */
627 NSMutableArray *matches;
628 NSString *askedExt, *normName, *storeName;
632 if (debugNegotiate) [self logWithFormat:@"negotiate: %@", _name];
634 availKeys = [self allKeys];
635 if ((count = [availKeys count]) == 0)
636 return nil; /* no content */
637 if ([availKeys containsObject:_name])
638 return _name; /* exact match */
640 /* some hard-coded content negotiation */
642 askedExt = [_name pathExtension];
643 normName = [_name stringByDeletingPathExtension];
645 for (i = 0, matches = nil; i < count; i++) {
646 NSString *storeName, *childNormName;
648 storeName = [availKeys objectAtIndex:i];
649 childNormName = [storeName stringByDeletingPathExtension];
651 if (debugNegotiate) [self logWithFormat:@" check: %@", storeName];
653 if (![normName isEqualToString:childNormName])
657 if (matches == nil) matches = [[NSMutableArray alloc] initWithCapacity:16];
658 [matches addObject:storeName];
662 return nil; /* no matches */
664 storeName = [self selectBestMatchForName:normName
665 fromChildNames:matches
667 storeName = [[storeName copy] autorelease];
672 - (BOOL)hasName:(NSString *)_name inContext:(id)_ctx {
673 _name = [self normalizeKey:_name inContext:_ctx];
675 if ([self hasKey:_name])
676 /* is a stored key ! */
679 /* queried something else */
680 return [super hasName:_name inContext:_ctx];
683 - (NSException *)validateName:(NSString *)_name inContext:(id)_ctx {
684 return [super validateName:[self normalizeKey:_name inContext:_ctx] inContext:_ctx];
687 - (id)lookupStoredName:(NSString *)_name inContext:(id)_ctx {
690 if ((storeName = [self negotiateName:_name inContext:_ctx]) == nil)
693 /* is a stored key ! */
694 return [self objectForKey:storeName];
697 - (id)handleMissingName:(NSString *)_name inContext:(id)_ctx {
698 // TODO: object autocreation support (aka "create on access")
700 [self debugWithFormat:@" found no matching value for key: %@", _name];
704 - (id)lookupName:(NSString *)_name inContext:(id)_ctx acquire:(BOOL)_flag {
707 if (debugLookup) [self debugWithFormat:@"lookup key '%@'", _name];
711 _name = [self normalizeKey:_name inContext:_ctx];
713 [self debugWithFormat:@" normalized '%@'", _name];
715 /* lookup in folder storage */
717 if ((value = [self lookupStoredName:_name inContext:_ctx])) {
718 /* found an SoOFS child in storage */
719 if (debugLookup) [self debugWithFormat:@" stored value: %@", value];
724 [self debugWithFormat:@" not a collection child: %@",
725 [[self allKeys] componentsJoinedByString:@","]];
728 /* queried something else */
729 if ((value = [super lookupName:_name inContext:_ctx acquire:_flag])) {
731 [self debugWithFormat:@" value from superclass: %@", value];
735 return [self handleMissingName:_name inContext:_ctx];
740 - (id)lookupAuthenticatorNamed:(NSString *)_name inContext:(id)_ctx {
741 /* look for a "user-folder" (an authentication database) */
744 if ((auth = [self lookupName:_name inContext:_ctx acquire:NO])==nil)
748 [self logWithFormat:@"use '%@' user-folder: %@", _name, auth];
751 [self logWithFormat:@" auth recursion detected: %@", auth];
755 res = [auth authenticatorInContext:_ctx];
757 [self logWithFormat:@" got authenticator: %@", res];
759 if (debugAuthLookup) {
761 @" recursion detected (%@ returned folder): %@, auth: %@",
766 else if (res == auth) {
767 if (debugAuthLookup) {
769 @" recursion detected (%@ returned auth): %@",
777 - (id)authenticatorInContext:(id)_ctx {
778 /* look for a "user-folder" (an authentication database) */
781 /* the following are flawed and can lead to recursions */
782 if ((auth = [self lookupAuthenticatorNamed:@"htpasswd" inContext:_ctx]))
784 if ((auth = [self lookupAuthenticatorNamed:@"acl_users" inContext:_ctx]))
787 // TODO: check children for extensions
790 [self logWithFormat:@"no user-folder in folder ..."];
792 return [super authenticatorInContext:_ctx];
795 - (NSString *)ownerInContext:(id)_ctx {
798 if ((owner = [self->props objectForKey:@"SoOwner"]))
801 /* let parent handle my owner */
802 return [[self container] ownerOfChild:self inContext:_ctx];
805 - (NSString *)ownerOfChild:(id)_child inContext:(id)_ctx {
806 NSDictionary *childOwners;
809 if ((childOwners = [self->props objectForKey:@"SoChildOwners"])) {
810 if ((owner = [childOwners objectForKey:[_ctx nameInContainer]]))
814 /* let child inherit owner of container */
815 return [self ownerInContext:_ctx];
820 - (WOResourceManager *)resourceManagerInContext:(id)_ctx {
821 if (self->resourceManager == nil) {
822 self->resourceManager =
823 [[OFSResourceManager alloc] initWithBaseObject:self inContext:_ctx];
825 return self->resourceManager;
828 /* version control */
830 - (void)checkVersionControlSpecials {
831 id<NGFileManager> fm;
835 if ((fm = [self fileManager]) == nil) return;
836 if ((sp = [self storagePath]) == nil) return;
838 p = [sp stringByAppendingPathComponent:@"CVS"];
839 self->flags.hasCVS = [fm fileExistsAtPath:p isDirectory:&isDir]
843 p = [sp stringByAppendingPathComponent:@".svn"];
844 self->flags.hasSvn = [fm fileExistsAtPath:p isDirectory:&isDir]
848 self->flags.checkedVersionSpecials = 1;
850 - (BOOL)isCvsControlled {
851 if (!self->flags.checkedVersionSpecials)
852 [self checkVersionControlSpecials];
853 return self->flags.hasCVS;
855 - (BOOL)isSvnControlled {
856 if (!self->flags.checkedVersionSpecials)
857 [self checkVersionControlSpecials];
858 return self->flags.hasSvn;
863 @implementation OFSFolder(Factory)
865 + (id)instantiateInFactoryContext:(OFSFactoryContext *)_ctx {
866 /* look into plist for class */
873 plistPath = [[_ctx storagePath]
874 stringByAppendingPathComponent:@".props.plist"];
875 content = [[_ctx fileManager] contentsAtPath:plistPath];
876 if (content == nil) {
878 clazz = [self soClass];
881 /* parse the existing plist file */
885 string = [[NSString alloc] initWithData:content
886 encoding:[NSString defaultCStringEncoding]];
888 [self logWithFormat:@"could not make string for stored data."];
889 return [NSException exceptionWithHTTPStatus:500
890 reason:@"stored property list is corrupted"];
893 if ((plist = [string propertyList]) == nil) {
895 [self logWithFormat:@"could not make plist for stored data."];
896 return [NSException exceptionWithHTTPStatus:500
898 @"stored property list is corrupted "
899 @"(not in plist format)"];
903 /* lookup the classname in plist */
905 className = [plist objectForKey:@"SoClassName"];
906 if ([className length] == 0) {
907 if ((className = [plist objectForKey:@"SoFolderClassName"]))
909 @"%@: SoFolderClassName is deprecated (use SoClassName) !",
913 if ([className length] == 0) {
914 /* no special class assigned, use default */
915 clazz = [self soClass];
918 clazz = [[SoClassRegistry sharedClassRegistry]
919 soClassWithName:className];
921 [self logWithFormat:@"did not find SoClass: %@", className];
929 if (factoryDebugOn) {
930 [self debugWithFormat:@"instantiate child %@ from class %@",
931 [_ctx nameInContainer], clazz];
934 object = [clazz instantiateObject];
935 [object takeStorageInfoFromContext:_ctx];
939 @end /* OFSFolder(Factory) */