// 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/OWCookieDomain.h>

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

#import <OWF/NSDate-OWExtensions.h>
#import <OWF/OWAddress.h>
#import <OWF/OWCookiePath.h>
#import <OWF/OWCookie.h>
#import <OWF/OWHeaderDictionary.h>
#import <OWF/OWHTTPSession.h>
#import <OWF/OWNetLocation.h>
#import <OWF/OWWebPipeline.h>
#import <OWF/OWURL.h>
#import <OmniExpat/xmlparse.h>


RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/HTTP.subproj/OWCookieDomain.m,v 1.46 2002/01/25 18:24:52 bungi Exp $")


static NSLock *domainLock;
static NSMutableDictionary *domainsByName;
static OFScheduledEvent *saveEvent;

static NSCharacterSet *endNameSet, *endNameValueSet, *endValueSet, *endDateSet, *endKeySet;
static NSTimeInterval distantPastInterval;

static OWCookieDomainBehavior defaultBehavior = OWCookieDomainPromptBehavior;

static id classDelegate;

static NSString *OWCookieImmunizedDomainPath = @"(Immunized Domain)";
static NSString *OW4CookieFileName = @"Cookies.xml";
static NSString *OW3CookieFileName = @"Cookies.plist";
NSString *OWCookiesChangedNotification = @"OWCookiesChangedNotification";

BOOL OWCookiesDebug = NO;

NSString *OWSetCookieHeader = @"set-cookie";


static inline void _locked_checkCookiesLoaded()
{
    if (!domainsByName) {
        [domainLock unlock];
        [NSException raise: NSInternalInconsistencyException format: @"Attempted to access cookies before they had been loaded."];
    }
}

static inline const char *_stringForScope(OWCookieBehaviorScope scope)
{
    switch (scope) {
        case OWCookieSingleBehaviorScope:
            return "single";
        case OWCookieDomainBehaviorScope:
            return "domain";
        case OWCookieAllDomainsBehaviorScope:
            return "global";
        default:
            return "INVALID";
    }
}

static inline const char *_stringForBehavior(OWCookieDomainBehavior behavior)
{
    const char *string;
    
    switch (behavior) {
        case OWCookieDomainAcceptBehavior:
            string = "accept";
            break;
        case OWCookieDomainAcceptForSessionBehavior:
            string = "accept-session";
            break;
        case OWCookieDomainRejectBehavior:
            string = "reject";
            break;
        case OWCookieDomainPromptBehavior:
        default:
            string = "prompt";
            break;
    }
    
    return string;
}

static inline OWCookieDomainBehavior _behaviorForString(NSString *behaviorString)
{
    if ([behaviorString isEqualToString:@"accept"])
        return OWCookieDomainAcceptBehavior;
    if ([behaviorString isEqualToString:@"accept-session"])
        return OWCookieDomainAcceptForSessionBehavior;
    if ([behaviorString isEqualToString:@"reject"])
        return OWCookieDomainRejectBehavior;
    if ([behaviorString isEqualToString:@"prompt"])
        return OWCookieDomainPromptBehavior;
    return OWCookieDomainDefaultBehavior;
}

static inline OWCookieDomainBehavior _behaviorForCString(const char *behaviorString)
{
    if (strcmp(behaviorString, "accept") == 0)
        return OWCookieDomainAcceptBehavior;
    if (strcmp(behaviorString, "accept-session") == 0)
        return OWCookieDomainAcceptForSessionBehavior;
    if (strcmp(behaviorString, "reject") == 0)
        return OWCookieDomainRejectBehavior;
    if (strcmp(behaviorString, "prompt") == 0)
        return OWCookieDomainPromptBehavior;
    return OWCookieDomainDefaultBehavior;
}


@interface OWCookieDomain (PrivateAPI)
+ (void)controllerDidInitialize:(OFController *)controller;
+ (void)controllerWillTerminate:(OFController *)controller;
+ (void)saveCookies;
+ (NSString *)cookiePath:(NSString *)fileName;
+ (void)locked_didChange;
+ (void)notifyCookiesChanged;
- (void)addCookie:(OWCookie *)cookie andNotify:(BOOL)shouldNotify;
+ (OWCookieDomain *)domainNamed:(NSString *)name behavior:(OWCookieDomainBehavior)behavior andNotify:(BOOL)shouldNotify;
- (OWCookiePath *)locked_pathNamed:(NSString *)pathName shouldCreate:(BOOL)shouldCreate;
+ (NSArray *)searchDomainsForDomain:(NSString *)aDomain;
+ (OWCookie *)cookieFromHeaderValue:(NSString *)headerValue defaultDomain:(NSString *)defaultDomain defaultPath:(NSString *)defaultPath;
- (void)locked_addApplicableCookies:(NSMutableArray *)cookies forPath:(NSString *)aPath urlIsSecure:(BOOL)secure;
+ (BOOL)locked_readOW3Cookies;
+ (BOOL)locked_readOW4Cookies;
- (id)initWithDomain:(NSString *)domain behavior:(OWCookieDomainBehavior)behavior;
- (void)beginPromptForCookie:(OWCookie *)cookie fromURL:(OWURL *)url withTarget:(id)target;
@end


@implementation OWCookieDomain

