webengine/osswebengine/WebKit/History/WebHistory.mm
changeset 0 dd21522fd290
equal deleted inserted replaced
-1:000000000000 0:dd21522fd290
       
     1 /*
       
     2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
       
     3  *
       
     4  * Redistribution and use in source and binary forms, with or without
       
     5  * modification, are permitted provided that the following conditions
       
     6  * are met:
       
     7  *
       
     8  * 1.  Redistributions of source code must retain the above copyright
       
     9  *     notice, this list of conditions and the following disclaimer. 
       
    10  * 2.  Redistributions in binary form must reproduce the above copyright
       
    11  *     notice, this list of conditions and the following disclaimer in the
       
    12  *     documentation and/or other materials provided with the distribution. 
       
    13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
       
    14  *     its contributors may be used to endorse or promote products derived
       
    15  *     from this software without specific prior written permission. 
       
    16  *
       
    17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
       
    18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
    19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
       
    20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
       
    21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
       
    22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
       
    23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
       
    24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
       
    26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    27  */
       
    28 
       
    29 #import "WebHistory.h"
       
    30 #import "WebHistoryPrivate.h"
       
    31 
       
    32 #import "WebHistoryItem.h"
       
    33 #import "WebHistoryItemInternal.h"
       
    34 #import "WebHistoryItemPrivate.h"
       
    35 #import "WebKitLogging.h"
       
    36 #import "WebNSURLExtras.h"
       
    37 #import <Foundation/NSError.h>
       
    38 #import <JavaScriptCore/Assertions.h>
       
    39 #import <WebCore/WebCoreHistory.h>
       
    40 #import <wtf/Vector.h>
       
    41 
       
    42 
       
    43 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
       
    44 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
       
    45 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
       
    46 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
       
    47 NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification";
       
    48 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
       
    49 NSString *WebHistoryItemsKey = @"WebHistoryItems";
       
    50 
       
    51 static WebHistory *_sharedHistory = nil;
       
    52 
       
    53 
       
    54 
       
    55 NSString *FileVersionKey = @"WebHistoryFileVersion";
       
    56 NSString *DatesArrayKey = @"WebHistoryDates";
       
    57 
       
    58 #define currentFileVersion 1
       
    59 
       
    60 @implementation WebHistoryPrivate
       
    61 
       
    62 #pragma mark OBJECT FRAMEWORK
       
    63 
       
    64 + (void)initialize
       
    65 {
       
    66     [[NSUserDefaults standardUserDefaults] registerDefaults:
       
    67         [NSDictionary dictionaryWithObjectsAndKeys:
       
    68             @"1000", @"WebKitHistoryItemLimit",
       
    69             @"7", @"WebKitHistoryAgeInDaysLimit",
       
    70             nil]];    
       
    71 }
       
    72 
       
    73 - (id)init
       
    74 {
       
    75     if (![super init]) {
       
    76         return nil;
       
    77     }
       
    78     
       
    79     _entriesByURL = [[NSMutableDictionary alloc] init];
       
    80     _entriesByDate = new DateToEntriesMap;
       
    81 
       
    82     return self;
       
    83 }
       
    84 
       
    85 - (void)dealloc
       
    86 {
       
    87     [_entriesByURL release];
       
    88     [_orderedLastVisitedDays release];
       
    89     delete _entriesByDate;
       
    90     
       
    91     [super dealloc];
       
    92 }
       
    93 
       
    94 - (void)finalize
       
    95 {
       
    96     delete _entriesByDate;
       
    97     [super finalize];
       
    98 }
       
    99 
       
   100 #pragma mark MODIFYING CONTENTS
       
   101 
       
   102 WebHistoryDateKey timeIntervalForBeginningOfDay(NSTimeInterval interval)
       
   103 {
       
   104     CFTimeZoneRef timeZone = CFTimeZoneCopyDefault();
       
   105     CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(interval, timeZone);
       
   106     date.hour = 0;
       
   107     date.minute = 0;
       
   108     date.second = 0;
       
   109     NSTimeInterval result = CFGregorianDateGetAbsoluteTime(date, timeZone);
       
   110     CFRelease(timeZone);
       
   111 
       
   112     // Converting from double to int64_t is safe here as NSDate's useful range
       
   113     // is -2**48 .. 2**47 which will safely fit in an int64_t.
       
   114     return (WebHistoryDateKey)result;
       
   115 }
       
   116 
       
   117 // Returns whether the day is already in the list of days,
       
   118 // and fills in *key with the key used to access its location
       
   119 - (BOOL)findKey:(WebHistoryDateKey*)key forDay:(NSTimeInterval)date
       
   120 {
       
   121     ASSERT_ARG(key, key != nil);
       
   122     *key = timeIntervalForBeginningOfDay(date);
       
   123     return _entriesByDate->contains(*key);
       
   124 }
       
   125 
       
   126 - (void)insertItem:(WebHistoryItem *)entry forDateKey:(WebHistoryDateKey)dateKey
       
   127 {
       
   128     ASSERT_ARG(entry, entry != nil);
       
   129     ASSERT(_entriesByDate->contains(dateKey));
       
   130 
       
   131     NSMutableArray *entriesForDate = _entriesByDate->get(dateKey).get();
       
   132     NSTimeInterval entryDate = [entry lastVisitedTimeInterval];
       
   133 
       
   134     unsigned count = [entriesForDate count];
       
   135 
       
   136     // The entries for each day are stored in a sorted array with the most recent entry first
       
   137     // Check for the common cases of the entry being newer than all existing entries or the first entry of the day
       
   138     if (!count || [[entriesForDate objectAtIndex:0] lastVisitedTimeInterval] < entryDate) {
       
   139         [entriesForDate insertObject:entry atIndex:0];
       
   140         return;
       
   141     }
       
   142     // .. or older than all existing entries
       
   143     if (count > 0 && [[entriesForDate objectAtIndex:count - 1] lastVisitedTimeInterval] >= entryDate) {
       
   144         [entriesForDate insertObject:entry atIndex:count];
       
   145         return;
       
   146     }
       
   147 
       
   148     unsigned low = 0;
       
   149     unsigned high = count;
       
   150     while (low < high) {
       
   151         unsigned mid = low + (high - low) / 2;
       
   152         if ([[entriesForDate objectAtIndex:mid] lastVisitedTimeInterval] >= entryDate)
       
   153             low = mid + 1;
       
   154         else
       
   155             high = mid;
       
   156     }
       
   157 
       
   158     // low is now the index of the first entry that is older than entryDate
       
   159     [entriesForDate insertObject:entry atIndex:low];
       
   160 }
       
   161 
       
   162 - (BOOL)_removeItemFromDateCaches:(WebHistoryItem *)entry
       
   163 {
       
   164     WebHistoryDateKey dateKey;
       
   165     BOOL foundDate = [self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]];
       
   166  
       
   167     if (!foundDate)
       
   168         return NO;
       
   169 
       
   170     DateToEntriesMap::iterator it = _entriesByDate->find(dateKey);
       
   171     NSMutableArray *entriesForDate = it->second.get();
       
   172     [entriesForDate removeObjectIdenticalTo:entry];
       
   173     
       
   174     // remove this date entirely if there are no other entries on it
       
   175     if ([entriesForDate count] == 0) {
       
   176         _entriesByDate->remove(it);
       
   177         // Clear _orderedLastVisitedDays so it will be regenerated when next requested.
       
   178         [_orderedLastVisitedDays release];
       
   179         _orderedLastVisitedDays = nil;
       
   180     }
       
   181     
       
   182     return YES;
       
   183 }
       
   184 
       
   185 - (BOOL)removeItemForURLString: (NSString *)URLString
       
   186 {
       
   187     WebHistoryItem *entry = [_entriesByURL objectForKey: URLString];
       
   188     if (entry == nil) {
       
   189         return NO;
       
   190     }
       
   191 
       
   192     [_entriesByURL removeObjectForKey: URLString];
       
   193     
       
   194 #if ASSERT_DISABLED
       
   195     [self _removeItemFromDateCaches:entry];
       
   196 #else
       
   197     BOOL itemWasInDateCaches = [self _removeItemFromDateCaches:entry];
       
   198     ASSERT(itemWasInDateCaches);
       
   199 #endif
       
   200 
       
   201     return YES;
       
   202 }
       
   203 
       
   204 - (void)_addItemToDateCaches:(WebHistoryItem *)entry
       
   205 {
       
   206     WebHistoryDateKey dateKey;
       
   207     if ([self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]])
       
   208         // other entries already exist for this date
       
   209         [self insertItem:entry forDateKey:dateKey];
       
   210     else {
       
   211         // no other entries exist for this date
       
   212         NSMutableArray *entries = [[NSMutableArray alloc] initWithObjects:&entry count:1];
       
   213         _entriesByDate->set(dateKey, entries);
       
   214         [entries release];
       
   215         // Clear _orderedLastVisitedDays so it will be regenerated when next requested.
       
   216         [_orderedLastVisitedDays release];
       
   217         _orderedLastVisitedDays = nil;
       
   218     }
       
   219 }
       
   220 
       
   221 - (void)addItem:(WebHistoryItem *)entry
       
   222 {
       
   223     ASSERT_ARG(entry, entry);
       
   224     ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0);
       
   225 
       
   226     NSString *URLString = [entry URLString];
       
   227 
       
   228     WebHistoryItem *oldEntry = [_entriesByURL objectForKey:URLString];
       
   229     if (oldEntry) {
       
   230         // The last reference to oldEntry might be this dictionary, so we hold onto a reference
       
   231         // until we're done with oldEntry.
       
   232         [oldEntry retain];
       
   233         [self removeItemForURLString:URLString];
       
   234 
       
   235         // If we already have an item with this URL, we need to merge info that drives the
       
   236         // URL autocomplete heuristics from that item into the new one.
       
   237         [entry _mergeAutoCompleteHints:oldEntry];
       
   238         [oldEntry release];
       
   239     }
       
   240 
       
   241     [self _addItemToDateCaches:entry];
       
   242     [_entriesByURL setObject:entry forKey:URLString];
       
   243 }
       
   244 
       
   245 - (void)setLastVisitedTimeInterval:(NSTimeInterval)time forItem:(WebHistoryItem *)entry
       
   246 {
       
   247 #if ASSERT_DISABLED
       
   248     [self _removeItemFromDateCaches:entry];
       
   249 #else
       
   250     BOOL entryWasPresent = [self _removeItemFromDateCaches:entry];
       
   251     ASSERT(entryWasPresent);
       
   252 #endif
       
   253     
       
   254     [entry _setLastVisitedTimeInterval:time];
       
   255     [self _addItemToDateCaches:entry];
       
   256 
       
   257     // Don't send notification until entry is back in the right place in the date caches,
       
   258     // since observers might fetch history by date when they receive the notification.
       
   259     [[NSNotificationCenter defaultCenter]
       
   260         postNotificationName:WebHistoryItemChangedNotification object:entry userInfo:nil];
       
   261 }
       
   262 
       
   263 - (BOOL)removeItem: (WebHistoryItem *)entry
       
   264 {
       
   265     WebHistoryItem *matchingEntry;
       
   266     NSString *URLString;
       
   267 
       
   268     URLString = [entry URLString];
       
   269 
       
   270     // If this exact object isn't stored, then make no change.
       
   271     // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is?
       
   272     // Maybe need to change the API to make something like removeEntryForURLString public instead.
       
   273     matchingEntry = [_entriesByURL objectForKey: URLString];
       
   274     if (matchingEntry != entry) {
       
   275         return NO;
       
   276     }
       
   277 
       
   278     [self removeItemForURLString: URLString];
       
   279 
       
   280     return YES;
       
   281 }
       
   282 
       
   283 - (BOOL)removeItems: (NSArray *)entries
       
   284 {
       
   285     int index, count;
       
   286 
       
   287     count = [entries count];
       
   288     if (count == 0) {
       
   289         return NO;
       
   290     }
       
   291 
       
   292     for (index = 0; index < count; ++index) {
       
   293         [self removeItem:[entries objectAtIndex:index]];
       
   294     }
       
   295     
       
   296     return YES;
       
   297 }
       
   298 
       
   299 - (BOOL)removeAllItems
       
   300 {
       
   301     if ([_entriesByURL count] == 0) {
       
   302         return NO;
       
   303     }
       
   304 
       
   305     _entriesByDate->clear();
       
   306     [_entriesByURL removeAllObjects];
       
   307 
       
   308     return YES;
       
   309 }
       
   310 
       
   311 - (void)addItems:(NSArray *)newEntries
       
   312 {
       
   313     NSEnumerator *enumerator;
       
   314     WebHistoryItem *entry;
       
   315 
       
   316     // There is no guarantee that the incoming entries are in any particular
       
   317     // order, but if this is called with a set of entries that were created by
       
   318     // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy
       
   319     // then they will be ordered chronologically from newest to oldest. We can make adding them
       
   320     // faster (fewer compares) by inserting them from oldest to newest.
       
   321     enumerator = [newEntries reverseObjectEnumerator];
       
   322     while ((entry = [enumerator nextObject]) != nil) {
       
   323         [self addItem:entry];
       
   324     }
       
   325 }
       
   326 
       
   327 #pragma mark DATE-BASED RETRIEVAL
       
   328 
       
   329 - (NSArray *)orderedLastVisitedDays
       
   330 {
       
   331     if (!_orderedLastVisitedDays) {
       
   332         Vector<int> daysAsTimeIntervals;
       
   333         daysAsTimeIntervals.reserveCapacity(_entriesByDate->size());
       
   334         DateToEntriesMap::const_iterator end = _entriesByDate->end();
       
   335         for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it)
       
   336             daysAsTimeIntervals.append(it->first);
       
   337 
       
   338         std::sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end());
       
   339         size_t count = daysAsTimeIntervals.size();
       
   340         _orderedLastVisitedDays = [[NSMutableArray alloc] initWithCapacity:count];
       
   341         for (int i = count - 1; i >= 0; i--) {
       
   342             NSTimeInterval interval = daysAsTimeIntervals[i];
       
   343             NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval];
       
   344             [_orderedLastVisitedDays addObject:date];
       
   345             [date release];
       
   346         }
       
   347     }
       
   348     return _orderedLastVisitedDays;
       
   349 }
       
   350 
       
   351 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
       
   352 {
       
   353     WebHistoryDateKey dateKey;
       
   354     if ([self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]])
       
   355         return _entriesByDate->get(dateKey).get();
       
   356 
       
   357     return nil;
       
   358 }
       
   359 
       
   360 #pragma mark URL MATCHING
       
   361 
       
   362 - (WebHistoryItem *)itemForURLString:(NSString *)URLString
       
   363 {
       
   364     return [_entriesByURL objectForKey: URLString];
       
   365 }
       
   366 
       
   367 - (BOOL)containsItemForURLString: (NSString *)URLString
       
   368 {
       
   369     return [self itemForURLString:URLString] != nil;
       
   370 }
       
   371 
       
   372 - (BOOL)containsURL: (NSURL *)URL
       
   373 {
       
   374     return [self itemForURLString:[URL _web_originalDataAsString]] != nil;
       
   375 }
       
   376 
       
   377 - (WebHistoryItem *)itemForURL:(NSURL *)URL
       
   378 {
       
   379     return [self itemForURLString:[URL _web_originalDataAsString]];
       
   380 }
       
   381 
       
   382 #pragma mark ARCHIVING/UNARCHIVING
       
   383 
       
   384 - (void)setHistoryAgeInDaysLimit:(int)limit
       
   385 {
       
   386     ageInDaysLimitSet = YES;
       
   387     ageInDaysLimit = limit;
       
   388 }
       
   389 
       
   390 - (int)historyAgeInDaysLimit
       
   391 {
       
   392     if (ageInDaysLimitSet)
       
   393         return ageInDaysLimit;
       
   394     return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryAgeInDaysLimit"];
       
   395 }
       
   396 
       
   397 - (void)setHistoryItemLimit:(int)limit
       
   398 {
       
   399     itemLimitSet = YES;
       
   400     itemLimit = limit;
       
   401 }
       
   402 
       
   403 - (int)historyItemLimit
       
   404 {
       
   405     if (itemLimitSet)
       
   406         return itemLimit;
       
   407     return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"];
       
   408 }
       
   409 
       
   410 // Return a date that marks the age limit for history entries saved to or
       
   411 // loaded from disk. Any entry older than this item should be rejected.
       
   412 - (NSCalendarDate *)_ageLimitDate
       
   413 {
       
   414     return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit]
       
   415                                                       hours:0 minutes:0 seconds:0];
       
   416 }
       
   417 
       
   418 // Return a flat array of WebHistoryItems. Ignores the date and item count limits; these are
       
   419 // respected when loading instead of when saving, so that clients can learn of discarded items
       
   420 // by listening to WebHistoryItemsDiscardedWhileLoadingNotification.
       
   421 - (NSArray *)arrayRepresentation
       
   422 {
       
   423     NSMutableArray *arrayRep = [NSMutableArray array];
       
   424 
       
   425     Vector<int> dateKeys;
       
   426     dateKeys.reserveCapacity(_entriesByDate->size());
       
   427     DateToEntriesMap::const_iterator end = _entriesByDate->end();
       
   428     for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it)
       
   429         dateKeys.append(it->first);
       
   430 
       
   431     std::sort(dateKeys.begin(), dateKeys.end());
       
   432     for (int dateIndex = dateKeys.size() - 1; dateIndex >= 0; dateIndex--) {
       
   433         NSArray *entries = _entriesByDate->get(dateKeys[dateIndex]).get();
       
   434         int entryCount = [entries count];
       
   435         for (int entryIndex = 0; entryIndex < entryCount; ++entryIndex)
       
   436             [arrayRep addObject:[[entries objectAtIndex:entryIndex] dictionaryRepresentation]];
       
   437     }
       
   438 
       
   439     return arrayRep;
       
   440 }
       
   441 
       
   442 - (BOOL)_loadHistoryGutsFromURL:(NSURL *)URL savedItemsCount:(int *)numberOfItemsLoaded collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error
       
   443 {
       
   444     *numberOfItemsLoaded = 0;
       
   445     NSDictionary *dictionary = nil;
       
   446 
       
   447     // Optimize loading from local file, which is faster than using the general URL loading mechanism
       
   448     if ([URL isFileURL]) {
       
   449         dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]];
       
   450         if (!dictionary) {
       
   451 #if !LOG_DISABLED
       
   452             if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]])
       
   453                 LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]);
       
   454 #endif
       
   455             // else file doesn't exist, which is normal the first time
       
   456             return NO;
       
   457         }
       
   458     } else {
       
   459         NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
       
   460         if (data && [data length] > 0) {
       
   461             dictionary = [NSPropertyListSerialization propertyListFromData:data
       
   462                 mutabilityOption:NSPropertyListImmutable
       
   463                 format:nil
       
   464                 errorDescription:nil];
       
   465         }
       
   466     }
       
   467 
       
   468     // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support
       
   469     // that ancient format, so anything that isn't an NSDictionary is bogus.
       
   470     if (![dictionary isKindOfClass:[NSDictionary class]])
       
   471         return NO;
       
   472 
       
   473     NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey];
       
   474     int fileVersion;
       
   475     // we don't trust data obtained from elsewhere, so double-check
       
   476     if (fileVersionObject != nil && [fileVersionObject isKindOfClass:[NSNumber class]]) {
       
   477         fileVersion = [fileVersionObject intValue];
       
   478     } else {
       
   479         LOG_ERROR("history file version can't be determined, therefore not loading");
       
   480         return NO;
       
   481     }
       
   482     if (fileVersion > currentFileVersion) {
       
   483         LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion);
       
   484         return NO;
       
   485     }    
       
   486 
       
   487     NSArray *array = [dictionary objectForKey:DatesArrayKey];
       
   488 
       
   489     int itemCountLimit = [self historyItemLimit];
       
   490     NSTimeInterval ageLimitDate = [[self _ageLimitDate] timeIntervalSinceReferenceDate];
       
   491     NSEnumerator *enumerator = [array objectEnumerator];
       
   492     BOOL ageLimitPassed = NO;
       
   493     BOOL itemLimitPassed = NO;
       
   494     ASSERT(*numberOfItemsLoaded == 0);
       
   495 
       
   496     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
       
   497     NSDictionary *itemAsDictionary;
       
   498     while ((itemAsDictionary = [enumerator nextObject]) != nil) {
       
   499         WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary];
       
   500 
       
   501         // item without URL is useless; data on disk must have been bad; ignore
       
   502         if ([item URLString]) {
       
   503             // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing
       
   504             // once we've found the first item that's too old.
       
   505             if (!ageLimitPassed && [item lastVisitedTimeInterval] <= ageLimitDate)
       
   506                 ageLimitPassed = YES;
       
   507 
       
   508             if (ageLimitPassed || itemLimitPassed)
       
   509                 [discardedItems addObject:item];
       
   510             else {
       
   511                 [self addItem:item];
       
   512                 ++(*numberOfItemsLoaded);
       
   513                 if (*numberOfItemsLoaded == itemCountLimit)
       
   514                     itemLimitPassed = YES;
       
   515 
       
   516                 // Draining the autorelease pool every 50 iterations was found by experimentation to be optimal
       
   517                 if (*numberOfItemsLoaded % 50 == 0) {
       
   518                     [pool drain];
       
   519                     pool = [[NSAutoreleasePool alloc] init];
       
   520                 }
       
   521             }
       
   522         }
       
   523         [item release];
       
   524     }
       
   525     [pool drain];
       
   526 
       
   527     return YES;
       
   528 }
       
   529 
       
   530 - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error
       
   531 {
       
   532     int numberOfItems;
       
   533     double start, duration;
       
   534     BOOL result;
       
   535 
       
   536     start = CFAbsoluteTimeGetCurrent();
       
   537     result = [self _loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error];
       
   538 
       
   539     if (result) {
       
   540         duration = CFAbsoluteTimeGetCurrent() - start;
       
   541         LOG(Timing, "loading %d history entries from %@ took %f seconds",
       
   542             numberOfItems, URL, duration);
       
   543     }
       
   544 
       
   545     return result;
       
   546 }
       
   547 
       
   548 - (BOOL)_saveHistoryGuts: (int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error
       
   549 {
       
   550     *numberOfItemsSaved = 0;
       
   551 
       
   552     // FIXME:  Correctly report error when new API is ready.
       
   553     if (error)
       
   554         *error = nil;
       
   555 
       
   556     NSArray *array = [self arrayRepresentation];
       
   557     NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
       
   558         array, DatesArrayKey,
       
   559         [NSNumber numberWithInt:currentFileVersion], FileVersionKey,
       
   560         nil];
       
   561     NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
       
   562     if (![data writeToURL:URL atomically:YES]) {
       
   563         LOG_ERROR("attempt to save %@ to %@ failed", dictionary, URL);
       
   564         return NO;
       
   565     }
       
   566     
       
   567     *numberOfItemsSaved = [array count];
       
   568     return YES;
       
   569 }
       
   570 
       
   571 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
       
   572 {
       
   573     int numberOfItems;
       
   574     double start, duration;
       
   575     BOOL result;
       
   576 
       
   577     start = CFAbsoluteTimeGetCurrent();
       
   578     result = [self _saveHistoryGuts: &numberOfItems URL:URL error:error];
       
   579 
       
   580     if (result) {
       
   581         duration = CFAbsoluteTimeGetCurrent() - start;
       
   582         LOG(Timing, "saving %d history entries to %@ took %f seconds",
       
   583             numberOfItems, URL, duration);
       
   584     }
       
   585 
       
   586     return result;
       
   587 }
       
   588 
       
   589 @end
       
   590 
       
   591 @interface _WebCoreHistoryProvider : NSObject  <WebCoreHistoryProvider> 
       
   592 {
       
   593     WebHistory *history;
       
   594 }
       
   595 - initWithHistory: (WebHistory *)h;
       
   596 @end
       
   597 
       
   598 @implementation _WebCoreHistoryProvider
       
   599 - initWithHistory: (WebHistory *)h
       
   600 {
       
   601     history = [h retain];
       
   602     return self;
       
   603 }
       
   604 
       
   605 static inline bool matchLetter(char c, char lowercaseLetter)
       
   606 {
       
   607     return (c | 0x20) == lowercaseLetter;
       
   608 }
       
   609 
       
   610 static inline bool matchUnicodeLetter(UniChar c, UniChar lowercaseLetter)
       
   611 {
       
   612     return (c | 0x20) == lowercaseLetter;
       
   613 }
       
   614 
       
   615 #define BUFFER_SIZE 2048
       
   616 
       
   617 - (BOOL)containsItemForURLLatin1:(const char *)latin1 length:(unsigned)length
       
   618 {
       
   619     const char *latin1Str = latin1;
       
   620     char staticStrBuffer[BUFFER_SIZE];
       
   621     char *strBuffer = NULL;
       
   622     BOOL needToAddSlash = FALSE;
       
   623 
       
   624     if (length >= 6 &&
       
   625         matchLetter(latin1[0], 'h') &&
       
   626         matchLetter(latin1[1], 't') &&
       
   627         matchLetter(latin1[2], 't') &&
       
   628         matchLetter(latin1[3], 'p') &&
       
   629         (latin1[4] == ':' 
       
   630          || (matchLetter(latin1[4], 's') && latin1[5] == ':'))) {
       
   631         int pos = latin1[4] == ':' ? 5 : 6;
       
   632         // skip possible initial two slashes
       
   633         if (latin1[pos] == '/' && latin1[pos + 1] == '/') {
       
   634             pos += 2;
       
   635         }
       
   636 
       
   637         char *nextSlash = strchr(latin1 + pos, '/');
       
   638         if (nextSlash == NULL) {
       
   639             needToAddSlash = TRUE;
       
   640         }
       
   641     }
       
   642 
       
   643     if (needToAddSlash) {
       
   644         if (length + 1 <= BUFFER_SIZE) {
       
   645             strBuffer = staticStrBuffer;
       
   646         } else {
       
   647             strBuffer = (char*)malloc(length + 2);
       
   648         }
       
   649         memcpy(strBuffer, latin1, length + 1);
       
   650         strBuffer[length] = '/';
       
   651         strBuffer[length+1] = '\0';
       
   652         length++;
       
   653 
       
   654         latin1Str = strBuffer;
       
   655     }
       
   656 
       
   657     CFStringRef str = CFStringCreateWithCStringNoCopy(NULL, latin1Str, kCFStringEncodingWindowsLatin1, kCFAllocatorNull);
       
   658     BOOL result = [history containsItemForURLString:(id)str];
       
   659     CFRelease(str);
       
   660 
       
   661     if (strBuffer != staticStrBuffer) {
       
   662         free(strBuffer);
       
   663     }
       
   664 
       
   665     return result;
       
   666 }
       
   667 
       
   668 #define UNICODE_BUFFER_SIZE 1024
       
   669 
       
   670 - (BOOL)containsItemForURLUnicode:(const UniChar *)unicode length:(unsigned)length
       
   671 {
       
   672     const UniChar *unicodeStr = unicode;
       
   673     UniChar staticStrBuffer[UNICODE_BUFFER_SIZE];
       
   674     UniChar *strBuffer = NULL;
       
   675     BOOL needToAddSlash = FALSE;
       
   676 
       
   677     if (length >= 6 &&
       
   678         matchUnicodeLetter(unicode[0], 'h') &&
       
   679         matchUnicodeLetter(unicode[1], 't') &&
       
   680         matchUnicodeLetter(unicode[2], 't') &&
       
   681         matchUnicodeLetter(unicode[3], 'p') &&
       
   682         (unicode[4] == ':' 
       
   683          || (matchUnicodeLetter(unicode[4], 's') && unicode[5] == ':'))) {
       
   684 
       
   685         unsigned pos = unicode[4] == ':' ? 5 : 6;
       
   686 
       
   687         // skip possible initial two slashes
       
   688         if (pos + 1 < length && unicode[pos] == '/' && unicode[pos + 1] == '/') {
       
   689             pos += 2;
       
   690         }
       
   691 
       
   692         while (pos < length && unicode[pos] != '/') {
       
   693             pos++;
       
   694         }
       
   695 
       
   696         if (pos == length) {
       
   697             needToAddSlash = TRUE;
       
   698         }
       
   699     }
       
   700 
       
   701     if (needToAddSlash) {
       
   702         if (length + 1 <= UNICODE_BUFFER_SIZE) {
       
   703             strBuffer = staticStrBuffer;
       
   704         } else {
       
   705             strBuffer = (UniChar*)malloc(sizeof(UniChar) * (length + 1));
       
   706         }
       
   707         memcpy(strBuffer, unicode, 2 * length);
       
   708         strBuffer[length] = '/';
       
   709         length++;
       
   710 
       
   711         unicodeStr = strBuffer;
       
   712     }
       
   713 
       
   714     CFStringRef str = CFStringCreateWithCharactersNoCopy(NULL, unicodeStr, length, kCFAllocatorNull);
       
   715     BOOL result = [history containsItemForURLString:(id)str];
       
   716     CFRelease(str);
       
   717 
       
   718     if (strBuffer != staticStrBuffer) {
       
   719         free(strBuffer);
       
   720     }
       
   721 
       
   722     return result;
       
   723 }
       
   724 
       
   725 - (void)dealloc
       
   726 {
       
   727     [history release];
       
   728     [super dealloc];
       
   729 }
       
   730 
       
   731 @end
       
   732 
       
   733 @implementation WebHistory
       
   734 
       
   735 + (WebHistory *)optionalSharedHistory
       
   736 {
       
   737     return _sharedHistory;
       
   738 }
       
   739 
       
   740 
       
   741 + (void)setOptionalSharedHistory: (WebHistory *)history
       
   742 {
       
   743     // FIXME.  Need to think about multiple instances of WebHistory per application
       
   744     // and correct synchronization of history file between applications.
       
   745     [WebCoreHistory setHistoryProvider: [[[_WebCoreHistoryProvider alloc] initWithHistory: history] autorelease]];
       
   746     if (_sharedHistory != history){
       
   747         [_sharedHistory release];
       
   748         _sharedHistory = [history retain];
       
   749     }
       
   750 }
       
   751 
       
   752 - (id)init
       
   753 {
       
   754     if ((self = [super init]) != nil) {
       
   755         _historyPrivate = [[WebHistoryPrivate alloc] init];
       
   756     }
       
   757 
       
   758     return self;
       
   759 }
       
   760 
       
   761 - (void)dealloc
       
   762 {
       
   763     [_historyPrivate release];
       
   764     [super dealloc];
       
   765 }
       
   766 
       
   767 #pragma mark MODIFYING CONTENTS
       
   768 
       
   769 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
       
   770 {
       
   771     NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil];
       
   772     [[NSNotificationCenter defaultCenter]
       
   773         postNotificationName: name object: self userInfo: userInfo];
       
   774 }
       
   775 
       
   776 - (WebHistoryItem *)addItemForURL: (NSURL *)URL
       
   777 {
       
   778     WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:nil];
       
   779     [entry _setLastVisitedTimeInterval: [NSDate timeIntervalSinceReferenceDate]];
       
   780     [self addItem: entry];
       
   781     [entry release];
       
   782     return entry;
       
   783 }
       
   784 
       
   785 
       
   786 - (void)addItem: (WebHistoryItem *)entry
       
   787 {
       
   788     LOG (History, "adding %@", entry);
       
   789     [_historyPrivate addItem: entry];
       
   790     [self _sendNotification: WebHistoryItemsAddedNotification
       
   791                     entries: [NSArray arrayWithObject:entry]];
       
   792 }
       
   793 
       
   794 - (void)removeItem: (WebHistoryItem *)entry
       
   795 {
       
   796     if ([_historyPrivate removeItem: entry]) {
       
   797         [self _sendNotification: WebHistoryItemsRemovedNotification
       
   798                         entries: [NSArray arrayWithObject:entry]];
       
   799     }
       
   800 }
       
   801 
       
   802 - (void)removeItems: (NSArray *)entries
       
   803 {
       
   804     if ([_historyPrivate removeItems:entries]) {
       
   805         [self _sendNotification: WebHistoryItemsRemovedNotification
       
   806                         entries: entries];
       
   807     }
       
   808 }
       
   809 
       
   810 - (void)removeAllItems
       
   811 {
       
   812     if ([_historyPrivate removeAllItems]) {
       
   813         [[NSNotificationCenter defaultCenter]
       
   814             postNotificationName: WebHistoryAllItemsRemovedNotification
       
   815                           object: self];
       
   816     }
       
   817 }
       
   818 
       
   819 - (void)addItems:(NSArray *)newEntries
       
   820 {
       
   821     [_historyPrivate addItems:newEntries];
       
   822     [self _sendNotification: WebHistoryItemsAddedNotification
       
   823                     entries: newEntries];
       
   824 }
       
   825 
       
   826 - (void)setLastVisitedTimeInterval:(NSTimeInterval)time forItem:(WebHistoryItem *)entry
       
   827 {
       
   828     [_historyPrivate setLastVisitedTimeInterval:time forItem:entry];
       
   829 }
       
   830 
       
   831 #pragma mark DATE-BASED RETRIEVAL
       
   832 
       
   833 - (NSArray *)orderedLastVisitedDays
       
   834 {
       
   835     return [_historyPrivate orderedLastVisitedDays];
       
   836 }
       
   837 
       
   838 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
       
   839 {
       
   840     return [_historyPrivate orderedItemsLastVisitedOnDay: date];
       
   841 }
       
   842 
       
   843 #pragma mark URL MATCHING
       
   844 
       
   845 - (BOOL)containsItemForURLString: (NSString *)URLString
       
   846 {
       
   847     return [_historyPrivate containsItemForURLString: URLString];
       
   848 }
       
   849 
       
   850 - (BOOL)containsURL: (NSURL *)URL
       
   851 {
       
   852     return [_historyPrivate containsURL: URL];
       
   853 }
       
   854 
       
   855 - (WebHistoryItem *)itemForURL:(NSURL *)URL
       
   856 {
       
   857     return [_historyPrivate itemForURL:URL];
       
   858 }
       
   859 
       
   860 #pragma mark SAVING TO DISK
       
   861 
       
   862 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
       
   863 {
       
   864     NSMutableArray *discardedItems = [NSMutableArray array];
       
   865     
       
   866     if ([_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) {
       
   867         [[NSNotificationCenter defaultCenter]
       
   868             postNotificationName:WebHistoryLoadedNotification
       
   869                           object:self];
       
   870         
       
   871         if ([discardedItems count] > 0)
       
   872             [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems];
       
   873         
       
   874         return YES;
       
   875     }
       
   876     return NO;
       
   877 }
       
   878 
       
   879 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
       
   880 {
       
   881     // FIXME:  Use new foundation API to get error when ready.
       
   882     if([_historyPrivate saveToURL:URL error:error]){
       
   883         [[NSNotificationCenter defaultCenter]
       
   884             postNotificationName: WebHistorySavedNotification
       
   885                           object: self];
       
   886         return YES;
       
   887     }
       
   888     return NO;    
       
   889 }
       
   890 
       
   891 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString
       
   892 {
       
   893     return [_historyPrivate itemForURLString: URLString];
       
   894 }
       
   895 
       
   896 - (NSCalendarDate*)ageLimitDate
       
   897 {
       
   898     return [_historyPrivate _ageLimitDate];
       
   899 }
       
   900 
       
   901 - (void)setHistoryItemLimit:(int)limit
       
   902 {
       
   903     [_historyPrivate setHistoryItemLimit:limit];
       
   904 }
       
   905 
       
   906 - (int)historyItemLimit
       
   907 {
       
   908     return [_historyPrivate historyItemLimit];
       
   909 }
       
   910 
       
   911 - (void)setHistoryAgeInDaysLimit:(int)limit
       
   912 {
       
   913     [_historyPrivate setHistoryAgeInDaysLimit:limit];
       
   914 }
       
   915 
       
   916 - (int)historyAgeInDaysLimit
       
   917 {
       
   918     return [_historyPrivate historyAgeInDaysLimit];
       
   919 }
       
   920 
       
   921 @end