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