+ (void)initialize;
{
    OBINITIALIZE;

    domainLock = [[NSRecursiveLock alloc] init];
    
    endNameSet = [[NSCharacterSet characterSetWithCharactersInString:@"=;, \t\r\n"] retain];
    endDateSet = [[NSCharacterSet characterSetWithCharactersInString:@";\r\n"] retain];
    endNameValueSet = [[NSCharacterSet characterSetWithCharactersInString:@";\r\n"] retain];
    endValueSet = [[NSCharacterSet characterSetWithCharactersInString:@"; \t\r\n"] retain];
    endKeySet = [[NSCharacterSet characterSetWithCharactersInString:@"=;, \t\r\n"] retain];
    
    distantPastInterval = [[NSDate distantPast] timeIntervalSinceReferenceDate];
}

+ (void)didLoad;
{
    [[OFController sharedController] addObserver:self];    
}

+ (void)readDefaults;
{
    NSUserDefaults *userDefaults;
    NSString *value;
    
    [domainLock lock];

    userDefaults = [NSUserDefaults standardUserDefaults];

    // Upgrade our defaults -- check if the old default exists, and if so, what it was
    if ((value = [userDefaults objectForKey:@"OWHTTPRefuseAllCookies"])) {
        if ([userDefaults boolForKey:@"OWHTTPRefuseAllCookies"]) {
            [userDefaults setObject:[NSString stringWithCString:_stringForBehavior(OWCookieDomainRejectBehavior)] forKey:@"OWHTTPCookieDefaultBehavior"];
        }
        [userDefaults removeObjectForKey:@"OWHTTPRefuseAllCookies"];
        [userDefaults synchronize];
    }
    
    defaultBehavior = _behaviorForString([userDefaults stringForKey:@"OWHTTPCookieDefaultBehavior"]);

    [domainLock unlock];
}

+ (void)registerCookiesFromURL:(OWURL *)url headerValue:(NSString *)headerValue promptView:(id)promptView;
{
    NSString *defaultDomain, *defaultPath;
    OWCookie *cookie;
    OWCookieDomain *domain;
    OWCookieDomainBehavior behavior;

    if (url == nil)
        return;

    defaultDomain = [[url parsedNetLocation] hostname];
    defaultPath = @"/";

    // defaultDomain could easily be nil:  for example, this might be a file: URL
    // OBASSERT(defaultDomain != nil);

    if (OWCookiesDebug)
        NSLog(@"COOKIES: Register url=%@ domain=%@ path=%@ header=%@", [url shortDescription], defaultDomain, defaultPath, headerValue);

    cookie = [self cookieFromHeaderValue:headerValue defaultDomain:defaultDomain defaultPath:defaultPath];
    if (cookie == nil)
        return;
    
    // The cookie itself can specify a domain, so get the domain that ends up in the actual cookie instance.
    if (OWCookiesDebug)
        NSLog(@"COOKIES: url=%@, adding cookie = %@", [url shortDescription], cookie);
        
    domain = [self domainNamed:[cookie domain]];
    behavior = [domain behavior];
    if (OWCookiesDebug)
        NSLog(@"COOKIES: behavior is %s", _stringForBehavior(behavior));

    // We cannot prompt synchronously here since this can get called in the main thread now (and we want to allow multiple sheets to be used on multiple windows).  Instead, we'll register the cookie unconditionally, but set it to require confirmation.  Any threads that are talking to HTTP servers will need to wait for confirmation on cookies they find (and thus the background threads will block until the cookie is known to be good).
#warning TJW: This means that if we did allow a previous cookie then this cookie will unconditionally overwrite that cookie (and then possibly get removed).
    [cookie setRequiresConfirmation];
    [domain addCookie: cookie];
    if (behavior == OWCookieDomainPromptBehavior) {
        if (OWCookiesDebug)
            NSLog(@"COOKIES: Starting prompt for cookie %@", cookie);

        [domain beginPromptForCookie:cookie fromURL:url withTarget:promptView];
#warning TJW: finish should get called asynchronously from where?
    } else {
        // We can just finish the cookie now.  Set the scope to just this cookie (this will avoid changing the scope) since clearly we already have a domain scope accept/deny or global scope accept/deny in effect if we aren't prompting.
        OWCookiePromptResponse response;
        response.behavior = behavior;
        response.scope = OWCookieSingleBehaviorScope;
        [domain finishPromptForCookie:cookie domain:domain response: response];
    }
}

+ (void)registerCookiesFromPipeline:(OWPipeline *)pipeline headerValue:(NSString *)headerValue;
{
    OWURL *url;
    
    url = [(OWAddress *)[pipeline lastAddress] url];
    [self registerCookiesFromURL:url headerValue:headerValue promptView:[pipeline target]];
}

+ (void)registerCookiesFromPipeline:(OWPipeline *)pipeline headerDictionary:(OWHeaderDictionary *)headerDictionary;
{
    NSArray *valueArray;
    unsigned int valueIndex, valueCount;

    valueArray = [headerDictionary stringArrayForKey:OWSetCookieHeader];
    if (!valueArray)
	return;

    valueCount = [valueArray count];
    for (valueIndex = 0; valueIndex < valueCount; valueIndex++) {
        NSString *headerValue;
        
        headerValue = [valueArray objectAtIndex:valueIndex];
        [self registerCookiesFromPipeline:pipeline headerValue:headerValue];
    }
}

