diff -r 000000000000 -r 6474c204b198 toolkit/components/places/nsFaviconService.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/components/places/nsFaviconService.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,613 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is the favicon service, which stores favicons for web pages with your + * history as you browse. It is also used to save the favicons for bookmarks. + * + * DANGER: The history query system makes assumptions about the favicon storage + * so that icons can be quickly generated for history/bookmark result sets. If + * you change the database layout at all, you will have to update both services. + */ + +#include "nsFaviconService.h" + +#include "nsNavHistory.h" +#include "nsPlacesMacros.h" +#include "Helpers.h" +#include "AsyncFaviconHelpers.h" + +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "plbase64.h" +#include "nsIClassInfoImpl.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Preferences.h" + +// For large favicons optimization. +#include "imgITools.h" +#include "imgIContainer.h" + +// Default value for mOptimizedIconDimension +#define OPTIMIZED_FAVICON_DIMENSION 16 + +#define MAX_FAVICON_CACHE_SIZE 256 +#define FAVICON_CACHE_REDUCE_COUNT 64 + +#define MAX_UNASSOCIATED_FAVICONS 64 + +// When replaceFaviconData is called, we store the icons in an in-memory cache +// instead of in storage. Icons in the cache are expired according to this +// interval. +#define UNASSOCIATED_ICON_EXPIRY_INTERVAL 60000 + +// The MIME type of the default favicon and favicons created by +// OptimizeFaviconImage. +#define DEFAULT_MIME_TYPE "image/png" + +using namespace mozilla; +using namespace mozilla::places; + +/** + * Used to notify a topic to system observers on async execute completion. + * Will throw on error. + */ +class ExpireFaviconsStatementCallbackNotifier : public AsyncStatementCallback +{ +public: + ExpireFaviconsStatementCallbackNotifier(); + NS_IMETHOD HandleCompletion(uint16_t aReason); +}; + + +PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsFaviconService, gFaviconService) + +NS_IMPL_CLASSINFO(nsFaviconService, nullptr, 0, NS_FAVICONSERVICE_CID) +NS_IMPL_ISUPPORTS_CI( + nsFaviconService +, nsIFaviconService +, mozIAsyncFavicons +, nsITimerCallback +) + +nsFaviconService::nsFaviconService() + : mOptimizedIconDimension(OPTIMIZED_FAVICON_DIMENSION) + , mFailedFaviconSerial(0) + , mFailedFavicons(MAX_FAVICON_CACHE_SIZE) + , mUnassociatedIcons(MAX_UNASSOCIATED_FAVICONS) +{ + NS_ASSERTION(!gFaviconService, + "Attempting to create two instances of the service!"); + gFaviconService = this; +} + + +nsFaviconService::~nsFaviconService() +{ + NS_ASSERTION(gFaviconService == this, + "Deleting a non-singleton instance of the service"); + if (gFaviconService == this) + gFaviconService = nullptr; +} + + +nsresult +nsFaviconService::Init() +{ + mDB = Database::GetDatabase(); + NS_ENSURE_STATE(mDB); + + mOptimizedIconDimension = Preferences::GetInt( + "places.favicons.optimizeToDimension", OPTIMIZED_FAVICON_DIMENSION + ); + + mExpireUnassociatedIconsTimer = do_CreateInstance("@mozilla.org/timer;1"); + NS_ENSURE_STATE(mExpireUnassociatedIconsTimer); + + return NS_OK; +} + +NS_IMETHODIMP +nsFaviconService::ExpireAllFavicons() +{ + nsCOMPtr unlinkIconsStmt = mDB->GetAsyncStatement( + "UPDATE moz_places " + "SET favicon_id = NULL " + "WHERE favicon_id NOT NULL" + ); + NS_ENSURE_STATE(unlinkIconsStmt); + nsCOMPtr removeIconsStmt = mDB->GetAsyncStatement( + "DELETE FROM moz_favicons WHERE id NOT IN (" + "SELECT favicon_id FROM moz_places WHERE favicon_id NOT NULL " + ")" + ); + NS_ENSURE_STATE(removeIconsStmt); + + mozIStorageBaseStatement* stmts[] = { + unlinkIconsStmt.get() + , removeIconsStmt.get() + }; + nsCOMPtr ps; + nsRefPtr callback = + new ExpireFaviconsStatementCallbackNotifier(); + nsresult rv = mDB->MainConn()->ExecuteAsync( + stmts, ArrayLength(stmts), callback, getter_AddRefs(ps) + ); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsITimerCallback + +static PLDHashOperator +ExpireNonrecentUnassociatedIconsEnumerator( + UnassociatedIconHashKey* aIconKey, + void* aNow) +{ + PRTime now = *(reinterpret_cast(aNow)); + if (now - aIconKey->created >= UNASSOCIATED_ICON_EXPIRY_INTERVAL) { + return PL_DHASH_REMOVE; + } + return PL_DHASH_NEXT; +} + +NS_IMETHODIMP +nsFaviconService::Notify(nsITimer* timer) +{ + if (timer != mExpireUnassociatedIconsTimer.get()) { + return NS_ERROR_INVALID_ARG; + } + + PRTime now = PR_Now(); + mUnassociatedIcons.EnumerateEntries( + ExpireNonrecentUnassociatedIconsEnumerator, &now); + // Re-init the expiry timer if the cache isn't empty. + if (mUnassociatedIcons.Count() > 0) { + mExpireUnassociatedIconsTimer->InitWithCallback( + this, UNASSOCIATED_ICON_EXPIRY_INTERVAL, nsITimer::TYPE_ONE_SHOT); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIFaviconService + +NS_IMETHODIMP +nsFaviconService::GetDefaultFavicon(nsIURI** _retval) +{ + NS_ENSURE_ARG_POINTER(_retval); + + // not found, use default + if (!mDefaultIcon) { + nsresult rv = NS_NewURI(getter_AddRefs(mDefaultIcon), + NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL)); + NS_ENSURE_SUCCESS(rv, rv); + } + return mDefaultIcon->Clone(_retval); +} + +void +nsFaviconService::SendFaviconNotifications(nsIURI* aPageURI, + nsIURI* aFaviconURI, + const nsACString& aGUID) +{ + nsAutoCString faviconSpec; + nsNavHistory* history = nsNavHistory::GetHistoryService(); + if (history && NS_SUCCEEDED(aFaviconURI->GetSpec(faviconSpec))) { + history->SendPageChangedNotification(aPageURI, + nsINavHistoryObserver::ATTRIBUTE_FAVICON, + NS_ConvertUTF8toUTF16(faviconSpec), + aGUID); + } +} + +NS_IMETHODIMP +nsFaviconService::SetAndFetchFaviconForPage(nsIURI* aPageURI, + nsIURI* aFaviconURI, + bool aForceReload, + uint32_t aFaviconLoadType, + nsIFaviconDataCallback* aCallback) +{ + NS_ENSURE_ARG(aPageURI); + NS_ENSURE_ARG(aFaviconURI); + + // If a favicon is in the failed cache, only load it during a forced reload. + bool previouslyFailed; + nsresult rv = IsFailedFavicon(aFaviconURI, &previouslyFailed); + NS_ENSURE_SUCCESS(rv, rv); + if (previouslyFailed) { + if (aForceReload) + RemoveFailedFavicon(aFaviconURI); + else + return NS_OK; + } + + // Check if the icon already exists and fetch it from the network, if needed. + // Finally associate the icon to the requested page if not yet associated. + rv = AsyncFetchAndSetIconForPage::start( + aFaviconURI, aPageURI, aForceReload ? FETCH_ALWAYS : FETCH_IF_MISSING, + aFaviconLoadType, aCallback + ); + NS_ENSURE_SUCCESS(rv, rv); + + // DB will be updated and observers notified when data has finished loading. + return NS_OK; +} + +NS_IMETHODIMP +nsFaviconService::ReplaceFaviconData(nsIURI* aFaviconURI, + const uint8_t* aData, + uint32_t aDataLen, + const nsACString& aMimeType, + PRTime aExpiration) +{ + NS_ENSURE_ARG(aFaviconURI); + NS_ENSURE_ARG(aData); + NS_ENSURE_TRUE(aDataLen > 0, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aMimeType.Length() > 0, NS_ERROR_INVALID_ARG); + if (aExpiration == 0) { + aExpiration = PR_Now() + MAX_FAVICON_EXPIRATION; + } + + UnassociatedIconHashKey* iconKey = mUnassociatedIcons.PutEntry(aFaviconURI); + if (!iconKey) { + return NS_ERROR_OUT_OF_MEMORY; + } + + iconKey->created = PR_Now(); + + // If the cache contains unassociated icons, an expiry timer should already exist, otherwise + // there may be a timer left hanging around, so make sure we fire a new one. + int32_t unassociatedCount = mUnassociatedIcons.Count(); + if (unassociatedCount == 1) { + mExpireUnassociatedIconsTimer->Cancel(); + mExpireUnassociatedIconsTimer->InitWithCallback( + this, UNASSOCIATED_ICON_EXPIRY_INTERVAL, nsITimer::TYPE_ONE_SHOT); + } + + IconData* iconData = &(iconKey->iconData); + iconData->expiration = aExpiration; + iconData->status = ICON_STATUS_CACHED; + iconData->fetchMode = FETCH_NEVER; + nsresult rv = aFaviconURI->GetSpec(iconData->spec); + NS_ENSURE_SUCCESS(rv, rv); + + // If the page provided a large image for the favicon (eg, a highres image + // or a multiresolution .ico file), we don't want to store more data than + // needed. + if (aDataLen > MAX_ICON_FILESIZE(mOptimizedIconDimension)) { + rv = OptimizeFaviconImage(aData, aDataLen, aMimeType, iconData->data, iconData->mimeType); + NS_ENSURE_SUCCESS(rv, rv); + + if (iconData->data.Length() > MAX_FAVICON_SIZE) { + // We cannot optimize this favicon size and we are over the maximum size + // allowed, so we will not save data to the db to avoid bloating it. + mUnassociatedIcons.RemoveEntry(aFaviconURI); + return NS_ERROR_FAILURE; + } + } else { + iconData->mimeType.Assign(aMimeType); + iconData->data.Assign(TO_CHARBUFFER(aData), aDataLen); + } + + // If the database contains an icon at the given url, we will update the + // database immediately so that the associated pages are kept in sync. + // Otherwise, do nothing and let the icon be picked up from the memory hash. + rv = AsyncReplaceFaviconData::start(iconData); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsFaviconService::ReplaceFaviconDataFromDataURL(nsIURI* aFaviconURI, + const nsAString& aDataURL, + PRTime aExpiration) +{ + NS_ENSURE_ARG(aFaviconURI); + NS_ENSURE_TRUE(aDataURL.Length() > 0, NS_ERROR_INVALID_ARG); + if (aExpiration == 0) { + aExpiration = PR_Now() + MAX_FAVICON_EXPIRATION; + } + + nsCOMPtr dataURI; + nsresult rv = NS_NewURI(getter_AddRefs(dataURI), aDataURL); + NS_ENSURE_SUCCESS(rv, rv); + + // Use the data: protocol handler to convert the data. + nsCOMPtr ioService = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr protocolHandler; + rv = ioService->GetProtocolHandler("data", getter_AddRefs(protocolHandler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr channel; + rv = protocolHandler->NewChannel(dataURI, getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + // Blocking stream is OK for data URIs. + nsCOMPtr stream; + rv = channel->Open(getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + uint64_t available64; + rv = stream->Available(&available64); + NS_ENSURE_SUCCESS(rv, rv); + if (available64 == 0 || available64 > UINT32_MAX / sizeof(uint8_t)) + return NS_ERROR_FILE_TOO_BIG; + uint32_t available = (uint32_t)available64; + + // Read all the decoded data. + uint8_t* buffer = static_cast + (nsMemory::Alloc(sizeof(uint8_t) * available)); + if (!buffer) + return NS_ERROR_OUT_OF_MEMORY; + uint32_t numRead; + rv = stream->Read(TO_CHARBUFFER(buffer), available, &numRead); + if (NS_FAILED(rv) || numRead != available) { + nsMemory::Free(buffer); + return rv; + } + + nsAutoCString mimeType; + rv = channel->GetContentType(mimeType); + if (NS_FAILED(rv)) { + nsMemory::Free(buffer); + return rv; + } + + // ReplaceFaviconData can now do the dirty work. + rv = ReplaceFaviconData(aFaviconURI, buffer, available, mimeType, aExpiration); + nsMemory::Free(buffer); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsFaviconService::GetFaviconURLForPage(nsIURI *aPageURI, + nsIFaviconDataCallback* aCallback) +{ + NS_ENSURE_ARG(aPageURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv = AsyncGetFaviconURLForPage::start(aPageURI, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsFaviconService::GetFaviconDataForPage(nsIURI* aPageURI, + nsIFaviconDataCallback* aCallback) +{ + NS_ENSURE_ARG(aPageURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv = AsyncGetFaviconDataForPage::start(aPageURI, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult +nsFaviconService::GetFaviconLinkForIcon(nsIURI* aFaviconURI, + nsIURI** aOutputURI) +{ + NS_ENSURE_ARG(aFaviconURI); + NS_ENSURE_ARG_POINTER(aOutputURI); + + nsAutoCString spec; + if (aFaviconURI) { + nsresult rv = aFaviconURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + } + return GetFaviconLinkForIconString(spec, aOutputURI); +} + + +static PLDHashOperator +ExpireFailedFaviconsCallback(nsCStringHashKey::KeyType aKey, + uint32_t& aData, + void* userArg) +{ + uint32_t* threshold = reinterpret_cast(userArg); + if (aData < *threshold) + return PL_DHASH_REMOVE; + return PL_DHASH_NEXT; +} + + +NS_IMETHODIMP +nsFaviconService::AddFailedFavicon(nsIURI* aFaviconURI) +{ + NS_ENSURE_ARG(aFaviconURI); + + nsAutoCString spec; + nsresult rv = aFaviconURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + mFailedFavicons.Put(spec, mFailedFaviconSerial); + mFailedFaviconSerial ++; + + if (mFailedFavicons.Count() > MAX_FAVICON_CACHE_SIZE) { + // need to expire some entries, delete the FAVICON_CACHE_REDUCE_COUNT number + // of items that are the oldest + uint32_t threshold = mFailedFaviconSerial - + MAX_FAVICON_CACHE_SIZE + FAVICON_CACHE_REDUCE_COUNT; + mFailedFavicons.Enumerate(ExpireFailedFaviconsCallback, &threshold); + } + return NS_OK; +} + + +NS_IMETHODIMP +nsFaviconService::RemoveFailedFavicon(nsIURI* aFaviconURI) +{ + NS_ENSURE_ARG(aFaviconURI); + + nsAutoCString spec; + nsresult rv = aFaviconURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + // we silently do nothing and succeed if the icon is not in the cache + mFailedFavicons.Remove(spec); + return NS_OK; +} + + +NS_IMETHODIMP +nsFaviconService::IsFailedFavicon(nsIURI* aFaviconURI, bool* _retval) +{ + NS_ENSURE_ARG(aFaviconURI); + nsAutoCString spec; + nsresult rv = aFaviconURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t serial; + *_retval = mFailedFavicons.Get(spec, &serial); + return NS_OK; +} + + +// nsFaviconService::GetFaviconLinkForIconString +// +// This computes a favicon URL with string input and using the cached +// default one to minimize parsing. + +nsresult +nsFaviconService::GetFaviconLinkForIconString(const nsCString& aSpec, + nsIURI** aOutput) +{ + if (aSpec.IsEmpty()) { + // default icon for empty strings + if (! mDefaultIcon) { + nsresult rv = NS_NewURI(getter_AddRefs(mDefaultIcon), + NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL)); + NS_ENSURE_SUCCESS(rv, rv); + } + return mDefaultIcon->Clone(aOutput); + } + + if (StringBeginsWith(aSpec, NS_LITERAL_CSTRING("chrome:"))) { + // pass through for chrome URLs, since they can be referenced without + // this service + return NS_NewURI(aOutput, aSpec); + } + + nsAutoCString annoUri; + annoUri.AssignLiteral("moz-anno:" FAVICON_ANNOTATION_NAME ":"); + annoUri += aSpec; + return NS_NewURI(aOutput, annoUri); +} + + +// nsFaviconService::GetFaviconSpecForIconString +// +// This computes a favicon spec for when you don't want a URI object (as in +// the tree view implementation), sparing all parsing and normalization. +void +nsFaviconService::GetFaviconSpecForIconString(const nsCString& aSpec, + nsACString& aOutput) +{ + if (aSpec.IsEmpty()) { + aOutput.AssignLiteral(FAVICON_DEFAULT_URL); + } else if (StringBeginsWith(aSpec, NS_LITERAL_CSTRING("chrome:"))) { + aOutput = aSpec; + } else { + aOutput.AssignLiteral("moz-anno:" FAVICON_ANNOTATION_NAME ":"); + aOutput += aSpec; + } +} + + +// nsFaviconService::OptimizeFaviconImage +// +// Given a blob of data (a image file already read into a buffer), optimize +// its size by recompressing it as a 16x16 PNG. +nsresult +nsFaviconService::OptimizeFaviconImage(const uint8_t* aData, uint32_t aDataLen, + const nsACString& aMimeType, + nsACString& aNewData, + nsACString& aNewMimeType) +{ + nsresult rv; + + nsCOMPtr imgtool = do_CreateInstance("@mozilla.org/image/tools;1"); + + nsCOMPtr stream; + rv = NS_NewByteInputStream(getter_AddRefs(stream), + reinterpret_cast(aData), aDataLen, + NS_ASSIGNMENT_DEPEND); + NS_ENSURE_SUCCESS(rv, rv); + + // decode image + nsCOMPtr container; + rv = imgtool->DecodeImageData(stream, aMimeType, getter_AddRefs(container)); + NS_ENSURE_SUCCESS(rv, rv); + + aNewMimeType.AssignLiteral(DEFAULT_MIME_TYPE); + + // scale and recompress + nsCOMPtr iconStream; + rv = imgtool->EncodeScaledImage(container, aNewMimeType, + mOptimizedIconDimension, + mOptimizedIconDimension, + EmptyString(), + getter_AddRefs(iconStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // Read the stream into a new buffer. + rv = NS_ConsumeStream(iconStream, UINT32_MAX, aNewData); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsFaviconService::GetFaviconDataAsync(nsIURI* aFaviconURI, + mozIStorageStatementCallback *aCallback) +{ + NS_ASSERTION(aCallback, "Doesn't make sense to call this without a callback"); + nsCOMPtr stmt = mDB->GetAsyncStatement( + "SELECT f.data, f.mime_type FROM moz_favicons f WHERE url = :icon_url" + ); + NS_ENSURE_STATE(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("icon_url"), aFaviconURI); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr pendingStatement; + return stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement)); +} + +//////////////////////////////////////////////////////////////////////////////// +//// ExpireFaviconsStatementCallbackNotifier + +ExpireFaviconsStatementCallbackNotifier::ExpireFaviconsStatementCallbackNotifier() +{ +} + + +NS_IMETHODIMP +ExpireFaviconsStatementCallbackNotifier::HandleCompletion(uint16_t aReason) +{ + // We should dispatch only if expiration has been successful. + if (aReason != mozIStorageStatementCallback::REASON_FINISHED) + return NS_OK; + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + (void)observerService->NotifyObservers(nullptr, + NS_PLACES_FAVICONS_EXPIRED_TOPIC_ID, + nullptr); + } + + return NS_OK; +}