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
39using namespace WebCore;
40using namespace std;
41
42typedef int64_t WebHistoryDateKey;
43typedef HashMap<WebHistoryDateKey, RetainPtr<NSMutableArray> > DateToEntriesMap;
44
45NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
46NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
47NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
48NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
49NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification";
50NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
51NSString *WebHistoryItemsKey = @"WebHistoryItems";
52
53static WebHistory *_sharedHistory = nil;
54
55NSString *FileVersionKey = @"WebHistoryFileVersion";
56NSString *DatesArrayKey = @"WebHistoryDates";
57
58#define currentFileVersion 1
59
60class WebHistoryWriter : public HistoryPropertyListWriter {
61public:
62    WebHistoryWriter(DateToEntriesMap*);
63
64private:
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// 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// MARK: MODIFYING CONTENTS
150
151static 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
164static 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
173static 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// 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// 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// 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// 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// 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// 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// 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
842WebHistoryWriter::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
852void 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