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