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