// Copyright 1997-2002 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import <OWF/OWWebPipeline.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

#import <OWF/NSDate-OWExtensions.h>
#import <OWF/OWAddress.h>
#import <OWF/OWContentCache.h>
#import <OWF/OWContentInfo.h>
#import <OWF/OWCookieDomain.h>
#import <OWF/OWCookie.h>
#import <OWF/OWHeaderDictionary.h>
#import <OWF/OWURL.h>

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWWebPipeline.m,v 1.37 2002/03/09 01:53:52 kc Exp $")

@interface OWWebPipeline (Private)
- (void)_setRefreshEvent:(OFScheduledEvent *) aRefreshEvent;
- (void)_postProcessHeaders;
- (void)_processRefreshHeader:(NSString *)refresh;
- (void)_processCacheControlHeaders:(NSArray *)values;
- (void)_processExpiresHeader:(NSString *)expires;
@end

@implementation OWWebPipeline

static OFScheduler *refreshScheduler;
OFCharacterSet *WhitespaceSet;
OFCharacterSet *CacheControlNameDelimiterSet;
OFCharacterSet *CacheControlValueDelimiterSet;

+ (void)initialize;
{
    OBINITIALIZE;

    refreshScheduler = [[[OFScheduler mainScheduler] subscheduler] retain];

    WhitespaceSet = [[OFCharacterSet alloc] initWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

    CacheControlNameDelimiterSet = [[OFCharacterSet alloc] initWithOFCharacterSet:WhitespaceSet];
    [CacheControlNameDelimiterSet addCharacter:','];
    [CacheControlNameDelimiterSet addCharacter:'='];

    CacheControlValueDelimiterSet = [[OFCharacterSet alloc] initWithOFCharacterSet:WhitespaceSet];
    [CacheControlValueDelimiterSet addCharacter:','];
}

- initWithContent:(id <OWContent>)aContent target:(id <OWTarget, OFWeakRetain, NSObject>)aTarget useCachedErrorContent:(BOOL)useError;
{
    if (![super initWithContent:aContent target:aTarget useCachedErrorContent:useError])
        return nil;

    webPipelineFlags.proxyCacheDisabled = NO;
    historyAction = OWWebPipelineForwardHistoryAction;
    if (parentContentInfo)
        [self setReferringContentInfo:parentContentInfo];
    
    return self;
}

- (void)dealloc;
{
    [referringContentInfo release];
    [referringAddress release];
    [refreshEvent release];
    [super dealloc];
}

- (id <OWAddress>)referringAddress;
{
    return referringAddress;
}

- (void)setReferringAddress:(id <OWAddress>)anAddress;
{
    id <OWAddress> oldReferringAddress;

    if (referringAddress == anAddress)
        return;

    oldReferringAddress = referringAddress;
    referringAddress = [anAddress retain];
    [oldReferringAddress release];
}

- (OWContentInfo *)referringContentInfo
{
    return referringContentInfo;
}

- (void)setReferringContentInfo:(OWContentInfo *)anInfo
{
    if (anInfo != referringContentInfo) {
        [anInfo retain];
        [referringContentInfo release];
        referringContentInfo = anInfo;
    }

    [self setReferringAddress:[referringContentInfo address]];
}

- (OWWebPipelineHistoryAction)historyAction;
{
    return historyAction;
}

- (void)setHistoryAction:(OWWebPipelineHistoryAction)newHistoryAction;
{
    historyAction = newHistoryAction;
}

- (BOOL)proxyCacheDisabled;
{
    return webPipelineFlags.proxyCacheDisabled || (lastAddress && [lastAddress isAlwaysUnique]);
}

- (void)setProxyCacheDisabled:(BOOL)newDisabled;
{
    webPipelineFlags.proxyCacheDisabled = newDisabled;
}

// OWPipeline subclass

- (void)startProcessingContent;
{
    if (state == PipelineInit && webPipelineFlags.proxyCacheDisabled)
        [[self contentCacheForLastAddress] flushCachedContent];
    [super startProcessingContent];
}

- (void)refetchContentFromLastAddress;
{
    historyAction = OWWebPipelineReloadHistoryAction;
    [self setProxyCacheDisabled:YES];
    [super refetchContentFromLastAddress];
}

- (void)invalidate;
{
    [refreshScheduler abortEvent:refreshEvent];
    [refreshEvent release];
    refreshEvent = nil;
    [super invalidate];
}

- (void)addHeader:(NSString *)headerName value:(NSString *)headerValue;
{
    [super addHeader:headerName value:headerValue];
    
    if ([headerName compare:OWSetCookieHeader options:NSCaseInsensitiveSearch] == NSOrderedSame) {
        [OWCookieDomain registerCookiesFromPipeline:self headerValue:headerValue];
    }
}

// OWPipeline subclass (SubclassesOnly)

- (void)deactivate;
{
    [self _postProcessHeaders];
    [super deactivate];
}

// NSCopying protocol

- copyWithZone:(NSZone *)zone;
{
    OWWebPipeline *newPipeline;

    newPipeline = [super copyWithZone:zone];
    [newPipeline setReferringContentInfo:referringContentInfo];
    [newPipeline setReferringAddress:referringAddress];
    [newPipeline setHistoryAction:OWWebPipelineReloadHistoryAction];
    return newPipeline;
}

@end

@implementation OWWebPipeline (Private)

- (void)_setRefreshEvent:(OFScheduledEvent *) aRefreshEvent;
{
    OBPRECONDITION(!refreshEvent);
    refreshEvent = [aRefreshEvent retain];
}

- (void)_postProcessHeaders;
{
    NSString *headerValue;
    NSArray *headerArray;

    if ((headerValue = [headerDictionary lastStringForKey:@"refresh"]))
	[self _processRefreshHeader:headerValue];
    if ((headerArray = [headerDictionary stringArrayForKey:@"cache-control"]))
	[self _processCacheControlHeaders:headerArray];
    if ((headerValue = [headerDictionary lastStringForKey:@"expires"]))
	[self _processExpiresHeader:headerValue];
    else {
        id <OWAddress> address;

        // According to HTTP/1.1 (RFC2616, 13.9), query URLs should not be cached unless an explicit expiration time was specified
        address = [self lastAddress];
        if ([address isKindOfClass:[OWAddress class]] && [[(OWAddress *)address url] query] != nil)
            [contentCacheForLastAddress expireAtDate:[NSDate dateWithTimeIntervalSinceNow:0.0]];
    }

    // DON'T register cookies here.  They've already been registered in OWHTTPSession.  We have to register the cookies at the beginning of the document so that they are around for pipelines that might get spawned off due to the documents content
}

- (void)_processRefreshHeader:(NSString *)refresh;
{
    NSString *refreshTimeString, *urlString;
    NSTimeInterval refreshTimeInterval;
    NSCalendarDate *refreshDate;
    OWURL *refreshURL, *referringURL;
    OWAddress *refreshAddress;
    OWWebPipeline *refreshPipeline;
    OFStringScanner *scanner;

    refreshTimeString = nil;
    urlString = nil;
    scanner = [[OFStringScanner alloc] initWithString:refresh];
    refreshTimeString = [scanner readFullTokenWithDelimiterCharacter:';'];
    while (scannerPeekCharacter(scanner) == ';') {
        scannerSkipPeekedCharacter(scanner);
        scannerScanUpToCharacterNotInOFCharacterSet(scanner, WhitespaceSet);
        if ([scanner scanStringCaseInsensitive:@"url=" peek:NO]) {
            urlString = [OWURL cleanURLString:[scanner readFullTokenWithDelimiterCharacter:';']];
        } else {
            scannerScanUpToCharacter(scanner, ';');
        }
    }
    [scanner release];
    if (refreshTimeString == nil || [refreshTimeString isEqualToString:@""])
        return;
    referringURL = [(OWAddress *)lastAddress url];
    refreshURL = referringURL;
    if (urlString) {
        if (refreshURL)
            refreshURL = [refreshURL urlFromRelativeString:urlString];
        else
            refreshURL = [OWURL urlFromString:urlString];
    }
    refreshAddress = [OWAddress addressWithURL:refreshURL];
    if (![refreshAddress isSameDocumentAsAddress:(OWAddress *)lastAddress]) {
        // If we've been asked to redirect to another page, we need to make sure we redirect on schedule the next time we load this page.
        // TODO: Rather than flushing our content from the cache, it would be much better to cache the HTTP headers so that the next pipeline gets the cached headers and content rather than having to start again from scratch.
        [[self contentCacheForLastAddress] flushCachedContent];
    }
    refreshTimeInterval = [refreshTimeString floatValue];
    refreshPipeline = [[[self class] alloc] initWithContent:refreshAddress target:[self target]];
    [refreshPipeline setProxyCacheDisabled:YES];
    refreshDate = [[NSCalendarDate alloc] initWithTimeIntervalSinceNow:refreshTimeInterval];
    [refreshDate setCalendarFormat:NSLocalizedStringFromTableInBundle(@"%b %d %H:%M:%S", @"OWF", [OWWebPipeline bundle], webpipeline timed refresh NSCalendarDate format)];
    [refreshPipeline setContextObject:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Timed Refresh at %@", @"OWF", [OWWebPipeline bundle], webpipeline timed refresh message), refreshDate] forKey:@"Status"];
    if (referringURL)
        [refreshPipeline setReferringAddress:[OWAddress addressWithURL:referringURL]];
    if (refreshTimeInterval <= 1.0) {
        [refreshPipeline startProcessingContent];
    } else {
        OFScheduledEvent *event;
        
        event = [refreshScheduler scheduleSelector:@selector(startProcessingContent) onObject:refreshPipeline withObject:nil atDate:refreshDate];
        [refreshPipeline _setRefreshEvent:event];
    }
    [refreshDate release];
    [refreshPipeline release];
}

- (void)_processCacheControlHeaders:(NSArray *)values;
{
    unsigned int valueIndex, valueCount;

    valueCount = [values count];
    for (valueIndex = 0; valueIndex < valueCount; valueIndex++) {
        OFStringScanner *scanner;

        scanner = [[OFStringScanner alloc] initWithString:[values objectAtIndex:valueIndex]];
        while (scannerScanUpToCharacterNotInOFCharacterSet(scanner, WhitespaceSet)) {
            NSString *name, *value;

            name = [scanner readFullTokenWithDelimiterOFCharacterSet:CacheControlNameDelimiterSet forceLowercase:YES];
            scannerScanUpToCharacterNotInOFCharacterSet(scanner, WhitespaceSet);
            if (scannerPeekCharacter(scanner) == '=') {
                scannerSkipPeekedCharacter(scanner);
                scannerScanUpToCharacterNotInOFCharacterSet(scanner, WhitespaceSet);
                if (scannerPeekCharacter(scanner) == '"') {
                    value = [scanner readFullTokenWithDelimiterCharacter:'"'];
                } else {
                    value = [scanner readFullTokenWithDelimiterOFCharacterSet:CacheControlValueDelimiterSet];
                }
            } else {
                value = nil;
            }
            scannerScanUpToCharacterNotInOFCharacterSet(scanner, WhitespaceSet);
            if (scannerPeekCharacter(scanner) == ',')
                scannerSkipPeekedCharacter(scanner);
            if ([name isEqualToString:@"no-cache"])
                [contentCacheForLastAddress expireAtDate:[NSDate dateWithTimeIntervalSinceNow:0.0]];
            if ([name isEqualToString:@"max-age"] && value != nil)
                [contentCacheForLastAddress expireAtDate:[NSDate dateWithTimeIntervalSinceNow:[value doubleValue]]];
        }
        [scanner release];
    }
}

- (void)_processExpiresHeader:(NSString *)expires;
{
    NSDate *expireDate;

    expireDate = [NSDate dateWithHTTPDateString:expires];
    if (!expireDate)
	expireDate = [NSDate dateWithTimeIntervalSinceNow:0.0];
    [contentCacheForLastAddress expireAtDate:expireDate];
}

@end
