/* * Copyright (C) 2005, 2008 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "WebHistoryInternal.h" #import "WebHistoryItemInternal.h" #import "WebKitLogging.h" #import "WebNSURLExtras.h" #import "WebTypesInternal.h" #import #import #import #import #import #import #import using namespace WebCore; typedef int64_t WebHistoryDateKey; typedef HashMap > DateToEntriesMap; NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification"; NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification"; NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification"; NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification"; NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification"; NSString *WebHistorySavedNotification = @"WebHistorySavedNotification"; NSString *WebHistoryItemsKey = @"WebHistoryItems"; static WebHistory *_sharedHistory = nil; NSString *FileVersionKey = @"WebHistoryFileVersion"; NSString *DatesArrayKey = @"WebHistoryDates"; #define currentFileVersion 1 @interface WebHistoryPrivate : NSObject { @private NSMutableDictionary *_entriesByURL; DateToEntriesMap* _entriesByDate; NSMutableArray *_orderedLastVisitedDays; BOOL itemLimitSet; int itemLimit; BOOL ageInDaysLimitSet; int ageInDaysLimit; } - (void)addItem:(WebHistoryItem *)entry; - (void)addItems:(NSArray *)newEntries; - (BOOL)removeItem:(WebHistoryItem *)entry; - (BOOL)removeItems:(NSArray *)entries; - (BOOL)removeAllItems; - (NSArray *)orderedLastVisitedDays; - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)calendarDate; - (BOOL)containsURL:(NSURL *)URL; - (WebHistoryItem *)itemForURL:(NSURL *)URL; - (WebHistoryItem *)itemForURLString:(NSString *)URLString; - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error; - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error; - (NSCalendarDate *)ageLimitDate; - (void)setHistoryItemLimit:(int)limit; - (int)historyItemLimit; - (void)setHistoryAgeInDaysLimit:(int)limit; - (int)historyAgeInDaysLimit; - (void)addVisitedLinksToPageGroup:(PageGroup&)group; @end @implementation WebHistoryPrivate #pragma mark OBJECT FRAMEWORK + (void)initialize { [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithObjectsAndKeys: @"1000", @"WebKitHistoryItemLimit", @"7", @"WebKitHistoryAgeInDaysLimit", nil]]; } - (id)init { if (![super init]) return nil; _entriesByURL = [[NSMutableDictionary alloc] init]; _entriesByDate = new DateToEntriesMap; return self; } - (void)dealloc { [_entriesByURL release]; [_orderedLastVisitedDays release]; delete _entriesByDate; [super dealloc]; } - (void)finalize { delete _entriesByDate; [super finalize]; } #pragma mark MODIFYING CONTENTS WebHistoryDateKey timeIntervalForBeginningOfDay(NSTimeInterval interval) { CFTimeZoneRef timeZone = CFTimeZoneCopyDefault(); CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(interval, timeZone); date.hour = 0; date.minute = 0; date.second = 0; NSTimeInterval result = CFGregorianDateGetAbsoluteTime(date, timeZone); CFRelease(timeZone); // Converting from double to int64_t is safe here as NSDate's useful range // is -2**48 .. 2**47 which will safely fit in an int64_t. return (WebHistoryDateKey)result; } // Returns whether the day is already in the list of days, // and fills in *key with the key used to access its location - (BOOL)findKey:(WebHistoryDateKey*)key forDay:(NSTimeInterval)date { ASSERT_ARG(key, key != nil); *key = timeIntervalForBeginningOfDay(date); return _entriesByDate->contains(*key); } - (void)insertItem:(WebHistoryItem *)entry forDateKey:(WebHistoryDateKey)dateKey { ASSERT_ARG(entry, entry != nil); ASSERT(_entriesByDate->contains(dateKey)); NSMutableArray *entriesForDate = _entriesByDate->get(dateKey).get(); NSTimeInterval entryDate = [entry lastVisitedTimeInterval]; unsigned count = [entriesForDate count]; // The entries for each day are stored in a sorted array with the most recent entry first // Check for the common cases of the entry being newer than all existing entries or the first entry of the day if (!count || [[entriesForDate objectAtIndex:0] lastVisitedTimeInterval] < entryDate) { [entriesForDate insertObject:entry atIndex:0]; return; } // .. or older than all existing entries if (count > 0 && [[entriesForDate objectAtIndex:count - 1] lastVisitedTimeInterval] >= entryDate) { [entriesForDate insertObject:entry atIndex:count]; return; } unsigned low = 0; unsigned high = count; while (low < high) { unsigned mid = low + (high - low) / 2; if ([[entriesForDate objectAtIndex:mid] lastVisitedTimeInterval] >= entryDate) low = mid + 1; else high = mid; } // low is now the index of the first entry that is older than entryDate [entriesForDate insertObject:entry atIndex:low]; } - (BOOL)removeItemFromDateCaches:(WebHistoryItem *)entry { WebHistoryDateKey dateKey; BOOL foundDate = [self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]; if (!foundDate) return NO; DateToEntriesMap::iterator it = _entriesByDate->find(dateKey); NSMutableArray *entriesForDate = it->second.get(); [entriesForDate removeObjectIdenticalTo:entry]; // remove this date entirely if there are no other entries on it if ([entriesForDate count] == 0) { _entriesByDate->remove(it); // Clear _orderedLastVisitedDays so it will be regenerated when next requested. [_orderedLastVisitedDays release]; _orderedLastVisitedDays = nil; } return YES; } - (BOOL)removeItemForURLString:(NSString *)URLString { WebHistoryItem *entry = [_entriesByURL objectForKey:URLString]; if (!entry) return NO; [_entriesByURL removeObjectForKey:URLString]; #if ASSERT_DISABLED [self removeItemFromDateCaches:entry]; #else BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry]; ASSERT(itemWasInDateCaches); #endif if (![_entriesByURL count]) PageGroup::removeAllVisitedLinks(); return YES; } - (void)addItemToDateCaches:(WebHistoryItem *)entry { WebHistoryDateKey dateKey; if ([self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]) // other entries already exist for this date [self insertItem:entry forDateKey:dateKey]; else { // no other entries exist for this date NSMutableArray *entries = [[NSMutableArray alloc] initWithObjects:&entry count:1]; _entriesByDate->set(dateKey, entries); [entries release]; // Clear _orderedLastVisitedDays so it will be regenerated when next requested. [_orderedLastVisitedDays release]; _orderedLastVisitedDays = nil; } } - (void)addItem:(WebHistoryItem *)entry { ASSERT_ARG(entry, entry); ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0); NSString *URLString = [entry URLString]; WebHistoryItem *oldEntry = [_entriesByURL objectForKey:URLString]; if (oldEntry) { // The last reference to oldEntry might be this dictionary, so we hold onto a reference // until we're done with oldEntry. [oldEntry retain]; [self removeItemForURLString:URLString]; // If we already have an item with this URL, we need to merge info that drives the // URL autocomplete heuristics from that item into the new one. [entry _mergeAutoCompleteHints:oldEntry]; [oldEntry release]; } [self addItemToDateCaches:entry]; [_entriesByURL setObject:entry forKey:URLString]; } - (BOOL)removeItem:(WebHistoryItem *)entry { NSString *URLString = [entry URLString]; // If this exact object isn't stored, then make no change. // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is? // Maybe need to change the API to make something like removeEntryForURLString public instead. WebHistoryItem *matchingEntry = [_entriesByURL objectForKey:URLString]; if (matchingEntry != entry) return NO; [self removeItemForURLString:URLString]; return YES; } - (BOOL)removeItems:(NSArray *)entries { NSUInteger count = [entries count]; if (!count) return NO; for (NSUInteger index = 0; index < count; ++index) [self removeItem:[entries objectAtIndex:index]]; return YES; } - (BOOL)removeAllItems { if (_entriesByDate->isEmpty()) return NO; _entriesByDate->clear(); [_entriesByURL removeAllObjects]; // Clear _orderedLastVisitedDays so it will be regenerated when next requested. [_orderedLastVisitedDays release]; _orderedLastVisitedDays = nil; PageGroup::removeAllVisitedLinks(); return YES; } - (void)addItems:(NSArray *)newEntries { // There is no guarantee that the incoming entries are in any particular // order, but if this is called with a set of entries that were created by // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy // then they will be ordered chronologically from newest to oldest. We can make adding them // faster (fewer compares) by inserting them from oldest to newest. NSEnumerator *enumerator = [newEntries reverseObjectEnumerator]; while (WebHistoryItem *entry = [enumerator nextObject]) [self addItem:entry]; } #pragma mark DATE-BASED RETRIEVAL - (NSArray *)orderedLastVisitedDays { if (!_orderedLastVisitedDays) { Vector daysAsTimeIntervals; daysAsTimeIntervals.reserveCapacity(_entriesByDate->size()); DateToEntriesMap::const_iterator end = _entriesByDate->end(); for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it) daysAsTimeIntervals.append(it->first); std::sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end()); size_t count = daysAsTimeIntervals.size(); _orderedLastVisitedDays = [[NSMutableArray alloc] initWithCapacity:count]; for (int i = count - 1; i >= 0; i--) { NSTimeInterval interval = daysAsTimeIntervals[i]; NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval]; [_orderedLastVisitedDays addObject:date]; [date release]; } } return _orderedLastVisitedDays; } - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date { WebHistoryDateKey dateKey; if (![self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]]) return nil; return _entriesByDate->get(dateKey).get(); } #pragma mark URL MATCHING - (WebHistoryItem *)itemForURLString:(NSString *)URLString { return [_entriesByURL objectForKey:URLString]; } - (BOOL)containsURL:(NSURL *)URL { return [self itemForURLString:[URL _web_originalDataAsString]] != nil; } - (WebHistoryItem *)itemForURL:(NSURL *)URL { return [self itemForURLString:[URL _web_originalDataAsString]]; } #pragma mark ARCHIVING/UNARCHIVING - (void)setHistoryAgeInDaysLimit:(int)limit { ageInDaysLimitSet = YES; ageInDaysLimit = limit; } - (int)historyAgeInDaysLimit { if (ageInDaysLimitSet) return ageInDaysLimit; return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryAgeInDaysLimit"]; } - (void)setHistoryItemLimit:(int)limit { itemLimitSet = YES; itemLimit = limit; } - (int)historyItemLimit { if (itemLimitSet) return itemLimit; return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryItemLimit"]; } // Return a date that marks the age limit for history entries saved to or // loaded from disk. Any entry older than this item should be rejected. - (NSCalendarDate *)ageLimitDate { return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit] hours:0 minutes:0 seconds:0]; } // Return a flat array of WebHistoryItems. Ignores the date and item count limits; these are // respected when loading instead of when saving, so that clients can learn of discarded items // by listening to WebHistoryItemsDiscardedWhileLoadingNotification. - (NSArray *)arrayRepresentation { NSMutableArray *arrayRep = [NSMutableArray array]; Vector dateKeys; dateKeys.reserveCapacity(_entriesByDate->size()); DateToEntriesMap::const_iterator end = _entriesByDate->end(); for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it) dateKeys.append(it->first); std::sort(dateKeys.begin(), dateKeys.end()); for (int dateIndex = dateKeys.size() - 1; dateIndex >= 0; dateIndex--) { NSArray *entries = _entriesByDate->get(dateKeys[dateIndex]).get(); int entryCount = [entries count]; for (int entryIndex = 0; entryIndex < entryCount; ++entryIndex) [arrayRep addObject:[[entries objectAtIndex:entryIndex] dictionaryRepresentation]]; } return arrayRep; } - (BOOL)loadHistoryGutsFromURL:(NSURL *)URL savedItemsCount:(int *)numberOfItemsLoaded collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error { *numberOfItemsLoaded = 0; NSDictionary *dictionary = nil; // Optimize loading from local file, which is faster than using the general URL loading mechanism if ([URL isFileURL]) { dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]]; if (!dictionary) { #if !LOG_DISABLED if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]); #endif // else file doesn't exist, which is normal the first time return NO; } } else { NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error]; if (data && [data length] > 0) { dictionary = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:nil errorDescription:nil]; } } // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support // that ancient format, so anything that isn't an NSDictionary is bogus. if (![dictionary isKindOfClass:[NSDictionary class]]) return NO; NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey]; int fileVersion; // we don't trust data obtained from elsewhere, so double-check if (!fileVersionObject || ![fileVersionObject isKindOfClass:[NSNumber class]]) { LOG_ERROR("history file version can't be determined, therefore not loading"); return NO; } fileVersion = [fileVersionObject intValue]; if (fileVersion > currentFileVersion) { LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion); return NO; } NSArray *array = [dictionary objectForKey:DatesArrayKey]; int itemCountLimit = [self historyItemLimit]; NSTimeInterval ageLimitDate = [[self ageLimitDate] timeIntervalSinceReferenceDate]; NSEnumerator *enumerator = [array objectEnumerator]; BOOL ageLimitPassed = NO; BOOL itemLimitPassed = NO; ASSERT(*numberOfItemsLoaded == 0); NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSDictionary *itemAsDictionary; while ((itemAsDictionary = [enumerator nextObject]) != nil) { WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary]; // item without URL is useless; data on disk must have been bad; ignore if ([item URLString]) { // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing // once we've found the first item that's too old. if (!ageLimitPassed && [item lastVisitedTimeInterval] <= ageLimitDate) ageLimitPassed = YES; if (ageLimitPassed || itemLimitPassed) [discardedItems addObject:item]; else { [self addItem:item]; ++(*numberOfItemsLoaded); if (*numberOfItemsLoaded == itemCountLimit) itemLimitPassed = YES; // Draining the autorelease pool every 50 iterations was found by experimentation to be optimal if (*numberOfItemsLoaded % 50 == 0) { [pool drain]; pool = [[NSAutoreleasePool alloc] init]; } } } [item release]; } [pool drain]; return YES; } - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error { #if !LOG_DISABLED double start = CFAbsoluteTimeGetCurrent(); #endif int numberOfItems; if (![self loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error]) return NO; #if !LOG_DISABLED double duration = CFAbsoluteTimeGetCurrent() - start; LOG(Timing, "loading %d history entries from %@ took %f seconds", numberOfItems, URL, duration); #endif return YES; } - (BOOL)saveHistoryGuts:(int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error { *numberOfItemsSaved = 0; NSArray *array = [self arrayRepresentation]; NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: array, DatesArrayKey, [NSNumber numberWithInt:currentFileVersion], FileVersionKey, nil]; NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil]; if (![data writeToURL:URL options:0 error:error]) { LOG_ERROR("attempt to save %@ to %@ failed", dictionary, URL); return NO; } *numberOfItemsSaved = [array count]; return YES; } - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error { #if !LOG_DISABLED double start = CFAbsoluteTimeGetCurrent(); #endif int numberOfItems; if (![self saveHistoryGuts:&numberOfItems URL:URL error:error]) return NO; #if !LOG_DISABLED double duration = CFAbsoluteTimeGetCurrent() - start; LOG(Timing, "saving %d history entries to %@ took %f seconds", numberOfItems, URL, duration); #endif return YES; } - (void)addVisitedLinksToPageGroup:(PageGroup&)group { NSEnumerator *enumerator = [_entriesByURL keyEnumerator]; while (NSString *url = [enumerator nextObject]) { size_t length = [url length]; const UChar* characters = CFStringGetCharactersPtr(reinterpret_cast(url)); if (characters) group.addVisitedLink(characters, length); else { Vector buffer(length); [url getCharacters:buffer.data()]; group.addVisitedLink(buffer.data(), length); } } } @end @implementation WebHistory + (WebHistory *)optionalSharedHistory { return _sharedHistory; } + (void)setOptionalSharedHistory:(WebHistory *)history { if (_sharedHistory == history) return; // FIXME: Need to think about multiple instances of WebHistory per application // and correct synchronization of history file between applications. [_sharedHistory release]; _sharedHistory = [history retain]; PageGroup::setShouldTrackVisitedLinks(history); PageGroup::removeAllVisitedLinks(); } - (id)init { self = [super init]; if (!self) return nil; _historyPrivate = [[WebHistoryPrivate alloc] init]; return self; } - (void)dealloc { [_historyPrivate release]; [super dealloc]; } #pragma mark MODIFYING CONTENTS - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries { NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil]; [[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:userInfo]; } - (void)removeItems:(NSArray *)entries { if ([_historyPrivate removeItems:entries]) { [self _sendNotification:WebHistoryItemsRemovedNotification entries:entries]; } } - (void)removeAllItems { if ([_historyPrivate removeAllItems]) { [[NSNotificationCenter defaultCenter] postNotificationName:WebHistoryAllItemsRemovedNotification object:self]; } } - (void)addItems:(NSArray *)newEntries { [_historyPrivate addItems:newEntries]; [self _sendNotification:WebHistoryItemsAddedNotification entries:newEntries]; } #pragma mark DATE-BASED RETRIEVAL - (NSArray *)orderedLastVisitedDays { return [_historyPrivate orderedLastVisitedDays]; } - (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date { return [_historyPrivate orderedItemsLastVisitedOnDay:date]; } #pragma mark URL MATCHING - (BOOL)containsURL:(NSURL *)URL { return [_historyPrivate containsURL:URL]; } - (WebHistoryItem *)itemForURL:(NSURL *)URL { return [_historyPrivate itemForURL:URL]; } #pragma mark SAVING TO DISK - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error { NSMutableArray *discardedItems = [[NSMutableArray alloc] init]; if (![_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) { [discardedItems release]; return NO; } [[NSNotificationCenter defaultCenter] postNotificationName:WebHistoryLoadedNotification object:self]; if ([discardedItems count]) [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems]; [discardedItems release]; return YES; } - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error { if (![_historyPrivate saveToURL:URL error:error]) return NO; [[NSNotificationCenter defaultCenter] postNotificationName:WebHistorySavedNotification object:self]; return YES; } - (void)setHistoryItemLimit:(int)limit { [_historyPrivate setHistoryItemLimit:limit]; } - (int)historyItemLimit { return [_historyPrivate historyItemLimit]; } - (void)setHistoryAgeInDaysLimit:(int)limit { [_historyPrivate setHistoryAgeInDaysLimit:limit]; } - (int)historyAgeInDaysLimit { return [_historyPrivate historyAgeInDaysLimit]; } @end @implementation WebHistory (WebPrivate) - (WebHistoryItem *)_itemForURLString:(NSString *)URLString { return [_historyPrivate itemForURLString:URLString]; } @end @implementation WebHistory (WebInternal) - (void)_addItemForURL:(NSURL *)URL title:(NSString *)title { WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:title]; [entry _setLastVisitedTimeInterval:[NSDate timeIntervalSinceReferenceDate]]; LOG(History, "adding %@", entry); [_historyPrivate addItem:entry]; [self _sendNotification:WebHistoryItemsAddedNotification entries:[NSArray arrayWithObject:entry]]; [entry release]; } - (void)_addVisitedLinksToPageGroup:(WebCore::PageGroup&)group { [_historyPrivate addVisitedLinksToPageGroup:group]; } @end