/** GSWResponse.m - GSWeb: Class GSWResponse Copyright (C) 1999-2004 Free Software Foundation, Inc. Written by: Manuel Guesdon Date: Jan 1999 $Revision$ $Date$ $Id$ This file is part of the GNUstep Web Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. **/ #include "config.h" RCS_ID("$Id$") #include "GSWeb.h" #include "NSData+Compress.h" //==================================================================== @implementation GSWResponse static NSString* disabledCacheDateString=nil; static NSArray* cacheControlHeaderValues=nil; //-------------------------------------------------------------------- +(void)initialize { if (self==[GSWResponse class]) { // So cache date stamp will be set to earlier date ASSIGN(disabledCacheDateString,[[NSCalendarDate date] htmlDescription]); // Other cache control headers ASSIGN(cacheControlHeaderValues,([NSArray arrayWithObjects:@"private", @"no-cache", @"no-store", @"must-revalidate", @"max-age=0", nil])); }; }; //-------------------------------------------------------------------- // init -(id)init { //OK LOGObjectFnStart(); if ((self=[super init])) { _canDisableClientCaching=YES; _status=200; }; LOGObjectFnStop(); return self; }; //-------------------------------------------------------------------- -(void)dealloc { // GSWLogAssertGood(self); // NSDebugFLog(@"dealloc Response %p",self); // NSDebugFLog0(@"Release Response contentFaults"); DESTROY(_contentFaults); // NSDebugFLog0(@"Release Response contentData"); DESTROY(_contentStreamFileHandle); DESTROY(_acceptedEncodings); // NSDebugFLog0(@"Release Response"); [super dealloc]; }; //-------------------------------------------------------------------- -(id)copyWithZone:(NSZone*)zone { GSWResponse* clone = (GSWResponse*)[super copyWithZone:zone]; if (clone) { clone->_status=_status; ASSIGNCOPY(clone->_contentFaults,_contentFaults); ASSIGNCOPY(clone->_acceptedEncodings,_acceptedEncodings); clone->_isClientCachingDisabled=_isClientCachingDisabled; clone->_canDisableClientCaching=_canDisableClientCaching; clone->_contentFaultsHaveBeenResolved=_contentFaultsHaveBeenResolved; }; return clone; }; //-------------------------------------------------------------------- // willSend //NDFN -(void)willSend { NSAssert(_isFinalizeInContextHasBeenCalled,@"GSWResponse _finalizeInContext: not called"); }; //-------------------------------------------------------------------- -(void)forceFinalizeInContext { _isFinalizeInContextHasBeenCalled=YES; }; //-------------------------------------------------------------------- -(NSArray*)acceptedEncodings { return _acceptedEncodings; }; //-------------------------------------------------------------------- -(void)setAcceptedEncodings:(NSArray*)acceptedEncodings { ASSIGN(_acceptedEncodings,acceptedEncodings); }; //-------------------------------------------------------------------- // setStatus: //sets http status -(void)setStatus:(unsigned int)status { _status=status; }; //-------------------------------------------------------------------- // status -(unsigned int)status { return _status; }; //-------------------------------------------------------------------- // should be called before finalizeInContext -(void)setCanDisableClientCaching:(BOOL)yn { _canDisableClientCaching=yn; }; //-------------------------------------------------------------------- -(void)disableClientCaching { LOGObjectFnStart(); if (!_isClientCachingDisabled && _canDisableClientCaching) { [self setHeader:disabledCacheDateString forKey:@"date"]; [self setHeader:disabledCacheDateString forKey:@"expires"]; [self setHeader:@"no-cache" forKey:@"pragma"]; if([[GSWApp class] _allowsCacheControlHeader]) [self setHeaders:cacheControlHeaderValues forKey:@"cache-control"]; _isClientCachingDisabled=YES; }; LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(NSString*)description { NSString* description=nil; LOGObjectFnStart(); description=[NSString stringWithFormat: @"<%s %p - httpVersion=%@ status=%d headers=%p contentFaults=%p contentData=%p contentEncoding=%d userInfo=%p>", object_get_class_name(self), (void*)self, _httpVersion, _status, (void*)_headers, (void*)_contentFaults, (void*)_contentData, (int)_contentEncoding, (void*)_userInfo]; LOGObjectFnStop(); return description; }; @end //==================================================================== @implementation GSWResponse (GSWResponseA) //-------------------------------------------------------------------- //NDFN -(BOOL)isFinalizeInContextHasBeenCalled { return _isFinalizeInContextHasBeenCalled; }; //-------------------------------------------------------------------- -(void)_finalizeContentEncodingInContext:(GSWContext*)aContext { int dataLength=0; LOGObjectFnStart(); #ifdef HAVE_ZLIB dataLength=[self _contentLength]; NSDebugMLog(@"dataLength=%d",dataLength); // Now we see if we can gzip the content if (dataLength>1024) // min length: 1024 { // we could do better by having parameters for types NSArray* appAcceptedContentEncodingArray=[GSWApplication acceptedContentEncodingArray]; NSDebugMLog(@"appAcceptedContentEncodingArray=%@",appAcceptedContentEncodingArray); if ([appAcceptedContentEncodingArray count]>0) { NSString* contentType=[self headerForKey:@"content-type"]; if ([contentType isEqualTo:@"text/html"]) { NSString* contentEncoding=[self headerForKey:@"content-encoding"]; // we could do better by handling compress,... if ([contentEncoding length]==0 // Not already encoded && [_acceptedEncodings containsObject:@"gzip"] && [appAcceptedContentEncodingArray containsObject:@"gzip"]) { NSDate* compressStartDate=[NSDate date]; NSData* content=[self content]; NSData* compressedData=[content deflate]; NSDebugMLog(@"compressedData=%@",compressedData); if (compressedData) { #ifdef DEBUG NSDate* compressStopDate=[NSDate date]; NSString* sizeInfoHeader=[NSString stringWithFormat:@"deflate from %d to %d in %0.3f s", dataLength, [compressedData length], [compressStopDate timeIntervalSinceDate:compressStartDate]]; [self setHeader:sizeInfoHeader forKey:@"deflate-info"]; #endif [self setContent:compressedData]; dataLength=[self _contentLength]; [self setHeader:@"gzip" forKey:@"content-encoding"]; }; }; }; }; }; #endif LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(void)_finalizeInContext:(GSWContext*)aContext { int dataLength=0; NSString* dataLengthString=nil; NSData* content=nil; LOGObjectFnStart(); NSAssert(!_isFinalizeInContextHasBeenCalled,@"GSWResponse _finalizeInContext: already called"); #ifndef NDEBUG if(GSDebugSet(@"GSWDocStructure")) { NSString* docStructure=[aContext docStructure]; if (docStructure) [self appendContentString:[NSString stringWithFormat:@"\n\n",docStructure]]; } #endif //TODOV: if !session in request and session created: no client cache if (![self _isClientCachingDisabled] && [aContext hasSession] && ![aContext _requestSessionID]) [self disableClientCaching]; [self _resolveContentFaultsInContext:aContext]; // Finalize cookies [self _finalizeCookiesInContext:aContext]; // Add load info to headers if (![self headersForKey:GSWHTTPHeader_LoadAverage[GSWebNamingConv]]) [self setHeader:GSWIntToNSString([GSWApp activeSessionsCount]) forKey:GSWHTTPHeader_LoadAverage[GSWebNamingConv]]; // Add refusing new sessions info to headers if ([GSWApp isRefusingNewSessions] && ![self headersForKey:GSWHTTPHeader_RefuseSessions[GSWebNamingConv]]) [self setHeader:GSWIntToNSString((int)[GSWApp _refuseNewSessionsTimeInterval]) forKey:GSWHTTPHeader_RefuseSessions[GSWebNamingConv]]; [self _finalizeContentEncodingInContext:aContext]; content=[self content]; dataLength=[self _contentLength]; NSDebugMLog(@"dataLength=%d",dataLength); dataLengthString=GSWIntToNSString(dataLength); [self setHeader:dataLengthString forKey:GSWHTTPHeader_ContentLength]; NSDebugMLLog(@"low",@"headers:%@",_headers); _isFinalizeInContextHasBeenCalled=YES; LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(void)_appendTagAttribute:(NSString*)attributeName value:(id)value escapingHTMLAttributeValue:(BOOL)escape { [self appendContentCharacter:' ']; [self _appendContentAsciiString:attributeName]; [self _appendContentAsciiString:@"=\""]; if (escape) [self _appendContentAsciiString:[[self class]stringByEscapingHTMLAttributeValue:value]]; else [self _appendContentAsciiString:value]; [self appendContentCharacter:'"']; }; @end //==================================================================== @implementation GSWResponse (GSWResponseB) -(void)_resolveContentFaultsInContext:(GSWContext*)aContext { LOGObjectFnNotImplemented(); //TODOFN }; //-------------------------------------------------------------------- -(void)_appendContentFault:(id)unknown { LOGObjectFnNotImplemented(); //TODOFN }; @end //==================================================================== @implementation GSWResponse (GSWResponseC) //-------------------------------------------------------------------- -(BOOL)_isClientCachingDisabled { return _isClientCachingDisabled; }; //-------------------------------------------------------------------- -(unsigned int)_contentDataLength { return [_contentData length]; }; @end //==================================================================== @implementation GSWResponse (GSWResponseD) //-------------------------------------------------------------------- -(BOOL)_responseIsEqual:(GSWResponse*)aResponse { LOGObjectFnNotImplemented(); //TODOFN return NO; }; @end //==================================================================== @implementation GSWResponse (GSWActionResults) //-------------------------------------------------------------------- -(GSWResponse*)generateResponse { return self; }; @end //==================================================================== @implementation GSWResponse (Stream) -(void)setContentStreamFileHandle:(NSFileHandle*)fileHandle bufferSize:(unsigned int)bufferSize length:(unsigned long)length { ASSIGN(_contentStreamFileHandle,fileHandle); if (bufferSize==0) _contentStreamBufferSize=4096; else _contentStreamBufferSize=bufferSize; _contentStreamBufferLength=length; }; @end //==================================================================== @implementation GSWResponse (GSWResponseError) //-------------------------------------------------------------------- //NDFN //Last cHance Response +(GSWResponse*)responseWithMessage:(NSString*)aMessage inContext:(GSWContext*)aContext forRequest:(GSWRequest*)aRequest { return [self responseWithMessage:aMessage inContext:aContext forRequest:aRequest forceFinalize:NO]; }; +(GSWResponse*)responseWithMessage:(NSString*)aMessage inContext:(GSWContext*)aContext forRequest:(GSWRequest*)aRequest forceFinalize:(BOOL)forceFinalize { GSWResponse* response=nil; NSString* httpVersion=nil; LOGClassFnStart(); response=[GSWApp createResponseInContext:aContext]; if (response) { NSString* responseString=nil; if (aContext && [aContext request]) aRequest=[aContext request]; httpVersion=[aRequest httpVersion]; if (httpVersion) [response setHTTPVersion:httpVersion]; [response setHeader:@"text/html" forKey:@"content-type"]; [aContext _setResponse:response]; responseString=[NSString stringWithFormat:@"\nGNUstepWeb Error\n\n\n
\n%@\n
\n\n\n", [[response class]stringByEscapingHTMLString:aMessage]]; [response appendContentString:responseString]; if (forceFinalize) [response forceFinalizeInContext]; }; LOGClassFnStop(); return response; }; @end //==================================================================== @implementation GSWResponse (GSWResponseRefused) //-------------------------------------------------------------------- // //Refuse Response +(GSWResponse*)generateRefusingResponseInContext:(GSWContext*)aContext forRequest:(GSWRequest*)aRequest { GSWResponse* response=nil; NSString* httpVersion=nil; LOGClassFnStart(); response=[GSWApp createResponseInContext:aContext]; if (response) { NSString* responseString=nil; NSString* locationURLString=nil; NSString* message=nil; if (aContext && [aContext request]) { aRequest=[aContext request]; } httpVersion=[aRequest httpVersion]; if (httpVersion) { [response setHTTPVersion:httpVersion]; } locationURLString = [NSString stringWithFormat:@"%@/%@.gswa", [aRequest adaptorPrefix], [aRequest applicationName]]; message = [NSString stringWithFormat:@"Sorry, your request could not immediately be processed. Please try this URL: %@\nConnection closed by foreign host.", locationURLString, locationURLString]; responseString=[NSString stringWithFormat:@"\nGNUstepWeb\n\n\n
\n%@\n
\n\n\n", message]; [response _generateRedirectResponseWithMessage:responseString location:locationURLString isDefinitive:NO]; if (aContext) { [aContext _setResponse:response]; } }; LOGClassFnStop(); return response; }; @end //==================================================================== @implementation GSWResponse (GSWResponseRedirected) //-------------------------------------------------------------------- -(void)_generateRedirectResponseWithMessage:(NSString*)message location:(NSString*)location isDefinitive:(BOOL)isDefinitive { if (message) { [self appendContentString:message]; [self setHeader:GSWIntToNSString([[self content] length]) forKey:@"content-length"]; }; if (isDefinitive) [self setStatus:301]; // redirect definitive ! else [self setStatus:302]; // redirect temporary ! [self setHeader:location forKey:@"Location"]; [self setHeader:@"text/html" forKey:@"content-type"]; [self setHeader:@"YES" forKey:@"x-gsweb-refusing-redirection"]; } //-------------------------------------------------------------------- // //Redirect Response +(GSWResponse*)generateRedirectResponseWithMessage:(NSString*)message location:(NSString*)location isDefinitive:(BOOL)isDefinitive inContext:(GSWContext*)aContext forRequest:(GSWRequest*)aRequest { GSWResponse* response=nil; NSString* httpVersion=nil; LOGClassFnStart(); response=[GSWApp createResponseInContext:aContext]; if (response) { if (aContext && [aContext request]) { aRequest=[aContext request]; } httpVersion=[aRequest httpVersion]; if (httpVersion) { [response setHTTPVersion:httpVersion]; } [response _generateRedirectResponseWithMessage:message location:location isDefinitive:isDefinitive]; if (aContext) { [aContext _setResponse:response]; } }; LOGClassFnStop(); return response; }; //-------------------------------------------------------------------- // //Redirect Response +(GSWResponse*)generateRedirectResponseWithMessage:(NSString*)message location:(NSString*)location isDefinitive:(BOOL)isDefinitive { GSWResponse* response=nil; LOGClassFnStart(); response=[self generateRedirectResponseWithMessage:message location:location isDefinitive:isDefinitive inContext:nil forRequest:nil]; LOGClassFnStop(); return response; }; //-------------------------------------------------------------------- +(GSWResponse*)generateRedirectDefaultResponseWithLocation:(NSString*)location isDefinitive:(BOOL)isDefinitive inContext:(GSWContext*)aContext forRequest:(GSWRequest*)aRequest { NSString* message=nil; GSWResponse* response=nil; LOGClassFnStart(); message=[NSString stringWithFormat:@"This page has been moved%s to %@", (isDefinitive ? "" : " temporarily"), location, location]; response=[self generateRedirectResponseWithMessage:message location:location isDefinitive:isDefinitive inContext:aContext forRequest:aRequest]; LOGClassFnStop(); return response; }; //-------------------------------------------------------------------- +(GSWResponse*)generateRedirectDefaultResponseWithLocation:(NSString*)location isDefinitive:(BOOL)isDefinitive { GSWResponse* response=nil; LOGClassFnStart(); response=[self generateRedirectDefaultResponseWithLocation:location isDefinitive:isDefinitive inContext:nil forRequest:nil]; LOGClassFnStop(); return response; }; @end