5 // Created by John Sullivan on Mon Feb 18 2002.
6 // Copyright (c) 2002 Apple Computer, Inc. All rights reserved.
9 #import <WebKit/WebHistory.h>
10 #import <WebKit/WebHistoryPrivate.h>
12 #import <WebKit/WebHistoryItem.h>
13 #import <WebKit/WebHistoryItemPrivate.h>
14 #import <WebKit/WebKitLogging.h>
15 #import <WebKit/WebNSURLExtras.h>
16 #import <Foundation/NSError.h>
17 #import <WebKit/WebAssertions.h>
18 #import <WebCore/WebCoreHistory.h>
20 #import <Foundation/NSCalendarDate_NSURLExtras.h>
23 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification";
24 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification";
25 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification";
26 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification";
27 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification";
28 NSString *WebHistoryItemsKey = @"WebHistoryItems";
30 static WebHistory *_sharedHistory = nil;
34 NSString *FileVersionKey = @"WebHistoryFileVersion";
35 NSString *DatesArrayKey = @"WebHistoryDates";
37 #define currentFileVersion 1
39 @implementation WebHistoryPrivate
41 #pragma mark OBJECT FRAMEWORK
45 [[NSUserDefaults standardUserDefaults] registerDefaults:
46 [NSDictionary dictionaryWithObjectsAndKeys:
47 @"1000", @"WebKitHistoryItemLimit",
48 @"7", @"WebKitHistoryAgeInDaysLimit",
58 _entriesByURL = [[NSMutableDictionary alloc] init];
59 _datesWithEntries = [[NSMutableArray alloc] init];
60 _entriesByDate = [[NSMutableArray alloc] init];
67 [_entriesByURL release];
68 [_datesWithEntries release];
69 [_entriesByDate release];
74 #pragma mark MODIFYING CONTENTS
76 // Returns whether the day is already in the list of days,
77 // and fills in *index with the found or proposed index.
78 - (BOOL)findIndex: (int *)index forDay: (NSCalendarDate *)date
82 ASSERT_ARG(index, index != nil);
84 //FIXME: just does linear search through days; inefficient if many days
85 count = [_datesWithEntries count];
86 for (*index = 0; *index < count; ++*index) {
87 NSComparisonResult result = [date _web_compareDay: [_datesWithEntries objectAtIndex: *index]];
88 if (result == NSOrderedSame) {
91 if (result == NSOrderedDescending) {
99 - (void)insertItem: (WebHistoryItem *)entry atDateIndex: (int)dateIndex
102 NSMutableArray *entriesForDate;
103 NSCalendarDate *entryDate;
105 ASSERT_ARG(entry, entry != nil);
106 ASSERT_ARG(dateIndex, dateIndex >= 0 && (uint)dateIndex < [_entriesByDate count]);
108 //FIXME: just does linear search through entries; inefficient if many entries for this date
109 entryDate = [entry _lastVisitedDate];
110 entriesForDate = [_entriesByDate objectAtIndex: dateIndex];
111 count = [entriesForDate count];
112 // optimized for inserting oldest to youngest
113 for (index = 0; index < count; ++index) {
114 if ([entryDate compare: [[entriesForDate objectAtIndex: index] _lastVisitedDate]] != NSOrderedAscending) {
119 [entriesForDate insertObject: entry atIndex: index];
122 - (BOOL)removeItemForURLString: (NSString *)URLString
124 NSMutableArray *entriesForDate;
125 WebHistoryItem *entry;
129 entry = [_entriesByURL objectForKey: URLString];
134 [_entriesByURL removeObjectForKey: URLString];
136 foundDate = [self findIndex: &dateIndex forDay: [entry _lastVisitedDate]];
140 entriesForDate = [_entriesByDate objectAtIndex: dateIndex];
141 [entriesForDate removeObjectIdenticalTo: entry];
143 // remove this date entirely if there are no other entries on it
144 if ([entriesForDate count] == 0) {
145 [_entriesByDate removeObjectAtIndex: dateIndex];
146 [_datesWithEntries removeObjectAtIndex: dateIndex];
153 - (void)addItem: (WebHistoryItem *)entry
158 ASSERT_ARG(entry, entry);
159 ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0);
161 URLString = [entry URLString];
163 // If we already have an item with this URL, we need to merge info that drives the
164 // URL autocomplete heuristics from that item into the new one.
165 WebHistoryItem *oldEntry = [_entriesByURL objectForKey: URLString];
167 [entry _mergeAutoCompleteHints:oldEntry];
170 [self removeItemForURLString: URLString];
172 if ([self findIndex: &dateIndex forDay: [entry _lastVisitedDate]]) {
173 // other entries already exist for this date
174 [self insertItem: entry atDateIndex: dateIndex];
176 // no other entries exist for this date
177 [_datesWithEntries insertObject: [entry _lastVisitedDate] atIndex: dateIndex];
178 [_entriesByDate insertObject: [NSMutableArray arrayWithObject:entry] atIndex: dateIndex];
181 [_entriesByURL setObject: entry forKey: URLString];
184 - (BOOL)removeItem: (WebHistoryItem *)entry
186 WebHistoryItem *matchingEntry;
189 URLString = [entry URLString];
191 // If this exact object isn't stored, then make no change.
192 // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is?
193 // Maybe need to change the API to make something like removeEntryForURLString public instead.
194 matchingEntry = [_entriesByURL objectForKey: URLString];
195 if (matchingEntry != entry) {
199 [self removeItemForURLString: URLString];
204 - (BOOL)removeItems: (NSArray *)entries
208 count = [entries count];
213 for (index = 0; index < count; ++index) {
214 [self removeItem:[entries objectAtIndex:index]];
220 - (BOOL)removeAllItems
222 if ([_entriesByURL count] == 0) {
226 [_entriesByDate removeAllObjects];
227 [_datesWithEntries removeAllObjects];
228 [_entriesByURL removeAllObjects];
233 - (void)addItems:(NSArray *)newEntries
235 NSEnumerator *enumerator;
236 WebHistoryItem *entry;
238 // There is no guarantee that the incoming entries are in any particular
239 // order, but if this is called with a set of entries that were created by
240 // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy
241 // then they will be ordered chronologically from newest to oldest. We can make adding them
242 // faster (fewer compares) by inserting them from oldest to newest.
243 enumerator = [newEntries reverseObjectEnumerator];
244 while ((entry = [enumerator nextObject]) != nil) {
245 [self addItem:entry];
249 #pragma mark DATE-BASED RETRIEVAL
251 - (NSArray *)orderedLastVisitedDays
253 return _datesWithEntries;
256 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
260 if ([self findIndex: &index forDay: date]) {
261 return [_entriesByDate objectAtIndex: index];
267 #pragma mark URL MATCHING
269 - (WebHistoryItem *)itemForURLString:(NSString *)URLString
271 return [_entriesByURL objectForKey: URLString];
274 - (BOOL)containsItemForURLString: (NSString *)URLString
276 return [self itemForURLString:URLString] != nil;
279 - (BOOL)containsURL: (NSURL *)URL
281 return [self itemForURLString:[URL _web_originalDataAsString]] != nil;
284 - (WebHistoryItem *)itemForURL:(NSURL *)URL
286 return [self itemForURLString:[URL _web_originalDataAsString]];
289 #pragma mark ARCHIVING/UNARCHIVING
291 - (void)setHistoryAgeInDaysLimit:(int)limit
293 ageInDaysLimitSet = YES;
294 ageInDaysLimit = limit;
297 - (int)historyAgeInDaysLimit
299 if (ageInDaysLimitSet)
300 return ageInDaysLimit;
301 return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryAgeInDaysLimit"];
304 - (void)setHistoryItemLimit:(int)limit
310 - (int)historyItemLimit
314 return [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"];
317 // Return a date that marks the age limit for history entries saved to or
318 // loaded from disk. Any entry on this day or older should be rejected,
319 // as tested with -[NSCalendarDate compareDay:]
320 - (NSCalendarDate *)_ageLimitDate
322 return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit]
323 hours:0 minutes:0 seconds:0];
326 // Return a flat array of WebHistoryItems. Leaves out entries older than the age limit.
327 // Stops filling array when item count limit is reached, even if there are currently
328 // more entries than that.
329 - (NSArray *)arrayRepresentation
331 int dateCount, dateIndex;
334 NSMutableArray *arrayRep;
335 NSCalendarDate *ageLimitDate;
337 arrayRep = [NSMutableArray array];
339 limit = [self historyItemLimit];
340 ageLimitDate = [self _ageLimitDate];
343 dateCount = [_entriesByDate count];
344 for (dateIndex = 0; dateIndex < dateCount; ++dateIndex) {
345 int entryCount, entryIndex;
348 // skip remaining days if they are older than the age limit
349 if ([[_datesWithEntries objectAtIndex:dateIndex] _web_compareDay:ageLimitDate] != NSOrderedDescending) {
353 entries = [_entriesByDate objectAtIndex:dateIndex];
354 entryCount = [entries count];
355 for (entryIndex = 0; entryIndex < entryCount; ++entryIndex) {
356 if (totalSoFar++ >= limit) {
359 [arrayRep addObject: [[entries objectAtIndex:entryIndex] dictionaryRepresentation]];
366 - (BOOL)_loadHistoryGuts: (int *)numberOfItemsLoaded URL:(NSURL *)URL error:(NSError **)error
368 *numberOfItemsLoaded = 0;
370 NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error];
371 id propertyList = nil;
372 if (data && [data length] > 0) {
373 propertyList = [NSPropertyListSerialization propertyListFromData:data
374 mutabilityOption:NSPropertyListImmutable
376 errorDescription:nil];
379 // propertyList might be an old-style NSArray or a more modern NSDictionary.
380 // If it's an NSArray, convert it to new format before further processing.
381 NSDictionary *fileAsDictionary = nil;
382 if ([propertyList isKindOfClass:[NSDictionary class]]) {
383 fileAsDictionary = propertyList;
384 } else if ([propertyList isKindOfClass:[NSArray class]]) {
385 // Convert old-style array into new-style dictionary
386 fileAsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
387 propertyList, DatesArrayKey,
388 [NSNumber numberWithInt:1], FileVersionKey,
391 if ([URL isFileURL] && [[NSFileManager defaultManager] fileExistsAtPath: [URL path]]) {
392 ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]);
397 NSNumber *fileVersionObject = [fileAsDictionary objectForKey:FileVersionKey];
399 // we don't trust data read from disk, so double-check
400 if (fileVersionObject != nil && [fileVersionObject isKindOfClass:[NSNumber class]]) {
401 fileVersion = [fileVersionObject intValue];
403 ERROR("history file version can't be determined, therefore not loading");
406 if (fileVersion > currentFileVersion) {
407 ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion);
411 NSArray *array = [fileAsDictionary objectForKey:DatesArrayKey];
413 int limit = [[NSUserDefaults standardUserDefaults] integerForKey: @"WebKitHistoryItemLimit"];
414 NSCalendarDate *ageLimitDate = [self _ageLimitDate];
416 // reverse dates so you're loading the oldest first, to minimize the number of comparisons
417 NSEnumerator *enumerator = [array reverseObjectEnumerator];
418 BOOL ageLimitPassed = NO;
420 NSDictionary *itemAsDictionary;
421 while ((itemAsDictionary = [enumerator nextObject]) != nil) {
422 WebHistoryItem *entry;
424 entry = [[[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary] autorelease];
426 if ([entry URLString] == nil) {
427 // entry without URL is useless; data on disk must have been bad; ignore
431 // test against date limit
432 if (!ageLimitPassed) {
433 if ([[entry _lastVisitedDate] _web_compareDay:ageLimitDate] != NSOrderedDescending) {
436 ageLimitPassed = YES;
440 [self addItem: entry];
441 if (++index >= limit) {
446 *numberOfItemsLoaded = MIN(index, limit);
450 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
453 double start, duration;
456 start = CFAbsoluteTimeGetCurrent();
457 result = [self _loadHistoryGuts: &numberOfItems URL:URL error:error];
460 duration = CFAbsoluteTimeGetCurrent() - start;
461 LOG(Timing, "loading %d history entries from %@ took %f seconds",
462 numberOfItems, URL, duration);
468 - (BOOL)_saveHistoryGuts: (int *)numberOfItemsSaved URL:(NSURL *)URL error:(NSError **)error
470 *numberOfItemsSaved = 0;
472 // FIXME: Correctly report error when new API is ready.
476 NSArray *array = [self arrayRepresentation];
477 NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:
478 array, DatesArrayKey,
479 [NSNumber numberWithInt:currentFileVersion], FileVersionKey,
481 NSData *data = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
482 if (![data writeToURL:URL atomically:YES]) {
483 ERROR("attempt to save %@ to %@ failed", dictionary, URL);
487 *numberOfItemsSaved = [array count];
491 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
494 double start, duration;
497 start = CFAbsoluteTimeGetCurrent();
498 result = [self _saveHistoryGuts: &numberOfItems URL:URL error:error];
501 duration = CFAbsoluteTimeGetCurrent() - start;
502 LOG(Timing, "saving %d history entries to %@ took %f seconds",
503 numberOfItems, URL, duration);
511 @interface _WebCoreHistoryProvider : NSObject <WebCoreHistoryProvider>
515 - initWithHistory: (WebHistory *)h;
518 @implementation _WebCoreHistoryProvider
519 - initWithHistory: (WebHistory *)h
521 history = [h retain];
525 static inline bool matchLetter(char c, char lowercaseLetter)
527 return (c | 0x20) == lowercaseLetter;
530 static inline bool matchUnicodeLetter(UniChar c, UniChar lowercaseLetter)
532 return (c | 0x20) == lowercaseLetter;
535 #define BUFFER_SIZE 2048
537 - (BOOL)containsItemForURLLatin1:(const char *)latin1 length:(unsigned)length
539 const char *latin1Str = latin1;
540 char staticStrBuffer[BUFFER_SIZE];
541 char *strBuffer = NULL;
542 BOOL needToAddSlash = FALSE;
545 matchLetter(latin1[0], 'h') &&
546 matchLetter(latin1[1], 't') &&
547 matchLetter(latin1[2], 't') &&
548 matchLetter(latin1[3], 'p') &&
550 || (matchLetter(latin1[4], 's') && latin1[5] == ':'))) {
551 int pos = latin1[4] == ':' ? 5 : 6;
552 // skip possible initial two slashes
553 if (latin1[pos] == '/' && latin1[pos + 1] == '/') {
557 char *nextSlash = strchr(latin1 + pos, '/');
558 if (nextSlash == NULL) {
559 needToAddSlash = TRUE;
563 if (needToAddSlash) {
564 if (length + 1 <= 2048) {
565 strBuffer = staticStrBuffer;
567 strBuffer = malloc(length + 2);
569 memcpy(strBuffer, latin1, length + 1);
570 strBuffer[length] = '/';
571 strBuffer[length+1] = '\0';
574 latin1Str = strBuffer;
577 CFStringRef str = CFStringCreateWithCStringNoCopy(NULL, latin1Str, kCFStringEncodingWindowsLatin1, kCFAllocatorNull);
578 BOOL result = [history containsItemForURLString:(id)str];
581 if (strBuffer != staticStrBuffer) {
588 - (BOOL)containsItemForURLUnicode:(const UniChar *)unicode length:(unsigned)length
590 const UniChar *unicodeStr = unicode;
591 UniChar staticStrBuffer[1024];
592 UniChar *strBuffer = NULL;
593 BOOL needToAddSlash = FALSE;
596 matchUnicodeLetter(unicode[0], 'h') &&
597 matchUnicodeLetter(unicode[1], 't') &&
598 matchUnicodeLetter(unicode[2], 't') &&
599 matchUnicodeLetter(unicode[3], 'p') &&
601 || (matchLetter(unicode[4], 's') && unicode[5] == ':'))) {
603 unsigned pos = unicode[4] == ':' ? 5 : 6;
605 // skip possible initial two slashes
606 if (pos + 1 < length && unicode[pos] == '/' && unicode[pos + 1] == '/') {
610 while (pos < length && unicode[pos] != '/') {
615 needToAddSlash = TRUE;
619 if (needToAddSlash) {
620 if (length + 1 <= 1024) {
621 strBuffer = staticStrBuffer;
623 strBuffer = malloc(sizeof(UniChar) * (length + 1));
625 memcpy(strBuffer, unicode, 2 * length);
626 strBuffer[length] = '/';
629 unicodeStr = strBuffer;
632 CFStringRef str = CFStringCreateWithCharactersNoCopy(NULL, unicodeStr, length, kCFAllocatorNull);
633 BOOL result = [history containsItemForURLString:(id)str];
636 if (strBuffer != staticStrBuffer) {
651 @implementation WebHistory
653 + (WebHistory *)optionalSharedHistory
655 return _sharedHistory;
659 + (void)setOptionalSharedHistory: (WebHistory *)history
661 // FIXME. Need to think about multiple instances of WebHistory per application
662 // and correct synchronization of history file between applications.
663 [WebCoreHistory setHistoryProvider: [[[_WebCoreHistoryProvider alloc] initWithHistory: history] autorelease]];
664 if (_sharedHistory != history){
665 [_sharedHistory release];
666 _sharedHistory = [history retain];
672 if ((self = [super init]) != nil) {
673 _historyPrivate = [[WebHistoryPrivate alloc] init];
681 [_historyPrivate release];
685 #pragma mark MODIFYING CONTENTS
687 - (void)_sendNotification:(NSString *)name entries:(NSArray *)entries
689 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil];
690 [[NSNotificationCenter defaultCenter]
691 postNotificationName: name object: self userInfo: userInfo];
694 - (WebHistoryItem *)addItemForURL: (NSURL *)URL
696 WebHistoryItem *entry = [[WebHistoryItem alloc] initWithURL:URL title:nil];
697 [entry _setLastVisitedTimeInterval: [NSDate timeIntervalSinceReferenceDate]];
698 [self addItem: entry];
704 - (void)addItem: (WebHistoryItem *)entry
706 LOG (History, "adding %@", entry);
707 [_historyPrivate addItem: entry];
708 [self _sendNotification: WebHistoryItemsAddedNotification
709 entries: [NSArray arrayWithObject:entry]];
712 - (void)removeItem: (WebHistoryItem *)entry
714 if ([_historyPrivate removeItem: entry]) {
715 [self _sendNotification: WebHistoryItemsRemovedNotification
716 entries: [NSArray arrayWithObject:entry]];
720 - (void)removeItems: (NSArray *)entries
722 if ([_historyPrivate removeItems:entries]) {
723 [self _sendNotification: WebHistoryItemsRemovedNotification
728 - (void)removeAllItems
730 if ([_historyPrivate removeAllItems]) {
731 [[NSNotificationCenter defaultCenter]
732 postNotificationName: WebHistoryAllItemsRemovedNotification
737 - (void)addItems:(NSArray *)newEntries
739 [_historyPrivate addItems:newEntries];
740 [self _sendNotification: WebHistoryItemsAddedNotification
741 entries: newEntries];
744 #pragma mark DATE-BASED RETRIEVAL
746 - (NSArray *)orderedLastVisitedDays
748 return [_historyPrivate orderedLastVisitedDays];
751 - (NSArray *)orderedItemsLastVisitedOnDay: (NSCalendarDate *)date
753 return [_historyPrivate orderedItemsLastVisitedOnDay: date];
756 #pragma mark URL MATCHING
758 - (BOOL)containsItemForURLString: (NSString *)URLString
760 return [_historyPrivate containsItemForURLString: URLString];
763 - (BOOL)containsURL: (NSURL *)URL
765 return [_historyPrivate containsURL: URL];
768 - (WebHistoryItem *)itemForURL:(NSURL *)URL
770 return [_historyPrivate itemForURL:URL];
773 #pragma mark SAVING TO DISK
775 - (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error
777 if ([_historyPrivate loadFromURL:URL error:error]) {
778 [[NSNotificationCenter defaultCenter]
779 postNotificationName: WebHistoryLoadedNotification
786 - (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error
788 // FIXME: Use new foundation API to get error when ready.
789 if([_historyPrivate saveToURL:URL error:error]){
790 [[NSNotificationCenter defaultCenter]
791 postNotificationName: WebHistorySavedNotification
798 - (WebHistoryItem *)_itemForURLString:(NSString *)URLString
800 return [_historyPrivate itemForURLString: URLString];
803 - (NSCalendarDate*)ageLimitDate
805 return [_historyPrivate _ageLimitDate];
808 - (void)setHistoryItemLimit:(int)limit
810 [_historyPrivate setHistoryItemLimit:limit];
813 - (int)historyItemLimit
815 return [_historyPrivate historyItemLimit];
818 - (void)setHistoryAgeInDaysLimit:(int)limit
820 [_historyPrivate setHistoryAgeInDaysLimit:limit];
823 - (int)historyAgeInDaysLimit
825 return [_historyPrivate historyAgeInDaysLimit];