blob: 0408eb57a9c0a117f36113a593ebfe48ac1385bd [file] [log] [blame]
/*
* Copyright (C) 2005-2018 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 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 "HistoryPropertyList.h"
#import "WebHistoryItemInternal.h"
#import "WebKitLogging.h"
#import "WebNSURLExtras.h"
#import "WebVisitedLinkStore.h"
#import <WebCore/HistoryItem.h>
#import <pal/spi/cocoa/NSCalendarDateSPI.h>
#if PLATFORM(IOS_FAMILY)
#import <WebCore/WebCoreThreadMessage.h>
#endif
using namespace WebCore;
typedef int64_t WebHistoryDateKey;
typedef HashMap<WebHistoryDateKey, RetainPtr<NSMutableArray>> DateToEntriesMap;
NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification";
NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
NSString *WebHistoryItemsKey = @"WebHistoryItems";
static RetainPtr<WebHistory>& sharedHistory()
{
static NeverDestroyed<RetainPtr<WebHistory>> _sharedHistory;
return _sharedHistory;
}
NSString *FileVersionKey = @"WebHistoryFileVersion";
NSString *DatesArrayKey = @"WebHistoryDates";
#define currentFileVersion 1
class WebHistoryWriter : public HistoryPropertyListWriter {
public:
WebHistoryWriter(DateToEntriesMap*);
private:
virtual void writeHistoryItems(BinaryPropertyListObjectStream&);
DateToEntriesMap* m_entriesByDate;
Vector<int> m_dateKeys;
};
@interface WebHistory ()
- (void)_sendNotification:(NSString *)name entries:(NSArray *)entries;
@end
@interface WebHistoryPrivate : NSObject {
@private
RetainPtr<NSMutableDictionary> _entriesByURL;
std::unique_ptr<DateToEntriesMap> _entriesByDate;
RetainPtr<NSMutableArray> _orderedLastVisitedDays;
BOOL itemLimitSet;
int itemLimit;
BOOL ageInDaysLimitSet;
int ageInDaysLimit;
}
- (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title;
- (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate;
- (void)addItems:(NSArray *)newEntries;
- (BOOL)removeItem:(WebHistoryItem *)entry;
- (BOOL)removeItems:(NSArray *)entries;
- (BOOL)removeAllItems;
- (void)rebuildHistoryByDayIfNeeded:(WebHistory *)webHistory;
- (NSArray *)orderedLastVisitedDays;
- (BOOL)containsURL:(NSURL *)URL;
- (WebHistoryItem *)itemForURL:(NSURL *)URL;
- (WebHistoryItem *)itemForURLString:(NSString *)URLString;
- (NSArray *)allItems;
- (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error;
- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error;
- (void)setHistoryItemLimit:(int)limit;
- (int)historyItemLimit;
- (void)setHistoryAgeInDaysLimit:(int)limit;
- (int)historyAgeInDaysLimit;
@end
@implementation WebHistoryPrivate
// MARK: OBJECT FRAMEWORK
+ (void)initialize
{
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
@"WebKitHistoryItemLimit": @"1000",
@"WebKitHistoryAgeInDaysLimit": @"7",
}];
}
- (id)init
{
self = [super init];
if (!self)
return nil;
_entriesByURL = adoptNS([[NSMutableDictionary alloc] init]);
_entriesByDate = makeUnique<DateToEntriesMap>();
return self;
}
// MARK: MODIFYING CONTENTS
static void getDayBoundaries(NSTimeInterval interval, NSTimeInterval& beginningOfDay, NSTimeInterval& beginningOfNextDay)
{
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:interval];
NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
NSDate *beginningOfDayDate = nil;
NSTimeInterval dayLength;
[calendar rangeOfUnit:NSCalendarUnitDay startDate:&beginningOfDayDate interval:&dayLength forDate:date];
beginningOfDay = beginningOfDayDate.timeIntervalSinceReferenceDate;
beginningOfNextDay = beginningOfDay + dayLength;
}
static inline NSTimeInterval beginningOfDay(NSTimeInterval date)
{
static NSTimeInterval cachedBeginningOfDay = NAN;
static NSTimeInterval cachedBeginningOfNextDay;
if (!(date >= cachedBeginningOfDay && date < cachedBeginningOfNextDay))
getDayBoundaries(date, cachedBeginningOfDay, cachedBeginningOfNextDay);
return cachedBeginningOfDay;
}
static inline WebHistoryDateKey dateKey(NSTimeInterval date)
{
// Converting from double (NSTimeInterval) to int64_t (WebHistoryDateKey) is
// safe here because all sensible dates are in the range -2**48 .. 2**47 which
// safely fits in an int64_t.
return beginningOfDay(date);
}
// 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);
*key = dateKey(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->value.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 = nil;
}
return YES;
}
- (BOOL)removeItemForURLString:(NSString *)URLString
{
WebHistoryItem *entry = [_entriesByURL objectForKey:URLString];
if (!entry)
return NO;
[_entriesByURL removeObjectForKey:URLString];
#if !ASSERT_ENABLED
[self removeItemFromDateCaches:entry];
#else
BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry];
ASSERT(itemWasInDateCaches);
#endif
if (![_entriesByURL count])
WebVisitedLinkStore::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
auto entries = adoptNS([[NSMutableArray alloc] initWithObjects:&entry count:1]);
_entriesByDate->set(dateKey, entries.get());
// Clear _orderedLastVisitedDays so it will be regenerated when next requested.
_orderedLastVisitedDays = nil;
}
}
- (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title
{
ASSERT(url);
ASSERT(title);
NSString *URLString = [url _web_originalDataAsString];
if (!URLString)
URLString = @"";
auto entry = retainPtr([_entriesByURL objectForKey:URLString]);
if (entry) {
LOG(History, "Updating global history entry %@", entry.get());
// Remove the item from date caches before changing its last visited date. Otherwise we might get duplicate entries
// as seen in <rdar://problem/6570573>.
BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry.get()];
ASSERT_UNUSED(itemWasInDateCaches, itemWasInDateCaches);
[entry _visitedWithTitle:title];
} else {
LOG(History, "Adding new global history entry for %@", url);
entry = adoptNS([[WebHistoryItem alloc] initWithURLString:URLString title:title lastVisitedTimeInterval:[NSDate timeIntervalSinceReferenceDate]]);
[_entriesByURL setObject:entry.get() forKey:URLString];
}
[self addItemToDateCaches:entry.get()];
return entry.autorelease();
}
- (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate
{
ASSERT_ARG(entry, entry);
ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0);
NSString *URLString = [entry URLString];
#if !PLATFORM(IOS_FAMILY)
if (auto oldEntry = retainPtr([_entriesByURL objectForKey:URLString])) {
if (discardDuplicate)
return NO;
[self removeItemForURLString:URLString];
}
[self addItemToDateCaches:entry];
[_entriesByURL setObject:entry forKey:URLString];
#else
if (auto otherEntry = retainPtr([_entriesByURL objectForKey:URLString])) {
if (discardDuplicate)
return NO;
if ([otherEntry lastVisitedTimeInterval] < [entry lastVisitedTimeInterval]) {
[self removeItemForURLString:URLString];
[self addItemToDateCaches:entry];
[_entriesByURL setObject:entry forKey:URLString];
} else
return NO; // Special case for merges when new items may be older than pre-existing entries.
} else {
[self addItemToDateCaches:entry];
[_entriesByURL setObject:entry forKey:URLString];
}
#endif
return YES;
}
- (void)rebuildHistoryByDayIfNeeded:(WebHistory *)webHistory
{
// We clear all the values to present a consistent state when sending the notifications.
// We keep a reference to the entries for rebuilding the history after the notification.
auto entryArrays = copyToVector(_entriesByDate->values());
_entriesByDate->clear();
auto entriesByURL = std::exchange(_entriesByURL, nil);
_orderedLastVisitedDays = nil;
NSArray *allEntries = [entriesByURL allValues];
[webHistory _sendNotification:WebHistoryAllItemsRemovedNotification entries:allEntries];
// Next, we rebuild the history, restore the states, and notify the clients.
_entriesByURL = entriesByURL;
for (size_t dayIndex = 0; dayIndex < entryArrays.size(); ++dayIndex) {
for (WebHistoryItem *entry in (entryArrays[dayIndex]).get())
[self addItemToDateCaches:entry];
}
[webHistory _sendNotification:WebHistoryItemsAddedNotification entries:allEntries];
}
- (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 = nil;
WebVisitedLinkStore::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 discardDuplicate:NO];
}
// MARK: DATE-BASED RETRIEVAL
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
- (NSArray *)orderedLastVisitedDays
{
if (!_orderedLastVisitedDays) {
Vector<int> daysAsTimeIntervals;
daysAsTimeIntervals.reserveCapacity(_entriesByDate->size());
DateToEntriesMap::const_iterator end = _entriesByDate->end();
for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it)
daysAsTimeIntervals.append(it->key);
std::sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end());
size_t count = daysAsTimeIntervals.size();
_orderedLastVisitedDays = adoptNS([[NSMutableArray alloc] initWithCapacity:count]);
for (int i = count - 1; i >= 0; i--) {
NSTimeInterval interval = daysAsTimeIntervals[i];
auto date = adoptNS([[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval]);
[_orderedLastVisitedDays addObject:date.get()];
}
}
return _orderedLastVisitedDays.get();
}
- (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date
{
WebHistoryDateKey dateKey;
if (![self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]])
return nil;
return _entriesByDate->get(dateKey).get();
}
ALLOW_DEPRECATED_DECLARATIONS_END
// 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]];
}
- (NSArray *)allItems
{
return [_entriesByURL allValues];
}
// 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"];
}
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
// 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];
}
ALLOW_DEPRECATED_DECLARATIONS_END
- (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 {
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
ALLOW_DEPRECATED_DECLARATIONS_END
if (data.length)
dictionary = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nullptr error:nullptr];
}
// 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];
BOOL ageLimitPassed = NO;
BOOL itemLimitPassed = NO;
ASSERT(*numberOfItemsLoaded == 0);
for (NSDictionary *itemAsDictionary in array) {
@autoreleasepool {
auto item = adoptNS([[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.get()];
else {
if ([self addItem:item.get() discardDuplicate:YES])
++(*numberOfItemsLoaded);
if (*numberOfItemsLoaded == itemCountLimit)
itemLimitPassed = YES;
}
}
}
}
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;
}
- (NSData *)data
{
if (_entriesByDate->isEmpty()) {
static NSData *emptyHistoryData = [[NSData alloc] init];
return emptyHistoryData;
}
// 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.
WebHistoryWriter writer(_entriesByDate.get());
writer.writePropertyList();
return retainPtr((NSData *)writer.releaseData().get()).autorelease();
}
- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
{
#if !LOG_DISABLED
double start = CFAbsoluteTimeGetCurrent();
#endif
BOOL result = [[self data] writeToURL:URL options:0 error:error];
#if !LOG_DISABLED
double duration = CFAbsoluteTimeGetCurrent() - start;
LOG(Timing, "saving history to %@ took %f seconds", URL, duration);
#endif
return result;
}
- (void)addVisitedLinksToVisitedLinkStore:(WebVisitedLinkStore&)visitedLinkStore
{
for (NSString *urlString in _entriesByURL.get())
visitedLinkStore.addVisitedLink(urlString);
}
@end
@implementation WebHistory
+ (WebHistory *)optionalSharedHistory
{
return sharedHistory().get();
}
+ (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() = history;
WebVisitedLinkStore::setShouldTrackVisitedLinks(history);
WebVisitedLinkStore::removeAllVisitedLinks();
}
- (void)timeZoneChanged:(NSNotification *)notification
{
[_historyPrivate rebuildHistoryByDayIfNeeded:self];
}
- (id)init
{
self = [super init];
if (!self)
return nil;
_historyPrivate = [[WebHistoryPrivate alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(timeZoneChanged:)
name:NSSystemTimeZoneDidChangeNotification
object:nil];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSSystemTimeZoneDidChangeNotification
object:nil];
[_historyPrivate release];
[super dealloc];
}
// MARK: MODIFYING CONTENTS
- (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
{
NSDictionary *userInfo = @{ WebHistoryItemsKey: entries };
#if PLATFORM(IOS_FAMILY)
WebThreadPostNotification(name, self, userInfo);
#else
[[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:userInfo];
#endif
}
- (void)removeItems:(NSArray *)entries
{
if ([_historyPrivate removeItems:entries]) {
[self _sendNotification:WebHistoryItemsRemovedNotification
entries:entries];
}
}
- (void)removeAllItems
{
NSArray *entries = [_historyPrivate allItems];
if ([_historyPrivate removeAllItems])
[self _sendNotification:WebHistoryAllItemsRemovedNotification entries:entries];
}
- (void)addItems:(NSArray *)newEntries
{
[_historyPrivate addItems:newEntries];
[self _sendNotification:WebHistoryItemsAddedNotification
entries:newEntries];
}
// MARK: DATE-BASED RETRIEVAL
- (NSArray *)orderedLastVisitedDays
{
return [_historyPrivate orderedLastVisitedDays];
}
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
- (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date
{
return [_historyPrivate orderedItemsLastVisitedOnDay:date];
}
ALLOW_DEPRECATED_DECLARATIONS_END
// MARK: URL MATCHING
- (BOOL)containsURL:(NSURL *)URL
{
return [_historyPrivate containsURL:URL];
}
- (WebHistoryItem *)itemForURL:(NSURL *)URL
{
return [_historyPrivate itemForURL:URL];
}
// MARK: SAVING TO DISK
- (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
{
auto discardedItems = adoptNS([[NSMutableArray alloc] init]);
if (![_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems.get() error:error])
return NO;
#if PLATFORM(IOS_FAMILY)
WebThreadPostNotification(WebHistoryLoadedNotification, self, nil);
#else
[[NSNotificationCenter defaultCenter]
postNotificationName:WebHistoryLoadedNotification
object:self];
#endif
if ([discardedItems count])
[self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems.get()];
return YES;
}
- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
{
if (![_historyPrivate saveToURL:URL error:error])
return NO;
#if PLATFORM(IOS_FAMILY)
WebThreadPostNotification(WebHistorySavedNotification, self, nil);
#else
[[NSNotificationCenter defaultCenter]
postNotificationName:WebHistorySavedNotification
object:self];
#endif
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];
}
- (NSArray *)allItems
{
return [_historyPrivate allItems];
}
- (NSData *)_data
{
return [_historyPrivate data];
}
+ (void)_setVisitedLinkTrackingEnabled:(BOOL)visitedLinkTrackingEnabled
{
WebVisitedLinkStore::setShouldTrackVisitedLinks(visitedLinkTrackingEnabled);
}
+ (void)_removeAllVisitedLinks
{
WebVisitedLinkStore::removeAllVisitedLinks();
}
@end
@implementation WebHistory (WebInternal)
- (void)_visitedURL:(NSURL *)url withTitle:(NSString *)title method:(NSString *)method wasFailure:(BOOL)wasFailure
{
WebHistoryItem *entry = [_historyPrivate visitedURL:url withTitle:title];
HistoryItem* item = core(entry);
item->setLastVisitWasFailure(wasFailure);
entry->_private->_redirectURLs = nullptr;
[self _sendNotification:WebHistoryItemsAddedNotification entries:@[entry]];
}
- (void)_addVisitedLinksToVisitedLinkStore:(WebVisitedLinkStore &)visitedLinkStore
{
[_historyPrivate addVisitedLinksToVisitedLinkStore:visitedLinkStore];
}
@end
WebHistoryWriter::WebHistoryWriter(DateToEntriesMap* entriesByDate)
: m_entriesByDate(entriesByDate)
{
m_dateKeys.reserveCapacity(m_entriesByDate->size());
DateToEntriesMap::const_iterator end = m_entriesByDate->end();
for (DateToEntriesMap::const_iterator it = m_entriesByDate->begin(); it != end; ++it)
m_dateKeys.append(it->key);
std::sort(m_dateKeys.begin(), m_dateKeys.end());
}
void WebHistoryWriter::writeHistoryItems(BinaryPropertyListObjectStream& stream)
{
for (int dateIndex = m_dateKeys.size() - 1; dateIndex >= 0; dateIndex--) {
NSArray *entries = m_entriesByDate->get(m_dateKeys[dateIndex]).get();
NSUInteger entryCount = [entries count];
for (NSUInteger entryIndex = 0; entryIndex < entryCount; ++entryIndex)
writeHistoryItem(stream, [entries objectAtIndex:entryIndex]);
}
}