+ (NSArray *)cookiesForURL:(OWURL *)url awaitConfirmation: (BOOL) awaitConfirmation;
{
    NSMutableArray *cookies;
    NSArray *searchDomains;
    unsigned int domainIndex, domainCount;
    NSString *hostname, *searchDomain, *path;
    OWCookieDomain *domain;


    hostname = [[[url parsedNetLocation] hostname] lowercaseString];
    path = [@"/" stringByAppendingString:[url path]];
    searchDomains = [self searchDomainsForDomain:hostname];

    if (OWCookiesDebug)
        NSLog(@"COOKIES: url=%@ hostname=%@, path=%@ --> domains=%@", url, hostname, path, searchDomains);

    cookies = [NSMutableArray array];
    
    [domainLock lock];
    _locked_checkCookiesLoaded();
    
    domainCount = [searchDomains count];
    for (domainIndex = 0; domainIndex < domainCount; domainIndex++) {
        searchDomain = [searchDomains objectAtIndex:domainIndex];
        domain = [domainsByName objectForKey:searchDomain];
        [domain locked_addApplicableCookies:cookies forPath:path urlIsSecure:[url isSecure]];
    }
    
    [domainLock unlock];

    // Now that we have released the domain lock, if we need to wait for confirmation on cookies we can do it w/o deadlocking
    if (awaitConfirmation) {
        unsigned int cookieIndex;

        cookieIndex = [cookies count];
        while (cookieIndex--) {
            if (![[cookies objectAtIndex: cookieIndex] awaitConfirmation])
                [cookies removeObjectAtIndex: cookieIndex];
        }
    }
    
    if (OWCookiesDebug)
        NSLog(@"COOKIES: -cookiesForURL:%@ --> %@", [url shortDescription], cookies);

    return cookies;
}

+ (NSArray *)cookiesForURL:(OWURL *)url;
{
    return [self cookiesForURL:url awaitConfirmation:YES];
}

+ (void)didChange;
{
    [domainLock lock];
    [self locked_didChange];
    [domainLock unlock];
}

+ (NSArray *)allDomains;
{
    NSArray *domains;
    
    [domainLock lock];
    _locked_checkCookiesLoaded();
    
    domains = [[domainsByName allValues] sortedArrayUsingSelector:@selector(compare:)];
    
    [domainLock unlock];
    
    return domains;
}

+ (OWCookieDomain *)domainNamed:(NSString *)name;
{
    return [self domainNamed:name behavior:OWCookieDomainDefaultBehavior andNotify:YES];
}

+ (void)deleteDomain:(OWCookieDomain *)domain;
{
    [domainLock lock];
    _locked_checkCookiesLoaded();
    
    [domainsByName removeObjectForKey:[domain name]];
    [self locked_didChange];
    
    [domainLock unlock];
}

+ (void)deleteCookie:(OWCookie *)cookie;
{
    OWCookieDomain *domain;
    
    [domainLock lock];
    _locked_checkCookiesLoaded();
    
    // Its domain might have been deleted already which is why this method exists -- the caller can't call +domainNamed: and delete the cookie from there since that might recreate a deleted domain.

    domain = [domainsByName objectForKey:[cookie domain]];
    [domain removeCookie:cookie];
    
    [domainLock unlock];
}

+ (void)setDelegate:(id)delegate;
{
    classDelegate = delegate;
}

+ (id)delegate;
{
    return classDelegate;
}

+ (OWCookieDomainBehavior)defaultBehavior;
{
    return defaultBehavior;
}

+ (void)setDefaultBehavior:(OWCookieDomainBehavior)behavior;
{
    NSUserDefaults *defaults;
    NSString *string;
    
    defaultBehavior = behavior;
    string = [NSString stringWithCString:_stringForBehavior(defaultBehavior)];
    defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:string forKey:@"OWHTTPCookieDefaultBehavior"];
    [defaults synchronize];
}

- (NSString *)name;
{
    return _name;
}

- (NSString *)stringValue;
{
    return _name;
}

- (OWCookieDomainBehavior)internalBehavior;
{
    return _behavior;
}


- (void)setInternalBehavior:(OWCookieDomainBehavior)behavior;
{
    // Don't really need this lock for the ivar set, but do for the method call
    [domainLock lock];
    _behavior = behavior;
    [isa locked_didChange];
    [domainLock unlock];
}

- (OWCookieDomainBehavior)behavior;
{
    if (_behavior == OWCookieDomainDefaultBehavior)
        return defaultBehavior;
    return _behavior;
}

- (NSArray *)paths;
{
    NSArray *paths;
    
    [domainLock lock];
    paths = [[NSArray alloc] initWithArray:_cookiePaths];
    [domainLock unlock];
    
    return [paths autorelease];
}

- (OWCookiePath *)pathNamed:(NSString *)pathName;
{
    OWCookiePath *path;
    
    [domainLock lock];
    path = [[self locked_pathNamed:pathName shouldCreate:YES] retain];
    [domainLock unlock];
    
    return [path autorelease];
}

