]> err.no Git - sope/blob - sope-appserver/SoOFS/OFSFolder.m
added strict OSX bundle dependencies
[sope] / sope-appserver / SoOFS / OFSFolder.m
1 /*
2   Copyright (C) 2002-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "OFSFolder.h"
23 #include "OFSFile.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>
30 #include "common.h"
31
32 @implementation OFSFolder
33
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;
39
40 + (int)version {
41   return [super version] + 1 /* v2 */;
42 }
43 + (void)initialize {
44   static BOOL didInit = NO;
45   if (!didInit) {
46     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
47     didInit = YES;
48     NSAssert2([super version] == 1,
49               @"invalid superclass (%@) version %i !",
50               NSStringFromClass([self superclass]), [super version]);
51     
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"];
57   }
58 }
59
60 - (void)dealloc {
61   [(OFSResourceManager *)self->resourceManager invalidate];
62   [self->resourceManager release];
63   
64   [[self->children allValues] 
65     makeObjectsPerformSelector:@selector(detachFromContainer)];
66   
67   [self->childNames  release];
68   [self->props    release];
69   [self->children release];
70   [super dealloc];
71 }
72
73 /* accessors */
74
75 - (NSString *)propertyFilename {
76   return @".props.plist";
77 }
78
79 - (BOOL)isCollection {
80   return YES;
81 }
82 - (BOOL)hasChildren {
83   return [self->childNames count] > 0 ? YES : NO;
84 }
85
86 - (NSArray *)allKeys {
87   return self->childNames;
88 }
89
90 - (BOOL)hasKey:(NSString *)_key {
91   return [self->childNames containsObject:_key];
92 }
93
94 - (id)objectForKey:(NSString *)_key {
95   OFSFactoryContext *ctx;
96   NSDictionary *fileAttrs;
97   NSString     *fileType, *mimeType;
98   NSString     *childPath;
99   id child;
100   id factory;
101   
102   if ((child = [self->children objectForKey:_key]))
103     /* cached */
104     return [child isNotNull] ? child : nil;
105   
106   if ([_key hasPrefix:@"."])
107     /* do not consider keys starting with a point ... */
108     return nil;
109   
110   if (self->flags.didLoadAll)
111     /* everything is cached, should be in there .. */
112     return nil;
113   
114   if (![self->childNames containsObject:_key])
115     /* not a storage key anyway */
116     return nil;
117   
118   /* find out filetype */
119   
120   childPath = [self storagePathForChildKey:_key];
121   fileAttrs = [[self fileManager] fileAttributesAtPath:childPath
122                                   traverseLink:YES];
123   fileType  = [fileAttrs objectForKey:NSFileType];
124   mimeType  = [fileAttrs objectForKey:@"NSFileMimeType"];
125   
126   if (fileType == nil)
127     [self logWithFormat:@"got no file type for child %@ ...", _key];
128   
129   /* create factory context */
130   
131   ctx = [OFSFactoryContext contextForChild:_key
132                            storagePath:childPath
133                            ofFolder:self];
134   ctx->fileType = [fileType copy];
135   ctx->mimeType = [mimeType copy];
136   
137   /* lookup factory */
138   
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 !"];
144   }
145   
146   if (factoryDebugOn)
147     [self debugWithFormat:@"selected factory %@ for key %@", factory, _key];
148   
149   /* instantiate and register */
150   
151   if (self->children == nil) {
152     self->children =
153       [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
154   }
155   
156   if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
157     [self logWithFormat:@"factory did not instantiate object for key '%@'",
158             _key];
159     child = [NSException exceptionWithHTTPStatus:500
160                          reason:@"instantiation of object failed !"];
161   }
162   
163   [self->children setObject:child forKey:_key];
164   
165   /* awake object, handle possible replacement result */
166   
167   if (![child isKindOfClass:[NSException class]]) {
168     id replacement;
169     
170     replacement = [child awakeFromFetchInContext:ctx];
171     if (replacement != child) {
172       if (replacement == nil)
173         [self->children removeObjectForKey:_key];
174       else
175         [self->children setObject:replacement forKey:_key];
176     }
177   }
178   
179   return child;
180 }
181
182 - (NSArray *)allValues {
183   NSEnumerator *keys;
184   NSString     *key;
185   
186   if (self->flags.didLoadAll)
187     return [self->children allValues];
188   
189   /* query each key to load it into the children cache */
190   
191   keys = [self->childNames objectEnumerator];
192   while ((key = [keys nextObject]))
193     [self objectForKey:key];
194   
195   self->flags.didLoadAll = 1;
196   return [self->children allValues];
197 }
198
199 - (NSEnumerator *)keyEnumerator {
200   return [self->childNames objectEnumerator];
201 }
202 - (NSEnumerator *)objectEnumerator {
203   return [[self allValues] objectEnumerator];
204 }
205
206 - (BOOL)isValidKey:(NSString *)_key {
207   /* 
208      Check whether key is usable for storage (extract some FS sensitive or 
209      private keys)
210   */
211   unichar c;
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
220     return NO;
221   if ([_key isEqualToString:[self propertyFilename]])
222     return NO;
223   return YES;
224 }
225
226 /* datasource */
227
228 - (EODataSource *)contentDataSource {
229   return [OFSFolderDataSource dataSourceOnFolder:self];
230 }
231
232 /* storage */
233
234 - (void)willChange {
235 }
236
237 - (NSString *)storagePathForChildKey:(NSString *)_name {
238   if (![self isValidKey:_name]) return nil;
239   return [[self storagePath] stringByAppendingPathComponent:_name];
240 }
241
242 - (OFSFactoryRegistry *)factoryRegistry {
243   return [OFSFactoryRegistry sharedFactoryRegistry];
244 }
245
246 - (id)restorationFactoryForContext:(OFSFactoryContext *)_ctx {
247   return [[self factoryRegistry] restorationFactoryForContext:_ctx];
248 }
249 - (id)creationFactoryForContext:(OFSFactoryContext *)_ctx {
250   return [[self factoryRegistry] creationFactoryForContext:_ctx];
251 }
252
253 /* unarchiving */
254
255 - (NSClassDescription *)soClassDescription {
256   // TODO: cache class description ?
257   return [[[OFSFolderClassDescription alloc] initWithFolder:self] autorelease];
258 }
259 - (NSArray *)attributeKeys {
260   return [self->props allKeys];
261 }
262 - (NSArray *)toOneRelationshipKeys {
263   return [self allKeys];
264 }
265
266 - (void)filterChildNameArray:(NSMutableArray *)p {
267   unsigned i;
268   
269   [p removeObject:[self propertyFilename]];
270   
271   for (i = 0; i < [p count];) {
272     NSString *k;
273     unsigned kl;
274     
275     k = [p objectAtIndex:i];
276     kl = [k length];
277     if (kl == 3 && !self->flags.hasCVS && [k isEqualToString:@"CVS"]) {
278       self->flags.hasCVS = 1;
279       [p removeObjectAtIndex:i];
280     }
281     else if (kl == 4 && !self->flags.hasSvn && [k isEqualToString:@".svn"]) {
282       self->flags.hasSvn = 1;
283       [p removeObjectAtIndex:i];
284     }
285     else if ([k hasPrefix:@"."])
286       [p removeObjectAtIndex:i];
287     else
288       i++;
289   }
290   self->flags.checkedVersionSpecials = 1;
291 }
292
293 - (id)awakeFromFetchInContext:(OFSFactoryContext *)_ctx {
294   NSString *sp;
295   id       p;
296   
297   if (debugRestore)
298     [self debugWithFormat:@"-awakeFromContext:%@", _ctx];
299   
300   if ((p = [super awakeFromFetchInContext:_ctx]) != self) {
301     if (debugRestore)
302       [self debugWithFormat:@"  parent replaced object with: %@", p];
303     return p;
304   }
305   
306   sp = [_ctx storagePath];
307   if (debugRestore)
308     [self debugWithFormat:@"  restore path: '%@'", sp];
309   
310   /* load the dictionary properties */
311   
312   p = [sp stringByAppendingPathComponent:[self propertyFilename]];
313   self->props = [[NSDictionary alloc] initWithContentsOfFile:p];
314   
315   if (debugRestore) {
316     [self debugWithFormat:@"  restored %i properties: %@", 
317             [self->props count],
318             [[self->props allKeys] componentsJoinedByString:@","]];
319   }
320   
321   /* load the collection children names */
322   
323   p = [[[_ctx fileManager] directoryContentsAtPath:sp] mutableCopy];
324   if (p == nil) {
325     [self debugWithFormat:@"couldn't get child names at path '%@'.", p];
326     return nil;
327   }
328   if (debugRestore)
329     [self debugWithFormat:@"  storage child names at '%@': %@", sp, p];
330   
331   [self filterChildNameArray:p];
332   [p sortUsingSelector:@selector(compare:)];
333   
334   self->childNames = [p copy];
335   [p release];
336   
337   if (debugRestore) {
338     [self debugWithFormat:@"  restored child names: %@", 
339             [self->childNames componentsJoinedByString:@","]];
340   }
341   
342   return self;
343 }
344
345 - (void)flushChildCache {
346   [[self->children allValues] 
347     makeObjectsPerformSelector:@selector(detachFromContainer)];
348   [self->children removeAllObjects];
349   self->flags.didLoadAll = 0;
350 }
351
352 - (NSException *)reload {
353   // TODO: reload folder !
354   [self flushChildCache];
355   return nil;
356 }
357
358 /* KVC */
359
360 - (id)valueForKey:(NSString *)_name {
361   /* map out some very private keys */
362   unsigned nl;
363   unichar  c;
364   NSString *v;
365   
366   if ((v = [self->props objectForKey:_name]))
367     return v;
368   
369   if ((nl = [_name length]) == 0)
370     return nil;
371   
372   c = [_name characterAtIndex:0];
373   // TBD ?
374   
375   return [super valueForKey:_name];
376 }
377
378 /* operations */
379
380 - (BOOL)allowRecursiveDeleteInContext:(id)_ctx {
381   return NO;
382 }
383
384 - (NSString *)defaultMethodNameInContext:(id)_ctx {
385   return @"index";
386 }
387 - (id)lookupDefaultMethod {
388   id ctx = nil;
389   
390   ctx = [[WOApplication application] context];
391   return [self lookupName:[self defaultMethodNameInContext:ctx]
392                inContext:ctx
393                acquire:YES];
394 }
395
396 - (id)GETAction:(id)_ctx {
397   WOResponse *r = [(id <WOPageGenerationContext>)_ctx response];
398   NSString   *uri, *qs, *method;
399   NSRange    ra;
400   
401   if (![[_ctx soRequestType] isEqualToString:@"METHOD"])
402     return self;
403   
404   if ((method = [self defaultMethodNameInContext:_ctx]) == nil)
405     /* no default method */
406     return self;
407
408   /* construct URI */
409   
410   uri = [[(id <WOPageGenerationContext>)_ctx request] uri];
411   ra = [uri rangeOfString:@"?"];
412   if (ra.length > 0) {
413     qs  = [uri substringFromIndex:ra.location];
414     uri = [uri substringToIndex:ra.location];
415   }
416   else
417     qs = nil;
418   uri = [uri stringByAppendingPathComponent:method];
419   if (qs) uri = [uri stringByAppendingString:qs];
420   
421   [r setStatus:302 /* moved */];
422   [r setHeader:uri forKey:@"location"];
423   return r;
424 }
425
426 - (id)DELETEAction:(id)_ctx {
427   NSException *e;
428   
429   if ((e = [self validateForDelete]))
430     return e;
431   
432   if ([self hasChildren]) {
433     if (![self allowRecursiveDeleteInContext:_ctx]) {
434       return [NSException exceptionWithHTTPStatus:403 /* forbidden */
435                           reason:@"tried to delete a filled folder"];
436     }
437   }
438   return [super DELETEAction:_ctx];
439 }
440
441 - (id)PUTAction:(id)_ctx {
442   OFSFactoryContext *ctx;
443   NSString    *pathInfo;
444   NSString    *childPath;
445   id          factory;
446   id          result, child;
447   id          childPutMethod;
448   
449   pathInfo = [_ctx pathInfo];
450   /* TODO: NEED TO REWRITE path info to a key (eg strip .vcf) ! */
451   
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]];
456     
457     return [NSException exceptionWithHTTPStatus:405 /* method not allowed */
458                         reason:@"HTTP PUT not allowed on a folder resource"];
459   }
460   
461   if ([self->childNames containsObject:pathInfo]) {
462     /*
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.
468     */
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"];
473   }
474   
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"];
479   }
480   
481   /* create factory context */
482   
483   ctx = [OFSFactoryContext contextForNewChild:pathInfo
484                            storagePath:childPath
485                            ofFolder:self];
486   ctx->fileType = [NSFileTypeRegular retain];
487   ctx->mimeType = [[[(id <WOPageGenerationContext>)_ctx request] headerForKey:@"content-type"] copy];
488   
489   /* lookup factory */
490   
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 !"];
496   }
497   
498   if (factoryDebugOn) {
499     [self debugWithFormat:@"selected factory %@ for new child named %@", 
500             factory, pathInfo];
501   }
502   
503   /* instantiate and register */
504   
505   if (self->children == nil) {
506     self->children =
507       [[NSMutableDictionary alloc] initWithCapacity:[self->childNames count]];
508   }
509   
510   if ((child = [factory instantiateInFactoryContext:ctx]) == nil) {
511     [self logWithFormat:
512             @"factory did not instantiate new object for key '%@'",
513             pathInfo];
514     return [NSException exceptionWithHTTPStatus:500
515                         reason:@"instantiation of object failed !"];
516   }
517   if ([child isKindOfClass:[NSException class]])
518     return child;
519   
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."];
524   }
525   
526   [self->children setObject:child forKey:pathInfo];
527   
528   /* awake object, handle possible replacement result */
529   
530   {
531     id replacement;
532     
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];
539       }
540       
541       if (replacement == nil) {
542         [self->children removeObjectForKey:pathInfo];
543         return [NSException exceptionWithHTTPStatus:500
544                             reason:@"awake failed, reason unknown"];
545       }
546       else {
547         childPutMethod = 
548           [replacement lookupName:@"PUT" inContext:_ctx acquire:NO];
549         
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."];
554         }
555         
556         [self->children setObject:replacement forKey:pathInfo];
557         child = replacement;
558       }
559     }
560   }
561   
562   /* now forward the PUT to the child */
563   
564   result = [[childPutMethod bindToObject:child inContext:_ctx]
565                             callOnObject:child inContext:_ctx];
566   
567   /* check whether put was successful */
568   
569   if ([result isKindOfClass:[NSException class]]) {
570     /* creation failed, unregister from childlist */
571     [child detachFromContainer];
572     [self->children removeObjectForKey:pathInfo];
573   }
574   
575   return result;
576 }
577
578 - (id)MKCOLAction:(id)_ctx {
579   NSString *pathInfo;
580   
581   pathInfo = [_ctx pathInfo];
582   pathInfo = [pathInfo stringByUnescapingURL];
583   
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"];
588   }
589   
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"];
594 }
595
596 /* lookup */
597
598 - (NSString *)normalizeKey:(NSString *)_name inContext:(id)_ctx {
599   /* useful for content-negotiation */
600   return _name;
601 }
602
603 - (NSString *)selectBestMatchForName:(NSString *)_name 
604   fromChildNames:(NSArray *)_matches
605   inContext:(id)_ctx
606 {
607   NSString *storeName;
608   unsigned count;
609   
610   if ((count = [_matches count]) == 0)
611     storeName = nil;
612   else if (count == 1)
613     storeName = [_matches objectAtIndex:0];
614   else {
615     // TODO: some real negotiation based on "accept", "language", ..
616     storeName = [_matches objectAtIndex:0];
617     [self logWithFormat:@"negotiate: selected '%@' from: %@.", storeName,
618             [_matches componentsJoinedByString:@","]];
619   }
620   if (debugNegotiate) [self logWithFormat:@"negotiated: '%@'", storeName];
621   return storeName;
622 }
623   
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;
628   NSArray  *availKeys;
629   unsigned i, count;
630
631   if (debugNegotiate) [self logWithFormat:@"negotiate: %@", _name];
632   
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 */
638   
639   /* some hard-coded content negotiation */
640   
641   askedExt = [_name pathExtension];
642   normName = [_name stringByDeletingPathExtension];
643   
644   for (i = 0, matches = nil; i < count; i++) {
645     NSString *storeName, *childNormName;
646     
647     storeName     = [availKeys objectAtIndex:i];
648     childNormName = [storeName stringByDeletingPathExtension];
649     
650     if (debugNegotiate) [self logWithFormat:@"  check: %@", storeName];
651     
652     if (![normName isEqualToString:childNormName])
653       /* does not match */
654       continue;
655     
656     if (matches == nil) matches = [[NSMutableArray alloc] initWithCapacity:16];
657     [matches addObject:storeName];
658   }
659   
660   if (matches == nil)
661     return nil; /* no matches */
662   
663   storeName = [self selectBestMatchForName:normName 
664                     fromChildNames:matches
665                     inContext:_ctx];
666   storeName = [[storeName copy] autorelease];
667   [matches release];
668   return storeName;
669 }
670
671 - (BOOL)hasName:(NSString *)_name inContext:(id)_ctx {
672   _name = [self normalizeKey:_name inContext:_ctx];
673   
674   if ([self hasKey:_name])
675     /* is a stored key ! */
676     return YES;
677   
678   /* queried something else */
679   return [super hasName:_name inContext:_ctx];
680 }
681
682 - (NSException *)validateName:(NSString *)_name inContext:(id)_ctx {
683   return [super validateName:[self normalizeKey:_name inContext:_ctx] inContext:_ctx];
684 }
685
686 - (id)lookupStoredName:(NSString *)_name inContext:(id)_ctx {
687   NSString *storeName;
688   
689   if ((storeName = [self negotiateName:_name inContext:_ctx]) == nil)
690     return nil;
691   
692   /* is a stored key ! */
693   return [self objectForKey:storeName];
694 }
695
696 - (id)handleMissingName:(NSString *)_name inContext:(id)_ctx {
697   // TODO: object autocreation support (aka "create on access")
698   if (debugLookup)
699     [self debugWithFormat:@"  found no matching value for key: %@", _name];
700   return nil;
701 }
702
703 - (id)lookupName:(NSString *)_name inContext:(id)_ctx acquire:(BOOL)_flag {
704   id value;
705   
706   if (debugLookup) [self debugWithFormat:@"lookup key '%@'", _name];
707
708   /* normalize key */
709   
710   _name = [self normalizeKey:_name inContext:_ctx];
711   if (debugLookup)
712     [self debugWithFormat:@"  normalized '%@'", _name];
713
714   /* lookup in folder storage */
715   
716   if ((value = [self lookupStoredName:_name inContext:_ctx])) {
717     /* found an SoOFS child in storage */
718     if (debugLookup) [self debugWithFormat:@"  stored value: %@", value];
719     return value;
720   }
721   
722   if (debugLookup) {
723     [self debugWithFormat:@"  not a collection child: %@", 
724             [[self allKeys] componentsJoinedByString:@","]];
725   }
726   
727   /* queried something else */
728   if ((value = [super lookupName:_name inContext:_ctx acquire:_flag])) {
729     if (debugLookup)
730       [self debugWithFormat:@"  value from superclass: %@", value];
731     return value;
732   }
733   
734   return [self handleMissingName:_name inContext:_ctx];
735 }
736
737 /* security */
738
739 - (id)lookupAuthenticatorNamed:(NSString *)_name inContext:(id)_ctx {
740   /* look for a "user-folder" (an authentication database) */
741   id auth, res;
742
743   if ((auth = [self lookupName:_name inContext:_ctx acquire:NO])==nil)
744     return nil;
745   
746   if (debugAuthLookup)
747     [self logWithFormat:@"use '%@' user-folder: %@", _name, auth];
748   if (auth == self) {
749     if (debugAuthLookup)
750       [self logWithFormat:@"  auth recursion detected: %@", auth];
751     return nil;
752   }
753   
754   res = [auth authenticatorInContext:_ctx];
755   if (debugAuthLookup)
756     [self logWithFormat:@"  got authenticator: %@", res];
757   if (res == self) {
758     if (debugAuthLookup) {
759       [self logWithFormat:
760               @"  recursion detected (%@ returned folder): %@, auth: %@", 
761               _name, res, auth];
762     }
763     return nil;
764   }
765   else if (res == auth) {
766     if (debugAuthLookup) {
767       [self logWithFormat:
768               @"  recursion detected (%@ returned auth): %@", 
769               _name, auth];
770     }
771     return nil;
772   }
773   return res;
774 }
775
776 - (id)authenticatorInContext:(id)_ctx {
777   /* look for a "user-folder" (an authentication database) */
778   id auth;
779   
780   /* the following are flawed and can lead to recursions */
781   if ((auth = [self lookupAuthenticatorNamed:@"htpasswd" inContext:_ctx]))
782     return auth;
783   if ((auth = [self lookupAuthenticatorNamed:@"acl_users" inContext:_ctx]))
784     return auth;
785   
786   // TODO: check children for extensions
787   
788   if (debugAuthLookup)
789     [self logWithFormat:@"no user-folder in folder ..."];
790   
791   return [super authenticatorInContext:_ctx];
792 }
793
794 - (NSString *)ownerInContext:(id)_ctx {
795   NSString *owner;
796   
797   if ((owner = [self->props objectForKey:@"SoOwner"]))
798     return owner;
799   
800   /* let parent handle my owner */
801   return [[self container] ownerOfChild:self inContext:_ctx];
802 }
803
804 - (NSString *)ownerOfChild:(id)_child inContext:(id)_ctx {
805   NSDictionary *childOwners;
806   NSString *owner;
807   
808   if ((childOwners = [self->props objectForKey:@"SoChildOwners"])) {
809     if ((owner = [childOwners objectForKey:[_ctx nameInContainer]]))
810       return owner;
811   }
812   
813   /* let child inherit owner of container */
814   return [self ownerInContext:_ctx];
815 }
816
817 /* WO integration */
818
819 - (WOResourceManager *)resourceManagerInContext:(id)_ctx {
820   if (self->resourceManager == nil) {
821     self->resourceManager =
822       [[OFSResourceManager alloc] initWithBaseObject:self inContext:_ctx];
823   }
824   return self->resourceManager;
825 }
826
827 /* version control */
828
829 - (void)checkVersionControlSpecials {
830   id<NGFileManager> fm;
831   NSString *sp, *p;
832   BOOL isDir = NO;
833   
834   if ((fm = [self fileManager]) == nil) return;
835   if ((sp = [self storagePath]) == nil) return;
836   
837   p = [sp stringByAppendingPathComponent:@"CVS"];
838   self->flags.hasCVS = [fm fileExistsAtPath:p isDirectory:&isDir]
839     ? (isDir ? 1 : 0)
840     : 0;
841   
842   p = [sp stringByAppendingPathComponent:@".svn"];
843   self->flags.hasSvn = [fm fileExistsAtPath:p isDirectory:&isDir]
844     ? (isDir ? 1 : 0)
845     : 0;
846   
847   self->flags.checkedVersionSpecials = 1;
848 }
849 - (BOOL)isCvsControlled {
850   if (!self->flags.checkedVersionSpecials)
851     [self checkVersionControlSpecials];
852   return self->flags.hasCVS;
853 }
854 - (BOOL)isSvnControlled {
855   if (!self->flags.checkedVersionSpecials)
856     [self checkVersionControlSpecials];
857   return self->flags.hasSvn;
858 }
859
860 @end /* OFSFolder */
861
862 @implementation OFSFolder(Factory)
863
864 + (id)instantiateInFactoryContext:(OFSFactoryContext *)_ctx {
865   /* look into plist for class */
866   NSDictionary *plist;
867   NSData       *content;
868   SoClass      *clazz;
869   NSString     *plistPath;
870   id object;
871   
872   plistPath = [[_ctx storagePath] 
873                      stringByAppendingPathComponent:@".props.plist"];
874   content = [[_ctx fileManager] contentsAtPath:plistPath];
875   if (content == nil) {
876     /* found no plist */
877     clazz = [self soClass];
878   }
879   else {
880     /* parse the existing plist file */
881     NSString *string;
882     NSString *className;
883     
884     string = [[NSString alloc] initWithData:content
885                              encoding:[NSString defaultCStringEncoding]];
886     if (string == nil) {
887       [self logWithFormat:@"could not make string for stored data."];
888       return [NSException exceptionWithHTTPStatus:500
889                         reason:@"stored property list is corrupted"];
890     }
891     
892     if ((plist = [string propertyList]) == nil) {
893       [string release];
894       [self logWithFormat:@"could not make plist for stored data."];
895       return [NSException exceptionWithHTTPStatus:500
896                         reason:
897                           @"stored property list is corrupted "
898                           @"(not in plist format)"];
899     }
900     [string release];
901   
902     /* lookup the classname in plist */
903     
904     className = [plist objectForKey:@"SoClassName"];
905     if ([className length] == 0) {
906       if ((className = [plist objectForKey:@"SoFolderClassName"]))
907         [self logWithFormat:
908                 @"%@: SoFolderClassName is deprecated (use SoClassName) !",
909                 plistPath];
910     }
911     
912     if ([className length] == 0) {
913       /* no special class assigned, use default */
914       clazz = [self soClass];
915     }
916     else {
917       clazz = [[SoClassRegistry sharedClassRegistry] 
918                 soClassWithName:className];
919       if (clazz == nil) {
920         [self logWithFormat:@"did not find SoClass: %@", className];
921         return nil;
922       }
923     }
924   }
925   
926   /* instantiate */
927   
928   if (factoryDebugOn) {
929     [self debugWithFormat:@"instantiate child %@ from class %@",
930             [_ctx nameInContainer], clazz];
931   }
932   
933   object = [clazz instantiateObject];
934   [object takeStorageInfoFromContext:_ctx];
935   return object;
936 }
937
938 @end /* OFSFolder(Factory) */