1/*
2  Copyright (C) 2000-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 "WORequestHandler+private.h"
23#include "WOApplication+private.h"
24#include "WOContext+private.h"
25#include <NGObjWeb/WOApplication.h>
26#include <NGObjWeb/WOStatisticsStore.h>
27#include <NGObjWeb/WOContext.h>
28#include <NGObjWeb/WOCookie.h>
29#include <NGObjWeb/WOComponent.h>
30#include <NGObjWeb/WORequest.h>
31#include <NGObjWeb/WOResponse.h>
32#include <NGObjWeb/WOSession.h>
33#include "common.h"
34
35//#define USE_POOLS 1
36
37#if APPLE_FOUNDATION_LIBRARY || NeXT_Foundation_LIBRARY
38@interface NSObject(Miss)
39- (id)subclassResponsibility:(SEL)cmd;
40@end
41#endif
42
43@interface WOApplication(Privates)
44- (void)_setCurrentContext:(WOContext *)_ctx;
45@end
46
47@implementation WORequestHandler
48
49static BOOL     doNotSetCookiePath = NO;
50static Class    NSDateClass        = Nil;
51static NGLogger *logger            = nil;
52static NGLogger *perfLogger        = nil;
53
54+ (void)initialize {
55  NSUserDefaults  *ud;
56  NGLoggerManager *lm;
57  static BOOL didInit = NO;
58
59  if (didInit)
60    return;
61  didInit = YES;
62  NSDateClass = [NSDate class];
63
64  lm         = [NGLoggerManager defaultLoggerManager];
65  logger     = [lm loggerForDefaultKey:@"WODebuggingEnabled"];
66  perfLogger = [lm loggerForDefaultKey:@"WOProfileRequestHandler"];
67
68  ud                 = [NSUserDefaults standardUserDefaults];
69  doNotSetCookiePath = [ud boolForKey:@"WOUseGlobalCookiePath"];
70}
71
72- (id)init {
73  if ((self = [super init])) {
74    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
75
76    if ([ud boolForKey:@"WORunMultithreaded"])
77      self->lock = [[NSRecursiveLock alloc] init];
78  }
79  return self;
80}
81
82- (void)dealloc {
83  [self->lock release];
84  [super dealloc];
85}
86
87/* request handling */
88
89- (BOOL)restoreSessionUsingIDs {
90  /* restore a session if an ID was given */
91  return YES;
92}
93- (BOOL)autocreateSessionForRequest:(WORequest *)_request {
94  /* autocreate a session if none was restored */
95  return NO;
96}
97- (BOOL)requiresSessionForRequest:(WORequest *)_request {
98  /* _ensure_ that a session is available */
99  return NO;
100}
101
102- (NSString *)sessionIDFromRequest:(WORequest *)_request
103  application:(WOApplication *)_app
104{
105  NSString *sid;
106
107  if ((sid = [_app sessionIDFromRequest:_request]) == nil)
108    return nil;
109
110#if DEBUG
111  NSAssert1([sid isKindOfClass:[NSString class]],
112            @"invalid session ID: %@", sid);
113#endif
114  return sid;
115}
116
117- (WOResponse *)handleRequest:(WORequest *)_request
118  inContext:(WOContext *)context
119  session:(WOSession *)session
120  application:(WOApplication *)app
121{
122  return [self subclassResponsibility:_cmd];
123}
124
125- (BOOL)doesRejectFavicon {
126  return YES;
127}
128
129- (WOResponse *)handleRequest:(WORequest *)_request {
130  NSTimeInterval    startHandling = 0.0;
131#if USE_POOLS
132  NSAutoreleasePool *pool = nil;
133#endif
134  WOApplication *app;
135  WOResponse    *response   = nil;
136  WOContext     *context    = nil;
137  NSThread      *thread;
138  NSString      *sessionId  = nil;
139  WOSession     *session    = nil;
140  NSString *uri;
141
142  /* first check URI for favicon requests ... */
143  uri = [_request uri];
144  if ([self doesRejectFavicon] && uri != nil) {
145    if ([@"/favicon.ico" isEqualToString:uri]) {
146      response = [WOResponse responseWithRequest:_request];
147      [response setStatus:404 /* not found */];
148      [self debugWithFormat:@"rejected favicon request: %@", uri];
149      return response;
150    }
151  }
152
153  if (perfLogger)
154    startHandling = [[NSDateClass date] timeIntervalSince1970];
155
156  thread = [NSThread currentThread];
157  NSAssert(thread, @"missing current thread ...");
158
159  if (_request == nil) return nil;
160
161  *(&app) = nil;
162  app = [WOApplication application];
163
164#if USE_POOLS
165  *(&pool) = [[NSAutoreleasePool alloc] init];
166#endif
167  {
168    /* setup context */
169    context = [WOContext contextWithRequest:_request];
170    NSAssert(context, @"no context assigned ..");
171    [app _setCurrentContext:context];
172
173    /* check session id */
174    *(&session)   = nil;
175    *(&sessionId) = [self sessionIDFromRequest:_request application:app];
176
177    if ([sessionId length] == 0)
178      sessionId = nil;
179    else if ([sessionId isEqualToString:@"nil"])
180      sessionId = nil;
181
182    NS_DURING {
183      [app awake];
184
185      /* retrieve session */
186      if ([self restoreSessionUsingIDs]) {
187        SYNCHRONIZED(app) {
188          if (sessionId) {
189            session = [app restoreSessionWithID:sessionId
190                           inContext:context];
191            if (session == nil) {
192              response  = [app handleSessionRestorationErrorInContext:context];
193              sessionId = nil;
194            }
195          }
196        }
197        END_SYNCHRONIZED;
198
199        [[session retain] autorelease];
200
201        if (response != nil)
202          /* some kind of error response from above ... */
203          goto responseDone;
204
205        if (session == nil) {
206          /* session autocreation .. */
207          if ([self autocreateSessionForRequest:_request]) {
208            if (![app isRefusingNewSessions]) {
209              session = [app _initializeSessionInContext:context];
210
211              [self debugWithFormat:@"autocreated session: %@", session];
212
213              if (session == nil)
214                response =[app handleSessionRestorationErrorInContext:context];
215            }
216            else { /* app refuses new sessions */
217              // TODO: this already failed once, will it return null again?
218              [self logWithFormat:@"app is refusing new sessions ..."];
219              response = [app handleSessionRestorationErrorInContext:context];
220            }
221          }
222          if (response)
223            /* some kind of error response from above ... */
224            goto responseDone;
225
226          /* check whether session is required ... */
227          if ([self requiresSessionForRequest:_request] && (session == nil)) {
228            response = [app handleSessionCreationErrorInContext:context];
229            goto responseDone;
230          }
231        }
232      }
233
234      [session lock];
235
236      NS_DURING {
237        response = [self handleRequest:_request
238                         inContext:context
239                         session:session
240                         application:app];
241
242        session = [context hasSession]
243          ? [context session]
244          : nil;
245
246        if (session != nil) {
247          if ([session storesIDsInCookies]) {
248            if (logger != nil) /* Note: required! do not remove */
249	      [self debugWithFormat:@"add cookie to session: %@", session];
250            [self addCookiesForSession:session
251                  toResponse:response
252                  inContext:context];
253          }
254
255          [self saveSession:session
256                inContext:context
257                withResponse:response
258                application:app];
259        }
260        else
261          [self debugWithFormat:@"no session to store."];
262      }
263      NS_HANDLER {
264        response = [app handleException:localException inContext:context];
265      }
266      NS_ENDHANDLER;
267
268      [session unlock];
269
270    responseDone:
271      [session _sleepWithContext:context];
272      response = [response retain];
273
274      [app sleep];
275    }
276    NS_HANDLER {
277      response = [app handleException:localException inContext:context];
278      response = [response retain];
279    }
280    NS_ENDHANDLER;
281
282    [app _setCurrentContext:nil];
283  }
284#if USE_POOLS
285  [pool release]; pool = nil;
286#endif
287
288  [app lock];
289  if ([app isRefusingNewSessions] &&
290      ([app activeSessionsCount] < [app minimumActiveSessionsCount])) {
291    [self logWithFormat:
292            @"application terminates because it refuses new sessions and "
293            @"the active session count (%i) is below the minimum (%i).",
294            [app activeSessionsCount], [app minimumActiveSessionsCount]];
295    [app terminate];
296  }
297  [app unlock];
298
299  if (perfLogger) {
300    NSTimeInterval rt;
301    rt = [[NSDateClass date] timeIntervalSince1970] - startHandling;
302    [perfLogger logWithFormat:@"handleRequest took %4.3fs.",
303                  rt < 0.0 ? -1.0 : rt];
304  }
305
306  return [response autorelease];
307}
308
309/* locking */
310
311- (void)lock {
312  [self->lock lock];
313}
314- (void)unlock {
315  [self->lock unlock];
316}
317
318/* KVC */
319
320- (id)valueForUndefinedKey:(NSString *)_key {
321  [self debugWithFormat:@"queried undefined KVC key (returning nil): '%@'",
322	  _key];
323  return nil;
324}
325
326/* logging */
327
328- (id)debugLogger {
329  return logger;
330}
331
332/* Cookies */
333
334- (void)addCookiesForSession:(WOSession *)_sn
335  toResponse:(WOResponse *)_response
336  inContext:(WOContext *)_ctx
337{
338  WOApplication *app;
339  WOCookie *cookie     = nil;
340  NSString *uri;
341  NSString *value;
342
343  if (![_sn storesIDsInCookies])
344    return;
345
346  app = [WOApplication application];
347
348  // TODO: there is a DUP of this section in OpenGroupware.m to set an
349  //       expiration cookie
350  if (!doNotSetCookiePath) {
351    NSString *tmp;
352
353    if ((uri = [[_ctx request] applicationName]) == nil)
354      uri = [app name];
355    uri = [@"/" stringByAppendingString:uri];
356    if ((tmp = [[_ctx request] adaptorPrefix]))
357      uri = [tmp stringByAppendingString:uri];
358  }
359  else
360    uri = @"/";
361
362#if 0 // TODO: explain!
363  uri = [_ctx urlSessionPrefix];
364  uri = [_ctx urlWithRequestHandlerKey:
365		[WOApplication componentRequestHandlerKey]
366	      path:@"/"
367	      queryString:nil];
368#endif
369
370  value = [_sn isTerminating]
371    ? (NSString *)@"nil"
372    : [_sn sessionID];
373
374  cookie = [WOCookie cookieWithName:[app name]
375		     value:value
376		     path:uri
377		     domain:[_sn domainForIDCookies]
378		     expires:[_sn expirationDateForIDCookies]
379		     isSecure:NO];
380  if (cookie != nil)
381    [_response addCookie:cookie];
382}
383
384@end /* WORequestHandler */
385
386
387@implementation WORequest(DblClickBrowser)
388
389- (BOOL)isDoubleClickBrowser {
390  return NO;
391}
392
393@end /* WORequest(DblClickBrowser) */
394
395@implementation WORequestHandler(Support)
396
397- (WOResponse *)doubleClickResponseForContext:(WOContext *)_ctx {
398  // DEPRECATED
399  return nil;
400}
401
402- (void)saveSession:(WOSession *)_session
403  inContext:(WOContext *)_ctx
404  withResponse:(WOResponse *)_response
405  application:(WOApplication *)_app
406{
407  static BOOL perflog = NO;
408  NSTimeInterval startSaveSn = 0.0;
409
410  if (_session == nil) return;
411
412  if (perflog)
413    startSaveSn = [[NSDate date] timeIntervalSince1970];
414
415  [_app saveSessionForContext:_ctx];
416
417  if (perflog) {
418    NSTimeInterval rt;
419    rt = [[NSDate date] timeIntervalSince1970] - startSaveSn;
420    NSLog(@"[rq]: saving of session took %4.3fs.",
421          rt < 0.0 ? -1.0 : rt);
422  }
423}
424
425- (void)_fixupResponse:(WOResponse *)_response {
426  NSString *ctype;
427  NSString *cntype = nil;
428
429  if ((ctype = [_response headerForKey:@"content-type"]) == nil) {
430    NSData *body;
431
432    ctype = @"text/html";
433
434    body = [_response content];
435    if ([body length] > 6) {
436      const unsigned char *bytes;
437
438      if ((bytes = [body bytes])) {
439        if ((bytes[0] == '<') && (bytes[1] == '?')) {
440          if ((bytes[2] == 'x') && (bytes[3] == 'm') && (bytes[4] == 'l'))
441            ctype = @"text/xml";
442        }
443      }
444    }
445
446    [_response setHeader:ctype forKey:@"content-type"];
447  }
448
449  if ([ctype isEqualToString:@"text/html"]) {
450    switch ([_response contentEncoding]) {
451      case NSISOLatin1StringEncoding:
452        cntype = [ctype stringByAppendingString:@"; charset=iso-8859-1"];
453        break;
454      case NSUTF8StringEncoding:
455        cntype = [ctype stringByAppendingString:@"; charset=utf-8"];
456        break;
457
458      default:
459        break;
460    }
461  }
462  if (cntype)
463    [_response setHeader:cntype forKey:@"content-type"];
464}
465
466- (WOResponse *)generateResponseForComponent:(WOComponent *)_component
467  inContext:(WOContext *)_ctx
468  application:(WOApplication *)_app
469{
470  WOResponse *response;
471
472  if (_component == nil) return nil;
473
474  /* make the component the "response page" */
475  [_ctx setPage:_component];
476
477  if ([_ctx hasSession]) {
478    response = [_ctx response];
479    [_app appendToResponse:response inContext:_ctx];
480  }
481  else {
482    //[self logWithFormat:@"generating component using -generateResponse"];
483    response = [_component generateResponse];
484
485    /* generate statistics */
486    [[_app statisticsStore]
487           recordStatisticsForResponse:response
488           inContext:_ctx];
489  }
490
491  [self _fixupResponse:response];
492  return response;
493}
494
495@end /* WORequestHandler(Support) */
496