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