1/* 2 * Copyright (C) 2005, 2008, 2009 Apple 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 "WebHistoryInternal.h" 30 31#import "WebHistoryItemInternal.h" 32#import "WebKitLogging.h" 33#import "WebNSURLExtras.h" 34#import "WebTypesInternal.h" 35#import <WebCore/HistoryItem.h> 36#import <WebCore/HistoryPropertyList.h> 37#import <WebCore/PageGroup.h> 38 39using namespace WebCore; 40using namespace std; 41 42typedef int64_t WebHistoryDateKey; 43typedef HashMap<WebHistoryDateKey, RetainPtr<NSMutableArray> > DateToEntriesMap; 44 45NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification"; 46NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification"; 47NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification"; 48NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification"; 49NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification"; 50NSString *WebHistorySavedNotification = @"WebHistorySavedNotification"; 51NSString *WebHistoryItemsKey = @"WebHistoryItems"; 52 53static WebHistory *_sharedHistory = nil; 54 55NSString *FileVersionKey = @"WebHistoryFileVersion"; 56NSString *DatesArrayKey = @"WebHistoryDates"; 57 58#define currentFileVersion 1 59 60class WebHistoryWriter : public HistoryPropertyListWriter { 61public: 62 WebHistoryWriter(DateToEntriesMap*); 63 64private: 65 virtual void writeHistoryItems(BinaryPropertyListObjectStream&); 66 67 DateToEntriesMap* m_entriesByDate; 68 Vector<int> m_dateKeys; 69}; 70 71@interface WebHistoryPrivate : NSObject { 72@private 73 NSMutableDictionary *_entriesByURL; 74 DateToEntriesMap* _entriesByDate; 75 NSMutableArray *_orderedLastVisitedDays; 76 BOOL itemLimitSet; 77 int itemLimit; 78 BOOL ageInDaysLimitSet; 79 int ageInDaysLimit; 80} 81 82- (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title increaseVisitCount:(BOOL)increaseVisitCount; 83 84- (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate; 85- (void)addItems:(NSArray *)newEntries; 86- (BOOL)removeItem:(WebHistoryItem *)entry; 87- (BOOL)removeItems:(NSArray *)entries; 88- (BOOL)removeAllItems; 89 90- (NSArray *)orderedLastVisitedDays; 91- (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)calendarDate; 92- (BOOL)containsURL:(NSURL *)URL; 93- (WebHistoryItem *)itemForURL:(NSURL *)URL; 94- (WebHistoryItem *)itemForURLString:(NSString *)URLString; 95- (NSArray *)allItems; 96 97- (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error; 98- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error; 99 100- (NSCalendarDate *)ageLimitDate; 101 102- (void)setHistoryItemLimit:(int)limit; 103- (int)historyItemLimit; 104- (void)setHistoryAgeInDaysLimit:(int)limit; 105- (int)historyAgeInDaysLimit; 106 107- (void)addVisitedLinksToPageGroup:(PageGroup&)group; 108 109@end 110 111@implementation WebHistoryPrivate 112 113// MARK: OBJECT FRAMEWORK 114 115+ (void)initialize 116{ 117 [[NSUserDefaults standardUserDefaults] registerDefaults: 118 [NSDictionary dictionaryWithObjectsAndKeys: 119 @"1000", @"WebKitHistoryItemLimit", 120 @"7", @"WebKitHistoryAgeInDaysLimit", 121 nil]]; 122} 123 124- (id)init 125{ 126 if (![super init]) 127 return nil; 128 129 _entriesByURL = [[NSMutableDictionary alloc] init]; 130 _entriesByDate = new DateToEntriesMap; 131 132 return self; 133} 134 135- (void)dealloc 136{ 137 [_entriesByURL release]; 138 [_orderedLastVisitedDays release]; 139 delete _entriesByDate; 140 [super dealloc]; 141} 142 143- (void)finalize 144{ 145 delete _entriesByDate; 146 [super finalize]; 147} 148 149// MARK: MODIFYING CONTENTS 150 151static void getDayBoundaries(NSTimeInterval interval, NSTimeInterval& beginningOfDay, NSTimeInterval& beginningOfNextDay) 152{ 153 CFTimeZoneRef timeZone = CFTimeZoneCopyDefault(); 154 CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(interval, timeZone); 155 date.hour = 0; 156 date.minute = 0; 157 date.second = 0; 158 beginningOfDay = CFGregorianDateGetAbsoluteTime(date, timeZone); 159 date.day += 1; 160 beginningOfNextDay = CFGregorianDateGetAbsoluteTime(date, timeZone); 161 CFRelease(timeZone); 162} 163 164static inline NSTimeInterval beginningOfDay(NSTimeInterval date) 165{ 166 static NSTimeInterval cachedBeginningOfDay = NAN; 167 static NSTimeInterval cachedBeginningOfNextDay; 168 if (!(date >= cachedBeginningOfDay && date < cachedBeginningOfNextDay)) 169 getDayBoundaries(date, cachedBeginningOfDay, cachedBeginningOfNextDay); 170 return cachedBeginningOfDay; 171} 172 173static inline WebHistoryDateKey dateKey(NSTimeInterval date) 174{ 175 // Converting from double (NSTimeInterval) to int64_t (WebHistoryDateKey) is 176 // safe here because all sensible dates are in the range -2**48 .. 2**47 which 177 // safely fits in an int64_t. 178 return beginningOfDay(date); 179} 180 181// Returns whether the day is already in the list of days, 182// and fills in *key with the key used to access its location 183- (BOOL)findKey:(WebHistoryDateKey*)key forDay:(NSTimeInterval)date 184{ 185 ASSERT_ARG(key, key); 186 *key = dateKey(date); 187 return _entriesByDate->contains(*key); 188} 189 190- (void)insertItem:(WebHistoryItem *)entry forDateKey:(WebHistoryDateKey)dateKey 191{ 192 ASSERT_ARG(entry, entry != nil); 193 ASSERT(_entriesByDate->contains(dateKey)); 194 195 NSMutableArray *entriesForDate = _entriesByDate->get(dateKey).get(); 196 NSTimeInterval entryDate = [entry lastVisitedTimeInterval]; 197 198 unsigned count = [entriesForDate count]; 199 200 // The entries for each day are stored in a sorted array with the most recent entry first 201 // Check for the common cases of the entry being newer than all existing entries or the first entry of the day 202 if (!count || [[entriesForDate objectAtIndex:0] lastVisitedTimeInterval] < entryDate) { 203 [entriesForDate insertObject:entry atIndex:0]; 204 return; 205 } 206 // .. or older than all existing entries 207 if (count > 0 && [[entriesForDate objectAtIndex:count - 1] lastVisitedTimeInterval] >= entryDate) { 208 [entriesForDate insertObject:entry atIndex:count]; 209 return; 210 } 211 212 unsigned low = 0; 213 unsigned high = count; 214 while (low < high) { 215 unsigned mid = low + (high - low) / 2; 216 if ([[entriesForDate objectAtIndex:mid] lastVisitedTimeInterval] >= entryDate) 217 low = mid + 1; 218 else 219 high = mid; 220 } 221 222 // low is now the index of the first entry that is older than entryDate 223 [entriesForDate insertObject:entry atIndex:low]; 224} 225 226- (BOOL)removeItemFromDateCaches:(WebHistoryItem *)entry 227{ 228 WebHistoryDateKey dateKey; 229 BOOL foundDate = [self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]; 230 231 if (!foundDate) 232 return NO; 233 234 DateToEntriesMap::iterator it = _entriesByDate->find(dateKey); 235 NSMutableArray *entriesForDate = it->second.get(); 236 [entriesForDate removeObjectIdenticalTo:entry]; 237 238 // remove this date entirely if there are no other entries on it 239 if ([entriesForDate count] == 0) { 240 _entriesByDate->remove(it); 241 // Clear _orderedLastVisitedDays so it will be regenerated when next requested. 242 [_orderedLastVisitedDays release]; 243 _orderedLastVisitedDays = nil; 244 } 245 246 return YES; 247} 248 249- (BOOL)removeItemForURLString:(NSString *)URLString 250{ 251 WebHistoryItem *entry = [_entriesByURL objectForKey:URLString]; 252 if (!entry) 253 return NO; 254 255 [_entriesByURL removeObjectForKey:URLString]; 256 257#if ASSERT_DISABLED 258 [self removeItemFromDateCaches:entry]; 259#else 260 BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry]; 261 ASSERT(itemWasInDateCaches); 262#endif 263 264 if (![_entriesByURL count]) 265 PageGroup::removeAllVisitedLinks(); 266 267 return YES; 268} 269 270- (void)addItemToDateCaches:(WebHistoryItem *)entry 271{ 272 WebHistoryDateKey dateKey; 273 if ([self findKey:&dateKey forDay:[entry lastVisitedTimeInterval]]) 274 // other entries already exist for this date 275 [self insertItem:entry forDateKey:dateKey]; 276 else { 277 // no other entries exist for this date 278 NSMutableArray *entries = [[NSMutableArray alloc] initWithObjects:&entry count:1]; 279 _entriesByDate->set(dateKey, entries); 280 [entries release]; 281 // Clear _orderedLastVisitedDays so it will be regenerated when next requested. 282 [_orderedLastVisitedDays release]; 283 _orderedLastVisitedDays = nil; 284 } 285} 286 287- (WebHistoryItem *)visitedURL:(NSURL *)url withTitle:(NSString *)title increaseVisitCount:(BOOL)increaseVisitCount 288{ 289 ASSERT(url); 290 ASSERT(title); 291 292 NSString *URLString = [url _web_originalDataAsString]; 293 WebHistoryItem *entry = [_entriesByURL objectForKey:URLString]; 294 295 if (entry) { 296 LOG(History, "Updating global history entry %@", entry); 297 // Remove the item from date caches before changing its last visited date. Otherwise we might get duplicate entries 298 // as seen in <rdar://problem/6570573>. 299 BOOL itemWasInDateCaches = [self removeItemFromDateCaches:entry]; 300 ASSERT_UNUSED(itemWasInDateCaches, itemWasInDateCaches); 301 302 [entry _visitedWithTitle:title increaseVisitCount:increaseVisitCount]; 303 } else { 304 LOG(History, "Adding new global history entry for %@", url); 305 entry = [[WebHistoryItem alloc] initWithURLString:URLString title:title lastVisitedTimeInterval:[NSDate timeIntervalSinceReferenceDate]]; 306 [entry _recordInitialVisit]; 307 [_entriesByURL setObject:entry forKey:URLString]; 308 [entry release]; 309 } 310 311 [self addItemToDateCaches:entry]; 312 313 return entry; 314} 315 316- (BOOL)addItem:(WebHistoryItem *)entry discardDuplicate:(BOOL)discardDuplicate 317{ 318 ASSERT_ARG(entry, entry); 319 ASSERT_ARG(entry, [entry lastVisitedTimeInterval] != 0); 320 321 NSString *URLString = [entry URLString]; 322 323 WebHistoryItem *oldEntry = [_entriesByURL objectForKey:URLString]; 324 if (oldEntry) { 325 if (discardDuplicate) 326 return NO; 327 328 // The last reference to oldEntry might be this dictionary, so we hold onto a reference 329 // until we're done with oldEntry. 330 [oldEntry retain]; 331 [self removeItemForURLString:URLString]; 332 333 // If we already have an item with this URL, we need to merge info that drives the 334 // URL autocomplete heuristics from that item into the new one. 335 [entry _mergeAutoCompleteHints:oldEntry]; 336 [oldEntry release]; 337 } 338 339 [self addItemToDateCaches:entry]; 340 [_entriesByURL setObject:entry forKey:URLString]; 341 342 return YES; 343} 344 345- (BOOL)removeItem:(WebHistoryItem *)entry 346{ 347 NSString *URLString = [entry URLString]; 348 349 // If this exact object isn't stored, then make no change. 350 // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is? 351 // Maybe need to change the API to make something like removeEntryForURLString public instead. 352 WebHistoryItem *matchingEntry = [_entriesByURL objectForKey:URLString]; 353 if (matchingEntry != entry) 354 return NO; 355 356 [self removeItemForURLString:URLString]; 357 358 return YES; 359} 360 361- (BOOL)removeItems:(NSArray *)entries 362{ 363 NSUInteger count = [entries count]; 364 if (!count) 365 return NO; 366 367 for (NSUInteger index = 0; index < count; ++index) 368 [self removeItem:[entries objectAtIndex:index]]; 369 370 return YES; 371} 372 373- (BOOL)removeAllItems 374{ 375 if (_entriesByDate->isEmpty()) 376 return NO; 377 378 _entriesByDate->clear(); 379 [_entriesByURL removeAllObjects]; 380 381 // Clear _orderedLastVisitedDays so it will be regenerated when next requested. 382 [_orderedLastVisitedDays release]; 383 _orderedLastVisitedDays = nil; 384 385 PageGroup::removeAllVisitedLinks(); 386 387 return YES; 388} 389 390- (void)addItems:(NSArray *)newEntries 391{ 392 // There is no guarantee that the incoming entries are in any particular 393 // order, but if this is called with a set of entries that were created by 394 // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDayy 395 // then they will be ordered chronologically from newest to oldest. We can make adding them 396 // faster (fewer compares) by inserting them from oldest to newest. 397 NSEnumerator *enumerator = [newEntries reverseObjectEnumerator]; 398 while (WebHistoryItem *entry = [enumerator nextObject]) 399 [self addItem:entry discardDuplicate:NO]; 400} 401 402// MARK: DATE-BASED RETRIEVAL 403 404- (NSArray *)orderedLastVisitedDays 405{ 406 if (!_orderedLastVisitedDays) { 407 Vector<int> daysAsTimeIntervals; 408 daysAsTimeIntervals.reserveCapacity(_entriesByDate->size()); 409 DateToEntriesMap::const_iterator end = _entriesByDate->end(); 410 for (DateToEntriesMap::const_iterator it = _entriesByDate->begin(); it != end; ++it) 411 daysAsTimeIntervals.append(it->first); 412 413 sort(daysAsTimeIntervals.begin(), daysAsTimeIntervals.end()); 414 size_t count = daysAsTimeIntervals.size(); 415 _orderedLastVisitedDays = [[NSMutableArray alloc] initWithCapacity:count]; 416 for (int i = count - 1; i >= 0; i--) { 417 NSTimeInterval interval = daysAsTimeIntervals[i]; 418 NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate:interval]; 419 [_orderedLastVisitedDays addObject:date]; 420 [date release]; 421 } 422 } 423 return _orderedLastVisitedDays; 424} 425 426- (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date 427{ 428 WebHistoryDateKey dateKey; 429 if (![self findKey:&dateKey forDay:[date timeIntervalSinceReferenceDate]]) 430 return nil; 431 return _entriesByDate->get(dateKey).get(); 432} 433 434// MARK: URL MATCHING 435 436- (WebHistoryItem *)itemForURLString:(NSString *)URLString 437{ 438 return [_entriesByURL objectForKey:URLString]; 439} 440 441- (BOOL)containsURL:(NSURL *)URL 442{ 443 return [self itemForURLString:[URL _web_originalDataAsString]] != nil; 444} 445 446- (WebHistoryItem *)itemForURL:(NSURL *)URL 447{ 448 return [self itemForURLString:[URL _web_originalDataAsString]]; 449} 450 451- (NSArray *)allItems 452{ 453 return [_entriesByURL allValues]; 454} 455 456// MARK: ARCHIVING/UNARCHIVING 457 458- (void)setHistoryAgeInDaysLimit:(int)limit 459{ 460 ageInDaysLimitSet = YES; 461 ageInDaysLimit = limit; 462} 463 464- (int)historyAgeInDaysLimit 465{ 466 if (ageInDaysLimitSet) 467 return ageInDaysLimit; 468 return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryAgeInDaysLimit"]; 469} 470 471- (void)setHistoryItemLimit:(int)limit 472{ 473 itemLimitSet = YES; 474 itemLimit = limit; 475} 476 477- (int)historyItemLimit 478{ 479 if (itemLimitSet) 480 return itemLimit; 481 return [[NSUserDefaults standardUserDefaults] integerForKey:@"WebKitHistoryItemLimit"]; 482} 483 484// Return a date that marks the age limit for history entries saved to or 485// loaded from disk. Any entry older than this item should be rejected. 486- (NSCalendarDate *)ageLimitDate 487{ 488 return [[NSCalendarDate calendarDate] dateByAddingYears:0 months:0 days:-[self historyAgeInDaysLimit] 489 hours:0 minutes:0 seconds:0]; 490} 491 492- (BOOL)loadHistoryGutsFromURL:(NSURL *)URL savedItemsCount:(int *)numberOfItemsLoaded collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error 493{ 494 *numberOfItemsLoaded = 0; 495 NSDictionary *dictionary = nil; 496 497 // Optimize loading from local file, which is faster than using the general URL loading mechanism 498 if ([URL isFileURL]) { 499 dictionary = [NSDictionary dictionaryWithContentsOfFile:[URL path]]; 500 if (!dictionary) { 501#if !LOG_DISABLED 502 if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]) 503 LOG_ERROR("unable to read history from file %@; perhaps contents are corrupted", [URL path]); 504#endif 505 // else file doesn't exist, which is normal the first time 506 return NO; 507 } 508 } else { 509 NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:URL] returningResponse:nil error:error]; 510 if (data && [data length] > 0) { 511 dictionary = [NSPropertyListSerialization propertyListFromData:data 512 mutabilityOption:NSPropertyListImmutable 513 format:nil 514 errorDescription:nil]; 515 } 516 } 517 518 // We used to support NSArrays here, but that was before Safari 1.0 shipped. We will no longer support 519 // that ancient format, so anything that isn't an NSDictionary is bogus. 520 if (![dictionary isKindOfClass:[NSDictionary class]]) 521 return NO; 522 523 NSNumber *fileVersionObject = [dictionary objectForKey:FileVersionKey]; 524 int fileVersion; 525 // we don't trust data obtained from elsewhere, so double-check 526 if (!fileVersionObject || ![fileVersionObject isKindOfClass:[NSNumber class]]) { 527 LOG_ERROR("history file version can't be determined, therefore not loading"); 528 return NO; 529 } 530 fileVersion = [fileVersionObject intValue]; 531 if (fileVersion > currentFileVersion) { 532 LOG_ERROR("history file version is %d, newer than newest known version %d, therefore not loading", fileVersion, currentFileVersion); 533 return NO; 534 } 535 536 NSArray *array = [dictionary objectForKey:DatesArrayKey]; 537 538 int itemCountLimit = [self historyItemLimit]; 539 NSTimeInterval ageLimitDate = [[self ageLimitDate] timeIntervalSinceReferenceDate]; 540 NSEnumerator *enumerator = [array objectEnumerator]; 541 BOOL ageLimitPassed = NO; 542 BOOL itemLimitPassed = NO; 543 ASSERT(*numberOfItemsLoaded == 0); 544 545 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 546 NSDictionary *itemAsDictionary; 547 while ((itemAsDictionary = [enumerator nextObject]) != nil) { 548 WebHistoryItem *item = [[WebHistoryItem alloc] initFromDictionaryRepresentation:itemAsDictionary]; 549 550 // item without URL is useless; data on disk must have been bad; ignore 551 if ([item URLString]) { 552 // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing 553 // once we've found the first item that's too old. 554 if (!ageLimitPassed && [item lastVisitedTimeInterval] <= ageLimitDate) 555 ageLimitPassed = YES; 556 557 if (ageLimitPassed || itemLimitPassed) 558 [discardedItems addObject:item]; 559 else { 560 if ([self addItem:item discardDuplicate:YES]) 561 ++(*numberOfItemsLoaded); 562 if (*numberOfItemsLoaded == itemCountLimit) 563 itemLimitPassed = YES; 564 565 // Draining the autorelease pool every 50 iterations was found by experimentation to be optimal 566 if (*numberOfItemsLoaded % 50 == 0) { 567 [pool drain]; 568 pool = [[NSAutoreleasePool alloc] init]; 569 } 570 } 571 } 572 [item release]; 573 } 574 [pool drain]; 575 576 return YES; 577} 578 579- (BOOL)loadFromURL:(NSURL *)URL collectDiscardedItemsInto:(NSMutableArray *)discardedItems error:(NSError **)error 580{ 581#if !LOG_DISABLED 582 double start = CFAbsoluteTimeGetCurrent(); 583#endif 584 585 int numberOfItems; 586 if (![self loadHistoryGutsFromURL:URL savedItemsCount:&numberOfItems collectDiscardedItemsInto:discardedItems error:error]) 587 return NO; 588 589#if !LOG_DISABLED 590 double duration = CFAbsoluteTimeGetCurrent() - start; 591 LOG(Timing, "loading %d history entries from %@ took %f seconds", numberOfItems, URL, duration); 592#endif 593 594 return YES; 595} 596 597- (NSData *)data 598{ 599 if (_entriesByDate->isEmpty()) { 600 static NSData *emptyHistoryData = (NSData *)CFDataCreate(0, 0, 0); 601 return emptyHistoryData; 602 } 603 604 // Ignores the date and item count limits; these are respected when loading instead of when saving, so 605 // that clients can learn of discarded items by listening to WebHistoryItemsDiscardedWhileLoadingNotification. 606 WebHistoryWriter writer(_entriesByDate); 607 writer.writePropertyList(); 608 return [[(NSData *)writer.releaseData().get() retain] autorelease]; 609} 610 611- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error 612{ 613#if !LOG_DISABLED 614 double start = CFAbsoluteTimeGetCurrent(); 615#endif 616 617 BOOL result = [[self data] writeToURL:URL options:0 error:error]; 618 619#if !LOG_DISABLED 620 double duration = CFAbsoluteTimeGetCurrent() - start; 621 LOG(Timing, "saving history to %@ took %f seconds", URL, duration); 622#endif 623 624 return result; 625} 626 627- (void)addVisitedLinksToPageGroup:(PageGroup&)group 628{ 629 NSEnumerator *enumerator = [_entriesByURL keyEnumerator]; 630 while (NSString *url = [enumerator nextObject]) { 631 size_t length = [url length]; 632 const UChar* characters = CFStringGetCharactersPtr(reinterpret_cast<CFStringRef>(url)); 633 if (characters) 634 group.addVisitedLink(characters, length); 635 else { 636 Vector<UChar, 512> buffer(length); 637 [url getCharacters:buffer.data()]; 638 group.addVisitedLink(buffer.data(), length); 639 } 640 } 641} 642 643@end 644 645@implementation WebHistory 646 647+ (WebHistory *)optionalSharedHistory 648{ 649 return _sharedHistory; 650} 651 652+ (void)setOptionalSharedHistory:(WebHistory *)history 653{ 654 if (_sharedHistory == history) 655 return; 656 // FIXME: Need to think about multiple instances of WebHistory per application 657 // and correct synchronization of history file between applications. 658 [_sharedHistory release]; 659 _sharedHistory = [history retain]; 660 PageGroup::setShouldTrackVisitedLinks(history); 661 PageGroup::removeAllVisitedLinks(); 662} 663 664- (id)init 665{ 666 self = [super init]; 667 if (!self) 668 return nil; 669 _historyPrivate = [[WebHistoryPrivate alloc] init]; 670 return self; 671} 672 673- (void)dealloc 674{ 675 [_historyPrivate release]; 676 [super dealloc]; 677} 678 679// MARK: MODIFYING CONTENTS 680 681- (void)_sendNotification:(NSString *)name entries:(NSArray *)entries 682{ 683 NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:entries, WebHistoryItemsKey, nil]; 684 [[NSNotificationCenter defaultCenter] 685 postNotificationName:name object:self userInfo:userInfo]; 686} 687 688- (void)removeItems:(NSArray *)entries 689{ 690 if ([_historyPrivate removeItems:entries]) { 691 [self _sendNotification:WebHistoryItemsRemovedNotification 692 entries:entries]; 693 } 694} 695 696- (void)removeAllItems 697{ 698 NSArray *entries = [_historyPrivate allItems]; 699 if ([_historyPrivate removeAllItems]) 700 [self _sendNotification:WebHistoryAllItemsRemovedNotification entries:entries]; 701} 702 703- (void)addItems:(NSArray *)newEntries 704{ 705 [_historyPrivate addItems:newEntries]; 706 [self _sendNotification:WebHistoryItemsAddedNotification 707 entries:newEntries]; 708} 709 710// MARK: DATE-BASED RETRIEVAL 711 712- (NSArray *)orderedLastVisitedDays 713{ 714 return [_historyPrivate orderedLastVisitedDays]; 715} 716 717- (NSArray *)orderedItemsLastVisitedOnDay:(NSCalendarDate *)date 718{ 719 return [_historyPrivate orderedItemsLastVisitedOnDay:date]; 720} 721 722// MARK: URL MATCHING 723 724- (BOOL)containsURL:(NSURL *)URL 725{ 726 return [_historyPrivate containsURL:URL]; 727} 728 729- (WebHistoryItem *)itemForURL:(NSURL *)URL 730{ 731 return [_historyPrivate itemForURL:URL]; 732} 733 734// MARK: SAVING TO DISK 735 736- (BOOL)loadFromURL:(NSURL *)URL error:(NSError **)error 737{ 738 NSMutableArray *discardedItems = [[NSMutableArray alloc] init]; 739 if (![_historyPrivate loadFromURL:URL collectDiscardedItemsInto:discardedItems error:error]) { 740 [discardedItems release]; 741 return NO; 742 } 743 744 [[NSNotificationCenter defaultCenter] 745 postNotificationName:WebHistoryLoadedNotification 746 object:self]; 747 748 if ([discardedItems count]) 749 [self _sendNotification:WebHistoryItemsDiscardedWhileLoadingNotification entries:discardedItems]; 750 751 [discardedItems release]; 752 return YES; 753} 754 755- (BOOL)saveToURL:(NSURL *)URL error:(NSError **)error 756{ 757 if (![_historyPrivate saveToURL:URL error:error]) 758 return NO; 759 [[NSNotificationCenter defaultCenter] 760 postNotificationName:WebHistorySavedNotification 761 object:self]; 762 return YES; 763} 764 765- (void)setHistoryItemLimit:(int)limit 766{ 767 [_historyPrivate setHistoryItemLimit:limit]; 768} 769 770- (int)historyItemLimit 771{ 772 return [_historyPrivate historyItemLimit]; 773} 774 775- (void)setHistoryAgeInDaysLimit:(int)limit 776{ 777 [_historyPrivate setHistoryAgeInDaysLimit:limit]; 778} 779 780- (int)historyAgeInDaysLimit 781{ 782 return [_historyPrivate historyAgeInDaysLimit]; 783} 784 785@end 786 787@implementation WebHistory (WebPrivate) 788 789- (WebHistoryItem *)_itemForURLString:(NSString *)URLString 790{ 791 return [_historyPrivate itemForURLString:URLString]; 792} 793 794- (NSArray *)allItems 795{ 796 return [_historyPrivate allItems]; 797} 798 799- (NSData *)_data 800{ 801 return [_historyPrivate data]; 802} 803 804+ (void)_setVisitedLinkTrackingEnabled:(BOOL)visitedLinkTrackingEnabled 805{ 806 PageGroup::setShouldTrackVisitedLinks(visitedLinkTrackingEnabled); 807} 808 809+ (void)_removeAllVisitedLinks 810{ 811 PageGroup::removeAllVisitedLinks(); 812} 813 814@end 815 816@implementation WebHistory (WebInternal) 817 818- (void)_visitedURL:(NSURL *)url withTitle:(NSString *)title method:(NSString *)method wasFailure:(BOOL)wasFailure increaseVisitCount:(BOOL)increaseVisitCount 819{ 820 WebHistoryItem *entry = [_historyPrivate visitedURL:url withTitle:title increaseVisitCount:increaseVisitCount]; 821 822 HistoryItem* item = core(entry); 823 item->setLastVisitWasFailure(wasFailure); 824 825 if ([method length]) 826 item->setLastVisitWasHTTPNonGet([method caseInsensitiveCompare:@"GET"] && (![[url scheme] caseInsensitiveCompare:@"http"] || ![[url scheme] caseInsensitiveCompare:@"https"])); 827 828 item->setRedirectURLs(0); 829 830 NSArray *entries = [[NSArray alloc] initWithObjects:entry, nil]; 831 [self _sendNotification:WebHistoryItemsAddedNotification entries:entries]; 832 [entries release]; 833} 834 835- (void)_addVisitedLinksToPageGroup:(WebCore::PageGroup&)group 836{ 837 [_historyPrivate addVisitedLinksToPageGroup:group]; 838} 839 840@end 841 842WebHistoryWriter::WebHistoryWriter(DateToEntriesMap* entriesByDate) 843 : m_entriesByDate(entriesByDate) 844{ 845 m_dateKeys.reserveCapacity(m_entriesByDate->size()); 846 DateToEntriesMap::const_iterator end = m_entriesByDate->end(); 847 for (DateToEntriesMap::const_iterator it = m_entriesByDate->begin(); it != end; ++it) 848 m_dateKeys.append(it->first); 849 sort(m_dateKeys.begin(), m_dateKeys.end()); 850} 851 852void WebHistoryWriter::writeHistoryItems(BinaryPropertyListObjectStream& stream) 853{ 854 for (int dateIndex = m_dateKeys.size() - 1; dateIndex >= 0; dateIndex--) { 855 NSArray *entries = m_entriesByDate->get(m_dateKeys[dateIndex]).get(); 856 NSUInteger entryCount = [entries count]; 857 for (NSUInteger entryIndex = 0; entryIndex < entryCount; ++entryIndex) 858 writeHistoryItem(stream, core([entries objectAtIndex:entryIndex])); 859 } 860} 861