1 /* 2 * Copyright (C) 2006, 2007 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 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26 #include "config.h" 27 #include "WebKitDLL.h" 28 #include "WebHistory.h" 29 30 #include "CFDictionaryPropertyBag.h" 31 #include "MemoryStream.h" 32 #include "WebKit.h" 33 #include "MarshallingHelpers.h" 34 #include "WebHistoryItem.h" 35 #include "WebKit.h" 36 #include "WebNotificationCenter.h" 37 #include "WebPreferences.h" 38 #include <CoreFoundation/CoreFoundation.h> 39 #include <WebCore/HistoryItem.h> 40 #include <WebCore/HistoryPropertyList.h> 41 #include <WebCore/KURL.h> 42 #include <WebCore/PageGroup.h> 43 #include <WebCore/SharedBuffer.h> 44 #include <functional> 45 #include <wtf/StdLibExtras.h> 46 #include <wtf/Vector.h> 47 48 using namespace WebCore; 49 using namespace std; 50 51 CFStringRef DatesArrayKey = CFSTR("WebHistoryDates"); 52 CFStringRef FileVersionKey = CFSTR("WebHistoryFileVersion"); 53 54 #define currentFileVersion 1 55 56 class WebHistoryWriter : public HistoryPropertyListWriter { 57 public: 58 WebHistoryWriter(const WebHistory::DateToEntriesMap&); 59 60 private: 61 virtual void writeHistoryItems(BinaryPropertyListObjectStream&); 62 63 const WebHistory::DateToEntriesMap& m_entriesByDate; 64 Vector<WebHistory::DateKey> m_dateKeys; 65 }; 66 67 WebHistoryWriter::WebHistoryWriter(const WebHistory::DateToEntriesMap& entriesByDate) 68 : m_entriesByDate(entriesByDate) 69 { 70 copyKeysToVector(m_entriesByDate, m_dateKeys); 71 sort(m_dateKeys.begin(), m_dateKeys.end()); 72 } 73 74 void WebHistoryWriter::writeHistoryItems(BinaryPropertyListObjectStream& stream) 75 { 76 for (int dateIndex = m_dateKeys.size() - 1; dateIndex >= 0; --dateIndex) { 77 // get the entries for that date 78 CFArrayRef entries = m_entriesByDate.get(m_dateKeys[dateIndex]).get(); 79 CFIndex entriesCount = CFArrayGetCount(entries); 80 for (CFIndex j = entriesCount - 1; j >= 0; --j) { 81 IWebHistoryItem* item = (IWebHistoryItem*) CFArrayGetValueAtIndex(entries, j); 82 COMPtr<WebHistoryItem> webItem(Query, item); 83 if (!webItem) 84 continue; 85 86 writeHistoryItem(stream, webItem->historyItem()); 87 } 88 } 89 } 90 91 static bool areEqualOrClose(double d1, double d2) 92 { 93 double diff = d1-d2; 94 return (diff < .000001 && diff > -.000001); 95 } 96 97 static COMPtr<CFDictionaryPropertyBag> createUserInfoFromArray(BSTR notificationStr, CFArrayRef arrayItem) 98 { 99 RetainPtr<CFMutableDictionaryRef> dictionary(AdoptCF, 100 CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); 101 102 RetainPtr<CFStringRef> key(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(notificationStr)); 103 CFDictionaryAddValue(dictionary.get(), key.get(), arrayItem); 104 105 COMPtr<CFDictionaryPropertyBag> result = CFDictionaryPropertyBag::createInstance(); 106 result->setDictionary(dictionary.get()); 107 return result; 108 } 109 110 static COMPtr<CFDictionaryPropertyBag> createUserInfoFromHistoryItem(BSTR notificationStr, IWebHistoryItem* item) 111 { 112 // reference counting of item added to the array is managed by the CFArray value callbacks 113 RetainPtr<CFArrayRef> itemList(AdoptCF, CFArrayCreate(0, (const void**) &item, 1, &MarshallingHelpers::kIUnknownArrayCallBacks)); 114 COMPtr<CFDictionaryPropertyBag> info = createUserInfoFromArray(notificationStr, itemList.get()); 115 return info; 116 } 117 118 // WebHistory ----------------------------------------------------------------- 119 120 WebHistory::WebHistory() 121 : m_refCount(0) 122 , m_preferences(0) 123 { 124 gClassCount++; 125 gClassNameCount.add("WebHistory"); 126 127 m_entriesByURL.adoptCF(CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &MarshallingHelpers::kIUnknownDictionaryValueCallBacks)); 128 129 m_preferences = WebPreferences::sharedStandardPreferences(); 130 } 131 132 WebHistory::~WebHistory() 133 { 134 gClassCount--; 135 gClassNameCount.remove("WebHistory"); 136 } 137 138 WebHistory* WebHistory::createInstance() 139 { 140 WebHistory* instance = new WebHistory(); 141 instance->AddRef(); 142 return instance; 143 } 144 145 HRESULT WebHistory::postNotification(NotificationType notifyType, IPropertyBag* userInfo /*=0*/) 146 { 147 IWebNotificationCenter* nc = WebNotificationCenter::defaultCenterInternal(); 148 HRESULT hr = nc->postNotificationName(getNotificationString(notifyType), static_cast<IWebHistory*>(this), userInfo); 149 if (FAILED(hr)) 150 return hr; 151 152 return S_OK; 153 } 154 155 BSTR WebHistory::getNotificationString(NotificationType notifyType) 156 { 157 static BSTR keys[6] = {0}; 158 if (!keys[0]) { 159 keys[0] = SysAllocString(WebHistoryItemsAddedNotification); 160 keys[1] = SysAllocString(WebHistoryItemsRemovedNotification); 161 keys[2] = SysAllocString(WebHistoryAllItemsRemovedNotification); 162 keys[3] = SysAllocString(WebHistoryLoadedNotification); 163 keys[4] = SysAllocString(WebHistoryItemsDiscardedWhileLoadingNotification); 164 keys[5] = SysAllocString(WebHistorySavedNotification); 165 } 166 return keys[notifyType]; 167 } 168 169 // IUnknown ------------------------------------------------------------------- 170 171 HRESULT STDMETHODCALLTYPE WebHistory::QueryInterface(REFIID riid, void** ppvObject) 172 { 173 *ppvObject = 0; 174 if (IsEqualGUID(riid, CLSID_WebHistory)) 175 *ppvObject = this; 176 else if (IsEqualGUID(riid, IID_IUnknown)) 177 *ppvObject = static_cast<IWebHistory*>(this); 178 else if (IsEqualGUID(riid, IID_IWebHistory)) 179 *ppvObject = static_cast<IWebHistory*>(this); 180 else if (IsEqualGUID(riid, IID_IWebHistoryPrivate)) 181 *ppvObject = static_cast<IWebHistoryPrivate*>(this); 182 else 183 return E_NOINTERFACE; 184 185 AddRef(); 186 return S_OK; 187 } 188 189 ULONG STDMETHODCALLTYPE WebHistory::AddRef(void) 190 { 191 return ++m_refCount; 192 } 193 194 ULONG STDMETHODCALLTYPE WebHistory::Release(void) 195 { 196 ULONG newRef = --m_refCount; 197 if (!newRef) 198 delete(this); 199 200 return newRef; 201 } 202 203 // IWebHistory ---------------------------------------------------------------- 204 205 static inline COMPtr<WebHistory>& sharedHistoryStorage() 206 { 207 DEFINE_STATIC_LOCAL(COMPtr<WebHistory>, sharedHistory, ()); 208 return sharedHistory; 209 } 210 211 WebHistory* WebHistory::sharedHistory() 212 { 213 return sharedHistoryStorage().get(); 214 } 215 216 HRESULT STDMETHODCALLTYPE WebHistory::optionalSharedHistory( 217 /* [retval][out] */ IWebHistory** history) 218 { 219 *history = sharedHistory(); 220 if (*history) 221 (*history)->AddRef(); 222 return S_OK; 223 } 224 225 HRESULT STDMETHODCALLTYPE WebHistory::setOptionalSharedHistory( 226 /* [in] */ IWebHistory* history) 227 { 228 if (sharedHistoryStorage() == history) 229 return S_OK; 230 sharedHistoryStorage().query(history); 231 PageGroup::setShouldTrackVisitedLinks(sharedHistoryStorage()); 232 PageGroup::removeAllVisitedLinks(); 233 return S_OK; 234 } 235 236 HRESULT STDMETHODCALLTYPE WebHistory::loadFromURL( 237 /* [in] */ BSTR url, 238 /* [out] */ IWebError** error, 239 /* [retval][out] */ BOOL* succeeded) 240 { 241 HRESULT hr = S_OK; 242 RetainPtr<CFMutableArrayRef> discardedItems(AdoptCF, 243 CFArrayCreateMutable(0, 0, &MarshallingHelpers::kIUnknownArrayCallBacks)); 244 245 RetainPtr<CFURLRef> urlRef(AdoptCF, MarshallingHelpers::BSTRToCFURLRef(url)); 246 247 hr = loadHistoryGutsFromURL(urlRef.get(), discardedItems.get(), error); 248 if (FAILED(hr)) 249 goto exit; 250 251 hr = postNotification(kWebHistoryLoadedNotification); 252 if (FAILED(hr)) 253 goto exit; 254 255 if (CFArrayGetCount(discardedItems.get()) > 0) { 256 COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromArray(getNotificationString(kWebHistoryItemsDiscardedWhileLoadingNotification), discardedItems.get()); 257 hr = postNotification(kWebHistoryItemsDiscardedWhileLoadingNotification, userInfo.get()); 258 if (FAILED(hr)) 259 goto exit; 260 } 261 262 exit: 263 if (succeeded) 264 *succeeded = SUCCEEDED(hr); 265 return hr; 266 } 267 268 static CFDictionaryRef createHistoryListFromStream(CFReadStreamRef stream, CFPropertyListFormat format) 269 { 270 return (CFDictionaryRef)CFPropertyListCreateFromStream(0, stream, 0, kCFPropertyListImmutable, &format, 0); 271 } 272 273 HRESULT WebHistory::loadHistoryGutsFromURL(CFURLRef url, CFMutableArrayRef discardedItems, IWebError** /*error*/) //FIXME 274 { 275 CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0 | kCFPropertyListXMLFormat_v1_0; 276 HRESULT hr = S_OK; 277 int numberOfItemsLoaded = 0; 278 279 RetainPtr<CFReadStreamRef> stream(AdoptCF, CFReadStreamCreateWithFile(0, url)); 280 if (!stream) 281 return E_FAIL; 282 283 if (!CFReadStreamOpen(stream.get())) 284 return E_FAIL; 285 286 RetainPtr<CFDictionaryRef> historyList(AdoptCF, createHistoryListFromStream(stream.get(), format)); 287 CFReadStreamClose(stream.get()); 288 289 if (!historyList) 290 return E_FAIL; 291 292 CFNumberRef fileVersionObject = (CFNumberRef)CFDictionaryGetValue(historyList.get(), FileVersionKey); 293 int fileVersion; 294 if (!CFNumberGetValue(fileVersionObject, kCFNumberIntType, &fileVersion)) 295 return E_FAIL; 296 297 if (fileVersion > currentFileVersion) 298 return E_FAIL; 299 300 CFArrayRef datesArray = (CFArrayRef)CFDictionaryGetValue(historyList.get(), DatesArrayKey); 301 302 int itemCountLimit; 303 hr = historyItemLimit(&itemCountLimit); 304 if (FAILED(hr)) 305 return hr; 306 307 CFAbsoluteTime limitDate; 308 hr = ageLimitDate(&limitDate); 309 if (FAILED(hr)) 310 return hr; 311 312 bool ageLimitPassed = false; 313 bool itemLimitPassed = false; 314 315 CFIndex itemCount = CFArrayGetCount(datesArray); 316 for (CFIndex i = 0; i < itemCount; ++i) { 317 CFDictionaryRef itemAsDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(datesArray, i); 318 COMPtr<WebHistoryItem> item(AdoptCOM, WebHistoryItem::createInstance()); 319 hr = item->initFromDictionaryRepresentation((void*)itemAsDictionary); 320 if (FAILED(hr)) 321 return hr; 322 323 // item without URL is useless; data on disk must have been bad; ignore 324 BOOL hasURL; 325 hr = item->hasURLString(&hasURL); 326 if (FAILED(hr)) 327 return hr; 328 329 if (hasURL) { 330 // Test against date limit. Since the items are ordered newest to oldest, we can stop comparing 331 // once we've found the first item that's too old. 332 if (!ageLimitPassed) { 333 DATE lastVisitedTime; 334 hr = item->lastVisitedTimeInterval(&lastVisitedTime); 335 if (FAILED(hr)) 336 return hr; 337 if (timeToDate(MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedTime)) <= limitDate) 338 ageLimitPassed = true; 339 } 340 if (ageLimitPassed || itemLimitPassed) 341 CFArrayAppendValue(discardedItems, item.get()); 342 else { 343 bool added; 344 addItem(item.get(), true, &added); // ref is added inside addItem 345 if (added) 346 ++numberOfItemsLoaded; 347 if (numberOfItemsLoaded == itemCountLimit) 348 itemLimitPassed = true; 349 } 350 } 351 } 352 return hr; 353 } 354 355 HRESULT STDMETHODCALLTYPE WebHistory::saveToURL( 356 /* [in] */ BSTR url, 357 /* [out] */ IWebError** error, 358 /* [retval][out] */ BOOL* succeeded) 359 { 360 HRESULT hr = S_OK; 361 RetainPtr<CFURLRef> urlRef(AdoptCF, MarshallingHelpers::BSTRToCFURLRef(url)); 362 363 hr = saveHistoryGuts(urlRef.get(), error); 364 365 if (succeeded) 366 *succeeded = SUCCEEDED(hr); 367 if (SUCCEEDED(hr)) 368 hr = postNotification(kWebHistorySavedNotification); 369 370 return hr; 371 } 372 373 HRESULT WebHistory::saveHistoryGuts(CFURLRef url, IWebError** error) 374 { 375 HRESULT hr = S_OK; 376 377 // FIXME: Correctly report error when new API is ready. 378 if (error) 379 *error = 0; 380 381 RetainPtr<CFDataRef> data = this->data(); 382 383 RetainPtr<CFWriteStreamRef> stream(AdoptCF, CFWriteStreamCreateWithFile(kCFAllocatorDefault, url)); 384 if (!stream) 385 return E_FAIL; 386 387 if (!CFWriteStreamOpen(stream.get())) 388 return E_FAIL; 389 390 const UInt8* dataPtr = CFDataGetBytePtr(data.get()); 391 CFIndex length = CFDataGetLength(data.get()); 392 393 while (length) { 394 CFIndex bytesWritten = CFWriteStreamWrite(stream.get(), dataPtr, length); 395 if (bytesWritten <= 0) { 396 hr = E_FAIL; 397 break; 398 } 399 dataPtr += bytesWritten; 400 length -= bytesWritten; 401 } 402 403 CFWriteStreamClose(stream.get()); 404 405 return hr; 406 } 407 408 HRESULT STDMETHODCALLTYPE WebHistory::addItems( 409 /* [in] */ int itemCount, 410 /* [in] */ IWebHistoryItem** items) 411 { 412 // There is no guarantee that the incoming entries are in any particular 413 // order, but if this is called with a set of entries that were created by 414 // iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDay 415 // then they will be ordered chronologically from newest to oldest. We can make adding them 416 // faster (fewer compares) by inserting them from oldest to newest. 417 418 HRESULT hr; 419 for (int i = itemCount - 1; i >= 0; --i) { 420 hr = addItem(items[i], false, 0); 421 if (FAILED(hr)) 422 return hr; 423 } 424 425 return S_OK; 426 } 427 428 HRESULT STDMETHODCALLTYPE WebHistory::removeItems( 429 /* [in] */ int itemCount, 430 /* [in] */ IWebHistoryItem** items) 431 { 432 HRESULT hr; 433 for (int i = 0; i < itemCount; ++i) { 434 hr = removeItem(items[i]); 435 if (FAILED(hr)) 436 return hr; 437 } 438 439 return S_OK; 440 } 441 442 HRESULT STDMETHODCALLTYPE WebHistory::removeAllItems( void) 443 { 444 m_entriesByDate.clear(); 445 m_orderedLastVisitedDays.clear(); 446 447 CFIndex itemCount = CFDictionaryGetCount(m_entriesByURL.get()); 448 Vector<IWebHistoryItem*> itemsVector(itemCount); 449 CFDictionaryGetKeysAndValues(m_entriesByURL.get(), 0, (const void**)itemsVector.data()); 450 RetainPtr<CFArrayRef> allItems(AdoptCF, CFArrayCreate(kCFAllocatorDefault, (const void**)itemsVector.data(), itemCount, &MarshallingHelpers::kIUnknownArrayCallBacks)); 451 452 CFDictionaryRemoveAllValues(m_entriesByURL.get()); 453 454 PageGroup::removeAllVisitedLinks(); 455 456 COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromArray(getNotificationString(kWebHistoryAllItemsRemovedNotification), allItems.get()); 457 return postNotification(kWebHistoryAllItemsRemovedNotification, userInfo.get()); 458 } 459 460 HRESULT STDMETHODCALLTYPE WebHistory::orderedLastVisitedDays( 461 /* [out][in] */ int* count, 462 /* [in] */ DATE* calendarDates) 463 { 464 int dateCount = m_entriesByDate.size(); 465 if (!calendarDates) { 466 *count = dateCount; 467 return S_OK; 468 } 469 470 if (*count < dateCount) { 471 *count = dateCount; 472 return E_FAIL; 473 } 474 475 *count = dateCount; 476 if (!m_orderedLastVisitedDays) { 477 m_orderedLastVisitedDays = adoptArrayPtr(new DATE[dateCount]); 478 DateToEntriesMap::const_iterator::Keys end = m_entriesByDate.end().keys(); 479 int i = 0; 480 for (DateToEntriesMap::const_iterator::Keys it = m_entriesByDate.begin().keys(); it != end; ++it, ++i) 481 m_orderedLastVisitedDays[i] = MarshallingHelpers::CFAbsoluteTimeToDATE(*it); 482 // Use std::greater to sort the days in descending order (i.e., most-recent first). 483 sort(m_orderedLastVisitedDays.get(), m_orderedLastVisitedDays.get() + dateCount, greater<DATE>()); 484 } 485 486 memcpy(calendarDates, m_orderedLastVisitedDays.get(), dateCount * sizeof(DATE)); 487 return S_OK; 488 } 489 490 HRESULT STDMETHODCALLTYPE WebHistory::orderedItemsLastVisitedOnDay( 491 /* [out][in] */ int* count, 492 /* [in] */ IWebHistoryItem** items, 493 /* [in] */ DATE calendarDate) 494 { 495 DateKey dateKey; 496 if (!findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(calendarDate))) { 497 *count = 0; 498 return 0; 499 } 500 501 CFArrayRef entries = m_entriesByDate.get(dateKey).get(); 502 if (!entries) { 503 *count = 0; 504 return 0; 505 } 506 507 int newCount = CFArrayGetCount(entries); 508 509 if (!items) { 510 *count = newCount; 511 return S_OK; 512 } 513 514 if (*count < newCount) { 515 *count = newCount; 516 return E_FAIL; 517 } 518 519 *count = newCount; 520 for (int i = 0; i < newCount; i++) { 521 IWebHistoryItem* item = (IWebHistoryItem*)CFArrayGetValueAtIndex(entries, i); 522 item->AddRef(); 523 items[i] = item; 524 } 525 526 return S_OK; 527 } 528 529 HRESULT STDMETHODCALLTYPE WebHistory::allItems( 530 /* [out][in] */ int* count, 531 /* [out][retval] */ IWebHistoryItem** items) 532 { 533 int entriesByURLCount = CFDictionaryGetCount(m_entriesByURL.get()); 534 535 if (!items) { 536 *count = entriesByURLCount; 537 return S_OK; 538 } 539 540 if (*count < entriesByURLCount) { 541 *count = entriesByURLCount; 542 return E_FAIL; 543 } 544 545 *count = entriesByURLCount; 546 CFDictionaryGetKeysAndValues(m_entriesByURL.get(), 0, (const void**)items); 547 for (int i = 0; i < entriesByURLCount; i++) 548 items[i]->AddRef(); 549 550 return S_OK; 551 } 552 553 HRESULT WebHistory::data(IStream** stream) 554 { 555 if (!stream) 556 return E_POINTER; 557 558 *stream = 0; 559 560 RetainPtr<CFDataRef> historyData = data(); 561 if (!historyData) 562 return S_OK; 563 564 COMPtr<MemoryStream> result = MemoryStream::createInstance(SharedBuffer::wrapCFData(historyData.get())); 565 return result.copyRefTo(stream); 566 } 567 568 HRESULT WebHistory::setVisitedLinkTrackingEnabled(BOOL visitedLinkTrackingEnabled) 569 { 570 PageGroup::setShouldTrackVisitedLinks(visitedLinkTrackingEnabled); 571 return S_OK; 572 } 573 574 HRESULT WebHistory::removeAllVisitedLinks() 575 { 576 PageGroup::removeAllVisitedLinks(); 577 return S_OK; 578 } 579 580 HRESULT STDMETHODCALLTYPE WebHistory::setHistoryItemLimit( 581 /* [in] */ int limit) 582 { 583 if (!m_preferences) 584 return E_FAIL; 585 return m_preferences->setHistoryItemLimit(limit); 586 } 587 588 HRESULT STDMETHODCALLTYPE WebHistory::historyItemLimit( 589 /* [retval][out] */ int* limit) 590 { 591 if (!m_preferences) 592 return E_FAIL; 593 return m_preferences->historyItemLimit(limit); 594 } 595 596 HRESULT STDMETHODCALLTYPE WebHistory::setHistoryAgeInDaysLimit( 597 /* [in] */ int limit) 598 { 599 if (!m_preferences) 600 return E_FAIL; 601 return m_preferences->setHistoryAgeInDaysLimit(limit); 602 } 603 604 HRESULT STDMETHODCALLTYPE WebHistory::historyAgeInDaysLimit( 605 /* [retval][out] */ int* limit) 606 { 607 if (!m_preferences) 608 return E_FAIL; 609 return m_preferences->historyAgeInDaysLimit(limit); 610 } 611 612 HRESULT WebHistory::removeItem(IWebHistoryItem* entry) 613 { 614 HRESULT hr = S_OK; 615 BSTR urlBStr = 0; 616 617 hr = entry->URLString(&urlBStr); 618 if (FAILED(hr)) 619 return hr; 620 621 RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(urlBStr)); 622 SysFreeString(urlBStr); 623 624 // If this exact object isn't stored, then make no change. 625 // FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is? 626 // Maybe need to change the API to make something like removeEntryForURLString public instead. 627 IWebHistoryItem *matchingEntry = (IWebHistoryItem*)CFDictionaryGetValue(m_entriesByURL.get(), urlString.get()); 628 if (matchingEntry != entry) 629 return E_FAIL; 630 631 hr = removeItemForURLString(urlString.get()); 632 if (FAILED(hr)) 633 return hr; 634 635 COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem( 636 getNotificationString(kWebHistoryItemsRemovedNotification), entry); 637 hr = postNotification(kWebHistoryItemsRemovedNotification, userInfo.get()); 638 639 return hr; 640 } 641 642 HRESULT WebHistory::addItem(IWebHistoryItem* entry, bool discardDuplicate, bool* added) 643 { 644 HRESULT hr = S_OK; 645 646 if (!entry) 647 return E_FAIL; 648 649 BSTR urlBStr = 0; 650 hr = entry->URLString(&urlBStr); 651 if (FAILED(hr)) 652 return hr; 653 654 RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(urlBStr)); 655 SysFreeString(urlBStr); 656 657 COMPtr<IWebHistoryItem> oldEntry((IWebHistoryItem*) CFDictionaryGetValue( 658 m_entriesByURL.get(), urlString.get())); 659 660 if (oldEntry) { 661 if (discardDuplicate) { 662 if (added) 663 *added = false; 664 return S_OK; 665 } 666 667 removeItemForURLString(urlString.get()); 668 669 // If we already have an item with this URL, we need to merge info that drives the 670 // URL autocomplete heuristics from that item into the new one. 671 IWebHistoryItemPrivate* entryPriv; 672 hr = entry->QueryInterface(IID_IWebHistoryItemPrivate, (void**)&entryPriv); 673 if (SUCCEEDED(hr)) { 674 entryPriv->mergeAutoCompleteHints(oldEntry.get()); 675 entryPriv->Release(); 676 } 677 } 678 679 hr = addItemToDateCaches(entry); 680 if (FAILED(hr)) 681 return hr; 682 683 CFDictionarySetValue(m_entriesByURL.get(), urlString.get(), entry); 684 685 COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem( 686 getNotificationString(kWebHistoryItemsAddedNotification), entry); 687 hr = postNotification(kWebHistoryItemsAddedNotification, userInfo.get()); 688 689 if (added) 690 *added = true; 691 692 return hr; 693 } 694 695 void WebHistory::visitedURL(const KURL& url, const String& title, const String& httpMethod, bool wasFailure, bool increaseVisitCount) 696 { 697 RetainPtr<CFStringRef> urlString(AdoptCF, url.string().createCFString()); 698 699 IWebHistoryItem* entry = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString.get()); 700 if (entry) { 701 COMPtr<IWebHistoryItemPrivate> entryPrivate(Query, entry); 702 if (!entryPrivate) 703 return; 704 705 // Remove the item from date caches before changing its last visited date. Otherwise we might get duplicate entries 706 // as seen in <rdar://problem/6570573>. 707 removeItemFromDateCaches(entry); 708 entryPrivate->visitedWithTitle(BString(title), increaseVisitCount); 709 } else { 710 COMPtr<WebHistoryItem> item(AdoptCOM, WebHistoryItem::createInstance()); 711 if (!item) 712 return; 713 714 entry = item.get(); 715 716 SYSTEMTIME currentTime; 717 GetSystemTime(¤tTime); 718 DATE lastVisited; 719 if (!SystemTimeToVariantTime(¤tTime, &lastVisited)) 720 return; 721 722 if (FAILED(entry->initWithURLString(BString(url.string()), BString(title), lastVisited))) 723 return; 724 725 item->recordInitialVisit(); 726 727 CFDictionarySetValue(m_entriesByURL.get(), urlString.get(), entry); 728 } 729 730 addItemToDateCaches(entry); 731 732 COMPtr<IWebHistoryItemPrivate> entryPrivate(Query, entry); 733 if (!entryPrivate) 734 return; 735 736 entryPrivate->setLastVisitWasFailure(wasFailure); 737 if (!httpMethod.isEmpty()) 738 entryPrivate->setLastVisitWasHTTPNonGet(!equalIgnoringCase(httpMethod, "GET") && url.protocolInHTTPFamily()); 739 740 COMPtr<WebHistoryItem> item(Query, entry); 741 item->historyItem()->setRedirectURLs(0); 742 743 COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem( 744 getNotificationString(kWebHistoryItemsAddedNotification), entry); 745 postNotification(kWebHistoryItemsAddedNotification, userInfo.get()); 746 } 747 748 HRESULT WebHistory::itemForURLString( 749 /* [in] */ CFStringRef urlString, 750 /* [retval][out] */ IWebHistoryItem** item) const 751 { 752 if (!item) 753 return E_FAIL; 754 *item = 0; 755 756 IWebHistoryItem* foundItem = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString); 757 if (!foundItem) 758 return E_FAIL; 759 760 foundItem->AddRef(); 761 *item = foundItem; 762 return S_OK; 763 } 764 765 HRESULT STDMETHODCALLTYPE WebHistory::itemForURL( 766 /* [in] */ BSTR url, 767 /* [retval][out] */ IWebHistoryItem** item) 768 { 769 RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(url)); 770 return itemForURLString(urlString.get(), item); 771 } 772 773 HRESULT WebHistory::removeItemForURLString(CFStringRef urlString) 774 { 775 IWebHistoryItem* entry = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString); 776 if (!entry) 777 return E_FAIL; 778 779 HRESULT hr = removeItemFromDateCaches(entry); 780 CFDictionaryRemoveValue(m_entriesByURL.get(), urlString); 781 782 if (!CFDictionaryGetCount(m_entriesByURL.get())) 783 PageGroup::removeAllVisitedLinks(); 784 785 return hr; 786 } 787 788 COMPtr<IWebHistoryItem> WebHistory::itemForURLString(const String& urlString) const 789 { 790 RetainPtr<CFStringRef> urlCFString(AdoptCF, urlString.createCFString()); 791 if (!urlCFString) 792 return 0; 793 COMPtr<IWebHistoryItem> item; 794 if (FAILED(itemForURLString(urlCFString.get(), &item))) 795 return 0; 796 return item; 797 } 798 799 HRESULT WebHistory::addItemToDateCaches(IWebHistoryItem* entry) 800 { 801 HRESULT hr = S_OK; 802 803 DATE lastVisitedCOMTime; 804 entry->lastVisitedTimeInterval(&lastVisitedCOMTime); 805 806 DateKey dateKey; 807 if (findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedCOMTime))) { 808 // other entries already exist for this date 809 hr = insertItem(entry, dateKey); 810 } else { 811 ASSERT(!m_entriesByDate.contains(dateKey)); 812 // no other entries exist for this date 813 RetainPtr<CFMutableArrayRef> entryArray(AdoptCF, 814 CFArrayCreateMutable(0, 0, &MarshallingHelpers::kIUnknownArrayCallBacks)); 815 CFArrayAppendValue(entryArray.get(), entry); 816 m_entriesByDate.set(dateKey, entryArray); 817 // Clear m_orderedLastVisitedDays so it will be regenerated when next requested. 818 m_orderedLastVisitedDays.clear(); 819 } 820 821 return hr; 822 } 823 824 HRESULT WebHistory::removeItemFromDateCaches(IWebHistoryItem* entry) 825 { 826 HRESULT hr = S_OK; 827 828 DATE lastVisitedCOMTime; 829 entry->lastVisitedTimeInterval(&lastVisitedCOMTime); 830 831 DateKey dateKey; 832 if (!findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedCOMTime))) 833 return E_FAIL; 834 835 DateToEntriesMap::iterator found = m_entriesByDate.find(dateKey); 836 ASSERT(found != m_entriesByDate.end()); 837 CFMutableArrayRef entriesForDate = found->second.get(); 838 839 CFIndex count = CFArrayGetCount(entriesForDate); 840 for (int i = count - 1; i >= 0; --i) { 841 if ((IWebHistoryItem*)CFArrayGetValueAtIndex(entriesForDate, i) == entry) 842 CFArrayRemoveValueAtIndex(entriesForDate, i); 843 } 844 845 // remove this date entirely if there are no other entries on it 846 if (CFArrayGetCount(entriesForDate) == 0) { 847 m_entriesByDate.remove(found); 848 // Clear m_orderedLastVisitedDays so it will be regenerated when next requested. 849 m_orderedLastVisitedDays.clear(); 850 } 851 852 return hr; 853 } 854 855 static void getDayBoundaries(CFAbsoluteTime day, CFAbsoluteTime& beginningOfDay, CFAbsoluteTime& beginningOfNextDay) 856 { 857 RetainPtr<CFTimeZoneRef> timeZone(AdoptCF, CFTimeZoneCopyDefault()); 858 CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(day, timeZone.get()); 859 date.hour = 0; 860 date.minute = 0; 861 date.second = 0; 862 beginningOfDay = CFGregorianDateGetAbsoluteTime(date, timeZone.get()); 863 date.day += 1; 864 beginningOfNextDay = CFGregorianDateGetAbsoluteTime(date, timeZone.get()); 865 } 866 867 static inline CFAbsoluteTime beginningOfDay(CFAbsoluteTime date) 868 { 869 static CFAbsoluteTime cachedBeginningOfDay = numeric_limits<CFAbsoluteTime>::quiet_NaN(); 870 static CFAbsoluteTime cachedBeginningOfNextDay; 871 if (!(date >= cachedBeginningOfDay && date < cachedBeginningOfNextDay)) 872 getDayBoundaries(date, cachedBeginningOfDay, cachedBeginningOfNextDay); 873 return cachedBeginningOfDay; 874 } 875 876 static inline WebHistory::DateKey dateKey(CFAbsoluteTime date) 877 { 878 // Converting from double (CFAbsoluteTime) to int64_t (WebHistoryDateKey) is 879 // safe here because all sensible dates are in the range -2**48 .. 2**47 which 880 // safely fits in an int64_t. 881 return beginningOfDay(date); 882 } 883 884 // Returns whether the day is already in the list of days, 885 // and fills in *key with the found or proposed key. 886 bool WebHistory::findKey(DateKey* key, CFAbsoluteTime forDay) 887 { 888 ASSERT_ARG(key, key); 889 890 *key = dateKey(forDay); 891 return m_entriesByDate.contains(*key); 892 } 893 894 HRESULT WebHistory::insertItem(IWebHistoryItem* entry, DateKey dateKey) 895 { 896 ASSERT_ARG(entry, entry); 897 ASSERT_ARG(dateKey, m_entriesByDate.contains(dateKey)); 898 899 HRESULT hr = S_OK; 900 901 if (!entry) 902 return E_FAIL; 903 904 DATE entryTime; 905 entry->lastVisitedTimeInterval(&entryTime); 906 CFMutableArrayRef entriesForDate = m_entriesByDate.get(dateKey).get(); 907 unsigned count = CFArrayGetCount(entriesForDate); 908 909 // The entries for each day are stored in a sorted array with the most recent entry first 910 // Check for the common cases of the entry being newer than all existing entries or the first entry of the day 911 bool isNewerThanAllEntries = false; 912 if (count) { 913 IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, 0))); 914 DATE itemTime; 915 isNewerThanAllEntries = SUCCEEDED(item->lastVisitedTimeInterval(&itemTime)) && itemTime < entryTime; 916 } 917 if (!count || isNewerThanAllEntries) { 918 CFArrayInsertValueAtIndex(entriesForDate, 0, entry); 919 return S_OK; 920 } 921 922 // .. or older than all existing entries 923 bool isOlderThanAllEntries = false; 924 if (count > 0) { 925 IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, count - 1))); 926 DATE itemTime; 927 isOlderThanAllEntries = SUCCEEDED(item->lastVisitedTimeInterval(&itemTime)) && itemTime >= entryTime; 928 } 929 if (isOlderThanAllEntries) { 930 CFArrayInsertValueAtIndex(entriesForDate, count, entry); 931 return S_OK; 932 } 933 934 unsigned low = 0; 935 unsigned high = count; 936 while (low < high) { 937 unsigned mid = low + (high - low) / 2; 938 IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, mid))); 939 DATE itemTime; 940 if (FAILED(item->lastVisitedTimeInterval(&itemTime))) 941 return E_FAIL; 942 943 if (itemTime >= entryTime) 944 low = mid + 1; 945 else 946 high = mid; 947 } 948 949 // low is now the index of the first entry that is older than entryDate 950 CFArrayInsertValueAtIndex(entriesForDate, low, entry); 951 return S_OK; 952 } 953 954 CFAbsoluteTime WebHistory::timeToDate(CFAbsoluteTime time) 955 { 956 // can't just divide/round since the day boundaries depend on our current time zone 957 const double secondsPerDay = 60 * 60 * 24; 958 CFTimeZoneRef timeZone = CFTimeZoneCopySystem(); 959 CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(time, timeZone); 960 date.hour = date.minute = 0; 961 date.second = 0.0; 962 CFAbsoluteTime timeInDays = CFGregorianDateGetAbsoluteTime(date, timeZone); 963 if (areEqualOrClose(time - timeInDays, secondsPerDay)) 964 timeInDays += secondsPerDay; 965 return timeInDays; 966 } 967 968 // Return a date that marks the age limit for history entries saved to or 969 // loaded from disk. Any entry older than this item should be rejected. 970 HRESULT WebHistory::ageLimitDate(CFAbsoluteTime* time) 971 { 972 // get the current date as a CFAbsoluteTime 973 CFAbsoluteTime currentDate = timeToDate(CFAbsoluteTimeGetCurrent()); 974 975 CFGregorianUnits ageLimit = {0}; 976 int historyLimitDays; 977 HRESULT hr = historyAgeInDaysLimit(&historyLimitDays); 978 if (FAILED(hr)) 979 return hr; 980 ageLimit.days = -historyLimitDays; 981 *time = CFAbsoluteTimeAddGregorianUnits(currentDate, CFTimeZoneCopySystem(), ageLimit); 982 return S_OK; 983 } 984 985 static void addVisitedLinkToPageGroup(const void* key, const void*, void* context) 986 { 987 CFStringRef url = static_cast<CFStringRef>(key); 988 PageGroup* group = static_cast<PageGroup*>(context); 989 990 CFIndex length = CFStringGetLength(url); 991 const UChar* characters = reinterpret_cast<const UChar*>(CFStringGetCharactersPtr(url)); 992 if (characters) 993 group->addVisitedLink(characters, length); 994 else { 995 Vector<UChar, 512> buffer(length); 996 CFStringGetCharacters(url, CFRangeMake(0, length), reinterpret_cast<UniChar*>(buffer.data())); 997 group->addVisitedLink(buffer.data(), length); 998 } 999 } 1000 1001 void WebHistory::addVisitedLinksToPageGroup(PageGroup& group) 1002 { 1003 CFDictionaryApplyFunction(m_entriesByURL.get(), addVisitedLinkToPageGroup, &group); 1004 } 1005 1006 RetainPtr<CFDataRef> WebHistory::data() const 1007 { 1008 if (m_entriesByDate.isEmpty()) 1009 return 0; 1010 1011 WebHistoryWriter writer(m_entriesByDate); 1012 writer.writePropertyList(); 1013 return writer.releaseData(); 1014 } 1015