Rubberstamped by Anders
[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 "WebHistory.h"
30 #import "WebHistoryPrivate.h"
31
32 #import "WebHistoryItem.h"
33 #import "WebHistoryItemPrivate.h"
34 #import "WebKitLogging.h"
35 #import "WebNSCalendarDateExtras.h"
36 #import "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     NSDictionary *dictionary = nil;
401     
402     // Optimize loading from local file, which is faster than using the general URL loading mechanism
403     if ([URL isFileURL]) {
404         dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]];
405         if (!dictionary) {
406 #if !LOG_DISABLED
407             if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]])
408                 LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]);
409 #endif
410             // else file doesn't exist, which is normal the first time
411             return NO;
412         }
413     } else {
414         NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
415         if (data && [data length] > 0) {
416             dictionary = [NSPropertyListSerialization propertyListFromData:data
417                 mutabilityOption:NSPropertyListImmutable
418                 format:nil
419                 errorDescription:nil];
420         }
421     }
422     
423
424     // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support
425     // that ancient format, so anything that isn't an NSDictionary is bogus.
426     if (![dictionary isKindOfClass:[NSDictionary class]])
427         return NO;
428
429     NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey];
430     int fileVersion;
431     // we don't trust data obtained from elsewhere, so double-check
432     if (fileVersionObject != nil && [fileVersionObject isKindOfClass:[NSNumber class]]) {
433         fileVersion = [fileVersionObject intValue];
434     } else {
435         LOG_ERROR("history file version can't be determined, therefore not loading");
436         return NO;
437     }
438     if (fileVersion > currentFileVersion) {
439         LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion);
440         return NO;
441     }    
442
443     NSArray *array = [dictionary objectForKey:DatesArrayKey];
444         
445     int itemCountLimit = [self historyItemLimit];
446     NSCalendarDate *ageLimitDate = [self _ageLimitDate];
447     NSEnumerator *enumerator = [array objectEnumerator];
448     BOOL ageLimitPassed = NO;
449     BOOL itemLimitPassed = NO;
450     ASSERT(*numberOfItemsLoaded == 0);
451     
452     NSDictionary *itemAsDictionary;
453     while ((itemAsDictionary = [enumerator nextObject]) != nil) {
454         WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary];
455
456         // item without URL is useless; data on disk must have been bad; ignore
457         if ([item URLString]) {
458             // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing
459             // once we've found the first item that's too old.
460             if (!ageLimitPassed && ([[item _lastVisitedDate] compare:ageLimitDate] != NSOrderedDescending))
461                 ageLimitPassed = YES;
462             
463             if (ageLimitPassed || itemLimitPassed)
464                 [discardedItems addObject:item];
465             else {
466                 [self addItem:item];
467                 ++(*numberOfItemsLoaded);
468                 if (*numberOfItemsLoaded == itemCountLimit)
469                     itemLimitPassed = YES;
470             }
471         }
472         
473         [item release];
474     }
475
476     return YES;    
477 }
478
479 - (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error
480 {
481     int numberOfItems;
482     double start, duration;
483     BOOL result;
484
485     start = CFAbsoluteTimeGetCurrent();
486     result = [self _loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error];
487
488     if (result) {
489         duration = CFAbsoluteTimeGetCurrent() - start;
490         LOG(Timing, "loading %d history entries from %@ took %f seconds",
491             numberOfItems, URL, duration);
492     }
493
494     return result;
495 }
496
497 - (BOOL)_saveHistoryGuts: (int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error
498 {
499     *numberOfItemsSaved = 0;
500
501     // FIXME:  Correctly report error when new API is ready.
502     if (error)
503         *error = nil;
504
505     NSArray *array = [self arrayRepresentation];
506     NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
507         array, DatesArrayKey,
508         [NSNumber numberWithInt:currentFileVersion], FileVersionKey,
509         nil];
510     NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
511     if (![data writeToURL:URL atomically:YES]) {
512         LOG_ERROR("attempt to save %@ to %@ failed", dictionary, URL);
513         return NO;
514     }
515     
516     *numberOfItemsSaved = [array count];
517     return YES;
518 }
519
520 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
521 {
522     int numberOfItems;
523     double start, duration;
524     BOOL result;
525
526     start = CFAbsoluteTimeGetCurrent();
527     result = [self _saveHistoryGuts: &numberOfItems URL:URL error:error];
528
529     if (result) {
530         duration = CFAbsoluteTimeGetCurrent() - start;
531         LOG(Timing, "saving %d history entries to %@ took %f seconds",
532             numberOfItems, URL, duration);
533     }
534
535     return result;
536 }
537
538 @end
539
540 @interface _WebCoreHistoryProvider : NSObject  <WebCoreHistoryProvider> 
541 {
542     WebHistory *history;
543 }
544 - initWithHistory: (WebHistory *)h;
545 @end
546
547 @implementation _WebCoreHistoryProvider
548 - initWithHistory: (WebHistory *)h
549 {
550     history = [h retain];
551     return self;
552 }
553
554 static inline bool matchLetter(char c, char lowercaseLetter)
555 {
556     return (c | 0x20) == lowercaseLetter;
557 }
558
559 static inline bool matchUnicodeLetter(UniChar c, UniChar lowercaseLetter)
560 {
561     return (c | 0x20) == lowercaseLetter;
562 }
563
564 #define BUFFER_SIZE 2048
565
566 - (BOOL)containsItemForURLLatin1:(const char *)latin1 length:(unsigned)length
567 {
568     const char *latin1Str = latin1;
569     char staticStrBuffer[BUFFER_SIZE];
570     char *strBuffer = NULL;
571     BOOL needToAddSlash = FALSE;
572
573     if (length >= 6 &&
574         matchLetter(latin1[0], 'h') &&
575         matchLetter(latin1[1], 't') &&
576         matchLetter(latin1[2], 't') &&
577         matchLetter(latin1[3], 'p') &&
578         (latin1[4] == ':' 
579          || (matchLetter(latin1[4], 's') && latin1[5] == ':'))) {
580         int pos = latin1[4] == ':' ? 5 : 6;
581         // skip possible initial two slashes
582         if (latin1[pos] == '/' && latin1[pos + 1] == '/') {
583             pos += 2;
584         }
585
586         char *nextSlash = strchr(latin1 + pos, '/');
587         if (nextSlash == NULL) {
588             needToAddSlash = TRUE;
589         }
590     }
591
592     if (needToAddSlash) {
593         if (length + 1 <= BUFFER_SIZE) {
594             strBuffer = staticStrBuffer;
595         } else {
596             strBuffer = malloc(length + 2);
597         }
598         memcpy(strBuffer, latin1, length + 1);
599         strBuffer[length] = '/';
600         strBuffer[length+1] = '\0';
601         length++;
602
603         latin1Str = strBuffer;
604     }
605
606     CFStringRef str = CFStringCreateWithCStringNoCopy(NULL, latin1Str, kCFStringEncodingWindowsLatin1, kCFAllocatorNull);
607     BOOL result = [history containsItemForURLString:(id)str];
608     CFRelease(str);
609
610     if (strBuffer != staticStrBuffer) {
611         free(strBuffer);
612     }
613
614     return result;
615 }
616
617 #define UNICODE_BUFFER_SIZE 1024
618
619 - (BOOL)containsItemForURLUnicode:(const UniChar *)unicode length:(unsigned)length
620 {
621     const UniChar *unicodeStr = unicode;
622     UniChar staticStrBuffer[UNICODE_BUFFER_SIZE];
623     UniChar *strBuffer = NULL;
624     BOOL needToAddSlash = FALSE;
625
626     if (length >= 6 &&
627         matchUnicodeLetter(unicode[0], 'h') &&
628         matchUnicodeLetter(unicode[1], 't') &&
629         matchUnicodeLetter(unicode[2], 't') &&
630         matchUnicodeLetter(unicode[3], 'p') &&
631         (unicode[4] == ':' 
632          || (matchUnicodeLetter(unicode[4], 's') && unicode[5] == ':'))) {
633
634         unsigned pos = unicode[4] == ':' ? 5 : 6;
635
636         // skip possible initial two slashes
637         if (pos + 1 < length && unicode[pos] == '/' && unicode[pos + 1] == '/') {
638             pos += 2;
639         }
640
641         while (pos < length && unicode[pos] != '/') {
642             pos++;
643         }
644
645         if (pos == length) {
646             needToAddSlash = TRUE;
647         }
648     }
649
650     if (needToAddSlash) {
651         if (length + 1 <= UNICODE_BUFFER_SIZE) {
652             strBuffer = staticStrBuffer;
653         } else {
654             strBuffer = malloc(sizeof(UniChar) * (length + 1));
655         }
656         memcpy(strBuffer, unicode, 2 * length);
657         strBuffer[length] = '/';
658         length++;
659
660         unicodeStr = strBuffer;
661     }
662
663     CFStringRef str = CFStringCreateWithCharactersNoCopy(NULL, unicodeStr, length, kCFAllocatorNull);
664     BOOL result = [history containsItemForURLString:(id)str];
665     CFRelease(str);
666
667     if (strBuffer != staticStrBuffer) {
668         free(strBuffer);
669     }
670
671     return result;
672 }
673
674 - (void)dealloc
675 {
676     [history release];
677     [super dealloc];
678 }
679
680 @end
681
682 @implementation WebHistory
683
684 + (WebHistory *)optionalSharedHistory
685 {
686     return _sharedHistory;
687 }
688
689
690 + (void)setOptionalSharedHistory: (WebHistory *)history
691 {
692     // FIXME.  Need to think about multiple instances of WebHistory per application
693     // and correct synchronization of history file between applications.
694     [WebCoreHistory setHistoryProvider: [[[_WebCoreHistoryProvider alloc] initWithHistory: history] autorelease]];
695     if (_sharedHistory != history){
696         [_sharedHistory release];
697         _sharedHistory = [history retain];
698     }
699 }
700
701 - (id)init
702 {
703     if ((self = [super init]) != nil) {
704         _historyPrivate = [[WebHistoryPrivate alloc] init];
705     }
706
707     return self;
708 }
709
710 - (void)dealloc
711 {
712     [_historyPrivate release];
713     [super dealloc];
714 }
715
716 #pragma mark MODIFYING CONTENTS
717
718 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
719 {
720     NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil];
721     [[NSNotificationCenter defaultCenter]
722         postNotificationName: name object: self userInfo: userInfo];
723 }
724
725 - (WebHistoryItem *)addItemForURL: (NSURL *)URL
726 {
727     WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:nil];
728     [entry _setLastVisitedTimeInterval: [NSDate timeIntervalSinceReferenceDate]];
729     [self addItem: entry];
730     [entry release];
731     return entry;
732 }
733
734
735 - (void)addItem: (WebHistoryItem *)entry
736 {
737     LOG (History, "adding %@", entry);
738     [_historyPrivate addItem: entry];
739     [self _sendNotification: WebHistoryItemsAddedNotification
740                     entries: [NSArray arrayWithObject:entry]];
741 }
742
743 - (void)removeItem: (WebHistoryItem *)entry
744 {
745     if ([_historyPrivate removeItem: entry]) {
746         [self _sendNotification: WebHistoryItemsRemovedNotification
747                         entries: [NSArray arrayWithObject:entry]];
748     }
749 }
750
751 - (void)removeItems: (NSArray *)entries
752 {
753     if ([_historyPrivate removeItems:entries]) {
754         [self _sendNotification: WebHistoryItemsRemovedNotification
755                         entries: entries];
756     }
757 }
758
759 - (void)removeAllItems
760 {
761     if ([_historyPrivate removeAllItems]) {
762         [[NSNotificationCenter defaultCenter]
763             postNotificationName: WebHistoryAllItemsRemovedNotification
764                           object: self];
765     }
766 }
767
768 - (void)addItems:(NSArray *)newEntries
769 {
770     [_historyPrivate addItems:newEntries];
771     [self _sendNotification: WebHistoryItemsAddedNotification
772                     entries: newEntries];
773 }
774
775 - (void)setLastVisitedTimeInterval:(NSTimeInterval)time forItem:(WebHistoryItem *)entry
776 {
777     [_historyPrivate setLastVisitedTimeInterval:time forItem:entry];
778 }
779
780 #pragma mark DATE-BASED RETRIEVAL
781
782 - (NSArray *)orderedLastVisitedDays
783 {
784     return [_historyPrivate orderedLastVisitedDays];
785 }
786
787 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
788 {
789     return [_historyPrivate orderedItemsLastVisitedOnDay: date];
790 }
791
792 #pragma mark URL MATCHING
793
794 - (BOOL)containsItemForURLString: (NSString *)URLString
795 {
796     return [_historyPrivate containsItemForURLString: URLString];
797 }
798
799 - (BOOL)containsURL: (NSURL *)URL
800 {
801     return [_historyPrivate containsURL: URL];
802 }
803
804 - (WebHistoryItem *)itemForURL:(NSURL *)URL
805 {
806     return [_historyPrivate itemForURL:URL];
807 }
808
809 #pragma mark SAVING TO DISK
810
811 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
812 {
813     NSMutableArray *discardedItems = [NSMutableArray array];
814     
815     if ([_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) {
816         [[NSNotificationCenter defaultCenter]
817             postNotificationName:WebHistoryLoadedNotification
818                           object:self];
819         
820         if ([discardedItems count] > 0)
821             [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems];
822         
823         return YES;
824     }
825     return NO;
826 }
827
828 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
829 {
830     // FIXME:  Use new foundation API to get error when ready.
831     if([_historyPrivate saveToURL:URL error:error]){
832         [[NSNotificationCenter defaultCenter]
833             postNotificationName: WebHistorySavedNotification
834                           object: self];
835         return YES;
836     }
837     return NO;    
838 }
839
840 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString
841 {
842     return [_historyPrivate itemForURLString: URLString];
843 }
844
845 - (NSCalendarDate*)ageLimitDate
846 {
847     return [_historyPrivate _ageLimitDate];
848 }
849
850 - (void)setHistoryItemLimit:(int)limit
851 {
852     [_historyPrivate setHistoryItemLimit:limit];
853 }
854
855 - (int)historyItemLimit
856 {
857     return [_historyPrivate historyItemLimit];
858 }
859
860 - (void)setHistoryAgeInDaysLimit:(int)limit
861 {
862     [_historyPrivate setHistoryAgeInDaysLimit:limit];
863 }
864
865 - (int)historyAgeInDaysLimit
866 {
867     return [_historyPrivate historyAgeInDaysLimit];
868 }
869
870 @end