/** GSWResponse.m - GSWeb: Class GSWResponse Copyright (C) 1999-2003 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 NSStringEncoding globalDefaultEncoding=NSISOLatin1StringEncoding; 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])) { _httpVersion=@"HTTP/1.0"; _status=200; _headers=[NSMutableDictionary new]; [self _initContentData]; _contentEncoding=NSISOLatin1StringEncoding; }; LOGObjectFnStop(); return self; }; //-------------------------------------------------------------------- -(void)dealloc { // GSWLogAssertGood(self); // NSDebugFLog(@"dealloc Response %p",self); // NSDebugFLog0(@"Release Response httpVersion"); DESTROY(_httpVersion); // NSDebugFLog0(@"Release Response headers"); DESTROY(_headers); // NSDebugFLog0(@"Release Response contentFaults"); DESTROY(_contentFaults); // NSDebugFLog0(@"Release Response contentData"); DESTROY(_contentData); DESTROY(_acceptedEncodings); // NSDebugFLog0(@"Release Response userInfo"); DESTROY(_userInfo); //NSDebugFLog0(@"Release Response cookies"); DESTROY(_cookies); // NSDebugFLog0(@"Release Response"); [super dealloc]; }; //-------------------------------------------------------------------- -(id)copyWithZone:(NSZone*)zone { GSWResponse* clone = [[isa allocWithZone:zone] init]; if (clone) { ASSIGNCOPY(clone->_httpVersion,_httpVersion); clone->_status=_status; ASSIGNCOPY(clone->_headers,_headers); ASSIGNCOPY(clone->_contentFaults,_contentFaults); ASSIGNCOPY(clone->_contentData,_contentData); clone->_contentEncoding=_contentEncoding; ASSIGNCOPY(clone->_acceptedEncodings,_acceptedEncodings); ASSIGNCOPY(clone->_userInfo,_userInfo); ASSIGNCOPY(clone->_cookies,_cookies); clone->_isClientCachingDisabled=_isClientCachingDisabled; clone->_contentFaultsHaveBeenResolved=_contentFaultsHaveBeenResolved; }; return clone; }; //-------------------------------------------------------------------- // content -(NSData*)content { //TODO exception.. return _contentData; }; //-------------------------------------------------------------------- // willSend //NDFN -(void)willSend { NSAssert(_isFinalizeInContextHasBeenCalled,@"GSWResponse _finalizeInContext: not called"); }; -(void)forceFinalizeInContext { _isFinalizeInContextHasBeenCalled=YES; }; //-------------------------------------------------------------------- // headerForKey: // return: // nil: if no header for key_ // 1st header: if multiple headers for key_ // header: otherwise -(NSString*)headerForKey:(NSString*)key { id object=[_headers objectForKey:key]; if (object && [object isKindOfClass:[NSArray class]]) return [object objectAtIndex:0]; else return (NSString*)object; }; //-------------------------------------------------------------------- // headerKeys // return array of header keys or nil if no header -(NSArray*)headerKeys { return [_headers allKeys]; }; //-------------------------------------------------------------------- // headersForKey: //return array of headers of key_ -(NSArray*)headersForKey:(NSString*)key { id object=[_headers objectForKey:key]; if (!object || [object isKindOfClass:[NSArray class]]) return (NSArray*)object; else return [NSArray arrayWithObject:object]; }; //-------------------------------------------------------------------- // httpVersion //return http version like @"HTTP/1.0" -(NSString*)httpVersion { return _httpVersion; }; //-------------------------------------------------------------------- -(NSArray*)acceptedEncodings { return _acceptedEncodings; }; //-------------------------------------------------------------------- // setContent: //Set content with contentData_ -(void)setContent:(NSData*)contentData { if (contentData) { NSMutableData* aData=[[NSMutableData new]autorelease]; [aData appendData:contentData]; contentData=aData; }; ASSIGN(_contentData,(NSMutableData*)contentData); }; //-------------------------------------------------------------------- // setHeader:forKey: -(void)setHeader:(NSString*)header forKey:(NSString*)key { //OK id object=nil; NSAssert(header,@"No header"); NSAssert(key,@"No header key"); object=[_headers objectForKey:key]; if (object) [self setHeaders:[object arrayByAddingObject:header] forKey:key]; else [self setHeaders:[NSArray arrayWithObject:header] forKey:key]; }; //-------------------------------------------------------------------- // setHeaders:forKey: -(void)setHeaders:(NSArray*)headers forKey:(NSString*)key { //OK NSAssert(headers,@"No headers"); NSAssert(key,@"No header key"); if (!_headers) _headers=[NSMutableDictionary new]; [_headers setObject:headers forKey:key]; }; //-------------------------------------------------------------------- // setHeaders: -(void)setHeaders:(NSDictionary*)headerDictionary { if (!_headers) _headers=[NSMutableDictionary new]; if (headerDictionary) { NSEnumerator* keyEnum=nil; id headerName=nil; keyEnum = [headerDictionary keyEnumerator]; while ((headerName = [keyEnum nextObject])) { [self setHeaders:[NSArray arrayWithObject:[headerDictionary objectForKey:headerName]] forKey:headerName]; }; }; }; //-------------------------------------------------------------------- // headers -(NSMutableDictionary*)headers { return _headers; }; //-------------------------------------------------------------------- // setHTTPVersion: //sets the http version (like @"HTTP/1.0"). -(void)setHTTPVersion:(NSString*)version { //OK ASSIGN(_httpVersion,version); }; //-------------------------------------------------------------------- -(void)setAcceptedEncodings:(NSArray*)acceptedEncodings { ASSIGN(_acceptedEncodings,acceptedEncodings); }; //-------------------------------------------------------------------- // setStatus: //sets http status -(void)setStatus:(unsigned int)status { _status=status; }; //-------------------------------------------------------------------- // setUserInfo: -(void)setUserInfo:(NSDictionary*)userInfo { ASSIGN(_userInfo,userInfo); }; //-------------------------------------------------------------------- // status -(unsigned int)status { return _status; }; //-------------------------------------------------------------------- // userInfo -(NSDictionary*)userInfo { return _userInfo; }; //-------------------------------------------------------------------- -(void)disableClientCaching { //OK LOGObjectFnStart(); if (!_isClientCachingDisabled) { [self setHeader:disabledCacheDateString forKey:@"date"]; [self setHeader:disabledCacheDateString forKey:@"expires"]; [self setHeader:@"no-cache" forKey:@"pragma"]; //TODO later /* if([GSWApp _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 (GSWContentConveniences) //-------------------------------------------------------------------- // appendContentBytes:length: -(void)appendContentBytes:(const void*)bytes length:(unsigned)length { LOGObjectFnStart(); if (length>0) { [_contentData appendBytes:bytes length:length]; }; LOGObjectFnStop(); }; //-------------------------------------------------------------------- // appendContentCharacter: -(void)appendContentCharacter:(char)aChar { LOGObjectFnStart(); [self appendContentBytes:&aChar length:1]; LOGObjectFnStop(); }; //-------------------------------------------------------------------- // appendContentData: -(void)appendContentData:(NSData*)dataObject { const void* bytes=NULL; unsigned int length=0; LOGObjectFnStart(); NSDebugMLLog(@"low",@"response=%p dataObject:%@",self,dataObject); bytes=[dataObject bytes]; length=[dataObject length]; [self appendContentBytes:bytes length:length]; LOGObjectFnStop(); }; //-------------------------------------------------------------------- // appendContentString: -(void)appendContentString:(NSString*)aString { LOGObjectFnStart(); NSDebugMLLog(@"low",@"response=%p contentEncoding=%d",self,(int)_contentEncoding); if (aString) { NSData* newData=nil; NSString* string=nil; string=[NSString stringWithObject:aString]; NSAssert(string,@"Can't get string from object"); #ifndef NDEBUG NSAssert3(![string isKindOfClass:[NSString class]] || [string canBeConvertedToEncoding:_contentEncoding], @"string %s (of class %@) can't be converted to encoding %d", [string lossyCString], [string class], _contentEncoding); #endif newData=[string dataUsingEncoding:_contentEncoding]; NSAssert3(newData,@"Can't create data from %@ \"%s\" using encoding %d", [string class], ([string isKindOfClass:[NSString class]] ? [string lossyCString] : "**Not a string**"), (int)_contentEncoding); NSDebugMLLog(@"low",@"newData=%@",newData); [_contentData appendData:newData]; }; LOGObjectFnStop(); }; //-------------------------------------------------------------------- // appendDebugCommentContentString: -(void)appendDebugCommentContentString:(NSString*)aString { #ifndef NDEBUG if (GSDebugSet(@"debugComments") == YES) [self appendContentString:[NSString stringWithFormat:@"\n\n",aString]]; #endif }; //-------------------------------------------------------------------- // contentEncoding -(NSStringEncoding)contentEncoding { return _contentEncoding; }; //-------------------------------------------------------------------- // setContentEncoding: -(void)setContentEncoding:(NSStringEncoding)encoding { NSDebugMLLog(@"low",@"setContentEncoding:%d",(int)encoding); _contentEncoding=encoding; }; @end //==================================================================== @implementation GSWResponse (GSWHTMLConveniences) //-------------------------------------------------------------------- // appendContentHTMLAttributeValue: -(void)appendContentHTMLAttributeValue:(NSString*)value { NSString* string=nil; LOGObjectFnStart(); NSDebugMLLog(@"low",@"response=%p value=%@",self,value); string=[NSString stringWithObject:value]; [self appendContentString:[[self class]stringByEscapingHTMLAttributeValue:string]]; LOGObjectFnStop(); }; //-------------------------------------------------------------------- // appendContentHTMLString: -(void)appendContentHTMLString:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; [self appendContentString:[[self class]stringByEscapingHTMLString:string]]; }; //-------------------------------------------------------------------- -(void)appendContentHTMLConvertString:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; [self appendContentString:[[self class]stringByConvertingToHTML:string]]; }; //-------------------------------------------------------------------- -(void)appendContentHTMLEntitiesConvertString:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; [self appendContentString:[[self class]stringByConvertingToHTMLEntities:string]]; }; //-------------------------------------------------------------------- +(NSString*)stringByEscapingHTMLString:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; return [string stringByEscapingHTMLString]; }; //-------------------------------------------------------------------- +(NSString*)stringByEscapingHTMLAttributeValue:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; return [string stringByEscapingHTMLAttributeValue]; }; //-------------------------------------------------------------------- +(NSString*)stringByConvertingToHTMLEntities:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; return [string stringByConvertingToHTMLEntities]; }; //-------------------------------------------------------------------- +(NSString*)stringByConvertingToHTML:(NSString*)aString { NSString* string=[NSString stringWithObject:aString]; return [string stringByConvertingToHTML]; }; @end //==================================================================== @implementation GSWResponse (Cookies) //-------------------------------------------------------------------- -(NSString*)_formattedCookiesString { LOGObjectFnNotImplemented(); //TODOFN return nil; }; //-------------------------------------------------------------------- -(NSArray*)allocCookiesIFND { //OK if (!_cookies) _cookies=[NSMutableArray new]; return _cookies; }; //-------------------------------------------------------------------- -(void)addCookie:(GSWCookie*)cookie { //OK NSMutableArray* cookies=nil; LOGObjectFnStart(); cookies=[self allocCookiesIFND]; if (cookie) [cookies addObject:cookie]; LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(void)removeCookie:(GSWCookie*)cookie { NSMutableArray* cookies=nil; LOGObjectFnStart(); cookies=[self allocCookiesIFND]; if (cookie) [cookies removeObject:cookie]; LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(NSArray*)cookies { NSMutableArray* cookies=[self allocCookiesIFND]; return cookies; }; //-------------------------------------------------------------------- //NDFN -(NSArray*)cookiesHeadersValues { NSMutableArray* strings=nil; NSArray* cookies=[self cookies]; if ([cookies count]>0) { int i=0; int count=[cookies count]; GSWCookie* cookie=nil; NSString* cookieString=nil; strings=[NSMutableArray array]; for(i=0;i0) { id cookiesHeadersValues=[self cookiesHeadersValues]; NSDebugMLLog(@"low",@"cookiesHeadersValues=%@",cookiesHeadersValues); [self setHeaders:cookiesHeadersValues forKey:GSWHTTPHeader_SetCookie]; }; request=[aContext request]; applicationNumber=[request applicationNumber]; NSDebugMLLog(@"low",@"applicationNumber=%d",applicationNumber); //TODO /* if (_applicationNumber>=0) { LOGError(); //TODO }; */ dataLength=[_contentData length]; #ifdef HAVE_ZLIB // Now we see if we can gzip the content if (dataLength>1024) // min length: 1024 { // we could do better by having parameters for types 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"]) { NSDate* compressStartDate=[NSDate date]; NSData* compressedData=[_contentData 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 ASSIGN(_contentData,compressedData); dataLength=[_contentData length]; [self setHeader:@"gzip" forKey:@"content-encoding"]; }; }; }; }; #endif dataLengthString=[NSString stringWithFormat:@"%d", dataLength]; [self setHeader:dataLengthString forKey:GSWHTTPHeader_ContentLength]; NSDebugMLLog(@"low",@"headers:%@",_headers); _isFinalizeInContextHasBeenCalled=YES; LOGObjectFnStop(); }; //-------------------------------------------------------------------- -(void)_initContentData { //OK DESTROY(_contentData); _contentData=[NSMutableData new]; }; //-------------------------------------------------------------------- -(void)_appendContentAsciiString:(NSString*)aString { NSData* newData=nil; NSString* string=nil; LOGObjectFnStart(); NSDebugMLLog(@"low",@"aString:%@",aString); string=[NSString stringWithObject:aString]; NSDebugMLLog(@"low",@"_string:%@",string); newData=[string dataUsingEncoding:_contentEncoding]; [self appendContentData:newData]; 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 (GSWResponseDefaultEncoding) //-------------------------------------------------------------------- +(void)setDefaultEncoding:(NSStringEncoding)encoding { globalDefaultEncoding=encoding; }; //-------------------------------------------------------------------- +(NSStringEncoding)defaultEncoding; { return globalDefaultEncoding; }; @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:[NSString stringWithFormat:@"%d",[[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