]> err.no Git - sope/blob - sope-appserver/NGObjWeb/SoObjects/SoObjectRequestHandler.m
fixed OGo bug #888
[sope] / sope-appserver / NGObjWeb / SoObjects / SoObjectRequestHandler.m
1 /*
2   Copyright (C) 2002-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
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: SoObjectRequestHandler.m 24 2004-08-20 22:54:26Z helge $
22
23 #include "SoObjectRequestHandler.h"
24 #include "SoObject.h"
25 #include "SoObjectMethodDispatcher.h"
26 #include "SoSecurityManager.h"
27 #include "SoDefaultRenderer.h"
28 #include "WOContext+SoObjects.h"
29 #include "WORequest+So.h"
30 #include <NGObjWeb/WOApplication.h>
31 #include <NGObjWeb/WORequest.h>
32 #include <NGObjWeb/WOResponse.h>
33 #include <NGObjWeb/WOElement.h>
34 #include <NGObjWeb/WOTemplate.h>
35 #include <NGObjWeb/WEClientCapabilities.h>
36 #include <NGExtensions/NGRuleContext.h>
37 #include <NGExtensions/NSString+Ext.h>
38 #include "WOComponent+private.h"
39 #include "common.h"
40
41 @interface NSObject(ObjectDispatcher)
42 - (id)dispatcherForContext:(WOContext *)_ctx;
43 - (WOResponse *)preprocessCredentialsInContext:(WOContext *)_ctx;
44 - (void)_sleepWithContext:(WOContext *)_ctx;
45 @end
46
47 @interface NSObject(SoOFS)
48 - (NSString *)storagePath;
49 @end
50
51 @implementation SoObjectRequestHandler
52
53 static BOOL debugOn       = NO;
54 static BOOL debugRulesOn  = NO;
55 static BOOL disableZLHack = NO;
56
57 static Class WOTemplateClass = Nil;
58 static NSString *rapidTurnAroundPath = nil;
59
60 + (int)version {
61   return [super version] + 0 /* 2 */;
62 }
63 + (void)initialize {
64   static BOOL didInit = NO;
65   if (!didInit) {
66     NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
67     didInit = YES;
68     NSAssert2([super version] == 2,
69               @"invalid superclass (%@) version %i !",
70               NSStringFromClass([self superclass]), [super version]);
71     debugOn       = [ud boolForKey:@"SoObjectRequestHandlerDebugEnabled"];
72     debugRulesOn  = [ud boolForKey:@"SoObjectRequestHandlerRulesDebugEnabled"];
73     disableZLHack = [ud boolForKey:@"DisableZideLookHack"];
74
75     WOTemplateClass     = [WOTemplate class];
76     rapidTurnAroundPath = [[ud stringForKey:@"WOProjectDirectory"] copy];    
77   }
78 }
79
80 - (id)init {
81   if ((self = [super init])) {
82     self->dispatcherRules =
83       [[NGRuleContext ruleContextWithModelInUserDefault:
84                         @"SoRequestDispatcherRules"] retain];
85     if (debugRulesOn) [self->dispatcherRules setDebugEnabled:YES];
86   }
87   return self;
88 }
89 - (void)dealloc {
90   [self->dispatcherRules release];
91   [self->rootObject      release];
92   [super dealloc];
93 }
94
95 /* type the request */
96
97 - (BOOL)isObjectPublishingContext:(WOContext *)_ctx {
98   /* 
99      Find out, whether we should do acquisition and dynamic method publishing.
100      This is only appropriate for HEAD/GET/POST requests from non-WebDAV
101      clients ?
102   */
103   id value;
104   
105   value = [self->dispatcherRules valueForKey:@"useAcquisition"];
106   if (debugRulesOn) [self debugWithFormat:@"acquision: %@", value];
107   return [value boolValue];
108 }
109
110 /* request path acquisition */
111
112 - (BOOL)enableZideLookHack {
113   /* Temporary Hack for ZideLook */
114   return disableZLHack ? NO : YES;
115 }
116
117 - (BOOL)skipApplicationName {
118   /* is the application name path of a URI part of the traversal path ? */
119   return NO;
120 }
121
122 - (NSString *)hackZideLookURI:(NSString *)m {
123   if ([m hasPrefix:@"H_chste_Ebene_der_Pers_nlichen_Ordner"]) {
124     m = [m stringByReplacingString:@"H_chste_Ebene_der_Pers_nlichen_Ordner"
125            withString:@"helge"];
126   }
127   else if ([m hasPrefix:@"Suchpfad"]) {
128     m = [m stringByReplacingString:@"Suchpfad"
129            withString:@"helge"];
130   }
131   else if ([m hasPrefix:@"public"]) {
132     /* Evolution query on "/public" */
133     ; // keep it completly
134   }
135   else if ([self skipApplicationName]) {
136     /* cut of appname */
137     NSRange r;
138     r = [m rangeOfString:@"/"];
139     m = [m substringFromIndex:(r.location + r.length)];
140   }
141   return m;
142 }
143 - (NSString *)hackZideLookName:(NSString *)_p {
144   if ([_p isEqualToString:@"Gel_schte_Objekte"])
145     return @"Trash";
146   return _p;
147 }
148
149 - (NSMutableArray *)addSpecialFormValuesInRequest:(WORequest *)_rq
150   toTraversalPath:(NSMutableArray *)_traversalPath
151 {
152   NSArray  *keys;
153   unsigned i, count;
154   
155   keys = [_rq formValueKeys];
156   if ((count = [keys count]) == 0)
157     return _traversalPath;
158   
159   for (i = 0; i < count; i++) {
160     NSString *key;
161     unsigned klen;
162     NSString *m;
163     
164     key  = [keys objectAtIndex:i];
165     klen = [key length];
166     if (klen != 3 && klen < 7)
167       continue;
168     
169     /* calculate method name */
170     
171     m = nil;
172     if (klen == 3 && [key isEqualToString:@"Cmd"]) {
173       /* 
174          check for ASP style ?Cmd query parameter (required in ZideStore),
175          the value is the additional path we need to add
176       */
177       m = [_rq formValueForKey:key]; // need to unescape somehow ?
178     }
179     else if (klen == 7 && [key isEqualToString:@":method"]) {
180       /* 
181          check for ":method" form value, the value is the additional path we 
182          need to add
183       */
184       m = [_rq formValueForKey:key]; // need to unescape somehow ?
185     }
186     else if ([key hasSuffix:@":method"]) {
187       /*
188         Check for XXX:method form-keys, the value is ignored and the
189         XXX is added to the path. This is useful for binding actions
190         to submit buttons, since the value of a submit button is 
191         displayed as it's label in the browser
192       */
193       klen = klen - 7;
194       m = [key substringToIndex:klen];
195     }
196     
197     /* check calculated method */
198     
199     if (m == nil)
200       continue;
201     else if ([m length] == 0) {
202       [self debugWithFormat:@"empty '%@' query parameter !", key];
203       continue;
204     }
205     
206     /* add to path */
207     [_traversalPath addObject:m];
208   }
209   return _traversalPath;
210 }
211
212 - (NSArray *)traversalPathFromRequest:(WORequest *)_rq {
213   static NSArray *rqKeys = nil;
214   NSMutableArray *traversalPath;
215   unsigned i, count;
216   NSString *m;
217   NSArray  *a;
218
219   if (rqKeys == nil)
220     /* cache set of registered request handlers */
221     rqKeys = [[[WOApplication application] registeredRequestHandlerKeys] copy];
222   
223   m = [_rq requestHandlerKey];
224   if ([rqKeys containsObject:m]) {
225     /* 
226        If the request-handler-key parsed by WORequest is valid, we'll consider
227        it a "usual" NGObjWeb query. Note that the appname is *not* processed !
228        Example:
229          /MyApp/wo/...   => match
230          /MyApp/bla/...  => fail
231          /blah/wa/...    => match
232     */
233     a = [_rq requestHandlerPathArray];
234   }
235   else {
236     /* TODO: more options, eg allow the appname to be part of the path */
237     NSRange r;
238
239     /* get URI, cut of leading slash */
240     m = [_rq uri];
241     m = [m substringFromIndex:1];
242     
243     if ([self enableZideLookHack]) 
244       m = [self hackZideLookURI:m];
245     else if ([self skipApplicationName]) {
246       /* cut of application name */
247       r = [m rangeOfString:@"/"];
248       m = [m substringFromIndex:(r.location + r.length)];
249     }
250     
251     /* cut of query parameters */
252     r = [m rangeOfString:@"?"];
253     if (r.length > 0)
254       m = [m substringToIndex:r.location];
255     
256     /* split into path components */
257     a = [m componentsSeparatedByString:@"/"];
258   }
259   
260   count = [a count];
261   traversalPath = [NSMutableArray arrayWithCapacity:(count + 1)];
262   for (i = 0; i < count; i++) {
263     NSString *p;
264     
265     p = [a objectAtIndex:i];
266     
267     if ([p hasPrefix:@"_range"])
268       /* a ZideLook range query, further handled by WebDAV dispatcher */
269       continue;
270     
271     p = [p stringByUnescapingURL];
272     
273     if ([self enableZideLookHack])
274       p = [self hackZideLookName:p];
275     
276     if ([p length] > 0)
277       [traversalPath addObject:p];
278   }
279   
280   traversalPath = [self addSpecialFormValuesInRequest:_rq
281                         toTraversalPath:traversalPath];
282   
283   return traversalPath;
284 }
285
286 - (id)rootObjectForRequest:(WORequest *)_rq inContext:(WOContext *)_ctx {
287   id object;
288   
289   if (self->rootObject)
290     return self->rootObject;
291     
292   if ((object = [_ctx application]) == nil)
293     object = [WOApplication application];
294   
295 #if 0
296   /* 
297     if we resolve in this location, we won't be able to resolve
298     application names like "Control_Panel".
299   */
300   if ([object respondsToSelector:@selector(rootObjectInContext:)])
301     object = [object rootObjectInContext:_ctx];
302 #endif
303   
304   return object;
305 }
306
307 - (id)lookupObjectForRequest:(WORequest *)_rq inContext:(WOContext *)_ctx {
308   NSAutoreleasePool *pool;
309   NSArray     *traversalPath;
310   id currentObject;
311   
312   pool = [[NSAutoreleasePool alloc] init];
313   {
314     NSException *error = nil;
315     id   root;
316     BOOL doAcquire;
317     
318     /* build traversal path */
319     
320     traversalPath = [self traversalPathFromRequest:_rq];
321     if (traversalPath)
322       [_ctx setSoRequestTraversalPath:traversalPath];
323     
324     /* setup root object */
325     
326     root = [self rootObjectForRequest:_rq inContext:_ctx];
327     
328     doAcquire = self->doesNoRequestPathAcquisition
329       ? NO
330       : [self isObjectPublishingContext:_ctx];
331
332     [self debugWithFormat:@"traverse (%@): %@ %@", 
333             [_rq uri], 
334             [traversalPath componentsJoinedByString:@" => "],
335             doAcquire ? @"[acquires]" : @"(no acquisition)"];
336     
337     currentObject = [root traversePathArray:traversalPath
338                           inContext:_ctx
339                           error:&error
340                           acquire:doAcquire];
341     if (error)
342       currentObject = error;
343     
344     /* retain result */
345     currentObject = [currentObject retain];
346   }
347   [pool release];
348   return [currentObject autorelease];
349 }
350
351 /* object invocation */
352
353 - (id)dispatcherForObject:(id)_object inContext:(WOContext *)_ctx {
354   NSString *dpClass, *rqType;
355   id dispatcher = nil;
356   
357   /* ask object for dispatcher */
358   
359   if ([_object respondsToSelector:@selector(dispatcherForContext:)]) {
360     if ((dispatcher = [_object dispatcherForContext:_ctx]))
361       return dispatcher;
362   }
363   
364   if (debugRulesOn) {
365     [self debugWithFormat:@"select dispatcher using rules: %@", 
366             self->dispatcherRules];
367   }
368   
369   /* query */
370   dpClass = [self->dispatcherRules valueForKey:@"dispatcher"];
371   rqType  = [self->dispatcherRules valueForKey:@"requestType"];
372   if (debugRulesOn) {
373     [self debugWithFormat:@"  selected dispatcher: %@", dpClass];
374     [self debugWithFormat:@"  selected rq-type:    %@", rqType];
375   }
376   
377   /* create dispatcher */
378   
379   if (rqType != nil) [_ctx setSoRequestType:rqType];
380   if ((dispatcher = NSClassFromString(dpClass)) == nil) {
381     [self logWithFormat:@"ERROR: did not find dispatcher class '%@'", dpClass];
382     return nil;
383   }
384   
385   if ((dispatcher = [[dispatcher alloc] initWithObject:_object]))
386     [_ctx setObjectDispatcher:dispatcher];
387   
388   return [dispatcher autorelease];
389 }
390
391 /* object rendering */
392
393 - (WOResponse *)renderObject:(id)_object inContext:(WOContext *)_ctx {
394   SoDefaultRenderer *renderer;
395   NSException  *error;
396   NSEnumerator *e;
397   id container;
398   
399   [self debugWithFormat:@"    render in ctx: %@", _ctx];
400
401   if ([_object isKindOfClass:[WOResponse class]])
402     /* already rendered ... */
403     return _object;
404   
405   /* check whether a container on the traversal stack provides a renderer */
406   
407   renderer = nil;
408   e = [[_ctx objectTraversalStack] reverseObjectEnumerator];
409   while ((container = [e nextObject])) {
410     if (![container respondsToSelector:
411                       @selector(rendererForObject:inContext:)]) {
412       /* does not provide a renderer factory ... */
413       continue;
414     }
415     
416     if ((renderer = [container rendererForObject:_object inContext:_ctx])) {
417       /* the container provided an own renderer for the object */
418       [self debugWithFormat:@"    use container renderer: %@", renderer];
419       break;
420     }
421   }
422   
423   /* if we didn't find a renderer, determine using rules */
424   
425   if (renderer == nil) {
426     NSString *rendererClass;
427     
428     rendererClass = [self->dispatcherRules valueForKey:@"renderer"];
429     if (rendererClass) {
430       Class clazz;
431       
432       if ((clazz = NSClassFromString(rendererClass)) == Nil) {
433         [self logWithFormat:@"did not find class of selected renderer %@", 
434                 rendererClass];
435       }
436       else if ((renderer = [clazz sharedRenderer]) == nil) {
437         [self logWithFormat:@"could not get renderer of class %@", 
438                 rendererClass];
439       }
440       else if (![renderer canRenderObject:_object inContext:_ctx]) {
441         [self debugWithFormat:@"renderer %@ rejected rendering of object %@", 
442                 renderer, _object];
443         renderer = [SoDefaultRenderer sharedRenderer];
444       }
445     }
446     
447     if (renderer)
448       [self debugWithFormat:@"    use rule-selected renderer: %@", renderer];
449   }
450   
451   if (renderer == nil)
452     [self debugWithFormat:@"    found no renderer for object: %@", _object];
453   
454   if ((error = [renderer renderObject:_object inContext:_ctx])) {
455     if (renderer != [SoDefaultRenderer sharedRenderer]) {
456       NSException *e2;
457       
458       e2 = [(SoDefaultRenderer *)[SoDefaultRenderer sharedRenderer] 
459                                  renderObject:error inContext:_ctx];
460       if (e2) {
461         [self logWithFormat:@"default renderer could not render error %@: %@",
462                 error, e2];
463         return nil;
464       }
465     }
466     else {
467       [self logWithFormat:@"default renderer returned error: %@", error];
468       return nil;
469     }
470   }
471   return [_ctx response];
472 }
473
474 - (BOOL)doesRejectFavicon {
475   return NO;
476 }
477
478 - (WOResponse *)handleRequest:(WORequest *)_rq
479   inContext:(WOContext *)_ctx
480   session:(WOSession *)_sn
481   application:(WOApplication *)app
482 {
483   /* split up this big method */
484   WOResponse *r;
485   id object;
486   id authenticator;
487   BOOL doDispatch;
488   
489   if (debugOn) {
490     [self debugWithFormat:@"request 0x%08X: %@ %@ (ctx=0x%08X)", _rq, 
491             [_rq method], [_rq uri], _ctx];
492     if (_sn) [self debugWithFormat:@"  session 0x%08X: %@", _sn, _sn];
493   }
494   
495   /* setup rule context */
496   
497   [self->dispatcherRules reset];
498   [self->dispatcherRules takeValue:_rq           forKey:@"request"];
499   [self->dispatcherRules takeValue:[_rq headers] forKey:@"headers"];
500   [self->dispatcherRules takeValue:[_rq method]  forKey:@"method"];
501   [self->dispatcherRules takeValue:_ctx          forKey:@"context"];
502   
503   /* preprocess authentication credentials with global auth handler */
504   
505   if ((authenticator = [app authenticatorInContext:_ctx])) {
506     [_ctx setObject:authenticator forKey:@"SoAuthenticator"];
507     
508     /* give authenticator the chance to reject invalid credentials */
509     
510     if ((r = [authenticator preprocessCredentialsInContext:_ctx])) {
511       [self->dispatcherRules reset];
512       return r;
513     }
514     
515     [self debugWithFormat:@"authenticator allowed request."];
516   }
517   else {
518     [self debugWithFormat:@"WARNING: no authenticator available."];
519   }
520   
521   /* lookup object */
522   
523   doDispatch = YES;
524   object = [self lookupObjectForRequest:_rq inContext:_ctx];
525   
526   if (object) {
527     [self->dispatcherRules 
528          takeValue:[_ctx clientObject] forKey:@"clientObject"];
529     [self->dispatcherRules takeValue:object forKey:@"object"];
530   }
531   else {
532     r = [_ctx response];
533     [r setStatus:404];
534     [r setHeader:@"text/html" forKey:@"content-type"];
535     [r appendContentString:@"object not found: "];
536     [r appendContentHTMLString:
537          [[_ctx soRequestTraversalPath] componentsJoinedByString:@" => "]];
538     doDispatch = NO;
539     object = r;
540   }
541   
542   /* dispatch object */
543   
544   if ([object isKindOfClass:[NSException class]]) {
545     /* exceptions are not called ... */
546     [self debugWithFormat:@"  not calling exception: %@", object];
547     doDispatch = NO;
548   }
549   
550   if (doDispatch) {
551     id dispatcher;
552     
553     dispatcher = [self dispatcherForObject:object inContext:_ctx];
554     [self debugWithFormat:@"  dispatcher: %@", dispatcher];
555     
556     [self debugWithFormat:@"  dispatch object: %@", object];
557     object = [dispatcher dispatchInContext:_ctx];
558
559     if (object) [self->dispatcherRules takeValue:object forKey:@"result"];
560   }
561   
562   /* render result */
563   
564   if (object == nil) {
565     [self debugWithFormat:@"  got an empty result !"];
566     r = [_ctx response];
567     [r setStatus:500];
568     [r appendContentString:@"the called object returned no result"];
569   }
570   else if ([object isKindOfClass:[WOResponse class]]) {
571     r = object;
572     [self debugWithFormat:
573             @"  got response: 0x%08X (status=%i,len=%@,type=%@)", 
574             r, [r status], 
575             [r headerForKey:@"content-length"],
576             [r headerForKey:@"content-type"]];
577   }
578   else {
579     if (debugOn) {
580       if ([object isKindOfClass:[NSData class]]) {
581         [self debugWithFormat:@"  render data 0x%08X[len=%i]",
582                 object, [object length]];
583       }
584       else
585         [self debugWithFormat:@"  render object: %@", object];
586     }
587     
588     [self->dispatcherRules takeValue:object forKey:@"result"];
589     r = [self renderObject:object inContext:_ctx];
590     
591     if (debugOn) {
592       [self debugWithFormat:
593             @"  made response: 0x%08X (status=%i,len=%@,type=%@)", 
594             r, [r status], 
595             [r headerForKey:@"content-length"],
596             [r headerForKey:@"content-type"]];
597     }
598   }
599   
600   /* add header with primary key of new objects (for ZideLook) */
601   if (r != nil) {
602     id key;
603     
604     if ((key = [_ctx objectForKey:@"SxNewObjectID"])) {
605       key = [NSString stringWithFormat:@"%@", key];
606       [r setHeader:key forKey:@"x-skyrix-newname"];
607       [self logWithFormat:@"added new key header to response: '%@'", key];
608     }
609   }
610
611   /* rapid turnaround */
612   if (rapidTurnAroundPath != nil) {
613     WOComponent *page;
614     NSString *_path = nil;
615
616     if ((page = [_ctx page])) {
617       WOElement *template;
618           
619       template = [page _woComponentTemplate];
620       if ([template isKindOfClass:WOTemplateClass])
621         _path = [[(WOTemplate *)template url] path];
622     }
623     else {
624       // TODO: ZNeK: explain!
625       //       I guess we need some generic method to retrieve a template path?
626       if ([object isKindOfClass:NSClassFromString(@"OFSBaseObject")])
627         _path = [object storagePath];
628     }
629     if (_path != nil)
630       [r setHeader:_path forKey:@"x-sope-template-path"];
631   }
632   
633   /* sleep traversal stack */
634   {
635     NSEnumerator *e;
636     id obj;
637     
638     e = [[_ctx objectTraversalStack] reverseObjectEnumerator];
639     while ((obj = [e nextObject])) {
640       if (![obj isNotNull])
641         continue;
642       
643       if ([obj respondsToSelector:@selector(_sleepWithContext:)])
644         [obj _sleepWithContext:_ctx];
645       else if ([obj respondsToSelector:@selector(sleep)])
646         [obj sleep];
647     }
648   }
649   
650   [self->dispatcherRules reset];
651   
652   return r;
653 }
654
655 @end /* SoObjectRequestHandler */
656
657 @implementation WOCoreApplication(RendererSelection)
658
659 - (id)rendererForObject:(id)_object inContext:(WOContext *)_ctx {
660   return nil;
661 }
662
663 @end /* WOCoreApplication(RendererSelection) */
664
665 @implementation SoObjectRequestHandler(Logging)
666
667 - (NSString *)loggingPrefix {
668   return @"[object-handler]";
669 }
670 - (BOOL)isDebuggingEnabled {
671   return debugOn ? YES : NO;
672 }
673
674 @end /* SoObjectRequestHandler(Logging) */