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