Reviewed by Darin.
[WebKit-https.git] / WebKit / History.subproj / WebHistory.m
1 //
2 //  WebHistory.m
3 //  WebKit
4 //
5 //  Created by John Sullivan on Mon Feb 18 2002.
6 //  Copyright (c) 2002 Apple Computer, Inc. All rights reserved.
7 //
8
9 #import <WebKit/WebHistory.h>
10 #import <WebKit/WebHistoryPrivate.h>
11
12 #import <WebKit/WebHistoryItem.h>
13 #import <WebKit/WebHistoryItemPrivate.h>
14 #import <WebKit/WebKitLogging.h>
15 #import <WebKit/WebNSURLExtras.h>
16 #import <Foundation/NSError.h>
17 #import <WebKit/WebAssertions.h>
18 #import <WebCore/WebCoreHistory.h>
19
20 #import <Foundation/NSCalendarDate_NSURLExtras.h>
21
22
23 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
24 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
25 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
26 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
27 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
28 NSString *WebHistoryItemsKey = @"WebHistoryItems";
29
30 static WebHistory *_sharedHistory = nil;
31
32
33
34 NSString *FileVersionKey = @"WebHistoryFileVersion";
35 NSString *DatesArrayKey = @"WebHistoryDates";
36
37 #define currentFileVersion      1
38
39 @implementation WebHistoryPrivate
40
41 #pragma mark OBJECT FRAMEWORK
42
43 + (void)initialize
44 {
45     [[NSUserDefaults standardUserDefaults] registerDefaults:
46         [NSDictionary dictionaryWithObjectsAndKeys:
47             @"1000", @"WebKitHistoryItemLimit",
48             @"7", @"WebKitHistoryAgeInDaysLimit",
49             nil]];    
50 }
51
52 - (id)init
53 {
54     if (![super init]) {
55         return nil;
56     }
57     
58     _entriesByURL = [[NSMutableDictionary alloc] init];
59     _datesWithEntries = [[NSMutableArray alloc] init];
60     _entriesByDate = [[NSMutableArray alloc] init];
61
62     return self;
63 }
64
65 - (void)dealloc
66 {
67     [_entriesByURL release];
68     [_datesWithEntries release];
69     [_entriesByDate release];
70     
71     [super dealloc];
72 }
73
74 #pragma mark MODIFYING CONTENTS
75
76 // Returns whether the day is already in the list of days,
77 // and fills in *index with the found or proposed index.
78 - (BOOL)findIndex: (int *)index forDay: (NSCalendarDate *)date
79 {
80     int count;
81
82     ASSERT_ARG(index, index != nil);
83
84     //FIXME: just does linear search through days; inefficient if many days
85     count = [_datesWithEntries count];
86     for (*index = 0; *index < count; ++*index) {
87         NSComparisonResult result = [date _web_compareDay: [_datesWithEntries objectAtIndex: *index]];
88         if (result == NSOrderedSame) {
89             return YES;
90         }
91         if (result == NSOrderedDescending) {
92             return NO;
93         }
94     }
95
96     return NO;
97 }
98
99 - (void)insertItem: (WebHistoryItem *)entry atDateIndex: (int)dateIndex
100 {
101     int index, count;
102     NSMutableArray *entriesForDate;
103     NSCalendarDate *entryDate;
104
105     ASSERT_ARG(entry, entry != nil);
106     ASSERT_ARG(dateIndex, dateIndex >= 0 && (uint)dateIndex < [_entriesByDate count]);
107
108     //FIXME: just does linear search through entries; inefficient if many entries for this date
109     entryDate = [entry _lastVisitedDate];
110     entriesForDate = [_entriesByDate objectAtIndex: dateIndex];
111     count = [entriesForDate count];
112     // optimized for inserting oldest to youngest
113     for (index = 0; index < count; ++index) {
114         if ([entryDate compare: [[entriesForDate objectAtIndex: index] _lastVisitedDate]] != NSOrderedAscending) {
115             break;
116         }
117     }
118
119     [entriesForDate insertObject: entry atIndex: index];
120 }
121
122 - (BOOL)removeItemForURLString: (NSString *)URLString
123 {
124     NSMutableArray *entriesForDate;
125     WebHistoryItem *entry;
126     int dateIndex;
127     BOOL foundDate;
128
129     entry = [_entriesByURL objectForKey: URLString];
130     if (entry == nil) {
131         return NO;
132     }
133
134     [_entriesByURL removeObjectForKey: URLString];
135
136     foundDate = [self findIndex: &dateIndex forDay: [entry _lastVisitedDate]];
137
138     ASSERT(foundDate);
139     
140     entriesForDate = [_entriesByDate objectAtIndex: dateIndex];
141     [entriesForDate removeObjectIdenticalTo: entry];
142
143     // remove this date entirely if there are no other entries on it
144     if ([entriesForDate count] == 0) {
145         [_entriesByDate removeObjectAtIndex: dateIndex];
146         [_datesWithEntries removeObjectAtIndex: dateIndex];
147     }
148
149     return YES;
150 }
151
152
153 - (void)addItem: (WebHistoryItem *)entry
154 {
155     int dateIndex;
156     NSString *URLString;
157
158     ASSERT_ARG(entry, entry);
159     ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0);
160
161     URLString = [entry URLString];
162
163     // If we already have an item with this URL, we need to merge info that drives the
164     // URL autocomplete heuristics from that item into the new one.
165     WebHistoryItem *oldEntry = [_entriesByURL objectForKey: URLString];
166     if (oldEntry) {
167         [entry _mergeAutoCompleteHints:oldEntry];
168     }
169
170     [self removeItemForURLString: URLString];
171
172     if ([self findIndex: &dateIndex forDay: [entry _lastVisitedDate]]) {
173         // other entries already exist for this date
174         [self insertItem: entry atDateIndex: dateIndex];
175     } else {
176         // no other entries exist for this date
177         [_datesWithEntries insertObject: [entry _lastVisitedDate] atIndex: dateIndex];
178         [_entriesByDate insertObject: [NSMutableArray arrayWithObject:entry] atIndex: dateIndex];
179     }
180
181     [_entriesByURL setObject: entry forKey: URLString];
182 }
183
184 - (BOOL)removeItem: (WebHistoryItem *)entry
185 {
186     WebHistoryItem *matchingEntry;
187     NSString *URLString;
188
189     URLString = [entry URLString];
190
191     // If this exact object isn't stored, then make no change.
192     // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is?
193     // Maybe need to change the API to make something like removeEntryForURLString public instead.
194     matchingEntry = [_entriesByURL objectForKey: URLString];
195     if (matchingEntry != entry) {
196         return NO;
197     }
198
199     [self removeItemForURLString: URLString];
200
201     return YES;
202 }
203
204 - (BOOL)removeItems: (NSArray *)entries
205 {
206     int index, count;
207
208     count = [entries count];
209     if (count == 0) {
210         return NO;
211     }
212
213     for (index = 0; index < count; ++index) {
214         [self removeItem:[entries objectAtIndex:index]];
215     }
216     
217     return YES;
218 }
219
220 - (BOOL)removeAllItems
221 {
222     if ([_entriesByURL count] == 0) {
223         return NO;
224     }
225
226     [_entriesByDate removeAllObjects];
227     [_datesWithEntries removeAllObjects];
228     [_entriesByURL removeAllObjects];
229
230     return YES;
231 }
232
233 - (void)addItems:(NSArray *)newEntries
234 {
235     NSEnumerator *enumerator;
236     WebHistoryItem *entry;
237
238     // There is no guarantee that the incoming entries are in any particular
239     // order, but if this is called with a set of entries that were created by
240     // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy
241     // then they will be ordered chronologically from newest to oldest. We can make adding them
242     // faster (fewer compares) by inserting them from oldest to newest.
243     enumerator = [newEntries reverseObjectEnumerator];
244     while ((entry = [enumerator nextObject]) != nil) {
245         [self addItem:entry];
246     }
247 }
248
249 #pragma mark DATE-BASED RETRIEVAL
250
251 - (NSArray *)orderedLastVisitedDays
252 {
253     return _datesWithEntries;
254 }
255
256 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
257 {
258     int index;
259
260     if ([self findIndex: &index forDay: date]) {
261         return [_entriesByDate objectAtIndex: index];
262     }
263
264     return nil;
265 }
266
267 #pragma mark URL MATCHING
268
269 - (WebHistoryItem *)itemForURLString:(NSString *)URLString
270 {
271     return [_entriesByURL objectForKey: URLString];
272 }
273
274 - (BOOL)containsItemForURLString: (NSString *)URLString
275 {
276     return [self itemForURLString:URLString] != nil;
277 }
278
279 - (BOOL)containsURL: (NSURL *)URL
280 {
281     return [self itemForURLString:[URL _web_originalDataAsString]] != nil;
282 }
283
284 - (WebHistoryItem *)itemForURL:(NSURL *)URL
285 {
286     return [self itemForURLString:[URL _web_originalDataAsString]];
287 }       
288
289 #pragma mark ARCHIVING/UNARCHIVING
290
291 - (void)setHistoryAgeInDaysLimit:(int)limit
292 {
293     ageInDaysLimitSet = YES;
294     ageInDaysLimit = limit;
295 }
296
297 - (int)historyAgeInDaysLimit
298 {
299     if (ageInDaysLimitSet)
300         return ageInDaysLimit;
301     return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryAgeInDaysLimit"];
302 }
303
304 - (void)setHistoryItemLimit:(int)limit
305 {
306     itemLimitSet = YES;
307     itemLimit = limit;
308 }
309
310 - (int)historyItemLimit
311 {
312     if (itemLimitSet)
313         return itemLimit;
314     return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"];
315 }
316
317 // Return a date that marks the age limit for history entries saved to or
318 // loaded from disk. Any entry on this day or older should be rejected,
319 // as tested with -[NSCalendarDate compareDay:]
320 - (NSCalendarDate *)_ageLimitDate
321 {
322     return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit]
323                                                       hours:0 minutes:0 seconds:0];
324 }
325
326 // Return a flat array of WebHistoryItems. Leaves out entries older than the age limit.
327 // Stops filling array when item count limit is reached, even if there are currently
328 // more entries than that.
329 - (NSArray *)arrayRepresentation
330 {
331     int dateCount, dateIndex;
332     int limit;
333     int totalSoFar;
334     NSMutableArray *arrayRep;
335     NSCalendarDate *ageLimitDate;
336
337     arrayRep = [NSMutableArray array];
338
339     limit = [self historyItemLimit];
340     ageLimitDate = [self _ageLimitDate];
341     totalSoFar = 0;
342     
343     dateCount = [_entriesByDate count];
344     for (dateIndex = 0; dateIndex < dateCount; ++dateIndex) {
345         int entryCount, entryIndex;
346         NSArray *entries;
347
348         // skip remaining days if they are older than the age limit
349         if ([[_datesWithEntries objectAtIndex:dateIndex] _web_compareDay:ageLimitDate] != NSOrderedDescending) {
350             break;
351         }
352
353         entries = [_entriesByDate objectAtIndex:dateIndex];
354         entryCount = [entries count];
355         for (entryIndex = 0; entryIndex < entryCount; ++entryIndex) {
356             if (totalSoFar++ >= limit) {
357                 break;
358             }
359             [arrayRep addObject: [[entries objectAtIndex:entryIndex] dictionaryRepresentation]];
360         }
361     }
362
363     return arrayRep;
364 }
365
366 - (BOOL)_loadHistoryGuts: (int *)numberOfItemsLoaded URL:(NSURL *)URL error:(NSError **)error
367 {
368     *numberOfItemsLoaded = 0;
369
370     NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
371     id propertyList = nil;
372     if (data && [data length] > 0) {
373         propertyList = [NSPropertyListSerialization propertyListFromData:data
374                                                         mutabilityOption:NSPropertyListImmutable
375                                                                   format:nil
376                                                         errorDescription:nil];
377     }
378
379     // propertyList might be an old-style NSArray or a more modern NSDictionary.
380     // If it's an NSArray, convert it to new format before further processing.
381     NSDictionary *fileAsDictionary = nil;
382     if ([propertyList isKindOfClass:[NSDictionary class]]) {
383         fileAsDictionary = propertyList;
384     } else if ([propertyList isKindOfClass:[NSArray class]]) {
385         // Convert old-style array into new-style dictionary
386         fileAsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
387             propertyList, DatesArrayKey,
388             [NSNumber numberWithInt:1], FileVersionKey,
389             nil];
390     } else {
391         if ([URL isFileURL] && [[NSFileManager defaultManager] fileExistsAtPath: [URL path]]) {
392             ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]);
393         }
394         return NO;
395     }
396
397     NSNumber *fileVersionObject = [fileAsDictionary objectForKey:FileVersionKey];
398     int fileVersion;
399     // we don't trust data read from disk, so double-check
400     if (fileVersionObject != nil && [fileVersionObject isKindOfClass:[NSNumber class]]) {
401         fileVersion = [fileVersionObject intValue];
402     } else {
403         ERROR("history file version can't be determined, therefore not loading");
404         return NO;
405     }
406     if (fileVersion > currentFileVersion) {
407         ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion);
408         return NO;
409     }    
410
411     NSArray *array = [fileAsDictionary objectForKey:DatesArrayKey];
412         
413     int limit = [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"];
414     NSCalendarDate *ageLimitDate = [self _ageLimitDate];
415     int index = 0;
416     // reverse dates so you're loading the oldest first, to minimize the number of comparisons
417     NSEnumerator *enumerator = [array reverseObjectEnumerator];
418     BOOL ageLimitPassed = NO;
419
420     NSDictionary *itemAsDictionary;
421     while ((itemAsDictionary = [enumerator nextObject]) != nil) {
422         WebHistoryItem *entry;
423
424         entry = [[[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary] autorelease];
425
426         if ([entry URLString] == nil) {
427             // entry without URL is useless; data on disk must have been bad; ignore
428             continue;
429         }
430
431         // test against date limit
432         if (!ageLimitPassed) {
433             if ([[entry _lastVisitedDate] _web_compareDay:ageLimitDate] != NSOrderedDescending) {
434                 continue;
435             } else {
436                 ageLimitPassed = YES;
437             }
438         }
439         
440         [self addItem: entry];
441         if (++index >= limit) {
442             break;
443         }
444     }
445
446     *numberOfItemsLoaded = MIN(index, limit);
447     return YES;    
448 }
449
450 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
451 {
452     int numberOfItems;
453     double start, duration;
454     BOOL result;
455
456     start = CFAbsoluteTimeGetCurrent();
457     result = [self _loadHistoryGuts: &numberOfItems URL:URL error:error];
458
459     if (result) {
460         duration = CFAbsoluteTimeGetCurrent() - start;
461         LOG(Timing, "loading %d history entries from %@ took %f seconds",
462             numberOfItems, URL, duration);
463     }
464
465     return result;
466 }
467
468 - (BOOL)_saveHistoryGuts: (int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error
469 {
470     *numberOfItemsSaved = 0;
471
472     // FIXME:  Correctly report error when new API is ready.
473     if (error)
474         *error = nil;
475
476     NSArray *array = [self arrayRepresentation];
477     NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
478         array, DatesArrayKey,
479         [NSNumber numberWithInt:currentFileVersion], FileVersionKey,
480         nil];
481     NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
482     if (![data writeToURL:URL atomically:YES]) {
483         ERROR("attempt to save %@ to %@ failed", dictionary, URL);
484         return NO;
485     }
486     
487     *numberOfItemsSaved = [array count];
488     return YES;
489 }
490
491 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
492 {
493     int numberOfItems;
494     double start, duration;
495     BOOL result;
496
497     start = CFAbsoluteTimeGetCurrent();
498     result = [self _saveHistoryGuts: &numberOfItems URL:URL error:error];
499
500     if (result) {
501         duration = CFAbsoluteTimeGetCurrent() - start;
502         LOG(Timing, "saving %d history entries to %@ took %f seconds",
503             numberOfItems, URL, duration);
504     }
505
506     return result;
507 }
508
509 @end
510
511 @interface _WebCoreHistoryProvider : NSObject  <WebCoreHistoryProvider> 
512 {
513     WebHistory *history;
514 }
515 - initWithHistory: (WebHistory *)h;
516 @end
517
518 @implementation _WebCoreHistoryProvider
519 - initWithHistory: (WebHistory *)h
520 {
521     history = [h retain];
522     return self;
523 }
524
525 static inline bool matchLetter(char c, char lowercaseLetter)
526 {
527     return (c | 0x20) == lowercaseLetter;
528 }
529
530 static inline bool matchUnicodeLetter(UniChar c, UniChar lowercaseLetter)
531 {
532     return (c | 0x20) == lowercaseLetter;
533 }
534
535 #define BUFFER_SIZE 2048
536
537 - (BOOL)containsItemForURLLatin1:(const char *)latin1 length:(unsigned)length
538 {
539     const char *latin1Str = latin1;
540     char staticStrBuffer[BUFFER_SIZE];
541     char *strBuffer = NULL;
542     BOOL needToAddSlash = FALSE;
543
544     if (length >= 6 &&
545         matchLetter(latin1[0], 'h') &&
546         matchLetter(latin1[1], 't') &&
547         matchLetter(latin1[2], 't') &&
548         matchLetter(latin1[3], 'p') &&
549         (latin1[4] == ':' 
550          || (matchLetter(latin1[4], 's') && latin1[5] == ':'))) {
551         int pos = latin1[4] == ':' ? 5 : 6;
552         // skip possible initial two slashes
553         if (latin1[pos] == '/' && latin1[pos + 1] == '/') {
554             pos += 2;
555         }
556
557         char *nextSlash = strchr(latin1 + pos, '/');
558         if (nextSlash == NULL) {
559             needToAddSlash = TRUE;
560         }
561     }
562
563     if (needToAddSlash) {
564         if (length + 1 <= 2048) {
565             strBuffer = staticStrBuffer;
566         } else {
567             strBuffer = malloc(length + 2);
568         }
569         memcpy(strBuffer, latin1, length + 1);
570         strBuffer[length] = '/';
571         strBuffer[length+1] = '\0';
572         length++;
573
574         latin1Str = strBuffer;
575     }
576
577     CFStringRef str = CFStringCreateWithCStringNoCopy(NULL, latin1Str, kCFStringEncodingWindowsLatin1, kCFAllocatorNull);
578     BOOL result = [history containsItemForURLString:(id)str];
579     CFRelease(str);
580
581     if (strBuffer != staticStrBuffer) {
582         free(strBuffer);
583     }
584
585     return result;
586 }
587
588 - (BOOL)containsItemForURLUnicode:(const UniChar *)unicode length:(unsigned)length
589 {
590     const UniChar *unicodeStr = unicode;
591     UniChar staticStrBuffer[1024];
592     UniChar *strBuffer = NULL;
593     BOOL needToAddSlash = FALSE;
594
595     if (length >= 6 &&
596         matchUnicodeLetter(unicode[0], 'h') &&
597         matchUnicodeLetter(unicode[1], 't') &&
598         matchUnicodeLetter(unicode[2], 't') &&
599         matchUnicodeLetter(unicode[3], 'p') &&
600         (unicode[4] == ':' 
601          || (matchLetter(unicode[4], 's') && unicode[5] == ':'))) {
602
603         unsigned pos = unicode[4] == ':' ? 5 : 6;
604
605         // skip possible initial two slashes
606         if (pos + 1 < length && unicode[pos] == '/' && unicode[pos + 1] == '/') {
607             pos += 2;
608         }
609
610         while (pos < length && unicode[pos] != '/') {
611             pos++;
612         }
613
614         if (pos == length) {
615             needToAddSlash = TRUE;
616         }
617     }
618
619     if (needToAddSlash) {
620         if (length + 1 <= 1024) {
621             strBuffer = staticStrBuffer;
622         } else {
623             strBuffer = malloc(sizeof(UniChar) * (length + 1));
624         }
625         memcpy(strBuffer, unicode, 2 * length);
626         strBuffer[length] = '/';
627         length++;
628
629         unicodeStr = strBuffer;
630     }
631
632     CFStringRef str = CFStringCreateWithCharactersNoCopy(NULL, unicodeStr, length, kCFAllocatorNull);
633     BOOL result = [history containsItemForURLString:(id)str];
634     CFRelease(str);
635
636     if (strBuffer != staticStrBuffer) {
637         free(strBuffer);
638     }
639
640     return result;
641 }
642
643 - (void)dealloc
644 {
645     [history release];
646     [super dealloc];
647 }
648
649 @end
650
651 @implementation WebHistory
652
653 + (WebHistory *)optionalSharedHistory
654 {
655     return _sharedHistory;
656 }
657
658
659 + (void)setOptionalSharedHistory: (WebHistory *)history
660 {
661     // FIXME.  Need to think about multiple instances of WebHistory per application
662     // and correct synchronization of history file between applications.
663     [WebCoreHistory setHistoryProvider: [[[_WebCoreHistoryProvider alloc] initWithHistory: history] autorelease]];
664     if (_sharedHistory != history){
665         [_sharedHistory release];
666         _sharedHistory = [history retain];
667     }
668 }
669
670 - (id)init
671 {
672     if ((self = [super init]) != nil) {
673         _historyPrivate = [[WebHistoryPrivate alloc] init];
674     }
675
676     return self;
677 }
678
679 - (void)dealloc
680 {
681     [_historyPrivate release];
682     [super dealloc];
683 }
684
685 #pragma mark MODIFYING CONTENTS
686
687 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
688 {
689     NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil];
690     [[NSNotificationCenter defaultCenter]
691         postNotificationName: name object: self userInfo: userInfo];
692 }
693
694 - (WebHistoryItem *)addItemForURL: (NSURL *)URL
695 {
696     WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:nil];
697     [entry _setLastVisitedTimeInterval: [NSDate timeIntervalSinceReferenceDate]];
698     [self addItem: entry];
699     [entry release];
700     return entry;
701 }
702
703
704 - (void)addItem: (WebHistoryItem *)entry
705 {
706     LOG (History, "adding %@", entry);
707     [_historyPrivate addItem: entry];
708     [self _sendNotification: WebHistoryItemsAddedNotification
709                     entries: [NSArray arrayWithObject:entry]];
710 }
711
712 - (void)removeItem: (WebHistoryItem *)entry
713 {
714     if ([_historyPrivate removeItem: entry]) {
715         [self _sendNotification: WebHistoryItemsRemovedNotification
716                         entries: [NSArray arrayWithObject:entry]];
717     }
718 }
719
720 - (void)removeItems: (NSArray *)entries
721 {
722     if ([_historyPrivate removeItems:entries]) {
723         [self _sendNotification: WebHistoryItemsRemovedNotification
724                         entries: entries];
725     }
726 }
727
728 - (void)removeAllItems
729 {
730     if ([_historyPrivate removeAllItems]) {
731         [[NSNotificationCenter defaultCenter]
732             postNotificationName: WebHistoryAllItemsRemovedNotification
733                           object: self];
734     }
735 }
736
737 - (void)addItems:(NSArray *)newEntries
738 {
739     [_historyPrivate addItems:newEntries];
740     [self _sendNotification: WebHistoryItemsAddedNotification
741                     entries: newEntries];
742 }
743
744 #pragma mark DATE-BASED RETRIEVAL
745
746 - (NSArray *)orderedLastVisitedDays
747 {
748     return [_historyPrivate orderedLastVisitedDays];
749 }
750
751 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
752 {
753     return [_historyPrivate orderedItemsLastVisitedOnDay: date];
754 }
755
756 #pragma mark URL MATCHING
757
758 - (BOOL)containsItemForURLString: (NSString *)URLString
759 {
760     return [_historyPrivate containsItemForURLString: URLString];
761 }
762
763 - (BOOL)containsURL: (NSURL *)URL
764 {
765     return [_historyPrivate containsURL: URL];
766 }
767
768 - (WebHistoryItem *)itemForURL:(NSURL *)URL
769 {
770     return [_historyPrivate itemForURL:URL];
771 }
772
773 #pragma mark SAVING TO DISK
774
775 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
776 {
777     if ([_historyPrivate loadFromURL:URL error:error]) {
778         [[NSNotificationCenter defaultCenter]
779             postNotificationName: WebHistoryLoadedNotification
780                           object: self];
781         return YES;
782     }
783     return NO;
784 }
785
786 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
787 {
788     // FIXME:  Use new foundation API to get error when ready.
789     if([_historyPrivate saveToURL:URL error:error]){
790         [[NSNotificationCenter defaultCenter]
791             postNotificationName: WebHistorySavedNotification
792                           object: self];
793         return YES;
794     }
795     return NO;    
796 }
797
798 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString
799 {
800     return [_historyPrivate itemForURLString: URLString];
801 }
802
803 - (NSCalendarDate*)ageLimitDate
804 {
805     return [_historyPrivate _ageLimitDate];
806 }
807
808 - (void)setHistoryItemLimit:(int)limit
809 {
810     [_historyPrivate setHistoryItemLimit:limit];
811 }
812
813 - (int)historyItemLimit
814 {
815     return [_historyPrivate historyItemLimit];
816 }
817
818 - (void)setHistoryAgeInDaysLimit:(int)limit
819 {
820     [_historyPrivate setHistoryAgeInDaysLimit:limit];
821 }
822
823 - (int)historyAgeInDaysLimit
824 {
825     return [_historyPrivate historyAgeInDaysLimit];
826 }
827
828 @end