]> err.no Git - sope/blob - sope-appserver/NGObjWeb/SoObjects/SoObjectRequestHandler.m
152e727270481ca20373d9f6d2ff146ee96fd519
[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     TODO: explain better!
301   */
302   if ([object respondsToSelector:@selector(rootObjectInContext:)])
303     object = [object rootObjectInContext:_ctx];
304 #endif
305   
306   return object;
307 }
308
309 - (id)lookupObjectForRequest:(WORequest *)_rq inContext:(WOContext *)_ctx {
310   NSAutoreleasePool *pool;
311   NSArray     *traversalPath;
312   id currentObject;
313   
314   pool = [[NSAutoreleasePool alloc] init];
315   {
316     NSException *error = nil;
317     id   root;
318     BOOL doAcquire;
319     
320     /* build traversal path */
321     
322     traversalPath = [self traversalPathFromRequest:_rq];
323     if (traversalPath)
324       [_ctx setSoRequestTraversalPath:traversalPath];
325     
326     /* setup root object */
327     
328     root = [self rootObjectForRequest:_rq inContext:_ctx];
329     
330     doAcquire = self->doesNoRequestPathAcquisition
331       ? NO
332       : [self isObjectPublishingContext:_ctx];
333
334     [self debugWithFormat:@"traverse (%@): %@ %@", 
335             [_rq uri], 
336             [traversalPath componentsJoinedByString:@" => "],
337             doAcquire ? @"[acquires]" : @"(no acquisition)"];
338     
339     currentObject = [root traversePathArray:traversalPath
340                           inContext:_ctx
341                           error:&error
342                           acquire:doAcquire];
343     if (error)
344       currentObject = error;
345     
346     /* retain result */
347     currentObject = [currentObject retain];
348   }
349   [pool release];
350   return [currentObject autorelease];
351 }
352
353 /* object invocation */
354
355 - (id)dispatcherForObject:(id)_object inContext:(WOContext *)_ctx {
356   NSString *dpClass, *rqType;
357   id dispatcher = nil;
358   
359   /* ask object for dispatcher */
360   
361   if ([_object respondsToSelector:@selector(dispatcherForContext:)]) {
362     if ((dispatcher = [_object dispatcherForContext:_ctx]))
363       return dispatcher;
364   }
365   
366   if (debugRulesOn) {
367     [self debugWithFormat:@"select dispatcher using rules: %@", 
368             self->dispatcherRules];
369   }
370   
371   /* query */
372   dpClass = [self->dispatcherRules valueForKey:@"dispatcher"];
373   rqType  = [self->dispatcherRules valueForKey:@"requestType"];
374   if (debugRulesOn) {
375     [self debugWithFormat:@"selected dispatcher: %@", dpClass];
376     [self debugWithFormat:@"selected rq-type:    %@", rqType];
377   }
378   
379   /* create dispatcher */
380   
381   if (rqType != nil) [_ctx setSoRequestType:rqType];
382   if ((dispatcher = NSClassFromString(dpClass)) == nil) {
383     [self errorWithFormat:@"did not find dispatcher class '%@'", dpClass];
384     return nil;
385   }
386   
387   if ((dispatcher = [[dispatcher alloc] initWithObject:_object]))
388     [_ctx setObjectDispatcher:dispatcher];
389   
390   return [dispatcher autorelease];
391 }
392
393 /* object rendering */
394
395 - (WOResponse *)renderObject:(id)_object inContext:(WOContext *)_ctx {
396   SoDefaultRenderer *renderer;
397   NSException  *error;
398   NSEnumerator *e;
399   id container;
400   
401   [self debugWithFormat:@"render in ctx: %@", _ctx];
402
403   if ([_object isKindOfClass:[WOResponse class]])
404     /* already rendered ... */
405     return _object;
406   
407   /* check whether a container on the traversal stack provides a renderer */
408   
409   renderer = nil;
410   e = [[_ctx objectTraversalStack] reverseObjectEnumerator];
411   while ((container = [e nextObject])) {
412     if (![container respondsToSelector:
413                       @selector(rendererForObject:inContext:)]) {
414       /* does not provide a renderer factory ... */
415       continue;
416     }
417     
418     if ((renderer = [container rendererForObject:_object inContext:_ctx])) {
419       /* the container provided an own renderer for the object */
420       [self debugWithFormat:@"use container renderer: %@", renderer];
421       break;
422     }
423   }
424   
425   /* if we didn't find a renderer, determine using rules */
426   
427   if (renderer == nil) {
428     NSString *rendererClass;
429     
430     rendererClass = [self->dispatcherRules valueForKey:@"renderer"];
431     if (rendererClass) {
432       Class clazz;
433       
434       if ((clazz = NSClassFromString(rendererClass)) == Nil) {
435         [self errorWithFormat:@"did not find class of selected renderer %@", 
436                 rendererClass];
437       }
438       else if ((renderer = [clazz sharedRenderer]) == nil) {
439         [self errorWithFormat:@"could not get renderer of class %@", 
440                 rendererClass];
441       }
442       else if (![renderer canRenderObject:_object inContext:_ctx]) {
443         [self debugWithFormat:@"renderer %@ rejected rendering of object %@", 
444                 renderer, _object];
445         renderer = [SoDefaultRenderer sharedRenderer];
446       }
447     }
448     
449     if (renderer)
450       [self debugWithFormat:@"use rule-selected renderer: %@", renderer];
451   }
452   
453   if (renderer == nil)
454     [self debugWithFormat:@"found no renderer for object: %@", _object];
455   
456   if ((error = [renderer renderObject:_object inContext:_ctx])) {
457     if (renderer != [SoDefaultRenderer sharedRenderer]) {
458       NSException *e2;
459       
460       e2 = [(SoDefaultRenderer *)[SoDefaultRenderer sharedRenderer] 
461                                  renderObject:error inContext:_ctx];
462       if (e2) {
463         [self errorWithFormat:
464                 @"default renderer could not render error %@: %@", error, e2];
465         return nil;
466       }
467     }
468     else {
469       [self errorWithFormat:@"default renderer returned error: %@", error];
470       return nil;
471     }
472   }
473   return [_ctx response];
474 }
475
476 - (BOOL)doesRejectFavicon {
477   return NO;
478 }
479
480 - (WOResponse *)handleRequest:(WORequest *)_rq
481   inContext:(WOContext *)_ctx
482   session:(WOSession *)_sn
483   application:(WOApplication *)app
484 {
485   /* split up this big method */
486   WOResponse *r;
487   id object;
488   id authenticator;
489   BOOL doDispatch;
490   
491   if (debugOn) {
492     [self debugWithFormat:@"request 0x%08X: %@ %@ (ctx=0x%08X)", _rq, 
493             [_rq method], [_rq uri], _ctx];
494     if (_sn) [self debugWithFormat:@"session 0x%08X: %@", _sn, _sn];
495   }
496   
497   /* setup rule context */
498   
499   [self->dispatcherRules reset];
500   [self->dispatcherRules takeValue:_rq           forKey:@"request"];
501   [self->dispatcherRules takeValue:[_rq headers] forKey:@"headers"];
502   [self->dispatcherRules takeValue:[_rq method]  forKey:@"method"];
503   [self->dispatcherRules takeValue:_ctx          forKey:@"context"];
504   
505   /* preprocess authentication credentials with global auth handler */
506   
507   if ((authenticator = [app authenticatorInContext:_ctx])) {
508     [_ctx setObject:authenticator forKey:@"SoAuthenticator"];
509     
510     /* give authenticator the chance to reject invalid credentials */
511     
512     if ((r = [authenticator preprocessCredentialsInContext:_ctx])) {
513       [self->dispatcherRules reset];
514       return r;
515     }
516     
517     [self debugWithFormat:@"authenticator allowed request."];
518   }
519   else {
520     [self warnWithFormat:@"no authenticator available."];
521   }
522   
523   /* lookup object */
524   
525   doDispatch = YES;
526   object = [self lookupObjectForRequest:_rq inContext:_ctx];
527   
528   if (object) {
529     [self->dispatcherRules 
530          takeValue:[_ctx clientObject] forKey:@"clientObject"];
531     [self->dispatcherRules takeValue:object forKey:@"object"];
532   }
533   else {
534     r = [_ctx response];
535     [r setStatus:404];
536     [r setHeader:@"text/html" forKey:@"content-type"];
537     [r appendContentString:@"object not found: "];
538     [r appendContentHTMLString:
539          [[_ctx soRequestTraversalPath] componentsJoinedByString:@" => "]];
540     doDispatch = NO;
541     object = r;
542   }
543   
544   /* dispatch object */
545   
546   if ([object isKindOfClass:[NSException class]]) {
547     /* exceptions are not called ... */
548     [self debugWithFormat:@"not calling exception: %@", object];
549     doDispatch = NO;
550   }
551   
552   if (doDispatch) {
553     id dispatcher;
554     
555     dispatcher = [self dispatcherForObject:object inContext:_ctx];
556     [self debugWithFormat:@"dispatcher: %@", dispatcher];
557     
558     [self debugWithFormat:@"dispatch object: %@", object];
559     object = [dispatcher dispatchInContext:_ctx];
560
561     if (object) [self->dispatcherRules takeValue:object forKey:@"result"];
562   }
563   
564   /* render result */
565   
566   if (object == nil) {
567     [self debugWithFormat:@"got an empty result !"];
568     r = [_ctx response];
569     [r setStatus:500];
570     [r appendContentString:@"the called object returned no result"];
571   }
572   else if ([object isKindOfClass:[WOResponse class]]) {
573     r = object;
574     [self debugWithFormat:
575             @"got response: 0x%08X (status=%i,len=%@,type=%@)", 
576             r, [r status], 
577             [r headerForKey:@"content-length"],
578             [r headerForKey:@"content-type"]];
579   }
580   else {
581     if (debugOn) {
582       if ([object isKindOfClass:[NSData class]]) {
583         [self debugWithFormat:@"render data 0x%08X[len=%i]",
584                 object, [object length]];
585       }
586       else
587         [self debugWithFormat:@"render object: %@", object];
588     }
589     
590     [self->dispatcherRules takeValue:object forKey:@"result"];
591     r = [self renderObject:object inContext:_ctx];
592     
593     if (debugOn) {
594       [self debugWithFormat:
595                     @"made response: 0x%08X (status=%i,len=%@,type=%@)", 
596               r, [r status], 
597               [r headerForKey:@"content-length"],
598               [r headerForKey:@"content-type"]];
599     }
600   }
601   
602   /* add header with primary key of new objects (for ZideLook) */
603   if (r != nil) {
604     id key;
605     
606     if ((key = [_ctx objectForKey:@"SxNewObjectID"])) {
607       key = [NSString stringWithFormat:@"%@", key];
608       [r setHeader:key forKey:@"x-skyrix-newname"];
609       [self logWithFormat:@"added new key header to response: '%@'", key];
610     }
611   }
612
613   /* rapid turnaround */
614   if (rapidTurnAroundPath != nil) {
615     WOComponent *page;
616     NSString *_path = nil;
617
618     if ((page = [_ctx page])) {
619       WOElement *template;
620           
621       template = [page _woComponentTemplate];
622       if ([template isKindOfClass:WOTemplateClass])
623         _path = [[(WOTemplate *)template url] path];
624     }
625     else {
626       // TODO: ZNeK: explain!
627       //       I guess we need some generic method to retrieve a template path?
628       if ([object isKindOfClass:NSClassFromString(@"OFSBaseObject")])
629         _path = [object storagePath];
630     }
631     if (_path != nil)
632       [r setHeader:_path forKey:@"x-sope-template-path"];
633   }
634   
635   /* sleep traversal stack */
636   {
637     NSEnumerator *e;
638     id obj;
639     
640     e = [[_ctx objectTraversalStack] reverseObjectEnumerator];
641     while ((obj = [e nextObject])) {
642       if (![obj isNotNull])
643         continue;
644       
645       if ([obj respondsToSelector:@selector(_sleepWithContext:)])
646         [obj _sleepWithContext:_ctx];
647       else if ([obj respondsToSelector:@selector(sleep)])
648         [obj sleep];
649     }
650   }
651   
652   [self->dispatcherRules reset];
653   
654   return r;
655 }
656
657 @end /* SoObjectRequestHandler */
658
659 @implementation WOCoreApplication(RendererSelection)
660
661 - (id)rendererForObject:(id)_object inContext:(WOContext *)_ctx {
662   return nil;
663 }
664
665 @end /* WOCoreApplication(RendererSelection) */
666
667 @implementation SoObjectRequestHandler(Logging)
668
669 - (NSString *)loggingPrefix {
670   return @"[object-handler]";
671 }
672 - (BOOL)isDebuggingEnabled {
673   return debugOn ? YES : NO;
674 }
675
676 @end /* SoObjectRequestHandler(Logging) */