/*
* Copyright (C) 2006, 2007 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebKitDLL.h"
#include "WebHistory.h"
#include "CFDictionaryPropertyBag.h"
#include "MemoryStream.h"
#include "WebKit.h"
#include "MarshallingHelpers.h"
#include "WebHistoryItem.h"
#include "WebKit.h"
#include "WebNotificationCenter.h"
#include "WebPreferences.h"
#include <CoreFoundation/CoreFoundation.h>
#include <WebCore/HistoryItem.h>
#include <WebCore/HistoryPropertyList.h>
#include <WebCore/KURL.h>
#include <WebCore/PageGroup.h>
#include <WebCore/SharedBuffer.h>
#include <functional>
#include <wtf/StdLibExtras.h>
#include <wtf/Vector.h>
using namespace WebCore;
using namespace std;
CFStringRef DatesArrayKey = CFSTR("WebHistoryDates");
CFStringRef FileVersionKey = CFSTR("WebHistoryFileVersion");
#define currentFileVersion 1
class WebHistoryWriter : public HistoryPropertyListWriter {
public:
WebHistoryWriter(const WebHistory::DateToEntriesMap&);
private:
virtual void writeHistoryItems(BinaryPropertyListObjectStream&);
const WebHistory::DateToEntriesMap& m_entriesByDate;
Vector<WebHistory::DateKey> m_dateKeys;
};
WebHistoryWriter::WebHistoryWriter(const WebHistory::DateToEntriesMap& entriesByDate)
: m_entriesByDate(entriesByDate)
{
copyKeysToVector(m_entriesByDate, m_dateKeys);
sort(m_dateKeys.begin(), m_dateKeys.end());
}
void WebHistoryWriter::writeHistoryItems(BinaryPropertyListObjectStream& stream)
{
for (int dateIndex = m_dateKeys.size() - 1; dateIndex >= 0; --dateIndex) {
// get the entries for that date
CFArrayRef entries = m_entriesByDate.get(m_dateKeys[dateIndex]).get();
CFIndex entriesCount = CFArrayGetCount(entries);
for (CFIndex j = entriesCount - 1; j >= 0; --j) {
IWebHistoryItem* item = (IWebHistoryItem*) CFArrayGetValueAtIndex(entries, j);
COMPtr<WebHistoryItem> webItem(Query, item);
if (!webItem)
continue;
writeHistoryItem(stream, webItem->historyItem());
}
}
}
static bool areEqualOrClose(double d1, double d2)
{
double diff = d1-d2;
return (diff < .000001 && diff > -.000001);
}
static COMPtr<CFDictionaryPropertyBag> createUserInfoFromArray(BSTR notificationStr, CFArrayRef arrayItem)
{
RetainPtr<CFMutableDictionaryRef> dictionary(AdoptCF,
CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
RetainPtr<CFStringRef> key(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(notificationStr));
CFDictionaryAddValue(dictionary.get(), key.get(), arrayItem);
COMPtr<CFDictionaryPropertyBag> result = CFDictionaryPropertyBag::createInstance();
result->setDictionary(dictionary.get());
return result;
}
static COMPtr<CFDictionaryPropertyBag> createUserInfoFromHistoryItem(BSTR notificationStr, IWebHistoryItem* item)
{
// reference counting of item added to the array is managed by the CFArray value callbacks
RetainPtr<CFArrayRef> itemList(AdoptCF, CFArrayCreate(0, (const void**) &item, 1, &MarshallingHelpers::kIUnknownArrayCallBacks));
COMPtr<CFDictionaryPropertyBag> info = createUserInfoFromArray(notificationStr, itemList.get());
return info;
}
// WebHistory -----------------------------------------------------------------
WebHistory::WebHistory()
: m_refCount(0)
, m_preferences(0)
{
gClassCount++;
gClassNameCount.add("WebHistory");
m_entriesByURL.adoptCF(CFDictionaryCreateMutable(0, 0, &kCFTypeDictionaryKeyCallBacks, &MarshallingHelpers::kIUnknownDictionaryValueCallBacks));
m_preferences = WebPreferences::sharedStandardPreferences();
}
WebHistory::~WebHistory()
{
gClassCount--;
gClassNameCount.remove("WebHistory");
}
WebHistory* WebHistory::createInstance()
{
WebHistory* instance = new WebHistory();
instance->AddRef();
return instance;
}
HRESULT WebHistory::postNotification(NotificationType notifyType, IPropertyBag* userInfo /*=0*/)
{
IWebNotificationCenter* nc = WebNotificationCenter::defaultCenterInternal();
HRESULT hr = nc->postNotificationName(getNotificationString(notifyType), static_cast<IWebHistory*>(this), userInfo);
if (FAILED(hr))
return hr;
return S_OK;
}
BSTR WebHistory::getNotificationString(NotificationType notifyType)
{
static BSTR keys[6] = {0};
if (!keys[0]) {
keys[0] = SysAllocString(WebHistoryItemsAddedNotification);
keys[1] = SysAllocString(WebHistoryItemsRemovedNotification);
keys[2] = SysAllocString(WebHistoryAllItemsRemovedNotification);
keys[3] = SysAllocString(WebHistoryLoadedNotification);
keys[4] = SysAllocString(WebHistoryItemsDiscardedWhileLoadingNotification);
keys[5] = SysAllocString(WebHistorySavedNotification);
}
return keys[notifyType];
}
// IUnknown -------------------------------------------------------------------
HRESULT STDMETHODCALLTYPE WebHistory::QueryInterface(REFIID riid, void** ppvObject)
{
*ppvObject = 0;
if (IsEqualGUID(riid, CLSID_WebHistory))
*ppvObject = this;
else if (IsEqualGUID(riid, IID_IUnknown))
*ppvObject = static_cast<IWebHistory*>(this);
else if (IsEqualGUID(riid, IID_IWebHistory))
*ppvObject = static_cast<IWebHistory*>(this);
else if (IsEqualGUID(riid, IID_IWebHistoryPrivate))
*ppvObject = static_cast<IWebHistoryPrivate*>(this);
else
return E_NOINTERFACE;
AddRef();
return S_OK;
}
ULONG STDMETHODCALLTYPE WebHistory::AddRef(void)
{
return ++m_refCount;
}
ULONG STDMETHODCALLTYPE WebHistory::Release(void)
{
ULONG newRef = --m_refCount;
if (!newRef)
delete(this);
return newRef;
}
// IWebHistory ----------------------------------------------------------------
static inline COMPtr<WebHistory>& sharedHistoryStorage()
{
DEFINE_STATIC_LOCAL(COMPtr<WebHistory>, sharedHistory, ());
return sharedHistory;
}
WebHistory* WebHistory::sharedHistory()
{
return sharedHistoryStorage().get();
}
HRESULT STDMETHODCALLTYPE WebHistory::optionalSharedHistory(
/* [retval][out] */ IWebHistory** history)
{
*history = sharedHistory();
if (*history)
(*history)->AddRef();
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::setOptionalSharedHistory(
/* [in] */ IWebHistory* history)
{
if (sharedHistoryStorage() == history)
return S_OK;
sharedHistoryStorage().query(history);
PageGroup::setShouldTrackVisitedLinks(sharedHistoryStorage());
PageGroup::removeAllVisitedLinks();
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::loadFromURL(
/* [in] */ BSTR url,
/* [out] */ IWebError** error,
/* [retval][out] */ BOOL* succeeded)
{
HRESULT hr = S_OK;
RetainPtr<CFMutableArrayRef> discardedItems(AdoptCF,
CFArrayCreateMutable(0, 0, &MarshallingHelpers::kIUnknownArrayCallBacks));
RetainPtr<CFURLRef> urlRef(AdoptCF, MarshallingHelpers::BSTRToCFURLRef(url));
hr = loadHistoryGutsFromURL(urlRef.get(), discardedItems.get(), error);
if (FAILED(hr))
goto exit;
hr = postNotification(kWebHistoryLoadedNotification);
if (FAILED(hr))
goto exit;
if (CFArrayGetCount(discardedItems.get()) > 0) {
COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromArray(getNotificationString(kWebHistoryItemsDiscardedWhileLoadingNotification), discardedItems.get());
hr = postNotification(kWebHistoryItemsDiscardedWhileLoadingNotification, userInfo.get());
if (FAILED(hr))
goto exit;
}
exit:
if (succeeded)
*succeeded = SUCCEEDED(hr);
return hr;
}
static CFDictionaryRef createHistoryListFromStream(CFReadStreamRef stream, CFPropertyListFormat format)
{
return (CFDictionaryRef)CFPropertyListCreateFromStream(0, stream, 0, kCFPropertyListImmutable, &format, 0);
}
HRESULT WebHistory::loadHistoryGutsFromURL(CFURLRef url, CFMutableArrayRef discardedItems, IWebError** /*error*/) //FIXME
{
CFPropertyListFormat format = kCFPropertyListBinaryFormat_v1_0 | kCFPropertyListXMLFormat_v1_0;
HRESULT hr = S_OK;
int numberOfItemsLoaded = 0;
RetainPtr<CFReadStreamRef> stream(AdoptCF, CFReadStreamCreateWithFile(0, url));
if (!stream)
return E_FAIL;
if (!CFReadStreamOpen(stream.get()))
return E_FAIL;
RetainPtr<CFDictionaryRef> historyList(AdoptCF, createHistoryListFromStream(stream.get(), format));
CFReadStreamClose(stream.get());
if (!historyList)
return E_FAIL;
CFNumberRef fileVersionObject = (CFNumberRef)CFDictionaryGetValue(historyList.get(), FileVersionKey);
int fileVersion;
if (!CFNumberGetValue(fileVersionObject, kCFNumberIntType, &fileVersion))
return E_FAIL;
if (fileVersion > currentFileVersion)
return E_FAIL;
CFArrayRef datesArray = (CFArrayRef)CFDictionaryGetValue(historyList.get(), DatesArrayKey);
int itemCountLimit;
hr = historyItemLimit(&itemCountLimit);
if (FAILED(hr))
return hr;
CFAbsoluteTime limitDate;
hr = ageLimitDate(&limitDate);
if (FAILED(hr))
return hr;
bool ageLimitPassed = false;
bool itemLimitPassed = false;
CFIndex itemCount = CFArrayGetCount(datesArray);
for (CFIndex i = 0; i < itemCount; ++i) {
CFDictionaryRef itemAsDictionary = (CFDictionaryRef)CFArrayGetValueAtIndex(datesArray, i);
COMPtr<WebHistoryItem> item(AdoptCOM, WebHistoryItem::createInstance());
hr = item->initFromDictionaryRepresentation((void*)itemAsDictionary);
if (FAILED(hr))
return hr;
// item without URL is useless; data on disk must have been bad; ignore
BOOL hasURL;
hr = item->hasURLString(&hasURL);
if (FAILED(hr))
return hr;
if (hasURL) {
// Test against date limit. Since the items are ordered newest to oldest, we can stop comparing
// once we've found the first item that's too old.
if (!ageLimitPassed) {
DATE lastVisitedTime;
hr = item->lastVisitedTimeInterval(&lastVisitedTime);
if (FAILED(hr))
return hr;
if (timeToDate(MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedTime)) <= limitDate)
ageLimitPassed = true;
}
if (ageLimitPassed || itemLimitPassed)
CFArrayAppendValue(discardedItems, item.get());
else {
bool added;
addItem(item.get(), true, &added); // ref is added inside addItem
if (added)
++numberOfItemsLoaded;
if (numberOfItemsLoaded == itemCountLimit)
itemLimitPassed = true;
}
}
}
return hr;
}
HRESULT STDMETHODCALLTYPE WebHistory::saveToURL(
/* [in] */ BSTR url,
/* [out] */ IWebError** error,
/* [retval][out] */ BOOL* succeeded)
{
HRESULT hr = S_OK;
RetainPtr<CFURLRef> urlRef(AdoptCF, MarshallingHelpers::BSTRToCFURLRef(url));
hr = saveHistoryGuts(urlRef.get(), error);
if (succeeded)
*succeeded = SUCCEEDED(hr);
if (SUCCEEDED(hr))
hr = postNotification(kWebHistorySavedNotification);
return hr;
}
HRESULT WebHistory::saveHistoryGuts(CFURLRef url, IWebError** error)
{
HRESULT hr = S_OK;
// FIXME: Correctly report error when new API is ready.
if (error)
*error = 0;
RetainPtr<CFDataRef> data = this->data();
RetainPtr<CFWriteStreamRef> stream(AdoptCF, CFWriteStreamCreateWithFile(kCFAllocatorDefault, url));
if (!stream)
return E_FAIL;
if (!CFWriteStreamOpen(stream.get()))
return E_FAIL;
const UInt8* dataPtr = CFDataGetBytePtr(data.get());
CFIndex length = CFDataGetLength(data.get());
while (length) {
CFIndex bytesWritten = CFWriteStreamWrite(stream.get(), dataPtr, length);
if (bytesWritten <= 0) {
hr = E_FAIL;
break;
}
dataPtr += bytesWritten;
length -= bytesWritten;
}
CFWriteStreamClose(stream.get());
return hr;
}
HRESULT STDMETHODCALLTYPE WebHistory::addItems(
/* [in] */ int itemCount,
/* [in] */ IWebHistoryItem** items)
{
// There is no guarantee that the incoming entries are in any particular
// order, but if this is called with a set of entries that were created by
// iterating through the results of orderedLastVisitedDays and orderedItemsLastVisitedOnDay
// then they will be ordered chronologically from newest to oldest. We can make adding them
// faster (fewer compares) by inserting them from oldest to newest.
HRESULT hr;
for (int i = itemCount - 1; i >= 0; --i) {
hr = addItem(items[i], false, 0);
if (FAILED(hr))
return hr;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::removeItems(
/* [in] */ int itemCount,
/* [in] */ IWebHistoryItem** items)
{
HRESULT hr;
for (int i = 0; i < itemCount; ++i) {
hr = removeItem(items[i]);
if (FAILED(hr))
return hr;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::removeAllItems( void)
{
m_entriesByDate.clear();
m_orderedLastVisitedDays.clear();
CFIndex itemCount = CFDictionaryGetCount(m_entriesByURL.get());
Vector<IWebHistoryItem*> itemsVector(itemCount);
CFDictionaryGetKeysAndValues(m_entriesByURL.get(), 0, (const void**)itemsVector.data());
RetainPtr<CFArrayRef> allItems(AdoptCF, CFArrayCreate(kCFAllocatorDefault, (const void**)itemsVector.data(), itemCount, &MarshallingHelpers::kIUnknownArrayCallBacks));
CFDictionaryRemoveAllValues(m_entriesByURL.get());
PageGroup::removeAllVisitedLinks();
COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromArray(getNotificationString(kWebHistoryAllItemsRemovedNotification), allItems.get());
return postNotification(kWebHistoryAllItemsRemovedNotification, userInfo.get());
}
HRESULT STDMETHODCALLTYPE WebHistory::orderedLastVisitedDays(
/* [out][in] */ int* count,
/* [in] */ DATE* calendarDates)
{
int dateCount = m_entriesByDate.size();
if (!calendarDates) {
*count = dateCount;
return S_OK;
}
if (*count < dateCount) {
*count = dateCount;
return E_FAIL;
}
*count = dateCount;
if (!m_orderedLastVisitedDays) {
m_orderedLastVisitedDays = adoptArrayPtr(new DATE[dateCount]);
DateToEntriesMap::const_iterator::Keys end = m_entriesByDate.end().keys();
int i = 0;
for (DateToEntriesMap::const_iterator::Keys it = m_entriesByDate.begin().keys(); it != end; ++it, ++i)
m_orderedLastVisitedDays[i] = MarshallingHelpers::CFAbsoluteTimeToDATE(*it);
// Use std::greater to sort the days in descending order (i.e., most-recent first).
sort(m_orderedLastVisitedDays.get(), m_orderedLastVisitedDays.get() + dateCount, greater<DATE>());
}
memcpy(calendarDates, m_orderedLastVisitedDays.get(), dateCount * sizeof(DATE));
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::orderedItemsLastVisitedOnDay(
/* [out][in] */ int* count,
/* [in] */ IWebHistoryItem** items,
/* [in] */ DATE calendarDate)
{
DateKey dateKey;
if (!findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(calendarDate))) {
*count = 0;
return 0;
}
CFArrayRef entries = m_entriesByDate.get(dateKey).get();
if (!entries) {
*count = 0;
return 0;
}
int newCount = CFArrayGetCount(entries);
if (!items) {
*count = newCount;
return S_OK;
}
if (*count < newCount) {
*count = newCount;
return E_FAIL;
}
*count = newCount;
for (int i = 0; i < newCount; i++) {
IWebHistoryItem* item = (IWebHistoryItem*)CFArrayGetValueAtIndex(entries, i);
item->AddRef();
items[i] = item;
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::allItems(
/* [out][in] */ int* count,
/* [out][retval] */ IWebHistoryItem** items)
{
int entriesByURLCount = CFDictionaryGetCount(m_entriesByURL.get());
if (!items) {
*count = entriesByURLCount;
return S_OK;
}
if (*count < entriesByURLCount) {
*count = entriesByURLCount;
return E_FAIL;
}
*count = entriesByURLCount;
CFDictionaryGetKeysAndValues(m_entriesByURL.get(), 0, (const void**)items);
for (int i = 0; i < entriesByURLCount; i++)
items[i]->AddRef();
return S_OK;
}
HRESULT WebHistory::data(IStream** stream)
{
if (!stream)
return E_POINTER;
*stream = 0;
RetainPtr<CFDataRef> historyData = data();
if (!historyData)
return S_OK;
COMPtr<MemoryStream> result = MemoryStream::createInstance(SharedBuffer::wrapCFData(historyData.get()));
return result.copyRefTo(stream);
}
HRESULT WebHistory::setVisitedLinkTrackingEnabled(BOOL visitedLinkTrackingEnabled)
{
PageGroup::setShouldTrackVisitedLinks(visitedLinkTrackingEnabled);
return S_OK;
}
HRESULT WebHistory::removeAllVisitedLinks()
{
PageGroup::removeAllVisitedLinks();
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::setHistoryItemLimit(
/* [in] */ int limit)
{
if (!m_preferences)
return E_FAIL;
return m_preferences->setHistoryItemLimit(limit);
}
HRESULT STDMETHODCALLTYPE WebHistory::historyItemLimit(
/* [retval][out] */ int* limit)
{
if (!m_preferences)
return E_FAIL;
return m_preferences->historyItemLimit(limit);
}
HRESULT STDMETHODCALLTYPE WebHistory::setHistoryAgeInDaysLimit(
/* [in] */ int limit)
{
if (!m_preferences)
return E_FAIL;
return m_preferences->setHistoryAgeInDaysLimit(limit);
}
HRESULT STDMETHODCALLTYPE WebHistory::historyAgeInDaysLimit(
/* [retval][out] */ int* limit)
{
if (!m_preferences)
return E_FAIL;
return m_preferences->historyAgeInDaysLimit(limit);
}
HRESULT WebHistory::removeItem(IWebHistoryItem* entry)
{
HRESULT hr = S_OK;
BSTR urlBStr = 0;
hr = entry->URLString(&urlBStr);
if (FAILED(hr))
return hr;
RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(urlBStr));
SysFreeString(urlBStr);
// If this exact object isn't stored, then make no change.
// FIXME: Is this the right behavior if this entry isn't present, but another entry for the same URL is?
// Maybe need to change the API to make something like removeEntryForURLString public instead.
IWebHistoryItem *matchingEntry = (IWebHistoryItem*)CFDictionaryGetValue(m_entriesByURL.get(), urlString.get());
if (matchingEntry != entry)
return E_FAIL;
hr = removeItemForURLString(urlString.get());
if (FAILED(hr))
return hr;
COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem(
getNotificationString(kWebHistoryItemsRemovedNotification), entry);
hr = postNotification(kWebHistoryItemsRemovedNotification, userInfo.get());
return hr;
}
HRESULT WebHistory::addItem(IWebHistoryItem* entry, bool discardDuplicate, bool* added)
{
HRESULT hr = S_OK;
if (!entry)
return E_FAIL;
BSTR urlBStr = 0;
hr = entry->URLString(&urlBStr);
if (FAILED(hr))
return hr;
RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(urlBStr));
SysFreeString(urlBStr);
COMPtr<IWebHistoryItem> oldEntry((IWebHistoryItem*) CFDictionaryGetValue(
m_entriesByURL.get(), urlString.get()));
if (oldEntry) {
if (discardDuplicate) {
if (added)
*added = false;
return S_OK;
}
removeItemForURLString(urlString.get());
// If we already have an item with this URL, we need to merge info that drives the
// URL autocomplete heuristics from that item into the new one.
IWebHistoryItemPrivate* entryPriv;
hr = entry->QueryInterface(IID_IWebHistoryItemPrivate, (void**)&entryPriv);
if (SUCCEEDED(hr)) {
entryPriv->mergeAutoCompleteHints(oldEntry.get());
entryPriv->Release();
}
}
hr = addItemToDateCaches(entry);
if (FAILED(hr))
return hr;
CFDictionarySetValue(m_entriesByURL.get(), urlString.get(), entry);
COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem(
getNotificationString(kWebHistoryItemsAddedNotification), entry);
hr = postNotification(kWebHistoryItemsAddedNotification, userInfo.get());
if (added)
*added = true;
return hr;
}
void WebHistory::visitedURL(const KURL& url, const String& title, const String& httpMethod, bool wasFailure, bool increaseVisitCount)
{
RetainPtr<CFStringRef> urlString(AdoptCF, url.string().createCFString());
IWebHistoryItem* entry = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString.get());
if (entry) {
COMPtr<IWebHistoryItemPrivate> entryPrivate(Query, entry);
if (!entryPrivate)
return;
// Remove the item from date caches before changing its last visited date. Otherwise we might get duplicate entries
// as seen in <rdar://problem/6570573>.
removeItemFromDateCaches(entry);
entryPrivate->visitedWithTitle(BString(title), increaseVisitCount);
} else {
COMPtr<WebHistoryItem> item(AdoptCOM, WebHistoryItem::createInstance());
if (!item)
return;
entry = item.get();
SYSTEMTIME currentTime;
GetSystemTime(¤tTime);
DATE lastVisited;
if (!SystemTimeToVariantTime(¤tTime, &lastVisited))
return;
if (FAILED(entry->initWithURLString(BString(url.string()), BString(title), lastVisited)))
return;
item->recordInitialVisit();
CFDictionarySetValue(m_entriesByURL.get(), urlString.get(), entry);
}
addItemToDateCaches(entry);
COMPtr<IWebHistoryItemPrivate> entryPrivate(Query, entry);
if (!entryPrivate)
return;
entryPrivate->setLastVisitWasFailure(wasFailure);
if (!httpMethod.isEmpty())
entryPrivate->setLastVisitWasHTTPNonGet(!equalIgnoringCase(httpMethod, "GET") && url.protocolInHTTPFamily());
COMPtr<WebHistoryItem> item(Query, entry);
item->historyItem()->setRedirectURLs(0);
COMPtr<CFDictionaryPropertyBag> userInfo = createUserInfoFromHistoryItem(
getNotificationString(kWebHistoryItemsAddedNotification), entry);
postNotification(kWebHistoryItemsAddedNotification, userInfo.get());
}
HRESULT WebHistory::itemForURLString(
/* [in] */ CFStringRef urlString,
/* [retval][out] */ IWebHistoryItem** item) const
{
if (!item)
return E_FAIL;
*item = 0;
IWebHistoryItem* foundItem = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString);
if (!foundItem)
return E_FAIL;
foundItem->AddRef();
*item = foundItem;
return S_OK;
}
HRESULT STDMETHODCALLTYPE WebHistory::itemForURL(
/* [in] */ BSTR url,
/* [retval][out] */ IWebHistoryItem** item)
{
RetainPtr<CFStringRef> urlString(AdoptCF, MarshallingHelpers::BSTRToCFStringRef(url));
return itemForURLString(urlString.get(), item);
}
HRESULT WebHistory::removeItemForURLString(CFStringRef urlString)
{
IWebHistoryItem* entry = (IWebHistoryItem*) CFDictionaryGetValue(m_entriesByURL.get(), urlString);
if (!entry)
return E_FAIL;
HRESULT hr = removeItemFromDateCaches(entry);
CFDictionaryRemoveValue(m_entriesByURL.get(), urlString);
if (!CFDictionaryGetCount(m_entriesByURL.get()))
PageGroup::removeAllVisitedLinks();
return hr;
}
COMPtr<IWebHistoryItem> WebHistory::itemForURLString(const String& urlString) const
{
RetainPtr<CFStringRef> urlCFString(AdoptCF, urlString.createCFString());
if (!urlCFString)
return 0;
COMPtr<IWebHistoryItem> item;
if (FAILED(itemForURLString(urlCFString.get(), &item)))
return 0;
return item;
}
HRESULT WebHistory::addItemToDateCaches(IWebHistoryItem* entry)
{
HRESULT hr = S_OK;
DATE lastVisitedCOMTime;
entry->lastVisitedTimeInterval(&lastVisitedCOMTime);
DateKey dateKey;
if (findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedCOMTime))) {
// other entries already exist for this date
hr = insertItem(entry, dateKey);
} else {
ASSERT(!m_entriesByDate.contains(dateKey));
// no other entries exist for this date
RetainPtr<CFMutableArrayRef> entryArray(AdoptCF,
CFArrayCreateMutable(0, 0, &MarshallingHelpers::kIUnknownArrayCallBacks));
CFArrayAppendValue(entryArray.get(), entry);
m_entriesByDate.set(dateKey, entryArray);
// Clear m_orderedLastVisitedDays so it will be regenerated when next requested.
m_orderedLastVisitedDays.clear();
}
return hr;
}
HRESULT WebHistory::removeItemFromDateCaches(IWebHistoryItem* entry)
{
HRESULT hr = S_OK;
DATE lastVisitedCOMTime;
entry->lastVisitedTimeInterval(&lastVisitedCOMTime);
DateKey dateKey;
if (!findKey(&dateKey, MarshallingHelpers::DATEToCFAbsoluteTime(lastVisitedCOMTime)))
return E_FAIL;
DateToEntriesMap::iterator found = m_entriesByDate.find(dateKey);
ASSERT(found != m_entriesByDate.end());
CFMutableArrayRef entriesForDate = found->second.get();
CFIndex count = CFArrayGetCount(entriesForDate);
for (int i = count - 1; i >= 0; --i) {
if ((IWebHistoryItem*)CFArrayGetValueAtIndex(entriesForDate, i) == entry)
CFArrayRemoveValueAtIndex(entriesForDate, i);
}
// remove this date entirely if there are no other entries on it
if (CFArrayGetCount(entriesForDate) == 0) {
m_entriesByDate.remove(found);
// Clear m_orderedLastVisitedDays so it will be regenerated when next requested.
m_orderedLastVisitedDays.clear();
}
return hr;
}
static void getDayBoundaries(CFAbsoluteTime day, CFAbsoluteTime& beginningOfDay, CFAbsoluteTime& beginningOfNextDay)
{
RetainPtr<CFTimeZoneRef> timeZone(AdoptCF, CFTimeZoneCopyDefault());
CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(day, timeZone.get());
date.hour = 0;
date.minute = 0;
date.second = 0;
beginningOfDay = CFGregorianDateGetAbsoluteTime(date, timeZone.get());
date.day += 1;
beginningOfNextDay = CFGregorianDateGetAbsoluteTime(date, timeZone.get());
}
static inline CFAbsoluteTime beginningOfDay(CFAbsoluteTime date)
{
static CFAbsoluteTime cachedBeginningOfDay = numeric_limits<CFAbsoluteTime>::quiet_NaN();
static CFAbsoluteTime cachedBeginningOfNextDay;
if (!(date >= cachedBeginningOfDay && date < cachedBeginningOfNextDay))
getDayBoundaries(date, cachedBeginningOfDay, cachedBeginningOfNextDay);
return cachedBeginningOfDay;
}
static inline WebHistory::DateKey dateKey(CFAbsoluteTime date)
{
// Converting from double (CFAbsoluteTime) to int64_t (WebHistoryDateKey) is
// safe here because all sensible dates are in the range -2**48 .. 2**47 which
// safely fits in an int64_t.
return beginningOfDay(date);
}
// Returns whether the day is already in the list of days,
// and fills in *key with the found or proposed key.
bool WebHistory::findKey(DateKey* key, CFAbsoluteTime forDay)
{
ASSERT_ARG(key, key);
*key = dateKey(forDay);
return m_entriesByDate.contains(*key);
}
HRESULT WebHistory::insertItem(IWebHistoryItem* entry, DateKey dateKey)
{
ASSERT_ARG(entry, entry);
ASSERT_ARG(dateKey, m_entriesByDate.contains(dateKey));
HRESULT hr = S_OK;
if (!entry)
return E_FAIL;
DATE entryTime;
entry->lastVisitedTimeInterval(&entryTime);
CFMutableArrayRef entriesForDate = m_entriesByDate.get(dateKey).get();
unsigned count = CFArrayGetCount(entriesForDate);
// The entries for each day are stored in a sorted array with the most recent entry first
// Check for the common cases of the entry being newer than all existing entries or the first entry of the day
bool isNewerThanAllEntries = false;
if (count) {
IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, 0)));
DATE itemTime;
isNewerThanAllEntries = SUCCEEDED(item->lastVisitedTimeInterval(&itemTime)) && itemTime < entryTime;
}
if (!count || isNewerThanAllEntries) {
CFArrayInsertValueAtIndex(entriesForDate, 0, entry);
return S_OK;
}
// .. or older than all existing entries
bool isOlderThanAllEntries = false;
if (count > 0) {
IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, count - 1)));
DATE itemTime;
isOlderThanAllEntries = SUCCEEDED(item->lastVisitedTimeInterval(&itemTime)) && itemTime >= entryTime;
}
if (isOlderThanAllEntries) {
CFArrayInsertValueAtIndex(entriesForDate, count, entry);
return S_OK;
}
unsigned low = 0;
unsigned high = count;
while (low < high) {
unsigned mid = low + (high - low) / 2;
IWebHistoryItem* item = const_cast<IWebHistoryItem*>(static_cast<const IWebHistoryItem*>(CFArrayGetValueAtIndex(entriesForDate, mid)));
DATE itemTime;
if (FAILED(item->lastVisitedTimeInterval(&itemTime)))
return E_FAIL;
if (itemTime >= entryTime)
low = mid + 1;
else
high = mid;
}
// low is now the index of the first entry that is older than entryDate
CFArrayInsertValueAtIndex(entriesForDate, low, entry);
return S_OK;
}
CFAbsoluteTime WebHistory::timeToDate(CFAbsoluteTime time)
{
// can't just divide/round since the day boundaries depend on our current time zone
const double secondsPerDay = 60 * 60 * 24;
CFTimeZoneRef timeZone = CFTimeZoneCopySystem();
CFGregorianDate date = CFAbsoluteTimeGetGregorianDate(time, timeZone);
date.hour = date.minute = 0;
date.second = 0.0;
CFAbsoluteTime timeInDays = CFGregorianDateGetAbsoluteTime(date, timeZone);
if (areEqualOrClose(time - timeInDays, secondsPerDay))
timeInDays += secondsPerDay;
return timeInDays;
}
// Return a date that marks the age limit for history entries saved to or
// loaded from disk. Any entry older than this item should be rejected.
HRESULT WebHistory::ageLimitDate(CFAbsoluteTime* time)
{
// get the current date as a CFAbsoluteTime
CFAbsoluteTime currentDate = timeToDate(CFAbsoluteTimeGetCurrent());
CFGregorianUnits ageLimit = {0};
int historyLimitDays;
HRESULT hr = historyAgeInDaysLimit(&historyLimitDays);
if (FAILED(hr))
return hr;
ageLimit.days = -historyLimitDays;
*time = CFAbsoluteTimeAddGregorianUnits(currentDate, CFTimeZoneCopySystem(), ageLimit);
return S_OK;
}
static void addVisitedLinkToPageGroup(const void* key, const void*, void* context)
{
CFStringRef url = static_cast<CFStringRef>(key);
PageGroup* group = static_cast<PageGroup*>(context);
CFIndex length = CFStringGetLength(url);
const UChar* characters = reinterpret_cast<const UChar*>(CFStringGetCharactersPtr(url));
if (characters)
group->addVisitedLink(characters, length);
else {
Vector<UChar, 512> buffer(length);
CFStringGetCharacters(url, CFRangeMake(0, length), reinterpret_cast<UniChar*>(buffer.data()));
group->addVisitedLink(buffer.data(), length);
}
}
void WebHistory::addVisitedLinksToPageGroup(PageGroup& group)
{
CFDictionaryApplyFunction(m_entriesByURL.get(), addVisitedLinkToPageGroup, &group);
}
RetainPtr<CFDataRef> WebHistory::data() const
{
if (m_entriesByDate.isEmpty())
return 0;
WebHistoryWriter writer(m_entriesByDate);
writer.writePropertyList();
return writer.releaseData();
}