diff options
Diffstat (limited to 'toolkit/components/places')
39 files changed, 1373 insertions, 1596 deletions
diff --git a/toolkit/components/places/BookmarkHTMLUtils.sys.mjs b/toolkit/components/places/BookmarkHTMLUtils.sys.mjs index 81bd791730..d619f9fed2 100644 --- a/toolkit/components/places/BookmarkHTMLUtils.sys.mjs +++ b/toolkit/components/places/BookmarkHTMLUtils.sys.mjs @@ -1082,21 +1082,11 @@ BookmarkExporter.prototype = { function insertFaviconForNode(node) { if (node.icon) { try { - // Create a fake faviconURI to use (FIXME: bug 523932) - let faviconURI = Services.io.newURI("fake-favicon-uri:" + node.url); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - faviconURI, - node.icon, - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - PlacesUtils.favicons.setAndFetchFaviconForPage( + PlacesUtils.favicons.setFaviconForPage( Services.io.newURI(node.url), - faviconURI, - false, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - null, - Services.scriptSecurityManager.getSystemPrincipal() + // Create a fake favicon URI to use (FIXME: bug 523932) + Services.io.newURI("fake-favicon-uri:" + node.url), + Services.io.newURI(node.icon) ); } catch (ex) { console.error("Failed to import favicon data:", ex); diff --git a/toolkit/components/places/BookmarkJSONUtils.sys.mjs b/toolkit/components/places/BookmarkJSONUtils.sys.mjs index 0a27db3696..e453e7e094 100644 --- a/toolkit/components/places/BookmarkJSONUtils.sys.mjs +++ b/toolkit/components/places/BookmarkJSONUtils.sys.mjs @@ -21,7 +21,7 @@ const OLD_BOOKMARK_QUERY_TRANSLATIONS = { MOBILE_BOOKMARKS: PlacesUtils.bookmarks.mobileGuid, }; -export var BookmarkJSONUtils = Object.freeze({ +export var BookmarkJSONUtils = { /** * Import bookmarks from a url. * @@ -162,7 +162,7 @@ export var BookmarkJSONUtils = Object.freeze({ }); return { count, hash }; }, -}); +}; function BookmarkImporter(aReplace, aSource) { this._replace = aReplace; @@ -503,21 +503,11 @@ function translateTreeTypes(node) { function insertFaviconForNode(node) { if (node.icon) { try { - // Create a fake faviconURI to use (FIXME: bug 523932) - let faviconURI = Services.io.newURI("fake-favicon-uri:" + node.url); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - faviconURI, - node.icon, - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - PlacesUtils.favicons.setAndFetchFaviconForPage( + PlacesUtils.favicons.setFaviconForPage( Services.io.newURI(node.url), - faviconURI, - false, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - null, - Services.scriptSecurityManager.getSystemPrincipal() + // Create a fake faviconURI to use (FIXME: bug 523932) + Services.io.newURI("fake-favicon-uri:" + node.url), + Services.io.newURI(node.icon) ); } catch (ex) { console.error("Failed to import favicon data:", ex); diff --git a/toolkit/components/places/FaviconHelpers.cpp b/toolkit/components/places/FaviconHelpers.cpp index 1c565a3de1..df06885e1f 100644 --- a/toolkit/components/places/FaviconHelpers.cpp +++ b/toolkit/components/places/FaviconHelpers.cpp @@ -960,6 +960,49 @@ AsyncAssociateIconToPage::Run() { } //////////////////////////////////////////////////////////////////////////////// +//// AsyncSetIconForPage + +AsyncSetIconForPage::AsyncSetIconForPage(const IconData& aIcon, + const PageData& aPage, + PlacesCompletionCallback* aCallback) + : Runnable("places::AsyncSetIconForPage"), + mCallback(new nsMainThreadPtrHolder<PlacesCompletionCallback>( + "AsyncSetIconForPage::mCallback", aCallback, false)), + mIcon(aIcon), + mPage(aPage) {} + +NS_IMETHODIMP +AsyncSetIconForPage::Run() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mIcon.payloads.Length(), "The icon should have valid data"); + MOZ_ASSERT(mPage.spec.Length(), "The page should have spec"); + MOZ_ASSERT(mPage.guid.IsEmpty(), "The page should not have guid"); + + nsresult rv = NS_OK; + auto guard = MakeScopeExit([&]() { + if (mCallback) { + NS_DispatchToMainThread( + NS_NewRunnableFunction("AsyncSetIconForPage::Callback", + [rv, callback = std::move(mCallback)]() { + (void)callback->Complete(rv); + })); + } + }); + + // Fetch the page data. + RefPtr<Database> DB = Database::GetDatabase(); + if (MOZ_UNLIKELY(!DB)) { + return (rv = NS_ERROR_UNEXPECTED); + } + rv = FetchPageInfo(DB, mPage); + NS_ENSURE_SUCCESS(rv, rv); + + nsMainThreadPtrHandle<nsIFaviconDataCallback> nullCallback; + AsyncAssociateIconToPage event(mIcon, mPage, nullCallback); + return (rv = event.Run()); +} + +//////////////////////////////////////////////////////////////////////////////// //// AsyncGetFaviconURLForPage AsyncGetFaviconURLForPage::AsyncGetFaviconURLForPage( @@ -1041,61 +1084,6 @@ AsyncGetFaviconDataForPage::Run() { } //////////////////////////////////////////////////////////////////////////////// -//// AsyncReplaceFaviconData - -AsyncReplaceFaviconData::AsyncReplaceFaviconData(const IconData& aIcon) - : Runnable("places::AsyncReplaceFaviconData"), mIcon(aIcon) { - MOZ_ASSERT(NS_IsMainThread()); -} - -NS_IMETHODIMP -AsyncReplaceFaviconData::Run() { - MOZ_ASSERT(!NS_IsMainThread()); - - RefPtr<Database> DB = Database::GetDatabase(); - NS_ENSURE_STATE(DB); - - mozStorageTransaction transaction( - DB->MainConn(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE); - - // XXX Handle the error, bug 1696133. - Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); - - nsresult rv = SetIconInfo(DB, mIcon, true); - if (rv == NS_ERROR_NOT_AVAILABLE) { - // There's no previous icon to replace, we don't need to do anything. - (void)transaction.Commit(); - return NS_OK; - } - NS_ENSURE_SUCCESS(rv, rv); - rv = transaction.Commit(); - NS_ENSURE_SUCCESS(rv, rv); - - // We can invalidate the cache version since we now persist the icon. - nsCOMPtr<nsIRunnable> event = NewRunnableMethod( - "places::AsyncReplaceFaviconData::RemoveIconDataCacheEntry", this, - &AsyncReplaceFaviconData::RemoveIconDataCacheEntry); - rv = NS_DispatchToMainThread(event); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - -nsresult AsyncReplaceFaviconData::RemoveIconDataCacheEntry() { - MOZ_ASSERT(NS_IsMainThread()); - - nsCOMPtr<nsIURI> iconURI; - nsresult rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec); - NS_ENSURE_SUCCESS(rv, rv); - - nsFaviconService* favicons = nsFaviconService::GetFaviconService(); - NS_ENSURE_STATE(favicons); - favicons->mUnassociatedIcons.RemoveEntry(iconURI); - - return NS_OK; -} - -//////////////////////////////////////////////////////////////////////////////// //// NotifyIconObservers NotifyIconObservers::NotifyIconObservers( @@ -1207,7 +1195,8 @@ AsyncCopyFavicons::Run() { } NS_ENSURE_SUCCESS(rv, rv); - // Get just one icon, to check whether the page has any, and to notify later. + // Get just one icon, to check whether the page has any, and to notify + // later. rv = FetchIconPerSpec(DB, mFromPage.spec, ""_ns, icon, UINT16_MAX); NS_ENSURE_SUCCESS(rv, rv); diff --git a/toolkit/components/places/FaviconHelpers.h b/toolkit/components/places/FaviconHelpers.h index ba3407722c..aaa5396fe6 100644 --- a/toolkit/components/places/FaviconHelpers.h +++ b/toolkit/components/places/FaviconHelpers.h @@ -14,6 +14,7 @@ #include "nsThreadUtils.h" #include "nsProxyRelease.h" #include "imgLoader.h" +#include "PlacesCompletionCallback.h" class nsIPrincipal; @@ -192,6 +193,33 @@ class AsyncAssociateIconToPage final : public Runnable { }; /** + * Set favicon for the page, finally dispatches an event to the + * main thread to notify the change to observers. + */ +class AsyncSetIconForPage final : public Runnable { + public: + NS_DECL_NSIRUNNABLE + + /** + * Constructor. + * + * @param aIcon + * Icon to be associated. + * @param aPage + * Page to which associate the icon. + * @param aCallback + * Function to be called when the associate process finishes. + */ + AsyncSetIconForPage(const IconData& aIcon, const PageData& aPage, + PlacesCompletionCallback* aCallback); + + private: + nsMainThreadPtrHandle<PlacesCompletionCallback> mCallback; + IconData mIcon; + PageData mPage; +}; + +/** * Asynchronously tries to get the URL of a page's favicon, then notifies the * given observer. */ @@ -256,18 +284,6 @@ class AsyncGetFaviconDataForPage final : public Runnable { nsCString mPageHost; }; -class AsyncReplaceFaviconData final : public Runnable { - public: - NS_DECL_NSIRUNNABLE - - explicit AsyncReplaceFaviconData(const IconData& aIcon); - - private: - nsresult RemoveIconDataCacheEntry(); - - IconData mIcon; -}; - /** * Notifies the icon change to favicon observers. */ @@ -276,7 +292,7 @@ class NotifyIconObservers final : public Runnable { NS_DECL_NSIRUNNABLE /** - * Constructor. + * Constructor for nsIFaviconDataCallback. * * @param aIcon * Icon information. Can be empty if no icon is associated to the page. diff --git a/toolkit/components/places/History.cpp b/toolkit/components/places/History.cpp index 6c066f47be..474b2cdac8 100644 --- a/toolkit/components/places/History.cpp +++ b/toolkit/components/places/History.cpp @@ -1434,6 +1434,13 @@ void NotifyEmbedVisit(VisitData& aPlace, (void)NS_DispatchToMainThread(event); } +void NotifyOriginRestrictedVisit(nsIURI* aURI) { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!"); + + nsCOMPtr<nsIRunnable> event = new NotifyManyVisitsObservers(VisitData(aURI)); + (void)NS_DispatchToMainThread(event); +} + void NotifyVisitIfHavingUserPass(nsIURI* aURI) { MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread!"); @@ -1868,6 +1875,64 @@ const mozIStorageConnection* History::GetConstDBConn() { return mDB->MainConn(); } +void History::UpdateOriginFloodingRestriction(nsACString& aOrigin) { + MOZ_ASSERT(NS_IsMainThread()); + + TimeStamp latestInputTimeStamp = UserActivation::LatestUserInputStart(); + if (latestInputTimeStamp.IsNull()) { + // Probably just after browser initialization. + return; + } + + TimeStamp now = TimeStamp::Now(); + + // Remove expired flooding resrictions. + for (auto iter = mOriginFloodingRestrictions.Iter(); !iter.Done(); + iter.Next()) { + if ((now - iter.Data().mLastVisitTimeStamp).ToSeconds() > + iter.Data().mExpireIntervalSeconds) { + iter.Remove(); + } + } + + // If not long enough time elapsed sincer last user interaction, this visit + // can be stored. + if ((now - latestInputTimeStamp).ToSeconds() < + StaticPrefs:: + places_history_floodingPrevention_maxSecondsFromLastUserInteraction()) { + mOriginFloodingRestrictions.Remove(aOrigin); + return; + } + + // Otherwise, update the flooding restriction for the origin. + auto restriction = mOriginFloodingRestrictions.Lookup(aOrigin); + if (restriction) { + if (restriction->mAllowedVisitCount) { + // Count down to 0. If 0, the origin should be restricted. + restriction->mAllowedVisitCount -= 1; + } else { + // Since the origin is marked as restricted make its expiration time + // longer. + restriction->mExpireIntervalSeconds *= 2; + } + } else { + // Initialize OriginFloodingRestriction to store the origin restriction. + mOriginFloodingRestrictions.InsertOrUpdate( + aOrigin, + OriginFloodingRestriction{ + now, + StaticPrefs:: + places_history_floodingPrevention_restrictionExpireSeconds(), + StaticPrefs::places_history_floodingPrevention_restrictionCount()}); + } +} + +bool History::IsRestrictedOrigin(nsACString& aOrigin) { + auto restriction = mOriginFloodingRestrictions.Lookup(aOrigin); + // If the count is 0, need to restrict the origin. + return restriction && !restriction->mAllowedVisitCount; +} + void History::Shutdown() { MOZ_ASSERT(NS_IsMainThread()); MutexAutoLock lockedScope(mBlockShutdownMutex); @@ -2019,6 +2084,31 @@ History::VisitURI(nsIWidget* aWidget, nsIURI* aURI, nsIURI* aLastVisitedURI, // Error pages should never be autocompleted. place.isUnrecoverableError = aFlags & IHistory::UNRECOVERABLE_ERROR; + // EMBED visits should not go through the database. + // They exist only to keep track of isVisited status during the session. + if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { + NotifyEmbedVisit(place); + return NS_OK; + } + + if (StaticPrefs::places_history_floodingPrevention_enabled()) { + // If the origin is restricted, make isVisited status available during the + // session but not stored in the database. + nsAutoCString origin; + Unused << visitedURI->GetHost(origin); + if (StringBeginsWith(origin, "www."_ns)) { + origin.Cut(0, 4); + } + + UpdateOriginFloodingRestriction(origin); + + if (IsRestrictedOrigin(origin)) { + NotifyOriginRestrictedVisit(visitedURI); + NotifyVisitIfHavingUserPass(aURI); + return NS_OK; + } + } + nsCOMPtr<nsIBrowserWindowTracker> bwt = do_ImportESModule("resource:///modules/BrowserWindowTracker.sys.mjs", "BrowserWindowTracker", &rv); @@ -2075,17 +2165,11 @@ History::VisitURI(nsIWidget* aWidget, nsIURI* aURI, nsIURI* aLastVisitedURI, } } - // EMBED visits should not go through the database. - // They exist only to keep track of isVisited status during the session. - if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { - NotifyEmbedVisit(place); - } else { - mozIStorageConnection* dbConn = GetDBConn(); - NS_ENSURE_STATE(dbConn); + mozIStorageConnection* dbConn = GetDBConn(); + NS_ENSURE_STATE(dbConn); - rv = InsertVisitedURIs::Start(dbConn, std::move(placeArray)); - NS_ENSURE_SUCCESS(rv, rv); - } + rv = InsertVisitedURIs::Start(dbConn, std::move(placeArray)); + NS_ENSURE_SUCCESS(rv, rv); // URIs with a userpass component are not stored in the database for security // reasons, we store the exposable URI version of them instead. @@ -2323,6 +2407,7 @@ History::IsURIVisited(nsIURI* aURI, mozIVisitedStatusCallback* aCallback) { NS_IMETHODIMP History::ClearCache() { mRecentlyVisitedURIs.Clear(); + mOriginFloodingRestrictions.Clear(); return NS_OK; } diff --git a/toolkit/components/places/History.h b/toolkit/components/places/History.h index 03a31186fd..a5007082cd 100644 --- a/toolkit/components/places/History.h +++ b/toolkit/components/places/History.h @@ -196,6 +196,16 @@ class History final : public BaseHistory, }; nsTHashMap<nsURIHashKey, RecentURIVisit> mRecentlyVisitedURIs; + + struct OriginFloodingRestriction { + TimeStamp mLastVisitTimeStamp; + uint32_t mExpireIntervalSeconds; + uint32_t mAllowedVisitCount; + }; + nsTHashMap<nsCStringHashKey, OriginFloodingRestriction> + mOriginFloodingRestrictions; + void UpdateOriginFloodingRestriction(nsACString& aOrigin); + bool IsRestrictedOrigin(nsACString& aOrigin); }; } // namespace mozilla::places diff --git a/toolkit/components/places/PlacesCompletionCallback.idl b/toolkit/components/places/PlacesCompletionCallback.idl new file mode 100644 index 0000000000..8cc089c0e7 --- /dev/null +++ b/toolkit/components/places/PlacesCompletionCallback.idl @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +#include "nsISupports.idl" + +[scriptable, function, uuid(ea26627b-d21a-4f97-b2d0-f8df7be24808)] +interface PlacesCompletionCallback : nsISupports { + /** + * Indicates that the event this callback was passed in for has completed. + * + * @param status + * The status of the call. Generally NS_OK if the operation + * completed successfully. + */ + void complete(in nsresult status); +}; diff --git a/toolkit/components/places/PlacesQuery.sys.mjs b/toolkit/components/places/PlacesQuery.sys.mjs index 7ddf5ee988..d969687def 100644 --- a/toolkit/components/places/PlacesQuery.sys.mjs +++ b/toolkit/components/places/PlacesQuery.sys.mjs @@ -58,7 +58,7 @@ export class PlacesQuery { #historyListenerCallback = null; /** @type {DeferredTask} */ #historyObserverTask = null; - searchInProgress = false; + #searchInProgress = false; /** * Get a snapshot of history visits at this moment. @@ -118,14 +118,18 @@ export class PlacesQuery { groupBy = "url"; break; } + const whereClause = + daysOld == Infinity + ? "" + : `WHERE visit_date >= (strftime('%s','now','localtime','start of day','-${Number( + daysOld + )} days','utc') * 1000000)`; const sql = `SELECT MAX(visit_date) as visit_date, title, url FROM moz_historyvisits v JOIN moz_places h ON v.place_id = h.id - WHERE visit_date >= (strftime('%s','now','localtime','start of day','-${Number( - daysOld - )} days','utc') * 1000000) AND hidden = 0 + ${whereClause} GROUP BY ${groupBy} ORDER BY visit_date DESC LIMIT ${limit > 0 ? limit : -1}`; @@ -169,11 +173,11 @@ export class PlacesQuery { GROUP BY url ORDER BY ${orderBy} LIMIT ${limit > 0 ? limit : -1}`; - if (this.searchInProgress) { + if (this.#searchInProgress) { db.interrupt(); } try { - this.searchInProgress = true; + this.#searchInProgress = true; const rows = await db.executeCached(sql, { query, matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED, @@ -181,7 +185,7 @@ export class PlacesQuery { }); return rows.map(row => this.formatRowAsVisit(row)); } finally { - this.searchInProgress = false; + this.#searchInProgress = false; } } @@ -309,7 +313,7 @@ export class PlacesQuery { } this.#historyListener = null; this.#historyListenerCallback = null; - if (!this.#historyObserverTask.isFinalized) { + if (this.#historyObserverTask && !this.#historyObserverTask.isFinalized) { this.#historyObserverTask.disarm(); this.#historyObserverTask.finalize(); } diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build index 28a7421fba..797d5709f6 100644 --- a/toolkit/components/places/moz.build +++ b/toolkit/components/places/moz.build @@ -23,6 +23,7 @@ if CONFIG["MOZ_PLACES"]: "nsINavBookmarksService.idl", "nsIPlacesPreviewsHelperService.idl", "nsITaggingService.idl", + "PlacesCompletionCallback.idl", ] EXPORTS.mozilla.places = [ diff --git a/toolkit/components/places/nsFaviconService.cpp b/toolkit/components/places/nsFaviconService.cpp index c236d7a680..ba338e9d52 100644 --- a/toolkit/components/places/nsFaviconService.cpp +++ b/toolkit/components/places/nsFaviconService.cpp @@ -14,6 +14,7 @@ */ #include "nsFaviconService.h" +#include "PlacesCompletionCallback.h" #include "nsNavHistory.h" #include "nsPlacesMacros.h" @@ -24,6 +25,7 @@ #include "nsStreamUtils.h" #include "plbase64.h" #include "nsIClassInfoImpl.h" +#include "mozilla/AppShutdown.h" #include "mozilla/ArrayUtils.h" #include "mozilla/LoadInfo.h" #include "mozilla/NullPrincipal.h" @@ -35,13 +37,6 @@ #include "nsContentUtils.h" #include "imgICache.h" -#define UNASSOCIATED_FAVICONS_LENGTH 32 - -// 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 - using namespace mozilla; using namespace mozilla::places; @@ -123,12 +118,10 @@ nsresult GetFramesInfoForContainer(imgIContainer* aContainer, PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsFaviconService, gFaviconService) NS_IMPL_CLASSINFO(nsFaviconService, nullptr, 0, NS_FAVICONSERVICE_CID) -NS_IMPL_ISUPPORTS_CI(nsFaviconService, nsIFaviconService, nsITimerCallback, - nsINamed) +NS_IMPL_ISUPPORTS_CI(nsFaviconService, nsIFaviconService) nsFaviconService::nsFaviconService() - : mUnassociatedIcons(UNASSOCIATED_FAVICONS_LENGTH), - mDefaultIconURIPreferredSize(UINT16_MAX) { + : mDefaultIconURIPreferredSize(UINT16_MAX) { NS_ASSERTION(!gFaviconService, "Attempting to create two instances of the service!"); gFaviconService = this; @@ -152,10 +145,6 @@ nsFaviconService::StoreLastInsertedId(const nsACString& aTable, nsresult nsFaviconService::Init() { mDB = Database::GetDatabase(); NS_ENSURE_STATE(mDB); - - mExpireUnassociatedIconsTimer = NS_NewTimer(); - NS_ENSURE_STATE(mExpireUnassociatedIconsTimer); - return NS_OK; } @@ -188,41 +177,6 @@ nsFaviconService::ExpireAllFavicons() { } //////////////////////////////////////////////////////////////////////////////// -//// nsITimerCallback - -NS_IMETHODIMP -nsFaviconService::Notify(nsITimer* timer) { - if (timer != mExpireUnassociatedIconsTimer.get()) { - return NS_ERROR_INVALID_ARG; - } - - PRTime now = PR_Now(); - for (auto iter = mUnassociatedIcons.Iter(); !iter.Done(); iter.Next()) { - UnassociatedIconHashKey* iconKey = iter.Get(); - if (now - iconKey->created >= UNASSOCIATED_ICON_EXPIRY_INTERVAL) { - iter.Remove(); - } - } - - // 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; -} - -//////////////////////////////////////////////////////////////////////// -//// nsINamed - -NS_IMETHODIMP -nsFaviconService::GetName(nsACString& aName) { - aName.AssignLiteral("nsFaviconService"); - return NS_OK; -} - -//////////////////////////////////////////////////////////////////////////////// //// nsIFaviconService NS_IMETHODIMP @@ -259,6 +213,161 @@ void nsFaviconService::ClearImageCache(nsIURI* aImageURI) { } NS_IMETHODIMP +nsFaviconService::SetFaviconForPage( + nsIURI* aPageURI, nsIURI* aFaviconURI, nsIURI* aDataURL, + PRTime aExpiration = 0, PlacesCompletionCallback* aCallback = nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aPageURI); + NS_ENSURE_ARG(aFaviconURI); + NS_ENSURE_ARG(aDataURL); + + MOZ_DIAGNOSTIC_ASSERT(aDataURL->SchemeIs("data")); + if (!aDataURL->SchemeIs("data")) { + return NS_ERROR_INVALID_ARG; + } + + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return NS_OK; + } + + PRTime now = PR_Now(); + if (aExpiration < now + MIN_FAVICON_EXPIRATION) { + // Invalid input, just use the default. + aExpiration = now + MAX_FAVICON_EXPIRATION; + } + + // Use the data: protocol handler to convert the data. + nsresult rv = NS_OK; + auto guard = MakeScopeExit([&]() { + if (aCallback) { + aCallback->Complete(rv); + } + }); + + nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIProtocolHandler> protocolHandler; + rv = ioService->GetProtocolHandler("data", getter_AddRefs(protocolHandler)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrincipal> loadingPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + if (MOZ_UNLIKELY(!(loadingPrincipal))) { + return (rv = NS_ERROR_NULL_POINTER); + } + + nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( + loadingPrincipal, + nullptr, // aTriggeringPrincipal + nullptr, // aLoadingNode + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT | + nsILoadInfo::SEC_ALLOW_CHROME | nsILoadInfo::SEC_DISALLOW_SCRIPT, + nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON); + + nsCOMPtr<nsIChannel> channel; + rv = protocolHandler->NewChannel(aDataURL, loadInfo, getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + // Blocking stream is OK for data URIs. + nsCOMPtr<nsIInputStream> 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 (rv = NS_ERROR_FILE_TOO_BIG); + } + uint32_t available = (uint32_t)available64; + + // Read all the decoded data. + nsTArray<uint8_t> buffer; + buffer.SetLength(available); + uint32_t numRead; + rv = stream->Read(TO_CHARBUFFER(buffer.Elements()), available, &numRead); + NS_ENSURE_SUCCESS(rv, rv); + if (numRead != available) { + return (rv = NS_ERROR_UNEXPECTED); + } + + nsAutoCString mimeType; + rv = channel->GetContentType(mimeType); + NS_ENSURE_SUCCESS(rv, rv); + if (!imgLoader::SupportImageWithMimeType( + mimeType, AcceptedMimeTypes::IMAGES_AND_DOCUMENTS)) { + return (rv = NS_ERROR_UNEXPECTED); + } + + // Favicon should be handled without userpass. + nsCOMPtr<nsIURI> faviconURI = GetExposableURI(aFaviconURI); + nsCOMPtr<nsIURI> pageURI = GetExposableURI(aPageURI); + + IconData icon; + icon.expiration = aExpiration; + icon.status = ICON_STATUS_CACHED; + icon.fetchMode = FETCH_NEVER; + rv = faviconURI->GetSpec(icon.spec); + NS_ENSURE_SUCCESS(rv, rv); + // URIs can arguably lack a host. + (void)faviconURI->GetHost(icon.host); + if (StringBeginsWith(icon.host, "www."_ns)) { + icon.host.Cut(0, 4); + } + + IconPayload payload; + payload.mimeType = mimeType; + payload.data.Assign(TO_CHARBUFFER(buffer.Elements()), buffer.Length()); + if (payload.mimeType.EqualsLiteral(SVG_MIME_TYPE)) { + payload.width = UINT16_MAX; + } + icon.payloads.AppendElement(payload); + + rv = OptimizeIconSizes(icon); + NS_ENSURE_SUCCESS(rv, rv); + + PageData page; + rv = pageURI->GetSpec(page.spec); + NS_ENSURE_SUCCESS(rv, rv); + // URIs can arguably lack a host. + (void)pageURI->GetHost(page.host); + if (StringBeginsWith(page.host, "www."_ns)) { + page.host.Cut(0, 4); + } + + // A root icon is when the icon and page have the same host and the path + // is just /favicon.ico. These icons are considered valid for the whole + // origin and expired with the origin through a trigger. + nsAutoCString path; + if (NS_SUCCEEDED(faviconURI->GetPathQueryRef(path)) && !icon.host.IsEmpty() && + icon.host.Equals(page.host) && path.EqualsLiteral("/favicon.ico")) { + icon.rootIcon = 1; + } + + // If the page url points to an image, the icon's url will be the same. + // TODO (Bug 403651): store a resample of the image. For now avoid that + // for database size and UX concerns. + // Don't store favicons for error pages either. + if (icon.spec.Equals(page.spec) || + icon.spec.EqualsLiteral(FAVICON_CERTERRORPAGE_URL) || + icon.spec.EqualsLiteral(FAVICON_ERRORPAGE_URL)) { + return NS_OK; + } + + RefPtr<AsyncSetIconForPage> event = + new AsyncSetIconForPage(icon, page, aCallback); + RefPtr<Database> DB = Database::GetDatabase(); + if (MOZ_UNLIKELY(!DB)) { + return (rv = NS_ERROR_UNEXPECTED); + } + + DB->DispatchToAsyncThread(event); + + guard.release(); + return NS_OK; +} + +NS_IMETHODIMP nsFaviconService::SetAndFetchFaviconForPage( nsIURI* aPageURI, nsIURI* aFaviconURI, bool aForceReload, uint32_t aFaviconLoadType, nsIFaviconDataCallback* aCallback, @@ -310,20 +419,13 @@ nsFaviconService::SetAndFetchFaviconForPage( // Build icon data. IconData icon; - // If we have an in-memory icon payload, it overwrites the actual request. - UnassociatedIconHashKey* iconKey = mUnassociatedIcons.GetEntry(faviconURI); - if (iconKey) { - icon = iconKey->iconData; - mUnassociatedIcons.RemoveEntry(iconKey); - } else { - icon.fetchMode = aForceReload ? FETCH_ALWAYS : FETCH_IF_MISSING; - rv = faviconURI->GetSpec(icon.spec); - NS_ENSURE_SUCCESS(rv, rv); - // URIs can arguably lack a host. - Unused << faviconURI->GetHost(icon.host); - if (StringBeginsWith(icon.host, "www."_ns)) { - icon.host.Cut(0, 4); - } + icon.fetchMode = aForceReload ? FETCH_ALWAYS : FETCH_IF_MISSING; + rv = faviconURI->GetSpec(icon.spec); + NS_ENSURE_SUCCESS(rv, rv); + // URIs can arguably lack a host. + Unused << faviconURI->GetHost(icon.host); + if (StringBeginsWith(icon.host, "www."_ns)) { + icon.host.Cut(0, 4); } // A root icon is when the icon and page have the same host and the path @@ -361,182 +463,6 @@ nsFaviconService::SetAndFetchFaviconForPage( } NS_IMETHODIMP -nsFaviconService::ReplaceFaviconData(nsIURI* aFaviconURI, - const nsTArray<uint8_t>& aData, - const nsACString& aMimeType, - PRTime aExpiration) { - MOZ_ASSERT(NS_IsMainThread()); - NS_ENSURE_ARG(aFaviconURI); - NS_ENSURE_ARG(aData.Length() > 0); - NS_ENSURE_ARG(aMimeType.Length() > 0); - NS_ENSURE_ARG(imgLoader::SupportImageWithMimeType( - aMimeType, AcceptedMimeTypes::IMAGES_AND_DOCUMENTS)); - - nsCOMPtr<nsIURI> faviconURI = GetExposableURI(aFaviconURI); - - PRTime now = PR_Now(); - if (aExpiration < now + MIN_FAVICON_EXPIRATION) { - // Invalid input, just use the default. - aExpiration = now + MAX_FAVICON_EXPIRATION; - } - - UnassociatedIconHashKey* iconKey = mUnassociatedIcons.PutEntry(faviconURI); - if (!iconKey) { - return NS_ERROR_OUT_OF_MEMORY; - } - - iconKey->created = 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. - uint32_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 = faviconURI->GetSpec(iconData->spec); - NS_ENSURE_SUCCESS(rv, rv); - // URIs can arguably lack a host. - Unused << faviconURI->GetHost(iconData->host); - if (StringBeginsWith(iconData->host, "www."_ns)) { - iconData->host.Cut(0, 4); - } - - // Note we can't set rootIcon here, because don't know the page it will be - // associated with. We'll do that later in SetAndFetchFaviconForPage if the - // icon doesn't exist; otherwise, if AsyncReplaceFaviconData updates an - // existing icon, it will take care of not overwriting an existing - // root = 1 value. - - IconPayload payload; - payload.mimeType = aMimeType; - payload.data.Assign(TO_CHARBUFFER(aData.Elements()), aData.Length()); - if (payload.mimeType.EqualsLiteral(SVG_MIME_TYPE)) { - payload.width = UINT16_MAX; - } - // There may already be a previous payload, so ensure to only have one. - iconData->payloads.Clear(); - iconData->payloads.AppendElement(payload); - - rv = OptimizeIconSizes(*iconData); - NS_ENSURE_SUCCESS(rv, rv); - - // If there's not valid payload, don't store the icon into to the database. - if ((*iconData).payloads.Length() == 0) { - // 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(faviconURI); - return NS_ERROR_FAILURE; - } - - // 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. - RefPtr<AsyncReplaceFaviconData> event = - new AsyncReplaceFaviconData(*iconData); - RefPtr<Database> DB = Database::GetDatabase(); - NS_ENSURE_STATE(DB); - DB->DispatchToAsyncThread(event); - - return NS_OK; -} - -NS_IMETHODIMP -nsFaviconService::ReplaceFaviconDataFromDataURL( - nsIURI* aFaviconURI, const nsAString& aDataURL, PRTime aExpiration, - nsIPrincipal* aLoadingPrincipal) { - NS_ENSURE_ARG(aFaviconURI); - NS_ENSURE_TRUE(aDataURL.Length() > 0, NS_ERROR_INVALID_ARG); - PRTime now = PR_Now(); - if (aExpiration < now + MIN_FAVICON_EXPIRATION) { - // Invalid input, just use the default. - aExpiration = now + MAX_FAVICON_EXPIRATION; - } - - nsCOMPtr<nsIURI> dataURI; - nsresult rv = NS_NewURI(getter_AddRefs(dataURI), aDataURL); - NS_ENSURE_SUCCESS(rv, rv); - - // Use the data: protocol handler to convert the data. - nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv); - NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr<nsIProtocolHandler> protocolHandler; - rv = ioService->GetProtocolHandler("data", getter_AddRefs(protocolHandler)); - NS_ENSURE_SUCCESS(rv, rv); - - nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadingPrincipal; - MOZ_ASSERT(loadingPrincipal, - "please provide aLoadingPrincipal for this favicon"); - if (!loadingPrincipal) { - // Let's default to the nullPrincipal if no loadingPrincipal is provided. - AutoTArray<nsString, 2> params = { - u"nsFaviconService::ReplaceFaviconDataFromDataURL()"_ns, - u"nsFaviconService::ReplaceFaviconDataFromDataURL(...," - " [optional aLoadingPrincipal])"_ns}; - nsContentUtils::ReportToConsole( - nsIScriptError::warningFlag, "Security by Default"_ns, - nullptr, // aDocument - nsContentUtils::eNECKO_PROPERTIES, "APIDeprecationWarning", params); - - loadingPrincipal = NullPrincipal::CreateWithoutOriginAttributes(); - } - NS_ENSURE_TRUE(loadingPrincipal, NS_ERROR_FAILURE); - - nsCOMPtr<nsILoadInfo> loadInfo = new mozilla::net::LoadInfo( - loadingPrincipal, - nullptr, // aTriggeringPrincipal - nullptr, // aLoadingNode - nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT | - nsILoadInfo::SEC_ALLOW_CHROME | nsILoadInfo::SEC_DISALLOW_SCRIPT, - nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON); - - nsCOMPtr<nsIChannel> channel; - rv = protocolHandler->NewChannel(dataURI, loadInfo, getter_AddRefs(channel)); - NS_ENSURE_SUCCESS(rv, rv); - - // Blocking stream is OK for data URIs. - nsCOMPtr<nsIInputStream> 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. - nsTArray<uint8_t> buffer; - buffer.SetLength(available); - uint32_t numRead; - rv = stream->Read(TO_CHARBUFFER(buffer.Elements()), available, &numRead); - if (NS_FAILED(rv) || numRead != available) { - return rv; - } - - nsAutoCString mimeType; - rv = channel->GetContentType(mimeType); - if (NS_FAILED(rv)) { - return rv; - } - - // ReplaceFaviconData can now do the dirty work. - rv = ReplaceFaviconData(aFaviconURI, buffer, mimeType, aExpiration); - NS_ENSURE_SUCCESS(rv, rv); - - return NS_OK; -} - -NS_IMETHODIMP nsFaviconService::GetFaviconURLForPage(nsIURI* aPageURI, nsIFaviconDataCallback* aCallback, uint16_t aPreferredWidth) { diff --git a/toolkit/components/places/nsFaviconService.h b/toolkit/components/places/nsFaviconService.h index 4485bbaf28..ab142bb5fc 100644 --- a/toolkit/components/places/nsFaviconService.h +++ b/toolkit/components/places/nsFaviconService.h @@ -16,8 +16,6 @@ #include "nsCOMPtr.h" #include "nsComponentManagerUtils.h" #include "nsIFaviconService.h" -#include "nsINamed.h" -#include "nsITimer.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsTHashtable.h" @@ -32,21 +30,7 @@ extern const uint16_t gFaviconSizes[7]; // forward class definitions class mozIStorageStatementCallback; -class UnassociatedIconHashKey : public nsURIHashKey { - public: - explicit UnassociatedIconHashKey(const nsIURI* aURI) - : nsURIHashKey(aURI), created(PR_Now()) {} - UnassociatedIconHashKey(UnassociatedIconHashKey&& aOther) noexcept - : nsURIHashKey(std::move(aOther)), - iconData(std::move(aOther.iconData)), - created(std::move(aOther.created)) {} - mozilla::places::IconData iconData; - PRTime created; -}; - -class nsFaviconService final : public nsIFaviconService, - public nsITimerCallback, - public nsINamed { +class nsFaviconService final : public nsIFaviconService { public: nsFaviconService(); @@ -107,8 +91,6 @@ class nsFaviconService final : public nsIFaviconService, NS_DECL_ISUPPORTS NS_DECL_NSIFAVICONSERVICE - NS_DECL_NSITIMERCALLBACK - NS_DECL_NSINAMED private: imgITools* GetImgTools() { @@ -122,7 +104,6 @@ class nsFaviconService final : public nsIFaviconService, RefPtr<mozilla::places::Database> mDB; - nsCOMPtr<nsITimer> mExpireUnassociatedIconsTimer; nsCOMPtr<imgITools> mImgTools; static nsFaviconService* gFaviconService; @@ -134,11 +115,6 @@ class nsFaviconService final : public nsIFaviconService, * they get back. May be null, in which case it needs initialization. */ nsCOMPtr<nsIURI> mDefaultIcon; - - // This class needs access to the icons cache. - friend class mozilla::places::AsyncReplaceFaviconData; - nsTHashtable<UnassociatedIconHashKey> mUnassociatedIcons; - uint16_t mDefaultIconURIPreferredSize; }; diff --git a/toolkit/components/places/nsIFaviconService.idl b/toolkit/components/places/nsIFaviconService.idl index 5bc26545c6..f0aeda711f 100644 --- a/toolkit/components/places/nsIFaviconService.idl +++ b/toolkit/components/places/nsIFaviconService.idl @@ -9,6 +9,7 @@ interface nsIURI; interface nsIPrincipal; interface mozIPlacesPendingOperation; interface nsIFaviconDataCallback; +interface PlacesCompletionCallback; [scriptable, uuid(e81e0b0c-b9f1-4c2e-8f3c-b809933cf73c)] interface nsIFaviconService : nsISupports @@ -141,72 +142,35 @@ interface nsIFaviconService : nsISupports [optional] in unsigned long long aRequestContextID); /** - * Sets the data for a given favicon URI either by replacing existing data in - * the database or taking the place of otherwise fetched icon data when - * calling setAndFetchFaviconForPage later. - * - * Favicon data for favicon URIs that are not associated with a page URI via - * setAndFetchFaviconForPage will be stored in memory, but may be expired at - * any time, so you should make an effort to associate favicon URIs with page - * URIs as soon as possible. - * - * It's better to not use this function for chrome: icon URIs since you can - * reference the chrome image yourself. getFaviconLinkForIcon/Page will ignore - * any associated data if the favicon URI is "chrome:" and just return the - * same chrome URI. - * - * This function does NOT send out notifications that the data has changed. - * Pages using this favicons that are visible in history or bookmarks views - * will keep the old icon until they have been refreshed by other means. - * - * This function tries to optimize the favicon size, if it is bigger - * than a defined limit we will try to convert it to a 16x16 png image. - * If the conversion fails and favicon is still bigger than our max accepted - * size it won't be saved. - * - * @param aFaviconURI - * URI of the favicon whose data is being set. - * @param aData - * Binary contents of the favicon to save - * @param aMimeType - * MIME type of the data to store. This is important so that we know - * what to report when the favicon is used. You should always set this - * param unless you are clearing an icon. - * @param [optional] aExpiration - * Time in microseconds since the epoch when this favicon expires. - * Until this time, we won't try to load it again. - * @throws NS_ERROR_FAILURE - * Thrown if the favicon is overbloated and won't be saved to the db. - */ - void replaceFaviconData(in nsIURI aFaviconURI, - in Array<octet> aData, - in AUTF8String aMimeType, - [optional] in PRTime aExpiration); - - /** - * Same as replaceFaviconData but the data is provided by a string - * containing a data URL. - * - * @see replaceFaviconData + * Stores the relation between a page URI and a favicon URI, whose icon data + * is provided through a data URL. + * The process is asynchronous and a callback with the status of the operation + * will be invoked at the end of it. * + * @param aPageURI + * URI of the page whose favicon is being set. * @param aFaviconURI - * URI of the favicon whose data is being set. + * URI of the favicon to associate with the page. * @param aDataURL - * string containing a data URL that represents the contents of - * the favicon to save + * String that represents a data URL to replace as the favicon content. * @param [optional] aExpiration * Time in microseconds since the epoch when this favicon expires. * Until this time, we won't try to load it again. - * @param [optional] aLoadingPrincipal - * Principal of the page whose favicon is being set. If this argument - * is omitted, the loadingPrincipal defaults to the nullPrincipal. - * @throws NS_ERROR_FAILURE - * Thrown if the favicon is overbloated and won't be saved to the db. + * If this argument is omitted, the expiration defaults to + * 7 days (FaviconHelpers::MAX_FAVICON_EXPIRATION) from now. + * @param [optional] aCallback + * Once we're done setting and/or fetching the favicon, we invoke this + * callback. + * + * @see PlacesCompletionCallback in PlacesCompletionCallback.idl. */ - void replaceFaviconDataFromDataURL(in nsIURI aFaviconURI, - in AString aDataURL, - [optional] in PRTime aExpiration, - [optional] in nsIPrincipal aLoadingPrincipal); + void setFaviconForPage( + in nsIURI aPageURI, + in nsIURI aFaviconURI, + in nsIURI aDataURL, + [optional] in PRTime aExpiration, + [optional] in PlacesCompletionCallback aCallback + ); /** * Retrieves the favicon URI associated to the given page, if any. diff --git a/toolkit/components/places/tests/PlacesTestUtils.sys.mjs b/toolkit/components/places/tests/PlacesTestUtils.sys.mjs index 4e459d2e32..97dda6367f 100644 --- a/toolkit/components/places/tests/PlacesTestUtils.sys.mjs +++ b/toolkit/components/places/tests/PlacesTestUtils.sys.mjs @@ -156,6 +156,43 @@ export var PlacesTestUtils = Object.freeze({ await Promise.all(faviconPromises); }, + /* + * Helper function to call PlacesUtils.favicons.setFaviconForPage() and waits + * finishing setting. This function throws an error if the status of + * PlacesUtils.favicons.setFaviconForPage() is not success. + * + * @param {string or nsIURI} pageURI + * @param {string or nsIURI} faviconURI + * @param {string or nsIURI} faviconDataURL + * @param {Number} [optional] expiration + * @return {Promise} waits for finishing setting + */ + setFaviconForPage(pageURI, faviconURI, faviconDataURL, expiration = 0) { + return new Promise((resolve, reject) => { + lazy.PlacesUtils.favicons.setFaviconForPage( + pageURI instanceof Ci.nsIURI ? pageURI : Services.io.newURI(pageURI), + faviconURI instanceof Ci.nsIURI + ? faviconURI + : Services.io.newURI(faviconURI), + faviconDataURL instanceof Ci.nsIURI + ? faviconDataURL + : Services.io.newURI(faviconDataURL), + expiration, + status => { + if (Components.isSuccessCode(status)) { + resolve(status); + } else { + reject( + new Error( + `Failed to process setFaviconForPage(): status code = ${status}` + ) + ); + } + } + ); + }); + }, + /** * Clears any favicons stored in the database. */ diff --git a/toolkit/components/places/tests/browser/browser.toml b/toolkit/components/places/tests/browser/browser.toml index b7d688c980..aceee48f24 100644 --- a/toolkit/components/places/tests/browser/browser.toml +++ b/toolkit/components/places/tests/browser/browser.toml @@ -117,3 +117,15 @@ support-files = [ "favicon.html", "final.html", ] + +["browser_visituri_restriction.js"] +skip-if = [ + "verify", + "os == 'linux' && (asan || tsan)", # Bug 1891145 +] + +["browser_visituri_restriction_origin.js"] +skip-if = [ + "verify", + "os == 'linux' && (asan || tsan)", # Bug 1891145 +] diff --git a/toolkit/components/places/tests/browser/browser_visituri_restriction.js b/toolkit/components/places/tests/browser/browser_visituri_restriction.js new file mode 100644 index 0000000000..2af41e6f51 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visituri_restriction.js @@ -0,0 +1,227 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const TEST_PAGE = `data:text/html, + <a href="https://example.com/" target="_blank">https://example.com</a> + <a href="http://example.com/" target="_blank">http://example.com</a> + <a href="https://www.example.com/" target="_blank">https://www.example.com</a> + <a href="https://example.org/" target="_blank">https://example.org</a>`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable restriction feature. + ["places.history.floodingPrevention.enabled", true], + // Restrict from the second visit. + ["places.history.floodingPrevention.restrictionCount", 1], + ["places.history.floodingPrevention.restrictionExpireSeconds", 4], + // Not apply flooding prevention until some seconds elapse after user + // interaction begins. + [ + "places.history.floodingPrevention.maxSecondsFromLastUserInteraction", + 4, + ], + // To enable UserActivation by EventUtils.synthesizeMouseAtCenter() in + // ContentTask.spawn() in synthesizeVisitByUser(). + ["test.events.async.enabled", true], + ], + }); + await clearHistoryAndHistoryCache(); +}); + +add_task(async function basic() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_PAGE }, + async browser => { + info("Sanity check"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://www.example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://example.org/", { + visitCount: 0, + isVisited: false, + }); + + info("Visit https://example.com/ by user"); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 1, + isVisited: true, + }); + + info( + "Visit https://example.com/ by script within maxSecondsFromLastUserInteraction" + ); + await synthesizeVisitByScript(browser, "https://example.com/"); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 3, + isVisited: true, + }); + + await waitForPrefSeconds("maxSecondsFromLastUserInteraction"); + + info( + "Visit https://example.com/ by script without maxSecondsFromLastUserInteraction" + ); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 4, + isVisited: true, + }); + + info("Visit again, but it should be restricted"); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 4, + isVisited: true, + }); + + info("Check other"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await assertLinkVisitedStatus(browser, "http://example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://www.example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://example.org/", { + visitCount: 0, + isVisited: false, + }); + } + ); + + await clearHistoryAndHistoryCache(); +}); + +add_task(async function expireRestriction() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_PAGE }, + async browser => { + info("Visit https://example.com/ by user"); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 1, + isVisited: true, + }); + + info( + "Visit https://example.com/ by script within maxSecondsFromLastUserInteraction" + ); + await synthesizeVisitByScript(browser, "https://example.com/"); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 3, + isVisited: true, + }); + + await waitForPrefSeconds("maxSecondsFromLastUserInteraction"); + + info( + "Visit https://example.com/ by script without maxSecondsFromLastUserInteraction" + ); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 4, + isVisited: true, + }); + + await waitForPrefSeconds("restrictionExpireSeconds"); + + info("Visit again, it should not be restricted"); + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 5, + isVisited: true, + }); + } + ); + + await clearHistoryAndHistoryCache(); +}); + +add_task(async function userInputAlwaysAcceptable() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_PAGE }, + async browser => { + info("Visit by user"); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 1, + isVisited: true, + }); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 2, + isVisited: true, + }); + + await waitForPrefSeconds("maxSecondsFromLastUserInteraction"); + + info("Visit by user input"); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 3, + isVisited: true, + }); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 4, + isVisited: true, + }); + } + ); + + await clearHistoryAndHistoryCache(); +}); + +add_task(async function disable() { + await SpecialPowers.pushPrefEnv({ + set: [["places.history.floodingPrevention.enabled", false]], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_PAGE }, + async browser => { + info("Any visits are stored"); + for (let i = 0; i < 3; i++) { + await synthesizeVisitByScript(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: i + 1, + isVisited: true, + }); + } + } + ); + + await clearHistoryAndHistoryCache(); +}); + +async function waitForPrefSeconds(pref) { + info(`Wait until elapsing ${pref}`); + return new Promise(r => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout( + r, + Services.prefs.getIntPref(`places.history.floodingPrevention.${pref}`) * + 1000 + + 100 + ) + ); +} diff --git a/toolkit/components/places/tests/browser/browser_visituri_restriction_origin.js b/toolkit/components/places/tests/browser/browser_visituri_restriction_origin.js new file mode 100644 index 0000000000..ee691c79e8 --- /dev/null +++ b/toolkit/components/places/tests/browser/browser_visituri_restriction_origin.js @@ -0,0 +1,92 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +"use strict"; + +const TEST_PAGE = `data:text/html, + <a href="https://example.com/" target="_blank">https://example.com</a> + <a href="http://example.com/" target="_blank">http://example.com</a> + <a href="https://www.example.com/" target="_blank">https://www.example.com</a> + <a href="https://example.org/" target="_blank">https://example.org</a>`; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable restriction feature. + ["places.history.floodingPrevention.enabled", true], + // Restrict from the second visit. + ["places.history.floodingPrevention.restrictionCount", 1], + // Stop expiring. + ["places.history.floodingPrevention.restrictionExpireSeconds", 100], + // Always appply flooding preveition. + [ + "places.history.floodingPrevention.maxSecondsFromLastUserInteraction", + 0, + ], + // To enable UserActivation by EventUtils.synthesizeMouseAtCenter() in ContentTask.spawn() in synthesizeVisitByUser(). + ["test.events.async.enabled", true], + ], + }); +}); + +add_task(async function same_origin_but_other() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: TEST_PAGE }, + async browser => { + info("Sanity check"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 0, + isVisited: false, + }); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await assertLinkVisitedStatus(browser, "http://example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://www.example.com/", { + visitCount: 0, + isVisited: false, + }); + await assertLinkVisitedStatus(browser, "https://example.org/", { + visitCount: 0, + isVisited: false, + }); + + info("Visit https://example.com/ by user"); + await synthesizeVisitByUser(browser, "https://example.com/"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 1, + isVisited: true, + }); + + info("Visit others by Scripts"); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await synthesizeVisitByScript(browser, "http://example.com/"); + await synthesizeVisitByScript(browser, "https://www.example.com/"); + await synthesizeVisitByScript(browser, "https://example.org/"); + + info("Check the status"); + await assertLinkVisitedStatus(browser, "https://example.com/", { + visitCount: 1, + isVisited: true, + }); + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + await assertLinkVisitedStatus(browser, "http://example.com/", { + visitCount: 0, + isVisited: true, + }); + await assertLinkVisitedStatus(browser, "https://www.example.com/", { + visitCount: 0, + isVisited: true, + }); + await assertLinkVisitedStatus(browser, "https://example.org/", { + visitCount: 1, + isVisited: true, + }); + } + ); + + await clearHistoryAndHistoryCache(); +}); diff --git a/toolkit/components/places/tests/browser/head.js b/toolkit/components/places/tests/browser/head.js index b1b916ee29..da40a832c6 100644 --- a/toolkit/components/places/tests/browser/head.js +++ b/toolkit/components/places/tests/browser/head.js @@ -17,3 +17,80 @@ function whenNewWindowLoaded(aOptions, aCallback) { BrowserTestUtils.waitForNewWindow().then(aCallback); OpenBrowserWindow(aOptions); } + +async function clearHistoryAndHistoryCache() { + await PlacesUtils.history.clear(); + // Clear HistoryRestiction cache as well. + Cc["@mozilla.org/browser/history;1"] + .getService(Ci.mozIAsyncHistory) + .clearCache(); +} + +async function synthesizeVisitByUser(browser, url) { + let onNewTab = BrowserTestUtils.waitForNewTab(browser.ownerGlobal.gBrowser); + // We intentionally turn off this a11y check, because the following click is + // purposefully sent on an arbitrary web content that is not expected to be + // tested by itself with the browser mochitests, therefore this rule check + // shall be ignored by a11y_checks suite. + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + await ContentTask.spawn(browser, [url], async ([href]) => { + EventUtils.synthesizeMouseAtCenter( + content.document.querySelector(`a[href='${href}'`), + {}, + content + ); + }); + AccessibilityUtils.resetEnv(); + let tab = await onNewTab; + BrowserTestUtils.removeTab(tab); +} + +async function synthesizeVisitByScript(browser, url) { + let onNewTab = BrowserTestUtils.waitForNewTab(browser.ownerGlobal.gBrowser); + AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false }); + await ContentTask.spawn(browser, [url], async ([href]) => { + let a = content.document.querySelector(`a[href='${href}'`); + a.click(); + }); + AccessibilityUtils.resetEnv(); + let tab = await onNewTab; + BrowserTestUtils.removeTab(tab); +} + +async function assertLinkVisitedStatus( + browser, + url, + { visitCount: expectedVisitCount, isVisited: expectedVisited } +) { + await BrowserTestUtils.waitForCondition(async () => { + let visitCount = + (await PlacesTestUtils.getDatabaseValue("moz_places", "visit_count", { + url, + })) ?? 0; + + if (visitCount != expectedVisitCount) { + return false; + } + + Assert.equal(visitCount, expectedVisitCount, "The visit count is correct"); + return true; + }); + + await ContentTask.spawn( + browser, + [url, expectedVisited], + async ([href, visited]) => { + // ElementState::VISITED + const VISITED_STATE = 1 << 18; + await ContentTaskUtils.waitForCondition(() => { + let isVisited = !!( + content.InspectorUtils.getContentState( + content.document.querySelector(`a[href='${href}']`) + ) & VISITED_STATE + ); + return isVisited == visited; + }); + } + ); + Assert.ok(true, "The visited state is corerct"); +} diff --git a/toolkit/components/places/tests/chrome/test_cached_favicon.xhtml b/toolkit/components/places/tests/chrome/test_cached_favicon.xhtml index f7e7f4f1d8..1046574bdf 100644 --- a/toolkit/components/places/tests/chrome/test_cached_favicon.xhtml +++ b/toolkit/components/places/tests/chrome/test_cached_favicon.xhtml @@ -104,19 +104,12 @@ function test() // icon with a page explicitly in order for it to be visible through // the protocol. info("Replace favicon data"); - var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"] - .createInstance(Ci.nsIPrincipal); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - Services.io.newURI(tests[1].url), - tests[1].expectedIcon, - (Date.now() + 86400) * 1000, - systemPrincipal); - info("Set favicon data"); - PlacesUtils.favicons.setAndFetchFaviconForPage( + PlacesUtils.favicons.setFaviconForPage( Services.io.newURI("https://example.com/favicon_annotations"), Services.io.newURI(tests[1].url), - true, PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null, - systemPrincipal); + Services.io.newURI(tests[1].expectedIcon), + (Date.now() + 86400) * 1000, + ); // And start our test process. loadNextTest(); diff --git a/toolkit/components/places/tests/expiration/test_debug_expiration.js b/toolkit/components/places/tests/expiration/test_debug_expiration.js index 204295d46c..22657e6b24 100644 --- a/toolkit/components/places/tests/expiration/test_debug_expiration.js +++ b/toolkit/components/places/tests/expiration/test_debug_expiration.js @@ -356,13 +356,7 @@ add_task(async function test_expire_icons() { } if (entry.icon) { - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - Services.io.newURI(entry.icon), - dataUrl, - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - await PlacesTestUtils.addFavicons(new Map([[entry.page, entry.icon]])); + await PlacesTestUtils.setFaviconForPage(entry.page, entry.icon, dataUrl); Assert.equal( await getFaviconUrlForPage(entry.page), entry.icon, @@ -380,13 +374,7 @@ add_task(async function test_expire_icons() { } if (entry.root) { - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - Services.io.newURI(entry.root), - dataUrl, - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - await PlacesTestUtils.addFavicons(new Map([[entry.page, entry.root]])); + await PlacesTestUtils.setFaviconForPage(entry.page, entry.root, dataUrl); } if (entry.iconExpired) { diff --git a/toolkit/components/places/tests/favicons/head_favicons.js b/toolkit/components/places/tests/favicons/head_favicons.js index afd2c4924f..73dd2a61ed 100644 --- a/toolkit/components/places/tests/favicons/head_favicons.js +++ b/toolkit/components/places/tests/favicons/head_favicons.js @@ -15,6 +15,9 @@ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); +// Used in createFavicon(). +let uniqueFaviconId = 0; + /** * Checks that the favicon for the given page matches the provided data. * @@ -24,6 +27,7 @@ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); * Expected MIME type of the icon, for example "image/png". * @param aExpectedData * Expected icon data, expressed as an array of byte values. + * If set null, skip the test for the favicon data. * @param aCallback * This function is called after the check finished. */ @@ -37,7 +41,9 @@ function checkFaviconDataForPage( aPageURI, async function (aURI, aDataLen, aData, aMimeType) { Assert.equal(aExpectedMimeType, aMimeType); - Assert.ok(compareArrays(aExpectedData, aData)); + if (aExpectedData) { + Assert.ok(compareArrays(aExpectedData, aData)); + } await check_guid_for_uri(aPageURI); aCallback(); } @@ -76,3 +82,82 @@ function promiseFaviconChanged(aExpectedPageURI, aExpectedFaviconURI) { }); }); } + +/** + * Create favicon file to temp directory. + * + * @param {string} aFileName + * File name that will be created in temp directory. + * @returns {object} + * { + * file: nsIFile, + * uri: nsIURI, + * data: byte Array, + * mimetype: String, + * } + */ +async function createFavicon(aFileName) { + // Copy the favicon file we have to the specified file in temp directory. + let originalFaviconFile = do_get_file("favicon-normal16.png"); + let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + let faviconFile = tempDir.clone(); + faviconFile.append(aFileName); + await IOUtils.copy(originalFaviconFile.path, faviconFile.path); + + // Append some data that sniffers/encoders will ignore that will distinguish + // the different favicons we'll create. + let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + const WRONLY_PERMISSION = 0o600; + stream.init( + faviconFile, + FileUtils.MODE_WRONLY | FileUtils.MODE_APPEND, + WRONLY_PERMISSION, + 0 + ); + uniqueFaviconId++; + let uniqueStr = "uid:" + uniqueFaviconId; + stream.write(uniqueStr, uniqueStr.length); + stream.close(); + + Assert.equal(faviconFile.leafName.substr(0, aFileName.length), aFileName); + + return { + file: faviconFile, + uri: uri(faviconFile), + data: readFileData(faviconFile), + mimeType: "image/png", + }; +} + +/** + * Create nsIURI for given favicon object. + * + * @param {object} aFavicon + * Favicon object created by createFavicon(). + * @returns {nsIURI} + */ +async function createDataURLForFavicon(aFavicon) { + let dataURL = await toDataURL(aFavicon.data, aFavicon.mimeType); + return uri(dataURL); +} + +/** + * Create data URL string from given byte array and type. + * + * @param {Array} data + * Byte array. + * @param {string} type + * The type of this data. + * @returns {string} + */ +function toDataURL(data, type) { + let blob = new Blob([new Uint8Array(data)], { type }); + return new Promise((resolve, reject) => { + let reader = new FileReader(); + reader.addEventListener("load", () => resolve(reader.result)); + reader.addEventListener("error", reject); + reader.readAsDataURL(blob); + }); +} diff --git a/toolkit/components/places/tests/favicons/test_cached-favicon_mime_type.js b/toolkit/components/places/tests/favicons/test_cached-favicon_mime_type.js index c1f6689a70..52eb81ae85 100644 --- a/toolkit/components/places/tests/favicons/test_cached-favicon_mime_type.js +++ b/toolkit/components/places/tests/favicons/test_cached-favicon_mime_type.js @@ -68,14 +68,12 @@ add_task(async function () { info("Test that the content type of a favicon we add is correct."); let testURI = uri("http://mozilla.org/"); // Add the data before opening - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.addVisits(testURI); + await PlacesTestUtils.setFaviconForPage( + testURI, testIconURI, - testFaviconData, - 0, - systemPrincipal + testFaviconData ); - await PlacesTestUtils.addVisits(testURI); - await setFaviconForPage(testURI, testIconURI); // Open the channel let channel = NetUtil.newChannel({ uri: PlacesUtils.favicons.getFaviconLinkForIcon(testIconURI).spec, @@ -120,14 +118,12 @@ add_task(async function test_userpass() { CACHED_ICON_NORMAL, CACHED_ICON_USERPASS, ]) { - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.addVisits(pageURI); + await PlacesTestUtils.setFaviconForPage( + pageURI, iconURI, - testFaviconData, - 0, - systemPrincipal + testFaviconData ); - await PlacesTestUtils.addVisits(pageURI); - await setFaviconForPage(pageURI, iconURI); // Open the channel let channel = NetUtil.newChannel({ diff --git a/toolkit/components/places/tests/favicons/test_expire_on_new_icons.js b/toolkit/components/places/tests/favicons/test_expire_on_new_icons.js index d5a7c42ba3..f0089d737b 100644 --- a/toolkit/components/places/tests/favicons/test_expire_on_new_icons.js +++ b/toolkit/components/places/tests/favicons/test_expire_on_new_icons.js @@ -28,23 +28,23 @@ add_task(async function test_expire_associated() { ]; for (let icon of favicons) { - let data = readFileData(do_get_file(icon.name)); - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(TEST_URL + icon.name), - data, + let dataURL = await readFileDataAsDataURL( + do_get_file(icon.name), icon.mimeType ); - await setFaviconForPage(TEST_URL, TEST_URL + icon.name); + await PlacesTestUtils.setFaviconForPage( + TEST_URL, + TEST_URL + icon.name, + dataURL + ); if (icon.expired) { await expireIconRelationsForPage(TEST_URL); // Add the same icon to another page. - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(TEST_URL + icon.name), - data, - icon.mimeType, - icon.expire + await PlacesTestUtils.setFaviconForPage( + TEST_URL2, + TEST_URL + icon.name, + dataURL ); - await setFaviconForPage(TEST_URL2, TEST_URL + icon.name); } } @@ -88,13 +88,7 @@ add_task(async function test_expire_root() { // Insert an expired icon. let iconURI = NetUtil.newURI(pageURI.spec + "favicon-normal16.png"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - iconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal - ); - await setFaviconForPage(pageURI, iconURI); + await PlacesTestUtils.setFaviconForPage(pageURI, iconURI, SMALLPNG_DATA_URI); Assert.equal( await countEntries("moz_icons_to_pages"), 1, @@ -105,13 +99,11 @@ add_task(async function test_expire_root() { // Now insert a new root icon. let rootIconURI = NetUtil.newURI(pageURI.spec + "favicon.ico"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + pageURI, rootIconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal + SMALLPNG_DATA_URI ); - await setFaviconForPage(pageURI, rootIconURI); // Only the root icon should have survived. Assert.equal( diff --git a/toolkit/components/places/tests/favicons/test_favicons_conversions.js b/toolkit/components/places/tests/favicons/test_favicons_conversions.js index 28a0fffb7f..dd8a35c5ac 100644 --- a/toolkit/components/places/tests/favicons/test_favicons_conversions.js +++ b/toolkit/components/places/tests/favicons/test_favicons_conversions.js @@ -43,29 +43,24 @@ async function checkFaviconDataConversion( }); let faviconURI = NetUtil.newURI("http://places.test/icon/" + aFileName); let fileData = readFileOfLength(aFileName, aFileLength); + let fileDataURL = await fileDataToDataURL(fileData, aFileMimeType); + await PlacesTestUtils.setFaviconForPage( + pageURI.spec, + faviconURI.spec, + fileDataURL + ); - PlacesUtils.favicons.replaceFaviconData(faviconURI, fileData, aFileMimeType); await new Promise(resolve => { - PlacesUtils.favicons.setAndFetchFaviconForPage( - pageURI, - faviconURI, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - (aURI, aDataLen, aData, aMimeType) => { - if (!aExpectConversion) { - Assert.ok(compareArrays(aData, fileData)); - Assert.equal(aMimeType, aFileMimeType); - } else { - if (!aVaryOnWindows || !isWindows) { - let expectedFile = do_get_file("expected-" + aFileName + ".png"); - Assert.ok(compareArrays(aData, readFileData(expectedFile))); - } - Assert.equal(aMimeType, "image/png"); - } - resolve(); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); + if (!aExpectConversion) { + checkFaviconDataForPage(pageURI, aFileMimeType, fileData, resolve); + } else if (!aVaryOnWindows || !isWindows) { + let expectedFile = do_get_file("expected-" + aFileName + ".png"); + let expectedData = readFileData(expectedFile); + checkFaviconDataForPage(pageURI, "image/png", expectedData, resolve); + } else { + // Not check the favicon data. + checkFaviconDataForPage(pageURI, "image/png", null, resolve); + } }); } diff --git a/toolkit/components/places/tests/favicons/test_favicons_protocols_ref.js b/toolkit/components/places/tests/favicons/test_favicons_protocols_ref.js index aa0241a3d2..4d359f7307 100644 --- a/toolkit/components/places/tests/favicons/test_favicons_protocols_ref.js +++ b/toolkit/components/places/tests/favicons/test_favicons_protocols_ref.js @@ -9,21 +9,16 @@ const ICON32_URL = "http://places.test/favicon-normal32.png"; add_task(async function () { await PlacesTestUtils.addVisits(PAGE_URL); // Add 2 differently sized favicons for this page. - - let data = readFileData(do_get_file("favicon-normal16.png")); - PlacesUtils.favicons.replaceFaviconData( - Services.io.newURI(ICON16_URL), - data, + let dataURL16 = await readFileDataAsDataURL( + do_get_file("favicon-normal16.png"), "image/png" ); - await setFaviconForPage(PAGE_URL, ICON16_URL); - data = readFileData(do_get_file("favicon-normal32.png")); - PlacesUtils.favicons.replaceFaviconData( - Services.io.newURI(ICON32_URL), - data, + await PlacesTestUtils.setFaviconForPage(PAGE_URL, ICON16_URL, dataURL16); + let dataURL32 = await readFileDataAsDataURL( + do_get_file("favicon-normal32.png"), "image/png" ); - await setFaviconForPage(PAGE_URL, ICON32_URL); + await PlacesTestUtils.setFaviconForPage(PAGE_URL, ICON32_URL, dataURL32); const PAGE_ICON_URL = "page-icon:" + PAGE_URL; diff --git a/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js index 80f498f33f..8c6fb0a2bb 100644 --- a/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js +++ b/toolkit/components/places/tests/favicons/test_getFaviconDataForPage.js @@ -60,12 +60,8 @@ add_task(async function test_fallback() { info("Set icon for the root"); await PlacesTestUtils.addVisits(ROOT_URL); let data = readFileData(do_get_file("favicon-normal16.png")); - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(ROOT_ICON_URL), - data, - "image/png" - ); - await setFaviconForPage(ROOT_URL, ROOT_ICON_URL); + let dataURL = await fileDataToDataURL(data, "image/png"); + await PlacesTestUtils.setFaviconForPage(ROOT_URL, ROOT_ICON_URL, dataURL); info("check fallback icons"); await new Promise(resolve => { @@ -96,12 +92,8 @@ add_task(async function test_fallback() { info("Now add a proper icon for the page"); await PlacesTestUtils.addVisits(SUBPAGE_URL); let data32 = readFileData(do_get_file("favicon-normal32.png")); - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(ICON32_URL), - data32, - "image/png" - ); - await setFaviconForPage(SUBPAGE_URL, ICON32_URL); + let dataURL32 = await fileDataToDataURL(data32, "image/png"); + await PlacesTestUtils.setFaviconForPage(SUBPAGE_URL, ICON32_URL, dataURL32); info("check no fallback icons"); await new Promise(resolve => { diff --git a/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js index e8f459cb08..3fa11ae6c0 100644 --- a/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js +++ b/toolkit/components/places/tests/favicons/test_getFaviconURLForPage.js @@ -57,13 +57,11 @@ add_task(async function test_fallback() { info("Set icon for the root"); await PlacesTestUtils.addVisits(ROOT_URL); - let data = readFileData(do_get_file("favicon-normal16.png")); - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(ROOT_ICON_URL), - data, + let dataURL = await readFileDataAsDataURL( + do_get_file("favicon-normal16.png"), "image/png" ); - await setFaviconForPage(ROOT_URL, ROOT_ICON_URL); + await PlacesTestUtils.setFaviconForPage(ROOT_URL, ROOT_ICON_URL, dataURL); info("check fallback icons"); Assert.equal( @@ -79,13 +77,11 @@ add_task(async function test_fallback() { info("Now add a proper icon for the page"); await PlacesTestUtils.addVisits(SUBPAGE_URL); - let data32 = readFileData(do_get_file("favicon-normal32.png")); - PlacesUtils.favicons.replaceFaviconData( - NetUtil.newURI(ICON32_URL), - data32, + let dataURL32 = await readFileDataAsDataURL( + do_get_file("favicon-normal32.png"), "image/png" ); - await setFaviconForPage(SUBPAGE_URL, ICON32_URL); + await PlacesTestUtils.setFaviconForPage(SUBPAGE_URL, ICON32_URL, dataURL32); info("check no fallback icons"); Assert.equal( diff --git a/toolkit/components/places/tests/favicons/test_heavy_favicon.js b/toolkit/components/places/tests/favicons/test_heavy_favicon.js index 09adcaf6fa..4541de9bff 100644 --- a/toolkit/components/places/tests/favicons/test_heavy_favicon.js +++ b/toolkit/components/places/tests/favicons/test_heavy_favicon.js @@ -26,8 +26,8 @@ add_task(async function () { let pageURI = uri("http://foo.bar/"); await PlacesTestUtils.addVisits(pageURI); - PlacesUtils.favicons.replaceFaviconData(icon.uri, icon.data, icon.mimetype); - await setFaviconForPage(pageURI, icon.uri); + let dataURI = await fileDataToDataURL(icon.data, icon.mimetype); + await PlacesTestUtils.setFaviconForPage(pageURI.spec, icon.uri.spec, dataURI); Assert.equal( await getFaviconUrlForPage(pageURI), icon.uri.spec, diff --git a/toolkit/components/places/tests/favicons/test_incremental_vacuum.js b/toolkit/components/places/tests/favicons/test_incremental_vacuum.js index ab93121d47..2c1a5ad3f9 100644 --- a/toolkit/components/places/tests/favicons/test_incremental_vacuum.js +++ b/toolkit/components/places/tests/favicons/test_incremental_vacuum.js @@ -16,10 +16,9 @@ add_task(async function () { let url = "http://foo.bar/"; await PlacesTestUtils.addVisits(url); for (let i = 0; i < 10; ++i) { - let iconUri = NetUtil.newURI("http://mozilla.org/" + i); - let data = readFileData(icon.file); - PlacesUtils.favicons.replaceFaviconData(iconUri, data, icon.mimetype); - await setFaviconForPage(url, iconUri); + let iconUri = "http://mozilla.org/" + i; + let dataURL = await readFileDataAsDataURL(icon.file, icon.mimetype); + await PlacesTestUtils.setFaviconForPage(url, iconUri, dataURL); } let promise = TestUtils.topicObserved("places-favicons-expired"); diff --git a/toolkit/components/places/tests/favicons/test_multiple_frames.js b/toolkit/components/places/tests/favicons/test_multiple_frames.js index 5c7f585715..a6aae572de 100644 --- a/toolkit/components/places/tests/favicons/test_multiple_frames.js +++ b/toolkit/components/places/tests/favicons/test_multiple_frames.js @@ -14,9 +14,15 @@ add_task(async function () { let faviconURI = NetUtil.newURI("http://places.test/icon/favicon-multi.ico"); // Fake window. let win = { devicePixelRatio: 1.0 }; - let icoData = readFileData(do_get_file("favicon-multi.ico")); - PlacesUtils.favicons.replaceFaviconData(faviconURI, icoData, "image/x-icon"); - await setFaviconForPage(pageURI, faviconURI); + let icoDataURL = await readFileDataAsDataURL( + do_get_file("favicon-multi.ico"), + "image/x-icon" + ); + await PlacesTestUtils.setFaviconForPage( + pageURI.spec, + faviconURI.spec, + icoDataURL + ); for (let size of [16, 32, 64]) { let file = do_get_file(`favicon-multi-frame${size}.png`); diff --git a/toolkit/components/places/tests/favicons/test_page-icon_protocol.js b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js index 287484868f..2241dec990 100644 --- a/toolkit/components/places/tests/favicons/test_page-icon_protocol.js +++ b/toolkit/components/places/tests/favicons/test_page-icon_protocol.js @@ -76,25 +76,12 @@ var gFavicon; add_task(async function setup() { await PlacesTestUtils.addVisits(TEST_URI); - - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + TEST_URI, ICON_URI, ICON_DATAURL, - (Date.now() + 8640000) * 1000, - Services.scriptSecurityManager.getSystemPrincipal() + (Date.now() + 8640000) * 1000 ); - - await new Promise(resolve => { - PlacesUtils.favicons.setAndFetchFaviconForPage( - TEST_URI, - ICON_URI, - false, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - resolve, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - gDefaultFavicon = await fetchIconForSpec( PlacesUtils.favicons.defaultFavicon.spec ); @@ -133,13 +120,11 @@ add_task(async function subpage_url_fallback() { add_task(async function svg_icon() { let faviconURI = NetUtil.newURI("http://places.test/favicon.svg"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + TEST_URI, faviconURI, - SMALLSVG_DATA_URI.spec, - 0, - Services.scriptSecurityManager.getSystemPrincipal() + SMALLSVG_DATA_URI ); - await setFaviconForPage(TEST_URI, faviconURI); let svgIcon = await fetchIconForSpec(SMALLSVG_DATA_URI.spec); info(svgIcon.contentType); let pageIcon = await fetchIconForSpec("page-icon:" + TEST_URI.spec); @@ -270,23 +255,8 @@ add_task(async function test_with_user_pass() { for (const { pageURI, iconURI } of testData) { for (const loadingIconURISpec of [PAGE_ICON_NORMAL, PAGE_ICON_USERPASS]) { - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - iconURI, - ICON_DATAURL, - 0, - systemPrincipal - ); await PlacesTestUtils.addVisits(pageURI); - await new Promise(resolve => { - PlacesUtils.favicons.setAndFetchFaviconForPage( - pageURI, - iconURI, - false, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - resolve, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); + await PlacesTestUtils.setFaviconForPage(pageURI, iconURI, ICON_DATAURL); let { data, contentType } = await fetchIconForSpec(loadingIconURISpec); Assert.equal(contentType, gFavicon.contentType); diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js deleted file mode 100644 index 2e9835eaa9..0000000000 --- a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js +++ /dev/null @@ -1,395 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -/* - * Tests for replaceFaviconData() - */ - -var iconsvc = PlacesUtils.favicons; - -var originalFavicon = { - file: do_get_file("favicon-normal16.png"), - uri: uri(do_get_file("favicon-normal16.png")), - data: readFileData(do_get_file("favicon-normal16.png")), - mimetype: "image/png", -}; - -var uniqueFaviconId = 0; -function createFavicon(fileName) { - let tempdir = Services.dirsvc.get("TmpD", Ci.nsIFile); - - // remove any existing file at the path we're about to copy to - let outfile = tempdir.clone(); - outfile.append(fileName); - try { - outfile.remove(false); - } catch (e) {} - - originalFavicon.file.copyToFollowingLinks(tempdir, fileName); - - let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( - Ci.nsIFileOutputStream - ); - stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0); - - // append some data that sniffers/encoders will ignore that will distinguish - // the different favicons we'll create - uniqueFaviconId++; - let uniqueStr = "uid:" + uniqueFaviconId; - stream.write(uniqueStr, uniqueStr.length); - stream.close(); - - Assert.equal(outfile.leafName.substr(0, fileName.length), fileName); - - return { - file: outfile, - uri: uri(outfile), - data: readFileData(outfile), - mimetype: "image/png", - }; -} - -function checkCallbackSucceeded( - callbackMimetype, - callbackData, - sourceMimetype, - sourceData -) { - Assert.equal(callbackMimetype, sourceMimetype); - Assert.ok(compareArrays(callbackData, sourceData)); -} - -function run_test() { - // check that the favicon loaded correctly - Assert.equal(originalFavicon.data.length, 286); - run_next_test(); -} - -add_task(async function test_replaceFaviconData_validHistoryURI() { - info("test replaceFaviconData for valid history uri"); - - let pageURI = uri("http://test1.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let favicon = createFavicon("favicon1.png"); - - iconsvc.replaceFaviconData(favicon.uri, favicon.data, favicon.mimetype); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - favicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconData_validHistoryURI_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - dump("GOT " + aMimeType + "\n"); - checkCallbackSucceeded( - aMimeType, - aData, - favicon.mimetype, - favicon.data - ); - checkFaviconDataForPage( - pageURI, - favicon.mimetype, - favicon.data, - function test_replaceFaviconData_validHistoryURI_callback() { - favicon.file.remove(false); - resolve(); - } - ); - }, - systemPrincipal - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_overrideDefaultFavicon() { - info("test replaceFaviconData to override a later setAndFetchFaviconForPage"); - - let pageURI = uri("http://test2.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon2.png"); - let secondFavicon = createFavicon("favicon3.png"); - - iconsvc.replaceFaviconData( - firstFavicon.uri, - secondFavicon.data, - secondFavicon.mimetype - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconData_overrideDefaultFavicon_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconData_overrideDefaultFavicon_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - }, - systemPrincipal - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_replaceExisting() { - info( - "test replaceFaviconData to override a previous setAndFetchFaviconForPage" - ); - - let pageURI = uri("http://test3.bar"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon4.png"); - let secondFavicon = createFavicon("favicon5.png"); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconData_replaceExisting_firstSet_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - firstFavicon.mimetype, - firstFavicon.data - ); - checkFaviconDataForPage( - pageURI, - firstFavicon.mimetype, - firstFavicon.data, - function test_replaceFaviconData_overrideDefaultFavicon_firstCallback() { - iconsvc.replaceFaviconData( - firstFavicon.uri, - secondFavicon.data, - secondFavicon.mimetype - ); - PlacesTestUtils.promiseAsyncUpdates().then(() => { - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconData_overrideDefaultFavicon_secondCallback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - }, - systemPrincipal - ); - }); - } - ); - }, - systemPrincipal - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_unrelatedReplace() { - info("test replaceFaviconData to not make unrelated changes"); - - let pageURI = uri("http://test4.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let favicon = createFavicon("favicon6.png"); - let unrelatedFavicon = createFavicon("favicon7.png"); - - iconsvc.replaceFaviconData( - unrelatedFavicon.uri, - unrelatedFavicon.data, - unrelatedFavicon.mimetype - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - favicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconData_unrelatedReplace_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - favicon.mimetype, - favicon.data - ); - checkFaviconDataForPage( - pageURI, - favicon.mimetype, - favicon.data, - function test_replaceFaviconData_unrelatedReplace_callback() { - favicon.file.remove(false); - unrelatedFavicon.file.remove(false); - resolve(); - } - ); - }, - systemPrincipal - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_badInputs() { - info("test replaceFaviconData to throw on bad inputs"); - let icon = createFavicon("favicon8.png"); - - Assert.throws( - () => iconsvc.replaceFaviconData(icon.uri, icon.data, ""), - /NS_ERROR_ILLEGAL_VALUE/ - ); - Assert.throws( - () => iconsvc.replaceFaviconData(icon.uri, icon.data, "not-an-image"), - /NS_ERROR_ILLEGAL_VALUE/ - ); - Assert.throws( - () => iconsvc.replaceFaviconData(null, icon.data, icon.mimetype), - /NS_ERROR_ILLEGAL_VALUE/ - ); - Assert.throws( - () => iconsvc.replaceFaviconData(icon.uri, [], icon.mimetype), - /NS_ERROR_ILLEGAL_VALUE/ - ); - Assert.throws( - () => iconsvc.replaceFaviconData(icon.uri, null, icon.mimetype), - /NS_ERROR_XPC_CANT_CONVERT_PRIMITIVE_TO_ARRAY/ - ); - - icon.file.remove(false); - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_twiceReplace() { - info("test replaceFaviconData on multiple replacements"); - - let pageURI = uri("http://test5.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon9.png"); - let secondFavicon = createFavicon("favicon10.png"); - - iconsvc.replaceFaviconData( - firstFavicon.uri, - firstFavicon.data, - firstFavicon.mimetype - ); - iconsvc.replaceFaviconData( - firstFavicon.uri, - secondFavicon.data, - secondFavicon.mimetype - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconData_twiceReplace_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconData_twiceReplace_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - }, - systemPrincipal - ); - }, - systemPrincipal - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconData_rootOverwrite() { - info("test replaceFaviconData doesn't overwrite root = 1"); - - async function getRootValue(url) { - let db = await PlacesUtils.promiseDBConnection(); - let rows = await db.execute( - "SELECT root FROM moz_icons WHERE icon_url = :url", - { url } - ); - return rows[0].getResultByName("root"); - } - - const PAGE_URL = "http://rootoverwrite.bar/"; - let pageURI = Services.io.newURI(PAGE_URL); - const ICON_URL = "http://rootoverwrite.bar/favicon.ico"; - let iconURI = Services.io.newURI(ICON_URL); - - await PlacesTestUtils.addVisits(pageURI); - - let icon = createFavicon("favicon9.png"); - PlacesUtils.favicons.replaceFaviconData(iconURI, icon.data, icon.mimetype); - await PlacesTestUtils.addFavicons(new Map([[PAGE_URL, ICON_URL]])); - - Assert.equal(await getRootValue(ICON_URL), 1, "Check root == 1"); - let icon2 = createFavicon("favicon10.png"); - PlacesUtils.favicons.replaceFaviconData(iconURI, icon2.data, icon2.mimetype); - // replaceFaviconData doesn't have a callback, but we must wait its updated. - await PlacesTestUtils.promiseAsyncUpdates(); - Assert.equal(await getRootValue(ICON_URL), 1, "Check root did not change"); - - await PlacesUtils.history.clear(); -}); diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js deleted file mode 100644 index c1b83fc8a7..0000000000 --- a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js +++ /dev/null @@ -1,537 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -/* - * Tests for replaceFaviconData() - */ - -var iconsvc = PlacesUtils.favicons; - -var originalFavicon = { - file: do_get_file("favicon-normal16.png"), - uri: uri(do_get_file("favicon-normal16.png")), - data: readFileData(do_get_file("favicon-normal16.png")), - mimetype: "image/png", -}; - -var uniqueFaviconId = 0; -function createFavicon(fileName) { - let tempdir = Services.dirsvc.get("TmpD", Ci.nsIFile); - - // remove any existing file at the path we're about to copy to - let outfile = tempdir.clone(); - outfile.append(fileName); - try { - outfile.remove(false); - } catch (e) {} - - originalFavicon.file.copyToFollowingLinks(tempdir, fileName); - - let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( - Ci.nsIFileOutputStream - ); - stream.init(outfile, 0x02 | 0x08 | 0x10, 0o600, 0); - - // append some data that sniffers/encoders will ignore that will distinguish - // the different favicons we'll create - uniqueFaviconId++; - let uniqueStr = "uid:" + uniqueFaviconId; - stream.write(uniqueStr, uniqueStr.length); - stream.close(); - - Assert.equal(outfile.leafName.substr(0, fileName.length), fileName); - - return { - file: outfile, - uri: uri(outfile), - data: readFileData(outfile), - mimetype: "image/png", - }; -} - -function createDataURLForFavicon(favicon) { - return "data:" + favicon.mimetype + ";base64," + toBase64(favicon.data); -} - -function checkCallbackSucceeded( - callbackMimetype, - callbackData, - sourceMimetype, - sourceData -) { - Assert.equal(callbackMimetype, sourceMimetype); - Assert.ok(compareArrays(callbackData, sourceData)); -} - -function run_test() { - // check that the favicon loaded correctly - Assert.equal(originalFavicon.data.length, 286); - run_next_test(); -} - -add_task(async function test_replaceFaviconDataFromDataURL_validHistoryURI() { - info("test replaceFaviconDataFromDataURL for valid history uri"); - - let pageURI = uri("http://test1.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let favicon = createFavicon("favicon1.png"); - iconsvc.replaceFaviconDataFromDataURL( - favicon.uri, - createDataURLForFavicon(favicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - favicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_validHistoryURI_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - favicon.mimetype, - favicon.data - ); - checkFaviconDataForPage( - pageURI, - favicon.mimetype, - favicon.data, - function test_replaceFaviconDataFromDataURL_validHistoryURI_callback() { - favicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task( - async function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() { - info( - "test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage" - ); - - let pageURI = uri("http://test2.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon2.png"); - let secondFavicon = createFavicon("favicon3.png"); - - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(secondFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); - } -); - -add_task(async function test_replaceFaviconDataFromDataURL_replaceExisting() { - info( - "test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage" - ); - - let pageURI = uri("http://test3.bar"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon4.png"); - let secondFavicon = createFavicon("favicon5.png"); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_replaceExisting_firstSet_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - firstFavicon.mimetype, - firstFavicon.data - ); - checkFaviconDataForPage( - pageURI, - firstFavicon.mimetype, - firstFavicon.data, - function test_replaceFaviconDataFromDataURL_replaceExisting_firstCallback() { - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(secondFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconDataFromDataURL_replaceExisting_secondCallback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconDataFromDataURL_unrelatedReplace() { - info("test replaceFaviconDataFromDataURL to not make unrelated changes"); - - let pageURI = uri("http://test4.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let favicon = createFavicon("favicon6.png"); - let unrelatedFavicon = createFavicon("favicon7.png"); - - iconsvc.replaceFaviconDataFromDataURL( - unrelatedFavicon.uri, - createDataURLForFavicon(unrelatedFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - favicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_unrelatedReplace_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - favicon.mimetype, - favicon.data - ); - checkFaviconDataForPage( - pageURI, - favicon.mimetype, - favicon.data, - function test_replaceFaviconDataFromDataURL_unrelatedReplace_callback() { - favicon.file.remove(false); - unrelatedFavicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconDataFromDataURL_badInputs() { - info("test replaceFaviconDataFromDataURL to throw on bad inputs"); - - let favicon = createFavicon("favicon8.png"); - - let ex = null; - try { - iconsvc.replaceFaviconDataFromDataURL( - favicon.uri, - "", - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - } catch (e) { - ex = e; - } finally { - Assert.ok(!!ex); - } - - ex = null; - try { - iconsvc.replaceFaviconDataFromDataURL( - null, - createDataURLForFavicon(favicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - } catch (e) { - ex = e; - } finally { - Assert.ok(!!ex); - } - - favicon.file.remove(false); - - await PlacesUtils.history.clear(); -}); - -add_task(async function test_replaceFaviconDataFromDataURL_twiceReplace() { - info("test replaceFaviconDataFromDataURL on multiple replacements"); - - let pageURI = uri("http://test5.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon9.png"); - let secondFavicon = createFavicon("favicon10.png"); - - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(firstFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(secondFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_twiceReplace_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconDataFromDataURL_twiceReplace_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); -}); - -add_task( - async function test_replaceFaviconDataFromDataURL_afterRegularAssign() { - info("test replaceFaviconDataFromDataURL after replaceFaviconData"); - - let pageURI = uri("http://test6.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon11.png"); - let secondFavicon = createFavicon("favicon12.png"); - - iconsvc.replaceFaviconData( - firstFavicon.uri, - firstFavicon.data, - firstFavicon.mimetype - ); - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(secondFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_afterRegularAssign_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconDataFromDataURL_afterRegularAssign_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); - } -); - -add_task( - async function test_replaceFaviconDataFromDataURL_beforeRegularAssign() { - info("test replaceFaviconDataFromDataURL before replaceFaviconData"); - - let pageURI = uri("http://test7.bar/"); - await PlacesTestUtils.addVisits(pageURI); - - let firstFavicon = createFavicon("favicon13.png"); - let secondFavicon = createFavicon("favicon14.png"); - - iconsvc.replaceFaviconDataFromDataURL( - firstFavicon.uri, - createDataURLForFavicon(firstFavicon), - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - iconsvc.replaceFaviconData( - firstFavicon.uri, - secondFavicon.data, - secondFavicon.mimetype - ); - - await new Promise(resolve => { - iconsvc.setAndFetchFaviconForPage( - pageURI, - firstFavicon.uri, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - function test_replaceFaviconDataFromDataURL_beforeRegularAssign_check( - aURI, - aDataLen, - aData, - aMimeType - ) { - checkCallbackSucceeded( - aMimeType, - aData, - secondFavicon.mimetype, - secondFavicon.data - ); - checkFaviconDataForPage( - pageURI, - secondFavicon.mimetype, - secondFavicon.data, - function test_replaceFaviconDataFromDataURL_beforeRegularAssign_callback() { - firstFavicon.file.remove(false); - secondFavicon.file.remove(false); - resolve(); - } - ); - }, - Services.scriptSecurityManager.getSystemPrincipal() - ); - }); - - await PlacesUtils.history.clear(); - } -); - -/* toBase64 copied from image/test/unit/test_encoder_png.js */ - -/* Convert data (an array of integers) to a Base64 string. */ -const toBase64Table = - // eslint-disable-next-line no-useless-concat - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789+/"; -const base64Pad = "="; -function toBase64(data) { - let result = ""; - let length = data.length; - let i; - // Convert every three bytes to 4 ascii characters. - for (i = 0; i < length - 2; i += 3) { - result += toBase64Table[data[i] >> 2]; - result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; - result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; - result += toBase64Table[data[i + 2] & 0x3f]; - } - - // Convert the remaining 1 or 2 bytes, pad out to 4 characters. - if (length % 3) { - i = length - (length % 3); - result += toBase64Table[data[i] >> 2]; - if (length % 3 == 2) { - result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; - result += toBase64Table[(data[i + 1] & 0x0f) << 2]; - result += base64Pad; - } else { - result += toBase64Table[(data[i] & 0x03) << 4]; - result += base64Pad + base64Pad; - } - } - - return result; -} diff --git a/toolkit/components/places/tests/favicons/test_root_icons.js b/toolkit/components/places/tests/favicons/test_root_icons.js index f0487cc162..5a101ed6a6 100644 --- a/toolkit/components/places/tests/favicons/test_root_icons.js +++ b/toolkit/components/places/tests/favicons/test_root_icons.js @@ -9,13 +9,11 @@ add_task(async function () { let pageURI = NetUtil.newURI("http://www.places.test/page/"); await PlacesTestUtils.addVisits(pageURI); let faviconURI = NetUtil.newURI("http://www.places.test/favicon.ico"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + pageURI, faviconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal + SMALLPNG_DATA_URI ); - await setFaviconForPage(pageURI, faviconURI); // Sanity checks. Assert.equal(await getFaviconUrlForPage(pageURI), faviconURI.spec); @@ -70,22 +68,18 @@ add_task(async function test_removePagesByTimeframe() { // Add a normal icon to the most recent page. let faviconURI = NetUtil.newURI(`${BASE_URL}/page/favicon.ico`); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + pageURI, faviconURI, - SMALLSVG_DATA_URI.spec, - 0, - systemPrincipal + SMALLSVG_DATA_URI ); - await setFaviconForPage(pageURI, faviconURI); // Add a root icon to the most recent page. let rootIconURI = NetUtil.newURI(`${BASE_URL}/favicon.ico`); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + pageURI, rootIconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal + SMALLPNG_DATA_URI ); - await setFaviconForPage(pageURI, rootIconURI); // Sanity checks. Assert.equal( @@ -141,13 +135,11 @@ add_task(async function test_different_host() { let pageURI = NetUtil.newURI("http://places.test/page/"); await PlacesTestUtils.addVisits(pageURI); let faviconURI = NetUtil.newURI("http://mozilla.test/favicon.ico"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( + await PlacesTestUtils.setFaviconForPage( + pageURI, faviconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal + SMALLPNG_DATA_URI ); - await setFaviconForPage(pageURI, faviconURI); Assert.equal( await getFaviconUrlForPage(pageURI), @@ -166,16 +158,25 @@ add_task(async function test_different_host() { add_task(async function test_same_size() { // Add two icons with the same size, one is a root icon. Check that the // non-root icon is preferred when a smaller size is requested. - let data = readFileData(do_get_file("favicon-normal32.png")); + let dataURL = await readFileDataAsDataURL( + do_get_file("favicon-normal32.png"), + "image/png" + ); let pageURI = NetUtil.newURI("http://new_places.test/page/"); await PlacesTestUtils.addVisits(pageURI); let faviconURI = NetUtil.newURI("http://new_places.test/favicon.ico"); - PlacesUtils.favicons.replaceFaviconData(faviconURI, data, "image/png"); - await setFaviconForPage(pageURI, faviconURI); + await PlacesTestUtils.setFaviconForPage( + pageURI.spec, + faviconURI.spec, + dataURL + ); faviconURI = NetUtil.newURI("http://new_places.test/another_icon.ico"); - PlacesUtils.favicons.replaceFaviconData(faviconURI, data, "image/png"); - await setFaviconForPage(pageURI, faviconURI); + await PlacesTestUtils.setFaviconForPage( + pageURI.spec, + faviconURI.spec, + dataURL + ); Assert.equal( await getFaviconUrlForPage(pageURI, 20), @@ -207,13 +208,7 @@ add_task(async function test_root_on_different_host() { // Root favicon for TEST_URL1. const ICON_URL = "http://places1.test/favicon.ico"; let iconURI = NetUtil.newURI(ICON_URL); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - iconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal - ); - await setFaviconForPage(pageURI1, iconURI); + await PlacesTestUtils.setFaviconForPage(pageURI1, iconURI, SMALLPNG_DATA_URI); Assert.equal(await getRootValue(ICON_URL), 1, "Check root == 1"); Assert.equal( await getFaviconUrlForPage(pageURI1, 16), @@ -222,13 +217,7 @@ add_task(async function test_root_on_different_host() { ); // Same favicon for TEST_URL2. - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - iconURI, - SMALLPNG_DATA_URI.spec, - 0, - systemPrincipal - ); - await setFaviconForPage(pageURI2, iconURI); + await PlacesTestUtils.setFaviconForPage(pageURI2, iconURI, SMALLPNG_DATA_URI); Assert.equal(await getRootValue(ICON_URL), 1, "Check root == 1"); Assert.equal( await getFaviconUrlForPage(pageURI2, 16), diff --git a/toolkit/components/places/tests/favicons/test_setFaviconForPage.js b/toolkit/components/places/tests/favicons/test_setFaviconForPage.js new file mode 100644 index 0000000000..22364e5410 --- /dev/null +++ b/toolkit/components/places/tests/favicons/test_setFaviconForPage.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests for setFaviconForPage() + */ + +add_task(async function test_validHistoryURI() { + let pageURI = uri("http://test1.bar/"); + await PlacesTestUtils.addVisits(pageURI); + + let favicon = await createFavicon("favicon1.png"); + + await doTestSetFaviconForPage({ + pageURI, + faviconURI: favicon.uri, + dataURL: await createDataURLForFavicon(favicon), + expectedFaviconData: favicon.data, + expectedFaviconMimeType: favicon.mimeType, + }); + + await IOUtils.remove(favicon.file.path); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_overrideDefaultFavicon() { + let pageURI = uri("http://test2.bar/"); + await PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = await createFavicon("favicon2.png"); + let secondFavicon = await createFavicon("favicon3.png"); + + await doTestSetFaviconForPage({ + pageURI, + faviconURI: firstFavicon.uri, + dataURL: await createDataURLForFavicon(secondFavicon), + expectedFaviconData: secondFavicon.data, + expectedFaviconMimeType: secondFavicon.mimeType, + }); + + await IOUtils.remove(firstFavicon.file.path); + await IOUtils.remove(secondFavicon.file.path); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_replaceExisting() { + let pageURI = uri("http://test3.bar"); + await PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = await createFavicon("favicon4.png"); + let secondFavicon = await createFavicon("favicon5.png"); + + await new Promise(resolve => { + PlacesUtils.favicons.setAndFetchFaviconForPage( + pageURI, + firstFavicon.uri, + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + function test_replaceExisting_firstSet_check( + aURI, + aDataLen, + aData, + aMimeType + ) { + Assert.equal(aMimeType, firstFavicon.mimeType); + Assert.ok(compareArrays(aData, firstFavicon.data)); + checkFaviconDataForPage( + pageURI, + firstFavicon.mimeType, + firstFavicon.data, + resolve + ); + }, + Services.scriptSecurityManager.getSystemPrincipal() + ); + }); + + await doTestSetFaviconForPage({ + pageURI, + faviconURI: firstFavicon.uri, + dataURL: await createDataURLForFavicon(secondFavicon), + expectedFaviconData: secondFavicon.data, + expectedFaviconMimeType: secondFavicon.mimeType, + }); + + await IOUtils.remove(firstFavicon.file.path); + await IOUtils.remove(secondFavicon.file.path); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_twiceReplace() { + let pageURI = uri("http://test5.bar/"); + await PlacesTestUtils.addVisits(pageURI); + + let firstFavicon = await createFavicon("favicon9.png"); + let secondFavicon = await createFavicon("favicon10.png"); + + let firstFaviconDataURL = await createDataURLForFavicon(firstFavicon); + await new Promise(resolve => { + PlacesUtils.favicons.setFaviconForPage( + pageURI, + firstFavicon.uri, + firstFaviconDataURL, + null, + resolve + ); + }); + + await doTestSetFaviconForPage({ + pageURI, + faviconURI: firstFavicon.uri, + dataURL: await createDataURLForFavicon(secondFavicon), + expectedFaviconData: secondFavicon.data, + expectedFaviconMimeType: secondFavicon.mimeType, + }); + + await IOUtils.remove(firstFavicon.file.path); + await IOUtils.remove(secondFavicon.file.path); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_userpass() { + let pageURI = uri("http://user:pass@test1.bar/"); + await PlacesTestUtils.addVisits(pageURI); + + let favicon = await createFavicon("favicon1.png"); + let faviconURI = uri("http://user:pass@test1.bar/favicon1.png"); + + await doTestSetFaviconForPage({ + pageURI, + faviconURI, + dataURL: await createDataURLForFavicon(favicon), + expectedFaviconData: favicon.data, + expectedFaviconMimeType: favicon.mimeType, + }); + + await IOUtils.remove(favicon.file.path); + await PlacesUtils.history.clear(); +}); + +add_task(async function test_svg() { + let pageURI = uri("http://example.com/"); + await PlacesTestUtils.addVisits(pageURI); + + const svgContent = "<svg><rect width='1px' height='1px%'/></svg>"; + + await doTestSetFaviconForPage({ + pageURI, + faviconURI: uri("http://example.com/favicon.svg"), + dataURL: uri(`data:image/svg+xml;utf8,${svgContent}`), + expectedFaviconData: Array.from(new TextEncoder().encode(svgContent)), + expectedFaviconMimeType: "image/svg+xml", + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_invalidPageURI() { + await PlacesTestUtils.addVisits(uri("http://example.com/")); + let favicon = await createFavicon("favicon-invalidPageURI.png"); + + for (let invalidURI of [null, "", "http://example.com"]) { + try { + info(`Invalid page URI test for [${invalidURI}]`); + PlacesUtils.favicons.setFaviconForPage( + invalidURI, + favicon.uri, + await createDataURLForFavicon(favicon) + ); + Assert.ok(false, "Error should happened"); + } catch (e) { + Assert.ok(true, `Expected error happend [${e.message}]`); + } + } +}); + +add_task(async function test_invalidFaviconURI() { + let pageURI = uri("http://example.com/"); + await PlacesTestUtils.addVisits(pageURI); + let favicon = await createFavicon("favicon-invalidFaviconURI.png"); + + for (let invalidURI of [null, "", favicon.uri.spec]) { + try { + info(`Invalid favicon URI test for [${invalidURI}]`); + PlacesUtils.favicons.setFaviconForPage( + pageURI, + invalidURI, + await createDataURLForFavicon(favicon) + ); + Assert.ok(false, "Error should happened"); + } catch (e) { + Assert.ok(true, `Expected error happend [${e.message}]`); + } + } +}); + +add_task(async function test_invalidFaviconDataURI() { + let pageURI = uri("http://example.com/"); + await PlacesTestUtils.addVisits(pageURI); + let favicon = createFavicon("favicon-invalidFaviconDataURI.png"); + + for (let invalidURI of [ + null, + "", + "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==", + ]) { + try { + info(`Invalid favicon data URI test for [${invalidURI}]`); + PlacesUtils.favicons.setFaviconForPage(pageURI, favicon.uri, invalidURI); + Assert.ok(false, "Error should happened"); + } catch (e) { + Assert.ok(true, `Expected error happend [${e.message}]`); + } + } +}); + +async function doTestSetFaviconForPage({ + pageURI, + faviconURI, + dataURL, + expectedFaviconData, + expectedFaviconMimeType, +}) { + let result = await new Promise(resolve => { + PlacesUtils.favicons.setFaviconForPage( + pageURI, + faviconURI, + dataURL, + null, + resolve + ); + }); + + info("Check the result of setFaviconForPage"); + Assert.equal(result, 0); + + await new Promise(resolve => { + checkFaviconDataForPage( + pageURI, + expectedFaviconMimeType, + expectedFaviconData, + resolve + ); + }); +} diff --git a/toolkit/components/places/tests/favicons/xpcshell.toml b/toolkit/components/places/tests/favicons/xpcshell.toml index 997aa48e0b..2716e72fda 100644 --- a/toolkit/components/places/tests/favicons/xpcshell.toml +++ b/toolkit/components/places/tests/favicons/xpcshell.toml @@ -57,10 +57,6 @@ support-files = [ ["test_query_result_favicon_changed_on_child.js"] -["test_replaceFaviconData.js"] - -["test_replaceFaviconDataFromDataURL.js"] - ["test_root_icons.js"] ["test_setAndFetchFaviconForPage.js"] @@ -69,4 +65,6 @@ support-files = [ ["test_setAndFetchFaviconForPage_redirects.js"] +["test_setFaviconForPage.js"] + ["test_svg_favicon.js"] diff --git a/toolkit/components/places/tests/gtest/places_test_harness_tail.h b/toolkit/components/places/tests/gtest/places_test_harness_tail.h index 0464d14e0d..27de44eaa9 100644 --- a/toolkit/components/places/tests/gtest/places_test_harness_tail.h +++ b/toolkit/components/places/tests/gtest/places_test_harness_tail.h @@ -20,7 +20,7 @@ class RunNextTest : public mozilla::Runnable { public: RunNextTest() : mozilla::Runnable("RunNextTest") {} NS_IMETHOD Run() override { - NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Not running on the main thread?"); if (gTestsIndex < int(mozilla::ArrayLength(gTests))) { do_test_pending(); Test& test = gTests[gTestsIndex++]; @@ -47,7 +47,7 @@ void run_next_test() { int gPendingTests = 0; void do_test_pending() { - NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Not running on the main thread?"); if (kDebugRunNextTest) { printf_stderr("do_test_pending()\n"); MozWalkTheStack(stderr); @@ -56,8 +56,8 @@ void do_test_pending() { } void do_test_finished() { - NS_ASSERTION(NS_IsMainThread(), "Not running on the main thread?"); - NS_ASSERTION(gPendingTests > 0, "Invalid pending test count!"); + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Not running on the main thread?"); + MOZ_RELEASE_ASSERT(gPendingTests > 0, "Invalid pending test count!"); gPendingTests--; } diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js index d7a465786e..22d89db139 100644 --- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -169,7 +169,6 @@ function readFileData(aFile) { } return bytes; } - /** * Reads the data from the named file, verifying the expected file length. * @@ -187,6 +186,42 @@ function readFileOfLength(aFileName, aExpectedLength) { } /** + * Reads the data from the specified nsIFile, then returns it as data URL. + * + * @param file + * The nsIFile to read from. + * @param mimeType + * The mime type of the file content. + * @return Promise that retunes data URL. + */ +async function readFileDataAsDataURL(file, mimeType) { + const data = readFileData(file); + return fileDataToDataURL(data, mimeType); +} + +/** + * Converts the given data to the data URL. + * + * @param data + * The file data. + * @param mimeType + * The mime type of the file content. + * @return Promise that retunes data URL. + */ +async function fileDataToDataURL(data, mimeType) { + const dataURL = await new Promise(resolve => { + const buffer = new Uint8ClampedArray(data); + const blob = new Blob([buffer], { type: mimeType }); + const reader = new FileReader(); + reader.onload = e => { + resolve(e.target.result); + }; + reader.readAsDataURL(blob); + }); + return dataURL; +} + +/** * Returns the base64-encoded version of the given string. This function is * similar to window.btoa, but is available to xpcshell tests also. * diff --git a/toolkit/components/places/tests/history/test_remove.js b/toolkit/components/places/tests/history/test_remove.js index c018a996f3..8c469b5930 100644 --- a/toolkit/components/places/tests/history/test_remove.js +++ b/toolkit/components/places/tests/history/test_remove.js @@ -292,20 +292,7 @@ add_task(async function test_orphans() { ); // Also create a root icon. let faviconURI = Services.io.newURI(uri.spec + "favicon.ico"); - PlacesUtils.favicons.replaceFaviconDataFromDataURL( - faviconURI, - SMALLPNG_DATA_URI.spec, - 0, - Services.scriptSecurityManager.getSystemPrincipal() - ); - PlacesUtils.favicons.setAndFetchFaviconForPage( - uri, - faviconURI, - true, - PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, - null, - Services.scriptSecurityManager.getSystemPrincipal() - ); + await PlacesTestUtils.setFaviconForPage(uri, faviconURI, SMALLPNG_DATA_URI); await PlacesUtils.history.update({ url: uri, diff --git a/toolkit/components/places/tests/unit/test_PlacesQuery_history.js b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js index 9546e03730..24a046bcdc 100644 --- a/toolkit/components/places/tests/unit/test_PlacesQuery_history.js +++ b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js @@ -3,6 +3,10 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; +const { sinon } = ChromeUtils.importESModule( + "resource://testing-common/Sinon.sys.mjs" +); + const { PlacesQuery } = ChromeUtils.importESModule( "resource://gre/modules/PlacesQuery.sys.mjs" ); @@ -243,3 +247,32 @@ add_task(async function test_search_visits() { await PlacesUtils.history.clear(); }); + +add_task(async function test_search_interrupt() { + const { promise, reject } = Promise.withResolvers(); + const mockDatabase = { + executeCached: async (_, { query }) => { + if (query === "First Query") { + // Simulate a slow-running query which runs long enough to be + // interrupted by the next one. + await promise; + } + return []; + }, + interrupt: () => reject("interrupt() was called."), + }; + const stub = sinon + .stub(PlacesUtils, "promiseLargeCacheDBConnection") + .resolves(mockDatabase); + + const promiseFirstQuery = placesQuery.searchHistory("First Query"); + const promiseSecondQuery = placesQuery.searchHistory("Second Query"); + await Assert.rejects( + promiseFirstQuery, + /interrupt/, + "The first query was interrupted." + ); + ok(await promiseSecondQuery, "The second query resolved normally."); + + stub.restore(); +}); |