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