//
// Prompt response
//
- (void)finishPromptForCookie:(OWCookie *)cookie domain: (OWCookieDomain *) domain response: (OWCookiePromptResponse) response;
{
    BOOL shouldAccept;
    
    if (OWCookiesDebug)
        NSLog(@"COOKIES: Finish cookie prompt behavior %s, scope %s, cookie %@", _stringForBehavior(response.behavior), _stringForScope(response.scope), cookie);
        
    if (response.scope == OWCookieDomainBehaviorScope)
        [self setInternalBehavior:response.behavior];
    else if (response.scope == OWCookieAllDomainsBehaviorScope)
        [OWCookieDomain setDefaultBehavior:response.behavior];
        
    switch (response.behavior) {
        case OWCookieDomainAcceptBehavior:
            shouldAccept = YES;
            break;
        case OWCookieDomainAcceptForSessionBehavior:
            shouldAccept = YES;
            [cookie expireAtEndOfSession];
            break;
        case OWCookieDomainRejectBehavior:
            shouldAccept = NO;
            break;
        case OWCookieDomainPromptBehavior:
        default:
            // shouldn't get here
            OBASSERT(NO);
            shouldAccept = YES;
            break;
    }
    
    if (!shouldAccept)
        [domain removeCookie: cookie];
        
    // Unblock any threads waiting to find out if the user likes this cookie or not
    [cookie setConfirmationState: shouldAccept];
}

//
// Saving
//

- (void)appendXML:(OFDataBuffer *)xmlBuffer;
{
    unsigned int pathIndex, pathCount;
    unsigned int cookieIndex, cookieCount;
    NSMutableArray *cookies;
    
    cookies = [NSMutableArray array];
    
    [domainLock lock];

    // The paths are not represented in the XML file (since they are usually the default and there are usually few enough cookies per path that it would be a waste.
    pathCount = [_cookiePaths count];
    for (pathIndex = 0; pathIndex < pathCount; pathIndex++) {
        OWCookiePath *path;
        
        path = [_cookiePaths objectAtIndex:pathIndex];
        // Don't wait for confirmation here since we are in the main thread and the confirmation must happen in the main thread.  If the cookie is denied, this will cause an update to the domain and the cookies will get saved again later.
        [path addNonExpiredCookiesToArray:cookies usageIsSecure:YES];
    }
    
    // Don't archive domains with zero cookies and a default behavior.
    cookieCount = [cookies count];
    if (cookieCount || _behavior != OWCookieDomainDefaultBehavior) {
        OFDataBufferAppendCString(xmlBuffer, "<domain name=\"");
        // This *shouldn't* have entities in it, but ...
        OFDataBufferAppendXMLQuotedString(xmlBuffer, (CFStringRef)_name);
        if (_behavior != OWCookieDomainDefaultBehavior) {
            OFDataBufferAppendCString(xmlBuffer, "\" behavior=\"");
            OFDataBufferAppendCString(xmlBuffer, _stringForBehavior(_behavior));
        }
        OFDataBufferAppendCString(xmlBuffer, "\">\n");
    
        for (cookieIndex = 0; cookieIndex < cookieCount; cookieIndex++) {
            OWCookie *cookie;
            
            // -addNonExpiredCookiesToArray:usageIsSecure: filters out the cookies with expiration dates that have passed, but it does NOT filter out cookies with no expiration date (those that expire at the end of the session, and thus should not be written out). This extra filtering here could cause us to write a domain with zero cookies, but that will be fixed the next time we write the file, most likely.
            cookie = [cookies objectAtIndex:cookieIndex];
            if ([cookie expirationDate])
                [cookie appendXML:xmlBuffer];
        }
        
        OFDataBufferAppendCString(xmlBuffer, "</domain>\n");
    }
    
    [domainLock unlock];
}

//
// Convenience methods that loop over all the paths
//

- (void)addCookie:(OWCookie *)cookie;
{
    [self addCookie:cookie andNotify:YES];
}

- (void)removeCookie:(OWCookie *)cookie;
{
    OWCookiePath *path;
    
    [domainLock lock];
    path = [self locked_pathNamed:[cookie path] shouldCreate:NO];
    [path removeCookie:cookie];
    [domainLock unlock];
}

- (NSArray *)cookies;
{
    NSMutableArray *cookies;
    unsigned int pathIndex, pathCount;
    
    cookies = [NSMutableArray array];
    [domainLock lock];
    pathCount = [_cookiePaths count];
    for (pathIndex = 0; pathIndex < pathCount; pathIndex++)
        // Don't await confirmation here -- this is called by UI code that runs in the main thread.
        [[_cookiePaths objectAtIndex:pathIndex] addNonExpiredCookiesToArray:cookies usageIsSecure:YES];
    [domainLock unlock];
    
    return cookies;
}

- (NSComparisonResult)compare:(id)otherObject;
{
    NSString *otherName;
    NSComparisonResult domainComparisonResult;

    if (![otherObject isKindOfClass:isa])
        return NSOrderedAscending;

    otherName = [(OWCookieDomain *)otherObject name];
    domainComparisonResult = [[OWURL domainForHostname:_name] compare:[OWURL domainForHostname:otherName]];
    switch (domainComparisonResult) {
        case NSOrderedSame:
            return [_name compare:otherName];
        default:
            return domainComparisonResult;
    }
}

//
//  NSCopying protocol (so this can go in table view columns like in the OW cookies inspector)
//

- (id)copyWithZone:(NSZone *)zone;
{
    return [self retain];
}

@end



@implementation OWCookieDomain (PrivateAPI)

+ (void)controllerDidInitialize:(OFController *)controller;
{
    [self readDefaults];
    
    [domainLock lock];
    
    domainsByName = [[NSMutableDictionary alloc] init];
    
    // Read the cookies, prefering the newest file is it exists
    if (![self locked_readOW4Cookies]) {
        if ([self locked_readOW3Cookies]) {
            // If we got an old file, make sure we save a new file immediately
            [self saveCookies];
        }
    }

    [domainLock unlock];
    
    if (OWCookiesDebug)
        NSLog(@"COOKIES: Read cookies");
}

