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