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