+ (void)controllerWillTerminate:(OFController *)controller;
{
    [self saveCookies];
}

+ (void)saveCookies;
{
    NSString *cookieFilename;
    NSArray *domains;
    OFDataBuffer xmlBuffer;
    unsigned int domainIndex, domainCount;
    NSDictionary *attributes;
    
    // This must get executed in the main thread so that the notification gets posted in the main thread (since that is where the cookie preferences panel is listening).
    OBPRECONDITION([NSThread inMainThread]);

    if (OWCookiesDebug)
        NSLog(@"COOKIES: Saving");

    if (!(cookieFilename = [self cookiePath:OW4CookieFileName])) {
        if (OWCookiesDebug)
            NSLog(@"COOKIES: Unable to compute cookie path");
        return;
    }

    [domainLock lock];
    
    [saveEvent release];
    saveEvent = nil;
    
    OFDataBufferInit(&xmlBuffer);
#warning TJW -- I still need to write a DTD for this file and put it on our web site
    OFDataBufferAppendCString(&xmlBuffer,
    "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
    "<!DOCTYPE OmniWebCookies SYSTEM \"http://www.omnigroup.com/DTDs/OmniWebCookies.dtd\">\n"
    "<OmniWebCookies>\n");

    domains = [[domainsByName allValues] sortedArrayUsingSelector:@selector(compare:)];
    domainCount = [domains count];
    for (domainIndex = 0; domainIndex < domainCount; domainIndex++)
        [[domains objectAtIndex:domainIndex] appendXML:&xmlBuffer];

    OFDataBufferAppendCString(&xmlBuffer, "</OmniWebCookies>\n");

    // Cookies must only be readable by the owner since they can contain
    // security sensitive information
    attributes = [NSDictionary dictionaryWithObjectsAndKeys:
            [NSNumber numberWithUnsignedLong: 0600], NSFilePosixPermissions,
            nil];
            
    OFDataBufferFlush(&xmlBuffer);
    if (![[NSFileManager defaultManager] atomicallyCreateFileAtPath:cookieFilename contents:OFDataBufferData(&xmlBuffer) attributes: attributes]) {
#warning TJW: There is not currently any good way to pop up a panel telling the user that they need to check the file permissions for a particular path.
        NSLog(@"Unable to save cookies to %@", cookieFilename);
    }
    
    OFDataBufferRelease(&xmlBuffer);
    
    [domainLock unlock];
}

+ (NSString *)cookiePath:(NSString *)fileName;
{
    NSString *directory;
    NSFileManager *fileManager;
    BOOL isDirectory;

    directory = [[[NSUserDefaults standardUserDefaults] objectForKey:@"OWLibraryDirectory"] stringByStandardizingPath];
    fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory)
        return nil;

    return [directory stringByAppendingPathComponent:fileName];
}

+ (void)locked_didChange;
{
    OFScheduler *mainScheduler;
    
    mainScheduler = [OFScheduler mainScheduler];
    
    // Kill the old scheduled event and schedule one for later
    if (saveEvent) {
        [mainScheduler abortEvent:saveEvent];
        [saveEvent release];
    }

    saveEvent = [[mainScheduler scheduleSelector:@selector(saveCookies) onObject:self withObject:nil afterTime:60.0] retain];

    if (OWCookiesDebug)
        NSLog(@"COOKIES: Did change, saveEvent = %@", saveEvent);
        
    [self queueSelectorOnce:@selector(notifyCookiesChanged)];
}

+ (void)notifyCookiesChanged;
{
    [[NSNotificationCenter defaultCenter] postNotificationName:OWCookiesChangedNotification object:nil];
}

+ (OWCookieDomain *)domainNamed:(NSString *)name                         behavior:(OWCookieDomainBehavior)behavior                        andNotify:(BOOL)shouldNotify;
{
    OWCookieDomain *domain;
    
    [domainLock lock];
    _locked_checkCookiesLoaded();
    
    if (!(domain = [domainsByName objectForKey:name])) {
        domain = [[self alloc] initWithDomain:name behavior:behavior];
        [domainsByName setObject:domain forKey:name];
        [domain release];
        if (shouldNotify)
            [self locked_didChange];
    }
    
    [domainLock unlock];
    
    return domain;
}

- (void)addCookie:(OWCookie *)cookie andNotify:(BOOL)shouldNotify;
{
    OWCookiePath *path;
    
    [domainLock lock];
    path = [self locked_pathNamed:[cookie path] shouldCreate:YES];
    [path addCookie:cookie andNotify:shouldNotify];
    [domainLock unlock];
}

- (OWCookiePath *)locked_pathNamed:(NSString *)pathName shouldCreate:(BOOL)shouldCreate;
{
    unsigned int pathIndex;
    OWCookiePath *path;
    
    [domainLock lock];

    pathIndex = [_cookiePaths count];
    while (pathIndex--) {
        path = [_cookiePaths objectAtIndex:pathIndex];
        if ([[path path] isEqualToString:pathName]) {
            [path retain];
            goto found;
        }
    }

    if (shouldCreate) {
        path = [[OWCookiePath alloc] initWithPath:pathName];
        [_cookiePaths insertObject:path inArraySortedUsingSelector:@selector(compare:)];
    } else
        path = nil;

found:
    [domainLock unlock];
    
    return [path autorelease];
}

