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