Home | History | Annotate | Download | only in History
      1 /*
      2  * Copyright (C) 2005, 2008, 2009 Apple 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 "WebHistoryInternal.h"
     30 
     31 #import "WebHistoryItemInternal.h"
     32 #import "WebKitLogging.h"
     33 #import "WebNSURLExtras.h"
     34 #import "WebTypesInternal.h"
     35 #import <WebCore/HistoryItem.h>
     36 #import <WebCore/HistoryPropertyList.h>
     37 #import <WebCore/PageGroup.h>
     38 
     39 using namespace WebCore;
     40 using namespace std;
     41 
     42 typedef int64_t WebHistoryDateKey;
     43 typedef HashMap<WebHistoryDateKey, RetainPtr<NSMutableArray> > DateToEntriesMap;
     44 
     45 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
     46 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
     47 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
     48 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
     49 NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification";
     50 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
     51 NSString *WebHistoryItemsKey = @"WebHistoryItems";
     52 
     53 static WebHistory *_sharedHistory = nil;
     54 
     55 NSString *FileVersionKey = @"WebHistoryFileVersion";
     56 NSString *DatesArrayKey = @"WebHistoryDates";
     57 
     58 #define currentFileVersion 1
     59 
     60 class WebHistoryWriter : public HistoryPropertyListWriter {
     61 public:
     62     WebHistoryWriter(DateToEntriesMap*);
     63 
     64 private:
     65     virtual void writeHistoryItems(BinaryPropertyListObjectStream&);
     66 
     67     DateToEntriesMap* m_entriesByDate;
     68     Vector<int> m_dateKeys;
     69 };
     70 
     71 @interface WebHistoryPrivate : NSObject {
     72 @private
     73     NSMutableDictionary *_entriesByURL;
     74     DateToEntriesMap* _entriesByDate;
     75     NSMutableArray *_orderedLastVisitedDays;
     76     BOOL itemLimitSet;
     77     int itemLimit;
     78     BOOL ageInDaysLimitSet;
     79     int ageInDaysLimit;
     80 }
     81 
     82 - (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title increaseVisitCount:(BOOL)increaseVisitCount;
     83 
     84 - (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate;
     85 - (void)addItems:(NSArray *)newEntries;
     86 - (BOOL)removeItem:(WebHistoryItem *)entry;
     87 - (BOOL)removeItems:(NSArray *)entries;
     88 - (BOOL)removeAllItems;
     89 
     90 - (NSArray *)orderedLastVisitedDays;
     91 - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)calendarDate;
     92 - (BOOL)containsURL:(NSURL *)URL;
     93 - (WebHistoryItem *)itemForURL:(NSURL *)URL;
     94 - (WebHistoryItem *)itemForURLString:(NSString *)URLString;
     95 - (NSArray *)allItems;
     96 
     97 - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error;
     98 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error;
     99 
    100 - (NSCalendarDate *)ageLimitDate;
    101 
    102 - (void)setHistoryItemLimit:(int)limit;
    103 - (int)historyItemLimit;
    104 - (void)setHistoryAgeInDaysLimit:(int)limit;
    105 - (int)historyAgeInDaysLimit;
    106 
    107 - (void)addVisitedLinksToPageGroup:(PageGroup&)group;
    108 
    109 @end
    110 
    111 @implementation WebHistoryPrivate
    112 
    113 #pragma mark OBJECT FRAMEWORK
    114 
    115 + (void)initialize
    116 {
    117     [[NSUserDefaults standardUserDefaults] registerDefaults:
    118         [NSDictionary dictionaryWithObjectsAndKeys:
    119             @"1000", @"WebKitHistoryItemLimit",
    120             @"7", @"WebKitHistoryAgeInDaysLimit",
    121             nil]];
    122 }
    123 
    124 - (id)init
    125 {
    126     if (![super init])
    127         return nil;
    128 
    129     _entriesByURL = [[NSMutableDictionary alloc] init];
    130     _entriesByDate = new DateToEntriesMap;
    131 
    132     return self;
    133 }
    134 
    135 - (void)dealloc
    136 {
    137     [_entriesByURL release];
    138     [_orderedLastVisitedDays release];
    139     delete _entriesByDate;
    140     [super dealloc];
    141 }
    142 
    143 - (void)finalize
    144 {
    145     delete _entriesByDate;
    146     [super finalize];
    147 }
    148 
    149 #pragma mark MODIFYING CONTENTS
    150 
    151 static void getDayBoundaries(NSTimeInterval interval, NSTimeInterval& beginningOfDay, NSTimeInterval& beginningOfNextDay)
    152 {
    153     CFTimeZoneRef timeZone = CFTimeZoneCopyDefault();
    154     CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(interval, timeZone);
    155     date.hour = 0;
    156     date.minute = 0;
    157     date.second = 0;
    158     beginningOfDay = CFGregorianDateGetAbsoluteTime(date, timeZone);
    159     date.day += 1;
    160     beginningOfNextDay = CFGregorianDateGetAbsoluteTime(date, timeZone);
    161     CFRelease(timeZone);
    162 }
    163 
    164 static inline NSTimeInterval beginningOfDay(NSTimeInterval date)
    165 {
    166     static NSTimeInterval cachedBeginningOfDay = NAN;
    167     static NSTimeInterval cachedBeginningOfNextDay;
    168     if (!(date >= cachedBeginningOfDay && date < cachedBeginningOfNextDay))
    169         getDayBoundaries(date, cachedBeginningOfDay, cachedBeginningOfNextDay);
    170     return cachedBeginningOfDay;
    171 }
    172 
    173 static inline WebHistoryDateKey dateKey(NSTimeInterval date)
    174 {
    175     // Converting from double (NSTimeInterval) to int64_t (WebHistoryDateKey) is
    176     // safe here because all sensible dates are in the range -2**48 .. 2**47 which
    177     // safely fits in an int64_t.
    178     return beginningOfDay(date);
    179 }
    180 
    181 // Returns whether the day is already in the list of days,
    182 // and fills in *key with the key used to access its location
    183 - (BOOL)findKey:(WebHistoryDateKey*)key forDay:(NSTimeInterval)date
    184 {
    185     ASSERT_ARG(key, key);
    186     *key = dateKey(date);
    187     return _entriesByDate->contains(*key);
    188 }
    189 
    190 - (void)insertItem:(WebHistoryItem *)entry forDateKey:(WebHistoryDateKey)dateKey
    191 {
    192     ASSERT_ARG(entry, entry != nil);
    193     ASSERT(_entriesByDate->contains(dateKey));
    194 
    195     NSMutableArray *entriesForDate = _entriesByDate->get(dateKey).get();
    196     NSTimeInterval entryDate = [entry lastVisitedTimeInterval];
    197 
    198     unsigned count = [entriesForDate count];
    199 
    200     // The entries for each day are stored in a sorted array with the most recent entry first
    201     // Check for the common cases of the entry being newer than all existing entries or the first entry of the day
    202     if (!count || [[entriesForDate objectAtIndex:0] lastVisitedTimeInterval] < entryDate) {
    203         [entriesForDate insertObject:entry atIndex:0];
    204         return;
    205     }
    206     // .. or older than all existing entries
    207     if (count > 0 && [[entriesForDate objectAtIndex:count - 1] lastVisitedTimeInterval] >= entryDate) {
    208         [entriesForDate insertObject:entry atIndex:count];
    209         return;
    210     }
    211 
    212     unsigned low = 0;
    213     unsigned high = count;
    214     while (low < high) {
    215         unsigned mid = low + (high - low) / 2;
    216         if ([[entriesForDate objectAtIndex:mid] lastVisitedTimeInterval] >= entryDate)
    217             low = mid + 1;
    218         else
    219             high = mid;
    220     }
    221 
    222     // low is now the index of the first entry that is older than entryDate
    223     [entriesForDate insertObject:entry atIndex:low];
    224 }
    225 
    226 - (BOOL)removeItemFromDateCaches:(WebHistoryItem *)entry
    227 {
    228     WebHistoryDateKey dateKey;
    229     BOOL foundDate = [self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]];
    230 
    231     if (!foundDate)
    232         return NO;
    233 
    234     DateToEntriesMap::iterator it = _entriesByDate->find(dateKey);
    235     NSMutableArray *entriesForDate = it->second.get();
    236     [entriesForDate removeObjectIdenticalTo:entry];
    237 
    238     // remove this date entirely if there are no other entries on it
    239     if ([entriesForDate count] == 0) {
    240         _entriesByDate->remove(it);
    241         // Clear _orderedLastVisitedDays so it will be regenerated when next requested.
    242         [_orderedLastVisitedDays release];
    243         _orderedLastVisitedDays = nil;
    244     }
    245 
    246     return YES;
    247 }
    248 
    249 - (BOOL)removeItemForURLString:(NSString *)URLString
    250 {
    251     WebHistoryItem *entry = [_entriesByURL objectForKey:URLString];
    252     if (!entry)
    253         return NO;
    254 
    255     [_entriesByURL removeObjectForKey:URLString];
    256 
    257 #if ASSERT_DISABLED
    258     [self removeItemFromDateCaches:entry];
    259 #else
    260     BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry];
    261     ASSERT(itemWasInDateCaches);
    262 #endif
    263 
    264     if (![_entriesByURL count])
    265         PageGroup::removeAllVisitedLinks();
    266 
    267     return YES;
    268 }
    269 
    270 - (void)addItemToDateCaches:(WebHistoryItem *)entry
    271 {
    272     WebHistoryDateKey dateKey;
    273     if ([self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]])
    274         // other entries already exist for this date
    275         [self insertItem:entry forDateKey:dateKey];
    276     else {
    277         // no other entries exist for this date
    278         NSMutableArray *entries = [[NSMutableArray alloc] initWithObjects:&entry count:1];
    279         _entriesByDate->set(dateKey, entries);
    280         [entries release];
    281         // Clear _orderedLastVisitedDays so it will be regenerated when next requested.
    282         [_orderedLastVisitedDays release];
    283         _orderedLastVisitedDays = nil;
    284     }
    285 }
    286 
    287 - (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title increaseVisitCount:(BOOL)increaseVisitCount
    288 {
    289     ASSERT(url);
    290     ASSERT(title);
    291 
    292     NSString *URLString = [url _web_originalDataAsString];
    293     WebHistoryItem *entry = [_entriesByURL objectForKey:URLString];
    294 
    295     if (entry) {
    296         LOG(History, "Updating global history entry %@", entry);
    297         // Remove the item from date caches before changing its last visited date.  Otherwise we might get duplicate entries
    298         // as seen in <rdar://problem/6570573>.
    299         BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry];
    300         ASSERT_UNUSED(itemWasInDateCaches, itemWasInDateCaches);
    301 
    302         [entry _visitedWithTitle:title increaseVisitCount:increaseVisitCount];
    303     } else {
    304         LOG(History, "Adding new global history entry for %@", url);
    305         entry = [[WebHistoryItem alloc] initWithURLString:URLString title:title lastVisitedTimeInterval:[NSDate timeIntervalSinceReferenceDate]];
    306         [entry _recordInitialVisit];
    307         [_entriesByURL setObject:entry forKey:URLString];
    308         [entry release];
    309     }
    310 
    311     [self addItemToDateCaches:entry];
    312 
    313     return entry;
    314 }
    315 
    316 - (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate
    317 {
    318     ASSERT_ARG(entry, entry);
    319     ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0);
    320 
    321     NSString *URLString = [entry URLString];
    322 
    323     WebHistoryItem *oldEntry = [_entriesByURL objectForKey:URLString];
    324     if (oldEntry) {
    325         if (discardDuplicate)
    326             return NO;
    327 
    328         // The last reference to oldEntry might be this dictionary, so we hold onto a reference
    329         // until we're done with oldEntry.
    330         [oldEntry retain];
    331         [self removeItemForURLString:URLString];
    332 
    333         // If we already have an item with this URL, we need to merge info that drives the
    334         // URL autocomplete heuristics from that item into the new one.
    335         [entry _mergeAutoCompleteHints:oldEntry];
    336         [oldEntry release];
    337     }
    338 
    339     [self addItemToDateCaches:entry];
    340     [_entriesByURL setObject:entry forKey:URLString];
    341 
    342     return YES;
    343 }
    344 
    345 - (BOOL)removeItem:(WebHistoryItem *)entry
    346 {
    347     NSString *URLString = [entry URLString];
    348 
    349     // If this exact object isn't stored, then make no change.
    350     // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is?
    351     // Maybe need to change the API to make something like removeEntryForURLString public instead.
    352     WebHistoryItem *matchingEntry = [_entriesByURL objectForKey:URLString];
    353     if (matchingEntry != entry)
    354         return NO;
    355 
    356     [self removeItemForURLString:URLString];
    357 
    358     return YES;
    359 }
    360 
    361 - (BOOL)removeItems:(NSArray *)entries
    362 {
    363     NSUInteger count = [entries count];
    364     if (!count)
    365         return NO;
    366 
    367     for (NSUInteger index = 0; index < count; ++index)
    368         [self removeItem:[entries objectAtIndex:index]];
    369 
    370     return YES;
    371 }
    372 
    373 - (BOOL)removeAllItems
    374 {
    375     if (_entriesByDate->isEmpty())
    376         return NO;
    377 
    378     _entriesByDate->clear();
    379     [_entriesByURL removeAllObjects];
    380 
    381     // Clear _orderedLastVisitedDays so it will be regenerated when next requested.
    382     [_orderedLastVisitedDays release];
    383     _orderedLastVisitedDays = nil;
    384 
    385     PageGroup::removeAllVisitedLinks();
    386 
    387     return YES;
    388 }
    389 
    390 - (void)addItems:(NSArray *)newEntries
    391 {
    392     // There is no guarantee that the incoming entries are in any particular
    393     // order, but if this is called with a set of entries that were created by
    394     // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy
    395     // then they will be ordered chronologically from newest to oldest. We can make adding them
    396     // faster (fewer compares) by inserting them from oldest to newest.
    397     NSEnumerator *enumerator = [newEntries reverseObjectEnumerator];
    398     while (WebHistoryItem *entry = [enumerator nextObject])
    399         [self addItem:entry discardDuplicate:NO];
    400 }
    401 
    402 #pragma mark DATE-BASED RETRIEVAL
    403 
    404 - (NSArray *)orderedLastVisitedDays
    405 {
    406     if (!_orderedLastVisitedDays) {
    407         Vector<int> daysAsTimeIntervals;
    408         daysAsTimeIntervals.reserveCapacity(_entriesByDate->size());
    409         DateToEntriesMap::const_iterator end = _entriesByDate->end();
    410         for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it)
    411             daysAsTimeIntervals.append(it->first);
    412 
    413         sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end());
    414         size_t count = daysAsTimeIntervals.size();
    415         _orderedLastVisitedDays = [[NSMutableArray alloc] initWithCapacity:count];
    416         for (int i = count - 1; i >= 0; i--) {
    417             NSTimeInterval interval = daysAsTimeIntervals[i];
    418             NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval];
    419             [_orderedLastVisitedDays addObject:date];
    420             [date release];
    421         }
    422     }
    423     return _orderedLastVisitedDays;
    424 }
    425 
    426 - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date
    427 {
    428     WebHistoryDateKey dateKey;
    429     if (![self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]])
    430         return nil;
    431     return _entriesByDate->get(dateKey).get();
    432 }
    433 
    434 #pragma mark URL MATCHING
    435 
    436 - (WebHistoryItem *)itemForURLString:(NSString *)URLString
    437 {
    438     return [_entriesByURL objectForKey:URLString];
    439 }
    440 
    441 - (BOOL)containsURL:(NSURL *)URL
    442 {
    443     return [self itemForURLString:[URL _web_originalDataAsString]] != nil;
    444 }
    445 
    446 - (WebHistoryItem *)itemForURL:(NSURL *)URL
    447 {
    448     return [self itemForURLString:[URL _web_originalDataAsString]];
    449 }
    450 
    451 - (NSArray *)allItems
    452 {
    453     return [_entriesByURL allValues];
    454 }
    455 
    456 #pragma mark ARCHIVING/UNARCHIVING
    457 
    458 - (void)setHistoryAgeInDaysLimit:(int)limit
    459 {
    460     ageInDaysLimitSet = YES;
    461     ageInDaysLimit = limit;
    462 }
    463 
    464 - (int)historyAgeInDaysLimit
    465 {
    466     if (ageInDaysLimitSet)
    467         return ageInDaysLimit;
    468     return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryAgeInDaysLimit"];
    469 }
    470 
    471 - (void)setHistoryItemLimit:(int)limit
    472 {
    473     itemLimitSet = YES;
    474     itemLimit = limit;
    475 }
    476 
    477 - (int)historyItemLimit
    478 {
    479     if (itemLimitSet)
    480         return itemLimit;
    481     return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryItemLimit"];
    482 }
    483 
    484 // Return a date that marks the age limit for history entries saved to or
    485 // loaded from disk. Any entry older than this item should be rejected.
    486 - (NSCalendarDate *)ageLimitDate
    487 {
    488     return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit]
    489                                                       hours:0 minutes:0 seconds:0];
    490 }
    491 
    492 - (BOOL)loadHistoryGutsFromURL:(NSURL *)URL savedItemsCount:(int *)numberOfItemsLoaded collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error
    493 {
    494     *numberOfItemsLoaded = 0;
    495     NSDictionary *dictionary = nil;
    496 
    497     // Optimize loading from local file, which is faster than using the general URL loading mechanism
    498     if ([URL isFileURL]) {
    499         dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]];
    500         if (!dictionary) {
    501 #if !LOG_DISABLED
    502             if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]])
    503                 LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]);
    504 #endif
    505             // else file doesn't exist, which is normal the first time
    506             return NO;
    507         }
    508     } else {
    509         NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
    510         if (data && [data length] > 0) {
    511             dictionary = [NSPropertyListSerialization propertyListFromData:data
    512                 mutabilityOption:NSPropertyListImmutable
    513                 format:nil
    514                 errorDescription:nil];
    515         }
    516     }
    517 
    518     // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support
    519     // that ancient format, so anything that isn't an NSDictionary is bogus.
    520     if (![dictionary isKindOfClass:[NSDictionary class]])
    521         return NO;
    522 
    523     NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey];
    524     int fileVersion;
    525     // we don't trust data obtained from elsewhere, so double-check
    526     if (!fileVersionObject || ![fileVersionObject isKindOfClass:[NSNumber class]]) {
    527         LOG_ERROR("history file version can't be determined, therefore not loading");
    528         return NO;
    529     }
    530     fileVersion = [fileVersionObject intValue];
    531     if (fileVersion > currentFileVersion) {
    532         LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion);
    533         return NO;
    534     }
    535 
    536     NSArray *array = [dictionary objectForKey:DatesArrayKey];
    537 
    538     int itemCountLimit = [self historyItemLimit];
    539     NSTimeInterval ageLimitDate = [[self ageLimitDate] timeIntervalSinceReferenceDate];
    540     NSEnumerator *enumerator = [array objectEnumerator];
    541     BOOL ageLimitPassed = NO;
    542     BOOL itemLimitPassed = NO;
    543     ASSERT(*numberOfItemsLoaded == 0);
    544 
    545     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    546     NSDictionary *itemAsDictionary;
    547     while ((itemAsDictionary = [enumerator nextObject]) != nil) {
    548         WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary];
    549 
    550         // item without URL is useless; data on disk must have been bad; ignore
    551         if ([item URLString]) {
    552             // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing
    553             // once we've found the first item that's too old.
    554             if (!ageLimitPassed && [item lastVisitedTimeInterval] <= ageLimitDate)
    555                 ageLimitPassed = YES;
    556 
    557             if (ageLimitPassed || itemLimitPassed)
    558                 [discardedItems addObject:item];
    559             else {
    560                 if ([self addItem:item discardDuplicate:YES])
    561                     ++(*numberOfItemsLoaded);
    562                 if (*numberOfItemsLoaded == itemCountLimit)
    563                     itemLimitPassed = YES;
    564 
    565                 // Draining the autorelease pool every 50 iterations was found by experimentation to be optimal
    566                 if (*numberOfItemsLoaded % 50 == 0) {
    567                     [pool drain];
    568                     pool = [[NSAutoreleasePool alloc] init];
    569                 }
    570             }
    571         }
    572         [item release];
    573     }
    574     [pool drain];
    575 
    576     return YES;
    577 }
    578 
    579 - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error
    580 {
    581 #if !LOG_DISABLED
    582     double start = CFAbsoluteTimeGetCurrent();
    583 #endif
    584 
    585     int numberOfItems;
    586     if (![self loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error])
    587         return NO;
    588 
    589 #if !LOG_DISABLED
    590     double duration = CFAbsoluteTimeGetCurrent() - start;
    591     LOG(Timing, "loading %d history entries from %@ took %f seconds", numberOfItems, URL, duration);
    592 #endif
    593 
    594     return YES;
    595 }
    596 
    597 - (NSData *)data
    598 {
    599     if (_entriesByDate->isEmpty()) {
    600         static NSData *emptyHistoryData = (NSData *)CFDataCreate(0, 0, 0);
    601         return emptyHistoryData;
    602     }
    603 
    604     // Ignores the date and item count limits; these are respected when loading instead of when saving, so
    605     // that clients can learn of discarded items by listening to WebHistoryItemsDiscardedWhileLoadingNotification.
    606     WebHistoryWriter writer(_entriesByDate);
    607     writer.writePropertyList();
    608     return [[(NSData *)writer.releaseData().get() retain] autorelease];
    609 }
    610 
    611 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
    612 {
    613 #if !LOG_DISABLED
    614     double start = CFAbsoluteTimeGetCurrent();
    615 #endif
    616 
    617     BOOL result = [[self data] writeToURL:URL options:0 error:error];
    618 
    619 #if !LOG_DISABLED
    620     double duration = CFAbsoluteTimeGetCurrent() - start;
    621     LOG(Timing, "saving history to %@ took %f seconds", URL, duration);
    622 #endif
    623 
    624     return result;
    625 }
    626 
    627 - (void)addVisitedLinksToPageGroup:(PageGroup&)group
    628 {
    629     NSEnumerator *enumerator = [_entriesByURL keyEnumerator];
    630     while (NSString *url = [enumerator nextObject]) {
    631         size_t length = [url length];
    632         const UChar* characters = CFStringGetCharactersPtr(reinterpret_cast<CFStringRef>(url));
    633         if (characters)
    634             group.addVisitedLink(characters, length);
    635         else {
    636             Vector<UChar, 512> buffer(length);
    637             [url getCharacters:buffer.data()];
    638             group.addVisitedLink(buffer.data(), length);
    639         }
    640     }
    641 }
    642 
    643 @end
    644 
    645 @implementation WebHistory
    646 
    647 + (WebHistory *)optionalSharedHistory
    648 {
    649     return _sharedHistory;
    650 }
    651 
    652 + (void)setOptionalSharedHistory:(WebHistory *)history
    653 {
    654     if (_sharedHistory == history)
    655         return;
    656     // FIXME: Need to think about multiple instances of WebHistory per application
    657     // and correct synchronization of history file between applications.
    658     [_sharedHistory release];
    659     _sharedHistory = [history retain];
    660     PageGroup::setShouldTrackVisitedLinks(history);
    661     PageGroup::removeAllVisitedLinks();
    662 }
    663 
    664 - (id)init
    665 {
    666     self = [super init];
    667     if (!self)
    668         return nil;
    669     _historyPrivate = [[WebHistoryPrivate alloc] init];
    670     return self;
    671 }
    672 
    673 - (void)dealloc
    674 {
    675     [_historyPrivate release];
    676     [super dealloc];
    677 }
    678 
    679 #pragma mark MODIFYING CONTENTS
    680 
    681 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
    682 {
    683     NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil];
    684     [[NSNotificationCenter defaultCenter]
    685         postNotificationName:name object:self userInfo:userInfo];
    686 }
    687 
    688 - (void)removeItems:(NSArray *)entries
    689 {
    690     if ([_historyPrivate removeItems:entries]) {
    691         [self _sendNotification:WebHistoryItemsRemovedNotification
    692                         entries:entries];
    693     }
    694 }
    695 
    696 - (void)removeAllItems
    697 {
    698     NSArray *entries = [_historyPrivate allItems];
    699     if ([_historyPrivate removeAllItems])
    700         [self _sendNotification:WebHistoryAllItemsRemovedNotification entries:entries];
    701 }
    702 
    703 - (void)addItems:(NSArray *)newEntries
    704 {
    705     [_historyPrivate addItems:newEntries];
    706     [self _sendNotification:WebHistoryItemsAddedNotification
    707                     entries:newEntries];
    708 }
    709 
    710 #pragma mark DATE-BASED RETRIEVAL
    711 
    712 - (NSArray *)orderedLastVisitedDays
    713 {
    714     return [_historyPrivate orderedLastVisitedDays];
    715 }
    716 
    717 - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date
    718 {
    719     return [_historyPrivate orderedItemsLastVisitedOnDay:date];
    720 }
    721 
    722 #pragma mark URL MATCHING
    723 
    724 - (BOOL)containsURL:(NSURL *)URL
    725 {
    726     return [_historyPrivate containsURL:URL];
    727 }
    728 
    729 - (WebHistoryItem *)itemForURL:(NSURL *)URL
    730 {
    731     return [_historyPrivate itemForURL:URL];
    732 }
    733 
    734 #pragma mark SAVING TO DISK
    735 
    736 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
    737 {
    738     NSMutableArray *discardedItems = [[NSMutableArray alloc] init];
    739     if (![_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) {
    740         [discardedItems release];
    741         return NO;
    742     }
    743 
    744     [[NSNotificationCenter defaultCenter]
    745         postNotificationName:WebHistoryLoadedNotification
    746                       object:self];
    747 
    748     if ([discardedItems count])
    749         [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems];
    750 
    751     [discardedItems release];
    752     return YES;
    753 }
    754 
    755 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
    756 {
    757     if (![_historyPrivate saveToURL:URL error:error])
    758         return NO;
    759     [[NSNotificationCenter defaultCenter]
    760         postNotificationName:WebHistorySavedNotification
    761                       object:self];
    762     return YES;
    763 }
    764 
    765 - (void)setHistoryItemLimit:(int)limit
    766 {
    767     [_historyPrivate setHistoryItemLimit:limit];
    768 }
    769 
    770 - (int)historyItemLimit
    771 {
    772     return [_historyPrivate historyItemLimit];
    773 }
    774 
    775 - (void)setHistoryAgeInDaysLimit:(int)limit
    776 {
    777     [_historyPrivate setHistoryAgeInDaysLimit:limit];
    778 }
    779 
    780 - (int)historyAgeInDaysLimit
    781 {
    782     return [_historyPrivate historyAgeInDaysLimit];
    783 }
    784 
    785 @end
    786 
    787 @implementation WebHistory (WebPrivate)
    788 
    789 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString
    790 {
    791     return [_historyPrivate itemForURLString:URLString];
    792 }
    793 
    794 - (NSArray *)allItems
    795 {
    796     return [_historyPrivate allItems];
    797 }
    798 
    799 - (NSData *)_data
    800 {
    801     return [_historyPrivate data];
    802 }
    803 
    804 + (void)_setVisitedLinkTrackingEnabled:(BOOL)visitedLinkTrackingEnabled
    805 {
    806     PageGroup::setShouldTrackVisitedLinks(visitedLinkTrackingEnabled);
    807 }
    808 
    809 + (void)_removeAllVisitedLinks
    810 {
    811     PageGroup::removeAllVisitedLinks();
    812 }
    813 
    814 @end
    815 
    816 @implementation WebHistory (WebInternal)
    817 
    818 - (void)_visitedURL:(NSURL *)url withTitle:(NSString *)title method:(NSString *)method wasFailure:(BOOL)wasFailure increaseVisitCount:(BOOL)increaseVisitCount
    819 {
    820     WebHistoryItem *entry = [_historyPrivate visitedURL:url withTitle:title increaseVisitCount:increaseVisitCount];
    821 
    822     HistoryItem* item = core(entry);
    823     item->setLastVisitWasFailure(wasFailure);
    824 
    825     if ([method length])
    826         item->setLastVisitWasHTTPNonGet([method caseInsensitiveCompare:@"GET"] && (![[url scheme] caseInsensitiveCompare:@"http"] || ![[url scheme] caseInsensitiveCompare:@"https"]));
    827 
    828     item->setRedirectURLs(0);
    829 
    830     NSArray *entries = [[NSArray alloc] initWithObjects:entry, nil];
    831     [self _sendNotification:WebHistoryItemsAddedNotification entries:entries];
    832     [entries release];
    833 }
    834 
    835 - (void)_addVisitedLinksToPageGroup:(WebCore::PageGroup&)group
    836 {
    837     [_historyPrivate addVisitedLinksToPageGroup:group];
    838 }
    839 
    840 @end
    841 
    842 WebHistoryWriter::WebHistoryWriter(DateToEntriesMap* entriesByDate)
    843     : m_entriesByDate(entriesByDate)
    844 {
    845     m_dateKeys.reserveCapacity(m_entriesByDate->size());
    846     DateToEntriesMap::const_iterator end = m_entriesByDate->end();
    847     for (DateToEntriesMap::const_iterator it = m_entriesByDate->begin(); it != end; ++it)
    848         m_dateKeys.append(it->first);
    849     sort(m_dateKeys.begin(), m_dateKeys.end());
    850 }
    851 
    852 void WebHistoryWriter::writeHistoryItems(BinaryPropertyListObjectStream& stream)
    853 {
    854     for (int dateIndex = m_dateKeys.size() - 1; dateIndex >= 0; dateIndex--) {
    855         NSArray *entries = m_entriesByDate->get(m_dateKeys[dateIndex]).get();
    856         NSUInteger entryCount = [entries count];
    857         for (NSUInteger entryIndex = 0; entryIndex < entryCount; ++entryIndex)
    858             writeHistoryItem(stream, core([entries objectAtIndex:entryIndex]));
    859     }
    860 }
    861