+ (NSArray *)searchDomainsForDomain:(NSString *)aDomain;
{
    NSMutableArray *searchDomains;
    NSMutableArray *domainComponents;
    unsigned int domainComponentCount;
    unsigned int minimumDomainComponents;

    if (aDomain == nil)
        return nil;
    domainComponents = [[aDomain componentsSeparatedByString:@"."] mutableCopy];
    domainComponentCount = [domainComponents count];
    minimumDomainComponents = [OWURL minimumDomainComponentsForTopLevelDomain:[domainComponents lastObject]];
    searchDomains = [NSMutableArray arrayWithCapacity:domainComponentCount];
    [searchDomains addObject:aDomain];
    if (domainComponentCount < minimumDomainComponents) {
        [domainComponents release];
	return searchDomains;
    }
    domainComponentCount -= minimumDomainComponents;
    while (domainComponentCount--) {
	NSString *searchDomain;

	[domainComponents removeObjectAtIndex:0];
	searchDomain = [domainComponents componentsJoinedByString:@"."];
	[searchDomains addObject:[@"." stringByAppendingString:searchDomain]];
	[searchDomains addObject:searchDomain];
    }
    [domainComponents release];
    return searchDomains;
}

+ (OWCookie *)cookieFromHeaderValue:(NSString *)headerValue defaultDomain:(NSString *)defaultDomain defaultPath:(NSString *)defaultPath;
{
    NSString *aName, *aValue;
    NSDate *aDate = nil;
    NSString *aDomain = defaultDomain, *aPath = defaultPath;
    BOOL isSecure = NO;
    NSScanner *scanner;
    NSString *aKey;

    scanner = [NSScanner scannerWithString:headerValue];
    if (![scanner scanUpToCharactersFromSet:endNameSet intoString:&aName])
        aName = [NSString string];
    
    if (![scanner scanString:@"=" intoString:NULL])
        return nil;

    // Scan the value if possible
    if ([scanner scanUpToCharactersFromSet:endNameValueSet intoString:&aValue]) {
        unsigned int valueLength;
        // Remove trailing whitespace
        // This could be more efficient.  (Actually, this whole method could be more efficient:  we should rewrite it using OFStringScanner.)

        valueLength = [aValue length];
        do {
            unichar character;
            
            character = [aValue characterAtIndex:valueLength - 1];
            if (character == ' ' || character == '\t')
                valueLength--;
            else
                break;
        } while (valueLength > 0);
        aValue = [aValue substringToIndex:valueLength];
    } else {
        // If there are no characters, treat it as an empty string.
        aValue = [NSString string];
    }

    [scanner scanCharactersFromSet:endKeySet intoString:NULL];
    while ([scanner scanUpToCharactersFromSet:endKeySet intoString:&aKey]) {
        aKey = [aKey lowercaseString];
        [scanner scanString:@"=" intoString:NULL];
        if ([aKey isEqualToString:@"expires"]) {
            NSString *dateString = nil;

            [scanner scanUpToCharactersFromSet:endDateSet intoString:&dateString];
            if (dateString) {
                aDate = [NSDate dateWithHTTPDateString:dateString];
                if (!aDate) {
                    NSCalendarDate *yearFromNowDate;

                    NSLog(@"OWCookie: could not parse expiration date, expiring cookie in one year");
                    yearFromNowDate = [[NSCalendarDate calendarDate] dateByAddingYears:1 months:0 days:0 hours:0 minutes:0 seconds:0];
                    [yearFromNowDate setCalendarFormat:[OWHTTPSession preferredDateFormat]];
                    aDate = yearFromNowDate;
                }
            }
        } else if ([aKey isEqualToString:@"domain"]) {
            [scanner scanUpToCharactersFromSet:endValueSet intoString:&aDomain];
            if (aDomain != nil) {
                NSArray *domainComponents;
                unsigned int domainComponentCount;

                domainComponents = [aDomain componentsSeparatedByString:@"."];
                domainComponentCount = [domainComponents count];
                if (domainComponentCount > 0 && [[domainComponents objectAtIndex:0] isEqualToString:@""]) {
                    // ".co.uk" -> ("", "co", "uk"):  we shouldn't count that initial empty component
                    domainComponentCount--;
                }
                
                if (OWCookiesDebug)
                    NSLog(@"COOKIES: domainComponents = %@, minimum = %d", domainComponents, [OWURL minimumDomainComponentsForTopLevelDomain:[domainComponents lastObject]]);

                if (defaultDomain && (![defaultDomain hasSuffix:aDomain] || domainComponentCount < [OWURL minimumDomainComponentsForTopLevelDomain:[domainComponents lastObject]])) {
                    // Sorry, you can't create cookies for other domains, nor can you create cookies for "com" or "co.uk".  Make sure that we allow for the case where there is no default domain (file: urls, for example).
                    aDomain = defaultDomain;
                }
            }
        } else if ([aKey isEqualToString:@"path"]) {
            if (![scanner scanUpToCharactersFromSet:endValueSet intoString:&aPath]) {
                // Some deranged people specify an empty string for the path. Assume they really meant "/" (not the default path, which is more limiting).
                aPath = @"/";
            }
        } else if ([aKey isEqualToString:@"secure"]) {
            isSecure = YES;
        }
        [scanner scanCharactersFromSet:endKeySet intoString:NULL];
    }
    
    return [[[OWCookie alloc] initWithDomain:aDomain path:aPath name:aName value:aValue expirationDate:aDate secure:isSecure] autorelease];
}

