From fbd4c8c55bc61fa9a96f49351a335ceb67b7ee71 Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Tue, 6 Oct 2015 22:35:12 -0400 Subject: [PATCH 001/121] Bug 1213150 - Part 1: Add a nsContentUtils::IsNonSubresourceRequest helper; r=jdm --- docshell/base/nsDocShell.cpp | 11 +++-------- dom/base/nsContentUtils.cpp | 17 ++++++++++++++--- dom/base/nsContentUtils.h | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 1e6fed73054d..c47d35c986df 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -14070,18 +14070,13 @@ nsDocShell::ChannelIntercepted(nsIInterceptedChannel* aChannel, return NS_OK; } - bool isNavigation = false; - nsresult rv = aChannel->GetIsNavigation(&isNavigation); - NS_ENSURE_SUCCESS(rv, rv); - - nsContentPolicyType loadType; - rv = aChannel->GetInternalContentPolicyType(&loadType); + nsCOMPtr channel; + nsresult rv = aChannel->GetChannel(getter_AddRefs(channel)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr doc; - bool isSubresourceLoad = !isNavigation && - !nsContentUtils::IsWorkerLoad(loadType); + bool isSubresourceLoad = !nsContentUtils::IsNonSubresourceRequest(channel); if (isSubresourceLoad) { doc = GetDocument(); if (!doc) { diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 4e8b6d4ea00a..ccec0245150f 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -8101,10 +8101,21 @@ nsContentUtils::PushEnabled(JSContext* aCx, JSObject* aObj) // static bool -nsContentUtils::IsWorkerLoad(nsContentPolicyType aType) +nsContentUtils::IsNonSubresourceRequest(nsIChannel* aChannel) { - return aType == nsIContentPolicy::TYPE_INTERNAL_WORKER || - aType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER; + nsLoadFlags loadFlags = 0; + aChannel->GetLoadFlags(&loadFlags); + if (loadFlags & nsIChannel::LOAD_DOCUMENT_URI) { + return true; + } + + nsCOMPtr loadInfo = aChannel->GetLoadInfo(); + if (!loadInfo) { + return false; + } + nsContentPolicyType type = loadInfo->InternalContentPolicyType(); + return type == nsIContentPolicy::TYPE_INTERNAL_WORKER || + type == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER; } // static, public diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 29fdf9c1f9d3..67c80efb2ac4 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -2522,7 +2522,7 @@ public: static bool PushEnabled(JSContext* aCx, JSObject* aObj); - static bool IsWorkerLoad(nsContentPolicyType aLoadType); + static bool IsNonSubresourceRequest(nsIChannel* aChannel); // The order of these entries matters, as we use std::min for total ordering // of permissions. Private Browsing is considered to be more limiting From f303ed36f20fe80ddaba3cd4d5ee2467801552be Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Tue, 6 Oct 2015 22:40:36 -0400 Subject: [PATCH 002/121] Bug 1213150 - Part 2: Rework ShouldPrepareForIntercept() in terms of subresource requests; r=jdm --- docshell/base/nsDocShell.cpp | 5 ++--- modules/libjar/nsJARChannel.cpp | 5 +---- netwerk/base/nsINetworkInterceptController.idl | 4 ++-- netwerk/protocol/http/HttpBaseChannel.cpp | 6 ++---- netwerk/protocol/http/HttpChannelParent.cpp | 4 ++-- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index c47d35c986df..71fd80d13599 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -13940,8 +13940,7 @@ nsDocShell::MaybeNotifyKeywordSearchLoading(const nsString& aProvider, } NS_IMETHODIMP -nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, bool aIsNavigate, - nsContentPolicyType aLoadContentType, +nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, bool aIsNonSubresourceRequest, bool* aShouldIntercept) { *aShouldIntercept = false; @@ -13994,7 +13993,7 @@ nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, bool aIsNavigate, } } - if (aIsNavigate || nsContentUtils::IsWorkerLoad(aLoadContentType)) { + if (aIsNonSubresourceRequest) { OriginAttributes attrs(GetAppId(), GetIsInBrowserElement()); *aShouldIntercept = swm->IsAvailable(attrs, aURI); return NS_OK; diff --git a/modules/libjar/nsJARChannel.cpp b/modules/libjar/nsJARChannel.cpp index fbb5723c6c66..42acacee4c44 100644 --- a/modules/libjar/nsJARChannel.cpp +++ b/modules/libjar/nsJARChannel.cpp @@ -883,11 +883,8 @@ nsJARChannel::ShouldIntercept() getter_AddRefs(controller)); bool shouldIntercept = false; if (controller && !BypassServiceWorker() && mLoadInfo) { - bool isNavigation = mLoadFlags & LOAD_DOCUMENT_URI; - nsContentPolicyType type = mLoadInfo->InternalContentPolicyType(); nsresult rv = controller->ShouldPrepareForIntercept(mAppURI, - isNavigation, - type, + nsContentUtils::IsNonSubresourceRequest(this), &shouldIntercept); NS_ENSURE_SUCCESS(rv, false); } diff --git a/netwerk/base/nsINetworkInterceptController.idl b/netwerk/base/nsINetworkInterceptController.idl index e0c3eb27151d..ba93d375236b 100644 --- a/netwerk/base/nsINetworkInterceptController.idl +++ b/netwerk/base/nsINetworkInterceptController.idl @@ -112,7 +112,7 @@ interface nsIFetchEventDispatcher : nsISupports * request should be intercepted before any network request is initiated. */ -[scriptable, uuid(7157fe12-20e3-45db-b09e-68fdf6d0614f)] +[scriptable, uuid(49eb1997-90fb-49d6-a25d-41f51c7c99e8)] interface nsINetworkInterceptController : nsISupports { /** @@ -122,7 +122,7 @@ interface nsINetworkInterceptController : nsISupports * @param aURI the URI being requested by a channel * @param aIsNavigate True if the request is for a navigation, false for a fetch. */ - bool shouldPrepareForIntercept(in nsIURI aURI, in bool aIsNavigate, in nsContentPolicyType aType); + bool shouldPrepareForIntercept(in nsIURI aURI, in bool aIsNonSubresourceRequest); /** * Notification when a given intercepted channel is prepared to accept a synthesized diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp index 7101a3dfddc1..a253e33f9438 100644 --- a/netwerk/protocol/http/HttpBaseChannel.cpp +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -2313,10 +2313,8 @@ HttpBaseChannel::ShouldIntercept() GetCallback(controller); bool shouldIntercept = false; if (controller && !BypassServiceWorker() && mLoadInfo) { - nsContentPolicyType type = mLoadInfo->InternalContentPolicyType(); - nsresult rv = controller->ShouldPrepareForIntercept(mURI, - IsNavigation(), - type, + nsresult rv = controller->ShouldPrepareForIntercept(aURI, + nsContentUtils::IsNonSubresourceRequest(this), &shouldIntercept); if (NS_FAILED(rv)) { return false; diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp index 6ec4f78d18f6..d6d23d5d6865 100644 --- a/netwerk/protocol/http/HttpChannelParent.cpp +++ b/netwerk/protocol/http/HttpChannelParent.cpp @@ -162,8 +162,8 @@ NS_IMPL_ISUPPORTS(HttpChannelParent, nsIDeprecationWarner) NS_IMETHODIMP -HttpChannelParent::ShouldPrepareForIntercept(nsIURI* aURI, bool aIsNavigate, - nsContentPolicyType aType, +HttpChannelParent::ShouldPrepareForIntercept(nsIURI* aURI, + bool aIsNonSubresourceRequest, bool* aShouldIntercept) { *aShouldIntercept = mShouldIntercept; From a9097bf0fb75f9b941b4336b861afe27a09ee361 Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Tue, 6 Oct 2015 22:52:43 -0400 Subject: [PATCH 003/121] Bug 1213150 - Part 3: Remove nsIInterceptedChannel.isNavigation; r=jdm --- modules/libjar/InterceptedJARChannel.cpp | 11 +---------- modules/libjar/InterceptedJARChannel.h | 6 +----- modules/libjar/nsJARChannel.cpp | 3 +-- netwerk/base/nsINetworkInterceptController.idl | 7 +------ netwerk/protocol/http/InterceptedChannel.cpp | 15 +++------------ netwerk/protocol/http/InterceptedChannel.h | 7 +------ 6 files changed, 8 insertions(+), 41 deletions(-) diff --git a/modules/libjar/InterceptedJARChannel.cpp b/modules/libjar/InterceptedJARChannel.cpp index b5c45794278c..c2057849b75c 100644 --- a/modules/libjar/InterceptedJARChannel.cpp +++ b/modules/libjar/InterceptedJARChannel.cpp @@ -13,11 +13,9 @@ using namespace mozilla::net; NS_IMPL_ISUPPORTS(InterceptedJARChannel, nsIInterceptedChannel) InterceptedJARChannel::InterceptedJARChannel(nsJARChannel* aChannel, - nsINetworkInterceptController* aController, - bool aIsNavigation) + nsINetworkInterceptController* aController) : mController(aController) , mChannel(aChannel) -, mIsNavigation(aIsNavigation) { } @@ -28,13 +26,6 @@ InterceptedJARChannel::GetResponseBody(nsIOutputStream** aStream) return NS_OK; } -NS_IMETHODIMP -InterceptedJARChannel::GetIsNavigation(bool* aIsNavigation) -{ - *aIsNavigation = mIsNavigation; - return NS_OK; -} - NS_IMETHODIMP InterceptedJARChannel::GetInternalContentPolicyType(nsContentPolicyType* aPolicyType) { diff --git a/modules/libjar/InterceptedJARChannel.h b/modules/libjar/InterceptedJARChannel.h index 1ecc6da11207..66922dfea06b 100644 --- a/modules/libjar/InterceptedJARChannel.h +++ b/modules/libjar/InterceptedJARChannel.h @@ -44,14 +44,10 @@ class InterceptedJARChannel : public nsIInterceptedChannel // The content type of the synthesized response. nsCString mContentType; - // Wether this intercepted channel was performing a navigation. - bool mIsNavigation; - virtual ~InterceptedJARChannel() {}; public: InterceptedJARChannel(nsJARChannel* aChannel, - nsINetworkInterceptController* aController, - bool aIsNavigation); + nsINetworkInterceptController* aController); NS_DECL_ISUPPORTS NS_DECL_NSIINTERCEPTEDCHANNEL diff --git a/modules/libjar/nsJARChannel.cpp b/modules/libjar/nsJARChannel.cpp index 42acacee4c44..2c5c35bd1596 100644 --- a/modules/libjar/nsJARChannel.cpp +++ b/modules/libjar/nsJARChannel.cpp @@ -968,9 +968,8 @@ nsJARChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *ctx) NS_GET_IID(nsINetworkInterceptController), getter_AddRefs(controller)); - bool isNavigation = mLoadFlags & LOAD_DOCUMENT_URI; nsRefPtr intercepted = - new InterceptedJARChannel(this, controller, isNavigation); + new InterceptedJARChannel(this, controller); intercepted->NotifyController(); // We get the JAREntry so we can infer the content type later in case diff --git a/netwerk/base/nsINetworkInterceptController.idl b/netwerk/base/nsINetworkInterceptController.idl index ba93d375236b..e5eabee6cd35 100644 --- a/netwerk/base/nsINetworkInterceptController.idl +++ b/netwerk/base/nsINetworkInterceptController.idl @@ -27,7 +27,7 @@ class ChannelInfo; * which do not implement nsIChannel. */ -[scriptable, uuid(91d430cc-7e04-4df1-a3c0-879fa30f7ab9)] +[scriptable, uuid(6be00c37-2e85-42ee-b53c-e6508ce4cef0)] interface nsIInterceptedChannel : nsISupports { /** @@ -72,11 +72,6 @@ interface nsIInterceptedChannel : nsISupports */ readonly attribute nsIChannel channel; - /** - * True if the underlying request was caused by a navigation attempt. - */ - readonly attribute bool isNavigation; - /** * This method allows to override the channel info for the channel. */ diff --git a/netwerk/protocol/http/InterceptedChannel.cpp b/netwerk/protocol/http/InterceptedChannel.cpp index 09e5cc1c9bdb..b92e969e7a99 100644 --- a/netwerk/protocol/http/InterceptedChannel.cpp +++ b/netwerk/protocol/http/InterceptedChannel.cpp @@ -30,10 +30,8 @@ DoAddCacheEntryHeaders(nsHttpChannel *self, NS_IMPL_ISUPPORTS(InterceptedChannelBase, nsIInterceptedChannel) -InterceptedChannelBase::InterceptedChannelBase(nsINetworkInterceptController* aController, - bool aIsNavigation) +InterceptedChannelBase::InterceptedChannelBase(nsINetworkInterceptController* aController) : mController(aController) -, mIsNavigation(aIsNavigation) { } @@ -76,13 +74,6 @@ InterceptedChannelBase::DoNotifyController() mController = nullptr; } -NS_IMETHODIMP -InterceptedChannelBase::GetIsNavigation(bool* aIsNavigation) -{ - *aIsNavigation = mIsNavigation; - return NS_OK; -} - nsresult InterceptedChannelBase::DoSynthesizeStatus(uint16_t aStatus, const nsACString& aReason) { @@ -114,7 +105,7 @@ InterceptedChannelBase::DoSynthesizeHeader(const nsACString& aName, const nsACSt InterceptedChannelChrome::InterceptedChannelChrome(nsHttpChannel* aChannel, nsINetworkInterceptController* aController, nsICacheEntry* aEntry) -: InterceptedChannelBase(aController, aChannel->IsNavigation()) +: InterceptedChannelBase(aController) , mChannel(aChannel) , mSynthesizedCacheEntry(aEntry) { @@ -279,7 +270,7 @@ InterceptedChannelChrome::GetInternalContentPolicyType(nsContentPolicyType* aPol InterceptedChannelContent::InterceptedChannelContent(HttpChannelChild* aChannel, nsINetworkInterceptController* aController, nsIStreamListener* aListener) -: InterceptedChannelBase(aController, aChannel->IsNavigation()) +: InterceptedChannelBase(aController) , mChannel(aChannel) , mStreamListener(aListener) { diff --git a/netwerk/protocol/http/InterceptedChannel.h b/netwerk/protocol/http/InterceptedChannel.h index dfb432efcd34..facf81bf0256 100644 --- a/netwerk/protocol/http/InterceptedChannel.h +++ b/netwerk/protocol/http/InterceptedChannel.h @@ -35,9 +35,6 @@ protected: // Response head for use when synthesizing Maybe> mSynthesizedResponseHead; - // Whether this intercepted channel was performing a navigation. - bool mIsNavigation; - void EnsureSynthesizedResponse(); void DoNotifyController(); nsresult DoSynthesizeStatus(uint16_t aStatus, const nsACString& aReason); @@ -45,8 +42,7 @@ protected: virtual ~InterceptedChannelBase(); public: - InterceptedChannelBase(nsINetworkInterceptController* aController, - bool aIsNavigation); + explicit InterceptedChannelBase(nsINetworkInterceptController* aController); // Notify the interception controller that the channel has been intercepted // and prepare the response body output stream. @@ -55,7 +51,6 @@ public: NS_DECL_ISUPPORTS NS_IMETHOD GetResponseBody(nsIOutputStream** aOutput) override; - NS_IMETHOD GetIsNavigation(bool* aIsNavigation) override; }; class InterceptedChannelChrome : public InterceptedChannelBase From 2aca052b56bedf84693ca9a6d86acc5c9ffc1433 Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Mon, 5 Oct 2015 12:27:35 -0400 Subject: [PATCH 004/121] Bug 1213151 - Part 1: Add a SpecialPowers API for cleaning up the STS data that works in both e10s and non-e10s modes; r=jdm --- testing/specialpowers/components/SpecialPowersObserver.js | 2 ++ testing/specialpowers/content/SpecialPowersObserverAPI.js | 8 ++++++++ testing/specialpowers/content/specialpowers.js | 3 ++- testing/specialpowers/content/specialpowersAPI.js | 4 ++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/testing/specialpowers/components/SpecialPowersObserver.js b/testing/specialpowers/components/SpecialPowersObserver.js index 659dce2d9fda..d57a7144c0fb 100644 --- a/testing/specialpowers/components/SpecialPowersObserver.js +++ b/testing/specialpowers/components/SpecialPowersObserver.js @@ -93,6 +93,7 @@ SpecialPowersObserver.prototype = new SpecialPowersObserverAPI(); this._messageManager.addMessageListener("SPStartupExtension", this); this._messageManager.addMessageListener("SPUnloadExtension", this); this._messageManager.addMessageListener("SPExtensionMessage", this); + this._messageManager.addMessageListener("SPCleanUpSTSData", this); this._messageManager.loadFrameScript(CHILD_LOGGER_SCRIPT, true); this._messageManager.loadFrameScript(CHILD_SCRIPT_API, true); @@ -173,6 +174,7 @@ SpecialPowersObserver.prototype = new SpecialPowersObserverAPI(); this._messageManager.removeMessageListener("SPStartupExtension", this); this._messageManager.removeMessageListener("SPUnloadExtension", this); this._messageManager.removeMessageListener("SPExtensionMessage", this); + this._messageManager.removeMessageListener("SPCleanUpSTSData", this); this._messageManager.removeDelayedFrameScript(CHILD_LOGGER_SCRIPT); this._messageManager.removeDelayedFrameScript(CHILD_SCRIPT_API); diff --git a/testing/specialpowers/content/SpecialPowersObserverAPI.js b/testing/specialpowers/content/SpecialPowersObserverAPI.js index 23928c4b7ac0..225bb467383c 100644 --- a/testing/specialpowers/content/SpecialPowersObserverAPI.js +++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js @@ -556,6 +556,14 @@ SpecialPowersObserverAPI.prototype = { return undefined; // See comment at the beginning of this function. } + case "SPCleanUpSTSData": { + let origin = aMessage.data.origin; + let uri = Services.io.newURI(origin, null, null); + let sss = Cc["@mozilla.org/ssservice;1"]. + getService(Ci.nsISiteSecurityService); + sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0); + } + case "SPLoadExtension": { let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); diff --git a/testing/specialpowers/content/specialpowers.js b/testing/specialpowers/content/specialpowers.js index 9f7416007681..a676adbbcf70 100644 --- a/testing/specialpowers/content/specialpowers.js +++ b/testing/specialpowers/content/specialpowers.js @@ -32,7 +32,8 @@ function SpecialPowers(window) { "SPProcessCrashService", "SPSetTestPluginEnabledState", "SPWebAppService", - "SPPeriodicServiceWorkerUpdates"]; + "SPPeriodicServiceWorkerUpdates", + "SPCleanUpSTSData"]; this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus", "SpecialPowers.Quit", diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js index 05520c72ec3e..b6b3a9ff3cac 100644 --- a/testing/specialpowers/content/specialpowersAPI.js +++ b/testing/specialpowers/content/specialpowersAPI.js @@ -2044,6 +2044,10 @@ SpecialPowersAPI.prototype = { this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com"); }, + cleanUpSTSData: function(origin) { + return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin}); + }, + loadExtension: function(ext, handler) { let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); let id = uuidGenerator.generateUUID().number; From e6a62c4d9d53d5de835727cdbbab0cad5c026721 Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Mon, 5 Oct 2015 00:32:54 -0400 Subject: [PATCH 005/121] Bug 1213151 - Part 2: Use SpecialPowers.cleanUpSTSData() in a few tests; r=jdm --- dom/base/test/test_websocket.html | 6 +----- .../tests/mochitest/stricttransportsecurity/mochitest.ini | 2 +- .../test_stricttransportsecurity.html | 8 +------- .../test_sts_privatebrowsing_perwindowpb.html | 8 +------- testing/specialpowers/content/SpecialPowersObserverAPI.js | 3 ++- testing/specialpowers/content/specialpowersAPI.js | 4 ++-- 6 files changed, 8 insertions(+), 23 deletions(-) diff --git a/dom/base/test/test_websocket.html b/dom/base/test/test_websocket.html index 1a79c5d8609e..a09cc7db6fbc 100644 --- a/dom/base/test/test_websocket.html +++ b/dom/base/test/test_websocket.html @@ -1225,11 +1225,7 @@ function test41() ok(true, "test 41c close"); // clean up the STS state - const Cc = SpecialPowers.Cc; const Ci = SpecialPowers.Ci; - var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); - var thehost = ios.newURI("http://example.com", null, null); - var sss = Cc["@mozilla.org/ssservice;1"].getService(Ci.nsISiteSecurityService); var loadContext = SpecialPowers.wrap(window) .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) @@ -1237,7 +1233,7 @@ function test41() var flags = 0; if (loadContext.usePrivateBrowsing) flags |= Ci.nsISocketProvider.NO_PERMANENT_STORAGE; - sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, thehost, flags); + SpecialPowers.cleanUpSTSData("http://example.com", flags); doTest(42); } } diff --git a/security/manager/ssl/tests/mochitest/stricttransportsecurity/mochitest.ini b/security/manager/ssl/tests/mochitest/stricttransportsecurity/mochitest.ini index f864afe0e181..1df2585ca74f 100644 --- a/security/manager/ssl/tests/mochitest/stricttransportsecurity/mochitest.ini +++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/mochitest.ini @@ -1,6 +1,6 @@ [DEFAULT] tags = psm -skip-if = buildapp == 'b2g' || e10s +skip-if = buildapp == 'b2g' support-files = nosts_bootstrap.html nosts_bootstrap.html^headers^ diff --git a/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_stricttransportsecurity.html b/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_stricttransportsecurity.html index 6a3e2df3f38d..5905865685e7 100644 --- a/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_stricttransportsecurity.html +++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_stricttransportsecurity.html @@ -47,13 +47,7 @@ document.body.removeChild(document.getElementById('ifr_' + test)); // clean up the STS state - const Cc = SpecialPowers.Cc; - const Ci = SpecialPowers.Ci; - var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); - var thehost = ios.newURI("http://example.com", null, null); - - var sss = Cc["@mozilla.org/ssservice;1"].getService(Ci.nsISiteSecurityService); - sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, thehost, 0); + SpecialPowers.cleanUpSTSData("http://example.com"); } function loadVerifyFrames(round) { diff --git a/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing_perwindowpb.html b/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing_perwindowpb.html index be3fd0081915..796517c98a13 100644 --- a/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing_perwindowpb.html +++ b/security/manager/ssl/tests/mochitest/stricttransportsecurity/test_sts_privatebrowsing_perwindowpb.html @@ -192,14 +192,8 @@ function clean_up_sts_state(isPrivate) { // erase all signs that this test ran. SimpleTest.info("Cleaning up STS data"); - var ios = - Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); - var thehost = ios.newURI("http://example.com", null, null); - var sss = - Cc["@mozilla.org/ssservice;1"]. - getService(Ci.nsISiteSecurityService); var flags = isPrivate ? Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0 - sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, thehost, flags); + SpecialPowers.cleanUpSTSData("http://example.com", flags); dump_STSState(isPrivate); } diff --git a/testing/specialpowers/content/SpecialPowersObserverAPI.js b/testing/specialpowers/content/SpecialPowersObserverAPI.js index 225bb467383c..4326bd988b90 100644 --- a/testing/specialpowers/content/SpecialPowersObserverAPI.js +++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js @@ -558,10 +558,11 @@ SpecialPowersObserverAPI.prototype = { case "SPCleanUpSTSData": { let origin = aMessage.data.origin; + let flags = aMessage.data.flags; let uri = Services.io.newURI(origin, null, null); let sss = Cc["@mozilla.org/ssservice;1"]. getService(Ci.nsISiteSecurityService); - sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0); + sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags); } case "SPLoadExtension": { diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js index b6b3a9ff3cac..e3afb57d114e 100644 --- a/testing/specialpowers/content/specialpowersAPI.js +++ b/testing/specialpowers/content/specialpowersAPI.js @@ -2044,8 +2044,8 @@ SpecialPowersAPI.prototype = { this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com"); }, - cleanUpSTSData: function(origin) { - return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin}); + cleanUpSTSData: function(origin, flags) { + return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin, flags: flags || 0}); }, loadExtension: function(ext, handler) { From 7b6410629725eaf3aa0cd696eaa91895c51e48bc Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Fri, 9 Oct 2015 01:59:00 +0200 Subject: [PATCH 006/121] Bug 1121800 - when all proxies fail try DIRECT, r=mcmanus To mimic how other browsers do it these days and better deal with wrong propxy settings lingering. --- netwerk/base/nsProtocolProxyService.cpp | 76 +++++++++++-------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/netwerk/base/nsProtocolProxyService.cpp b/netwerk/base/nsProtocolProxyService.cpp index 2412204d231e..50b1cb486959 100644 --- a/netwerk/base/nsProtocolProxyService.cpp +++ b/netwerk/base/nsProtocolProxyService.cpp @@ -1377,11 +1377,9 @@ nsProtocolProxyService::GetFailoverForProxy(nsIProxyInfo *aProxy, nsresult aStatus, nsIProxyInfo **aResult) { - // We only support failover when a PAC file is configured, either - // directly or via system settings - if (mProxyConfig != PROXYCONFIG_PAC && mProxyConfig != PROXYCONFIG_WPAD && - mProxyConfig != PROXYCONFIG_SYSTEM) + if (mProxyConfig == PROXYCONFIG_DIRECT) { return NS_ERROR_NOT_AVAILABLE; + } // Verify that |aProxy| is one of our nsProxyInfo objects. nsCOMPtr pi = do_QueryInterface(aProxy); @@ -1977,52 +1975,42 @@ nsProtocolProxyService::PruneProxyInfo(const nsProtocolInfo &info, return; } - // Now, scan to see if all remaining proxies are disabled. If so, then - // we'll just bail and return them all. Otherwise, we'll go and prune the - // disabled ones. + // Now, scan to the next proxy not disabled. If all proxies are disabled, + // return blank list to enforce a DIRECT rule. bool allDisabled = true; - nsProxyInfo *iter; - for (iter = head; iter; iter = iter->mNext) { - if (!IsProxyDisabled(iter)) { - allDisabled = false; - break; + + // remove any disabled proxies. + nsProxyInfo *last = nullptr; + for (iter = head; iter; ) { + if (IsProxyDisabled(iter)) { + // reject! + nsProxyInfo *reject = iter; + + iter = iter->mNext; + if (last) + last->mNext = iter; + else + head = iter; + + reject->mNext = nullptr; + NS_RELEASE(reject); + continue; } + + allDisabled = false; + EnableProxy(iter); + + last = iter; + iter = iter->mNext; } - if (allDisabled) - LOG(("All proxies are disabled, so trying all again")); - else { - // remove any disabled proxies. - nsProxyInfo *last = nullptr; - for (iter = head; iter; ) { - if (IsProxyDisabled(iter)) { - // reject! - nsProxyInfo *reject = iter; - - iter = iter->mNext; - if (last) - last->mNext = iter; - else - head = iter; - - reject->mNext = nullptr; - NS_RELEASE(reject); - continue; - } - - // since we are about to use this proxy, make sure it is not on - // the disabled proxy list. we'll add it back to that list if - // we have to (in GetFailoverForProxy). - // - // XXX(darin): It might be better to do this as a final pass. - // - EnableProxy(iter); - - last = iter; - iter = iter->mNext; - } + if (allDisabled) { + LOG(("All proxies are disabled, try a DIRECT rule!")); + NS_RELEASE(head); + *list = nullptr; + return; } // if only DIRECT was specified then return no proxy info, and we're done. From d5f54678275eafce68b4a604dc58975bf45541fe Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Fri, 9 Oct 2015 11:27:05 -0400 Subject: [PATCH 007/121] Bug 1213150 follow-up: fix build bustage --- netwerk/protocol/http/HttpBaseChannel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp index a253e33f9438..ba3ac7b8ba85 100644 --- a/netwerk/protocol/http/HttpBaseChannel.cpp +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -2313,7 +2313,7 @@ HttpBaseChannel::ShouldIntercept() GetCallback(controller); bool shouldIntercept = false; if (controller && !BypassServiceWorker() && mLoadInfo) { - nsresult rv = controller->ShouldPrepareForIntercept(aURI, + nsresult rv = controller->ShouldPrepareForIntercept(mURI, nsContentUtils::IsNonSubresourceRequest(this), &shouldIntercept); if (NS_FAILED(rv)) { From 24d5f445ce34c4a61386a0b2873859286998f305 Mon Sep 17 00:00:00 2001 From: Andrew Comminos Date: Thu, 24 Sep 2015 12:37:08 -0700 Subject: [PATCH 008/121] Bug 1176929 - Disable Ctrl-K in GtkEntry unless custom key bindings are set on GTK3. r=karlt --- widget/gtk/NativeKeyBindings.cpp | 20 ++++++++++++++++++-- widget/gtk/mozgtk/mozgtk.c | 2 ++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/widget/gtk/NativeKeyBindings.cpp b/widget/gtk/NativeKeyBindings.cpp index d766ac0eaa75..2700ad05fe3a 100644 --- a/widget/gtk/NativeKeyBindings.cpp +++ b/widget/gtk/NativeKeyBindings.cpp @@ -65,9 +65,25 @@ delete_from_cursor_cb(GtkWidget *w, GtkDeleteType del_type, gint count, gpointer user_data) { g_signal_stop_emission_by_name(w, "delete_from_cursor"); - gHandled = true; - bool forward = count > 0; + +#if (MOZ_WIDGET_GTK == 3) + // Ignore GTK's Ctrl-K keybinding introduced in GTK 3.14 and removed in + // 3.18 if the user has custom bindings set. See bug 1176929. + if (del_type == GTK_DELETE_PARAGRAPH_ENDS && forward && GTK_IS_ENTRY(w) && + !gtk_check_version(3, 14, 1) && gtk_check_version(3, 17, 9)) { + GtkStyleContext* context = gtk_widget_get_style_context(w); + GtkStateFlags flags = gtk_widget_get_state_flags(w); + + GPtrArray* array; + gtk_style_context_get(context, flags, "gtk-key-bindings", &array, nullptr); + if (!array) + return; + g_ptr_array_unref(array); + } +#endif + + gHandled = true; if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) { // unsupported deletion type return; diff --git a/widget/gtk/mozgtk/mozgtk.c b/widget/gtk/mozgtk/mozgtk.c index 6ff2359aff83..88e4334fdb74 100644 --- a/widget/gtk/mozgtk/mozgtk.c +++ b/widget/gtk/mozgtk/mozgtk.c @@ -540,6 +540,7 @@ STUB(gtk_scale_new) STUB(gtk_scrollbar_new) STUB(gtk_style_context_add_class) STUB(gtk_style_context_add_region) +STUB(gtk_style_context_get) STUB(gtk_style_context_get_background_color) STUB(gtk_style_context_get_border) STUB(gtk_style_context_get_border_color) @@ -555,6 +556,7 @@ STUB(gtk_style_context_set_path) STUB(gtk_style_context_set_state) STUB(gtk_tree_view_column_get_button) STUB(gtk_widget_get_preferred_size) +STUB(gtk_widget_get_state_flags) STUB(gtk_widget_get_style_context) STUB(gtk_widget_path_append_type) STUB(gtk_widget_path_free) From b5919042114f53a1104152b9c7d893981aae0d5d Mon Sep 17 00:00:00 2001 From: Andrew Comminos Date: Tue, 6 Oct 2015 14:10:38 -0700 Subject: [PATCH 009/121] Bug 1209774 - Transform from GDK coords to layout device pixels before calling DispatchEvent. r=karlt --- widget/gtk/nsWindow.cpp | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp index 470e5b8594bb..ab8fbdb20fd1 100644 --- a/widget/gtk/nsWindow.cpp +++ b/widget/gtk/nsWindow.cpp @@ -531,10 +531,6 @@ nsWindow::DispatchEvent(WidgetGUIEvent* aEvent, nsEventStatus& aStatus) debug_DumpEvent(stdout, aEvent->widget, aEvent, nsAutoCString("something"), 0); #endif - // Translate the mouse event into device pixels. - aEvent->refPoint.x = GdkCoordToDevicePixels(aEvent->refPoint.x); - aEvent->refPoint.y = GdkCoordToDevicePixels(aEvent->refPoint.y); - aStatus = nsEventStatus_eIgnore; nsIWidgetListener* listener = mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener; @@ -2439,9 +2435,7 @@ nsWindow::OnEnterNotifyEvent(GdkEventCrossing *aEvent) WidgetMouseEvent event(true, eMouseEnterIntoWidget, this, WidgetMouseEvent::eReal); - event.refPoint.x = nscoord(aEvent->x); - event.refPoint.y = nscoord(aEvent->y); - + event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); event.time = aEvent->time; event.timeStamp = GetEventTimeStamp(aEvent->time); @@ -2482,9 +2476,7 @@ nsWindow::OnLeaveNotifyEvent(GdkEventCrossing *aEvent) WidgetMouseEvent event(true, eMouseExitFromWidget, this, WidgetMouseEvent::eReal); - event.refPoint.x = nscoord(aEvent->x); - event.refPoint.y = nscoord(aEvent->y); - + event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); event.time = aEvent->time; event.timeStamp = GetEventTimeStamp(aEvent->time); @@ -2549,8 +2541,7 @@ nsWindow::OnMotionNotifyEvent(GdkEventMotion *aEvent) event.time = xevent.xmotion.time; event.timeStamp = GetEventTimeStamp(xevent.xmotion.time); #else - event.refPoint.x = nscoord(aEvent->x); - event.refPoint.y = nscoord(aEvent->y); + event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); modifierState = aEvent->state; @@ -2561,11 +2552,10 @@ nsWindow::OnMotionNotifyEvent(GdkEventMotion *aEvent) else { // XXX see OnScrollEvent() if (aEvent->window == mGdkWindow) { - event.refPoint.x = nscoord(aEvent->x); - event.refPoint.y = nscoord(aEvent->y); + event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); } else { - LayoutDeviceIntPoint point(NSToIntFloor(aEvent->x_root), - NSToIntFloor(aEvent->y_root)); + LayoutDeviceIntPoint point = GdkPointToDevicePixels( + {aEvent->x_root, aEvent->y_root}); event.refPoint = point - WidgetToScreenOffset(); } @@ -2638,11 +2628,10 @@ nsWindow::InitButtonEvent(WidgetMouseEvent& aEvent, { // XXX see OnScrollEvent() if (aGdkEvent->window == mGdkWindow) { - aEvent.refPoint.x = nscoord(aGdkEvent->x); - aEvent.refPoint.y = nscoord(aGdkEvent->y); + aEvent.refPoint = GdkPointToDevicePixels({aGdkEvent->x, aGdkEvent->y}); } else { - LayoutDeviceIntPoint point(NSToIntFloor(aGdkEvent->x_root), - NSToIntFloor(aGdkEvent->y_root)); + LayoutDeviceIntPoint point = GdkPointToDevicePixels( + {aGdkEvent->x_root, aGdkEvent->y_root}); aEvent.refPoint = point - WidgetToScreenOffset(); } @@ -3161,14 +3150,13 @@ nsWindow::OnScrollEvent(GdkEventScroll *aEvent) if (aEvent->window == mGdkWindow) { // we are the window that the event happened on so no need for expensive WidgetToScreenOffset - wheelEvent.refPoint.x = nscoord(aEvent->x); - wheelEvent.refPoint.y = nscoord(aEvent->y); + wheelEvent.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); } else { // XXX we're never quite sure which GdkWindow the event came from due to our custom bubbling // in scroll_event_cb(), so use ScreenToWidget to translate the screen root coordinates into // coordinates relative to this widget. - LayoutDeviceIntPoint point(NSToIntFloor(aEvent->x_root), - NSToIntFloor(aEvent->y_root)); + LayoutDeviceIntPoint point = GdkPointToDevicePixels( + {aEvent->x_root, aEvent->y_root}); wheelEvent.refPoint = point - WidgetToScreenOffset(); } From 3dbb3a51aae35c98f4db3ce161cc5dd007ce8fdb Mon Sep 17 00:00:00 2001 From: Eric Faust Date: Fri, 9 Oct 2015 09:33:57 -0700 Subject: [PATCH 010/121] Bug 1105463 - Implement default constructors for ES6 class definitions. (r=jorendorff) --- js/src/frontend/BytecodeEmitter.cpp | 21 +++-- js/src/frontend/Parser.cpp | 6 -- js/src/jsfun.cpp | 22 ++++- .../ecma_6/Class/defaultConstructorBase.js | 25 +++++ .../Class/defaultConstructorNotCallable.js | 15 +++ .../Class/superCallBadDynamicSuperClass.js | 4 +- .../Class/superCallBadNewTargetPrototype.js | 4 +- .../ecma_6/Class/superCallBaseInvoked.js | 13 ++- .../ecma_6/Class/superCallInvalidBase.js | 2 + .../tests/ecma_6/Class/superCallProperBase.js | 8 ++ js/src/vm/Interpreter.cpp | 91 ++++++++++++++++++- js/src/vm/Interpreter.h | 6 ++ js/src/vm/Opcodes.h | 21 ++++- 13 files changed, 214 insertions(+), 24 deletions(-) create mode 100644 js/src/tests/ecma_6/Class/defaultConstructorBase.js create mode 100644 js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 5212e6c8a6cc..e3b6ca1cf4d6 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -7451,7 +7451,6 @@ BytecodeEmitter::emitClass(ParseNode* pn) break; } } - MOZ_ASSERT(constructor, "For now, no default constructors"); bool savedStrictness = sc->setLocalStrictMode(true); @@ -7483,12 +7482,22 @@ BytecodeEmitter::emitClass(ParseNode* pn) return false; } - if (!emitFunction(constructor, !!heritageExpression)) - return false; - - if (constructor->pn_funbox->needsHomeObject()) { - if (!emit2(JSOP_INITHOMEOBJECT, 0)) + if (constructor) { + if (!emitFunction(constructor, !!heritageExpression)) return false; + if (constructor->pn_funbox->needsHomeObject()) { + if (!emit2(JSOP_INITHOMEOBJECT, 0)) + return false; + } + } else { + JSAtom *name = names ? names->innerBinding()->pn_atom : nullptr; + if (heritageExpression) { + if (!emitAtomOp(name, JSOP_DERIVEDCONSTRUCTOR)) + return false; + } else { + if (!emitAtomOp(name, JSOP_CLASSCONSTRUCTOR)) + return false; + } } if (!emit1(JSOP_SWAP)) diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index c855c98cc332..a7abde0c010e 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -6673,12 +6673,6 @@ Parser::classDefinition(YieldHandling yieldHandling, return null(); } - // Default constructors not yet implemented. See bug 1105463 - if (!seenConstructor) { - report(ParseError, false, null(), JSMSG_NO_CLASS_CONSTRUCTOR); - return null(); - } - ParseNode* nameNode = null(); ParseNode* methodsOrBlock = classMethods; if (name) { diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 080f7b82e41f..433c6a2dd5f4 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -1074,11 +1074,23 @@ js::FunctionToString(JSContext* cx, HandleFunction fun, bool bodyOnly, bool lamb } else { MOZ_ASSERT(!fun->isExprBody()); - if ((!bodyOnly && !out.append("() {\n ")) - || !out.append("[native code]") - || (!bodyOnly && !out.append("\n}"))) - { - return nullptr; + if (fun->isNative() && fun->native() == js::DefaultDerivedClassConstructor) { + if ((!bodyOnly && !out.append("(...args) {\n ")) || + !out.append("super(...args);\n}")) + { + return nullptr; + } + } else { + if (!bodyOnly && !out.append("() {\n ")) + return nullptr; + + if (!fun->isNative() || fun->native() != js::DefaultClassConstructor) { + if (!out.append("[native code]")) + return nullptr; + } + + if (!bodyOnly && !out.append("\n}")) + return nullptr; } } return out.finishString(); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorBase.js b/js/src/tests/ecma_6/Class/defaultConstructorBase.js new file mode 100644 index 000000000000..851cf10672ad --- /dev/null +++ b/js/src/tests/ecma_6/Class/defaultConstructorBase.js @@ -0,0 +1,25 @@ +var test = ` + +class base { + method() { return 1; } + *gen() { return 2; } + static sMethod() { return 3; } + get answer() { return 42; } +} + +// Having a default constructor should work, and also not make you lose +// everything for no good reason + +assertEq(Object.getPrototypeOf(new base()), base.prototype); +assertEq(new base().method(), 1); +assertEq(new base().gen().next().value, 2); +assertEq(base.sMethod(), 3); +assertEq(new base().answer, 42); + +`; + +if (classesEnabled()) + eval(test); + +if (typeof reportCompare === 'function') + reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js b/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js new file mode 100644 index 000000000000..0df212bb22ac --- /dev/null +++ b/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js @@ -0,0 +1,15 @@ +var test = ` + +class badBase {} +assertThrowsInstanceOf(badBase, TypeError); + +class badSub extends (class {}) {} +assertThrowsInstanceOf(badSub, TypeError); + +`; + +if (classesEnabled()) + eval(test); + +if (typeof reportCompare === 'function') + reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js index e904d82efdab..17924345380c 100644 --- a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js +++ b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js @@ -3,9 +3,11 @@ var test = ` class base { constructor() { } } class inst extends base { constructor() { super(); } } - Object.setPrototypeOf(inst, Math.sin); +assertThrowsInstanceOf(() => new inst(), TypeError); +class defaultInst extends base { } +Object.setPrototypeOf(inst, Math.sin); assertThrowsInstanceOf(() => new inst(), TypeError); `; diff --git a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js index 577e7d3cda7b..cc79da2a1a58 100644 --- a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js +++ b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js @@ -18,9 +18,11 @@ function get(target, property, receiver) { class inst extends base { constructor() { super(); } } - assertThrowsInstanceOf(()=>new new Proxy(inst, {get})(), TypeError); +class defaultInst extends base {} +assertThrowsInstanceOf(()=>new new Proxy(defaultInst, {get})(), TypeError); + `; if (classesEnabled()) diff --git a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js index 37ab89f5cfa0..807b132000be 100644 --- a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js +++ b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js @@ -10,6 +10,11 @@ function testBase(base) { let inst = new instance(instance, 1); assertEq(Object.getPrototypeOf(inst), instance.prototype); assertEq(inst.calledBase, true); + + class defaultInstance extends base { } + let defInst = new defaultInstance(defaultInstance, 1); + assertEq(Object.getPrototypeOf(defInst), defaultInstance.prototype); + assertEq(defInst.calledBase, true); } class base { @@ -40,7 +45,13 @@ function baseFunc(nt, one) { } testBase(baseFunc); -testBase(new Proxy(baseFunc, {})); + +let handler = {}; +let p = new Proxy(baseFunc, handler); +testBase(p); + +handler.construct = (target, args, nt) => Reflect.construct(target, args, nt); +testBase(p); // Object will have to wait for fixed builtins. diff --git a/js/src/tests/ecma_6/Class/superCallInvalidBase.js b/js/src/tests/ecma_6/Class/superCallInvalidBase.js index 03ffb5e58d33..3f15ae6927e0 100644 --- a/js/src/tests/ecma_6/Class/superCallInvalidBase.js +++ b/js/src/tests/ecma_6/Class/superCallInvalidBase.js @@ -5,6 +5,8 @@ class instance extends null { } assertThrowsInstanceOf(() => new instance(), TypeError); +assertThrowsInstanceOf(() => new class extends null { }(), TypeError); + `; diff --git a/js/src/tests/ecma_6/Class/superCallProperBase.js b/js/src/tests/ecma_6/Class/superCallProperBase.js index a33bbcf45762..262cdc5e8374 100644 --- a/js/src/tests/ecma_6/Class/superCallProperBase.js +++ b/js/src/tests/ecma_6/Class/superCallProperBase.js @@ -24,6 +24,14 @@ Object.setPrototypeOf(inst, base2); assertEq(new inst().base, 2); +// Still works with default constructor + +class defaultInst extends base1 { } + +assertEq(new defaultInst().base, 1); +Object.setPrototypeOf(defaultInst, base2); +assertEq(new defaultInst().base, 2); + `; if (classesEnabled()) diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 2956d21a6968..c70dd2017ed9 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -2087,8 +2087,6 @@ CASE(JSOP_NOP) CASE(JSOP_UNUSED2) CASE(JSOP_UNUSED14) CASE(JSOP_BACKPATCH) -CASE(JSOP_UNUSED167) -CASE(JSOP_UNUSED168) CASE(JSOP_UNUSED169) CASE(JSOP_UNUSED170) CASE(JSOP_UNUSED171) @@ -4167,6 +4165,38 @@ CASE(JSOP_SETTHIS) } END_CASE(JSOP_SETTHIS) +CASE(JSOP_DERIVEDCONSTRUCTOR) +{ + static_assert(JSOP_DERIVEDCONSTRUCTOR_LENGTH == JSOP_CLASSCONSTRUCTOR_LENGTH, + "classconstructor and derivedconstructor must have same length"); + + ReservedRooted proto(&rootValue0, REGS.sp[-1]); + + MOZ_ASSERT(proto.isObject()); + + /* FALL THROUGH */ +CASE(JSOP_CLASSCONSTRUCTOR) + bool derived = JSOp(*REGS.pc) == JSOP_DERIVEDCONSTRUCTOR; + + ReservedRooted protoObj(&rootObject0); + if (derived) + protoObj = &proto.toObject(); + + RootedAtom name(cx, script->getAtom(REGS.pc)); + + JSNative native = derived ? DefaultDerivedClassConstructor : DefaultClassConstructor; + JSFunction* constructor = NewFunctionWithProto(cx, native, 0, JSFunction::NATIVE_CTOR, nullptr, + name, protoObj); + if (!constructor) + goto error; + + if (derived) + REGS.sp[-1].setObject(*constructor); + else + PUSH_OBJECT(*constructor); +} +END_CASE(JSOP_CLASSCONSTRUCTOR) + DEFAULT() { char numBuf[12]; @@ -5028,6 +5058,63 @@ js::ReportUninitializedLexical(JSContext* cx, HandleScript script, jsbytecode* p ReportUninitializedLexical(cx, name); } +bool +js::DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.isConstructing()) { + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); + return false; + } + + RootedObject newTarget(cx, &args.newTarget().toObject()); + RootedValue protoVal(cx); + + if (!GetProperty(cx, newTarget, newTarget, cx->names().prototype, &protoVal)) + return false; + + RootedObject proto(cx); + if (!protoVal.isObject()) { + if (!GetBuiltinPrototype(cx, JSProto_Object, &proto)) + return false; + } else { + proto = &protoVal.toObject(); + } + + JSObject* obj = NewObjectWithGivenProto(cx, &PlainObject::class_, proto); + if (!obj) + return false; + + args.rval().set(ObjectValue(*obj)); + return true; +} + +bool +js::DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.isConstructing()) { + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); + return false; + } + + RootedObject fun(cx, &args.callee()); + RootedObject superFun(cx); + if (!GetPrototype(cx, fun, &superFun)) + return false; + + RootedValue fval(cx, ObjectOrNullValue(superFun)); + if (!IsConstructor(fval)) { + ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, JSDVG_IGNORE_STACK, fval, nullptr); + return false; + } + + ConstructArgs constArgs(cx); + if (!FillArgumentsFromArraylike(cx, constArgs, args)) + return false; + return Construct(cx, fval, constArgs, args.newTarget(), args.rval()); +} + void js::ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, frontend::Definition::Kind declKind) diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 19ae644f2a83..3c6016079ada 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -488,6 +488,12 @@ ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, bool ThrowUninitializedThis(JSContext* cx, AbstractFramePtr frame); +bool +DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp); + +bool +DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp); + } /* namespace js */ #endif /* vm_Interpreter_h */ diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index f7ef7b23cebc..a58760dbb7d0 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -79,6 +79,7 @@ * Object * Array * RegExp + * Class * [Other] */ @@ -1708,8 +1709,24 @@ * Stack: callee, this, args, newTarget => rval */ \ macro(JSOP_SPREADSUPERCALL, 166, "spreadsupercall", NULL, 1, 4, 1, JOF_BYTE|JOF_INVOKE|JOF_TYPESET) \ - macro(JSOP_UNUSED167, 167,"unused167", NULL, 1, 0, 0, JOF_BYTE) \ - macro(JSOP_UNUSED168, 168,"unused168", NULL, 1, 0, 0, JOF_BYTE) \ + /* + * Push a default constructor for a base class literal. + * + * Category: Literals + * Type: Class + * Operands: atom className + * Stack: => constructor + */ \ + macro(JSOP_CLASSCONSTRUCTOR, 167,"classconstructor", NULL, 5, 0, 1, JOF_ATOM) \ + /* + * Push a default constructor for a derived class literal. + * + * Category: Literals + * Type: Class + * Operands: atom className + * Stack: => constructor + */ \ + macro(JSOP_DERIVEDCONSTRUCTOR, 168,"derivedconstructor", NULL, 5, 1, 1, JOF_ATOM) \ macro(JSOP_UNUSED169, 169,"unused169", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED170, 170,"unused170", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED171, 171,"unused171", NULL, 1, 0, 0, JOF_BYTE) \ From 38adbbea677d6da36057c8f9805cd2fd115c1e02 Mon Sep 17 00:00:00 2001 From: Andrew Comminos Date: Fri, 9 Oct 2015 09:36:13 -0700 Subject: [PATCH 011/121] Revert "Bug 1209774 - Transform from GDK coords to layout device pixels before calling DispatchEvent. r=karlt" This reverts commit bc5ba9c53f2a2c08a17b028c0aa8ff24e35847b6. --- widget/gtk/nsWindow.cpp | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp index ab8fbdb20fd1..470e5b8594bb 100644 --- a/widget/gtk/nsWindow.cpp +++ b/widget/gtk/nsWindow.cpp @@ -531,6 +531,10 @@ nsWindow::DispatchEvent(WidgetGUIEvent* aEvent, nsEventStatus& aStatus) debug_DumpEvent(stdout, aEvent->widget, aEvent, nsAutoCString("something"), 0); #endif + // Translate the mouse event into device pixels. + aEvent->refPoint.x = GdkCoordToDevicePixels(aEvent->refPoint.x); + aEvent->refPoint.y = GdkCoordToDevicePixels(aEvent->refPoint.y); + aStatus = nsEventStatus_eIgnore; nsIWidgetListener* listener = mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener; @@ -2435,7 +2439,9 @@ nsWindow::OnEnterNotifyEvent(GdkEventCrossing *aEvent) WidgetMouseEvent event(true, eMouseEnterIntoWidget, this, WidgetMouseEvent::eReal); - event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); + event.refPoint.x = nscoord(aEvent->x); + event.refPoint.y = nscoord(aEvent->y); + event.time = aEvent->time; event.timeStamp = GetEventTimeStamp(aEvent->time); @@ -2476,7 +2482,9 @@ nsWindow::OnLeaveNotifyEvent(GdkEventCrossing *aEvent) WidgetMouseEvent event(true, eMouseExitFromWidget, this, WidgetMouseEvent::eReal); - event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); + event.refPoint.x = nscoord(aEvent->x); + event.refPoint.y = nscoord(aEvent->y); + event.time = aEvent->time; event.timeStamp = GetEventTimeStamp(aEvent->time); @@ -2541,7 +2549,8 @@ nsWindow::OnMotionNotifyEvent(GdkEventMotion *aEvent) event.time = xevent.xmotion.time; event.timeStamp = GetEventTimeStamp(xevent.xmotion.time); #else - event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); + event.refPoint.x = nscoord(aEvent->x); + event.refPoint.y = nscoord(aEvent->y); modifierState = aEvent->state; @@ -2552,10 +2561,11 @@ nsWindow::OnMotionNotifyEvent(GdkEventMotion *aEvent) else { // XXX see OnScrollEvent() if (aEvent->window == mGdkWindow) { - event.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); + event.refPoint.x = nscoord(aEvent->x); + event.refPoint.y = nscoord(aEvent->y); } else { - LayoutDeviceIntPoint point = GdkPointToDevicePixels( - {aEvent->x_root, aEvent->y_root}); + LayoutDeviceIntPoint point(NSToIntFloor(aEvent->x_root), + NSToIntFloor(aEvent->y_root)); event.refPoint = point - WidgetToScreenOffset(); } @@ -2628,10 +2638,11 @@ nsWindow::InitButtonEvent(WidgetMouseEvent& aEvent, { // XXX see OnScrollEvent() if (aGdkEvent->window == mGdkWindow) { - aEvent.refPoint = GdkPointToDevicePixels({aGdkEvent->x, aGdkEvent->y}); + aEvent.refPoint.x = nscoord(aGdkEvent->x); + aEvent.refPoint.y = nscoord(aGdkEvent->y); } else { - LayoutDeviceIntPoint point = GdkPointToDevicePixels( - {aGdkEvent->x_root, aGdkEvent->y_root}); + LayoutDeviceIntPoint point(NSToIntFloor(aGdkEvent->x_root), + NSToIntFloor(aGdkEvent->y_root)); aEvent.refPoint = point - WidgetToScreenOffset(); } @@ -3150,13 +3161,14 @@ nsWindow::OnScrollEvent(GdkEventScroll *aEvent) if (aEvent->window == mGdkWindow) { // we are the window that the event happened on so no need for expensive WidgetToScreenOffset - wheelEvent.refPoint = GdkPointToDevicePixels({aEvent->x, aEvent->y}); + wheelEvent.refPoint.x = nscoord(aEvent->x); + wheelEvent.refPoint.y = nscoord(aEvent->y); } else { // XXX we're never quite sure which GdkWindow the event came from due to our custom bubbling // in scroll_event_cb(), so use ScreenToWidget to translate the screen root coordinates into // coordinates relative to this widget. - LayoutDeviceIntPoint point = GdkPointToDevicePixels( - {aEvent->x_root, aEvent->y_root}); + LayoutDeviceIntPoint point(NSToIntFloor(aEvent->x_root), + NSToIntFloor(aEvent->y_root)); wheelEvent.refPoint = point - WidgetToScreenOffset(); } From e97be1e0dcb93f3b70d8d95fec6ac5d17aebce7d Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Fri, 9 Oct 2015 09:55:35 -0700 Subject: [PATCH 012/121] Backed out changeset cf4cdc781c72 (bug 1105463) for build bustage CLOSED TREE --- js/src/frontend/BytecodeEmitter.cpp | 21 ++--- js/src/frontend/Parser.cpp | 6 ++ js/src/jsfun.cpp | 22 +---- .../ecma_6/Class/defaultConstructorBase.js | 25 ----- .../Class/defaultConstructorNotCallable.js | 15 --- .../Class/superCallBadDynamicSuperClass.js | 4 +- .../Class/superCallBadNewTargetPrototype.js | 4 +- .../ecma_6/Class/superCallBaseInvoked.js | 13 +-- .../ecma_6/Class/superCallInvalidBase.js | 2 - .../tests/ecma_6/Class/superCallProperBase.js | 8 -- js/src/vm/Interpreter.cpp | 91 +------------------ js/src/vm/Interpreter.h | 6 -- js/src/vm/Opcodes.h | 21 +---- 13 files changed, 24 insertions(+), 214 deletions(-) delete mode 100644 js/src/tests/ecma_6/Class/defaultConstructorBase.js delete mode 100644 js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index e3b6ca1cf4d6..5212e6c8a6cc 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -7451,6 +7451,7 @@ BytecodeEmitter::emitClass(ParseNode* pn) break; } } + MOZ_ASSERT(constructor, "For now, no default constructors"); bool savedStrictness = sc->setLocalStrictMode(true); @@ -7482,22 +7483,12 @@ BytecodeEmitter::emitClass(ParseNode* pn) return false; } - if (constructor) { - if (!emitFunction(constructor, !!heritageExpression)) + if (!emitFunction(constructor, !!heritageExpression)) + return false; + + if (constructor->pn_funbox->needsHomeObject()) { + if (!emit2(JSOP_INITHOMEOBJECT, 0)) return false; - if (constructor->pn_funbox->needsHomeObject()) { - if (!emit2(JSOP_INITHOMEOBJECT, 0)) - return false; - } - } else { - JSAtom *name = names ? names->innerBinding()->pn_atom : nullptr; - if (heritageExpression) { - if (!emitAtomOp(name, JSOP_DERIVEDCONSTRUCTOR)) - return false; - } else { - if (!emitAtomOp(name, JSOP_CLASSCONSTRUCTOR)) - return false; - } } if (!emit1(JSOP_SWAP)) diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index a7abde0c010e..c855c98cc332 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -6673,6 +6673,12 @@ Parser::classDefinition(YieldHandling yieldHandling, return null(); } + // Default constructors not yet implemented. See bug 1105463 + if (!seenConstructor) { + report(ParseError, false, null(), JSMSG_NO_CLASS_CONSTRUCTOR); + return null(); + } + ParseNode* nameNode = null(); ParseNode* methodsOrBlock = classMethods; if (name) { diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 433c6a2dd5f4..080f7b82e41f 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -1074,23 +1074,11 @@ js::FunctionToString(JSContext* cx, HandleFunction fun, bool bodyOnly, bool lamb } else { MOZ_ASSERT(!fun->isExprBody()); - if (fun->isNative() && fun->native() == js::DefaultDerivedClassConstructor) { - if ((!bodyOnly && !out.append("(...args) {\n ")) || - !out.append("super(...args);\n}")) - { - return nullptr; - } - } else { - if (!bodyOnly && !out.append("() {\n ")) - return nullptr; - - if (!fun->isNative() || fun->native() != js::DefaultClassConstructor) { - if (!out.append("[native code]")) - return nullptr; - } - - if (!bodyOnly && !out.append("\n}")) - return nullptr; + if ((!bodyOnly && !out.append("() {\n ")) + || !out.append("[native code]") + || (!bodyOnly && !out.append("\n}"))) + { + return nullptr; } } return out.finishString(); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorBase.js b/js/src/tests/ecma_6/Class/defaultConstructorBase.js deleted file mode 100644 index 851cf10672ad..000000000000 --- a/js/src/tests/ecma_6/Class/defaultConstructorBase.js +++ /dev/null @@ -1,25 +0,0 @@ -var test = ` - -class base { - method() { return 1; } - *gen() { return 2; } - static sMethod() { return 3; } - get answer() { return 42; } -} - -// Having a default constructor should work, and also not make you lose -// everything for no good reason - -assertEq(Object.getPrototypeOf(new base()), base.prototype); -assertEq(new base().method(), 1); -assertEq(new base().gen().next().value, 2); -assertEq(base.sMethod(), 3); -assertEq(new base().answer, 42); - -`; - -if (classesEnabled()) - eval(test); - -if (typeof reportCompare === 'function') - reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js b/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js deleted file mode 100644 index 0df212bb22ac..000000000000 --- a/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js +++ /dev/null @@ -1,15 +0,0 @@ -var test = ` - -class badBase {} -assertThrowsInstanceOf(badBase, TypeError); - -class badSub extends (class {}) {} -assertThrowsInstanceOf(badSub, TypeError); - -`; - -if (classesEnabled()) - eval(test); - -if (typeof reportCompare === 'function') - reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js index 17924345380c..e904d82efdab 100644 --- a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js +++ b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js @@ -3,11 +3,9 @@ var test = ` class base { constructor() { } } class inst extends base { constructor() { super(); } } -Object.setPrototypeOf(inst, Math.sin); -assertThrowsInstanceOf(() => new inst(), TypeError); -class defaultInst extends base { } Object.setPrototypeOf(inst, Math.sin); + assertThrowsInstanceOf(() => new inst(), TypeError); `; diff --git a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js index cc79da2a1a58..577e7d3cda7b 100644 --- a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js +++ b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js @@ -18,10 +18,8 @@ function get(target, property, receiver) { class inst extends base { constructor() { super(); } } -assertThrowsInstanceOf(()=>new new Proxy(inst, {get})(), TypeError); -class defaultInst extends base {} -assertThrowsInstanceOf(()=>new new Proxy(defaultInst, {get})(), TypeError); +assertThrowsInstanceOf(()=>new new Proxy(inst, {get})(), TypeError); `; diff --git a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js index 807b132000be..37ab89f5cfa0 100644 --- a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js +++ b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js @@ -10,11 +10,6 @@ function testBase(base) { let inst = new instance(instance, 1); assertEq(Object.getPrototypeOf(inst), instance.prototype); assertEq(inst.calledBase, true); - - class defaultInstance extends base { } - let defInst = new defaultInstance(defaultInstance, 1); - assertEq(Object.getPrototypeOf(defInst), defaultInstance.prototype); - assertEq(defInst.calledBase, true); } class base { @@ -45,13 +40,7 @@ function baseFunc(nt, one) { } testBase(baseFunc); - -let handler = {}; -let p = new Proxy(baseFunc, handler); -testBase(p); - -handler.construct = (target, args, nt) => Reflect.construct(target, args, nt); -testBase(p); +testBase(new Proxy(baseFunc, {})); // Object will have to wait for fixed builtins. diff --git a/js/src/tests/ecma_6/Class/superCallInvalidBase.js b/js/src/tests/ecma_6/Class/superCallInvalidBase.js index 3f15ae6927e0..03ffb5e58d33 100644 --- a/js/src/tests/ecma_6/Class/superCallInvalidBase.js +++ b/js/src/tests/ecma_6/Class/superCallInvalidBase.js @@ -5,8 +5,6 @@ class instance extends null { } assertThrowsInstanceOf(() => new instance(), TypeError); -assertThrowsInstanceOf(() => new class extends null { }(), TypeError); - `; diff --git a/js/src/tests/ecma_6/Class/superCallProperBase.js b/js/src/tests/ecma_6/Class/superCallProperBase.js index 262cdc5e8374..a33bbcf45762 100644 --- a/js/src/tests/ecma_6/Class/superCallProperBase.js +++ b/js/src/tests/ecma_6/Class/superCallProperBase.js @@ -24,14 +24,6 @@ Object.setPrototypeOf(inst, base2); assertEq(new inst().base, 2); -// Still works with default constructor - -class defaultInst extends base1 { } - -assertEq(new defaultInst().base, 1); -Object.setPrototypeOf(defaultInst, base2); -assertEq(new defaultInst().base, 2); - `; if (classesEnabled()) diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index c70dd2017ed9..2956d21a6968 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -2087,6 +2087,8 @@ CASE(JSOP_NOP) CASE(JSOP_UNUSED2) CASE(JSOP_UNUSED14) CASE(JSOP_BACKPATCH) +CASE(JSOP_UNUSED167) +CASE(JSOP_UNUSED168) CASE(JSOP_UNUSED169) CASE(JSOP_UNUSED170) CASE(JSOP_UNUSED171) @@ -4165,38 +4167,6 @@ CASE(JSOP_SETTHIS) } END_CASE(JSOP_SETTHIS) -CASE(JSOP_DERIVEDCONSTRUCTOR) -{ - static_assert(JSOP_DERIVEDCONSTRUCTOR_LENGTH == JSOP_CLASSCONSTRUCTOR_LENGTH, - "classconstructor and derivedconstructor must have same length"); - - ReservedRooted proto(&rootValue0, REGS.sp[-1]); - - MOZ_ASSERT(proto.isObject()); - - /* FALL THROUGH */ -CASE(JSOP_CLASSCONSTRUCTOR) - bool derived = JSOp(*REGS.pc) == JSOP_DERIVEDCONSTRUCTOR; - - ReservedRooted protoObj(&rootObject0); - if (derived) - protoObj = &proto.toObject(); - - RootedAtom name(cx, script->getAtom(REGS.pc)); - - JSNative native = derived ? DefaultDerivedClassConstructor : DefaultClassConstructor; - JSFunction* constructor = NewFunctionWithProto(cx, native, 0, JSFunction::NATIVE_CTOR, nullptr, - name, protoObj); - if (!constructor) - goto error; - - if (derived) - REGS.sp[-1].setObject(*constructor); - else - PUSH_OBJECT(*constructor); -} -END_CASE(JSOP_CLASSCONSTRUCTOR) - DEFAULT() { char numBuf[12]; @@ -5058,63 +5028,6 @@ js::ReportUninitializedLexical(JSContext* cx, HandleScript script, jsbytecode* p ReportUninitializedLexical(cx, name); } -bool -js::DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.isConstructing()) { - JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); - return false; - } - - RootedObject newTarget(cx, &args.newTarget().toObject()); - RootedValue protoVal(cx); - - if (!GetProperty(cx, newTarget, newTarget, cx->names().prototype, &protoVal)) - return false; - - RootedObject proto(cx); - if (!protoVal.isObject()) { - if (!GetBuiltinPrototype(cx, JSProto_Object, &proto)) - return false; - } else { - proto = &protoVal.toObject(); - } - - JSObject* obj = NewObjectWithGivenProto(cx, &PlainObject::class_, proto); - if (!obj) - return false; - - args.rval().set(ObjectValue(*obj)); - return true; -} - -bool -js::DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - if (!args.isConstructing()) { - JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); - return false; - } - - RootedObject fun(cx, &args.callee()); - RootedObject superFun(cx); - if (!GetPrototype(cx, fun, &superFun)) - return false; - - RootedValue fval(cx, ObjectOrNullValue(superFun)); - if (!IsConstructor(fval)) { - ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, JSDVG_IGNORE_STACK, fval, nullptr); - return false; - } - - ConstructArgs constArgs(cx); - if (!FillArgumentsFromArraylike(cx, constArgs, args)) - return false; - return Construct(cx, fval, constArgs, args.newTarget(), args.rval()); -} - void js::ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, frontend::Definition::Kind declKind) diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 3c6016079ada..19ae644f2a83 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -488,12 +488,6 @@ ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, bool ThrowUninitializedThis(JSContext* cx, AbstractFramePtr frame); -bool -DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp); - -bool -DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp); - } /* namespace js */ #endif /* vm_Interpreter_h */ diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index a58760dbb7d0..f7ef7b23cebc 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -79,7 +79,6 @@ * Object * Array * RegExp - * Class * [Other] */ @@ -1709,24 +1708,8 @@ * Stack: callee, this, args, newTarget => rval */ \ macro(JSOP_SPREADSUPERCALL, 166, "spreadsupercall", NULL, 1, 4, 1, JOF_BYTE|JOF_INVOKE|JOF_TYPESET) \ - /* - * Push a default constructor for a base class literal. - * - * Category: Literals - * Type: Class - * Operands: atom className - * Stack: => constructor - */ \ - macro(JSOP_CLASSCONSTRUCTOR, 167,"classconstructor", NULL, 5, 0, 1, JOF_ATOM) \ - /* - * Push a default constructor for a derived class literal. - * - * Category: Literals - * Type: Class - * Operands: atom className - * Stack: => constructor - */ \ - macro(JSOP_DERIVEDCONSTRUCTOR, 168,"derivedconstructor", NULL, 5, 1, 1, JOF_ATOM) \ + macro(JSOP_UNUSED167, 167,"unused167", NULL, 1, 0, 0, JOF_BYTE) \ + macro(JSOP_UNUSED168, 168,"unused168", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED169, 169,"unused169", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED170, 170,"unused170", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED171, 171,"unused171", NULL, 1, 0, 0, JOF_BYTE) \ From 894f6502ecb180e98e97a80dd3670bb0ad36e1c4 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Fri, 9 Oct 2015 10:03:48 -0400 Subject: [PATCH 013/121] Bug 1213283 - Add option to only print task names in |mach taskcluster-graph|, r=dustin --HG-- extra : commitid : B1GqUmUNAgr extra : rebase_source : 77a628083140070356e5374390d59187593ae5dc --- testing/taskcluster/mach_commands.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/testing/taskcluster/mach_commands.py b/testing/taskcluster/mach_commands.py index 7794f36afd51..f0a4b1fdadf1 100644 --- a/testing/taskcluster/mach_commands.py +++ b/testing/taskcluster/mach_commands.py @@ -6,6 +6,7 @@ from __future__ import absolute_import +from collections import defaultdict import os import json import copy @@ -288,6 +289,9 @@ class Graph(object): action="store_true", dest="interactive", help="Run the tasks with the interactive feature enabled") + @CommandArgument('--print-names-only', + action='store_true', default=False, + help="Only print the names of each scheduled task, one per line.") def create_graph(self, **params): from taskcluster_graph.commit_parser import parse_commit from slugid import nice as slugid @@ -504,6 +508,27 @@ class Graph(object): graph['scopes'] = list(set(graph['scopes'])) + if params['print_names_only']: + tIDs = defaultdict(list) + + def print_task(task, indent=0): + print('{}- {}'.format(' ' * indent, task['task']['metadata']['name'])) + + for child in tIDs[task['taskId']]: + print_task(child, indent=indent+2) + + # build a dependency map + for task in graph['tasks']: + if 'requires' in task: + for tID in task['requires']: + tIDs[tID].append(task) + + # recursively print root tasks + for task in graph['tasks']: + if 'requires' not in task: + print_task(task) + return + # When we are extending the graph remove extra fields... if params['ci'] is True: graph.pop('scopes', None) From 747f1dd47c36a250c4c0fc5f8ffbd73a6c1eb01e Mon Sep 17 00:00:00 2001 From: Andrew McCreight Date: Fri, 9 Oct 2015 10:24:23 -0700 Subject: [PATCH 014/121] Bug 931283, part 1 - Rename nsVariant to nsVariantBase. r=froydnj Leave a typedef for compatibility. nsVariant will be defined as a separate class in the next patch. Also, remove an obsolete comment and fix some whitespace. --- xpcom/ds/nsVariant.cpp | 120 ++++++++++++++++++++--------------------- xpcom/ds/nsVariant.h | 14 +++-- 2 files changed, 66 insertions(+), 68 deletions(-) diff --git a/xpcom/ds/nsVariant.cpp b/xpcom/ds/nsVariant.cpp index 4cff91b94739..440726d5ab01 100644 --- a/xpcom/ds/nsVariant.cpp +++ b/xpcom/ds/nsVariant.cpp @@ -1667,9 +1667,9 @@ nsDiscriminatedUnion::Traverse(nsCycleCollectionTraversalCallback& aCb) const /***************************************************************************/ // members... -NS_IMPL_ISUPPORTS(nsVariant, nsIVariant, nsIWritableVariant) +NS_IMPL_ISUPPORTS(nsVariantBase, nsIVariant, nsIWritableVariant) -nsVariant::nsVariant() +nsVariantBase::nsVariantBase() : mWritable(true) { #ifdef DEBUG @@ -1725,104 +1725,104 @@ nsVariant::nsVariant() // 'ConvertTo' functions. NS_IMETHODIMP -nsVariant::GetDataType(uint16_t* aDataType) +nsVariantBase::GetDataType(uint16_t* aDataType) { *aDataType = mData.GetType(); return NS_OK; } NS_IMETHODIMP -nsVariant::GetAsInt8(uint8_t* aResult) +nsVariantBase::GetAsInt8(uint8_t* aResult) { return mData.ConvertToInt8(aResult); } NS_IMETHODIMP -nsVariant::GetAsInt16(int16_t* aResult) +nsVariantBase::GetAsInt16(int16_t* aResult) { return mData.ConvertToInt16(aResult); } NS_IMETHODIMP -nsVariant::GetAsInt32(int32_t* aResult) +nsVariantBase::GetAsInt32(int32_t* aResult) { return mData.ConvertToInt32(aResult); } NS_IMETHODIMP -nsVariant::GetAsInt64(int64_t* aResult) +nsVariantBase::GetAsInt64(int64_t* aResult) { return mData.ConvertToInt64(aResult); } NS_IMETHODIMP -nsVariant::GetAsUint8(uint8_t* aResult) +nsVariantBase::GetAsUint8(uint8_t* aResult) { return mData.ConvertToUint8(aResult); } NS_IMETHODIMP -nsVariant::GetAsUint16(uint16_t* aResult) +nsVariantBase::GetAsUint16(uint16_t* aResult) { return mData.ConvertToUint16(aResult); } NS_IMETHODIMP -nsVariant::GetAsUint32(uint32_t* aResult) +nsVariantBase::GetAsUint32(uint32_t* aResult) { return mData.ConvertToUint32(aResult); } NS_IMETHODIMP -nsVariant::GetAsUint64(uint64_t* aResult) +nsVariantBase::GetAsUint64(uint64_t* aResult) { return mData.ConvertToUint64(aResult); } NS_IMETHODIMP -nsVariant::GetAsFloat(float* aResult) +nsVariantBase::GetAsFloat(float* aResult) { return mData.ConvertToFloat(aResult); } NS_IMETHODIMP -nsVariant::GetAsDouble(double* aResult) +nsVariantBase::GetAsDouble(double* aResult) { return mData.ConvertToDouble(aResult); } NS_IMETHODIMP -nsVariant::GetAsBool(bool* aResult) +nsVariantBase::GetAsBool(bool* aResult) { return mData.ConvertToBool(aResult); } NS_IMETHODIMP -nsVariant::GetAsChar(char* aResult) +nsVariantBase::GetAsChar(char* aResult) { return mData.ConvertToChar(aResult); } NS_IMETHODIMP -nsVariant::GetAsWChar(char16_t* aResult) +nsVariantBase::GetAsWChar(char16_t* aResult) { return mData.ConvertToWChar(aResult); } NS_IMETHODIMP_(nsresult) -nsVariant::GetAsID(nsID* aResult) +nsVariantBase::GetAsID(nsID* aResult) { return mData.ConvertToID(aResult); } NS_IMETHODIMP -nsVariant::GetAsAString(nsAString& aResult) +nsVariantBase::GetAsAString(nsAString& aResult) { return mData.ConvertToAString(aResult); } NS_IMETHODIMP -nsVariant::GetAsDOMString(nsAString& aResult) +nsVariantBase::GetAsDOMString(nsAString& aResult) { // A DOMString maps to an AString internally, so we can re-use // ConvertToAString here. @@ -1830,63 +1830,63 @@ nsVariant::GetAsDOMString(nsAString& aResult) } NS_IMETHODIMP -nsVariant::GetAsACString(nsACString& aResult) +nsVariantBase::GetAsACString(nsACString& aResult) { return mData.ConvertToACString(aResult); } NS_IMETHODIMP -nsVariant::GetAsAUTF8String(nsAUTF8String& aResult) +nsVariantBase::GetAsAUTF8String(nsAUTF8String& aResult) { return mData.ConvertToAUTF8String(aResult); } NS_IMETHODIMP -nsVariant::GetAsString(char** aResult) +nsVariantBase::GetAsString(char** aResult) { return mData.ConvertToString(aResult); } NS_IMETHODIMP -nsVariant::GetAsWString(char16_t** aResult) +nsVariantBase::GetAsWString(char16_t** aResult) { return mData.ConvertToWString(aResult); } NS_IMETHODIMP -nsVariant::GetAsISupports(nsISupports** aResult) +nsVariantBase::GetAsISupports(nsISupports** aResult) { return mData.ConvertToISupports(aResult); } NS_IMETHODIMP -nsVariant::GetAsJSVal(JS::MutableHandleValue) +nsVariantBase::GetAsJSVal(JS::MutableHandleValue) { // Can only get the jsval from an XPCVariant. return NS_ERROR_CANNOT_CONVERT_DATA; } NS_IMETHODIMP -nsVariant::GetAsInterface(nsIID** aIID, void** aInterface) +nsVariantBase::GetAsInterface(nsIID** aIID, void** aInterface) { return mData.ConvertToInterface(aIID, aInterface); } NS_IMETHODIMP_(nsresult) -nsVariant::GetAsArray(uint16_t* aType, nsIID* aIID, +nsVariantBase::GetAsArray(uint16_t* aType, nsIID* aIID, uint32_t* aCount, void** aPtr) { return mData.ConvertToArray(aType, aIID, aCount, aPtr); } NS_IMETHODIMP -nsVariant::GetAsStringWithSize(uint32_t* aSize, char** aStr) +nsVariantBase::GetAsStringWithSize(uint32_t* aSize, char** aStr) { return mData.ConvertToStringWithSize(aSize, aStr); } NS_IMETHODIMP -nsVariant::GetAsWStringWithSize(uint32_t* aSize, char16_t** aStr) +nsVariantBase::GetAsWStringWithSize(uint32_t* aSize, char16_t** aStr) { return mData.ConvertToWStringWithSize(aSize, aStr); } @@ -1894,13 +1894,13 @@ nsVariant::GetAsWStringWithSize(uint32_t* aSize, char16_t** aStr) /***************************************************************************/ NS_IMETHODIMP -nsVariant::GetWritable(bool* aWritable) +nsVariantBase::GetWritable(bool* aWritable) { *aWritable = mWritable; return NS_OK; } NS_IMETHODIMP -nsVariant::SetWritable(bool aWritable) +nsVariantBase::SetWritable(bool aWritable) { if (!mWritable && aWritable) { return NS_ERROR_FAILURE; @@ -1915,7 +1915,7 @@ nsVariant::SetWritable(bool aWritable) // 'SetFrom' functions. NS_IMETHODIMP -nsVariant::SetAsInt8(uint8_t aValue) +nsVariantBase::SetAsInt8(uint8_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1924,7 +1924,7 @@ nsVariant::SetAsInt8(uint8_t aValue) } NS_IMETHODIMP -nsVariant::SetAsInt16(int16_t aValue) +nsVariantBase::SetAsInt16(int16_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1933,7 +1933,7 @@ nsVariant::SetAsInt16(int16_t aValue) } NS_IMETHODIMP -nsVariant::SetAsInt32(int32_t aValue) +nsVariantBase::SetAsInt32(int32_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1942,7 +1942,7 @@ nsVariant::SetAsInt32(int32_t aValue) } NS_IMETHODIMP -nsVariant::SetAsInt64(int64_t aValue) +nsVariantBase::SetAsInt64(int64_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1951,7 +1951,7 @@ nsVariant::SetAsInt64(int64_t aValue) } NS_IMETHODIMP -nsVariant::SetAsUint8(uint8_t aValue) +nsVariantBase::SetAsUint8(uint8_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1960,7 +1960,7 @@ nsVariant::SetAsUint8(uint8_t aValue) } NS_IMETHODIMP -nsVariant::SetAsUint16(uint16_t aValue) +nsVariantBase::SetAsUint16(uint16_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1969,7 +1969,7 @@ nsVariant::SetAsUint16(uint16_t aValue) } NS_IMETHODIMP -nsVariant::SetAsUint32(uint32_t aValue) +nsVariantBase::SetAsUint32(uint32_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1978,7 +1978,7 @@ nsVariant::SetAsUint32(uint32_t aValue) } NS_IMETHODIMP -nsVariant::SetAsUint64(uint64_t aValue) +nsVariantBase::SetAsUint64(uint64_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1987,7 +1987,7 @@ nsVariant::SetAsUint64(uint64_t aValue) } NS_IMETHODIMP -nsVariant::SetAsFloat(float aValue) +nsVariantBase::SetAsFloat(float aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -1996,7 +1996,7 @@ nsVariant::SetAsFloat(float aValue) } NS_IMETHODIMP -nsVariant::SetAsDouble(double aValue) +nsVariantBase::SetAsDouble(double aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2005,7 +2005,7 @@ nsVariant::SetAsDouble(double aValue) } NS_IMETHODIMP -nsVariant::SetAsBool(bool aValue) +nsVariantBase::SetAsBool(bool aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2014,7 +2014,7 @@ nsVariant::SetAsBool(bool aValue) } NS_IMETHODIMP -nsVariant::SetAsChar(char aValue) +nsVariantBase::SetAsChar(char aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2023,7 +2023,7 @@ nsVariant::SetAsChar(char aValue) } NS_IMETHODIMP -nsVariant::SetAsWChar(char16_t aValue) +nsVariantBase::SetAsWChar(char16_t aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2032,7 +2032,7 @@ nsVariant::SetAsWChar(char16_t aValue) } NS_IMETHODIMP -nsVariant::SetAsID(const nsID& aValue) +nsVariantBase::SetAsID(const nsID& aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2041,7 +2041,7 @@ nsVariant::SetAsID(const nsID& aValue) } NS_IMETHODIMP -nsVariant::SetAsAString(const nsAString& aValue) +nsVariantBase::SetAsAString(const nsAString& aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2050,7 +2050,7 @@ nsVariant::SetAsAString(const nsAString& aValue) } NS_IMETHODIMP -nsVariant::SetAsDOMString(const nsAString& aValue) +nsVariantBase::SetAsDOMString(const nsAString& aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2060,7 +2060,7 @@ nsVariant::SetAsDOMString(const nsAString& aValue) } NS_IMETHODIMP -nsVariant::SetAsACString(const nsACString& aValue) +nsVariantBase::SetAsACString(const nsACString& aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2069,7 +2069,7 @@ nsVariant::SetAsACString(const nsACString& aValue) } NS_IMETHODIMP -nsVariant::SetAsAUTF8String(const nsAUTF8String& aValue) +nsVariantBase::SetAsAUTF8String(const nsAUTF8String& aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2078,7 +2078,7 @@ nsVariant::SetAsAUTF8String(const nsAUTF8String& aValue) } NS_IMETHODIMP -nsVariant::SetAsString(const char* aValue) +nsVariantBase::SetAsString(const char* aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2087,7 +2087,7 @@ nsVariant::SetAsString(const char* aValue) } NS_IMETHODIMP -nsVariant::SetAsWString(const char16_t* aValue) +nsVariantBase::SetAsWString(const char16_t* aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2096,7 +2096,7 @@ nsVariant::SetAsWString(const char16_t* aValue) } NS_IMETHODIMP -nsVariant::SetAsISupports(nsISupports* aValue) +nsVariantBase::SetAsISupports(nsISupports* aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2105,7 +2105,7 @@ nsVariant::SetAsISupports(nsISupports* aValue) } NS_IMETHODIMP -nsVariant::SetAsInterface(const nsIID& aIID, void* aInterface) +nsVariantBase::SetAsInterface(const nsIID& aIID, void* aInterface) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2114,7 +2114,7 @@ nsVariant::SetAsInterface(const nsIID& aIID, void* aInterface) } NS_IMETHODIMP -nsVariant::SetAsArray(uint16_t aType, const nsIID* aIID, +nsVariantBase::SetAsArray(uint16_t aType, const nsIID* aIID, uint32_t aCount, void* aPtr) { if (!mWritable) { @@ -2124,7 +2124,7 @@ nsVariant::SetAsArray(uint16_t aType, const nsIID* aIID, } NS_IMETHODIMP -nsVariant::SetAsStringWithSize(uint32_t aSize, const char* aStr) +nsVariantBase::SetAsStringWithSize(uint32_t aSize, const char* aStr) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2133,7 +2133,7 @@ nsVariant::SetAsStringWithSize(uint32_t aSize, const char* aStr) } NS_IMETHODIMP -nsVariant::SetAsWStringWithSize(uint32_t aSize, const char16_t* aStr) +nsVariantBase::SetAsWStringWithSize(uint32_t aSize, const char16_t* aStr) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2142,7 +2142,7 @@ nsVariant::SetAsWStringWithSize(uint32_t aSize, const char16_t* aStr) } NS_IMETHODIMP -nsVariant::SetAsVoid() +nsVariantBase::SetAsVoid() { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2151,7 +2151,7 @@ nsVariant::SetAsVoid() } NS_IMETHODIMP -nsVariant::SetAsEmpty() +nsVariantBase::SetAsEmpty() { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2160,7 +2160,7 @@ nsVariant::SetAsEmpty() } NS_IMETHODIMP -nsVariant::SetAsEmptyArray() +nsVariantBase::SetAsEmptyArray() { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; @@ -2169,7 +2169,7 @@ nsVariant::SetAsEmptyArray() } NS_IMETHODIMP -nsVariant::SetFromVariant(nsIVariant* aValue) +nsVariantBase::SetFromVariant(nsIVariant* aValue) { if (!mWritable) { return NS_ERROR_OBJECT_IS_IMMUTABLE; diff --git a/xpcom/ds/nsVariant.h b/xpcom/ds/nsVariant.h index cf5709d387eb..daf5835f6e21 100644 --- a/xpcom/ds/nsVariant.h +++ b/xpcom/ds/nsVariant.h @@ -173,28 +173,26 @@ public: * these objects. They are created 'empty' and 'writable'. * * nsIVariant users won't usually need to see this class. - * - * This class also has static helper methods that nsIVariant *implementors* can - * use to help them do all the 'standard' nsIVariant data conversions. */ - -class nsVariant final : public nsIWritableVariant +class nsVariantBase final : public nsIWritableVariant { public: NS_DECL_ISUPPORTS NS_DECL_NSIVARIANT NS_DECL_NSIWRITABLEVARIANT - nsVariant(); + nsVariantBase(); private: - ~nsVariant() {}; + ~nsVariantBase() {}; protected: nsDiscriminatedUnion mData; - bool mWritable; + bool mWritable; }; +typedef nsVariantBase nsVariant; + /** * Users of nsIVariant should be using the contractID and not this CID. * - see NS_VARIANT_CONTRACTID in nsIVariant.idl. From 642c4344e2fab6e52188892e0824eef9a3258a79 Mon Sep 17 00:00:00 2001 From: Andrew McCreight Date: Fri, 9 Oct 2015 10:24:23 -0700 Subject: [PATCH 015/121] Bug 931283, part 2 - Split out nsVariant into a subclass. r=froydnj --- xpcom/ds/nsVariant.cpp | 6 ++++-- xpcom/ds/nsVariant.h | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/xpcom/ds/nsVariant.cpp b/xpcom/ds/nsVariant.cpp index 440726d5ab01..a4bd8626222d 100644 --- a/xpcom/ds/nsVariant.cpp +++ b/xpcom/ds/nsVariant.cpp @@ -1667,8 +1667,6 @@ nsDiscriminatedUnion::Traverse(nsCycleCollectionTraversalCallback& aCb) const /***************************************************************************/ // members... -NS_IMPL_ISUPPORTS(nsVariantBase, nsIVariant, nsIWritableVariant) - nsVariantBase::nsVariantBase() : mWritable(true) { @@ -2176,3 +2174,7 @@ nsVariantBase::SetFromVariant(nsIVariant* aValue) } return mData.SetFromVariant(aValue); } + +/* nsVariant implementation */ + +NS_IMPL_ISUPPORTS(nsVariant, nsIVariant, nsIWritableVariant) diff --git a/xpcom/ds/nsVariant.h b/xpcom/ds/nsVariant.h index daf5835f6e21..197041807159 100644 --- a/xpcom/ds/nsVariant.h +++ b/xpcom/ds/nsVariant.h @@ -174,24 +174,31 @@ public: * * nsIVariant users won't usually need to see this class. */ -class nsVariantBase final : public nsIWritableVariant +class nsVariantBase : public nsIWritableVariant { public: - NS_DECL_ISUPPORTS NS_DECL_NSIVARIANT NS_DECL_NSIWRITABLEVARIANT nsVariantBase(); -private: +protected: ~nsVariantBase() {}; -protected: nsDiscriminatedUnion mData; bool mWritable; }; -typedef nsVariantBase nsVariant; +class nsVariant final : public nsVariantBase +{ +public: + NS_DECL_ISUPPORTS + + nsVariant() {}; + +private: + ~nsVariant() {}; +}; /** * Users of nsIVariant should be using the contractID and not this CID. From c97953414d09a579b9ebec501ca4735117e0b8d5 Mon Sep 17 00:00:00 2001 From: Andrew McCreight Date: Fri, 9 Oct 2015 10:24:23 -0700 Subject: [PATCH 016/121] Bug 931283, part 3 - Implement a cycle collected version of nsVariant. r=froydnj Also, use this class for the component manager etc. in XPCOM. --- xpcom/build/XPCOMInit.cpp | 2 +- xpcom/build/XPCOMModule.inc | 2 +- xpcom/ds/nsVariant.cpp | 21 +++++++++++++++++++++ xpcom/ds/nsVariant.h | 15 +++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/xpcom/build/XPCOMInit.cpp b/xpcom/build/XPCOMInit.cpp index 3ab7445d9423..1cf814960f53 100644 --- a/xpcom/build/XPCOMInit.cpp +++ b/xpcom/build/XPCOMInit.cpp @@ -213,7 +213,7 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsStorageStream) NS_GENERIC_FACTORY_CONSTRUCTOR(nsVersionComparatorImpl) NS_GENERIC_FACTORY_CONSTRUCTOR(nsScriptableBase64Encoder) -NS_GENERIC_FACTORY_CONSTRUCTOR(nsVariant) +NS_GENERIC_FACTORY_CONSTRUCTOR(nsVariantCC) NS_GENERIC_FACTORY_CONSTRUCTOR(nsHashPropertyBagCC) diff --git a/xpcom/build/XPCOMModule.inc b/xpcom/build/XPCOMModule.inc index cc2e3424f2b7..52d7497dfa63 100644 --- a/xpcom/build/XPCOMModule.inc +++ b/xpcom/build/XPCOMModule.inc @@ -57,7 +57,7 @@ COMPONENT(STRINGINPUTSTREAM, nsStringInputStreamConstructor) COMPONENT(MULTIPLEXINPUTSTREAM, nsMultiplexInputStreamConstructor) - COMPONENT(VARIANT, nsVariantConstructor) + COMPONENT(VARIANT, nsVariantCCConstructor) COMPONENT(INTERFACEINFOMANAGER_SERVICE, nsXPTIInterfaceInfoManagerGetSingleton) COMPONENT(HASH_PROPERTY_BAG, nsHashPropertyBagCCConstructor) diff --git a/xpcom/ds/nsVariant.cpp b/xpcom/ds/nsVariant.cpp index a4bd8626222d..1c4ad76289cd 100644 --- a/xpcom/ds/nsVariant.cpp +++ b/xpcom/ds/nsVariant.cpp @@ -2178,3 +2178,24 @@ nsVariantBase::SetFromVariant(nsIVariant* aValue) /* nsVariant implementation */ NS_IMPL_ISUPPORTS(nsVariant, nsIVariant, nsIWritableVariant) + + +/* nsVariantCC implementation */ +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsVariantCC) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIVariant) + NS_INTERFACE_MAP_ENTRY(nsIWritableVariant) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsVariantCC) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsVariantCC) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsVariantCC) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsVariantCC) + tmp->mData.Traverse(cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsVariantCC) + tmp->mData.Cleanup(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END diff --git a/xpcom/ds/nsVariant.h b/xpcom/ds/nsVariant.h index 197041807159..0029cd3b7fd1 100644 --- a/xpcom/ds/nsVariant.h +++ b/xpcom/ds/nsVariant.h @@ -10,8 +10,7 @@ #include "nsIVariant.h" #include "nsStringFwd.h" #include "mozilla/Attributes.h" - -class nsCycleCollectionTraversalCallback; +#include "nsCycleCollectionParticipant.h" /** * Map the nsAUTF8String, nsUTF8String classes to the nsACString and @@ -200,6 +199,18 @@ private: ~nsVariant() {}; }; +class nsVariantCC final : public nsVariantBase +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(nsVariantCC) + + nsVariantCC() {}; + +private: + ~nsVariantCC() {}; +}; + /** * Users of nsIVariant should be using the contractID and not this CID. * - see NS_VARIANT_CONTRACTID in nsIVariant.idl. From ae5fe6a2adbfb08bd14fa2c41fe071b2a4a78cac Mon Sep 17 00:00:00 2001 From: Andrew McCreight Date: Fri, 9 Oct 2015 10:24:23 -0700 Subject: [PATCH 017/121] Bug 1210591, part 1 - Use nsVariantCC in various places. r=smaug Most of these will end up in DataTransfer:mItems, so this is needed for it to do anything useful. --- dom/base/nsContentAreaDragDrop.cpp | 6 +++--- dom/base/nsGlobalWindow.cpp | 2 +- dom/events/DataTransfer.cpp | 4 ++-- dom/html/HTMLInputElement.cpp | 2 +- dom/html/TextTrackManager.cpp | 2 +- dom/ipc/ContentChild.cpp | 2 +- dom/ipc/TabParent.cpp | 2 +- dom/media/MediaManager.cpp | 3 ++- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/dom/base/nsContentAreaDragDrop.cpp b/dom/base/nsContentAreaDragDrop.cpp index f35346c42561..8490d9778fda 100644 --- a/dom/base/nsContentAreaDragDrop.cpp +++ b/dom/base/nsContentAreaDragDrop.cpp @@ -727,7 +727,7 @@ DragDataProducer::AddString(DataTransfer* aDataTransfer, const nsAString& aData, nsIPrincipal* aPrincipal) { - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); variant->SetAsAString(aData); aDataTransfer->SetDataWithPrincipal(aFlavor, variant, 0, aPrincipal); } @@ -783,7 +783,7 @@ DragDataProducer::AddStringsToDataTransfer(nsIContent* aDragNode, // a new flavor so as not to confuse anyone who is really registered // for image/gif or image/jpg. if (mImage) { - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); variant->SetAsISupports(mImage); aDataTransfer->SetDataWithPrincipal(NS_LITERAL_STRING(kNativeImageMime), variant, 0, principal); @@ -795,7 +795,7 @@ DragDataProducer::AddStringsToDataTransfer(nsIContent* aDragNode, nsCOMPtr dataProvider = new nsContentAreaDragDropDataProvider(); if (dataProvider) { - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); variant->SetAsISupports(dataProvider); aDataTransfer->SetDataWithPrincipal(NS_LITERAL_STRING(kFilePromiseMime), variant, 0, principal); diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 9c6661e6cd85..3b76fb38e8ac 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -563,7 +563,7 @@ nsTimeout::HasRefCntOne() static already_AddRefed CreateVoidVariant() { - nsRefPtr writable = new nsVariant(); + nsRefPtr writable = new nsVariantCC(); writable->SetAsVoid(); return writable.forget(); } diff --git a/dom/events/DataTransfer.cpp b/dom/events/DataTransfer.cpp index f7ffa726141b..0ed7f54e3281 100644 --- a/dom/events/DataTransfer.cpp +++ b/dom/events/DataTransfer.cpp @@ -435,7 +435,7 @@ void DataTransfer::SetData(const nsAString& aFormat, const nsAString& aData, ErrorResult& aRv) { - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); variant->SetAsAString(aData); aRv = MozSetDataAt(aFormat, variant, 0); @@ -1345,7 +1345,7 @@ DataTransfer::FillInExternalData(TransferItem& aItem, uint32_t aIndex) if (!data) return; - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); nsCOMPtr supportsstr = do_QueryInterface(data); if (supportsstr) { diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index bb86518ca38c..6bafc7ae7f7b 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -808,7 +808,7 @@ UploadLastDir::StoreLastUsedDirectory(nsIDocument* aDoc, nsIFile* aDir) aDir->GetPath(unicodePath); if (unicodePath.IsEmpty()) // nothing to do return NS_OK; - nsRefPtr prefValue = new nsVariant(); + nsRefPtr prefValue = new nsVariantCC(); prefValue->SetAsAString(unicodePath); // Use the document's current load context to ensure that the content pref diff --git a/dom/html/TextTrackManager.cpp b/dom/html/TextTrackManager.cpp index e9fed00226ee..786f1a183f4a 100644 --- a/dom/html/TextTrackManager.cpp +++ b/dom/html/TextTrackManager.cpp @@ -215,7 +215,7 @@ TextTrackManager::UpdateCueDisplay() mTextTracks->UpdateAndGetShowingCues(activeCues); if (activeCues.Length() > 0) { - nsRefPtr jsCues = new nsVariant(); + nsRefPtr jsCues = new nsVariantCC(); jsCues->SetAsArray(nsIDataType::VTYPE_INTERFACE, &NS_GET_IID(nsIDOMEventTarget), diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index 015bcd9189c0..e93bdcefd244 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -2877,7 +2877,7 @@ ContentChild::RecvInvokeDragSession(nsTArray&& aTransfers, auto& items = aTransfers[i].items(); for (uint32_t j = 0; j < items.Length(); ++j) { const IPCDataTransferItem& item = items[j]; - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); if (item.data().type() == IPCDataTransferData::TnsString) { const nsString& data = item.data().get_nsString(); variant->SetAsAString(data); diff --git a/dom/ipc/TabParent.cpp b/dom/ipc/TabParent.cpp index a767aa1ad245..5595149585f6 100644 --- a/dom/ipc/TabParent.cpp +++ b/dom/ipc/TabParent.cpp @@ -3588,7 +3588,7 @@ TabParent::AddInitialDnDDataTo(DataTransfer* aDataTransfer) nsTArray& itemArray = mInitialDataTransferItems[i]; for (uint32_t j = 0; j < itemArray.Length(); ++j) { DataTransferItem& item = itemArray[j]; - nsRefPtr variant = new nsVariant(); + nsRefPtr variant = new nsVariantCC(); // Special case kFilePromiseMime so that we get the right // nsIFlavorDataProvider for it. if (item.mFlavor.EqualsLiteral(kFilePromiseMime)) { diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 2e1b3643d9f0..d0e8c9ede0f4 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -2204,7 +2204,8 @@ MediaManager::AnonymizeId(nsAString& aId, const nsACString& aOriginKey) already_AddRefed MediaManager::ToJSArray(SourceSet& aDevices) { - nsRefPtr var = new nsVariant(); + MOZ_ASSERT(NS_IsMainThread()); + nsRefPtr var = new nsVariantCC(); size_t len = aDevices.Length(); if (len) { nsTArray tmp(len); From 73faab2f065bd069c840970e7e63328971058817 Mon Sep 17 00:00:00 2001 From: Andrew McCreight Date: Fri, 9 Oct 2015 10:24:23 -0700 Subject: [PATCH 018/121] Bug 1210591, part 2 - Cycle collect DataTransfer::mItems. r=smaug --- dom/events/DataTransfer.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dom/events/DataTransfer.cpp b/dom/events/DataTransfer.cpp index 0ed7f54e3281..c53350b9b4c1 100644 --- a/dom/events/DataTransfer.cpp +++ b/dom/events/DataTransfer.cpp @@ -35,11 +35,21 @@ namespace mozilla { namespace dom { +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + TransferItem& aField, + const char* aName, + uint32_t aFlags = 0) +{ + ImplCycleCollectionTraverse(aCallback, aField.mData, aName, aFlags); +} + NS_IMPL_CYCLE_COLLECTION_CLASS(DataTransfer) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DataTransfer) NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFiles) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mItems) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragTarget) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragImage) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER @@ -47,6 +57,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DataTransfer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFiles) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mItems) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragTarget) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragImage) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS From ac53f535f2d744063d232d32f080a37659314f05 Mon Sep 17 00:00:00 2001 From: Ehsan Akhgari Date: Wed, 7 Oct 2015 17:01:28 -0400 Subject: [PATCH 019/121] Bug 1179397 - Disallow FetchEvent.respondWith() when the dispatch flag is unset; r=jdm --- dom/workers/ServiceWorkerEvents.cpp | 2 +- .../fetch-event-async-respond-with.https.html.ini | 5 ----- .../service-worker/fetch-event-async-respond-with.https.html | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-event-async-respond-with.https.html.ini diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp index 9c4638650e85..b4eb020cfdb0 100644 --- a/dom/workers/ServiceWorkerEvents.cpp +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -412,7 +412,7 @@ RespondWithHandler::CancelRequest(nsresult aStatus) void FetchEvent::RespondWith(Promise& aArg, ErrorResult& aRv) { - if (mWaitToRespond) { + if (EventPhase() == nsIDOMEvent::NONE || mWaitToRespond) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } diff --git a/testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-event-async-respond-with.https.html.ini b/testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-event-async-respond-with.https.html.ini deleted file mode 100644 index 32689d1a489a..000000000000 --- a/testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-event-async-respond-with.https.html.ini +++ /dev/null @@ -1,5 +0,0 @@ -[fetch-event-async-respond-with.https.html] - type: testharness - [Calling respondWith asynchronously throws an exception] - expected: FAIL - diff --git a/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html index 5c908b3d94fd..912e709ca38f 100644 --- a/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html +++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html @@ -22,8 +22,8 @@ promise_test(function(t) { }); var worker = frame.contentWindow.navigator.serviceWorker.controller; - frame.remove(); worker.postMessage({port: channel.port2}, [channel.port2]); + frame.remove(); return saw_message; }) .then(function(message) { From 4a0f7cbf1d7c6234c5438bda90435c185e2fa649 Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Mon, 20 Jul 2015 09:34:57 -0700 Subject: [PATCH 020/121] Bug 1181867 - correct app_path, set abs_res_dir for webapprt; r=jlund --HG-- extra : commitid : DvHJluCpkvN --- .../mozharness/scripts/desktop_unittest.py | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/testing/mozharness/scripts/desktop_unittest.py b/testing/mozharness/scripts/desktop_unittest.py index d57e35930132..d7d8369c92ca 100755 --- a/testing/mozharness/scripts/desktop_unittest.py +++ b/testing/mozharness/scripts/desktop_unittest.py @@ -298,6 +298,32 @@ class DesktopUnittest(TestingMixin, MercurialScript, BlobUploadMixin, MozbaseMix self.symbols_url = symbols_url return self.symbols_url + def get_webapprt_path(self, res_dir, mochitest_dir): + """Get the path to the webapp runtime binary. + On Mac, we copy the stub from the resources dir to the test app bundle, + since we have to run it from the executable directory of a bundle + in order for its windows to appear. Ideally, the build system would do + this for us at build time, and we should find a way for it to do that. + """ + exe_suffix = self.config.get('exe_suffix', '') + app_name = 'webapprt-stub' + exe_suffix + app_path = os.path.join(res_dir, app_name) + if self._is_darwin(): + mac_dir_name = os.path.join( + mochitest_dir, + 'webapprtChrome', + 'webapprt', + 'test', + 'chrome', + 'TestApp.app', + 'Contents', + 'MacOS') + mac_app_name = 'webapprt' + exe_suffix + mac_app_path = os.path.join(mac_dir_name, mac_app_name) + self.copyfile(app_path, mac_app_path, copystat=True) + return mac_app_path + return app_path + def _query_abs_base_cmd(self, suite_category, suite): if self.binary_path: c = self.config @@ -308,11 +334,6 @@ class DesktopUnittest(TestingMixin, MercurialScript, BlobUploadMixin, MozbaseMix abs_app_dir = self.query_abs_app_dir() abs_res_dir = self.query_abs_res_dir() - webapprt_path = os.path.join(os.path.dirname(self.binary_path), - 'webapprt-stub') - if c.get('exe_suffix'): - webapprt_path += c['exe_suffix'] - raw_log_file = os.path.join(dirs['abs_blob_upload_dir'], '%s_raw.log' % suite) @@ -322,7 +343,7 @@ class DesktopUnittest(TestingMixin, MercurialScript, BlobUploadMixin, MozbaseMix 'binary_path': self.binary_path, 'symbols_path': self._query_symbols_url(), 'abs_app_dir': abs_app_dir, - 'app_path': webapprt_path, + 'abs_res_dir': abs_res_dir, 'raw_log_file': raw_log_file, 'error_summary_file': error_summary_file, 'gtest_dir': os.path.join(dirs['abs_test_install_dir'], @@ -334,6 +355,9 @@ class DesktopUnittest(TestingMixin, MercurialScript, BlobUploadMixin, MozbaseMix if self.symbols_path: str_format_values['symbols_path'] = self.symbols_path + if suite_category == 'webapprt': + str_format_values['app_path'] = self.get_webapprt_path(abs_res_dir, dirs['abs_mochitest_dir']) + if c['e10s']: base_cmd.append('--e10s') From 7b4a70a169d40dd806004ff1c9215c9d49a0a25b Mon Sep 17 00:00:00 2001 From: Patrick McManus Date: Fri, 9 Oct 2015 09:28:37 -0400 Subject: [PATCH 021/121] bug 718797 - allow heuristic cache of query string resources r=hurley --- netwerk/protocol/http/nsHttpChannel.cpp | 35 ++++--------------------- netwerk/protocol/http/nsHttpChannel.h | 1 - netwerk/test/unit/test_bug468594.js | 15 ++++++----- netwerk/test/unit/test_bug490095.js | 4 +-- 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 57981e312dd8..08be9b99cfd8 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -3296,14 +3296,11 @@ nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appC else if (mCachedResponseHead->MustValidate()) { LOG(("Validating based on MustValidate() returning TRUE\n")); doValidation = true; - } - else if (MustValidateBasedOnQueryUrl()) { - LOG(("Validating based on RFC 2616 section 13.9 " - "(query-url w/o explicit expiration-time)\n")); - doValidation = true; - } - // Check if the cache entry has expired... - else { + } else { + // previously we also checked for a query-url w/out expiration + // and didn't do heuristic on it. but defacto that is allowed now. + // + // Check if the cache entry has expired... uint32_t time = 0; // a temporary variable for storing time values... rv = entry->GetExpirationTime(&time); @@ -3799,28 +3796,6 @@ nsHttpChannel::HasQueryString(nsHttpRequestHead::ParsedMethodType method, nsIURI return NS_SUCCEEDED(rv) && !query.IsEmpty(); } -bool -nsHttpChannel::MustValidateBasedOnQueryUrl() const -{ - // RFC 2616, section 13.9 states that GET-requests with a query-url - // MUST NOT be treated as fresh unless the server explicitly provides - // an expiration-time in the response. See bug #468594 - // Section 13.2.1 (6th paragraph) defines "explicit expiration time" - if (mHasQueryString) - { - uint32_t tmp; // we don't need the value, just whether it's set - nsresult rv = mCachedResponseHead->GetExpiresValue(&tmp); - if (NS_FAILED(rv)) { - rv = mCachedResponseHead->GetMaxAgeValue(&tmp); - if (NS_FAILED(rv)) { - return true; - } - } - } - return false; -} - - bool nsHttpChannel::ShouldUpdateOfflineCacheEntry() { diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h index 7112602d7620..25306d3dc8ac 100644 --- a/netwerk/protocol/http/nsHttpChannel.h +++ b/netwerk/protocol/http/nsHttpChannel.h @@ -388,7 +388,6 @@ private: static bool HasQueryString(nsHttpRequestHead::ParsedMethodType method, nsIURI * uri); bool ResponseWouldVary(nsICacheEntry* entry) const; - bool MustValidateBasedOnQueryUrl() const; bool IsResumable(int64_t partialLen, int64_t contentLength, bool ignoreMissingPartialLen = false) const; nsresult MaybeSetupByteRangeRequest(int64_t partialLen, int64_t contentLength, diff --git a/netwerk/test/unit/test_bug468594.js b/netwerk/test/unit/test_bug468594.js index 7c117914c579..81c333ae1dde 100644 --- a/netwerk/test/unit/test_bug468594.js +++ b/netwerk/test/unit/test_bug468594.js @@ -22,15 +22,16 @@ var tests = [ {url: "/freshness", server: "0", expected: "0"}, {url: "/freshness", server: "1", expected: "0"}, // cached - // RFC 2616 section 13.9 2nd paragraph + // RFC 2616 section 13.9 2nd paragraph says not to heuristically cache + // querystring, but we allow it to maintain web compat {url: "/freshness?a", server: "2", expected: "2"}, - {url: "/freshness?a", server: "3", expected: "3"}, + {url: "/freshness?a", server: "3", expected: "2"}, // explicit expiration dates in the future should be cached {url: "/freshness?b", server: "4", expected: "4", responseheader: "Expires: "+getDateString(1)}, {url: "/freshness?b", server: "5", expected: "4"},// cached due to Expires - + {url: "/freshness?c", server: "6", expected: "6", responseheader: "Cache-Control: max-age=3600"}, {url: "/freshness?c", server: "7", expected: "6"}, // cached due to max-age @@ -39,7 +40,7 @@ var tests = [ {url: "/freshness?d", server: "8", expected: "8", responseheader: "Expires: "+getDateString(-1)}, {url: "/freshness?d", server: "9", expected: "9"}, - + {url: "/freshness?e", server: "10", expected: "10", responseheader: "Cache-Control: max-age=0"}, {url: "/freshness?e", server: "11", expected: "11"}, @@ -104,7 +105,7 @@ function handler(metadata, response) { var body = metadata.getHeader("x-request"); response.setHeader("Content-Type", "text/plain", false); response.setHeader("Date", getDateString(0), false); - + var header = tests[index].responseheader; if (header == null) { response.setHeader("Last-Modified", getDateString(-1), false); @@ -112,11 +113,11 @@ function handler(metadata, response) { var splitHdr = header.split(": "); response.setHeader(splitHdr[0], splitHdr[1], false); } - + response.setStatusLine(metadata.httpVersion, 200, "OK"); response.bodyOutputStream.write(body, body.length); } - + function getDateString(yearDelta) { var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; diff --git a/netwerk/test/unit/test_bug490095.js b/netwerk/test/unit/test_bug490095.js index 74c0265b195a..f75d26c9efa0 100644 --- a/netwerk/test/unit/test_bug490095.js +++ b/netwerk/test/unit/test_bug490095.js @@ -9,7 +9,6 @@ Cu.import("resource://gre/modules/Services.jsm"); var httpserver = new HttpServer(); var index = 0; var tests = [ - // RFC 2616 section 13.9 2nd paragraph - query-url should be validated {url: "/freshness?a", server: "0", expected: "0"}, {url: "/freshness?a", server: "1", expected: "1"}, @@ -20,8 +19,6 @@ var tests = [ // Finally, check that request is validated with no flags set {url: "/freshness?a", server: "99", expected: "99"}, - - // RFC 2616 section 13.9 2nd paragraph - query-url should be validated {url: "/freshness?b", server: "0", expected: "0"}, {url: "/freshness?b", server: "1", expected: "1"}, @@ -98,6 +95,7 @@ function handler(metadata, response) { var body = metadata.getHeader("x-request"); response.setHeader("Content-Type", "text/plain", false); response.setHeader("Date", getDateString(0), false); + response.setHeader("Cache-Control", "max-age=0", false); var header = tests[index].responseheader; if (header == null) { From 1b2cdaa4c0952a01897dccaba4dc838fc6a68c01 Mon Sep 17 00:00:00 2001 From: Jonathan Griffin Date: Fri, 9 Oct 2015 12:00:08 -0700 Subject: [PATCH 022/121] Bug 1209327 - Add in-tree config for mediatests, r=maja --HG-- extra : commitid : Cgn8JqjOn8p --- .../mediatests/buildbot_posix_config.py | 10 +++++++ .../mediatests/buildbot_windows_config.py | 10 +++++++ .../mozilla/testing/firefox_media_tests.py | 1 + .../scripts/firefox_media_tests_buildbot.py | 26 ++++++++++++++++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/testing/mozharness/configs/mediatests/buildbot_posix_config.py b/testing/mozharness/configs/mediatests/buildbot_posix_config.py index 0cce8276c9ce..b912b8aa779a 100644 --- a/testing/mozharness/configs/mediatests/buildbot_posix_config.py +++ b/testing/mozharness/configs/mediatests/buildbot_posix_config.py @@ -46,4 +46,14 @@ config = { "firefox_ui_branch": 'mozilla-central', "firefox_ui_rev": '6d6d57917f85399e903ac69b7e4297091b2d474c', + "suite_definitions": { + "media-tests": { + "options": [], + }, + "media-youtube-tests": { + "options": [ + "--tests=%(test_manifest)s" + ], + }, + }, } diff --git a/testing/mozharness/configs/mediatests/buildbot_windows_config.py b/testing/mozharness/configs/mediatests/buildbot_windows_config.py index 9f837a09f246..fc6a0c25cbd4 100644 --- a/testing/mozharness/configs/mediatests/buildbot_windows_config.py +++ b/testing/mozharness/configs/mediatests/buildbot_windows_config.py @@ -57,4 +57,14 @@ config = { "firefox_ui_branch": 'mozilla-central', "firefox_ui_rev": '6d6d57917f85399e903ac69b7e4297091b2d474c', + "suite_definitions": { + "media-tests": { + "options": [], + }, + "media-youtube-tests": { + "options": [ + "--tests=%(test_manifest)s" + ], + }, + }, } diff --git a/testing/mozharness/mozharness/mozilla/testing/firefox_media_tests.py b/testing/mozharness/mozharness/mozilla/testing/firefox_media_tests.py index d4107e05914b..52ff6fa0be64 100644 --- a/testing/mozharness/mozharness/mozilla/testing/firefox_media_tests.py +++ b/testing/mozharness/mozharness/mozilla/testing/firefox_media_tests.py @@ -225,6 +225,7 @@ class FirefoxMediaTestsBase(TestingMixin, VCSToolsScript): cmd.append(self.tests) if self.e10s: cmd.append('--e10s') + return cmd def run_media_tests(self): diff --git a/testing/mozharness/scripts/firefox_media_tests_buildbot.py b/testing/mozharness/scripts/firefox_media_tests_buildbot.py index cf54c75eb1af..268dbed95bc3 100644 --- a/testing/mozharness/scripts/firefox_media_tests_buildbot.py +++ b/testing/mozharness/scripts/firefox_media_tests_buildbot.py @@ -28,13 +28,22 @@ from mozharness.mozilla.testing.firefox_media_tests import ( FirefoxMediaTestsBase, BUSTED, TESTFAILED, UNKNOWN, EXCEPTION, SUCCESS ) +buildbot_media_test_options = [ + [["--suite"], + {"action": "store", + "dest": "test_suite", + "default": "media-tests", + "help": "suite name", + }], +] + class FirefoxMediaTestsBuildbot(FirefoxMediaTestsBase, BlobUploadMixin): def __init__(self): config_options = copy.deepcopy(blobupload_config_options) super(FirefoxMediaTestsBuildbot, self).__init__( - config_options=config_options, + config_options=config_options + buildbot_media_test_options, all_actions=['clobber', 'read-buildbot-config', 'checkout', @@ -84,6 +93,21 @@ class FirefoxMediaTestsBuildbot(FirefoxMediaTestsBase, BlobUploadMixin): 'media_tests.html')] cmd += ['--log-mach', os.path.join(blob_upload_dir, 'media_tests_mach.log')] + + test_suite = self.config.get('test_suite') + test_manifest = None if test_suite != 'media-youtube-tests' else \ + os.path.join(dirs['firefox_media_dir'], + 'firefox_media_tests', + 'playback', 'youtube', 'manifest.ini') + config_fmt_args = { + 'test_manifest': test_manifest, + } + + if test_suite not in self.config["suite_definitions"]: + self.fatal("%s is not defined in the config!" % test_suite) + for s in self.config["suite_definitions"][test_suite]["options"]: + cmd.append(s % config_fmt_args) + return cmd def run_media_tests(self): From 6bdfc72d3755908bffeb2f81fdd7ffa5f2aa1feb Mon Sep 17 00:00:00 2001 From: Eric Faust Date: Fri, 9 Oct 2015 09:33:57 -0700 Subject: [PATCH 023/121] Bug 1105463 - Implement default constructors for ES6 class definitions. (r=jorendorff) --- js/src/frontend/BytecodeEmitter.cpp | 21 +++-- js/src/frontend/Parser.cpp | 6 -- js/src/jsfun.cpp | 22 ++++- .../ecma_6/Class/defaultConstructorBase.js | 25 +++++ .../Class/defaultConstructorNotCallable.js | 15 +++ .../Class/superCallBadDynamicSuperClass.js | 4 +- .../Class/superCallBadNewTargetPrototype.js | 4 +- .../ecma_6/Class/superCallBaseInvoked.js | 13 ++- .../ecma_6/Class/superCallInvalidBase.js | 2 + .../tests/ecma_6/Class/superCallProperBase.js | 8 ++ js/src/vm/Interpreter.cpp | 94 ++++++++++++++++++- js/src/vm/Interpreter.h | 6 ++ js/src/vm/Opcodes.h | 21 ++++- 13 files changed, 217 insertions(+), 24 deletions(-) create mode 100644 js/src/tests/ecma_6/Class/defaultConstructorBase.js create mode 100644 js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 5212e6c8a6cc..e3b6ca1cf4d6 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -7451,7 +7451,6 @@ BytecodeEmitter::emitClass(ParseNode* pn) break; } } - MOZ_ASSERT(constructor, "For now, no default constructors"); bool savedStrictness = sc->setLocalStrictMode(true); @@ -7483,12 +7482,22 @@ BytecodeEmitter::emitClass(ParseNode* pn) return false; } - if (!emitFunction(constructor, !!heritageExpression)) - return false; - - if (constructor->pn_funbox->needsHomeObject()) { - if (!emit2(JSOP_INITHOMEOBJECT, 0)) + if (constructor) { + if (!emitFunction(constructor, !!heritageExpression)) return false; + if (constructor->pn_funbox->needsHomeObject()) { + if (!emit2(JSOP_INITHOMEOBJECT, 0)) + return false; + } + } else { + JSAtom *name = names ? names->innerBinding()->pn_atom : nullptr; + if (heritageExpression) { + if (!emitAtomOp(name, JSOP_DERIVEDCONSTRUCTOR)) + return false; + } else { + if (!emitAtomOp(name, JSOP_CLASSCONSTRUCTOR)) + return false; + } } if (!emit1(JSOP_SWAP)) diff --git a/js/src/frontend/Parser.cpp b/js/src/frontend/Parser.cpp index c855c98cc332..a7abde0c010e 100644 --- a/js/src/frontend/Parser.cpp +++ b/js/src/frontend/Parser.cpp @@ -6673,12 +6673,6 @@ Parser::classDefinition(YieldHandling yieldHandling, return null(); } - // Default constructors not yet implemented. See bug 1105463 - if (!seenConstructor) { - report(ParseError, false, null(), JSMSG_NO_CLASS_CONSTRUCTOR); - return null(); - } - ParseNode* nameNode = null(); ParseNode* methodsOrBlock = classMethods; if (name) { diff --git a/js/src/jsfun.cpp b/js/src/jsfun.cpp index 080f7b82e41f..433c6a2dd5f4 100644 --- a/js/src/jsfun.cpp +++ b/js/src/jsfun.cpp @@ -1074,11 +1074,23 @@ js::FunctionToString(JSContext* cx, HandleFunction fun, bool bodyOnly, bool lamb } else { MOZ_ASSERT(!fun->isExprBody()); - if ((!bodyOnly && !out.append("() {\n ")) - || !out.append("[native code]") - || (!bodyOnly && !out.append("\n}"))) - { - return nullptr; + if (fun->isNative() && fun->native() == js::DefaultDerivedClassConstructor) { + if ((!bodyOnly && !out.append("(...args) {\n ")) || + !out.append("super(...args);\n}")) + { + return nullptr; + } + } else { + if (!bodyOnly && !out.append("() {\n ")) + return nullptr; + + if (!fun->isNative() || fun->native() != js::DefaultClassConstructor) { + if (!out.append("[native code]")) + return nullptr; + } + + if (!bodyOnly && !out.append("\n}")) + return nullptr; } } return out.finishString(); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorBase.js b/js/src/tests/ecma_6/Class/defaultConstructorBase.js new file mode 100644 index 000000000000..851cf10672ad --- /dev/null +++ b/js/src/tests/ecma_6/Class/defaultConstructorBase.js @@ -0,0 +1,25 @@ +var test = ` + +class base { + method() { return 1; } + *gen() { return 2; } + static sMethod() { return 3; } + get answer() { return 42; } +} + +// Having a default constructor should work, and also not make you lose +// everything for no good reason + +assertEq(Object.getPrototypeOf(new base()), base.prototype); +assertEq(new base().method(), 1); +assertEq(new base().gen().next().value, 2); +assertEq(base.sMethod(), 3); +assertEq(new base().answer, 42); + +`; + +if (classesEnabled()) + eval(test); + +if (typeof reportCompare === 'function') + reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js b/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js new file mode 100644 index 000000000000..0df212bb22ac --- /dev/null +++ b/js/src/tests/ecma_6/Class/defaultConstructorNotCallable.js @@ -0,0 +1,15 @@ +var test = ` + +class badBase {} +assertThrowsInstanceOf(badBase, TypeError); + +class badSub extends (class {}) {} +assertThrowsInstanceOf(badSub, TypeError); + +`; + +if (classesEnabled()) + eval(test); + +if (typeof reportCompare === 'function') + reportCompare(0,0,"OK"); diff --git a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js index e904d82efdab..17924345380c 100644 --- a/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js +++ b/js/src/tests/ecma_6/Class/superCallBadDynamicSuperClass.js @@ -3,9 +3,11 @@ var test = ` class base { constructor() { } } class inst extends base { constructor() { super(); } } - Object.setPrototypeOf(inst, Math.sin); +assertThrowsInstanceOf(() => new inst(), TypeError); +class defaultInst extends base { } +Object.setPrototypeOf(inst, Math.sin); assertThrowsInstanceOf(() => new inst(), TypeError); `; diff --git a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js index 577e7d3cda7b..cc79da2a1a58 100644 --- a/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js +++ b/js/src/tests/ecma_6/Class/superCallBadNewTargetPrototype.js @@ -18,9 +18,11 @@ function get(target, property, receiver) { class inst extends base { constructor() { super(); } } - assertThrowsInstanceOf(()=>new new Proxy(inst, {get})(), TypeError); +class defaultInst extends base {} +assertThrowsInstanceOf(()=>new new Proxy(defaultInst, {get})(), TypeError); + `; if (classesEnabled()) diff --git a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js index 37ab89f5cfa0..807b132000be 100644 --- a/js/src/tests/ecma_6/Class/superCallBaseInvoked.js +++ b/js/src/tests/ecma_6/Class/superCallBaseInvoked.js @@ -10,6 +10,11 @@ function testBase(base) { let inst = new instance(instance, 1); assertEq(Object.getPrototypeOf(inst), instance.prototype); assertEq(inst.calledBase, true); + + class defaultInstance extends base { } + let defInst = new defaultInstance(defaultInstance, 1); + assertEq(Object.getPrototypeOf(defInst), defaultInstance.prototype); + assertEq(defInst.calledBase, true); } class base { @@ -40,7 +45,13 @@ function baseFunc(nt, one) { } testBase(baseFunc); -testBase(new Proxy(baseFunc, {})); + +let handler = {}; +let p = new Proxy(baseFunc, handler); +testBase(p); + +handler.construct = (target, args, nt) => Reflect.construct(target, args, nt); +testBase(p); // Object will have to wait for fixed builtins. diff --git a/js/src/tests/ecma_6/Class/superCallInvalidBase.js b/js/src/tests/ecma_6/Class/superCallInvalidBase.js index 03ffb5e58d33..3f15ae6927e0 100644 --- a/js/src/tests/ecma_6/Class/superCallInvalidBase.js +++ b/js/src/tests/ecma_6/Class/superCallInvalidBase.js @@ -5,6 +5,8 @@ class instance extends null { } assertThrowsInstanceOf(() => new instance(), TypeError); +assertThrowsInstanceOf(() => new class extends null { }(), TypeError); + `; diff --git a/js/src/tests/ecma_6/Class/superCallProperBase.js b/js/src/tests/ecma_6/Class/superCallProperBase.js index a33bbcf45762..262cdc5e8374 100644 --- a/js/src/tests/ecma_6/Class/superCallProperBase.js +++ b/js/src/tests/ecma_6/Class/superCallProperBase.js @@ -24,6 +24,14 @@ Object.setPrototypeOf(inst, base2); assertEq(new inst().base, 2); +// Still works with default constructor + +class defaultInst extends base1 { } + +assertEq(new defaultInst().base, 1); +Object.setPrototypeOf(defaultInst, base2); +assertEq(new defaultInst().base, 2); + `; if (classesEnabled()) diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp index 2956d21a6968..a83cedeab353 100644 --- a/js/src/vm/Interpreter.cpp +++ b/js/src/vm/Interpreter.cpp @@ -319,6 +319,17 @@ SetPropertyOperation(JSContext* cx, JSOp op, HandleValue lval, HandleId id, Hand result.checkStrictErrorOrWarning(cx, obj, id, op == JSOP_STRICTSETPROP); } +static JSFunction* +MakeDefaultConstructor(JSContext* cx, JSOp op, JSAtom* atom, HandleObject proto) +{ + bool derived = op == JSOP_DERIVEDCONSTRUCTOR; + MOZ_ASSERT(derived == !!proto); + + RootedAtom name(cx, atom); + JSNative native = derived ? DefaultDerivedClassConstructor : DefaultClassConstructor; + return NewFunctionWithProto(cx, native, 0, JSFunction::NATIVE_CTOR, nullptr, name, proto); +} + bool js::ReportIsNotFunction(JSContext* cx, HandleValue v, int numToSkip, MaybeConstruct construct) { @@ -2087,8 +2098,6 @@ CASE(JSOP_NOP) CASE(JSOP_UNUSED2) CASE(JSOP_UNUSED14) CASE(JSOP_BACKPATCH) -CASE(JSOP_UNUSED167) -CASE(JSOP_UNUSED168) CASE(JSOP_UNUSED169) CASE(JSOP_UNUSED170) CASE(JSOP_UNUSED171) @@ -4167,6 +4176,30 @@ CASE(JSOP_SETTHIS) } END_CASE(JSOP_SETTHIS) +CASE(JSOP_DERIVEDCONSTRUCTOR) +{ + MOZ_ASSERT(REGS.sp[-1].isObject()); + ReservedRooted proto(&rootObject0, ®S.sp[-1].toObject()); + + JSFunction* constructor = MakeDefaultConstructor(cx, JSOp(*REGS.pc), script->getAtom(REGS.pc), + proto); + if (!constructor) + goto error; + + REGS.sp[-1].setObject(*constructor); +} +END_CASE(JSOP_DERIVEDCONSTRUCTOR) + +CASE(JSOP_CLASSCONSTRUCTOR) +{ + JSFunction* constructor = MakeDefaultConstructor(cx, JSOp(*REGS.pc), script->getAtom(REGS.pc), + nullptr); + if (!constructor) + goto error; + PUSH_OBJECT(*constructor); +} +END_CASE(JSOP_CLASSCONSTRUCTOR) + DEFAULT() { char numBuf[12]; @@ -5028,6 +5061,63 @@ js::ReportUninitializedLexical(JSContext* cx, HandleScript script, jsbytecode* p ReportUninitializedLexical(cx, name); } +bool +js::DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.isConstructing()) { + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); + return false; + } + + RootedObject newTarget(cx, &args.newTarget().toObject()); + RootedValue protoVal(cx); + + if (!GetProperty(cx, newTarget, newTarget, cx->names().prototype, &protoVal)) + return false; + + RootedObject proto(cx); + if (!protoVal.isObject()) { + if (!GetBuiltinPrototype(cx, JSProto_Object, &proto)) + return false; + } else { + proto = &protoVal.toObject(); + } + + JSObject* obj = NewObjectWithGivenProto(cx, &PlainObject::class_, proto); + if (!obj) + return false; + + args.rval().set(ObjectValue(*obj)); + return true; +} + +bool +js::DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.isConstructing()) { + JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_CANT_CALL_CLASS_CONSTRUCTOR); + return false; + } + + RootedObject fun(cx, &args.callee()); + RootedObject superFun(cx); + if (!GetPrototype(cx, fun, &superFun)) + return false; + + RootedValue fval(cx, ObjectOrNullValue(superFun)); + if (!IsConstructor(fval)) { + ReportValueError(cx, JSMSG_NOT_CONSTRUCTOR, JSDVG_IGNORE_STACK, fval, nullptr); + return false; + } + + ConstructArgs constArgs(cx); + if (!FillArgumentsFromArraylike(cx, constArgs, args)) + return false; + return Construct(cx, fval, constArgs, args.newTarget(), args.rval()); +} + void js::ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, frontend::Definition::Kind declKind) diff --git a/js/src/vm/Interpreter.h b/js/src/vm/Interpreter.h index 19ae644f2a83..3c6016079ada 100644 --- a/js/src/vm/Interpreter.h +++ b/js/src/vm/Interpreter.h @@ -488,6 +488,12 @@ ReportRuntimeRedeclaration(JSContext* cx, HandlePropertyName name, bool ThrowUninitializedThis(JSContext* cx, AbstractFramePtr frame); +bool +DefaultClassConstructor(JSContext* cx, unsigned argc, Value* vp); + +bool +DefaultDerivedClassConstructor(JSContext* cx, unsigned argc, Value* vp); + } /* namespace js */ #endif /* vm_Interpreter_h */ diff --git a/js/src/vm/Opcodes.h b/js/src/vm/Opcodes.h index f7ef7b23cebc..a58760dbb7d0 100644 --- a/js/src/vm/Opcodes.h +++ b/js/src/vm/Opcodes.h @@ -79,6 +79,7 @@ * Object * Array * RegExp + * Class * [Other] */ @@ -1708,8 +1709,24 @@ * Stack: callee, this, args, newTarget => rval */ \ macro(JSOP_SPREADSUPERCALL, 166, "spreadsupercall", NULL, 1, 4, 1, JOF_BYTE|JOF_INVOKE|JOF_TYPESET) \ - macro(JSOP_UNUSED167, 167,"unused167", NULL, 1, 0, 0, JOF_BYTE) \ - macro(JSOP_UNUSED168, 168,"unused168", NULL, 1, 0, 0, JOF_BYTE) \ + /* + * Push a default constructor for a base class literal. + * + * Category: Literals + * Type: Class + * Operands: atom className + * Stack: => constructor + */ \ + macro(JSOP_CLASSCONSTRUCTOR, 167,"classconstructor", NULL, 5, 0, 1, JOF_ATOM) \ + /* + * Push a default constructor for a derived class literal. + * + * Category: Literals + * Type: Class + * Operands: atom className + * Stack: => constructor + */ \ + macro(JSOP_DERIVEDCONSTRUCTOR, 168,"derivedconstructor", NULL, 5, 1, 1, JOF_ATOM) \ macro(JSOP_UNUSED169, 169,"unused169", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED170, 170,"unused170", NULL, 1, 0, 0, JOF_BYTE) \ macro(JSOP_UNUSED171, 171,"unused171", NULL, 1, 0, 0, JOF_BYTE) \ From 8004ebf0483437aac9051064f72ff92d70f4b21f Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Fri, 9 Oct 2015 13:03:52 -0700 Subject: [PATCH 024/121] Bug 1212427 - Reference extra Python paths in Sphinx config; r=mshal f6d18678498b / bug 1212427 introduced a few more dependencies. Add them to sys.path in the Sphinx config so Sphinx works on Read the Docs. --HG-- extra : commitid : I2WmZNyGBLu extra : rebase_source : 61b7a66c558321fc6550453411744c12fdd49f60 extra : amend_source : 8020e34f6c8a809fb1c323180b3a96433bbc2d2f --- tools/docs/conf.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/docs/conf.py b/tools/docs/conf.py index df6a808747ae..890c05490777 100644 --- a/tools/docs/conf.py +++ b/tools/docs/conf.py @@ -14,8 +14,17 @@ from datetime import datetime OUR_DIR = os.path.dirname(__file__) topsrcdir = os.path.normpath(os.path.join(OUR_DIR, '..', '..')) -sys.path.insert(0, os.path.join(topsrcdir, 'python', 'jsmin')) -sys.path.insert(0, os.path.join(topsrcdir, 'python', 'mozbuild')) +EXTRA_PATHS = ( + 'python/jsmin', + 'python/mach', + 'python/mozbuild', + 'python/which', + 'testing/mozbase/mozfile', + 'testing/mozbase/mozprocess', +) + +sys.path[:0] = [os.path.join(topsrcdir, p) for p in EXTRA_PATHS] + sys.path.insert(0, OUR_DIR) extensions = [ From 980f4a8f37a622f904b8ae0386a7cc126c50f66e Mon Sep 17 00:00:00 2001 From: Eric Faust Date: Fri, 9 Oct 2015 13:22:10 -0700 Subject: [PATCH 025/121] Bug 1105463 - Follow up: Fix erroneous syntax test. (r=theSheriffMadeMeDoIt) --- js/src/tests/js1_8_5/reflect-parse/classes.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/js/src/tests/js1_8_5/reflect-parse/classes.js b/js/src/tests/js1_8_5/reflect-parse/classes.js index 497e79730650..44fcd4081cc5 100644 --- a/js/src/tests/js1_8_5/reflect-parse/classes.js +++ b/js/src/tests/js1_8_5/reflect-parse/classes.js @@ -127,8 +127,9 @@ function testClasses() { [ctorPlaceholder, emptyCPNMethod("prototype", true)]); /* Constructor */ - // Currently, we do not allow default constructors - assertClassError("class NAME { }", TypeError); + // Allow default constructors + assertClass("class NAME { }", []); + assertClass("class NAME extends null { }", [], lit(null)); // For now, disallow arrow functions in derived class constructors assertClassError("class NAME extends null { constructor() { (() => 0); }", InternalError); From c3796d63cb68adda60a1def880c2394ee36e1a29 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Fri, 9 Oct 2015 13:29:34 -0700 Subject: [PATCH 026/121] Block some Radeon devices that crash on D3D9. (bug 1213107, r=jrmuizel) --- widget/windows/GfxInfo.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/widget/windows/GfxInfo.cpp b/widget/windows/GfxInfo.cpp index 5e2366b2c9f3..095ee4a60d88 100644 --- a/widget/windows/GfxInfo.cpp +++ b/widget/windows/GfxInfo.cpp @@ -1086,6 +1086,16 @@ GfxInfo::GetGfxDriverInfo() nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, DRIVER_EQUAL, V(15,200,1006,0)); + /* Bug 1213107: D3D9 crashes with ATI cards on Windows 7. */ + APPEND_TO_DRIVER_BLOCKLIST2(DRIVER_OS_WINDOWS_7, + (nsAString&)GfxDriverInfo::GetDeviceVendor(VendorATI), GfxDriverInfo::allDevices, + nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(8,861,0,0), V(8,862,6,5000)); + APPEND_TO_DRIVER_BLOCKLIST2(DRIVER_OS_WINDOWS_7, + (nsAString&)GfxDriverInfo::GetDeviceVendor(VendorATI), GfxDriverInfo::allDevices, + nsIGfxInfo::FEATURE_WEBGL_ANGLE, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, + DRIVER_BETWEEN_INCLUSIVE_START, V(8,861,0,0), V(8,862,6,5000)); + APPEND_TO_DRIVER_BLOCKLIST2(DRIVER_OS_WINDOWS_7, (nsAString&)GfxDriverInfo::GetDeviceVendor(VendorIntel), (GfxDeviceFamily*)GfxDriverInfo::GetDeviceFamily(Bug1155608), nsIGfxInfo::FEATURE_HARDWARE_VIDEO_DECODING, nsIGfxInfo::FEATURE_BLOCKED_DRIVER_VERSION, From 75b257996cb69a1eb8e50c78193ea5086dfa780e Mon Sep 17 00:00:00 2001 From: Jeff Walden Date: Thu, 8 Oct 2015 15:08:13 -0700 Subject: [PATCH 027/121] Bug 1052139 - Make more parts of the global object's prototype chain immutable, when we flip that switch. r=bz --HG-- extra : rebase_source : 7d196acc689d26153ba2462ff2c78fc94ec7351a --- dom/bindings/BindingUtils.h | 8 ++++++++ dom/bindings/Codegen.py | 25 ++++++++++++++++++++++++- js/src/builtin/Object.cpp | 7 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h index 5543ceeeefc5..3f46ed3fbdfc 100644 --- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -3111,6 +3111,14 @@ CreateGlobal(JSContext* aCx, T* aNative, nsWrapperCache* aCache, return nullptr; } + bool succeeded; + if (!JS_SetImmutablePrototype(aCx, aGlobal, &succeeded)) { + return nullptr; + } + MOZ_ASSERT(succeeded, + "making a fresh global object's [[Prototype]] immutable can " + "internally fail, but it should never be unsuccessful"); + return proto; } diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index 8ccab38a3c6c..6459e7afd499 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -2953,9 +2953,32 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): else: unforgeableHolderSetup = None + if (self.descriptor.interface.isOnGlobalProtoChain() and + needInterfacePrototypeObject): + makeProtoPrototypeImmutable = CGGeneric(fill( + """ + if (*${protoCache}) { + bool succeeded; + JS::Handle prot = GetProtoObjectHandle(aCx, aGlobal); + if (!JS_SetImmutablePrototype(aCx, prot, &succeeded)) { + $*{failureCode} + } + + MOZ_ASSERT(succeeded, + "making a fresh prototype object's [[Prototype]] " + "immutable can internally fail, but it should " + "never be unsuccessful"); + } + """, + protoCache=protoCache, + failureCode=failureCode)) + else: + makeProtoPrototypeImmutable = None + return CGList( [getParentProto, CGGeneric(getConstructorProto), initIds, - prefCache, CGGeneric(call), defineAliases, unforgeableHolderSetup], + prefCache, CGGeneric(call), defineAliases, unforgeableHolderSetup, + makeProtoPrototypeImmutable], "\n").define() diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index bf8ba22a3004..5375da75cfb0 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -1056,6 +1056,13 @@ CreateObjectPrototype(JSContext* cx, JSProtoKey key) if (!objectProto) return nullptr; + bool succeeded; + if (!SetImmutablePrototype(cx, objectProto, &succeeded)) + return nullptr; + MOZ_ASSERT(succeeded, + "should have been able to make a fresh Object.prototype's " + "[[Prototype]] immutable"); + /* * The default 'new' type of Object.prototype is required by type inference * to have unknown properties, to simplify handling of e.g. heterogenous From 3d9eb107a89aa9e5507c146c40597c316f69db2f Mon Sep 17 00:00:00 2001 From: Terrence Cole Date: Fri, 9 Oct 2015 12:54:25 -0700 Subject: [PATCH 028/121] Bug 1213104 - Pass the ProgressBar directly, since we are no longer using results; r=sfink --HG-- extra : rebase_source : 37fe5b3614ddd2165ead9d2366e643691f507187 --- js/src/tests/jstests.py | 2 +- js/src/tests/lib/tasks_unix.py | 16 +++++++++++----- js/src/tests/lib/tasks_win.py | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/js/src/tests/jstests.py b/js/src/tests/jstests.py index 04d7006c508e..58296a79d9a0 100755 --- a/js/src/tests/jstests.py +++ b/js/src/tests/jstests.py @@ -373,7 +373,7 @@ def main(): results = ResultsSink(options, test_count) try: - for out in run_all_tests(test_gen, prefix, results, options): + for out in run_all_tests(test_gen, prefix, results.pb, options): results.push(out) results.finish(True) except KeyboardInterrupt: diff --git a/js/src/tests/lib/tasks_unix.py b/js/src/tests/lib/tasks_unix.py index 01b27c98684e..6bed571bedd8 100644 --- a/js/src/tests/lib/tasks_unix.py +++ b/js/src/tests/lib/tasks_unix.py @@ -137,10 +137,10 @@ def timed_out(task, timeout): def reap_zombies(tasks, timeout): """ - Search for children of this process that have finished. If they are tasks, - then this routine will clean up the child and send a TestOutput to the - results channel. This method returns a new task list that has had the ended - tasks removed. + Search for children of this process that have finished. If they are tasks, + then this routine will clean up the child. This method returns a new task + list that has had the ended tasks removed, followed by the list of finished + tasks. """ finished = [] while True: @@ -182,7 +182,7 @@ def kill_undead(tasks, timeout): if timed_out(task, timeout): os.kill(task.pid, 9) -def run_all_tests(tests, prefix, results, options): +def run_all_tests(tests, prefix, pb, options): # Copy and reverse for fast pop off end. tests = list(tests) tests = tests[:] @@ -208,3 +208,9 @@ def run_all_tests(tests, prefix, results, options): # With Python3.4+ we could use yield from to remove this loop. for out in finished: yield out + + # If we did not finish any tasks, poke the progress bar to show that + # the test harness is at least not frozen. + if len(finished) == 0: + pb.poke() + diff --git a/js/src/tests/lib/tasks_win.py b/js/src/tests/lib/tasks_win.py index b121760c546b..71449f414cb1 100644 --- a/js/src/tests/lib/tasks_win.py +++ b/js/src/tests/lib/tasks_win.py @@ -68,7 +68,7 @@ def _do_watch(qWatch, timeout): assert fin is TaskFinishedMarker, "invalid finish marker" -def run_all_tests(tests, prefix, results, options): +def run_all_tests(tests, prefix, pb, options): """ Uses scatter-gather to a thread-pool to manage children. """ @@ -114,7 +114,7 @@ def run_all_tests(tests, prefix, results, options): else: yield result except Empty: - results.pb.poke() + pb.poke() # Cleanup and exit. pusher.join() From 7f709faaea162038caf1150137191197a8342ccf Mon Sep 17 00:00:00 2001 From: Terrence Cole Date: Fri, 9 Oct 2015 12:54:57 -0700 Subject: [PATCH 029/121] Bug 1213111 - Extract progressbar from jittest's process_test_results; r=sfink --HG-- extra : rebase_source : 3704f33be30bb370218b38246b27a17ac8c7b546 --- js/src/tests/lib/jittests.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py index da9507dad0ec..36eca844bed6 100755 --- a/js/src/tests/lib/jittests.py +++ b/js/src/tests/lib/jittests.py @@ -629,8 +629,9 @@ def get_parallel_results(async_test_result_queue, notify_queue): def process_test_results_parallel(async_test_result_queue, return_queue, notify_queue, num_tests, options): + pb = create_progressbar(num_tests, options) gen = get_parallel_results(async_test_result_queue, notify_queue) - ok = process_test_results(gen, num_tests, options) + ok = process_test_results(gen, num_tests, pb, options) return_queue.put(ok) def print_test_summary(num_tests, failures, complete, doing, options): @@ -685,8 +686,19 @@ def print_test_summary(num_tests, failures, complete, doing, options): return not failures -def process_test_results(results, num_tests, options): - pb = NullProgressBar() +def create_progressbar(num_tests, options): + if not options.hide_progress and not options.show_cmd \ + and ProgressBar.conservative_isatty(): + fmt = [ + {'value': 'PASS', 'color': 'green'}, + {'value': 'FAIL', 'color': 'red'}, + {'value': 'TIMEOUT', 'color': 'blue'}, + {'value': 'SKIP', 'color': 'brightgray'}, + ] + return ProgressBar(num_tests, fmt) + return NullProgressBar() + +def process_test_results(results, num_tests, pb, options): failures = [] timeouts = 0 complete = False @@ -697,16 +709,6 @@ def process_test_results(results, num_tests, options): complete = True return print_test_summary(num_tests, failures, complete, doing, options) - if not options.hide_progress and not options.show_cmd \ - and ProgressBar.conservative_isatty(): - fmt = [ - {'value': 'PASS', 'color': 'green'}, - {'value': 'FAIL', 'color': 'red'}, - {'value': 'TIMEOUT', 'color': 'blue'}, - {'value': 'SKIP', 'color': 'brightgray'}, - ] - pb = ProgressBar(num_tests, fmt) - try: for i, res in enumerate(results): ok = check_output(res.out, res.err, res.rc, res.timed_out, @@ -760,8 +762,10 @@ def get_serial_results(tests, prefix, options): yield run_test(test, prefix, options) def run_tests(tests, prefix, options): + num_tests = len(tests) * options.repeat + pb = create_progressbar(num_tests, options) gen = get_serial_results(tests, prefix, options) - ok = process_test_results(gen, len(tests) * options.repeat, options) + ok = process_test_results(gen, num_tests, pb, options) return ok def get_remote_results(tests, device, prefix, options): @@ -842,8 +846,10 @@ def run_tests_remote(tests, prefix, options): prefix[0] = os.path.join(options.remote_test_root, 'js') # Run all tests. + num_tests = len(tests) * options.repeat + pb = create_progressbar(num_tests, options) gen = get_remote_results(tests, dm, prefix, options) - ok = process_test_results(gen, len(tests) * options.repeat, options) + ok = process_test_results(gen, num_tests, pb, options) return ok def platform_might_be_android(): From 8ee503dbff1554802d0e18d8f92958b4429d28e2 Mon Sep 17 00:00:00 2001 From: Terrence Cole Date: Fri, 9 Oct 2015 12:56:27 -0700 Subject: [PATCH 030/121] Bug 1213365 - Share environment control code between js and jit test harnesses; r=sfink --HG-- extra : rebase_source : ed75f1c4bb4d3ca853f1b6e6d9b93f019d39cc24 --- js/src/jit-test/jit_test.py | 13 ++++++---- js/src/tests/jstests.py | 13 ++++------ js/src/tests/lib/tests.py | 49 +++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/js/src/jit-test/jit_test.py b/js/src/jit-test/jit_test.py index f7bdc2f6140a..0cb11bdc138a 100755 --- a/js/src/jit-test/jit_test.py +++ b/js/src/jit-test/jit_test.py @@ -17,7 +17,7 @@ def add_libdir_to_path(): add_libdir_to_path() import jittests -from tests import get_jitflags +from tests import get_jitflags, get_environment_overlay, change_env # Python 3.3 added shutil.which, but we can't use that yet. def which(name): @@ -149,8 +149,9 @@ def main(argv): options, args = op.parse_args(argv) if len(args) < 1: op.error('missing JS_SHELL argument') - # We need to make sure we are using backslashes on Windows. + js_shell = which(args[0]) test_args = args[1:] + test_environment = get_environment_overlay(js_shell) if jittests.stdio_might_be_broken(): # Prefer erring on the side of caution and not using stdio if @@ -249,7 +250,7 @@ def main(argv): else: options.ignore_timeouts = set() - prefix = [which(args[0])] + shlex.split(options.shell_args) + prefix = [js_shell] + shlex.split(options.shell_args) prologue = os.path.join(jittests.LIB_DIR, 'prologue.js') if options.remote: prologue = posixpath.join(options.remote_test_root, @@ -277,7 +278,8 @@ def main(argv): else: debug_cmd = options.debugger.split() - subprocess.call(debug_cmd + tc.command(prefix, jittests.LIB_DIR)) + with change_env(test_environment): + subprocess.call(debug_cmd + tc.command(prefix, jittests.LIB_DIR)) sys.exit() try: @@ -287,7 +289,8 @@ def main(argv): elif options.max_jobs > 1 and jittests.HAVE_MULTIPROCESSING: ok = jittests.run_tests_parallel(job_list, prefix, options) else: - ok = jittests.run_tests(job_list, prefix, options) + with change_env(test_environment): + ok = jittests.run_tests(job_list, prefix, options) if not ok: sys.exit(2) except OSError: diff --git a/js/src/tests/jstests.py b/js/src/tests/jstests.py index 58296a79d9a0..d0e03f33d392 100755 --- a/js/src/tests/jstests.py +++ b/js/src/tests/jstests.py @@ -13,7 +13,8 @@ from contextlib import contextmanager from copy import copy from subprocess import list2cmdline, call -from lib.tests import RefTestCase, get_jitflags +from lib.tests import RefTestCase, get_jitflags, get_environment_overlay, \ + change_env from lib.results import ResultsSink from lib.progressbar import ProgressBar @@ -342,6 +343,7 @@ def main(): print('Could not find shell at given path.') return 1 test_count, test_gen = load_tests(options, requested_paths, excluded_paths) + test_environment = get_environment_overlay(options.js_shell) if test_count == 0: print('no tests selected') @@ -361,16 +363,11 @@ def main(): cmd = tests[0].get_command(prefix) if options.show_cmd: print(list2cmdline(cmd)) - with changedir(test_dir): + with changedir(test_dir), change_env(test_environment): call(cmd) return 0 - with changedir(test_dir): - # Force Pacific time zone to avoid failures in Date tests. - os.environ['TZ'] = 'PST8PDT' - # Force date strings to English. - os.environ['LC_TIME'] = 'en_US.UTF-8' - + with changedir(test_dir), change_env(test_environment): results = ResultsSink(options, test_count) try: for out in run_all_tests(test_gen, prefix, results.pb, options): diff --git a/js/src/tests/lib/tests.py b/js/src/tests/lib/tests.py index 346ed4cff7d0..3c7c9495ac7e 100644 --- a/js/src/tests/lib/tests.py +++ b/js/src/tests/lib/tests.py @@ -4,6 +4,7 @@ # metadata, and know how to run the tests and determine failures. import datetime, os, sys, time +from contextlib import contextmanager from subprocess import Popen, PIPE from threading import Thread @@ -45,6 +46,54 @@ def get_jitflags(variant, **kwargs): return kwargs['none'] return JITFLAGS[variant] + +def get_environment_overlay(js_shell): + """ + Build a dict of additional environment variables that must be set to run + tests successfully. + """ + env = { + # Force Pacific time zone to avoid failures in Date tests. + 'TZ': 'PST8PDT', + # Force date strings to English. + 'LC_TIME': 'en_US.UTF-8', + } + # Add the binary's directory to the library search path so that we find the + # nspr and icu we built, instead of the platform supplied ones (or none at + # all on windows). + if sys.platform.startswith('linux'): + env['LD_LIBRARY_PATH'] = os.path.dirname(js_shell) + elif sys.platform.startswith('darwin'): + env['DYLD_LIBRARY_PATH'] = os.path.dirname(js_shell) + elif sys.platform.startswith('win'): + env['PATH'] = os.path.dirname(js_shell) + return env + + +@contextmanager +def change_env(env_overlay): + # Apply the overlaid environment and record the current state. + prior_env = {} + for key, val in env_overlay.items(): + prior_env[key] = os.environ.get(key, None) + if 'PATH' in key and key in os.environ: + os.environ[key] = '{}{}{}'.format(val, os.pathsep, os.environ[key]) + else: + os.environ[key] = val + + try: + # Execute with the new environment. + yield + + finally: + # Restore the prior environment. + for key, val in prior_env.items(): + if val is not None: + os.environ[key] = val + else: + del os.environ[key] + + class RefTest(object): """A runnable test.""" def __init__(self, path): From 842a6a169a6cac2c952c8310be70faea0a9921f9 Mon Sep 17 00:00:00 2001 From: Terrence Cole Date: Fri, 9 Oct 2015 13:28:59 -0700 Subject: [PATCH 031/121] Bug 1213127 - Switch jit-tests over to using jstest's task runner; r=sfink --HG-- extra : rebase_source : e7898cef866653af74374cc5440a9005fc6fbcad --- js/src/jit-test/jit_test.py | 2 -- js/src/tests/lib/jittests.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/js/src/jit-test/jit_test.py b/js/src/jit-test/jit_test.py index 0cb11bdc138a..c45cb8ff22f8 100755 --- a/js/src/jit-test/jit_test.py +++ b/js/src/jit-test/jit_test.py @@ -286,8 +286,6 @@ def main(argv): ok = None if options.remote: ok = jittests.run_tests_remote(job_list, prefix, options) - elif options.max_jobs > 1 and jittests.HAVE_MULTIPROCESSING: - ok = jittests.run_tests_parallel(job_list, prefix, options) else: with change_env(test_environment): ok = jittests.run_tests(job_list, prefix, options) diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py index 36eca844bed6..246cdb31859a 100755 --- a/js/src/tests/lib/jittests.py +++ b/js/src/tests/lib/jittests.py @@ -9,11 +9,17 @@ from __future__ import print_function import os, posixpath, sys, tempfile, traceback, time import subprocess +from collections import namedtuple from subprocess import Popen, PIPE from threading import Thread import signal import StringIO +if sys.platform.startswith('linux') or sys.platform.startswith('darwin'): + from tasks_unix import run_all_tests +else: + from tasks_win import run_all_tests + try: from multiprocessing import Process, Manager, cpu_count HAVE_MULTIPROCESSING = True @@ -121,6 +127,9 @@ class JitTest: self.expect_error = '' # Errors to expect and consider passing self.expect_status = 0 # Exit status to expect from shell + # Expected by the test runner. Always true for jit-tests. + self.enable = True + def copy(self): t = JitTest(self.path) t.jitflags = self.jitflags[:] @@ -135,6 +144,7 @@ class JitTest: t.test_join = self.test_join t.expect_error = self.expect_error t.expect_status = self.expect_status + t.enable = True return t def copy_and_extend_jitflags(self, variant): @@ -261,6 +271,13 @@ class JitTest: cmd = self.VALGRIND_CMD + cmd return cmd + # The test runner expects this to be set to give to get_command. + js_cmd_prefix = None + def get_command(self, prefix): + """Shim for the test runner.""" + return self.command(prefix, LIB_DIR) + + def find_tests(substring=None): ans = [] for dirpath, dirnames, filenames in os.walk(TEST_DIR): @@ -762,9 +779,21 @@ def get_serial_results(tests, prefix, options): yield run_test(test, prefix, options) def run_tests(tests, prefix, options): + # The jstests tasks runner requires the following options. The names are + # taken from the jstests options processing code, which are frequently + # subtly different from the options jit-tests expects. As such, we wrap + # them here, as needed. + AdaptorOptions = namedtuple("AdaptorOptions", ["worker_count", + "passthrough", "timeout", "output_fp", "hide_progress", "run_skipped"]) + shim_options = AdaptorOptions(options.max_jobs, False, options.timeout, + sys.stdout, False, True) + + # The test runner wants the prefix as a static on the Test class. + JitTest.js_cmd_prefix = prefix + num_tests = len(tests) * options.repeat pb = create_progressbar(num_tests, options) - gen = get_serial_results(tests, prefix, options) + gen = run_all_tests(tests, prefix, pb, shim_options) ok = process_test_results(gen, num_tests, pb, options) return ok From be3bd61d574e47a7f42194c8af6b953537156d2a Mon Sep 17 00:00:00 2001 From: Olivier Yiptong Date: Fri, 9 Oct 2015 16:39:19 -0400 Subject: [PATCH 032/121] Bug 1210940 - New Browser Component: Newtab r=Mardak --HG-- extra : commitid : Il6Y1bicn26 --- browser/components/moz.build | 1 + .../newtab}/NewTabURL.jsm | 7 +- .../components/newtab/RemoteAboutNewTab.jsm | 233 +++ .../newtab/RemoteDirectoryLinksProvider.jsm | 1169 +++++++++++++++ .../newtab/RemoteNewTabLocation.jsm | 42 + .../components/newtab/RemoteNewTabUtils.jsm | 766 ++++++++++ browser/components/newtab/moz.build | 19 + .../newtab/tests/browser/browser.ini | 5 + .../browser/browser_remotenewtab_pageloads.js | 47 + .../newtab/tests/browser/dummy_page.html | 22 + .../newtab/tests}/xpcshell/test_NewTabURL.js | 5 + .../test_RemoteDirectoryLinksProvider.js | 1325 +++++++++++++++++ .../xpcshell/test_RemoteNewTabLocation.js | 40 + .../tests/xpcshell/test_RemoteNewTabUtils.js | 375 +++++ .../newtab/tests/xpcshell/xpcshell.ini | 10 + browser/components/nsBrowserGlue.js | 44 +- browser/modules/moz.build | 1 - browser/modules/test/xpcshell/xpcshell.ini | 1 - 18 files changed, 4096 insertions(+), 16 deletions(-) rename browser/{modules => components/newtab}/NewTabURL.jsm (82%) create mode 100644 browser/components/newtab/RemoteAboutNewTab.jsm create mode 100644 browser/components/newtab/RemoteDirectoryLinksProvider.jsm create mode 100644 browser/components/newtab/RemoteNewTabLocation.jsm create mode 100644 browser/components/newtab/RemoteNewTabUtils.jsm create mode 100644 browser/components/newtab/moz.build create mode 100644 browser/components/newtab/tests/browser/browser.ini create mode 100644 browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js create mode 100644 browser/components/newtab/tests/browser/dummy_page.html rename browser/{modules/test => components/newtab/tests}/xpcshell/test_NewTabURL.js (82%) create mode 100644 browser/components/newtab/tests/xpcshell/test_RemoteDirectoryLinksProvider.js create mode 100644 browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js create mode 100644 browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js create mode 100644 browser/components/newtab/tests/xpcshell/xpcshell.ini diff --git a/browser/components/moz.build b/browser/components/moz.build index 67fb7086b5cd..3c028efc3759 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -13,6 +13,7 @@ DIRS += [ 'feeds', 'loop', 'migration', + 'newtab', 'places', 'pocket', 'preferences', diff --git a/browser/modules/NewTabURL.jsm b/browser/components/newtab/NewTabURL.jsm similarity index 82% rename from browser/modules/NewTabURL.jsm rename to browser/components/newtab/NewTabURL.jsm index f4ed8b755eaf..878586dbd2be 100644 --- a/browser/modules/NewTabURL.jsm +++ b/browser/components/newtab/NewTabURL.jsm @@ -14,10 +14,15 @@ Components.utils.import("resource://gre/modules/Services.jsm"); this.NewTabURL = { _url: "about:newtab", + _remoteUrl: "about:remote-newtab", _overridden: false, get: function() { - return this._url; + let output = this._url; + if (Services.prefs.getBoolPref("browser.newtabpage.remote")) { + output = this._remoteUrl; + } + return output; }, get overridden() { diff --git a/browser/components/newtab/RemoteAboutNewTab.jsm b/browser/components/newtab/RemoteAboutNewTab.jsm new file mode 100644 index 000000000000..a8d13d842269 --- /dev/null +++ b/browser/components/newtab/RemoteAboutNewTab.jsm @@ -0,0 +1,233 @@ +/* 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/. */ + +/* globals Services, XPCOMUtils, RemotePages, RemoteNewTabLocation, RemoteNewTabUtils, Task */ +/* globals BackgroundPageThumbs, PageThumbs, RemoteDirectoryLinksProvider */ +/* exported RemoteAboutNewTab */ + +"use strict"; + +let Ci = Components.interfaces; +let Cu = Components.utils; +const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + +this.EXPORTED_SYMBOLS = ["RemoteAboutNewTab"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.importGlobalProperties(["URL"]); + +XPCOMUtils.defineLazyModuleGetter(this, "RemotePages", + "resource://gre/modules/RemotePageManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils", + "resource:///modules/RemoteNewTabUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs", + "resource://gre/modules/BackgroundPageThumbs.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteDirectoryLinksProvider", + "resource:///modules/RemoteDirectoryLinksProvider.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation", + "resource:///modules/RemoteNewTabLocation.jsm"); + +let RemoteAboutNewTab = { + + pageListener: null, + + /** + * Initialize the RemotePageManager and add all message listeners for this page + */ + init: function() { + this.pageListener = new RemotePages("about:remote-newtab"); + this.pageListener.addMessageListener("NewTab:InitializeGrid", this.initializeGrid.bind(this)); + this.pageListener.addMessageListener("NewTab:UpdateGrid", this.updateGrid.bind(this)); + this.pageListener.addMessageListener("NewTab:CaptureBackgroundPageThumbs", + this.captureBackgroundPageThumb.bind(this)); + this.pageListener.addMessageListener("NewTab:PageThumbs", this.createPageThumb.bind(this)); + this.pageListener.addMessageListener("NewTabFrame:GetInit", this.initContentFrame.bind(this)); + + this._addObservers(); + }, + + /** + * Initializes the grid for the first time when the page loads. + * Fetch all the links and send them down to the child to populate + * the grid with. + * + * @param {Object} message + * A RemotePageManager message. + */ + initializeGrid: function(message) { + RemoteNewTabUtils.links.populateCache(() => { + message.target.sendAsyncMessage("NewTab:InitializeLinks", { + links: RemoteNewTabUtils.links.getLinks(), + enhancedLinks: this.getEnhancedLinks(), + }); + }); + }, + + /** + * Inits the content iframe with the newtab location + */ + initContentFrame: function(message) { + message.target.sendAsyncMessage("NewTabFrame:Init", { + href: RemoteNewTabLocation.href, + origin: RemoteNewTabLocation.origin + }); + }, + + /** + * Updates the grid by getting a new set of links. + * + * @param {Object} message + * A RemotePageManager message. + */ + updateGrid: function(message) { + message.target.sendAsyncMessage("NewTab:UpdateLinks", { + links: RemoteNewTabUtils.links.getLinks(), + enhancedLinks: this.getEnhancedLinks(), + }); + }, + + /** + * Captures the site's thumbnail in the background, then attemps to show the thumbnail. + * + * @param {Object} message + * A RemotePageManager message with the following data: + * + * link (Object): + * A link object that contains: + * + * baseDomain (String) + * blockState (Boolean) + * frecency (Integer) + * lastVisiteDate (Integer) + * pinState (Boolean) + * title (String) + * type (String) + * url (String) + */ + captureBackgroundPageThumb: Task.async(function* (message) { + try { + yield BackgroundPageThumbs.captureIfMissing(message.data.link.url); + this.createPageThumb(message); + } catch (err) { + Cu.reportError("error: " + err); + } + }), + + /** + * Creates the thumbnail to display for each site based on the unique URL + * of the site and it's type (regular or enhanced). If the thumbnail is of + * type "regular", we create a blob and send that down to the child. If the + * thumbnail is of type "enhanced", get the file path for the URL and create + * and enhanced URI that will be sent down to the child. + * + * @param {Object} message + * A RemotePageManager message with the following data: + * + * link (Object): + * A link object that contains: + * + * baseDomain (String) + * blockState (Boolean) + * frecency (Integer) + * lastVisiteDate (Integer) + * pinState (Boolean) + * title (String) + * type (String) + * url (String) + */ + createPageThumb: function(message) { + let imgSrc = PageThumbs.getThumbnailURL(message.data.link.url); + let doc = Services.appShell.hiddenDOMWindow.document; + let img = doc.createElementNS(XHTML_NAMESPACE, "img"); + let canvas = doc.createElementNS(XHTML_NAMESPACE, "canvas"); + let enhanced = Services.prefs.getBoolPref("browser.newtabpage.enhanced"); + + img.onload = function(e) { // jshint ignore:line + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + var ctx = canvas.getContext("2d"); + ctx.drawImage(this, 0, 0, this.naturalWidth, this.naturalHeight); + canvas.toBlob(function(blob) { + let host = new URL(message.data.link.url).host; + RemoteAboutNewTab.pageListener.sendAsyncMessage("NewTab:RegularThumbnailURI", { + thumbPath: "/pagethumbs/" + host, + enhanced, + url: message.data.link.url, + blob, + }); + }); + }; + img.src = imgSrc; + }, + + /** + * Get the set of enhanced links (if any) from the Directory Links Provider. + */ + getEnhancedLinks: function() { + let enhancedLinks = []; + for (let link of RemoteNewTabUtils.links.getLinks()) { + if (link) { + enhancedLinks.push(RemoteDirectoryLinksProvider.getEnhancedLink(link)); + } + } + return enhancedLinks; + }, + + /** + * Listens for a preference change or session purge for all pages and sends + * a message to update the pages that are open. If a session purge occured, + * also clear the links cache and update the set of links to display, as they + * may have changed, then proceed with the page update. + */ + observe: function(aSubject, aTopic, aData) { // jshint ignore:line + let extraData; + let refreshPage = false; + if (aTopic === "browser:purge-session-history") { + RemoteNewTabUtils.links.resetCache(); + RemoteNewTabUtils.links.populateCache(() => { + this.pageListener.sendAsyncMessage("NewTab:UpdateLinks", { + links: RemoteNewTabUtils.links.getLinks(), + enhancedLinks: this.getEnhancedLinks(), + }); + }); + } + + if (extraData !== undefined || aTopic === "page-thumbnail:create") { + if (aTopic !== "page-thumbnail:create") { + // Change the topic for enhanced and enabled observers. + aTopic = aData; + } + this.pageListener.sendAsyncMessage("NewTab:Observe", {topic: aTopic, data: extraData}); + } + }, + + /** + * Add all observers that about:newtab page must listen for. + */ + _addObservers: function() { + Services.obs.addObserver(this, "page-thumbnail:create", true); + Services.obs.addObserver(this, "browser:purge-session-history", true); + }, + + /** + * Remove all observers on the page. + */ + _removeObservers: function() { + Services.obs.removeObserver(this, "page-thumbnail:create"); + Services.obs.removeObserver(this, "browser:purge-session-history"); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + uninit: function() { + this._removeObservers(); + this.pageListener.destroy(); + this.pageListener = null; + }, +}; diff --git a/browser/components/newtab/RemoteDirectoryLinksProvider.jsm b/browser/components/newtab/RemoteDirectoryLinksProvider.jsm new file mode 100644 index 000000000000..30bb29aac62a --- /dev/null +++ b/browser/components/newtab/RemoteDirectoryLinksProvider.jsm @@ -0,0 +1,1169 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["RemoteDirectoryLinksProvider"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; +const ParserUtils = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); + +Cu.importGlobalProperties(["XMLHttpRequest"]); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils", + "resource:///modules/RemoteNewTabUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm") +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "eTLD", + "@mozilla.org/network/effective-tld-service;1", + "nsIEffectiveTLDService"); +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => { + return new TextDecoder(); +}); +XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () { + return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); +}); +XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = 'utf8'; + return converter; +}); + + +// The filename where directory links are stored locally +const DIRECTORY_LINKS_FILE = "directoryLinks.json"; +const DIRECTORY_LINKS_TYPE = "application/json"; + +// The preference that tells whether to match the OS locale +const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS"; + +// The preference that tells what locale the user selected +const PREF_SELECTED_LOCALE = "general.useragent.locale"; + +// The preference that tells where to obtain directory links +const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source"; + +// The preference that tells where to send click/view pings +const PREF_DIRECTORY_PING = "browser.newtabpage.directory.ping"; + +// The preference that tells if newtab is enhanced +const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced"; + +// Only allow link urls that are http(s) +const ALLOWED_LINK_SCHEMES = new Set(["http", "https"]); + +// Only allow link image urls that are https or data +const ALLOWED_IMAGE_SCHEMES = new Set(["https", "data"]); + +// Only allow urls to Mozilla's CDN or empty (for data URIs) +const ALLOWED_URL_BASE = new Set(["mozilla.net", ""]); + +// The frecency of a directory link +const DIRECTORY_FRECENCY = 1000; + +// The frecency of a suggested link +const SUGGESTED_FRECENCY = Infinity; + +// The filename where frequency cap data stored locally +const FREQUENCY_CAP_FILE = "frequencyCap.json"; + +// Default settings for daily and total frequency caps +const DEFAULT_DAILY_FREQUENCY_CAP = 3; +const DEFAULT_TOTAL_FREQUENCY_CAP = 10; + +// Default timeDelta to prune unused frequency cap objects +// currently set to 10 days in milliseconds +const DEFAULT_PRUNE_TIME_DELTA = 10*24*60*60*1000; + +// The min number of visible (not blocked) history tiles to have before showing suggested tiles +const MIN_VISIBLE_HISTORY_TILES = 8; + +// The max number of visible (not blocked) history tiles to test for inadjacency +const MAX_VISIBLE_HISTORY_TILES = 15; + +// Divide frecency by this amount for pings +const PING_SCORE_DIVISOR = 10000; + +// Allowed ping actions remotely stored as columns: case-insensitive [a-z0-9_] +const PING_ACTIONS = ["block", "click", "pin", "sponsored", "sponsored_link", "unpin", "view"]; + +// Location of inadjacent sites json +const INADJACENCY_SOURCE = "chrome://browser/content/newtab/newTab.inadjacent.json"; + +/** + * Singleton that serves as the provider of directory links. + * Directory links are a hard-coded set of links shown if a user's link + * inventory is empty. + */ +let RemoteDirectoryLinksProvider = { + + __linksURL: null, + + _observers: new Set(), + + // links download deferred, resolved upon download completion + _downloadDeferred: null, + + // download default interval is 24 hours in milliseconds + _downloadIntervalMS: 86400000, + + /** + * A mapping from eTLD+1 to an enhanced link objects + */ + _enhancedLinks: new Map(), + + /** + * A mapping from site to a list of suggested link objects + */ + _suggestedLinks: new Map(), + + /** + * Frequency Cap object - maintains daily and total tile counts, and frequency cap settings + */ + _frequencyCaps: {}, + + /** + * A set of top sites that we can provide suggested links for + */ + _topSitesWithSuggestedLinks: new Set(), + + /** + * lookup Set of inadjacent domains + */ + _inadjacentSites: new Set(), + + /** + * This flag is set if there is a suggested tile configured to avoid + * inadjacent sites in new tab + */ + _avoidInadjacentSites: false, + + /** + * This flag is set if _avoidInadjacentSites is true and there is + * an inadjacent site in the new tab + */ + _newTabHasInadjacentSite: false, + + get _observedPrefs() Object.freeze({ + enhanced: PREF_NEWTAB_ENHANCED, + linksURL: PREF_DIRECTORY_SOURCE, + matchOSLocale: PREF_MATCH_OS_LOCALE, + prefSelectedLocale: PREF_SELECTED_LOCALE, + }), + + get _linksURL() { + if (!this.__linksURL) { + try { + this.__linksURL = Services.prefs.getCharPref(this._observedPrefs["linksURL"]); + this.__linksURLModified = Services.prefs.prefHasUserValue(this._observedPrefs["linksURL"]); + } + catch (e) { + Cu.reportError("Error fetching directory links url from prefs: " + e); + } + } + return this.__linksURL; + }, + + /** + * Gets the currently selected locale for display. + * @return the selected locale or "en-US" if none is selected + */ + get locale() { + let matchOS; + try { + matchOS = Services.prefs.getBoolPref(PREF_MATCH_OS_LOCALE); + } + catch (e) {} + + if (matchOS) { + return Services.locale.getLocaleComponentForUserAgent(); + } + + try { + let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, + Ci.nsIPrefLocalizedString); + if (locale) { + return locale.data; + } + } + catch (e) {} + + try { + return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); + } + catch (e) {} + + return "en-US"; + }, + + /** + * Set appropriate default ping behavior controlled by enhanced pref + */ + _setDefaultEnhanced: function RemoteDirectoryLinksProvider_setDefaultEnhanced() { + if (!Services.prefs.prefHasUserValue(PREF_NEWTAB_ENHANCED)) { + let enhanced = Services.prefs.getBoolPref(PREF_NEWTAB_ENHANCED); + try { + // Default to not enhanced if DNT is set to tell websites to not track + if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled")) { + enhanced = false; + } + } + catch(ex) {} + Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, enhanced); + } + }, + + observe: function RemoteDirectoryLinksProvider_observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + switch (aData) { + // Re-set the default in case the user clears the pref + case this._observedPrefs.enhanced: + this._setDefaultEnhanced(); + break; + + case this._observedPrefs.linksURL: + delete this.__linksURL; + // fallthrough + + // Force directory download on changes to fetch related prefs + case this._observedPrefs.matchOSLocale: + case this._observedPrefs.prefSelectedLocale: + this._fetchAndCacheLinksIfNecessary(true); + break; + } + } + }, + + _addPrefsObserver: function RemoteDirectoryLinksProvider_addObserver() { + for (let pref in this._observedPrefs) { + let prefName = this._observedPrefs[pref]; + Services.prefs.addObserver(prefName, this, false); + } + }, + + _removePrefsObserver: function RemoteDirectoryLinksProvider_removeObserver() { + for (let pref in this._observedPrefs) { + let prefName = this._observedPrefs[pref]; + Services.prefs.removeObserver(prefName, this); + } + }, + + _cacheSuggestedLinks: function(link) { + // Don't cache links that don't have the expected 'frecent_sites' + if (!link.frecent_sites) { + return; + } + + for (let suggestedSite of link.frecent_sites) { + let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map(); + suggestedMap.set(link.url, link); + this._setupStartEndTime(link); + this._suggestedLinks.set(suggestedSite, suggestedMap); + } + }, + + _fetchAndCacheLinks: function RemoteDirectoryLinksProvider_fetchAndCacheLinks(uri) { + // Replace with the same display locale used for selecting links data + uri = uri.replace("%LOCALE%", this.locale); + uri = uri.replace("%CHANNEL%", UpdateUtils.UpdateChannel); + + return this._downloadJsonData(uri).then(json => { + return OS.File.writeAtomic(this._directoryFilePath, json, {tmpPath: this._directoryFilePath + ".tmp"}); + }); + }, + + /** + * Downloads a links with json content + * @param download uri + * @return promise resolved to json string, "{}" returned if status != 200 + */ + _downloadJsonData: function RemoteDirectoryLinksProvider__downloadJsonData(uri) { + let deferred = Promise.defer(); + let xmlHttp = this._newXHR(); + + xmlHttp.onload = function(aResponse) { + let json = this.responseText; + if (this.status && this.status != 200) { + json = "{}"; + } + deferred.resolve(json); + }; + + xmlHttp.onerror = function(e) { + deferred.reject("Fetching " + uri + " results in error code: " + e.target.status); + }; + + try { + xmlHttp.open("GET", uri); + // Override the type so XHR doesn't complain about not well-formed XML + xmlHttp.overrideMimeType(DIRECTORY_LINKS_TYPE); + // Set the appropriate request type for servers that require correct types + xmlHttp.setRequestHeader("Content-Type", DIRECTORY_LINKS_TYPE); + xmlHttp.send(); + } catch (e) { + deferred.reject("Error fetching " + uri); + Cu.reportError(e); + } + return deferred.promise; + }, + + /** + * Downloads directory links if needed + * @return promise resolved immediately if no download needed, or upon completion + */ + _fetchAndCacheLinksIfNecessary: function RemoteDirectoryLinksProvider_fetchAndCacheLinksIfNecessary(forceDownload=false) { + if (this._downloadDeferred) { + // fetching links already - just return the promise + return this._downloadDeferred.promise; + } + + if (forceDownload || this._needsDownload) { + this._downloadDeferred = Promise.defer(); + this._fetchAndCacheLinks(this._linksURL).then(() => { + // the new file was successfully downloaded and cached, so update a timestamp + this._lastDownloadMS = Date.now(); + this._downloadDeferred.resolve(); + this._downloadDeferred = null; + this._callObservers("onManyLinksChanged") + }, + error => { + this._downloadDeferred.resolve(); + this._downloadDeferred = null; + this._callObservers("onDownloadFail"); + }); + return this._downloadDeferred.promise; + } + + // download is not needed + return Promise.resolve(); + }, + + /** + * @return true if download is needed, false otherwise + */ + get _needsDownload () { + // fail if last download occured less then 24 hours ago + if ((Date.now() - this._lastDownloadMS) > this._downloadIntervalMS) { + return true; + } + return false; + }, + + /** + * Create a new XMLHttpRequest that is anonymous, i.e., doesn't send cookies + */ + _newXHR() { + return new XMLHttpRequest({mozAnon: true}); + }, + + /** + * Reads directory links file and parses its content + * @return a promise resolved to an object with keys 'directory' and 'suggested', + * each containing a valid list of links, + * or {'directory': [], 'suggested': []} if read or parse fails. + */ + _readDirectoryLinksFile: function RemoteDirectoryLinksProvider_readDirectoryLinksFile() { + let emptyOutput = {directory: [], suggested: [], enhanced: []}; + return OS.File.read(this._directoryFilePath).then(binaryData => { + let output; + try { + let json = gTextDecoder.decode(binaryData); + let linksObj = JSON.parse(json); + output = {directory: linksObj.directory || [], + suggested: linksObj.suggested || [], + enhanced: linksObj.enhanced || []}; + } + catch (e) { + Cu.reportError(e); + } + return output || emptyOutput; + }, + error => { + Cu.reportError(error); + return emptyOutput; + }); + }, + + /** + * Translates link.time_limits to UTC miliseconds and sets + * link.startTime and link.endTime properties in link object + */ + _setupStartEndTime: function RemoteDirectoryLinksProvider_setupStartEndTime(link) { + // set start/end limits. Use ISO_8601 format: '2014-01-10T20:20:20.600Z' + // (details here http://en.wikipedia.org/wiki/ISO_8601) + // Note that if timezone is missing, FX will interpret as local time + // meaning that the server can sepecify any time, but if the capmaign + // needs to start at same time across multiple timezones, the server + // omits timezone indicator + if (!link.time_limits) { + return; + } + + let parsedTime; + if (link.time_limits.start) { + parsedTime = Date.parse(link.time_limits.start); + if (parsedTime && !isNaN(parsedTime)) { + link.startTime = parsedTime; + } + } + if (link.time_limits.end) { + parsedTime = Date.parse(link.time_limits.end); + if (parsedTime && !isNaN(parsedTime)) { + link.endTime = parsedTime; + } + } + }, + + /* + * Handles campaign timeout + */ + _onCampaignTimeout: function RemoteDirectoryLinksProvider_onCampaignTimeout() { + // _campaignTimeoutID is invalid here, so just set it to null + this._campaignTimeoutID = null; + this._updateSuggestedTile(); + }, + + /* + * Clears capmpaign timeout + */ + _clearCampaignTimeout: function RemoteDirectoryLinksProvider_clearCampaignTimeout() { + if (this._campaignTimeoutID) { + clearTimeout(this._campaignTimeoutID); + this._campaignTimeoutID = null; + } + }, + + /** + * Setup capmpaign timeout to recompute suggested tiles upon + * reaching soonest start or end time for the campaign + * @param timeout in milliseconds + */ + _setupCampaignTimeCheck: function RemoteDirectoryLinksProvider_setupCampaignTimeCheck(timeout) { + // sanity check + if (!timeout || timeout <= 0) { + return; + } + this._clearCampaignTimeout(); + // setup next timeout + this._campaignTimeoutID = setTimeout(this._onCampaignTimeout.bind(this), timeout); + }, + + /** + * Test link for campaign time limits: checks if link falls within start/end time + * and returns an object containing a use flag and the timeoutDate milliseconds + * when the link has to be re-checked for campaign start-ready or end-reach + * @param link + * @return object {use: true or false, timeoutDate: milliseconds or null} + */ + _testLinkForCampaignTimeLimits: function RemoteDirectoryLinksProvider_testLinkForCampaignTimeLimits(link) { + let currentTime = Date.now(); + // test for start time first + if (link.startTime && link.startTime > currentTime) { + // not yet ready for start + return {use: false, timeoutDate: link.startTime}; + } + // otherwise check for end time + if (link.endTime) { + // passed end time + if (link.endTime <= currentTime) { + return {use: false}; + } + // otherwise link is still ok, but we need to set timeoutDate + return {use: true, timeoutDate: link.endTime}; + } + // if we are here, the link is ok and no timeoutDate needed + return {use: true}; + }, + + /** + * Get the enhanced link object for a link (whether history or directory) + */ + getEnhancedLink: function RemoteDirectoryLinksProvider_getEnhancedLink(link) { + // Use the provided link if it's already enhanced + return link.enhancedImageURI && link ? link : + this._enhancedLinks.get(RemoteNewTabUtils.extractSite(link.url)); + }, + + /** + * Check if a url's scheme is in a Set of allowed schemes and if the base + * domain is allowed. + * @param url to check + * @param allowed Set of allowed schemes + * @param checkBase boolean to check the base domain + */ + isURLAllowed(url, allowed, checkBase) { + // Assume no url is an allowed url + if (!url) { + return true; + } + + let scheme = "", base = ""; + try { + // A malformed url will not be allowed + let uri = Services.io.newURI(url, null, null); + scheme = uri.scheme; + + // URIs without base domains will be allowed + base = Services.eTLD.getBaseDomain(uri); + } + catch(ex) {} + // Require a scheme match and the base only if desired + return allowed.has(scheme) && (!checkBase || ALLOWED_URL_BASE.has(base)); + }, + + _escapeChars(text) { + let charMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + return text.replace(/[&<>"']/g, (character) => charMap[character]); + }, + + /** + * Gets the current set of directory links. + * @param aCallback The function that the array of links is passed to. + */ + getLinks: function RemoteDirectoryLinksProvider_getLinks(aCallback) { + this._readDirectoryLinksFile().then(rawLinks => { + // Reset the cache of suggested tiles and enhanced images for this new set of links + this._enhancedLinks.clear(); + this._suggestedLinks.clear(); + this._clearCampaignTimeout(); + this._avoidInadjacentSites = false; + + // Only check base domain for images when using the default pref + let checkBase = !this.__linksURLModified; + let validityFilter = function(link) { + // Make sure the link url is allowed and images too if they exist + return this.isURLAllowed(link.url, ALLOWED_LINK_SCHEMES, false) && + this.isURLAllowed(link.imageURI, ALLOWED_IMAGE_SCHEMES, checkBase) && + this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES, checkBase); + }.bind(this); + + rawLinks.suggested.filter(validityFilter).forEach((link, position) => { + // Suggested sites must have an adgroup name. + if (!link.adgroup_name) { + return; + } + + let sanitizeFlags = ParserUtils.SanitizerCidEmbedsOnly | + ParserUtils.SanitizerDropForms | + ParserUtils.SanitizerDropNonCSSPresentation; + + link.explanation = this._escapeChars(link.explanation ? ParserUtils.convertToPlainText(link.explanation, sanitizeFlags, 0) : ""); + link.targetedName = this._escapeChars(ParserUtils.convertToPlainText(link.adgroup_name, sanitizeFlags, 0)); + link.lastVisitDate = rawLinks.suggested.length - position; + // check if link wants to avoid inadjacent sites + if (link.check_inadjacency) { + this._avoidInadjacentSites = true; + } + + // We cache suggested tiles here but do not push any of them in the links list yet. + // The decision for which suggested tile to include will be made separately. + this._cacheSuggestedLinks(link); + this._updateFrequencyCapSettings(link); + }); + + rawLinks.enhanced.filter(validityFilter).forEach((link, position) => { + link.lastVisitDate = rawLinks.enhanced.length - position; + + // Stash the enhanced image for the site + if (link.enhancedImageURI) { + this._enhancedLinks.set(RemoteNewTabUtils.extractSite(link.url), link); + } + }); + + let links = rawLinks.directory.filter(validityFilter).map((link, position) => { + link.lastVisitDate = rawLinks.directory.length - position; + link.frecency = DIRECTORY_FRECENCY; + return link; + }); + + // Allow for one link suggestion on top of the default directory links + this.maxNumLinks = links.length + 1; + + // prune frequency caps of outdated urls + this._pruneFrequencyCapUrls(); + // write frequency caps object to disk asynchronously + this._writeFrequencyCapFile(); + + return links; + }).catch(ex => { + Cu.reportError(ex); + return []; + }).then(links => { + aCallback(links); + this._populatePlacesLinks(); + }); + }, + + init: function RemoteDirectoryLinksProvider_init() { + this._setDefaultEnhanced(); + this._addPrefsObserver(); + // setup directory file path and last download timestamp + this._directoryFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, DIRECTORY_LINKS_FILE); + this._lastDownloadMS = 0; + + // setup frequency cap file path + this._frequencyCapFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, FREQUENCY_CAP_FILE); + // setup inadjacent sites URL + this._inadjacentSitesUrl = INADJACENCY_SOURCE; + + RemoteNewTabUtils.placesProvider.addObserver(this); + RemoteNewTabUtils.links.addObserver(this); + + return Task.spawn(function() { + // get the last modified time of the links file if it exists + let doesFileExists = yield OS.File.exists(this._directoryFilePath); + if (doesFileExists) { + let fileInfo = yield OS.File.stat(this._directoryFilePath); + this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate); + } + // read frequency cap file + yield this._readFrequencyCapFile(); + // fetch directory on startup without force + yield this._fetchAndCacheLinksIfNecessary(); + // fecth inadjacent sites on startup + yield this._loadInadjacentSites(); + }.bind(this)); + }, + + _handleManyLinksChanged: function() { + this._topSitesWithSuggestedLinks.clear(); + this._suggestedLinks.forEach((suggestedLinks, site) => { + if (RemoteNewTabUtils.isTopPlacesSite(site)) { + this._topSitesWithSuggestedLinks.add(site); + } + }); + this._updateSuggestedTile(); + }, + + /** + * Updates _topSitesWithSuggestedLinks based on the link that was changed. + * + * @return true if _topSitesWithSuggestedLinks was modified, false otherwise. + */ + _handleLinkChanged: function(aLink) { + let changedLinkSite = RemoteNewTabUtils.extractSite(aLink.url); + let linkStored = this._topSitesWithSuggestedLinks.has(changedLinkSite); + + if (!RemoteNewTabUtils.isTopPlacesSite(changedLinkSite) && linkStored) { + this._topSitesWithSuggestedLinks.delete(changedLinkSite); + return true; + } + + if (this._suggestedLinks.has(changedLinkSite) && + RemoteNewTabUtils.isTopPlacesSite(changedLinkSite) && !linkStored) { + this._topSitesWithSuggestedLinks.add(changedLinkSite); + return true; + } + + // always run _updateSuggestedTile if aLink is inadjacent + // and there are tiles configured to avoid it + if (this._avoidInadjacentSites && this._isInadjacentLink(aLink)) { + return true; + } + + return false; + }, + + _populatePlacesLinks: function () { + RemoteNewTabUtils.links.populateProviderCache(RemoteNewTabUtils.placesProvider, () => { + this._handleManyLinksChanged(); + }); + }, + + onDeleteURI: function(aProvider, aLink) { + let {url} = aLink; + // remove clicked flag for that url and + // call observer upon disk write completion + this._removeTileClick(url).then(() => { + this._callObservers("onDeleteURI", url); + }); + }, + + onClearHistory: function() { + // remove all clicked flags and call observers upon file write + this._removeAllTileClicks().then(() => { + this._callObservers("onClearHistory"); + }); + }, + + onLinkChanged: function (aProvider, aLink) { + // Make sure RemoteNewTabUtils.links handles the notification first. + setTimeout(() => { + if (this._handleLinkChanged(aLink) || this._shouldUpdateSuggestedTile()) { + this._updateSuggestedTile(); + } + }, 0); + }, + + onManyLinksChanged: function () { + // Make sure RemoteNewTabUtils.links handles the notification first. + setTimeout(() => { + this._handleManyLinksChanged(); + }, 0); + }, + + _getCurrentTopSiteCount: function() { + let visibleTopSiteCount = 0; + let newTabLinks = RemoteNewTabUtils.links.getLinks(); + for (let link of newTabLinks.slice(0, MIN_VISIBLE_HISTORY_TILES)) { + // compute visibleTopSiteCount for suggested tiles + if (link && (link.type == "history" || link.type == "enhanced")) { + visibleTopSiteCount++; + } + } + // since newTabLinks are available, set _newTabHasInadjacentSite here + // note that _shouldUpdateSuggestedTile is called by _updateSuggestedTile + this._newTabHasInadjacentSite = this._avoidInadjacentSites && this._checkForInadjacentSites(newTabLinks); + + return visibleTopSiteCount; + }, + + _shouldUpdateSuggestedTile: function() { + let sortedLinks = RemoteNewTabUtils.getProviderLinks(this); + + let mostFrecentLink = {}; + if (sortedLinks && sortedLinks.length) { + mostFrecentLink = sortedLinks[0] + } + + let currTopSiteCount = this._getCurrentTopSiteCount(); + if ((!mostFrecentLink.targetedSite && currTopSiteCount >= MIN_VISIBLE_HISTORY_TILES) || + (mostFrecentLink.targetedSite && currTopSiteCount < MIN_VISIBLE_HISTORY_TILES)) { + // If mostFrecentLink has a targetedSite then mostFrecentLink is a suggested link. + // If we have enough history links (8+) to show a suggested tile and we are not + // already showing one, then we should update (to *attempt* to add a suggested tile). + // OR if we don't have enough history to show a suggested tile (<8) and we are + // currently showing one, we should update (to remove it). + return true; + } + + return false; + }, + + /** + * Chooses and returns a suggested tile based on a user's top sites + * that we have an available suggested tile for. + * + * @return the chosen suggested tile, or undefined if there isn't one + */ + _updateSuggestedTile: function() { + let sortedLinks = RemoteNewTabUtils.getProviderLinks(this); + + if (!sortedLinks) { + // If RemoteNewTabUtils.links.resetCache() is called before getting here, + // sortedLinks may be undefined. + return; + } + + // Delete the current suggested tile, if one exists. + let initialLength = sortedLinks.length; + if (initialLength) { + let mostFrecentLink = sortedLinks[0]; + if (mostFrecentLink.targetedSite) { + this._callObservers("onLinkChanged", { + url: mostFrecentLink.url, + frecency: SUGGESTED_FRECENCY, + lastVisitDate: mostFrecentLink.lastVisitDate, + type: mostFrecentLink.type, + }, 0, true); + } + } + + if (this._topSitesWithSuggestedLinks.size == 0 || !this._shouldUpdateSuggestedTile()) { + // There are no potential suggested links we can show or not + // enough history for a suggested tile. + return; + } + + // Create a flat list of all possible links we can show as suggested. + // Note that many top sites may map to the same suggested links, but we only + // want to count each suggested link once (based on url), thus possibleLinks is a map + // from url to suggestedLink. Thus, each link has an equal chance of being chosen at + // random from flattenedLinks if it appears only once. + let nextTimeout; + let possibleLinks = new Map(); + let targetedSites = new Map(); + this._topSitesWithSuggestedLinks.forEach(topSiteWithSuggestedLink => { + let suggestedLinksMap = this._suggestedLinks.get(topSiteWithSuggestedLink); + suggestedLinksMap.forEach((suggestedLink, url) => { + // Skip this link if we've shown it too many times already + if (!this._testFrequencyCapLimits(url)) { + return; + } + + // as we iterate suggestedLinks, check for campaign start/end + // time limits, and set nextTimeout to the closest timestamp + let {use, timeoutDate} = this._testLinkForCampaignTimeLimits(suggestedLink); + // update nextTimeout is necessary + if (timeoutDate && (!nextTimeout || nextTimeout > timeoutDate)) { + nextTimeout = timeoutDate; + } + // Skip link if it falls outside campaign time limits + if (!use) { + return; + } + + // Skip link if it avoids inadjacent sites and newtab has one + if (suggestedLink.check_inadjacency && this._newTabHasInadjacentSite) { + return; + } + + possibleLinks.set(url, suggestedLink); + + // Keep a map of URL to targeted sites. We later use this to show the user + // what site they visited to trigger this suggestion. + if (!targetedSites.get(url)) { + targetedSites.set(url, []); + } + targetedSites.get(url).push(topSiteWithSuggestedLink); + }) + }); + + // setup timeout check for starting or ending campaigns + if (nextTimeout) { + this._setupCampaignTimeCheck(nextTimeout - Date.now()); + } + + // We might have run out of possible links to show + let numLinks = possibleLinks.size; + if (numLinks == 0) { + return; + } + + let flattenedLinks = [...possibleLinks.values()]; + + // Choose our suggested link at random + let suggestedIndex = Math.floor(Math.random() * numLinks); + let chosenSuggestedLink = flattenedLinks[suggestedIndex]; + + // Add the suggested link to the front with some extra values + this._callObservers("onLinkChanged", Object.assign({ + frecency: SUGGESTED_FRECENCY, + + // Choose the first site a user has visited as the target. In the future, + // this should be the site with the highest frecency. However, we currently + // store frecency by URL not by site. + targetedSite: targetedSites.get(chosenSuggestedLink.url).length ? + targetedSites.get(chosenSuggestedLink.url)[0] : null + }, chosenSuggestedLink)); + return chosenSuggestedLink; + }, + + /** + * Loads inadjacent sites + * @return a promise resolved when lookup Set for sites is built + */ + _loadInadjacentSites: function RemoteDirectoryLinksProvider_loadInadjacentSites() { + return this._downloadJsonData(this._inadjacentSitesUrl).then(jsonString => { + let jsonObject = {}; + try { + jsonObject = JSON.parse(jsonString); + } + catch (e) { + Cu.reportError(e); + } + + this._inadjacentSites = new Set(jsonObject.domains); + }); + }, + + /** + * Genegrates hash suitable for looking up inadjacent site + * @param value to hsh + * @return hased value, base64-ed + */ + _generateHash: function RemoteDirectoryLinksProvider_generateHash(value) { + let byteArr = gUnicodeConverter.convertToByteArray(value); + gCryptoHash.init(gCryptoHash.MD5); + gCryptoHash.update(byteArr, byteArr.length); + return gCryptoHash.finish(true); + }, + + /** + * Checks if link belongs to inadjacent domain + * @param link to check + * @return true for inadjacent domains, false otherwise + */ + _isInadjacentLink: function RemoteDirectoryLinksProvider_isInadjacentLink(link) { + let baseDomain = link.baseDomain || RemoteNewTabUtils.extractSite(link.url || ""); + if (!baseDomain) { + return false; + } + // check if hashed domain is inadjacent + return this._inadjacentSites.has(this._generateHash(baseDomain)); + }, + + /** + * Checks if new tab has inadjacent site + * @param new tab links (or nothing, in which case RemoteNewTabUtils.links.getLinks() is called + * @return true if new tab shows has inadjacent site + */ + _checkForInadjacentSites: function RemoteDirectoryLinksProvider_checkForInadjacentSites(newTabLink) { + let links = newTabLink || RemoteNewTabUtils.links.getLinks(); + for (let link of links.slice(0, MAX_VISIBLE_HISTORY_TILES)) { + // check links against inadjacent list - specifically include ALL link types + if (this._isInadjacentLink(link)) { + return true; + } + } + return false; + }, + + /** + * Reads json file, parses its content, and returns resulting object + * @param json file path + * @param json object to return in case file read or parse fails + * @return a promise resolved to a valid object or undefined upon error + */ + _readJsonFile: Task.async(function* (filePath, nullObject) { + let jsonObj; + try { + let binaryData = yield OS.File.read(filePath); + let json = gTextDecoder.decode(binaryData); + jsonObj = JSON.parse(json); + } + catch (e) {} + return jsonObj || nullObject; + }), + + /** + * Loads frequency cap object from file and parses its content + * @return a promise resolved upon load completion + * on error or non-exstent file _frequencyCaps is set to empty object + */ + _readFrequencyCapFile: Task.async(function* () { + // set _frequencyCaps object to file's content or empty object + this._frequencyCaps = yield this._readJsonFile(this._frequencyCapFilePath, {}); + }), + + /** + * Saves frequency cap object to file + * @return a promise resolved upon file i/o completion + */ + _writeFrequencyCapFile: function RemoteDirectoryLinksProvider_writeFrequencyCapFile() { + let json = JSON.stringify(this._frequencyCaps || {}); + return OS.File.writeAtomic(this._frequencyCapFilePath, json, {tmpPath: this._frequencyCapFilePath + ".tmp"}); + }, + + /** + * Clears frequency cap object and writes empty json to file + * @return a promise resolved upon file i/o completion + */ + _clearFrequencyCap: function RemoteDirectoryLinksProvider_clearFrequencyCap() { + this._frequencyCaps = {}; + return this._writeFrequencyCapFile(); + }, + + /** + * updates frequency cap configuration for a link + */ + _updateFrequencyCapSettings: function RemoteDirectoryLinksProvider_updateFrequencyCapSettings(link) { + let capsObject = this._frequencyCaps[link.url]; + if (!capsObject) { + // create an object with empty counts + capsObject = { + dailyViews: 0, + totalViews: 0, + lastShownDate: 0, + }; + this._frequencyCaps[link.url] = capsObject; + } + // set last updated timestamp + capsObject.lastUpdated = Date.now(); + // check for link configuration + if (link.frequency_caps) { + capsObject.dailyCap = link.frequency_caps.daily || DEFAULT_DAILY_FREQUENCY_CAP; + capsObject.totalCap = link.frequency_caps.total || DEFAULT_TOTAL_FREQUENCY_CAP; + } + else { + // fallback to defaults + capsObject.dailyCap = DEFAULT_DAILY_FREQUENCY_CAP; + capsObject.totalCap = DEFAULT_TOTAL_FREQUENCY_CAP; + } + }, + + /** + * Prunes frequency cap objects for outdated links + * @param timeDetla milliseconds + * all cap objects with lastUpdated less than (now() - timeDelta) + * will be removed. This is done to remove frequency cap objects + * for unused tile urls + */ + _pruneFrequencyCapUrls: function RemoteDirectoryLinksProvider_pruneFrequencyCapUrls(timeDelta = DEFAULT_PRUNE_TIME_DELTA) { + let timeThreshold = Date.now() - timeDelta; + Object.keys(this._frequencyCaps).forEach(url => { + if (this._frequencyCaps[url].lastUpdated <= timeThreshold) { + delete this._frequencyCaps[url]; + } + }); + }, + + /** + * Checks if supplied timestamp happened today + * @param timestamp in milliseconds + * @return true if the timestamp was made today, false otherwise + */ + _wasToday: function RemoteDirectoryLinksProvider_wasToday(timestamp) { + let showOn = new Date(timestamp); + let today = new Date(); + // call timestamps identical if both day and month are same + return showOn.getDate() == today.getDate() && + showOn.getMonth() == today.getMonth() && + showOn.getYear() == today.getYear(); + }, + + /** + * adds some number of views for a url + * @param url String url of the suggested link + */ + _addFrequencyCapView: function RemoteDirectoryLinksProvider_addFrequencyCapView(url) { + let capObject = this._frequencyCaps[url]; + // sanity check + if (!capObject) { + return; + } + + // if the day is new: reset the daily counter and lastShownDate + if (!this._wasToday(capObject.lastShownDate)) { + capObject.dailyViews = 0; + // update lastShownDate + capObject.lastShownDate = Date.now(); + } + + // bump both daily and total counters + capObject.totalViews++; + capObject.dailyViews++; + + // if any of the caps is reached - update suggested tiles + if (capObject.totalViews >= capObject.totalCap || + capObject.dailyViews >= capObject.dailyCap) { + this._updateSuggestedTile(); + } + }, + + /** + * Sets clicked flag for link url + * @param url String url of the suggested link + */ + _setFrequencyCapClick(url) { + let capObject = this._frequencyCaps[url]; + // sanity check + if (!capObject) { + return; + } + capObject.clicked = true; + // and update suggested tiles, since current tile became invalid + this._updateSuggestedTile(); + }, + + /** + * Tests frequency cap limits for link url + * @param url String url of the suggested link + * @return true if link is viewable, false otherwise + */ + _testFrequencyCapLimits: function RemoteDirectoryLinksProvider_testFrequencyCapLimits(url) { + let capObject = this._frequencyCaps[url]; + // sanity check: if url is missing - do not show this tile + if (!capObject) { + return false; + } + + // check for clicked set or total views reached + if (capObject.clicked || capObject.totalViews >= capObject.totalCap) { + return false; + } + + // otherwise check if link is over daily views limit + if (this._wasToday(capObject.lastShownDate) && + capObject.dailyViews >= capObject.dailyCap) { + return false; + } + + // we passed all cap tests: return true + return true; + }, + + /** + * Removes clicked flag from frequency cap entry for tile landing url + * @param url String url of the suggested link + * @return promise resolved upon disk write completion + */ + _removeTileClick: function RemoteDirectoryLinksProvider_removeTileClick(url = "") { + // remove trailing slash, to accomodate Places sending site urls ending with '/' + let noTrailingSlashUrl = url.replace(/\/$/,""); + let capObject = this._frequencyCaps[url] || this._frequencyCaps[noTrailingSlashUrl]; + // return resolved promise if capObject is not found + if (!capObject) { + return Promise.resolve(); + } + // otherwise remove clicked flag + delete capObject.clicked; + return this._writeFrequencyCapFile(); + }, + + /** + * Removes all clicked flags from frequency cap object + * @return promise resolved upon disk write completion + */ + _removeAllTileClicks: function RemoteDirectoryLinksProvider_removeAllTileClicks() { + Object.keys(this._frequencyCaps).forEach(url => { + delete this._frequencyCaps[url].clicked; + }); + return this._writeFrequencyCapFile(); + }, + + /** + * Return the object to its pre-init state + */ + reset: function RemoteDirectoryLinksProvider_reset() { + delete this.__linksURL; + this._removePrefsObserver(); + this._removeObservers(); + }, + + addObserver: function RemoteDirectoryLinksProvider_addObserver(aObserver) { + this._observers.add(aObserver); + }, + + removeObserver: function RemoteDirectoryLinksProvider_removeObserver(aObserver) { + this._observers.delete(aObserver); + }, + + _callObservers(methodName, ...args) { + for (let obs of this._observers) { + if (typeof(obs[methodName]) == "function") { + try { + obs[methodName](this, ...args); + } catch (err) { + Cu.reportError(err); + } + } + } + }, + + _removeObservers: function() { + this._observers.clear(); + } +}; diff --git a/browser/components/newtab/RemoteNewTabLocation.jsm b/browser/components/newtab/RemoteNewTabLocation.jsm new file mode 100644 index 000000000000..9ec171987721 --- /dev/null +++ b/browser/components/newtab/RemoteNewTabLocation.jsm @@ -0,0 +1,42 @@ +/* globals Services */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["RemoteNewTabLocation"]; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.importGlobalProperties(["URL"]); + +// TODO: will get dynamically set in bug 1210478 +const DEFAULT_PAGE_LOCATION = "https://newtab.cdn.mozilla.net/v0/nightly/en-US/index.html"; + +this.RemoteNewTabLocation = { + _url: new URL(DEFAULT_PAGE_LOCATION), + _overridden: false, + + get href() { + return this._url.href; + }, + + get origin() { + return this._url.origin; + }, + + get overridden() { + return this._overridden; + }, + + override: function(newURL) { + this._url = new URL(newURL); + this._overridden = true; + Services.obs.notifyObservers(null, "remote-new-tab-location-changed", + this._url.href); + }, + + reset: function() { + this._url = new URL(DEFAULT_PAGE_LOCATION); + this._overridden = false; + Services.obs.notifyObservers(null, "remote-new-tab-location-changed", + this._url.href); + } +}; diff --git a/browser/components/newtab/RemoteNewTabUtils.jsm b/browser/components/newtab/RemoteNewTabUtils.jsm new file mode 100644 index 000000000000..8baa63c3341d --- /dev/null +++ b/browser/components/newtab/RemoteNewTabUtils.jsm @@ -0,0 +1,766 @@ +/* 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["RemoteNewTabUtils"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch", + "resource://gre/modules/BinarySearch.jsm"); + +XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () { + let uri = Services.io.newURI("about:newtab", null, null); + return Services.scriptSecurityManager.createCodebasePrincipal(uri, {}); +}); + +// The maximum number of results PlacesProvider retrieves from history. +const HISTORY_RESULTS_LIMIT = 100; + +// The maximum number of links Links.getLinks will return. +const LINKS_GET_LINKS_LIMIT = 100; + +/** + * Singleton that serves as the default link provider for the grid. It queries + * the history to retrieve the most frequently visited sites. + */ +let PlacesProvider = { + /** + * A count of how many batch updates are under way (batches may be nested, so + * we keep a counter instead of a simple bool). + **/ + _batchProcessingDepth: 0, + + /** + * A flag that tracks whether onFrecencyChanged was notified while a batch + * operation was in progress, to tell us whether to take special action after + * the batch operation completes. + **/ + _batchCalledFrecencyChanged: false, + + /** + * Set this to change the maximum number of links the provider will provide. + */ + maxNumLinks: HISTORY_RESULTS_LIMIT, + + /** + * Must be called before the provider is used. + */ + init: function PlacesProvider_init() { + PlacesUtils.history.addObserver(this, true); + }, + + /** + * Gets the current set of links delivered by this provider. + * @param aCallback The function that the array of links is passed to. + */ + getLinks: function PlacesProvider_getLinks(aCallback) { + let options = PlacesUtils.history.getNewQueryOptions(); + options.maxResults = this.maxNumLinks; + + // Sort by frecency, descending. + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING + + let links = []; + + let callback = { + handleResult: function (aResultSet) { + let row; + + while ((row = aResultSet.getNextRow())) { + let url = row.getResultByIndex(1); + if (LinkChecker.checkLoadURI(url)) { + let title = row.getResultByIndex(2); + let frecency = row.getResultByIndex(12); + let lastVisitDate = row.getResultByIndex(5); + links.push({ + url: url, + title: title, + frecency: frecency, + lastVisitDate: lastVisitDate, + type: "history", + }); + } + } + }, + + handleError: function (aError) { + // Should we somehow handle this error? + aCallback([]); + }, + + handleCompletion: function (aReason) { + // The Places query breaks ties in frecency by place ID descending, but + // that's different from how Links.compareLinks breaks ties, because + // compareLinks doesn't have access to place IDs. It's very important + // that the initial list of links is sorted in the same order imposed by + // compareLinks, because Links uses compareLinks to perform binary + // searches on the list. So, ensure the list is so ordered. + let i = 1; + let outOfOrder = []; + while (i < links.length) { + if (Links.compareLinks(links[i - 1], links[i]) > 0) + outOfOrder.push(links.splice(i, 1)[0]); + else + i++; + } + for (let link of outOfOrder) { + i = BinarySearch.insertionIndexOf(Links.compareLinks, links, link); + links.splice(i, 0, link); + } + + aCallback(links); + } + }; + + // Execute the query. + let query = PlacesUtils.history.getNewQuery(); + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase); + db.asyncExecuteLegacyQueries([query], 1, options, callback); + }, + + /** + * Registers an object that will be notified when the provider's links change. + * @param aObserver An object with the following optional properties: + * * onLinkChanged: A function that's called when a single link + * changes. It's passed the provider and the link object. Only the + * link's `url` property is guaranteed to be present. If its `title` + * property is present, then its title has changed, and the + * property's value is the new title. If any sort properties are + * present, then its position within the provider's list of links may + * have changed, and the properties' values are the new sort-related + * values. Note that this link may not necessarily have been present + * in the lists returned from any previous calls to getLinks. + * * onManyLinksChanged: A function that's called when many links + * change at once. It's passed the provider. You should call + * getLinks to get the provider's new list of links. + */ + addObserver: function PlacesProvider_addObserver(aObserver) { + this._observers.push(aObserver); + }, + + _observers: [], + + /** + * Called by the history service. + */ + onBeginUpdateBatch: function() { + this._batchProcessingDepth += 1; + }, + + onEndUpdateBatch: function() { + this._batchProcessingDepth -= 1; + if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) { + this.onManyFrecenciesChanged(); + this._batchCalledFrecencyChanged = false; + } + }, + + onDeleteURI: function PlacesProvider_onDeleteURI(aURI, aGUID, aReason) { + // let observers remove sensetive data associated with deleted visit + this._callObservers("onDeleteURI", { + url: aURI.spec, + }); + }, + + onClearHistory: function() { + this._callObservers("onClearHistory") + }, + + /** + * Called by the history service. + */ + onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) { + // If something is doing a batch update of history entries we don't want + // to do lots of work for each record. So we just track the fact we need + // to call onManyFrecenciesChanged() once the batch is complete. + if (this._batchProcessingDepth > 0) { + this._batchCalledFrecencyChanged = true; + return; + } + // The implementation of the query in getLinks excludes hidden and + // unvisited pages, so it's important to exclude them here, too. + if (!aHidden && aLastVisitDate) { + this._callObservers("onLinkChanged", { + url: aURI.spec, + frecency: aNewFrecency, + lastVisitDate: aLastVisitDate, + type: "history", + }); + } + }, + + /** + * Called by the history service. + */ + onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() { + this._callObservers("onManyLinksChanged"); + }, + + /** + * Called by the history service. + */ + onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) { + this._callObservers("onLinkChanged", { + url: aURI.spec, + title: aNewTitle + }); + }, + + _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) { + for (let obs of this._observers) { + if (obs[aMethodName]) { + try { + obs[aMethodName](this, aArg); + } catch (err) { + Cu.reportError(err); + } + } + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver, + Ci.nsISupportsWeakReference]), +}; + +/** + * Singleton that provides access to all links contained in the grid (including + * the ones that don't fit on the grid). A link is a plain object that looks + * like this: + * + * { + * url: "http://www.mozilla.org/", + * title: "Mozilla", + * frecency: 1337, + * lastVisitDate: 1394678824766431, + * } + */ +let Links = { + /** + * The maximum number of links returned by getLinks. + */ + maxNumLinks: LINKS_GET_LINKS_LIMIT, + + /** + * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }. + * sortedLinks is the cached, sorted array of links for the provider. + * siteMap is a mapping from base domains to URL count associated with the domain. + * siteMap is used to look up a user's top sites that can be targeted + * with a suggested tile. + * linkMap is a Map from link URLs to link objects. + */ + _providers: new Map(), + + /** + * The properties of link objects used to sort them. + */ + _sortProperties: [ + "frecency", + "lastVisitDate", + "url", + ], + + /** + * List of callbacks waiting for the cache to be populated. + */ + _populateCallbacks: [], + + /** + * A list of objects that are observing links updates. + */ + _observers: [], + + /** + * Registers an object that will be notified when links updates. + */ + addObserver: function (aObserver) { + this._observers.push(aObserver); + }, + + /** + * Adds a link provider. + * @param aProvider The link provider. + */ + addProvider: function Links_addProvider(aProvider) { + this._providers.set(aProvider, null); + aProvider.addObserver(this); + }, + + /** + * Removes a link provider. + * @param aProvider The link provider. + */ + removeProvider: function Links_removeProvider(aProvider) { + if (!this._providers.delete(aProvider)) + throw new Error("Unknown provider"); + }, + + /** + * Populates the cache with fresh links from the providers. + * @param aCallback The callback to call when finished (optional). + * @param aForce When true, populates the cache even when it's already filled. + */ + populateCache: function Links_populateCache(aCallback, aForce) { + let callbacks = this._populateCallbacks; + + // Enqueue the current callback. + callbacks.push(aCallback); + + // There was a callback waiting already, thus the cache has not yet been + // populated. + if (callbacks.length > 1) + return; + + function executeCallbacks() { + while (callbacks.length) { + let callback = callbacks.shift(); + if (callback) { + try { + callback(); + } catch (e) { + // We want to proceed even if a callback fails. + } + } + } + } + + let numProvidersRemaining = this._providers.size; + for (let [provider, links] of this._providers) { + this._populateProviderCache(provider, () => { + if (--numProvidersRemaining == 0) + executeCallbacks(); + }, aForce); + } + }, + + /** + * Gets the current set of links contained in the grid. + * @return The links in the grid. + */ + getLinks: function Links_getLinks() { + let links = this._getMergedProviderLinks(); + + let sites = new Set(); + + // Filter duplicate base domains. + links = links.filter(function (link) { + let site = RemoteNewTabUtils.extractSite(link.url); + link.baseDomain = site; + if (site == null || sites.has(site)) + return false; + sites.add(site); + + return true; + }); + + return links; + }, + + /** + * Resets the links cache. + */ + resetCache: function Links_resetCache() { + for (let provider of this._providers.keys()) { + this._providers.set(provider, null); + } + }, + + /** + * Compares two links. + * @param aLink1 The first link. + * @param aLink2 The second link. + * @return A negative number if aLink1 is ordered before aLink2, zero if + * aLink1 and aLink2 have the same ordering, or a positive number if + * aLink1 is ordered after aLink2. + * + * @note compareLinks's this object is bound to Links below. + */ + compareLinks: function Links_compareLinks(aLink1, aLink2) { + for (let prop of this._sortProperties) { + if (!(prop in aLink1) || !(prop in aLink2)) + throw new Error("Comparable link missing required property: " + prop); + } + return aLink2.frecency - aLink1.frecency || + aLink2.lastVisitDate - aLink1.lastVisitDate || + aLink1.url.localeCompare(aLink2.url); + }, + + _incrementSiteMap: function(map, link) { + let site = RemoteNewTabUtils.extractSite(link.url); + map.set(site, (map.get(site) || 0) + 1); + }, + + _decrementSiteMap: function(map, link) { + let site = RemoteNewTabUtils.extractSite(link.url); + let previousURLCount = map.get(site); + if (previousURLCount === 1) { + map.delete(site); + } else { + map.set(site, previousURLCount - 1); + } + }, + + /** + * Update the siteMap cache based on the link given and whether we need + * to increment or decrement it. We do this by iterating over all stored providers + * to find which provider this link already exists in. For providers that + * have this link, we will adjust siteMap for them accordingly. + * + * @param aLink The link that will affect siteMap + * @param increment A boolean for whether to increment or decrement siteMap + */ + _adjustSiteMapAndNotify: function(aLink, increment=true) { + for (let [provider, cache] of this._providers) { + // We only update siteMap if aLink is already stored in linkMap. + if (cache.linkMap.get(aLink.url)) { + if (increment) { + this._incrementSiteMap(cache.siteMap, aLink); + continue; + } + this._decrementSiteMap(cache.siteMap, aLink); + } + } + this._callObservers("onLinkChanged", aLink); + }, + + populateProviderCache: function(provider, callback) { + if (!this._providers.has(provider)) { + throw new Error("Can only populate provider cache for existing provider."); + } + + return this._populateProviderCache(provider, callback, false); + }, + + /** + * Calls getLinks on the given provider and populates our cache for it. + * @param aProvider The provider whose cache will be populated. + * @param aCallback The callback to call when finished. + * @param aForce When true, populates the provider's cache even when it's + * already filled. + */ + _populateProviderCache: function (aProvider, aCallback, aForce) { + let cache = this._providers.get(aProvider); + let createCache = !cache; + if (createCache) { + cache = { + // Start with a resolved promise. + populatePromise: new Promise(resolve => resolve()), + }; + this._providers.set(aProvider, cache); + } + // Chain the populatePromise so that calls are effectively queued. + cache.populatePromise = cache.populatePromise.then(() => { + return new Promise(resolve => { + if (!createCache && !aForce) { + aCallback(); + resolve(); + return; + } + aProvider.getLinks(links => { + // Filter out null and undefined links so we don't have to deal with + // them in getLinks when merging links from providers. + links = links.filter((link) => !!link); + cache.sortedLinks = links; + cache.siteMap = links.reduce((map, link) => { + this._incrementSiteMap(map, link); + return map; + }, new Map()); + cache.linkMap = links.reduce((map, link) => { + map.set(link.url, link); + return map; + }, new Map()); + aCallback(); + resolve(); + }); + }); + }); + }, + + /** + * Merges the cached lists of links from all providers whose lists are cached. + * @return The merged list. + */ + _getMergedProviderLinks: function Links__getMergedProviderLinks() { + // Build a list containing a copy of each provider's sortedLinks list. + let linkLists = []; + for (let provider of this._providers.keys()) { + let links = this._providers.get(provider); + if (links && links.sortedLinks) { + linkLists.push(links.sortedLinks.slice()); + } + } + + function getNextLink() { + let minLinks = null; + for (let links of linkLists) { + if (links.length && + (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0)) + minLinks = links; + } + return minLinks ? minLinks.shift() : null; + } + + let finalLinks = []; + for (let nextLink = getNextLink(); + nextLink && finalLinks.length < this.maxNumLinks; + nextLink = getNextLink()) { + finalLinks.push(nextLink); + } + + return finalLinks; + }, + + /** + * Called by a provider to notify us when a single link changes. + * @param aProvider The provider whose link changed. + * @param aLink The link that changed. If the link is new, it must have all + * of the _sortProperties. Otherwise, it may have as few or as + * many as is convenient. + * @param aIndex The current index of the changed link in the sortedLinks + cache in _providers. Defaults to -1 if the provider doesn't know the index + * @param aDeleted Boolean indicating if the provider has deleted the link. + */ + onLinkChanged: function Links_onLinkChanged(aProvider, aLink, aIndex=-1, aDeleted=false) { + if (!("url" in aLink)) + throw new Error("Changed links must have a url property"); + + let links = this._providers.get(aProvider); + if (!links) + // This is not an error, it just means that between the time the provider + // was added and the future time we call getLinks on it, it notified us of + // a change. + return; + + let { sortedLinks, siteMap, linkMap } = links; + let existingLink = linkMap.get(aLink.url); + let insertionLink = null; + + if (existingLink) { + // Update our copy's position in O(lg n) by first removing it from its + // list. It's important to do this before modifying its properties. + if (this._sortProperties.some(prop => prop in aLink)) { + let idx = aIndex; + if (idx < 0) { + idx = this._indexOf(sortedLinks, existingLink); + } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) { + throw new Error("aLink should be the same as sortedLinks[idx]"); + } + + if (idx < 0) { + throw new Error("Link should be in _sortedLinks if in _linkMap"); + } + sortedLinks.splice(idx, 1); + + if (aDeleted) { + linkMap.delete(existingLink.url); + this._decrementSiteMap(siteMap, existingLink); + } else { + // Update our copy's properties. + Object.assign(existingLink, aLink); + + // Finally, reinsert our copy below. + insertionLink = existingLink; + } + } + // Update our copy's title in O(1). + if ("title" in aLink && aLink.title != existingLink.title) { + existingLink.title = aLink.title; + } + } + else if (this._sortProperties.every(prop => prop in aLink)) { + // Before doing the O(lg n) insertion below, do an O(1) check for the + // common case where the new link is too low-ranked to be in the list. + if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) { + let lastLink = sortedLinks[sortedLinks.length - 1]; + if (this.compareLinks(lastLink, aLink) < 0) { + return; + } + } + // Copy the link object so that changes later made to it by the caller + // don't affect our copy. + insertionLink = {}; + for (let prop in aLink) { + insertionLink[prop] = aLink[prop]; + } + linkMap.set(aLink.url, insertionLink); + this._incrementSiteMap(siteMap, aLink); + } + + if (insertionLink) { + let idx = this._insertionIndexOf(sortedLinks, insertionLink); + sortedLinks.splice(idx, 0, insertionLink); + if (sortedLinks.length > aProvider.maxNumLinks) { + let lastLink = sortedLinks.pop(); + linkMap.delete(lastLink.url); + this._decrementSiteMap(siteMap, lastLink); + } + } + }, + + /** + * Called by a provider to notify us when many links change. + */ + onManyLinksChanged: function Links_onManyLinksChanged(aProvider) { + this._populateProviderCache(aProvider, () => {}, true); + }, + + _indexOf: function Links__indexOf(aArray, aLink) { + return this._binsearch(aArray, aLink, "indexOf"); + }, + + _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) { + return this._binsearch(aArray, aLink, "insertionIndexOf"); + }, + + _binsearch: function Links__binsearch(aArray, aLink, aMethod) { + return BinarySearch[aMethod](this.compareLinks, aArray, aLink); + }, + + _callObservers(methodName, ...args) { + for (let obs of this._observers) { + if (typeof(obs[methodName]) == "function") { + try { + obs[methodName](this, ...args); + } catch (err) { + Cu.reportError(err); + } + } + } + }, +}; + +Links.compareLinks = Links.compareLinks.bind(Links); + +/** + * Singleton that checks if a given link should be displayed on about:newtab + * or if we should rather not do it for security reasons. URIs that inherit + * their caller's principal will be filtered. + */ +let LinkChecker = { + _cache: {}, + + get flags() { + return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL | + Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS; + }, + + checkLoadURI: function LinkChecker_checkLoadURI(aURI) { + if (!(aURI in this._cache)) + this._cache[aURI] = this._doCheckLoadURI(aURI); + + return this._cache[aURI]; + }, + + _doCheckLoadURI: function Links_doCheckLoadURI(aURI) { + try { + Services.scriptSecurityManager. + checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags); + return true; + } catch (e) { + // We got a weird URI or one that would inherit the caller's principal. + return false; + } + } +}; + +let ExpirationFilter = { + init: function ExpirationFilter_init() { + PageThumbs.addExpirationFilter(this); + }, + + filterForThumbnailExpiration: + function ExpirationFilter_filterForThumbnailExpiration(aCallback) { + Links.populateCache(function () { + let urls = []; + + // Add all URLs to the list that we want to keep thumbnails for. + for (let link of Links.getLinks().slice(0, 25)) { + if (link && link.url) + urls.push(link.url); + } + + aCallback(urls); + }); + } +}; + +/** + * Singleton that provides the public API of this JSM. + */ +this.RemoteNewTabUtils = { + _initialized: false, + + /** + * Extract a "site" from a url in a way that multiple urls of a "site" returns + * the same "site." + * @param aUrl Url spec string + * @return The "site" string or null + */ + extractSite: function Links_extractSite(url) { + let host; + try { + // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of + // URIs, including jar and moz-icon URIs. + host = Services.io.newURI(url, null, null).asciiHost; + } catch (ex) { + return null; + } + + // Strip off common subdomains of the same site (e.g., www, load balancer) + return host.replace(/^(m|mobile|www\d*)\./, ""); + }, + + init: function RemoteNewTabUtils_init() { + if (this.initWithoutProviders()) { + PlacesProvider.init(); + Links.addProvider(PlacesProvider); + } + }, + + initWithoutProviders: function RemoteNewTabUtils_initWithoutProviders() { + if (!this._initialized) { + this._initialized = true; + ExpirationFilter.init(); + return true; + } + return false; + }, + + getProviderLinks: function(aProvider) { + let cache = Links._providers.get(aProvider); + if (cache && cache.sortedLinks) { + return cache.sortedLinks; + } + return []; + }, + + isTopSiteGivenProvider: function(aSite, aProvider) { + let cache = Links._providers.get(aProvider); + if (cache && cache.siteMap) { + return cache.siteMap.has(aSite); + } + return false; + }, + + isTopPlacesSite: function(aSite) { + return this.isTopSiteGivenProvider(aSite, PlacesProvider); + }, + + links: Links, + linkChecker: LinkChecker, + placesProvider: PlacesProvider +}; diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build new file mode 100644 index 000000000000..c005fffb9479 --- /dev/null +++ b/browser/components/newtab/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini'] + +XPCSHELL_TESTS_MANIFESTS += [ + 'tests/xpcshell/xpcshell.ini', +] + +EXTRA_JS_MODULES += [ + 'NewTabURL.jsm', + 'RemoteAboutNewTab.jsm', + 'RemoteDirectoryLinksProvider.jsm', + 'RemoteNewTabLocation.jsm', + 'RemoteNewTabUtils.jsm', +] diff --git a/browser/components/newtab/tests/browser/browser.ini b/browser/components/newtab/tests/browser/browser.ini new file mode 100644 index 000000000000..8ec39550ccff --- /dev/null +++ b/browser/components/newtab/tests/browser/browser.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + dummy_page.html + +[browser_remotenewtab_pageloads.js] diff --git a/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js new file mode 100644 index 000000000000..7b3128054aad --- /dev/null +++ b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js @@ -0,0 +1,47 @@ +/* globals XPCOMUtils, Task, RemoteAboutNewTab, RemoteNewTabLocation, ok */ +"use strict"; + +let Cu = Components.utils; +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation", + "resource:///modules/RemoteNewTabLocation.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab", + "resource:///modules/RemoteAboutNewTab.jsm"); + +const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/dummy_page.html"; + +let tests = []; + +/* + * Tests that: + * 1. overriding the RemoteNewTabPageLocation url causes a remote newtab page + * to load with the new url. + * 2. Messages pass between remote page <--> newTab.js <--> RemoteAboutNewTab.js + */ +tests.push(Task.spawn(function* testMessage() { + yield new Promise(resolve => { + RemoteAboutNewTab.pageListener.addMessageListener("NewTab:testMessage", () => { + ok(true, "message received"); + resolve(); + }); + }); +})); + +add_task(function* open_newtab() { + RemoteNewTabLocation.override(TEST_URL); + let tabOptions = { + gBrowser, + url: "about:remote-newtab" + }; + + function* testLoader() { + yield Promise.all(tests); + } + + yield BrowserTestUtils.withNewTab( + tabOptions, + browser => testLoader // jshint ignore:line + ); +}); diff --git a/browser/components/newtab/tests/browser/dummy_page.html b/browser/components/newtab/tests/browser/dummy_page.html new file mode 100644 index 000000000000..664d33738670 --- /dev/null +++ b/browser/components/newtab/tests/browser/dummy_page.html @@ -0,0 +1,22 @@ + + + + + + + +

Dummy Page

+ + + diff --git a/browser/modules/test/xpcshell/test_NewTabURL.js b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js similarity index 82% rename from browser/modules/test/xpcshell/test_NewTabURL.js rename to browser/components/newtab/tests/xpcshell/test_NewTabURL.js index aad233729d1d..266e14d68a9e 100644 --- a/browser/modules/test/xpcshell/test_NewTabURL.js +++ b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js @@ -20,6 +20,11 @@ add_task(function* () { yield notificationPromise; Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden"); Assert.equal(NewTabURL.get(), "about:newtab", "Newtab URL should be the about:newtab"); + + // change newtab page to remote + Services.prefs.setBoolPref("browser.newtabpage.remote", true); + Assert.equal(NewTabURL.get(), "about:remote-newtab", "Newtab URL should be the about:remote-newtab"); + Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden"); }); function promiseNewtabURLNotification(aNewURL) { diff --git a/browser/components/newtab/tests/xpcshell/test_RemoteDirectoryLinksProvider.js b/browser/components/newtab/tests/xpcshell/test_RemoteDirectoryLinksProvider.js new file mode 100644 index 000000000000..dafec88bd7eb --- /dev/null +++ b/browser/components/newtab/tests/xpcshell/test_RemoteDirectoryLinksProvider.js @@ -0,0 +1,1325 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +/** + * This file tests the RemoteDirectoryLinksProvider singleton in the RemoteDirectoryLinksProvider.jsm module. + */ + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/RemoteDirectoryLinksProvider.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Http.jsm"); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils", + "resource:///modules/RemoteNewTabUtils.jsm"); + +do_get_profile(); + +const DIRECTORY_LINKS_FILE = "directoryLinks.json"; +const DIRECTORY_FRECENCY = 1000; +const SUGGESTED_FRECENCY = Infinity; +const kURLData = {"directory": [{"url":"http://example.com","title":"LocalSource"}]}; +const kTestURL = 'data:application/json,' + JSON.stringify(kURLData); + +// RemoteDirectoryLinksProvider preferences +const kLocalePref = RemoteDirectoryLinksProvider._observedPrefs.prefSelectedLocale; +const kSourceUrlPref = RemoteDirectoryLinksProvider._observedPrefs.linksURL; +const kPingUrlPref = "browser.newtabpage.directory.ping"; +const kNewtabEnhancedPref = "browser.newtabpage.enhanced"; + +// httpd settings +var server; +const kDefaultServerPort = 9000; +const kBaseUrl = "http://localhost:" + kDefaultServerPort; +const kExamplePath = "/exampleTest/"; +const kFailPath = "/fail/"; +const kPingPath = "/ping/"; +const kExampleURL = kBaseUrl + kExamplePath; +const kFailURL = kBaseUrl + kFailPath; +const kPingUrl = kBaseUrl + kPingPath; + +// app/profile/firefox.js are not avaialble in xpcshell: hence, preset them +Services.prefs.setCharPref(kLocalePref, "en-US"); +Services.prefs.setCharPref(kSourceUrlPref, kTestURL); +Services.prefs.setCharPref(kPingUrlPref, kPingUrl); +Services.prefs.setBoolPref(kNewtabEnhancedPref, true); + +const kHttpHandlerData = {}; +kHttpHandlerData[kExamplePath] = {"directory": [{"url":"http://example.com","title":"RemoteSource"}]}; + +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +let gLastRequestPath; + +let suggestedTile1 = { + url: "http://turbotax.com", + type: "affiliate", + lastVisitDate: 4, + adgroup_name: "Adgroup1", + frecent_sites: [ + "taxact.com", + "hrblock.com", + "1040.com", + "taxslayer.com" + ] +}; +let suggestedTile2 = { + url: "http://irs.gov", + type: "affiliate", + lastVisitDate: 3, + adgroup_name: "Adgroup2", + frecent_sites: [ + "taxact.com", + "hrblock.com", + "freetaxusa.com", + "taxslayer.com" + ] +}; +let suggestedTile3 = { + url: "http://hrblock.com", + type: "affiliate", + lastVisitDate: 2, + adgroup_name: "Adgroup3", + frecent_sites: [ + "taxact.com", + "freetaxusa.com", + "1040.com", + "taxslayer.com" + ] +}; +let suggestedTile4 = { + url: "http://sponsoredtile.com", + type: "sponsored", + lastVisitDate: 1, + adgroup_name: "Adgroup4", + frecent_sites: [ + "sponsoredtarget.com" + ] +} +let suggestedTile5 = { + url: "http://eviltile.com", + type: "affiliate", + lastVisitDate: 5, + explanation: "This is an evil tile
muhahaha", + adgroup_name: "WE ARE EVIL ", + frecent_sites: [ + "eviltarget.com" + ] +} +let someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"}; + +function getHttpHandler(path) { + let code = 200; + let body = JSON.stringify(kHttpHandlerData[path]); + if (path == kFailPath) { + code = 204; + } + return function(aRequest, aResponse) { + gLastRequestPath = aRequest.path; + aResponse.setStatusLine(null, code); + aResponse.setHeader("Content-Type", "application/json"); + aResponse.write(body); + }; +} + +function isIdentical(actual, expected) { + if (expected == null) { + do_check_eq(actual, expected); + } + else if (typeof expected == "object") { + // Make sure all the keys match up + do_check_eq(Object.keys(actual).sort() + "", Object.keys(expected).sort()); + + // Recursively check each value individually + Object.keys(expected).forEach(key => { + isIdentical(actual[key], expected[key]); + }); + } + else { + do_check_eq(actual, expected); + } +} + +function fetchData() { + let deferred = Promise.defer(); + + RemoteDirectoryLinksProvider.getLinks(linkData => { + deferred.resolve(linkData); + }); + return deferred.promise; +} + +function readJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { + let decoder = new TextDecoder(); + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); + return OS.File.read(directoryLinksFilePath).then(array => { + let json = decoder.decode(array); + return JSON.parse(json); + }, () => { return "" }); +} + +function cleanJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); + return OS.File.remove(directoryLinksFilePath); +} + +function LinksChangeObserver() { + this.deferred = Promise.defer(); + this.onManyLinksChanged = () => this.deferred.resolve(); + this.onDownloadFail = this.onManyLinksChanged; +} + +function promiseDirectoryDownloadOnPrefChange(pref, newValue) { + let oldValue = Services.prefs.getCharPref(pref); + if (oldValue != newValue) { + // if the preference value is already equal to newValue + // the pref service will not call our observer and we + // deadlock. Hence only setup observer if values differ + let observer = new LinksChangeObserver(); + RemoteDirectoryLinksProvider.addObserver(observer); + Services.prefs.setCharPref(pref, newValue); + return observer.deferred.promise.then(() => { + RemoteDirectoryLinksProvider.removeObserver(observer); + }); + } + return Promise.resolve(); +} + +function promiseSetupRemoteDirectoryLinksProvider(options = {}) { + return Task.spawn(function() { + let linksURL = options.linksURL || kTestURL; + yield RemoteDirectoryLinksProvider.init(); + yield promiseDirectoryDownloadOnPrefChange(kLocalePref, options.locale || "en-US"); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, linksURL); + do_check_eq(RemoteDirectoryLinksProvider._linksURL, linksURL); + RemoteDirectoryLinksProvider._lastDownloadMS = options.lastDownloadMS || 0; + }); +} + +function promiseCleanRemoteDirectoryLinksProvider() { + return Task.spawn(function() { + yield promiseDirectoryDownloadOnPrefChange(kLocalePref, "en-US"); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kTestURL); + yield RemoteDirectoryLinksProvider._clearFrequencyCap(); + yield RemoteDirectoryLinksProvider._loadInadjacentSites(); + RemoteDirectoryLinksProvider._lastDownloadMS = 0; + RemoteDirectoryLinksProvider.reset(); + }); +} + +function run_test() { + // Set up a mock HTTP server to serve a directory page + server = new HttpServer(); + server.registerPrefixHandler(kExamplePath, getHttpHandler(kExamplePath)); + server.registerPrefixHandler(kFailPath, getHttpHandler(kFailPath)); + server.start(kDefaultServerPort); + RemoteNewTabUtils.init(); + + run_next_test(); + + // Teardown. + do_register_cleanup(function() { + server.stop(function() { }); + RemoteDirectoryLinksProvider.reset(); + Services.prefs.clearUserPref(kLocalePref); + Services.prefs.clearUserPref(kSourceUrlPref); + Services.prefs.clearUserPref(kPingUrlPref); + Services.prefs.clearUserPref(kNewtabEnhancedPref); + }); +} + + +function setTimeout(fun, timeout) { + let timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + var event = { + notify: function (timer) { + fun(); + } + }; + timer.initWithCallback(event, timeout, + Components.interfaces.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +add_task(function test_shouldUpdateSuggestedTile() { + let suggestedLink = { + targetedSite: "somesite.com" + }; + + // RemoteDirectoryLinksProvider has no suggested tile and no top sites => no need to update + do_check_eq(RemoteDirectoryLinksProvider._getCurrentTopSiteCount(), 0); + isIdentical(RemoteNewTabUtils.getProviderLinks(), []); + do_check_eq(RemoteDirectoryLinksProvider._shouldUpdateSuggestedTile(), false); + + // RemoteDirectoryLinksProvider has a suggested tile and no top sites => need to update + let origGetProviderLinks = RemoteNewTabUtils.getProviderLinks; + RemoteNewTabUtils.getProviderLinks = (provider) => [suggestedLink]; + + do_check_eq(RemoteDirectoryLinksProvider._getCurrentTopSiteCount(), 0); + isIdentical(RemoteNewTabUtils.getProviderLinks(), [suggestedLink]); + do_check_eq(RemoteDirectoryLinksProvider._shouldUpdateSuggestedTile(), true); + + // RemoteDirectoryLinksProvider has a suggested tile and 8 top sites => no need to update + let origCurrentTopSiteCount = RemoteDirectoryLinksProvider._getCurrentTopSiteCount; + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(RemoteDirectoryLinksProvider._getCurrentTopSiteCount(), 8); + isIdentical(RemoteNewTabUtils.getProviderLinks(), [suggestedLink]); + do_check_eq(RemoteDirectoryLinksProvider._shouldUpdateSuggestedTile(), false); + + // RemoteDirectoryLinksProvider has no suggested tile and 8 top sites => need to update + RemoteNewTabUtils.getProviderLinks = origGetProviderLinks; + do_check_eq(RemoteDirectoryLinksProvider._getCurrentTopSiteCount(), 8); + isIdentical(RemoteNewTabUtils.getProviderLinks(), []); + do_check_eq(RemoteDirectoryLinksProvider._shouldUpdateSuggestedTile(), true); + + // Cleanup + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function test_updateSuggestedTile() { + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + + // Initial setup + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + let testObserver = new TestFirstRun(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = RemoteNewTabUtils.isTopPlacesSite; + RemoteNewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = RemoteNewTabUtils.getProviderLinks; + RemoteNewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = RemoteDirectoryLinksProvider._getCurrentTopSiteCount; + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(RemoteDirectoryLinksProvider._updateSuggestedTile(), undefined); + + function TestFirstRun() { + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + links.unshift(link); + let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; + + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com", "freetaxusa.com"]); + do_check_true(possibleLinks.indexOf(link.url) > -1); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.type, "affiliate"); + resolve(); + }; + }); + } + + function TestChangingSuggestedTile() { + this.count = 0; + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + this.count++; + let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; + + do_check_true(possibleLinks.indexOf(link.url) > -1); + do_check_eq(link.type, "affiliate"); + do_check_true(this.count <= 2); + + if (this.count == 1) { + // The removed suggested link is the one we added initially. + do_check_eq(link.url, links.shift().url); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + } else { + links.unshift(link); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + } + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "freetaxusa.com"]); + resolve(); + } + }); + } + + function TestRemovingSuggestedTile() { + this.count = 0; + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + this.count++; + + do_check_eq(link.type, "affiliate"); + do_check_eq(this.count, 1); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.url, links.shift().url); + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], []); + resolve(); + } + }); + } + + // Test first call to '_updateSuggestedTile()', called when fetching directory links. + yield testObserver.promise; + RemoteDirectoryLinksProvider.removeObserver(testObserver); + + // Removing a top site that doesn't have a suggested link should + // not change the current suggested tile. + let removedTopsite = topSites.shift(); + do_check_eq(removedTopsite, "site0.com"); + do_check_false(RemoteNewTabUtils.isTopPlacesSite(removedTopsite)); + let updateSuggestedTile = RemoteDirectoryLinksProvider._handleLinkChanged({ + url: "http://" + removedTopsite, + type: "history", + }); + do_check_false(updateSuggestedTile); + + // Removing a top site that has a suggested link should + // remove any current suggested tile and add a new one. + testObserver = new TestChangingSuggestedTile(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + removedTopsite = topSites.shift(); + do_check_eq(removedTopsite, "1040.com"); + do_check_false(RemoteNewTabUtils.isTopPlacesSite(removedTopsite)); + RemoteDirectoryLinksProvider.onLinkChanged(RemoteDirectoryLinksProvider, { + url: "http://" + removedTopsite, + type: "history", + }); + yield testObserver.promise; + do_check_eq(testObserver.count, 2); + RemoteDirectoryLinksProvider.removeObserver(testObserver); + + // Removing all top sites with suggested links should remove + // the current suggested link and not replace it. + topSites = []; + testObserver = new TestRemovingSuggestedTile(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + RemoteDirectoryLinksProvider.onManyLinksChanged(); + yield testObserver.promise; + + // Cleanup + yield promiseCleanRemoteDirectoryLinksProvider(); + RemoteNewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + RemoteNewTabUtils.getProviderLinks = origGetProviderLinks; + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function test_suggestedLinksMap() { + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + // Ensure the suggested tiles were not considered directory tiles. + do_check_eq(links.length, 1); + let expected_data = [{url: "http://someothersite.com", title: "Not_A_Suggested_Site", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + isIdentical(links, expected_data); + + // Check for correctly saved suggested tiles data. + expected_data = { + "taxact.com": [suggestedTile1, suggestedTile2, suggestedTile3], + "hrblock.com": [suggestedTile1, suggestedTile2], + "1040.com": [suggestedTile1, suggestedTile3], + "taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3], + "freetaxusa.com": [suggestedTile2, suggestedTile3], + "sponsoredtarget.com": [suggestedTile4], + }; + + let suggestedSites = [...RemoteDirectoryLinksProvider._suggestedLinks.keys()]; + do_check_eq(suggestedSites.indexOf("sponsoredtarget.com"), 5); + do_check_eq(suggestedSites.length, Object.keys(expected_data).length); + + RemoteDirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => { + let suggestedLinksItr = suggestedLinks.values(); + for (let link of expected_data[site]) { + let linkCopy = JSON.parse(JSON.stringify(link)); + linkCopy.targetedName = link.adgroup_name; + linkCopy.explanation = ""; + isIdentical(suggestedLinksItr.next().value, linkCopy); + } + }) + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_topSitesWithSuggestedLinks() { + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let origIsTopPlacesSite = RemoteNewTabUtils.isTopPlacesSite; + RemoteNewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + // Mock out getProviderLinks() so we don't have to populate cache in RemoteNewTabUtils + let origGetProviderLinks = RemoteNewTabUtils.getProviderLinks; + RemoteNewTabUtils.getProviderLinks = function(provider) { + return []; + } + + // We start off with no top sites with suggested links. + do_check_eq(RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks.size, 0); + + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + // Check we've populated suggested links as expected. + do_check_eq(RemoteDirectoryLinksProvider._suggestedLinks.size, 5); + + // When many sites change, we update _topSitesWithSuggestedLinks as expected. + let expectedTopSitesWithSuggestedLinks = ["hrblock.com", "1040.com", "freetaxusa.com"]; + RemoteDirectoryLinksProvider._handleManyLinksChanged(); + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Removing site6.com as a topsite has no impact on _topSitesWithSuggestedLinks. + let popped = topSites.pop(); + RemoteDirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Removing freetaxusa.com as a topsite will remove it from _topSitesWithSuggestedLinks. + popped = topSites.pop(); + expectedTopSitesWithSuggestedLinks.pop(); + RemoteDirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithSuggestedLinks. + topSites.push(popped); + expectedTopSitesWithSuggestedLinks.push(popped); + RemoteDirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Cleanup. + RemoteNewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + RemoteNewTabUtils.getProviderLinks = origGetProviderLinks; +}); + +add_task(function test_fetchAndCacheLinks_local() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + // Trigger cache of data or chrome uri files in profD + yield RemoteDirectoryLinksProvider._fetchAndCacheLinks(kTestURL); + let data = yield readJsonFile(); + isIdentical(data, kURLData); +}); + +add_task(function test_fetchAndCacheLinks_remote() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + // this must trigger directory links json download and save it to cache file + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL + "%LOCALE%"); + do_check_eq(gLastRequestPath, kExamplePath + "en-US"); + let data = yield readJsonFile(); + isIdentical(data, kHttpHandlerData[kExamplePath]); +}); + +add_task(function test_fetchAndCacheLinks_malformedURI() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + let someJunk = "some junk"; + try { + yield RemoteDirectoryLinksProvider._fetchAndCacheLinks(someJunk); + do_throw("Malformed URIs should fail") + } catch (e) { + do_check_eq(e, "Error fetching " + someJunk) + } + + // File should be empty. + let data = yield readJsonFile(); + isIdentical(data, ""); +}); + +add_task(function test_fetchAndCacheLinks_unknownHost() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + let nonExistentServer = "http://localhost:56789/"; + try { + yield RemoteDirectoryLinksProvider._fetchAndCacheLinks(nonExistentServer); + do_throw("BAD URIs should fail"); + } catch (e) { + do_check_true(e.startsWith("Fetching " + nonExistentServer + " results in error code: ")) + } + + // File should be empty. + let data = yield readJsonFile(); + isIdentical(data, ""); +}); + +add_task(function test_fetchAndCacheLinks_non200Status() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kFailURL); + do_check_eq(gLastRequestPath, kFailPath); + let data = yield readJsonFile(); + isIdentical(data, {}); +}); + +// To test onManyLinksChanged observer, trigger a fetch +add_task(function test_RemoteDirectoryLinksProvider__linkObservers() { + yield RemoteDirectoryLinksProvider.init(); + + let testObserver = new LinksChangeObserver(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + do_check_eq(RemoteDirectoryLinksProvider._observers.size, 1); + RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + + yield testObserver.deferred.promise; + RemoteDirectoryLinksProvider._removeObservers(); + do_check_eq(RemoteDirectoryLinksProvider._observers.size, 0); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider__prefObserver_url() { + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: kTestURL}); + + let links = yield fetchData(); + do_check_eq(links.length, 1); + let expectedData = [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + isIdentical(links, expectedData); + + // tests these 2 things: + // 1. _linksURL is properly set after the pref change + // 2. invalid source url is correctly handled + let exampleUrl = 'http://localhost:56789/bad'; + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl); + do_check_eq(RemoteDirectoryLinksProvider._linksURL, exampleUrl); + + // since the download fail, the directory file must remain the same + let newLinks = yield fetchData(); + isIdentical(newLinks, expectedData); + + // now remove the file, and re-download + yield cleanJsonFile(); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl + " "); + // we now should see empty links + newLinks = yield fetchData(); + isIdentical(newLinks, []); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getLinks_noDirectoryData() { + let data = { + "directory": [], + }; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 0); + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getLinks_badData() { + let data = { + "en-US": { + "en-US": [{url: "http://example.com", title: "US"}], + }, + }; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + // Make sure we get nothing for incorrectly formatted data + let links = yield fetchData(); + do_check_eq(links.length, 0); + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_needsDownload() { + // test timestamping + RemoteDirectoryLinksProvider._lastDownloadMS = 0; + do_check_true(RemoteDirectoryLinksProvider._needsDownload); + RemoteDirectoryLinksProvider._lastDownloadMS = Date.now(); + do_check_false(RemoteDirectoryLinksProvider._needsDownload); + RemoteDirectoryLinksProvider._lastDownloadMS = Date.now() - (60*60*24 + 1)*1000; + do_check_true(RemoteDirectoryLinksProvider._needsDownload); + RemoteDirectoryLinksProvider._lastDownloadMS = 0; +}); + +add_task(function test_RemoteDirectoryLinksProvider_fetchAndCacheLinksIfNecessary() { + yield RemoteDirectoryLinksProvider.init(); + yield cleanJsonFile(); + // explicitly change source url to cause the download during setup + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: kTestURL+" "}); + yield RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); + + // inspect lastDownloadMS timestamp which should be 5 seconds less then now() + let lastDownloadMS = RemoteDirectoryLinksProvider._lastDownloadMS; + do_check_true((Date.now() - lastDownloadMS) < 5000); + + // we should have fetched a new file during setup + let data = yield readJsonFile(); + isIdentical(data, kURLData); + + // attempt to download again - the timestamp should not change + yield RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); + do_check_eq(RemoteDirectoryLinksProvider._lastDownloadMS, lastDownloadMS); + + // clean the file and force the download + yield cleanJsonFile(); + yield RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + data = yield readJsonFile(); + isIdentical(data, kURLData); + + // make sure that failed download does not corrupt the file, nor changes lastDownloadMS + lastDownloadMS = RemoteDirectoryLinksProvider._lastDownloadMS; + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, "http://"); + yield RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + data = yield readJsonFile(); + isIdentical(data, kURLData); + do_check_eq(RemoteDirectoryLinksProvider._lastDownloadMS, lastDownloadMS); + + // _fetchAndCacheLinksIfNecessary must return same promise if download is in progress + let downloadPromise = RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + let anotherPromise = RemoteDirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + do_check_true(downloadPromise === anotherPromise); + yield downloadPromise; + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_fetchDirectoryOnPrefChange() { + yield RemoteDirectoryLinksProvider.init(); + + let testObserver = new LinksChangeObserver(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + + yield cleanJsonFile(); + // ensure that provider does not think it needs to download + do_check_false(RemoteDirectoryLinksProvider._needsDownload); + + // change the source URL, which should force directory download + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL); + // then wait for testObserver to fire and test that json is downloaded + yield testObserver.deferred.promise; + do_check_eq(gLastRequestPath, kExamplePath); + let data = yield readJsonFile(); + isIdentical(data, kHttpHandlerData[kExamplePath]); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_fetchDirectoryOnInit() { + // ensure preferences are set to defaults + yield promiseSetupRemoteDirectoryLinksProvider(); + // now clean to provider, so we can init it again + yield promiseCleanRemoteDirectoryLinksProvider(); + + yield cleanJsonFile(); + yield RemoteDirectoryLinksProvider.init(); + let data = yield readJsonFile(); + isIdentical(data, kURLData); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getLinksFromCorruptedFile() { + yield promiseSetupRemoteDirectoryLinksProvider(); + + // write bogus json to a file and attempt to fetch from it + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.profileDir, DIRECTORY_LINKS_FILE); + yield OS.File.writeAtomic(directoryLinksFilePath, '{"en-US":'); + let data = yield fetchData(); + isIdentical(data, []); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getAllowedLinks() { + let data = {"directory": [ + {url: "ftp://example.com"}, + {url: "http://example.net"}, + {url: "javascript:5"}, + {url: "https://example.com"}, + {url: "httpJUNKjavascript:42"}, + {url: "data:text/plain,hi"}, + {url: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining url should be http and https + do_check_eq(links[0].url, data["directory"][1].url); + do_check_eq(links[1].url, data["directory"][3].url); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getAllowedImages() { + let data = {"directory": [ + {url: "http://example.com", imageURI: "ftp://example.com"}, + {url: "http://example.com", imageURI: "http://example.net"}, + {url: "http://example.com", imageURI: "javascript:5"}, + {url: "http://example.com", imageURI: "https://example.com"}, + {url: "http://example.com", imageURI: "httpJUNKjavascript:42"}, + {url: "http://example.com", imageURI: "data:text/plain,hi"}, + {url: "http://example.com", imageURI: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining images should be https and data + do_check_eq(links[0].imageURI, data["directory"][3].imageURI); + do_check_eq(links[1].imageURI, data["directory"][5].imageURI); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getAllowedImages_base() { + let data = {"directory": [ + {url: "http://example1.com", imageURI: "https://example.com"}, + {url: "http://example2.com", imageURI: "https://tiles.cdn.mozilla.net"}, + {url: "http://example3.com", imageURI: "https://tiles2.cdn.mozilla.net"}, + {url: "http://example4.com", enhancedImageURI: "https://mozilla.net"}, + {url: "http://example5.com", imageURI: "data:text/plain,hi"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + // Pretend we're using the default pref to trigger base matching + RemoteDirectoryLinksProvider.__linksURLModified = false; + + let links = yield fetchData(); + do_check_eq(links.length, 4); + + // The only remaining images should be https with mozilla.net or data URI + do_check_eq(links[0].url, data["directory"][1].url); + do_check_eq(links[1].url, data["directory"][2].url); + do_check_eq(links[2].url, data["directory"][3].url); + do_check_eq(links[3].url, data["directory"][4].url); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getAllowedEnhancedImages() { + let data = {"directory": [ + {url: "http://example.com", enhancedImageURI: "ftp://example.com"}, + {url: "http://example.com", enhancedImageURI: "http://example.net"}, + {url: "http://example.com", enhancedImageURI: "javascript:5"}, + {url: "http://example.com", enhancedImageURI: "https://example.com"}, + {url: "http://example.com", enhancedImageURI: "httpJUNKjavascript:42"}, + {url: "http://example.com", enhancedImageURI: "data:text/plain,hi"}, + {url: "http://example.com", enhancedImageURI: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining enhancedImages should be http and https and data + do_check_eq(links[0].enhancedImageURI, data["directory"][3].enhancedImageURI); + do_check_eq(links[1].enhancedImageURI, data["directory"][5].enhancedImageURI); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getEnhancedLink() { + let data = {"enhanced": [ + {url: "http://example.net", enhancedImageURI: "data:,net1"}, + {url: "http://example.com", enhancedImageURI: "data:,com1"}, + {url: "http://example.com", enhancedImageURI: "data:,com2"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 0); // There are no directory links. + + function checkEnhanced(url, image) { + let enhanced = RemoteDirectoryLinksProvider.getEnhancedLink({url: url}); + do_check_eq(enhanced && enhanced.enhancedImageURI, image); + } + + // Get the expected image for the same site + checkEnhanced("http://example.net/", "data:,net1"); + checkEnhanced("http://example.net/path", "data:,net1"); + checkEnhanced("https://www.example.net/", "data:,net1"); + checkEnhanced("https://www3.example.net/", "data:,net1"); + + // Get the image of the last entry + checkEnhanced("http://example.com", "data:,com2"); + + // Get the inline enhanced image + let inline = RemoteDirectoryLinksProvider.getEnhancedLink({ + url: "http://example.com/echo", + enhancedImageURI: "data:,echo", + }); + do_check_eq(inline.enhancedImageURI, "data:,echo"); + do_check_eq(inline.url, "http://example.com/echo"); + + // Undefined for not enhanced + checkEnhanced("http://sub.example.net/", undefined); + checkEnhanced("http://example.org", undefined); + checkEnhanced("http://localhost", undefined); + checkEnhanced("http://127.0.0.1", undefined); + + // Make sure old data is not cached + data = {"enhanced": [ + {url: "http://example.com", enhancedImageURI: "data:,fresh"}, + ]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + links = yield fetchData(); + do_check_eq(links.length, 0); // There are no directory links. + checkEnhanced("http://example.net", undefined); + checkEnhanced("http://example.com", "data:,fresh"); +}); + +add_task(function test_RemoteDirectoryLinksProvider_setDefaultEnhanced() { + function checkDefault(expected) { + Services.prefs.clearUserPref(kNewtabEnhancedPref); + do_check_eq(Services.prefs.getBoolPref(kNewtabEnhancedPref), expected); + } + + // Use the default donottrack prefs (enabled = false) + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + checkDefault(true); + + // Turn on DNT - no track + Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); + checkDefault(false); + + // Turn off DNT header + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + checkDefault(true); + + // Clean up + Services.prefs.clearUserPref("privacy.donottrackheader.value"); +}); + +add_task(function test_timeSensetiveSuggestedTiles() { + // make tile json with start and end dates + let testStartTime = Date.now(); + // start date is now + 1 seconds + let startDate = new Date(testStartTime + 1000); + // end date is now + 3 seconds + let endDate = new Date(testStartTime + 3000); + let suggestedTile = Object.assign({ + time_limits: { + start: startDate.toISOString(), + end: endDate.toISOString(), + } + }, suggestedTile1); + + // Initial setup + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + let testObserver = new TestTimingRun(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = RemoteNewTabUtils.isTopPlacesSite; + RemoteNewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = RemoteNewTabUtils.getProviderLinks; + RemoteNewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = RemoteDirectoryLinksProvider._getCurrentTopSiteCount; + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(RemoteDirectoryLinksProvider._updateSuggestedTile(), undefined); + + // this tester will fire twice: when start limit is reached and when tile link + // is removed upon end of the campaign, in which case deleteFlag will be set + function TestTimingRun() { + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link, ignoreFlag, deleteFlag) => { + // if we are not deleting, add link to links, so we can catch it's removal + if (!deleteFlag) { + links.unshift(link); + } + + isIdentical([...RemoteDirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com"]); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + let timeDelta = Date.now() - testStartTime; + if (!deleteFlag) { + // this is start timeout corresponding to campaign start + // a seconds must pass and targetedSite must be set + do_print("TESTING START timeDelta: " + timeDelta); + do_check_true(timeDelta >= 1000 / 2); // check for at least half time + do_check_eq(link.targetedSite, "hrblock.com"); + do_check_true(RemoteDirectoryLinksProvider._campaignTimeoutID); + } + else { + // this is the campaign end timeout, so 3 seconds must pass + // and timeout should be cleared + do_print("TESTING END timeDelta: " + timeDelta); + do_check_true(timeDelta >= 3000 / 2); // check for at least half time + do_check_false(link.targetedSite); + do_check_false(RemoteDirectoryLinksProvider._campaignTimeoutID); + resolve(); + } + }; + }); + } + + // _updateSuggestedTile() is called when fetching directory links. + yield testObserver.promise; + RemoteDirectoryLinksProvider.removeObserver(testObserver); + + // shoudl suggest nothing + do_check_eq(RemoteDirectoryLinksProvider._updateSuggestedTile(), undefined); + + // set links back to contain directory tile only + links.shift(); + + // drop the end time - we should pick up the tile + suggestedTile.time_limits.end = null; + data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + + // redownload json and getLinks to force time recomputation + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); + + // ensure that there's a link returned by _updateSuggestedTile and no timeout + let deferred = Promise.defer(); + RemoteDirectoryLinksProvider.getLinks(() => { + let link = RemoteDirectoryLinksProvider._updateSuggestedTile(); + // we should have a suggested tile and no timeout + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + do_check_false(RemoteDirectoryLinksProvider._campaignTimeoutID); + deferred.resolve(); + }); + yield deferred.promise; + + // repeat the test for end time only + suggestedTile.time_limits.start = null; + suggestedTile.time_limits.end = (new Date(Date.now() + 3000)).toISOString(); + + data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + + // redownload json and call getLinks() to force time recomputation + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); + + // ensure that there's a link returned by _updateSuggestedTile and timeout set + deferred = Promise.defer(); + RemoteDirectoryLinksProvider.getLinks(() => { + let link = RemoteDirectoryLinksProvider._updateSuggestedTile(); + // we should have a suggested tile and timeout set + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + do_check_true(RemoteDirectoryLinksProvider._campaignTimeoutID); + RemoteDirectoryLinksProvider._clearCampaignTimeout(); + deferred.resolve(); + }); + yield deferred.promise; + + // Cleanup + yield promiseCleanRemoteDirectoryLinksProvider(); + RemoteNewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + RemoteNewTabUtils.getProviderLinks = origGetProviderLinks; + RemoteDirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function test_setupStartEndTime() { + let currentTime = Date.now(); + let dt = new Date(currentTime); + let link = { + time_limits: { + start: dt.toISOString() + } + }; + + // test ISO translation + RemoteDirectoryLinksProvider._setupStartEndTime(link); + do_check_eq(link.startTime, currentTime); + + // test localtime translation + let shiftedDate = new Date(currentTime - dt.getTimezoneOffset()*60*1000); + link.time_limits.start = shiftedDate.toISOString().replace(/Z$/, ""); + + RemoteDirectoryLinksProvider._setupStartEndTime(link); + do_check_eq(link.startTime, currentTime); + + // throw some garbage into date string + delete link.startTime; + link.time_limits.start = "no date" + RemoteDirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); + + link.time_limits.start = "2015-99999-01T00:00:00" + RemoteDirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); + + link.time_limits.start = "20150501T00:00:00" + RemoteDirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); +}); + +add_task(function test_RemoteDirectoryLinksProvider_frequencyCapSetup() { + yield promiseSetupRemoteDirectoryLinksProvider(); + yield RemoteDirectoryLinksProvider.init(); + + yield promiseCleanRemoteDirectoryLinksProvider(); + yield RemoteDirectoryLinksProvider._readFrequencyCapFile(); + isIdentical(RemoteDirectoryLinksProvider._frequencyCaps, {}); + + // setup few links + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + }); + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "2", + frequency_caps: {daily: 1, total: 2} + }); + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "3", + frequency_caps: {total: 2} + }); + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "4", + frequency_caps: {daily: 1} + }); + let freqCapsObject = RemoteDirectoryLinksProvider._frequencyCaps; + let capObject = freqCapsObject["1"]; + let defaultDaily = capObject.dailyCap; + let defaultTotal = capObject.totalCap; + // check if we have defaults set + do_check_true(capObject.dailyCap > 0); + do_check_true(capObject.totalCap > 0); + // check if defaults are properly handled + do_check_eq(freqCapsObject["2"].dailyCap, 1); + do_check_eq(freqCapsObject["2"].totalCap, 2); + do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); + do_check_eq(freqCapsObject["3"].totalCap, 2); + do_check_eq(freqCapsObject["4"].dailyCap, 1); + do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); + + // write object to file + yield RemoteDirectoryLinksProvider._writeFrequencyCapFile(); + // empty out freqCapsObject and read file back + RemoteDirectoryLinksProvider._frequencyCaps = {}; + yield RemoteDirectoryLinksProvider._readFrequencyCapFile(); + // re-ran tests - they should all pass + do_check_eq(freqCapsObject["2"].dailyCap, 1); + do_check_eq(freqCapsObject["2"].totalCap, 2); + do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); + do_check_eq(freqCapsObject["3"].totalCap, 2); + do_check_eq(freqCapsObject["4"].dailyCap, 1); + do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); + + // wait a second and prune frequency caps + yield new Promise(resolve => { + setTimeout(resolve, 1100); + }); + + // update one link and create another + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "3", + frequency_caps: {daily: 1, total: 2} + }); + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "7", + frequency_caps: {daily: 1, total: 2} + }); + // now prune the ones that have been in the object longer than 1 second + RemoteDirectoryLinksProvider._pruneFrequencyCapUrls(1000); + // make sure all keys but "3" and "7" are deleted + Object.keys(RemoteDirectoryLinksProvider._frequencyCaps).forEach(key => { + do_check_true(key == "3" || key == "7"); + }); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_getFrequencyCapLogic() { + yield promiseSetupRemoteDirectoryLinksProvider(); + yield RemoteDirectoryLinksProvider.init(); + + // setup suggested links + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + frequency_caps: {daily: 2, total: 4} + }); + + do_check_true(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + // exhaust daily views + RemoteDirectoryLinksProvider._addFrequencyCapView("1") + do_check_true(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + RemoteDirectoryLinksProvider._addFrequencyCapView("1") + do_check_false(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + + // now step into the furture + let _wasTodayOrig = RemoteDirectoryLinksProvider._wasToday; + RemoteDirectoryLinksProvider._wasToday = function () {return false;} + // exhaust total views + RemoteDirectoryLinksProvider._addFrequencyCapView("1") + do_check_true(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + RemoteDirectoryLinksProvider._addFrequencyCapView("1") + // reached totalViews 4, should return false + do_check_false(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + + // add more views by updating configuration + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + frequency_caps: {daily: 5, total: 10} + }); + // should be true, since we have more total views + do_check_true(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + + // set click flag + RemoteDirectoryLinksProvider._setFrequencyCapClick("1"); + // always false after click + do_check_false(RemoteDirectoryLinksProvider._testFrequencyCapLimits("1")); + + // use unknown urls and ensure nothing breaks + RemoteDirectoryLinksProvider._addFrequencyCapView("nosuch.url"); + RemoteDirectoryLinksProvider._setFrequencyCapClick("nosuch.url"); + // testing unknown url should always return false + do_check_false(RemoteDirectoryLinksProvider._testFrequencyCapLimits("nosuch.url")); + + // reset _wasToday back to original function + RemoteDirectoryLinksProvider._wasToday = _wasTodayOrig; + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_ClickRemoval() { + yield promiseSetupRemoteDirectoryLinksProvider(); + yield RemoteDirectoryLinksProvider.init(); + let landingUrl = "http://foo.com"; + + // setup suggested links + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: landingUrl, + frequency_caps: {daily: 2, total: 4} + }); + + // add views + RemoteDirectoryLinksProvider._addFrequencyCapView(landingUrl) + RemoteDirectoryLinksProvider._addFrequencyCapView(landingUrl) + // make a click + RemoteDirectoryLinksProvider._setFrequencyCapClick(landingUrl); + + // views must be 2 and click must be set + do_check_eq(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); + do_check_true(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].clicked); + + // now insert a visit into places + yield new Promise(resolve => { + PlacesUtils.asyncHistory.updatePlaces( + { + uri: NetUtil.newURI(landingUrl), + title: "HELLO", + visits: [{ + visitDate: Date.now()*1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK + }] + }, + { + handleError: function () {do_check_true(false);}, + handleResult: function () {}, + handleCompletion: function () {resolve();} + } + ); + }); + + function UrlDeletionTester() { + this.promise = new Promise(resolve => { + this.onDeleteURI = (directoryLinksProvider, link) => { + resolve(); + }; + this.onClearHistory = (directoryLinksProvider) => { + resolve(); + }; + }); + }; + + let testObserver = new UrlDeletionTester(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + + PlacesUtils.bhistory.removePage(NetUtil.newURI(landingUrl)); + yield testObserver.promise; + RemoteDirectoryLinksProvider.removeObserver(testObserver); + // views must be 2 and click should not exist + do_check_eq(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); + do_check_false(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); + + // verify that disk written data is kosher + let data = yield readJsonFile(RemoteDirectoryLinksProvider._frequencyCapFilePath); + do_check_eq(data[landingUrl].totalViews, 2); + do_check_false(data[landingUrl].hasOwnProperty("clicked")); + + // now test clear history + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: landingUrl, + frequency_caps: {daily: 2, total: 4} + }); + RemoteDirectoryLinksProvider._updateFrequencyCapSettings({ + url: "http://bar.com", + frequency_caps: {daily: 2, total: 4} + }); + + RemoteDirectoryLinksProvider._setFrequencyCapClick(landingUrl); + RemoteDirectoryLinksProvider._setFrequencyCapClick("http://bar.com"); + // both tiles must have clicked + do_check_true(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].clicked); + do_check_true(RemoteDirectoryLinksProvider._frequencyCaps["http://bar.com"].clicked); + + testObserver = new UrlDeletionTester(); + RemoteDirectoryLinksProvider.addObserver(testObserver); + // remove all hostory + PlacesUtils.bhistory.removeAllPages(); + + yield testObserver.promise; + RemoteDirectoryLinksProvider.removeObserver(testObserver); + // no clicks should remain in the cap object + do_check_false(RemoteDirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); + do_check_false(RemoteDirectoryLinksProvider._frequencyCaps["http://bar.com"].hasOwnProperty("clicked")); + + // verify that disk written data is kosher + data = yield readJsonFile(RemoteDirectoryLinksProvider._frequencyCapFilePath); + do_check_false(data[landingUrl].hasOwnProperty("clicked")); + do_check_false(data["http://bar.com"].hasOwnProperty("clicked")); + + yield promiseCleanRemoteDirectoryLinksProvider(); +}); + +add_task(function test_RemoteDirectoryLinksProvider_anonymous() { + do_check_true(RemoteDirectoryLinksProvider._newXHR().mozAnon); +}); + +add_task(function test_sanitizeExplanation() { + // Note: this is a basic test to ensure we applied sanitization to the link explanation. + // Full testing for appropriate sanitization is done in parser/xml/test/unit/test_sanitizer.js. + let data = {"suggested": [suggestedTile5]}; + let dataURI = 'data:application/json,' + encodeURIComponent(JSON.stringify(data)); + + yield promiseSetupRemoteDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let suggestedSites = [...RemoteDirectoryLinksProvider._suggestedLinks.keys()]; + do_check_eq(suggestedSites.indexOf("eviltarget.com"), 0); + do_check_eq(suggestedSites.length, 1); + + let suggestedLink = [...RemoteDirectoryLinksProvider._suggestedLinks.get(suggestedSites[0]).values()][0]; + do_check_eq(suggestedLink.explanation, "This is an evil tile X muhahaha"); + do_check_eq(suggestedLink.targetedName, "WE ARE EVIL "); +}); diff --git a/browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js new file mode 100644 index 000000000000..e280b03426a5 --- /dev/null +++ b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js @@ -0,0 +1,40 @@ +/* globals ok, equal, RemoteNewTabLocation, Services */ +"use strict"; + +Components.utils.import("resource:///modules/RemoteNewTabLocation.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.importGlobalProperties(["URL"]); + +add_task(function* () { + var notificationPromise; + let defaultHref = RemoteNewTabLocation.href; + + ok(RemoteNewTabLocation.href, "Default location has an href"); + ok(RemoteNewTabLocation.origin, "Default location has an origin"); + ok(!RemoteNewTabLocation.overridden, "Default location is not overridden"); + + let testURL = new URL("https://example.com/"); + + notificationPromise = changeNotificationPromise(testURL.href); + RemoteNewTabLocation.override(testURL.href); + yield notificationPromise; + ok(RemoteNewTabLocation.overridden, "Remote location should be overridden"); + equal(RemoteNewTabLocation.href, testURL.href, "Remote href should be the custom URL"); + equal(RemoteNewTabLocation.origin, testURL.origin, "Remote origin should be the custom URL"); + + notificationPromise = changeNotificationPromise(defaultHref); + RemoteNewTabLocation.reset(); + yield notificationPromise; + ok(!RemoteNewTabLocation.overridden, "Newtab URL should not be overridden"); + equal(RemoteNewTabLocation.href, defaultHref, "Remote href should be reset"); +}); + +function changeNotificationPromise(aNewURL) { + return new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint ignore:line + Services.obs.removeObserver(observer, aTopic); + equal(aData, aNewURL, "remote-new-tab-location-changed data should be new URL."); + resolve(); + }, "remote-new-tab-location-changed", false); + }); +} diff --git a/browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js new file mode 100644 index 000000000000..7c3e0e5cc20e --- /dev/null +++ b/browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js @@ -0,0 +1,375 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// See also browser/base/content/test/newtab/. + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +Cu.import("resource:///modules/RemoteNewTabUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + run_next_test(); +} + +add_task(function validCacheMidPopulation() { + let expectedLinks = makeLinks(0, 3, 1); + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + let promise = new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + + // isTopSiteGivenProvider() and getProviderLinks() should still return results + // even when cache is empty or being populated. + do_check_false(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider)); + do_check_links(RemoteNewTabUtils.getProviderLinks(provider), []); + + yield promise; + + // Once the cache is populated, we get the expected results + do_check_true(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider)); + do_check_links(RemoteNewTabUtils.getProviderLinks(provider), expectedLinks); + RemoteNewTabUtils.links.removeProvider(provider); +}); + +add_task(function notifyLinkDelete() { + let expectedLinks = makeLinks(0, 3, 1); + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Remove a link. + let removedLink = expectedLinks[2]; + provider.notifyLinkChanged(removedLink, 2, true); + let links = RemoteNewTabUtils.links._providers.get(provider); + + // Check that sortedLinks is correctly updated. + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks.slice(0, 2)); + + // Check that linkMap is accurately updated. + do_check_eq(links.linkMap.size, 2); + do_check_true(links.linkMap.get(expectedLinks[0].url)); + do_check_true(links.linkMap.get(expectedLinks[1].url)); + do_check_false(links.linkMap.get(removedLink.url)); + + // Check that siteMap is correctly updated. + do_check_eq(links.siteMap.size, 2); + do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[0].url))); + do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[1].url))); + do_check_false(links.siteMap.has(RemoteNewTabUtils.extractSite(removedLink.url))); + + RemoteNewTabUtils.links.removeProvider(provider); +}); + +add_task(function populatePromise() { + let count = 0; + let expectedLinks = makeLinks(0, 10, 2); + + let getLinksFcn = Task.async(function* (callback) { + //Should not be calling getLinksFcn twice + count++; + do_check_eq(count, 1); + yield Promise.resolve(); + callback(expectedLinks); + }); + + let provider = new TestProvider(getLinksFcn); + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + + RemoteNewTabUtils.links.populateProviderCache(provider, () => {}); + RemoteNewTabUtils.links.populateProviderCache(provider, () => { + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + RemoteNewTabUtils.links.removeProvider(provider); + }); +}); + +add_task(function isTopSiteGivenProvider() { + let expectedLinks = makeLinks(0, 10, 2); + + // The lowest 2 frecencies have the same base domain. + expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test"; + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider), false); + + // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3 + let newLink = makeLink(3); + provider.notifyLinkChanged(newLink); + + // There is still a frecent url with example2 domain, so it's still frecent. + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example3.com", provider), true); + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + + // Push out frecency 3 + newLink = makeLink(5); + provider.notifyLinkChanged(newLink); + + // Push out frecency 4 + newLink = makeLink(9); + provider.notifyLinkChanged(newLink); + + // Our count reached 0 for the example2.com domain so it's no longer a frecent site. + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example5.com", provider), true); + do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), false); + + RemoteNewTabUtils.links.removeProvider(provider); +}); + +add_task(function multipleProviders() { + // Make each provider generate RemoteNewTabUtils.links.maxNumLinks links to check + // that no more than maxNumLinks are actually returned in the merged list. + let evenLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks, 2); + let evenProvider = new TestProvider(done => done(evenLinks)); + let oddLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks - 1, 2); + let oddProvider = new TestProvider(done => done(oddLinks)); + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(evenProvider); + RemoteNewTabUtils.links.addProvider(oddProvider); + + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + + let links = RemoteNewTabUtils.links.getLinks(); + let expectedLinks = makeLinks(RemoteNewTabUtils.links.maxNumLinks, + 2 * RemoteNewTabUtils.links.maxNumLinks, + 1); + do_check_eq(links.length, RemoteNewTabUtils.links.maxNumLinks); + do_check_links(links, expectedLinks); + + RemoteNewTabUtils.links.removeProvider(evenProvider); + RemoteNewTabUtils.links.removeProvider(oddProvider); +}); + +add_task(function changeLinks() { + let expectedLinks = makeLinks(0, 20, 2); + let provider = new TestProvider(done => done(expectedLinks)); + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a new link. + let newLink = makeLink(19); + expectedLinks.splice(1, 0, newLink); + provider.notifyLinkChanged(newLink); + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a link that's changed sort criteria. + newLink.frecency = 17; + expectedLinks.splice(1, 1); + expectedLinks.splice(2, 0, newLink); + provider.notifyLinkChanged({ + url: newLink.url, + frecency: 17, + }); + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a link that's changed title. + newLink.title = "My frecency is now 17"; + provider.notifyLinkChanged({ + url: newLink.url, + title: newLink.title, + }); + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a new link again, but this time make it overflow maxNumLinks. + provider.maxNumLinks = expectedLinks.length; + newLink = makeLink(21); + expectedLinks.unshift(newLink); + expectedLinks.pop(); + do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check. + provider.notifyLinkChanged(newLink); + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + // Notify of many links changed. + expectedLinks = makeLinks(0, 3, 1); + provider.notifyManyLinksChanged(); + + // Since _populateProviderCache() is async, we must wait until the provider's + // populate promise has been resolved. + yield RemoteNewTabUtils.links._providers.get(provider).populatePromise; + + // RemoteNewTabUtils.links will now repopulate its cache + do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks); + + RemoteNewTabUtils.links.removeProvider(provider); +}); + +add_task(function oneProviderAlreadyCached() { + let links1 = makeLinks(0, 10, 1); + let provider1 = new TestProvider(done => done(links1)); + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider1); + + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + do_check_links(RemoteNewTabUtils.links.getLinks(), links1); + + let links2 = makeLinks(10, 20, 1); + let provider2 = new TestProvider(done => done(links2)); + RemoteNewTabUtils.links.addProvider(provider2); + + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + do_check_links(RemoteNewTabUtils.links.getLinks(), links2.concat(links1)); + + RemoteNewTabUtils.links.removeProvider(provider1); + RemoteNewTabUtils.links.removeProvider(provider2); +}); + +add_task(function newLowRankedLink() { + // Init a provider with 10 links and make its maximum number also 10. + let links = makeLinks(0, 10, 1); + let provider = new TestProvider(done => done(links)); + provider.maxNumLinks = links.length; + + RemoteNewTabUtils.initWithoutProviders(); + RemoteNewTabUtils.links.addProvider(provider); + + yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve)); + do_check_links(RemoteNewTabUtils.links.getLinks(), links); + + // Notify of a new link that's low-ranked enough not to make the list. + let newLink = makeLink(0); + provider.notifyLinkChanged(newLink); + do_check_links(RemoteNewTabUtils.links.getLinks(), links); + + // Notify about the new link's title change. + provider.notifyLinkChanged({ + url: newLink.url, + title: "a new title", + }); + do_check_links(RemoteNewTabUtils.links.getLinks(), links); + + RemoteNewTabUtils.links.removeProvider(provider); +}); + +add_task(function extractSite() { + // All these should extract to the same site + [ "mozilla.org", + "m.mozilla.org", + "mobile.mozilla.org", + "www.mozilla.org", + "www3.mozilla.org", + ].forEach(host => { + let url = "http://" + host; + do_check_eq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host); + }); + + // All these should extract to the same subdomain + [ "bugzilla.mozilla.org", + "www.bugzilla.mozilla.org", + ].forEach(host => { + let url = "http://" + host; + do_check_eq(RemoteNewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host); + }); + + // All these should not extract to the same site + [ "bugzilla.mozilla.org", + "bug123.bugzilla.mozilla.org", + "too.many.levels.bugzilla.mozilla.org", + "m2.mozilla.org", + "mobile30.mozilla.org", + "ww.mozilla.org", + "ww2.mozilla.org", + "wwwww.mozilla.org", + "wwwww50.mozilla.org", + "wwws.mozilla.org", + "secure.mozilla.org", + "secure10.mozilla.org", + "many.levels.deep.mozilla.org", + "just.check.in", + "192.168.0.1", + "localhost", + ].forEach(host => { + let url = "http://" + host; + do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host); + }); + + // All these should not extract to the same site + [ "about:blank", + "file:///Users/user/file", + "chrome://browser/something", + "ftp://ftp.mozilla.org/", + ].forEach(url => { + do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url); + }); +}); + +function TestProvider(getLinksFn) { + this.getLinks = getLinksFn; + this._observers = new Set(); +} + +TestProvider.prototype = { + addObserver: function (observer) { + this._observers.add(observer); + }, + notifyLinkChanged: function (link, index=-1, deleted=false) { + this._notifyObservers("onLinkChanged", link, index, deleted); + }, + notifyManyLinksChanged: function () { + this._notifyObservers("onManyLinksChanged"); + }, + _notifyObservers: function () { + let observerMethodName = arguments[0]; + let args = Array.prototype.slice.call(arguments, 1); + args.unshift(this); + for (let obs of this._observers) { + if (obs[observerMethodName]) + obs[observerMethodName].apply(RemoteNewTabUtils.links, args); + } + }, +}; + +function do_check_links(actualLinks, expectedLinks) { + do_check_true(Array.isArray(actualLinks)); + do_check_eq(actualLinks.length, expectedLinks.length); + for (let i = 0; i < expectedLinks.length; i++) { + let expected = expectedLinks[i]; + let actual = actualLinks[i]; + do_check_eq(actual.url, expected.url); + do_check_eq(actual.title, expected.title); + do_check_eq(actual.frecency, expected.frecency); + do_check_eq(actual.lastVisitDate, expected.lastVisitDate); + } +} + +function makeLinks(frecRangeStart, frecRangeEnd, step) { + let links = []; + // Remember, links are ordered by frecency descending. + for (let i = frecRangeEnd; i > frecRangeStart; i -= step) { + links.push(makeLink(i)); + } + return links; +} + +function makeLink(frecency) { + return { + url: "http://example" + frecency + ".com/", + title: "My frecency is " + frecency, + frecency: frecency, + lastVisitDate: 0, + }; +} diff --git a/browser/components/newtab/tests/xpcshell/xpcshell.ini b/browser/components/newtab/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000000..b01a2e6fa391 --- /dev/null +++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' || toolkit == 'gonk' + +[test_NewTabURL.js] +[test_RemoteDirectoryLinksProvider.js] +[test_RemoteNewTabLocation.js] +[test_RemoteNewTabUtils.js] diff --git a/browser/components/nsBrowserGlue.js b/browser/components/nsBrowserGlue.js index 755b1d51780d..51b1d5e4c77b 100644 --- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -20,6 +20,21 @@ XPCOMUtils.defineLazyModuleGetter(this, "AboutHome", XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab", "resource:///modules/AboutNewTab.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider", + "resource:///modules/DirectoryLinksProvider.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", + "resource://gre/modules/NewTabUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab", + "resource:///modules/RemoteAboutNewTab.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "RemoteDirectoryLinksProvider", + "resource:///modules/RemoteDirectoryLinksProvider.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils", + "resource:///modules/RemoteNewTabUtils.jsm"); + XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm"); @@ -29,9 +44,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", XPCOMUtils.defineLazyModuleGetter(this, "ContentClick", "resource:///modules/ContentClick.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider", - "resource:///modules/DirectoryLinksProvider.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); @@ -53,9 +65,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", "resource://gre/modules/PageThumbs.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", - "resource://gre/modules/NewTabUtils.jsm"); - XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader", "resource:///modules/CustomizationTabPreloader.jsm"); @@ -591,7 +600,7 @@ BrowserGlue.prototype = { } }, - // initialization (called on application startup) + // initialization (called on application startup) _init: function BG__init() { let os = Services.obs; os.addObserver(this, "notifications-open-settings", false); @@ -811,7 +820,7 @@ BrowserGlue.prototype = { this._sanitizer.onStartup(); // check if we're in safe mode if (Services.appinfo.inSafeMode) { - Services.ww.openWindow(null, "chrome://browser/content/safeMode.xul", + Services.ww.openWindow(null, "chrome://browser/content/safeMode.xul", "_blank", "chrome,centerscreen,modal,resizable=no", null); } @@ -826,9 +835,6 @@ BrowserGlue.prototype = { WebappManager.init(); PageThumbs.init(); - NewTabUtils.init(); - DirectoryLinksProvider.init(); - NewTabUtils.links.addProvider(DirectoryLinksProvider); #ifdef NIGHTLY_BUILD if (Services.prefs.getBoolPref("dom.identity.enabled")) { SignInToWebsiteUX.init(); @@ -836,7 +842,17 @@ BrowserGlue.prototype = { #endif webrtcUI.init(); AboutHome.init(); + + RemoteDirectoryLinksProvider.init(); + RemoteNewTabUtils.init(); + RemoteNewTabUtils.links.addProvider(RemoteDirectoryLinksProvider); + RemoteAboutNewTab.init(); + + DirectoryLinksProvider.init(); + NewTabUtils.init(); + NewTabUtils.links.addProvider(DirectoryLinksProvider); AboutNewTab.init(); + SessionStore.init(); BrowserUITelemetry.init(); ContentSearch.init(); @@ -1161,6 +1177,8 @@ BrowserGlue.prototype = { CustomizationTabPreloader.uninit(); WebappManager.uninit(); + + RemoteAboutNewTab.uninit(); AboutNewTab.uninit(); #ifdef NIGHTLY_BUILD if (Services.prefs.getBoolPref("dom.identity.enabled")) { @@ -2043,7 +2061,7 @@ BrowserGlue.prototype = { } if (currentUIVersion < 19) { - let detector = null; + let detector = null; try { detector = Services.prefs.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString).data; @@ -2205,7 +2223,7 @@ BrowserGlue.prototype = { Services.prefs.clearUserPref("browser.devedition.showCustomizeButton"); } - + if (currentUIVersion < 31) { xulStore.removeValue(BROWSER_DOCURL, "bookmarks-menu-button", "class"); xulStore.removeValue(BROWSER_DOCURL, "home-button", "class"); diff --git a/browser/modules/moz.build b/browser/modules/moz.build index f7fa398ba9ad..4f6038e0cb67 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -30,7 +30,6 @@ EXTRA_JS_MODULES += [ 'FormValidationHandler.jsm', 'HiddenFrame.jsm', 'NetworkPrioritizer.jsm', - 'NewTabURL.jsm', 'offlineAppCache.jsm', 'PanelFrame.jsm', 'PluginContent.jsm', diff --git a/browser/modules/test/xpcshell/xpcshell.ini b/browser/modules/test/xpcshell/xpcshell.ini index 820f643bfd47..939f3d6d2c38 100644 --- a/browser/modules/test/xpcshell/xpcshell.ini +++ b/browser/modules/test/xpcshell/xpcshell.ini @@ -5,5 +5,4 @@ firefox-appdir = browser skip-if = toolkit == 'android' || toolkit == 'gonk' [test_DirectoryLinksProvider.js] -[test_NewTabURL.js] [test_SitePermissions.js] From 5634b62e5dac50ae9915b12e0ef194a8257221d1 Mon Sep 17 00:00:00 2001 From: Olivier Yiptong Date: Fri, 9 Oct 2015 16:39:43 -0400 Subject: [PATCH 033/121] Bug 1210936 - Remote New Tab Content Frame and AboutRedirector location r=mconley --HG-- extra : commitid : 5I9gHJn4LxX --- browser/app/profile/firefox.js | 3 + browser/base/content/browser.js | 7 +- browser/base/content/remote-newtab/newTab.css | 23 ++++ browser/base/content/remote-newtab/newTab.js | 126 ++++++++++++++++++ .../base/content/remote-newtab/newTab.xhtml | 24 ++++ browser/base/jar.mn | 3 + browser/components/about/AboutRedirector.cpp | 3 + browser/components/build/nsModule.cpp | 1 + 8 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 browser/base/content/remote-newtab/newTab.css create mode 100644 browser/base/content/remote-newtab/newTab.js create mode 100644 browser/base/content/remote-newtab/newTab.xhtml diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 072fe22937c9..afb762c310fb 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1657,6 +1657,9 @@ pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/ // endpoint to send newtab click and view pings pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v3/links/"); +// activates the remote-hosted newtab page +pref("browser.newtabpage.remote", false); + // Enable the DOM fullscreen API. pref("full-screen-api.enabled", true); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 664d6cfbd74f..22e3cea3ffec 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -34,6 +34,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils", + "resource:///modules/RemoteNewTabUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch", "resource:///modules/ContentSearch.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AboutHome", @@ -3518,8 +3520,11 @@ const BrowserSearch = { if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) { let url = gBrowser.currentURI.spec.toLowerCase(); let mm = gBrowser.selectedBrowser.messageManager; + let newTabRemoted = Services.prefs.getBoolPref("browser.newtabpage.remote"); if (url === "about:home" || - (url === "about:newtab" && NewTabUtils.allPages.enabled)) { + (url === "about:newtab" && + ((!newTabRemoted && NewTabUtils.allPages.enabled) || + (newTabRemoted && RemoteNewTabUtils.allPages.enabled)))) { ContentSearch.focusInput(mm); } else { openUILinkIn("about:home", "current"); diff --git a/browser/base/content/remote-newtab/newTab.css b/browser/base/content/remote-newtab/newTab.css new file mode 100644 index 000000000000..303cafddc5e2 --- /dev/null +++ b/browser/base/content/remote-newtab/newTab.css @@ -0,0 +1,23 @@ +/* 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/. */ + +html { + width: 100%; + height: 100%; +} + +body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + position: relative; +} + +#remotedoc { + width: 100%; + height: 100%; + border: none; + position: absolute; +} diff --git a/browser/base/content/remote-newtab/newTab.js b/browser/base/content/remote-newtab/newTab.js new file mode 100644 index 000000000000..920830c96767 --- /dev/null +++ b/browser/base/content/remote-newtab/newTab.js @@ -0,0 +1,126 @@ +/* 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/. */ +/*globals XPCOMUtils, Components, sendAsyncMessage, addMessageListener, removeMessageListener, + Services, PrivateBrowsingUtils*/ +"use strict"; + +const {utils: Cu, interfaces: Ci} = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +(function() { + let remoteNewTabLocation; + let remoteIFrame; + + /** + * Attempts to handle commands sent from the remote IFrame within this content frame. + * Expected commands below, with data types explained. + * + * @returns {Boolean} whether or not the command was handled + * @param {String} command + * The command passed from the remote IFrame + * @param {Object} data + * Parameters to the command + */ + function handleCommand(command, data) { + let commandHandled = true; + switch (command) { + case "NewTab:UpdateTelemetryProbe": + /** + * Update a given Telemetry histogram + * + * @param {String} data.probe + * Probe name to update + * @param {Number} data.value + * Value to update histogram by + */ + Services.telemetry.getHistogramById(data.probe).add(data.value); + break; + case "NewTab:Register": + registerEvent(data.type); + break; + case "NewTab:GetInitialState": + getInitialState(); + break; + default: + commandHandled = false; + } + return commandHandled; + } + + function initRemotePage(initData) { + // Messages that the iframe sends the browser will be passed onto + // the privileged parent process + remoteNewTabLocation = initData; + remoteIFrame = document.querySelector("#remotedoc"); + + let loadHandler = () => { + if (remoteIFrame.src !== remoteNewTabLocation.href) { + return; + } + + remoteIFrame.removeEventListener("load", loadHandler); + + remoteIFrame.contentDocument.addEventListener("NewTabCommand", (e) => { + // If the commands are not handled within this content frame, the command will be + // passed on to main process, in RemoteAboutNewTab.jsm + let handled = handleCommand(e.detail.command, e.detail.data); + if (!handled) { + sendAsyncMessage(e.detail.command, e.detail.data); + } + }); + registerEvent("NewTab:Observe"); + let ev = new CustomEvent("NewTabCommandReady"); + remoteIFrame.contentDocument.dispatchEvent(ev); + }; + + remoteIFrame.src = remoteNewTabLocation.href; + remoteIFrame.addEventListener("load", loadHandler); + } + + /** + * Allows the content IFrame to register a listener to an event sent by + * the privileged parent process, in RemoteAboutNewTab.jsm + * + * @param {String} eventName + * Event name to listen to + */ + function registerEvent(eventName) { + addMessageListener(eventName, (message) => { + remoteIFrame.contentWindow.postMessage(message, remoteNewTabLocation.origin); + }); + } + + /** + * Sends the initial data payload to a content IFrame so it can bootstrap + */ + function getInitialState() { + let prefs = Services.prefs; + let isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window); + let state = { + enabled: prefs.getBoolPref("browser.newtabpage.enabled"), + enhanced: prefs.getBoolPref("browser.newtabpage.enhanced"), + rows: prefs.getIntPref("browser.newtabpage.rows"), + columns: prefs.getIntPref("browser.newtabpage.columns"), + introShown: prefs.getBoolPref("browser.newtabpage.introShown"), + windowID: window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID, + privateBrowsingMode: isPrivate + }; + remoteIFrame.contentWindow.postMessage({ + name: "NewTab:State", + data: state + }, remoteNewTabLocation.origin); + } + + addMessageListener("NewTabFrame:Init", function loadHandler(message) { + // Everything is loaded. Initialize the New Tab Page. + removeMessageListener("NewTabFrame:Init", loadHandler); + initRemotePage(message.data); + }); + sendAsyncMessage("NewTabFrame:GetInit"); +}()); diff --git a/browser/base/content/remote-newtab/newTab.xhtml b/browser/base/content/remote-newtab/newTab.xhtml new file mode 100644 index 000000000000..1ae754eedbfd --- /dev/null +++ b/browser/base/content/remote-newtab/newTab.xhtml @@ -0,0 +1,24 @@ + + + + + + %newTabDTD; +]> + + + + &newtab.pageTitle; + + + +