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