- (void)locked_addApplicableCookies:(NSMutableArray *)cookies forPath:(NSString *)aPath urlIsSecure:(BOOL)secure;
{
    unsigned int pathIndex;
    OWCookiePath *path;
    
    pathIndex = [_cookiePaths count];
    while (pathIndex--) {
        path = [_cookiePaths objectAtIndex:pathIndex];
        if (![path appliesToPath:aPath])
            continue;
        
        [path addNonExpiredCookiesToArray:cookies usageIsSecure:secure];
    }
}
    
+ (BOOL)locked_readOW3Cookies;
{
    NSMutableDictionary *registrationDict;
    NSEnumerator *domainEnumerator;
    NSString *aDomain;
    BOOL success;
    NSString *cookieFilename;
    
    cookieFilename = [self cookiePath:OW3CookieFileName];
    
    if (!cookieFilename)
        return NO;

    [domainLock lock];
    
    NS_DURING {
        registrationDict = [[NSMutableDictionary alloc] initWithContentsOfFile:cookieFilename];
        domainEnumerator = [registrationDict keyEnumerator];
        while ((aDomain = [domainEnumerator nextObject])) {
            NSString *aPath;
            NSEnumerator *pathEnumerator;
            NSMutableDictionary *domainDict;

            domainDict = [registrationDict objectForKey:aDomain];
            pathEnumerator = [domainDict keyEnumerator];
            while ((aPath = [pathEnumerator nextObject])) {
                NSString *aName;
                NSEnumerator *nameEnumerator;
                NSMutableDictionary *pathDict;

                // Convert old style immunizations to new domain behaviors
                if ([aPath isEqualToString:OWCookieImmunizedDomainPath]) {
                    OWCookieDomain *domain;
                    
                    domain = [self domainNamed:aDomain behavior:OWCookieDomainDefaultBehavior andNotify:NO];
                    [domain setInternalBehavior:OWCookieDomainRejectBehavior];
                    continue;
                }
                
                pathDict = [domainDict objectForKey:aPath];
                nameEnumerator = [pathDict keyEnumerator];
                while ((aName = [nameEnumerator nextObject])) {
                    OWCookie *cookie;
                    OWCookieDomain *domain;
                    NSString *value, *expirationString;
                    NSDate *date;
                    BOOL secure;
                    NSDictionary *dict;
                    
                    // Don't notify on this registration since we aren't dirty here.
                    // We've inlined the OW3 cookie unarchiving logic here which is
                    // somewhat different from the current stuff (which is in OWCookie itself).
                    dict = [pathDict objectForKey:aName];
                    value = [dict objectForKey:@"value"];
                    expirationString = [dict objectForKey:@"expirationTime"];
                    
                    if (expirationString) {
                        NSTimeInterval timeInterval;
                
                        timeInterval = [expirationString doubleValue];
                        date = [NSCalendarDate dateWithTimeIntervalSinceReferenceDate:timeInterval];
                    } else {
                        // Cookies with no specified expiration were supposed to have
                        // expired before being saved.
                        continue;
                    }
                    
                    secure = [[dict objectForKey:@"secure"] isEqualToString:@"YES"];
                    cookie = [[OWCookie alloc] initWithDomain:aDomain path:aPath name:aName value:value expirationDate:date secure:secure];
                                        
                    domain = [self domainNamed:[cookie domain]];
                    if (OWCookiesDebug)
                        NSLog(@"COOKIES: Reading OW3, Domain = %@, adding cookie = %@", [cookie domain], cookie);
                    [domain addCookie:cookie andNotify:NO];
                    [cookie release];
                }
            }
        }
        success = YES;
    } NS_HANDLER {
        // No saved cookies
        registrationDict = nil;
        success = NO;
    } NS_ENDHANDLER;
    [registrationDict release];
    
    [domainLock unlock];
    
    return success;
}

//
// OW4.x XML Cookie file parsing
//


typedef struct _OW4CookieParseInfo {
    OWCookieDomain *domain;
    NSTimeInterval  parseTime;
} OW4CookieParseInfo;



// Under DP4 CFXMLParser does not resolve character entities (like &#60;).  This
// means that it is not easy to read and write files that contain characters that
// are supposed to be quoted --  &<>"' 
// We could post process the strings read from CFXMLParser to resolve the quoted
// characters, but for now I'll just use expat (in the OmniExpat framework).
//
// There is CFXMLParser-based implmenetation of locked_readOW4Cookies in version 1.1 of this file.

