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 39 using namespace WebCore; 40 using namespace std; 41 42 typedef int64_t WebHistoryDateKey; 43 typedef HashMap<WebHistoryDateKey, RetainPtr<NSMutableArray> > DateToEntriesMap; 44 45 NSString *WebHistoryItemsAddedNotification = @"WebHistoryItemsAddedNotification"; 46 NSString *WebHistoryItemsRemovedNotification = @"WebHistoryItemsRemovedNotification"; 47 NSString *WebHistoryAllItemsRemovedNotification = @"WebHistoryAllItemsRemovedNotification"; 48 NSString *WebHistoryLoadedNotification = @"WebHistoryLoadedNotification"; 49 NSString *WebHistoryItemsDiscardedWhileLoadingNotification = @"WebHistoryItemsDiscardedWhileLoadingNotification"; 50 NSString *WebHistorySavedNotification = @"WebHistorySavedNotification"; 51 NSString *WebHistoryItemsKey = @"WebHistoryItems"; 52 53 static WebHistory *_sharedHistory = nil; 54 55 NSString *FileVersionKey = @"WebHistoryFileVersion"; 56 NSString *DatesArrayKey = @"WebHistoryDates"; 57 58 #define currentFileVersion 1 59 60 class WebHistoryWriter : public HistoryPropertyListWriter { 61 public: 62 WebHistoryWriter(DateToEntriesMap*); 63 64 private: 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 #pragma 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 #pragma mark MODIFYING CONTENTS 150 151 static 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 164 static 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 173 static 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 #pragma 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 #pragma 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 #pragma 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 #pragma 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 #pragma 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 #pragma 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 #pragma 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 842 WebHistoryWriter::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 852 void 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