void startElement(void *userData,
                  const XML_Char *name,
		  const XML_Char **atts)
{
#if 0
    unsigned int attIndex = 0;
    
    printf("start %s", name);
    while (atts[attIndex])
        printf(" %s", atts[attIndex++]);
    printf("\n");
#endif

    if (strcmp(name, "domain") == 0) {
        OWCookieDomain *domain;
        OWCookieDomainBehavior behavior = OWCookieDomainDefaultBehavior;
        NSString *name = nil;
        
        while (*atts) {
            if (strcmp(*atts, "name") == 0)
                name = [[NSString alloc] initWithUTF8String:*(++atts)];
            else if (strcmp(*atts, "behavior") == 0)
                behavior = _behaviorForCString(*(++atts));
            atts++;
        }
        
        if (!name)
            // Invalid
            domain = nil;
        else
            domain = [OWCookieDomain domainNamed:name behavior:behavior andNotify:NO];
            
        [name release];
        ((OW4CookieParseInfo *)userData)->domain = domain;
    } else if (strcmp(name, "cookie") == 0) {
        OWCookie *cookie;
        OWCookieDomain *domain;
        NSString *path = OWCookieGlobalPath, *name = nil, *value = nil;
        NSTimeInterval expireInterval = distantPastInterval;
        NSDate *expires;
        BOOL secure = NO;
        
        domain = ((OW4CookieParseInfo *)userData)->domain;
        if (!domain)
            // Cookie elements must be inside a domain element
            return;
            
        while (*atts) {
            if (strcmp(*atts, "name") == 0)
                name = [[NSString alloc] initWithUTF8String:*(++atts)];
            else if (strcmp(*atts, "path") == 0)
                path = [[NSString alloc] initWithUTF8String:*(++atts)];
            else if (strcmp(*atts, "value") == 0)
                value = [[NSString alloc] initWithUTF8String:*(++atts)];
            else if (strcmp(*atts, "expires") == 0)
                expireInterval = atof(*(++atts));
            else if (strcmp(*atts, "secure") == 0)
                secure = strcmp(*(++atts), "YES") == 0;
            atts++;
        }
        
        // Cookies without names are invalid.  Also, skip cookies that have expired.
        if (name && expireInterval > ((OW4CookieParseInfo *)userData)->parseTime) {
            expires = [[NSDate alloc] initWithTimeIntervalSinceReferenceDate:expireInterval];
            cookie = [[OWCookie alloc] initWithDomain:[domain name]                                                  path:path                                                  name:name                                                 value:value                                        expirationDate:expires                                                secure:secure];

            [name release];
            [path release];
            [value release];
            [expires release];

            [domain addCookie:cookie andNotify:NO];
            [cookie release];
        }
        
    } else {
        // Ignore it
    }
}

void endElement(void *userData,
                const XML_Char *name)
{
#if 0
    printf("end %s\n", name);
#endif
    
    if (strcmp(name, "domain") == 0)
        ((OW4CookieParseInfo *)userData)->domain = nil;
}

+ (BOOL)locked_readOW4Cookies;
{
    NSString *filename;
    NSData *xmlData;
    XML_Parser parser;
    OW4CookieParseInfo info;
    
    filename = [self cookiePath:OW4CookieFileName];
    xmlData = [[[NSData alloc] initWithContentsOfFile:filename] autorelease];
    if (!xmlData)
        return NO;
        
    memset(&info, 0, sizeof(info));
    info.parseTime = [NSDate timeIntervalSinceReferenceDate];
    
    parser = XML_ParserCreate("UTF-8");
    XML_SetElementHandler(parser, startElement, endElement);
    XML_SetUserData(parser, &info);
    
    // Pass the entire file in one buffer, passing '1' to indicate that this is the last buffer
    if (!XML_Parse(parser, [xmlData bytes], [xmlData length], 1)) {
        NSLog(@"OWCookieDomain XML Parse: XML_Parse returned %d, %s, Line: %d, Column: %d",
                XML_GetErrorCode(parser),
                XML_ErrorString(XML_GetErrorCode(parser)),
                XML_GetCurrentLineNumber(parser),
                XML_GetCurrentColumnNumber(parser));
        return NO;
    }
    
    return YES;
}


- (id)initWithDomain:(NSString *)domain behavior:(OWCookieDomainBehavior)behavior;
{
    _name = [domain copy];
    _cookiePaths = [[NSMutableArray alloc] init];
    _behavior = behavior;
    
    return self;
}

- (void)dealloc;
{
    [_name release];
    [_cookiePaths release];
    [super dealloc];
}

- (void)beginPromptForCookie:(OWCookie *)cookie fromURL:(OWURL *)url withTarget:(id)target;
{
    OWCookieDomainBehavior behavior;
    OWCookiePromptResponse response;
    
    if (!classDelegate) {
        // Just immediately finish the addition of the cookie using the default behavior.
        response.behavior = (defaultBehavior != OWCookieDomainPromptBehavior) ? defaultBehavior : OWCookieDomainAcceptBehavior;
        response.scope = OWCookieSingleBehaviorScope;
        if (OWCookiesDebug)
            NSLog(@"COOKIES: No delegate -- will %s", _stringForBehavior(behavior));
        [self finishPromptForCookie:cookie domain:self response: response];
        return;
    }

#warning TJW: Make sure that the OWCookiePrompt will only present a single sheet per window and will not present the sheet if the domain behavior is not to prompt (if you get two cookies in a row and accept the first with a scope of the whole domain).

    NS_DURING {
        [classDelegate cookieDomain:self promptResponseForCookie:cookie fromURL:url withTarget:target];
    } NS_HANDLER {
        NSLog(@"Ignoring exception raised while prompting for cookie behavior: %@", localException);
        NSLog(@"Will accept the cookie");
        response.scope = OWCookieSingleBehaviorScope;
        response.behavior = OWCookieDomainAcceptBehavior;
        [self finishPromptForCookie:cookie domain:self response: response];
    } NS_ENDHANDLER;
}


- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *dict;
    
    dict = [super debugDictionary];
    [dict setObject:_name forKey:@"name"];
    [dict setObject:[NSString stringWithCString:_stringForBehavior(_behavior)] forKey:@"behavior"];
    [dict setObject:_cookiePaths forKey:@"cookiePaths"];
    
    return dict;
}

@end

