diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index 46b89edb875a..c649ce0a6de8 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -877,8 +877,8 @@ pref("memory.system_memory_reporter", true); // Don't dump memory reports on OOM, by default. pref("memory.dump_reports_on_oom", false); -pref("layout.imagevisibility.numscrollportwidths", 1); -pref("layout.imagevisibility.numscrollportheights", 1); +pref("layout.framevisibility.numscrollportwidths", 1); +pref("layout.framevisibility.numscrollportheights", 1); // Enable native identity (persona/browserid) pref("dom.identity.enabled", true); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 2e3d8f616f27..1efedf0b30fa 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -3353,7 +3353,7 @@ const DOMLinkHandler = { break; case "Link:SetIcon": - return this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal); + this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal); break; case "Link:AddSearch": diff --git a/browser/base/content/test/general/browser_bug408415.js b/browser/base/content/test/general/browser_bug408415.js index 8a34374ac939..d8f80f8be42f 100644 --- a/browser/base/content/test/general/browser_bug408415.js +++ b/browser/base/content/test/general/browser_bug408415.js @@ -1,21 +1,45 @@ -function test() { - waitForExplicitFinish(); - +add_task(function* test() { let testPath = getRootDirectory(gTestPath); - let tab = gBrowser.addTab(testPath + "file_with_favicon.html"); + yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, + function* (tabBrowser) { + const URI = testPath + "file_with_favicon.html"; + const expectedIcon = testPath + "file_generic_favicon.ico"; - tab.linkedBrowser.addEventListener("DOMContentLoaded", function() { - tab.linkedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, true); + let got_favicon = Promise.defer(); + let listener = { + onLinkIconAvailable(browser, iconURI) { + if (got_favicon && iconURI && browser === tabBrowser) { + got_favicon.resolve(iconURI); + got_favicon = null; + } + } + }; + gBrowser.addTabsProgressListener(listener); - let expectedIcon = testPath + "file_generic_favicon.ico"; + BrowserTestUtils.loadURI(tabBrowser, URI); - is(gBrowser.getIcon(tab), expectedIcon, "Correct icon before hash change."); - tab.linkedBrowser.contentWindow.location.href += "#foo"; - is(gBrowser.getIcon(tab), expectedIcon, "Correct icon after hash change."); + let iconURI = yield got_favicon.promise; + is(iconURI, expectedIcon, "Correct icon before pushState."); - gBrowser.removeTab(tab); + got_favicon = Promise.defer(); + got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e); + yield ContentTask.spawn(tabBrowser, null, function() { + content.location.href += "#foo"; + }); + + // We've navigated and shouldn't get a call to onLinkIconAvailable. + TestUtils.executeSoon(() => { + got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser))); + }); + try { + yield got_favicon.promise; + } catch (e) { + iconURI = e; + } + is(iconURI, expectedIcon, "Correct icon after pushState."); + + gBrowser.removeTabsProgressListener(listener); + }); +}); - finish(); - }, true); -} diff --git a/browser/base/content/test/general/browser_bug550565.js b/browser/base/content/test/general/browser_bug550565.js index 84f7b803b2b9..b0e094e079d2 100644 --- a/browser/base/content/test/general/browser_bug550565.js +++ b/browser/base/content/test/general/browser_bug550565.js @@ -1,21 +1,44 @@ -function test() { - waitForExplicitFinish(); - +add_task(function* test() { let testPath = getRootDirectory(gTestPath); - let tab = gBrowser.addTab(testPath + "file_with_favicon.html"); + yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, + function* (tabBrowser) { + const URI = testPath + "file_with_favicon.html"; + const expectedIcon = testPath + "file_generic_favicon.ico"; - tab.linkedBrowser.addEventListener("DOMContentLoaded", function() { - tab.linkedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, true); + let got_favicon = Promise.defer(); + let listener = { + onLinkIconAvailable(browser, iconURI) { + if (got_favicon && iconURI && browser === tabBrowser) { + got_favicon.resolve(iconURI); + got_favicon = null; + } + } + }; + gBrowser.addTabsProgressListener(listener); - let expectedIcon = testPath + "file_generic_favicon.ico"; + BrowserTestUtils.loadURI(tabBrowser, URI); - is(gBrowser.getIcon(tab), expectedIcon, "Correct icon before pushState."); - tab.linkedBrowser.contentWindow.history.pushState("page2", "page2", "page2"); - is(gBrowser.getIcon(tab), expectedIcon, "Correct icon after pushState."); + let iconURI = yield got_favicon.promise; + is(iconURI, expectedIcon, "Correct icon before pushState."); - gBrowser.removeTab(tab); + got_favicon = Promise.defer(); + got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e); + yield ContentTask.spawn(tabBrowser, null, function() { + content.history.pushState("page2", "page2", "page2"); + }); - finish(); - }, true); -} + // We've navigated and shouldn't get a call to onLinkIconAvailable. + TestUtils.executeSoon(() => { + got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser))); + }); + try { + yield got_favicon.promise; + } catch (e) { + iconURI = e; + } + is(iconURI, expectedIcon, "Correct icon after pushState."); + + gBrowser.removeTabsProgressListener(listener); + }); +}); diff --git a/browser/components/preferences/in-content/main.xul b/browser/components/preferences/in-content/main.xul index f51e3f825af9..116961e8297a 100644 --- a/browser/components/preferences/in-content/main.xul +++ b/browser/components/preferences/in-content/main.xul @@ -282,10 +282,6 @@ accesskey="&warnOpenManyTabs.accesskey;" preference="browser.tabs.warnOnOpen"/> - - diff --git a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd index 8480d9cac9a1..328c219ecc9b 100644 --- a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd +++ b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd @@ -11,9 +11,6 @@ - - - diff --git a/browser/modules/ContentLinkHandler.jsm b/browser/modules/ContentLinkHandler.jsm index 3d9cc461aca2..9d136ccfbb2b 100644 --- a/browser/modules/ContentLinkHandler.jsm +++ b/browser/modules/ContentLinkHandler.jsm @@ -71,47 +71,46 @@ this.ContentLinkHandler = { } break; case "icon": - if (!iconAdded) { - if (!Services.prefs.getBoolPref("browser.chrome.site_icons")) - break; + if (iconAdded || !Services.prefs.getBoolPref("browser.chrome.site_icons")) + break; - var uri = this.getLinkIconURI(link); - if (!uri) - break; + var uri = this.getLinkIconURI(link); + if (!uri) + break; - // Telemetry probes for measuring the sizes attribute - // usage and available dimensions. - let sizeHistogramTypes = Services.telemetry. - getHistogramById("LINK_ICON_SIZES_ATTR_USAGE"); - let sizeHistogramDimension = Services.telemetry. - getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION"); - let sizesType; - if (link.sizes.length) { - for (let size of link.sizes) { - if (size.toLowerCase() == "any") { - sizesType = SIZES_TELEMETRY_ENUM.ANY; - break; + // Telemetry probes for measuring the sizes attribute + // usage and available dimensions. + let sizeHistogramTypes = Services.telemetry. + getHistogramById("LINK_ICON_SIZES_ATTR_USAGE"); + let sizeHistogramDimension = Services.telemetry. + getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION"); + let sizesType; + if (link.sizes.length) { + for (let size of link.sizes) { + if (size.toLowerCase() == "any") { + sizesType = SIZES_TELEMETRY_ENUM.ANY; + break; + } else { + let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i; + let values = re.exec(size); + if (values && values.length > 1) { + sizesType = SIZES_TELEMETRY_ENUM.DIMENSION; + sizeHistogramDimension.add(parseInt(values[1])); } else { - let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i; - let values = re.exec(size); - if (values && values.length > 1) { - sizesType = SIZES_TELEMETRY_ENUM.DIMENSION; - sizeHistogramDimension.add(parseInt(values[1])); - } else { - sizesType = SIZES_TELEMETRY_ENUM.INVALID; - break; - } + sizesType = SIZES_TELEMETRY_ENUM.INVALID; + break; } } - } else { - sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES; } - sizeHistogramTypes.add(sizesType); - - [iconAdded] = chromeGlobal.sendSyncMessage( - "Link:SetIcon", - {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal}); + } else { + sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES; } + sizeHistogramTypes.add(sizesType); + + chromeGlobal.sendAsyncMessage( + "Link:SetIcon", + {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal}); + iconAdded = true; break; case "search": if (!searchAdded && event.type == "DOMLinkAdded") { diff --git a/build/moz.configure/checks.configure b/build/moz.configure/checks.configure index c1db155ed8fa..d42d0e1504ee 100644 --- a/build/moz.configure/checks.configure +++ b/build/moz.configure/checks.configure @@ -21,8 +21,10 @@ # @checking('for something') # def check(value): # ... +# An optional callback can be given, that will be used to format the returned +# value when displaying it. @template -def checking(what): +def checking(what, callback=None): def decorator(func): @advanced def wrapped(*args, **kwargs): @@ -30,7 +32,9 @@ def checking(what): print('checking', what, end='... ') sys.stdout.flush() ret = func(*args, **kwargs) - if ret is True: + if callback: + print(callback(ret)) + elif ret is True: print('yes') elif ret is False: print('no') @@ -51,13 +55,12 @@ def checking(what): def check_prog(var, progs, allow_missing=False): option(env=var, nargs=1, help='Path to the %s program' % var.lower()) - not_found = 'not found' if not (isinstance(progs, tuple) or isinstance(progs, list)): configure_error('progs should be a list or tuple!') progs = list(progs) @depends(var) - @checking('for %s' % var.lower()) + @checking('for %s' % var.lower(), lambda x: x or 'not found') def check(value): if value: progs[:] = value @@ -65,21 +68,19 @@ def check_prog(var, progs, allow_missing=False): result = find_program(prog) if result: return result - return not_found @depends(check, var) @advanced def postcheck(value, raw_value): - if value is not_found and (not allow_missing or raw_value): + if value is None and (not allow_missing or raw_value): from mozbuild.shellutil import quote error('Cannot find %s (tried: %s)' % (var.lower(), ', '.join(quote(p) for p in progs))) - return None if value is not_found else value - @depends(postcheck) + @depends(check) def normalized_for_config(value): return ':' if value is None else value set_config(var, normalized_for_config) - return postcheck + return check diff --git a/build/moz.configure/init.configure b/build/moz.configure/init.configure index cfb731a56471..e5d1acaa366d 100644 --- a/build/moz.configure/init.configure +++ b/build/moz.configure/init.configure @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. include('util.configure') +include('checks.configure') option(env='DIST', nargs=1, help='DIST directory') @@ -464,6 +465,7 @@ def config_sub(shell, triplet): @depends('--host', shell) +@checking('for host system type', lambda h: h.alias) @advanced def host(value, shell): if not value: @@ -478,6 +480,7 @@ def host(value, shell): @depends('--target', host, shell) +@checking('for target system type', lambda t: t.alias) def target(value, host, shell): if not value: return host diff --git a/dom/base/nsIImageLoadingContent.idl b/dom/base/nsIImageLoadingContent.idl index 0a469c165e1d..fea261a34c6e 100644 --- a/dom/base/nsIImageLoadingContent.idl +++ b/dom/base/nsIImageLoadingContent.idl @@ -5,6 +5,11 @@ #include "imgINotificationObserver.idl" +%{C++ +#include "mozilla/Maybe.h" +#include "Visibility.h" +%} + interface imgIRequest; interface nsIChannel; interface nsIStreamListener; @@ -12,6 +17,9 @@ interface nsIURI; interface nsIDocument; interface nsIFrame; +[ref] native MaybeOnNonvisible(const mozilla::Maybe); +native Visibility(mozilla::Visibility); + /** * This interface represents a content node that loads images. The interface * exists to allow getting information on the images that the content node @@ -32,7 +40,7 @@ interface nsIFrame; * interface to mirror this interface when changing it. */ -[scriptable, builtinclass, uuid(770f7d84-c917-42d7-bf8d-d1b70649e733)] +[scriptable, builtinclass, uuid(0357123d-9224-4d12-a47e-868c32689777)] interface nsIImageLoadingContent : imgINotificationObserver { /** @@ -169,18 +177,17 @@ interface nsIImageLoadingContent : imgINotificationObserver readonly attribute unsigned long naturalHeight; /** - * A visible count is stored, if it is non-zero then this image is considered - * visible. These methods increment, decrement, or return the visible count. + * Called by layout to announce when the frame associated with this content + * has changed its visibility state. * - * @param aNonvisibleAction What to do if the image's visibility count is now - * zero. If ON_NONVISIBLE_NO_ACTION, nothing will be - * done. If ON_NONVISIBLE_REQUEST_DISCARD, the image - * will be asked to discard its surfaces if possible. + * @param aNewVisibility The new visibility state. + * @param aNonvisibleAction A requested action if the frame has become + * nonvisible. If Nothing(), no action is + * requested. If DISCARD_IMAGES is specified, the + * frame is requested to ask any images it's + * associated with to discard their surfaces if + * possible. */ - [noscript, notxpcom] void IncrementVisibleCount(); - [noscript, notxpcom] void DecrementVisibleCount(in uint32_t aNonvisibleAction); - [noscript, notxpcom] uint32_t GetVisibleCount(); - - const long ON_NONVISIBLE_NO_ACTION = 0; - const long ON_NONVISIBLE_REQUEST_DISCARD = 1; + [noscript, notxpcom] void onVisibilityChange(in Visibility aNewVisibility, + in MaybeOnNonvisible aNonvisibleAction); }; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index 76342eb619dc..dd9ffc0a06fd 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -726,24 +726,24 @@ nsINode::LookupPrefix(const nsAString& aNamespaceURI, nsAString& aPrefix) Element *element = GetNameSpaceElement(); if (element) { // XXX Waiting for DOM spec to list error codes. - + // Trace up the content parent chain looking for the namespace // declaration that defines the aNamespaceURI namespace. Once found, // return the prefix (i.e. the attribute localName). for (nsIContent* content = element; content; content = content->GetParent()) { uint32_t attrCount = content->GetAttrCount(); - + for (uint32_t i = 0; i < attrCount; ++i) { const nsAttrName* name = content->GetAttrNameAt(i); - + if (name->NamespaceEquals(kNameSpaceID_XMLNS) && content->AttrValueIs(kNameSpaceID_XMLNS, name->LocalName(), aNamespaceURI, eCaseMatters)) { // If the localName is "xmlns", the prefix we output should be // null. nsIAtom *localName = name->LocalName(); - + if (localName != nsGkAtoms::xmlns) { localName->ToString(aPrefix); } @@ -972,7 +972,13 @@ nsINode::CompareDocumentPosition(nsINode& aOtherNode) const (nsIDOMNode::DOCUMENT_POSITION_PRECEDING | nsIDOMNode::DOCUMENT_POSITION_CONTAINS) : (nsIDOMNode::DOCUMENT_POSITION_FOLLOWING | - nsIDOMNode::DOCUMENT_POSITION_CONTAINED_BY); + nsIDOMNode::DOCUMENT_POSITION_CONTAINED_BY); +} + +bool +nsINode::IsSameNode(nsINode *other) +{ + return other == this; } bool @@ -1020,7 +1026,7 @@ nsINode::IsEqualNode(nsINode* aOther) element1->GetAttr(attrName->NamespaceID(), attrName->LocalName(), string1); NS_ASSERTION(hasAttr, "Why don't we have an attr?"); - + if (!element2->AttrValueIs(attrName->NamespaceID(), attrName->LocalName(), string1, @@ -1056,7 +1062,7 @@ nsINode::IsEqualNode(nsINode* aOther) "subtree?"); node1->GetNodeValue(string1); node2->GetNodeValue(string2); - + // Returning here as to not bother walking subtree. And there is no // risk that we're half way through walking some other subtree since // attribute nodes doesn't appear in subtrees. @@ -1066,7 +1072,7 @@ nsINode::IsEqualNode(nsINode* aOther) { nsCOMPtr docType1 = do_QueryInterface(node1); nsCOMPtr docType2 = do_QueryInterface(node2); - + NS_ASSERTION(docType1 && docType2, "Why don't we have a document type node?"); // Public ID @@ -1075,7 +1081,7 @@ nsINode::IsEqualNode(nsINode* aOther) if (!string1.Equals(string2)) { return false; } - + // System ID docType1->GetSystemId(string1); docType2->GetSystemId(string2); @@ -1119,7 +1125,7 @@ nsINode::IsEqualNode(nsINode* aOther) // node2 has a nextSibling, but node1 doesn't return false; } - + node1 = node1->GetParentNode(); node2 = node2->GetParentNode(); NS_ASSERTION(node1 && node2, "no parent while walking subtree"); @@ -1795,7 +1801,7 @@ bool IsAllowedAsChild(nsIContent* aNewChild, nsINode* aParent, // Now we're OK in the following two cases only: // 1) We're replacing something that's not before the doctype - // 2) We're inserting before something that comes after the doctype + // 2) We're inserting before something that comes after the doctype return aIsReplace ? (insertIndex >= doctypeIndex) : insertIndex > doctypeIndex; } @@ -2036,7 +2042,7 @@ nsINode::ReplaceOrInsertBefore(bool aReplace, nsINode* aNewChild, // We expect one mutation (the removal) to have happened. if (guard.Mutated(1)) { // XBL destructors, yuck. - + // Verify that nodeToInsertBefore, if non-null, is still our child. If // it's not, there's no way we can do this insert sanely; just bail out. if (nodeToInsertBefore && nodeToInsertBefore->GetParent() != this) { @@ -2112,7 +2118,7 @@ nsINode::ReplaceOrInsertBefore(bool aReplace, nsINode* aNewChild, // We expect |count| removals if (guard.Mutated(count)) { // XBL destructors, yuck. - + // Verify that nodeToInsertBefore, if non-null, is still our child. If // it's not, there's no way we can do this insert sanely; just bail out. if (nodeToInsertBefore && nodeToInsertBefore->GetParent() != this) { @@ -2144,7 +2150,7 @@ nsINode::ReplaceOrInsertBefore(bool aReplace, nsINode* aNewChild, nodeToInsertBefore = aRefChild->GetNextSibling(); } else { nodeToInsertBefore = aRefChild; - } + } // And verify that newContent is still allowed as our child. Sadly, we // need to reimplement the relevant part of IsAllowedAsChild() because diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h index ab516ae6b158..975a849a2c49 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h @@ -725,7 +725,7 @@ public: { return InsertChildAt(aKid, GetChildCount(), aNotify); } - + /** * Remove a child from this node. This method handles calling UnbindFromTree * on the child appropriately. @@ -888,7 +888,7 @@ public: virtual void* UnsetProperty(uint16_t aCategory, nsIAtom *aPropertyName, nsresult *aStatus = nullptr); - + bool HasProperties() const { return HasFlag(NODE_HAS_PROPERTIES); @@ -920,7 +920,7 @@ public: { return mParent; } - + /** * Get the parent nsINode for this node if it is an Element. * @return the parent node @@ -1255,7 +1255,7 @@ protected: } return nullptr; } - + public: void GetTextContent(nsAString& aTextContent, mozilla::ErrorResult& aError) @@ -1310,7 +1310,7 @@ public: nsresult GetUserData(const nsAString& aKey, nsIVariant** aResult) { NS_IF_ADDREF(*aResult = GetUserData(aKey)); - + return NS_OK; } @@ -1471,7 +1471,7 @@ private: NodeIsCCMarkedRoot, // Maybe set if this node is in black subtree. NodeIsCCBlackTree, - // Maybe set if the node is a root of a subtree + // Maybe set if the node is a root of a subtree // which needs to be kept in the purple buffer. NodeIsPurpleRoot, // Set if the node has an explicit base URI stored @@ -1773,6 +1773,7 @@ public: } nsINode* RemoveChild(nsINode& aChild, mozilla::ErrorResult& aError); already_AddRefed CloneNode(bool aDeep, mozilla::ErrorResult& aError); + bool IsSameNode(nsINode* aNode); bool IsEqualNode(nsINode* aNode); void GetNamespaceURI(nsAString& aNamespaceURI) const { diff --git a/dom/base/nsImageLoadingContent.cpp b/dom/base/nsImageLoadingContent.cpp index e7336207d2fa..bc7b2ae5e97b 100644 --- a/dom/base/nsImageLoadingContent.cpp +++ b/dom/base/nsImageLoadingContent.cpp @@ -94,8 +94,7 @@ nsImageLoadingContent::nsImageLoadingContent() mStateChangerDepth(0), mCurrentRequestRegistered(false), mPendingRequestRegistered(false), - mFrameCreateCalled(false), - mVisibleCount(0) + mFrameCreateCalled(false) { if (!nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) { mLoadingEnabled = false; @@ -110,8 +109,8 @@ nsImageLoadingContent::DestroyImageLoadingContent() { // Cancel our requests so they won't hold stale refs to us // NB: Don't ask to discard the images here. - ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_NO_ACTION); - ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_NO_ACTION); + ClearCurrentRequest(NS_BINDING_ABORTED); + ClearPendingRequest(NS_BINDING_ABORTED); } nsImageLoadingContent::~nsImageLoadingContent() @@ -264,12 +263,7 @@ ImageIsAnimated(imgIRequest* aRequest) void nsImageLoadingContent::OnUnlockedDraw() { - if (mVisibleCount > 0) { - // We should already be marked as visible, there is nothing more we can do. - return; - } - - // It's OK for non-animated images to wait until the next image visibility + // It's OK for non-animated images to wait until the next frame visibility // update to become locked. (And that's preferable, since in the case of // scrolling it keeps memory usage minimal.) For animated images, though, we // want to mark them visible right away so we can call @@ -278,15 +272,27 @@ nsImageLoadingContent::OnUnlockedDraw() return; } - nsPresContext* presContext = GetFramePresContext(); - if (!presContext) + nsIFrame* frame = GetOurPrimaryFrame(); + if (!frame) { return; + } + + if (frame->GetVisibility() == Visibility::APPROXIMATELY_VISIBLE) { + // This frame is already marked visible; there's nothing to do. + return; + } + + nsPresContext* presContext = frame->PresContext(); + if (!presContext) { + return; + } nsIPresShell* presShell = presContext->PresShell(); - if (!presShell) + if (!presShell) { return; + } - presShell->EnsureImageInVisibleList(this); + presShell->EnsureFrameInApproximatelyVisibleList(frame); } nsresult @@ -470,11 +476,6 @@ nsImageLoadingContent::FrameCreated(nsIFrame* aFrame) mFrameCreateCalled = true; - if (aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP)) { - // Assume all images in popups are visible. - IncrementVisibleCount(); - } - TrackImage(mCurrentRequest); TrackImage(mPendingRequest); @@ -518,13 +519,7 @@ nsImageLoadingContent::FrameDestroyed(nsIFrame* aFrame) nsIPresShell* presShell = presContext ? presContext->GetPresShell() : nullptr; if (presShell) { - presShell->RemoveImageFromVisibleList(this); - } - - if (aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP)) { - // We assume all images in popups are visible, so this decrement balances - // out the increment in FrameCreated above. - DecrementVisibleCount(ON_NONVISIBLE_NO_ACTION); + presShell->RemoveFrameFromApproximatelyVisibleList(aFrame); } } @@ -738,34 +733,6 @@ nsImageLoadingContent::UnblockOnload(imgIRequest* aRequest) return NS_OK; } -void -nsImageLoadingContent::IncrementVisibleCount() -{ - mVisibleCount++; - if (mVisibleCount == 1) { - TrackImage(mCurrentRequest); - TrackImage(mPendingRequest); - } -} - -void -nsImageLoadingContent::DecrementVisibleCount(uint32_t aNonvisibleAction) -{ - NS_ASSERTION(mVisibleCount > 0, "visible count should be positive here"); - mVisibleCount--; - - if (mVisibleCount == 0) { - UntrackImage(mCurrentRequest, aNonvisibleAction); - UntrackImage(mPendingRequest, aNonvisibleAction); - } -} - -uint32_t -nsImageLoadingContent::GetVisibleCount() -{ - return mVisibleCount; -} - /* * Non-interface methods */ @@ -1076,8 +1043,8 @@ void nsImageLoadingContent::CancelImageRequests(bool aNotify) { AutoStateChanger changer(this, aNotify); - ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD); - ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD); + ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); + ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); } nsresult @@ -1089,8 +1056,8 @@ nsImageLoadingContent::UseAsPrimaryRequest(imgRequestProxy* aRequest, AutoStateChanger changer(this, aNotify); // Get rid if our existing images - ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD); - ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD); + ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); + ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES)); // Clone the request we were given. RefPtr& req = PrepareNextRequest(aImageLoadType); @@ -1228,7 +1195,9 @@ nsImageLoadingContent::SetBlockedRequest(nsIURI* aURI, int16_t aContentDecision) // reason "image source changed". However, apparently there's some abuse // over in nsImageFrame where the displaying of the "broken" icon for the // next image depends on the cancel reason of the previous image. ugh. - ClearPendingRequest(NS_ERROR_IMAGE_BLOCKED, ON_NONVISIBLE_REQUEST_DISCARD); + // XXX(seth): So shouldn't we fix nsImageFrame?! + ClearPendingRequest(NS_ERROR_IMAGE_BLOCKED, + Some(OnNonvisible::DISCARD_IMAGES)); // For the blocked case, we only want to cancel the existing current request // if size is not available. bz says the web depends on this behavior. @@ -1236,7 +1205,8 @@ nsImageLoadingContent::SetBlockedRequest(nsIURI* aURI, int16_t aContentDecision) mImageBlockingStatus = aContentDecision; uint32_t keepFlags = mCurrentRequestFlags & REQUEST_IS_IMAGESET; - ClearCurrentRequest(NS_ERROR_IMAGE_BLOCKED, ON_NONVISIBLE_REQUEST_DISCARD); + ClearCurrentRequest(NS_ERROR_IMAGE_BLOCKED, + Some(OnNonvisible::DISCARD_IMAGES)); // We still want to remember what URI we were and if it was an imageset, // despite not having an actual request. These are both cleared as part of @@ -1255,7 +1225,7 @@ nsImageLoadingContent::PrepareCurrentRequest(ImageLoadType aImageLoadType) // Get rid of anything that was there previously. ClearCurrentRequest(NS_ERROR_IMAGE_SRC_CHANGED, - ON_NONVISIBLE_REQUEST_DISCARD); + Some(OnNonvisible::DISCARD_IMAGES)); if (mNewRequestsWillNeedAnimationReset) { mCurrentRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET; @@ -1274,7 +1244,7 @@ nsImageLoadingContent::PreparePendingRequest(ImageLoadType aImageLoadType) { // Get rid of anything that was there previously. ClearPendingRequest(NS_ERROR_IMAGE_SRC_CHANGED, - ON_NONVISIBLE_REQUEST_DISCARD); + Some(OnNonvisible::DISCARD_IMAGES)); if (mNewRequestsWillNeedAnimationReset) { mPendingRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET; @@ -1339,7 +1309,7 @@ nsImageLoadingContent::MakePendingRequestCurrent() void nsImageLoadingContent::ClearCurrentRequest(nsresult aReason, - uint32_t aNonvisibleAction) + const Maybe& aNonvisibleAction) { if (!mCurrentRequest) { // Even if we didn't have a current request, we might have been keeping @@ -1365,7 +1335,7 @@ nsImageLoadingContent::ClearCurrentRequest(nsresult aReason, void nsImageLoadingContent::ClearPendingRequest(nsresult aReason, - uint32_t aNonvisibleAction) + const Maybe& aNonvisibleAction) { if (!mPendingRequest) return; @@ -1451,6 +1421,27 @@ nsImageLoadingContent::UnbindFromTree(bool aDeep, bool aNullParent) doc->UnblockOnload(false); } +void +nsImageLoadingContent::OnVisibilityChange(Visibility aNewVisibility, + const Maybe& aNonvisibleAction) +{ + switch (aNewVisibility) { + case Visibility::APPROXIMATELY_VISIBLE: + TrackImage(mCurrentRequest); + TrackImage(mPendingRequest); + break; + + case Visibility::APPROXIMATELY_NONVISIBLE: + UntrackImage(mCurrentRequest, aNonvisibleAction); + UntrackImage(mPendingRequest, aNonvisibleAction); + break; + + case Visibility::UNTRACKED: + MOZ_ASSERT_UNREACHABLE("Shouldn't notify for untracked visibility"); + break; + } +} + void nsImageLoadingContent::TrackImage(imgIRequest* aImage) { @@ -1461,32 +1452,34 @@ nsImageLoadingContent::TrackImage(imgIRequest* aImage) "Why haven't we heard of this request?"); nsIDocument* doc = GetOurCurrentDoc(); - if (doc && (mFrameCreateCalled || GetOurPrimaryFrame()) && - (mVisibleCount > 0)) { + if (!doc) { + return; + } - if (mVisibleCount == 1) { - // Since we're becoming visible, request a decode. - nsImageFrame* f = do_QueryFrame(GetOurPrimaryFrame()); - if (f) { - f->MaybeDecodeForPredictedSize(); - } - } + // We only want to track this request if we're visible. Ordinarily we check + // the visible count, but that requires a frame; in cases where + // GetOurPrimaryFrame() cannot obtain a frame (e.g. ), we assume + // we're visible if FrameCreated() was called. + nsIFrame* frame = GetOurPrimaryFrame(); + if ((frame && frame->GetVisibility() == Visibility::APPROXIMATELY_NONVISIBLE) || + (!frame && !mFrameCreateCalled)) { + return; + } - if (aImage == mCurrentRequest && !(mCurrentRequestFlags & REQUEST_IS_TRACKED)) { - mCurrentRequestFlags |= REQUEST_IS_TRACKED; - doc->AddImage(mCurrentRequest); - } - if (aImage == mPendingRequest && !(mPendingRequestFlags & REQUEST_IS_TRACKED)) { - mPendingRequestFlags |= REQUEST_IS_TRACKED; - doc->AddImage(mPendingRequest); - } + if (aImage == mCurrentRequest && !(mCurrentRequestFlags & REQUEST_IS_TRACKED)) { + mCurrentRequestFlags |= REQUEST_IS_TRACKED; + doc->AddImage(mCurrentRequest); + } + if (aImage == mPendingRequest && !(mPendingRequestFlags & REQUEST_IS_TRACKED)) { + mPendingRequestFlags |= REQUEST_IS_TRACKED; + doc->AddImage(mPendingRequest); } } void nsImageLoadingContent::UntrackImage(imgIRequest* aImage, - uint32_t aNonvisibleAction - /* = ON_NONVISIBLE_NO_ACTION */) + const Maybe& aNonvisibleAction + /* = Nothing() */) { if (!aImage) return; @@ -1503,10 +1496,10 @@ nsImageLoadingContent::UntrackImage(imgIRequest* aImage, if (doc && (mCurrentRequestFlags & REQUEST_IS_TRACKED)) { mCurrentRequestFlags &= ~REQUEST_IS_TRACKED; doc->RemoveImage(mCurrentRequest, - (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) + aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES) ? nsIDocument::REQUEST_DISCARD : 0); - } else if (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) { + } else if (aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)) { // If we're not in the document we may still need to be discarded. aImage->RequestDiscard(); } @@ -1515,10 +1508,10 @@ nsImageLoadingContent::UntrackImage(imgIRequest* aImage, if (doc && (mPendingRequestFlags & REQUEST_IS_TRACKED)) { mPendingRequestFlags &= ~REQUEST_IS_TRACKED; doc->RemoveImage(mPendingRequest, - (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) + aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES) ? nsIDocument::REQUEST_DISCARD : 0); - } else if (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) { + } else if (aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)) { // If we're not in the document we may still need to be discarded. aImage->RequestDiscard(); } diff --git a/dom/base/nsImageLoadingContent.h b/dom/base/nsImageLoadingContent.h index 7595440cf1bc..34648fa85808 100644 --- a/dom/base/nsImageLoadingContent.h +++ b/dom/base/nsImageLoadingContent.h @@ -41,6 +41,11 @@ class imgRequestProxy; class nsImageLoadingContent : public nsIImageLoadingContent, public imgIOnloadBlocker { + template using Maybe = mozilla::Maybe; + using Nothing = mozilla::Nothing; + using OnNonvisible = mozilla::OnNonvisible; + using Visibility = mozilla::Visibility; + /* METHODS */ public: nsImageLoadingContent(); @@ -318,8 +323,10 @@ protected: * @param aNonvisibleAction An action to take if the image is no longer * visible as a result; see |UntrackImage|. */ - void ClearCurrentRequest(nsresult aReason, uint32_t aNonvisibleAction); - void ClearPendingRequest(nsresult aReason, uint32_t aNonvisibleAction); + void ClearCurrentRequest(nsresult aReason, + const Maybe& aNonvisibleAction = Nothing()); + void ClearPendingRequest(nsresult aReason, + const Maybe& aNonvisibleAction = Nothing()); /** * Retrieve a pointer to the 'registered with the refresh driver' flag for @@ -348,14 +355,16 @@ protected: * * No-op if aImage is null. * - * @param aNonvisibleAction What to do if the image's visibility count is now - * zero. If ON_NONVISIBLE_NO_ACTION, nothing will be - * done. If ON_NONVISIBLE_REQUEST_DISCARD, the image - * will be asked to discard its surfaces if possible. + * @param aNonvisibleAction A requested action if the frame has become + * nonvisible. If Nothing(), no action is + * requested. If DISCARD_IMAGES is specified, the + * frame is requested to ask any images it's + * associated with to discard their surfaces if + * possible. */ void TrackImage(imgIRequest* aImage); void UntrackImage(imgIRequest* aImage, - uint32_t aNonvisibleAction = ON_NONVISIBLE_NO_ACTION); + const Maybe& aNonvisibleAction = Nothing()); /* MEMBERS */ RefPtr mCurrentRequest; @@ -439,8 +448,6 @@ private: // True when FrameCreate has been called but FrameDestroy has not. bool mFrameCreateCalled; - - uint32_t mVisibleCount; }; #endif // nsImageLoadingContent_h__ diff --git a/dom/bindings/CallbackObject.cpp b/dom/bindings/CallbackObject.cpp index cb993fe5c532..6a6f568fd7a3 100644 --- a/dom/bindings/CallbackObject.cpp +++ b/dom/bindings/CallbackObject.cpp @@ -265,31 +265,13 @@ CallbackObject::CallSetup::~CallSetup() if (needToDealWithException) { // Either we're supposed to report our exceptions, or we're supposed to // re-throw them but we failed to get the exception value. Either way, - // just report the pending exception, if any. - // - // XXXbz FIXME: bug 979525 means we don't always JS_SaveFrameChain here, - // so we need to go ahead and do that. This is also the reason we don't - // just rely on ~AutoJSAPI reporting the exception for us. I think if we - // didn't need to JS_SaveFrameChain here, we could just rely on that. - JS::Rooted oldGlobal(mCx, JS::CurrentGlobalOrNull(mCx)); - MOZ_ASSERT(oldGlobal, "How can we not have a global here??"); - bool saved = JS_SaveFrameChain(mCx); - // Make sure the JSAutoCompartment goes out of scope before the - // JS_RestoreFrameChain call! - { - JSAutoCompartment ac(mCx, oldGlobal); - MOZ_ASSERT(!JS::DescribeScriptedCaller(mCx), - "Our comment above about JS_SaveFrameChain having been " - "called is a lie?"); - mAutoEntryScript->ReportException(); - } - if (saved) { - JS_RestoreFrameChain(mCx); - } - + // we'll just report the pending exception, if any, once ~mAutoEntryScript + // runs. Note that we've already run ~mAc, effectively, so we don't have + // to worry about ordering here. if (mErrorResult.IsJSContextException()) { // XXXkhuey bug 1117269. - // This isn't true anymore ... so throw something else. + // This won't be true anymore because we will report the exception on + // the JSContext ... so throw something else. mErrorResult.Throw(NS_ERROR_UNEXPECTED); } } diff --git a/dom/indexedDB/IDBDatabase.cpp b/dom/indexedDB/IDBDatabase.cpp index 470b3a8584de..5efba3de734e 100644 --- a/dom/indexedDB/IDBDatabase.cpp +++ b/dom/indexedDB/IDBDatabase.cpp @@ -39,8 +39,6 @@ #include "mozilla/ipc/InputStreamParams.h" #include "mozilla/ipc/InputStreamUtils.h" #include "nsCOMPtr.h" -#include "nsContentUtils.h" -#include "nsIConsoleService.h" #include "nsIDocument.h" #include "nsIObserver.h" #include "nsIObserverService.h" @@ -49,6 +47,7 @@ #include "nsThreadUtils.h" #include "ProfilerHelpers.h" #include "ReportInternalError.h" +#include "ScriptErrorHelper.h" #include "nsQueryObject.h" // Include this last to avoid path problems on Windows. @@ -132,50 +131,6 @@ private: } // namespace -class IDBDatabase::LogWarningRunnable final - : public nsRunnable -{ - nsCString mMessageName; - nsString mFilename; - uint32_t mLineNumber; - uint32_t mColumnNumber; - uint64_t mInnerWindowID; - bool mIsChrome; - -public: - LogWarningRunnable(const char* aMessageName, - const nsAString& aFilename, - uint32_t aLineNumber, - uint32_t aColumnNumber, - bool aIsChrome, - uint64_t aInnerWindowID) - : mMessageName(aMessageName) - , mFilename(aFilename) - , mLineNumber(aLineNumber) - , mColumnNumber(aColumnNumber) - , mInnerWindowID(aInnerWindowID) - , mIsChrome(aIsChrome) - { - MOZ_ASSERT(!NS_IsMainThread()); - } - - static void - LogWarning(const char* aMessageName, - const nsAString& aFilename, - uint32_t aLineNumber, - uint32_t aColumnNumber, - bool aIsChrome, - uint64_t aInnerWindowID); - - NS_DECL_ISUPPORTS_INHERITED - -private: - ~LogWarningRunnable() - { } - - NS_DECL_NSIRUNNABLE -}; - class IDBDatabase::Observer final : public nsIObserver { @@ -1257,23 +1212,13 @@ IDBDatabase::LogWarning(const char* aMessageName, AssertIsOnOwningThread(); MOZ_ASSERT(aMessageName); - if (NS_IsMainThread()) { - LogWarningRunnable::LogWarning(aMessageName, - aFilename, - aLineNumber, - aColumnNumber, - mFactory->IsChrome(), - mFactory->InnerWindowID()); - } else { - RefPtr runnable = - new LogWarningRunnable(aMessageName, - aFilename, - aLineNumber, - aColumnNumber, - mFactory->IsChrome(), - mFactory->InnerWindowID()); - MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable))); - } + ScriptErrorHelper::DumpLocalizedMessage(nsDependentCString(aMessageName), + aFilename, + aLineNumber, + aColumnNumber, + nsIScriptError::warningFlag, + mFactory->IsChrome(), + mFactory->InnerWindowID()); } NS_IMPL_ADDREF_INHERITED(IDBDatabase, IDBWrapperCache) @@ -1356,85 +1301,6 @@ CancelableRunnableWrapper::Cancel() return NS_ERROR_UNEXPECTED; } -// static -void -IDBDatabase:: -LogWarningRunnable::LogWarning(const char* aMessageName, - const nsAString& aFilename, - uint32_t aLineNumber, - uint32_t aColumnNumber, - bool aIsChrome, - uint64_t aInnerWindowID) -{ - MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(aMessageName); - - nsXPIDLString localizedMessage; - if (NS_WARN_IF(NS_FAILED( - nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, - aMessageName, - localizedMessage)))) { - return; - } - - nsAutoCString category; - if (aIsChrome) { - category.AssignLiteral("chrome "); - } else { - category.AssignLiteral("content "); - } - category.AppendLiteral("javascript"); - - nsCOMPtr consoleService = - do_GetService(NS_CONSOLESERVICE_CONTRACTID); - MOZ_ASSERT(consoleService); - - nsCOMPtr scriptError = - do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); - MOZ_ASSERT(consoleService); - - if (aInnerWindowID) { - MOZ_ALWAYS_TRUE(NS_SUCCEEDED( - scriptError->InitWithWindowID(localizedMessage, - aFilename, - /* aSourceLine */ EmptyString(), - aLineNumber, - aColumnNumber, - nsIScriptError::warningFlag, - category, - aInnerWindowID))); - } else { - MOZ_ALWAYS_TRUE(NS_SUCCEEDED( - scriptError->Init(localizedMessage, - aFilename, - /* aSourceLine */ EmptyString(), - aLineNumber, - aColumnNumber, - nsIScriptError::warningFlag, - category.get()))); - } - - MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError))); -} - -NS_IMPL_ISUPPORTS_INHERITED0(IDBDatabase::LogWarningRunnable, nsRunnable) - -NS_IMETHODIMP -IDBDatabase:: -LogWarningRunnable::Run() -{ - MOZ_ASSERT(NS_IsMainThread()); - - LogWarning(mMessageName.get(), - mFilename, - mLineNumber, - mColumnNumber, - mIsChrome, - mInnerWindowID); - - return NS_OK; -} - NS_IMPL_ISUPPORTS(IDBDatabase::Observer, nsIObserver) NS_IMETHODIMP diff --git a/dom/indexedDB/IDBDatabase.h b/dom/indexedDB/IDBDatabase.h index c9ba3a52410d..0b11156de23d 100644 --- a/dom/indexedDB/IDBDatabase.h +++ b/dom/indexedDB/IDBDatabase.h @@ -53,9 +53,6 @@ class IDBDatabase final typedef mozilla::dom::StorageType StorageType; typedef mozilla::dom::quota::PersistenceType PersistenceType; - class LogWarningRunnable; - friend class LogWarningRunnable; - class Observer; friend class Observer; diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp index b6061bc549e0..f5df62cf8b92 100644 --- a/dom/indexedDB/IndexedDatabaseManager.cpp +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -41,6 +41,7 @@ #include "IDBKeyRange.h" #include "IDBRequest.h" #include "ProfilerHelpers.h" +#include "ScriptErrorHelper.h" #include "WorkerScope.h" #include "WorkerPrivate.h" @@ -538,45 +539,14 @@ IndexedDatabaseManager::CommonPostHandleEvent(EventChainPostVisitor& aVisitor, return NS_OK; } - nsAutoCString category; - if (aFactory->IsChrome()) { - category.AssignLiteral("chrome "); - } else { - category.AssignLiteral("content "); - } - category.AppendLiteral("javascript"); - // Log the error to the error console. - nsCOMPtr consoleService = - do_GetService(NS_CONSOLESERVICE_CONTRACTID); - MOZ_ASSERT(consoleService); - - nsCOMPtr scriptError = - do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); - MOZ_ASSERT(consoleService); - - if (uint64_t innerWindowID = aFactory->InnerWindowID()) { - MOZ_ALWAYS_TRUE(NS_SUCCEEDED( - scriptError->InitWithWindowID(errorName, - init.mFilename, - /* aSourceLine */ EmptyString(), - init.mLineno, - init.mColno, - nsIScriptError::errorFlag, - category, - innerWindowID))); - } else { - MOZ_ALWAYS_TRUE(NS_SUCCEEDED( - scriptError->Init(errorName, - init.mFilename, - /* aSourceLine */ EmptyString(), - init.mLineno, - init.mColno, - nsIScriptError::errorFlag, - category.get()))); - } - - MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError))); + ScriptErrorHelper::Dump(errorName, + init.mFilename, + init.mLineno, + init.mColno, + nsIScriptError::errorFlag, + aFactory->IsChrome(), + aFactory->InnerWindowID()); return NS_OK; } diff --git a/dom/indexedDB/ScriptErrorHelper.cpp b/dom/indexedDB/ScriptErrorHelper.cpp new file mode 100644 index 000000000000..161eb9f71b33 --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.cpp @@ -0,0 +1,249 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ScriptErrorHelper.h" + +#include "MainThreadUtils.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace { + +class ScriptErrorRunnable final : public nsRunnable +{ + nsString mMessage; + nsCString mMessageName; + nsString mFilename; + uint32_t mLineNumber; + uint32_t mColumnNumber; + uint32_t mSeverityFlag; + uint64_t mInnerWindowID; + bool mIsChrome; + +public: + ScriptErrorRunnable(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + : mMessage(aMessage) + , mFilename(aFilename) + , mLineNumber(aLineNumber) + , mColumnNumber(aColumnNumber) + , mSeverityFlag(aSeverityFlag) + , mInnerWindowID(aInnerWindowID) + , mIsChrome(aIsChrome) + { + MOZ_ASSERT(!NS_IsMainThread()); + mMessageName.SetIsVoid(true); + } + + ScriptErrorRunnable(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + : mMessageName(aMessageName) + , mFilename(aFilename) + , mLineNumber(aLineNumber) + , mColumnNumber(aColumnNumber) + , mSeverityFlag(aSeverityFlag) + , mInnerWindowID(aInnerWindowID) + , mIsChrome(aIsChrome) + { + MOZ_ASSERT(!NS_IsMainThread()); + mMessage.SetIsVoid(true); + } + + static void + DumpLocalizedMessage(const nsCString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aMessageName.IsEmpty()); + + nsXPIDLString localizedMessage; + if (NS_WARN_IF(NS_FAILED( + nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, + aMessageName.get(), + localizedMessage)))) { + return; + } + + Dump(localizedMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } + + static void + Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) + { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoCString category; + if (aIsChrome) { + category.AssignLiteral("chrome "); + } else { + category.AssignLiteral("content "); + } + category.AppendLiteral("javascript"); + + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + MOZ_ASSERT(consoleService); + + nsCOMPtr scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + MOZ_ASSERT(scriptError); + + if (aInnerWindowID) { + MOZ_ALWAYS_TRUE(NS_SUCCEEDED( + scriptError->InitWithWindowID(aMessage, + aFilename, + /* aSourceLine */ EmptyString(), + aLineNumber, + aColumnNumber, + aSeverityFlag, + category, + aInnerWindowID))); + } else { + MOZ_ALWAYS_TRUE(NS_SUCCEEDED( + scriptError->Init(aMessage, + aFilename, + /* aSourceLine */ EmptyString(), + aLineNumber, + aColumnNumber, + aSeverityFlag, + category.get()))); + } + + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError))); + } + + NS_IMETHOD + Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mMessage.IsVoid() != mMessageName.IsVoid()); + + if (!mMessage.IsVoid()) { + Dump(mMessage, + mFilename, + mLineNumber, + mColumnNumber, + mSeverityFlag, + mIsChrome, + mInnerWindowID); + return NS_OK; + } + + DumpLocalizedMessage(mMessageName, + mFilename, + mLineNumber, + mColumnNumber, + mSeverityFlag, + mIsChrome, + mInnerWindowID); + + return NS_OK; + } + +private: + virtual ~ScriptErrorRunnable() {} +}; + +} // namespace + +namespace mozilla { +namespace dom { +namespace indexedDB { + +/*static*/ void +ScriptErrorHelper::Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) +{ + if (NS_IsMainThread()) { + ScriptErrorRunnable::Dump(aMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } else { + RefPtr runnable = + new ScriptErrorRunnable(aMessage, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable))); + } +} + +/*static*/ void +ScriptErrorHelper::DumpLocalizedMessage(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, + bool aIsChrome, + uint64_t aInnerWindowID) +{ + if (NS_IsMainThread()) { + ScriptErrorRunnable::DumpLocalizedMessage(nsAutoCString(aMessageName), + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + } else { + RefPtr runnable = + new ScriptErrorRunnable(aMessageName, + aFilename, + aLineNumber, + aColumnNumber, + aSeverityFlag, + aIsChrome, + aInnerWindowID); + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable))); + } +} + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla \ No newline at end of file diff --git a/dom/indexedDB/ScriptErrorHelper.h b/dom/indexedDB/ScriptErrorHelper.h new file mode 100644 index 000000000000..d1cd8c749342 --- /dev/null +++ b/dom/indexedDB/ScriptErrorHelper.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_indexeddb_scripterrorhelper_h__ +#define mozilla_dom_indexeddb_scripterrorhelper_h__ + +class nsAString; + +namespace mozilla { +namespace dom { +namespace indexedDB { + +// Helper to report a script error to the main thread. +class ScriptErrorHelper +{ +public: + static void Dump(const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */ + bool aIsChrome, + uint64_t aInnerWindowID); + + static void DumpLocalizedMessage(const nsACString& aMessageName, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */ + bool aIsChrome, + uint64_t aInnerWindowID); +}; + +} // namespace indexedDB +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_indexeddb_scripterrorhelper_h__ \ No newline at end of file diff --git a/dom/indexedDB/moz.build b/dom/indexedDB/moz.build index 28a56befc9b8..141bb1cbcec8 100644 --- a/dom/indexedDB/moz.build +++ b/dom/indexedDB/moz.build @@ -67,6 +67,7 @@ UNIFIED_SOURCES += [ 'KeyPath.cpp', 'PermissionRequestBase.cpp', 'ReportInternalError.cpp', + 'ScriptErrorHelper.cpp', ] SOURCES += [ diff --git a/dom/interfaces/core/nsIDOMNode.idl b/dom/interfaces/core/nsIDOMNode.idl index c9ba03db4c10..fc390dd7bfd6 100644 --- a/dom/interfaces/core/nsIDOMNode.idl +++ b/dom/interfaces/core/nsIDOMNode.idl @@ -8,11 +8,11 @@ interface nsIVariant; /** - * The nsIDOMNode interface is the primary datatype for the entire + * The nsIDOMNode interface is the primary datatype for the entire * Document Object Model. * It represents a single node in the document tree. * - * For more information on this interface please see + * For more information on this interface please see * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html */ @@ -46,10 +46,10 @@ interface nsIDOMNode : nsISupports readonly attribute nsIDOMNode nextSibling; // Modified in DOM Level 2: readonly attribute nsIDOMDocument ownerDocument; - nsIDOMNode insertBefore(in nsIDOMNode newChild, + nsIDOMNode insertBefore(in nsIDOMNode newChild, in nsIDOMNode refChild) raises(DOMException); - nsIDOMNode replaceChild(in nsIDOMNode newChild, + nsIDOMNode replaceChild(in nsIDOMNode newChild, in nsIDOMNode oldChild) raises(DOMException); nsIDOMNode removeChild(in nsIDOMNode oldChild) diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp index 54e150ca5afb..0bae7dab72c8 100644 --- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -1256,10 +1256,6 @@ MediaDecoderStateMachine::SetDormant(bool aDormant) return; } - if (!mReader) { - return; - } - if (mMetadataRequest.Exists()) { if (mPendingDormant && mPendingDormant.ref() != aDormant && !aDormant) { // We already have a dormant request pending; the new request would have diff --git a/dom/media/PeerConnection.js b/dom/media/PeerConnection.js index 240a031e1578..cffa3b87dddc 100644 --- a/dom/media/PeerConnection.js +++ b/dom/media/PeerConnection.js @@ -1060,7 +1060,13 @@ RTCPeerConnection.prototype = { "InvalidParameterError"); } }); - this._impl.addTrack(track, stream); + try { + this._impl.addTrack(track, stream); + } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) { + throw new this._win.DOMException( + "track in constructed stream not yet supported (see Bug 1259236).", + "NotSupportedError"); + } let sender = this._win.RTCRtpSender._create(this._win, new RTCRtpSender(this, track, stream)); diff --git a/dom/media/mediasink/DecodedStream.cpp b/dom/media/mediasink/DecodedStream.cpp index 65add000a65a..35828a39a258 100644 --- a/dom/media/mediasink/DecodedStream.cpp +++ b/dom/media/mediasink/DecodedStream.cpp @@ -6,6 +6,7 @@ #include "mozilla/CheckedInt.h" #include "mozilla/gfx/Point.h" +#include "mozilla/SyncRunnable.h" #include "AudioSegment.h" #include "DecodedStream.h" @@ -269,23 +270,35 @@ DecodedStream::Start(int64_t aStartTime, const MediaInfo& aInfo) class R : public nsRunnable { typedef MozPromiseHolder Promise; - typedef decltype(&DecodedStream::CreateData) Method; public: - R(DecodedStream* aThis, Method aMethod, PlaybackInfoInit&& aInit, Promise&& aPromise) - : mThis(aThis), mMethod(aMethod), mInit(Move(aInit)) + R(PlaybackInfoInit&& aInit, Promise&& aPromise, OutputStreamManager* aManager) + : mInit(Move(aInit)), mOutputStreamManager(aManager) { mPromise = Move(aPromise); } NS_IMETHOD Run() override { - (mThis->*mMethod)(Move(mInit), Move(mPromise)); + MOZ_ASSERT(NS_IsMainThread()); + // No need to create a source stream when there are no output streams. This + // happens when RemoveOutput() is called immediately after StartPlayback(). + if (!mOutputStreamManager->Graph()) { + // Resolve the promise to indicate the end of playback. + mPromise.Resolve(true, __func__); + return NS_OK; + } + mData = MakeUnique( + mOutputStreamManager, Move(mInit), Move(mPromise)); return NS_OK; } + UniquePtr ReleaseData() + { + return Move(mData); + } private: - RefPtr mThis; - Method mMethod; PlaybackInfoInit mInit; Promise mPromise; + RefPtr mOutputStreamManager; + UniquePtr mData; }; MozPromiseHolder promise; @@ -293,8 +306,15 @@ DecodedStream::Start(int64_t aStartTime, const MediaInfo& aInfo) PlaybackInfoInit init { aStartTime, aInfo }; - nsCOMPtr r = new R(this, &DecodedStream::CreateData, Move(init), Move(promise)); - AbstractThread::MainThread()->Dispatch(r.forget()); + nsCOMPtr r = new R(Move(init), Move(promise), mOutputStreamManager); + nsCOMPtr mainThread = do_GetMainThread(); + SyncRunnable::DispatchToThread(mainThread, r); + mData = static_cast(r.get())->ReleaseData(); + + if (mData) { + mData->SetPlaying(mPlaying); + SendData(); + } } void @@ -342,77 +362,6 @@ DecodedStream::DestroyData(UniquePtr aData) AbstractThread::MainThread()->Dispatch(r.forget()); } -void -DecodedStream::CreateData(PlaybackInfoInit&& aInit, MozPromiseHolder&& aPromise) -{ - MOZ_ASSERT(NS_IsMainThread()); - - // No need to create a source stream when there are no output streams. This - // happens when RemoveOutput() is called immediately after StartPlayback(). - if (!mOutputStreamManager->Graph()) { - // Resolve the promise to indicate the end of playback. - aPromise.Resolve(true, __func__); - return; - } - - auto data = new DecodedStreamData(mOutputStreamManager, Move(aInit), Move(aPromise)); - - class R : public nsRunnable { - typedef void(DecodedStream::*Method)(UniquePtr); - public: - R(DecodedStream* aThis, Method aMethod, DecodedStreamData* aData) - : mThis(aThis), mMethod(aMethod), mData(aData) {} - NS_IMETHOD Run() override - { - (mThis->*mMethod)(Move(mData)); - return NS_OK; - } - private: - virtual ~R() - { - // mData is not transferred when dispatch fails and Run() is not called. - // We need to dispatch a task to ensure DecodedStreamData is destroyed - // properly on the main thread. - if (mData) { - DecodedStreamData* data = mData.release(); - nsCOMPtr r = NS_NewRunnableFunction([=] () { - delete data; - }); - // We are in tail dispatching phase. Don't call - // AbstractThread::MainThread()->Dispatch() to avoid reentrant - // AutoTaskDispatcher. - NS_DispatchToMainThread(r.forget()); - } - } - RefPtr mThis; - Method mMethod; - UniquePtr mData; - }; - - // Post a message to ensure |mData| is only updated on the worker thread. - // Note this could fail when MDSM begin to shut down the worker thread. - nsCOMPtr r = new R(this, &DecodedStream::OnDataCreated, data); - mOwnerThread->Dispatch(r.forget(), AbstractThread::DontAssertDispatchSuccess); -} - -void -DecodedStream::OnDataCreated(UniquePtr aData) -{ - AssertOwnerThread(); - MOZ_ASSERT(!mData, "Already created."); - - // Start to send data to the stream immediately - if (mStartTime.isSome()) { - aData->SetPlaying(mPlaying); - mData = Move(aData); - SendData(); - return; - } - - // Playback has ended. Destroy aData which is not needed anymore. - DestroyData(Move(aData)); -} - void DecodedStream::SetPlaying(bool aPlaying) { diff --git a/dom/media/mediasink/DecodedStream.h b/dom/media/mediasink/DecodedStream.h index 3bda109f40b3..94820addc923 100644 --- a/dom/media/mediasink/DecodedStream.h +++ b/dom/media/mediasink/DecodedStream.h @@ -66,9 +66,7 @@ protected: virtual ~DecodedStream(); private: - void CreateData(PlaybackInfoInit&& aInit, MozPromiseHolder&& aPromise); void DestroyData(UniquePtr aData); - void OnDataCreated(UniquePtr aData); void AdvanceTracks(); void SendAudio(double aVolume, bool aIsSameOrigin); void SendVideo(bool aIsSameOrigin); diff --git a/dom/media/tests/mochitest/head.js b/dom/media/tests/mochitest/head.js index 225860c74dbf..c49312e10c2c 100644 --- a/dom/media/tests/mochitest/head.js +++ b/dom/media/tests/mochitest/head.js @@ -420,6 +420,17 @@ var listenUntil = (target, eventName, onFire) => { }, false)); }; +/* Test that a function throws the right error */ +function mustThrowWith(msg, reason, f) { + try { + f(); + ok(false, msg + " must throw"); + } catch (e) { + is(e.name, reason, msg + " must throw: " + e.message); + } +}; + + /*** Test control flow methods */ /** diff --git a/dom/media/tests/mochitest/mochitest.ini b/dom/media/tests/mochitest/mochitest.ini index 591cb8af48a4..69b2200270d6 100644 --- a/dom/media/tests/mochitest/mochitest.ini +++ b/dom/media/tests/mochitest/mochitest.ini @@ -70,6 +70,7 @@ skip-if = (toolkit == 'gonk' || buildapp == 'mulet' && debug) # copied from basi [test_getUserMedia_peerIdentity.html] skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 1021776, too --ing slow on b2g) [test_peerConnection_addIceCandidate.html] +[test_peerConnection_addTrack.html] [test_peerConnection_basicAudio.html] skip-if = toolkit == 'gonk' # B2G emulator is too slow to handle a two-way audio call reliably [test_peerConnection_basicAudioRequireEOC.html] diff --git a/dom/media/tests/mochitest/test_peerConnection_addTrack.html b/dom/media/tests/mochitest/test_peerConnection_addTrack.html new file mode 100644 index 000000000000..f250454fae72 --- /dev/null +++ b/dom/media/tests/mochitest/test_peerConnection_addTrack.html @@ -0,0 +1,31 @@ + + + + + + +
+
+
+ + diff --git a/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html index 3e3e3365d990..f7d7d773e9bb 100644 --- a/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html +++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html @@ -19,15 +19,6 @@ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback()); pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback()); - function mustThrowWith(msg, reason, f) { - try { - f(); - ok(false, msg + " must throw"); - } catch (e) { - is(e.name, reason, msg + " must throw: " + e.message); - } - }; - var v1, v2; var delivered = new Promise(resolve => pc2.ontrack = e => { // Test RTCTrackEvent here. diff --git a/dom/plugins/base/nsPluginStreamListenerPeer.cpp b/dom/plugins/base/nsPluginStreamListenerPeer.cpp index bfceea950213..48cf5cf673d0 100644 --- a/dom/plugins/base/nsPluginStreamListenerPeer.cpp +++ b/dom/plugins/base/nsPluginStreamListenerPeer.cpp @@ -8,6 +8,7 @@ #include "nsContentPolicyUtils.h" #include "nsIDOMElement.h" #include "nsIStreamConverterService.h" +#include "nsIStreamLoader.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIFileChannel.h" @@ -34,7 +35,7 @@ #include "nsDataHashtable.h" #include "nsNullPrincipal.h" -#define MAGIC_REQUEST_CONTEXT 0x01020304 +#define BYTERANGE_REQUEST_CONTEXT 0x01020304 // nsPluginByteRangeStreamListener @@ -55,7 +56,7 @@ private: nsCOMPtr mStreamConverter; nsWeakPtr mWeakPtrPluginStreamListenerPeer; - bool mRemoveMagicNumber; + bool mRemoveByteRangeRequest; }; NS_IMPL_ISUPPORTS(nsPluginByteRangeStreamListener, @@ -66,7 +67,7 @@ NS_IMPL_ISUPPORTS(nsPluginByteRangeStreamListener, nsPluginByteRangeStreamListener::nsPluginByteRangeStreamListener(nsIWeakReference* aWeakPtr) { mWeakPtrPluginStreamListenerPeer = aWeakPtr; - mRemoveMagicNumber = false; + mRemoveByteRangeRequest = false; } nsPluginByteRangeStreamListener::~nsPluginByteRangeStreamListener() @@ -149,7 +150,7 @@ nsPluginByteRangeStreamListener::OnStartRequest(nsIRequest *request, nsISupports // if server cannot continue with byte range (206 status) and sending us whole object (200 status) // reset this seekable stream & try serve it to plugin instance as a file mStreamConverter = finalStreamListener; - mRemoveMagicNumber = true; + mRemoveByteRangeRequest = true; rv = pslp->ServeStreamAsFile(request, ctxt); return rv; @@ -173,15 +174,15 @@ nsPluginByteRangeStreamListener::OnStopRequest(nsIRequest *request, nsISupports NS_ERROR("OnStopRequest received for untracked byte-range request!"); } - if (mRemoveMagicNumber) { - // remove magic number from container + if (mRemoveByteRangeRequest) { + // remove byte range request from container nsCOMPtr container = do_QueryInterface(ctxt); if (container) { - uint32_t magicNumber = 0; - container->GetData(&magicNumber); - if (magicNumber == MAGIC_REQUEST_CONTEXT) { + uint32_t byteRangeRequest = 0; + container->GetData(&byteRangeRequest); + if (byteRangeRequest == BYTERANGE_REQUEST_CONTEXT) { // to allow properly finish nsPluginStreamListenerPeer->OnStopRequest() - // set it to something that is not the magic number. + // set it to something that is not the byte range request. container->SetData(0); } } else { @@ -660,6 +661,63 @@ nsPluginStreamListenerPeer::MakeByteRangeString(NPByteRange* aRangeList, nsACStr return; } +// XXX: Converting the channel within nsPluginStreamListenerPeer +// to use asyncOpen2() and do not want to touch the fragile logic +// of byte range requests. Hence we just introduce this lightweight +// wrapper to proxy the context. +class PluginContextProxy final : public nsIStreamListener +{ +public: + NS_DECL_ISUPPORTS + + PluginContextProxy(nsIStreamListener *aListener, nsISupports* aContext) + : mListener(aListener) + , mContext(aContext) + { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aContext); + } + + NS_IMETHOD + OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream *aIStream, + uint64_t aSourceOffset, + uint32_t aLength) override + { + // Proxy OnDataAvailable using the internal context + return mListener->OnDataAvailable(aRequest, + mContext, + aIStream, + aSourceOffset, + aLength); + } + + NS_IMETHOD + OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) override + { + // Proxy OnStartRequest using the internal context + return mListener->OnStartRequest(aRequest, mContext); + } + + NS_IMETHOD + OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, + nsresult aStatusCode) override + { + // Proxy OnStopRequest using the inernal context + return mListener->OnStopRequest(aRequest, + mContext, + aStatusCode); + } + +private: + ~PluginContextProxy() {} + nsCOMPtr mListener; + nsCOMPtr mContext; +}; + +NS_IMPL_ISUPPORTS(PluginContextProxy, nsIStreamListener) + nsresult nsPluginStreamListenerPeer::RequestRead(NPByteRange* rangeList) { @@ -692,22 +750,19 @@ nsPluginStreamListenerPeer::RequestRead(NPByteRange* rangeList) rv = NS_NewChannel(getter_AddRefs(channel), mURL, requestingNode, - nsILoadInfo::SEC_NORMAL, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER, loadGroup, callbacks, nsIChannel::LOAD_BYPASS_SERVICE_WORKER); } else { - // in this else branch we really don't know where the load is coming - // from and in fact should use something better than just using - // a nullPrincipal as the loadingPrincipal. - nsCOMPtr principal = nsNullPrincipal::Create(); - NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); + // In this else branch we really don't know where the load is coming + // from. Let's fall back to using the SystemPrincipal for such Plugins. rv = NS_NewChannel(getter_AddRefs(channel), mURL, - principal, - nsILoadInfo::SEC_NORMAL, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, nsIContentPolicy::TYPE_OTHER, loadGroup, callbacks, @@ -743,16 +798,16 @@ nsPluginStreamListenerPeer::RequestRead(NPByteRange* rangeList) mPendingRequests += numRequests; nsCOMPtr container = do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); - if (NS_FAILED(rv)) - return rv; - rv = container->SetData(MAGIC_REQUEST_CONTEXT); - if (NS_FAILED(rv)) - return rv; + NS_ENSURE_SUCCESS(rv, rv); + rv = container->SetData(BYTERANGE_REQUEST_CONTEXT); + NS_ENSURE_SUCCESS(rv, rv); - rv = channel->AsyncOpen(converter, container); - if (NS_SUCCEEDED(rv)) - TrackRequest(channel); - return rv; + RefPtr pluginContextProxy = + new PluginContextProxy(converter, container); + rv = channel->AsyncOpen2(pluginContextProxy); + NS_ENSURE_SUCCESS(rv, rv); + TrackRequest(channel); + return NS_OK; } nsresult @@ -842,12 +897,12 @@ NS_IMETHODIMP nsPluginStreamListenerPeer::OnDataAvailable(nsIRequest *request, return NS_ERROR_FAILURE; if (mAbort) { - uint32_t magicNumber = 0; // set it to something that is not the magic number. + uint32_t byteRangeRequest = 0; // set it to something that is not the byte range request. nsCOMPtr container = do_QueryInterface(aContext); if (container) - container->GetData(&magicNumber); + container->GetData(&byteRangeRequest); - if (magicNumber != MAGIC_REQUEST_CONTEXT) { + if (byteRangeRequest != BYTERANGE_REQUEST_CONTEXT) { // this is not one of our range requests mAbort = false; return NS_BINDING_ABORTED; @@ -980,9 +1035,9 @@ NS_IMETHODIMP nsPluginStreamListenerPeer::OnStopRequest(nsIRequest *request, // we keep our connections around... nsCOMPtr container = do_QueryInterface(aContext); if (container) { - uint32_t magicNumber = 0; // set it to something that is not the magic number. - container->GetData(&magicNumber); - if (magicNumber == MAGIC_REQUEST_CONTEXT) { + uint32_t byteRangeRequest = 0; // something other than the byte range request. + container->GetData(&byteRangeRequest); + if (byteRangeRequest == BYTERANGE_REQUEST_CONTEXT) { // this is one of our range requests return NS_OK; } diff --git a/dom/push/PushComponents.js b/dom/push/PushComponents.js index dd1f9cf03b44..3a2ea231169b 100644 --- a/dom/push/PushComponents.js +++ b/dom/push/PushComponents.js @@ -16,6 +16,14 @@ Cu.import("resource://gre/modules/Services.jsm"); var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +// The default Push service implementation. +XPCOMUtils.defineLazyGetter(this, "PushService", function() { + const {PushService} = Cu.import("resource://gre/modules/PushService.jsm", + {}); + PushService.init(); + return PushService; +}); + // Observer notification topics for system subscriptions. These are duplicated // and used in `PushNotifier.cpp`. They're exposed on `nsIPushService` instead // of `nsIPushNotifier` so that JS callers only need to import this service. @@ -99,14 +107,6 @@ PushServiceParent.prototype = Object.create(PushServiceBase.prototype); XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm", "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster"); -XPCOMUtils.defineLazyGetter(PushServiceParent.prototype, "_service", - function() { - const {PushService} = Cu.import("resource://gre/modules/PushService.jsm", - {}); - PushService.init(); - return PushService; -}); - Object.assign(PushServiceParent.prototype, { _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent), @@ -164,11 +164,11 @@ Object.assign(PushServiceParent.prototype, { // nsIPushQuotaManager methods notificationForOriginShown(origin) { - this._service.notificationForOriginShown(origin); + this.service.notificationForOriginShown(origin); }, notificationForOriginClosed(origin) { - this._service.notificationForOriginClosed(origin); + this.service.notificationForOriginClosed(origin); }, receiveMessage(message) { @@ -201,7 +201,7 @@ Object.assign(PushServiceParent.prototype, { }, _handleReady() { - this._service.init(); + this.service.init(); }, _toPageRecord(principal, data) { @@ -228,7 +228,7 @@ Object.assign(PushServiceParent.prototype, { _handleRequest(name, principal, data) { if (name == "Push:Clear") { - return this._service.clear(data); + return this.service.clear(data); } let pageRecord; @@ -239,13 +239,13 @@ Object.assign(PushServiceParent.prototype, { } if (name === "Push:Register") { - return this._service.register(pageRecord); + return this.service.register(pageRecord); } if (name === "Push:Registration") { - return this._service.registration(pageRecord); + return this.service.registration(pageRecord); } if (name === "Push:Unregister") { - return this._service.unregister(pageRecord); + return this.service.unregister(pageRecord); } return Promise.reject(new Error("Invalid request: unknown name")); @@ -259,12 +259,22 @@ Object.assign(PushServiceParent.prototype, { // Methods used for mocking in tests. replaceServiceBackend(options) { - this._service.changeTestServer(options.serverURI, options); + this.service.changeTestServer(options.serverURI, options); }, restoreServiceBackend() { var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL"); - this._service.changeTestServer(defaultServerURL); + this.service.changeTestServer(defaultServerURL); + }, +}); + +// Used to replace the implementation with a mock. +Object.defineProperty(PushServiceParent.prototype, "service", { + get() { + return this._service || PushService; + }, + set(impl) { + this._service = impl; }, }); diff --git a/dom/push/PushCrypto.jsm b/dom/push/PushCrypto.jsm index 15438b48340e..0717166c1ea0 100644 --- a/dom/push/PushCrypto.jsm +++ b/dom/push/PushCrypto.jsm @@ -14,7 +14,15 @@ this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray', 'base64UrlDecode']; var UTF8 = new TextEncoder('utf-8'); -var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128'); + +// Legacy encryption scheme (draft-thomson-http-encryption-02). +var AESGCM128_ENCODING = 'aesgcm128'; +var AESGCM128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128'); + +// New encryption scheme (draft-ietf-httpbis-encryption-encoding-01). +var AESGCM_ENCODING = 'aesgcm'; +var AESGCM_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm'); + var NONCE_INFO = UTF8.encode('Content-Encoding: nonce'); var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus var P256DH_INFO = UTF8.encode('P-256\0'); @@ -53,14 +61,28 @@ this.getCryptoParams = function(headers) { } var requiresAuthenticationSecret = true; - var keymap = getEncryptionKeyParams(headers.crypto_key); - if (!keymap) { - requiresAuthenticationSecret = false; - keymap = getEncryptionKeyParams(headers.encryption_key); + var keymap; + var padSize; + if (headers.encoding == AESGCM_ENCODING) { + // aesgcm uses the Crypto-Key header, 2 bytes for the pad length, and an + // authentication secret. + keymap = getEncryptionKeyParams(headers.crypto_key); + padSize = 2; + } else if (headers.encoding == AESGCM128_ENCODING) { + // aesgcm128 uses Crypto-Key or Encryption-Key, and 1 byte for the pad + // length. + keymap = getEncryptionKeyParams(headers.crypto_key); + padSize = 1; if (!keymap) { - return null; + // Encryption-Key header indicates unauthenticated encryption. + requiresAuthenticationSecret = false; + keymap = getEncryptionKeyParams(headers.encryption_key); } } + if (!keymap) { + return null; + } + var enc = getEncryptionParams(headers.encryption); if (!enc) { return null; @@ -69,10 +91,10 @@ this.getCryptoParams = function(headers) { var salt = enc.salt; var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096; - if (!dh || !salt || isNaN(rs) || (rs <= 1)) { + if (!dh || !salt || isNaN(rs) || (rs <= padSize)) { return null; } - return {dh, salt, rs, auth: requiresAuthenticationSecret}; + return {dh, salt, rs, auth: requiresAuthenticationSecret, padSize}; } var parseHeaderFieldParams = (m, v) => { @@ -195,8 +217,8 @@ this.PushCrypto = { ])); }, - decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, - aSalt, aRs, aAuthenticationSecret) { + decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs, + aAuthenticationSecret, aPadSize) { if (aData.byteLength === 0) { // Zero length messages will be passed as null. @@ -219,7 +241,8 @@ this.PushCrypto = { .then(([appServerKey, subscriptionPrivateKey]) => crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey }, subscriptionPrivateKey, 256)) - .then(ikm => this._deriveKeyAndNonce(new Uint8Array(ikm), + .then(ikm => this._deriveKeyAndNonce(aPadSize, + new Uint8Array(ikm), base64UrlDecode(aSalt), aPublicKey, senderKey, @@ -227,13 +250,15 @@ this.PushCrypto = { .then(r => // AEAD_AES_128_GCM expands ciphertext to be 16 octets longer. Promise.all(chunkArray(aData, aRs + 16).map((slice, index) => - this._decodeChunk(slice, index, r[1], r[0])))) + this._decodeChunk(aPadSize, slice, index, r[1], r[0])))) .then(r => concatArray(r)); }, - _deriveKeyAndNonce(ikm, salt, receiverKey, senderKey, authenticationSecret) { + _deriveKeyAndNonce(padSize, ikm, salt, receiverKey, senderKey, + authenticationSecret) { var kdfPromise; var context; + var encryptInfo; // The authenticationSecret, when present, is mixed with the ikm using HKDF. // This is its primary purpose. However, since the authentication secret // was added at the same time that the info string was changed, we also use @@ -260,12 +285,19 @@ this.PushCrypto = { this._encodeLength(receiverKey), receiverKey, this._encodeLength(senderKey), senderKey ]); + // Finally, we use the pad size to infer the content encoding. + encryptInfo = padSize == 2 ? AESGCM_ENCRYPT_INFO : + AESGCM128_ENCRYPT_INFO; } else { + if (padSize == 2) { + throw new Error("aesgcm encoding requires an authentication secret"); + } kdfPromise = Promise.resolve(new hkdf(salt, ikm)); context = new Uint8Array(0); + encryptInfo = AESGCM128_ENCRYPT_INFO; } return kdfPromise.then(kdf => Promise.all([ - kdf.extract(concatArray([ENCRYPT_INFO, context]), 16) + kdf.extract(concatArray([encryptInfo, context]), 16) .then(gcmBits => crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false, ['decrypt'])), kdf.extract(concatArray([NONCE_INFO, context]), 12) @@ -276,27 +308,44 @@ this.PushCrypto = { return new Uint8Array([0, buffer.byteLength]); }, - _decodeChunk(aSlice, aIndex, aNonce, aKey) { + _decodeChunk(aPadSize, aSlice, aIndex, aNonce, aKey) { let params = { name: 'AES-GCM', iv: generateNonce(aNonce, aIndex) }; return crypto.subtle.decrypt(params, aKey, aSlice) - .then(decoded => { - decoded = new Uint8Array(decoded); - if (decoded.length == 0) { - return Promise.reject(new Error('Decoded array is too short!')); - } else if (decoded[0] > decoded.length) { - return Promise.reject(new Error ('Padding is wrong!')); - } else { - // All padded bytes must be zero except the first one. - for (var i = 1; i <= decoded[0]; i++) { - if (decoded[i] != 0) { - return Promise.reject(new Error('Padding is wrong!')); - } - } - return decoded.slice(decoded[0] + 1); - } - }); - } + .then(decoded => this._unpadChunk(aPadSize, new Uint8Array(decoded))); + }, + + /** + * Removes padding from a decrypted chunk. + * + * @param {Number} padSize The size of the padding length prepended to each + * chunk. For aesgcm, the padding length is expressed as a 16-bit unsigned + * big endian integer. For aesgcm128, the padding is an 8-bit integer. + * @param {Uint8Array} decoded The decrypted, padded chunk. + * @returns {Uint8Array} The chunk with padding removed. + */ + _unpadChunk(padSize, decoded) { + if (padSize < 1 || padSize > 2) { + throw new Error('Unsupported pad size'); + } + if (decoded.length < padSize) { + throw new Error('Decoded array is too short!'); + } + var pad = decoded[0]; + if (padSize == 2) { + pad = (pad << 8) | decoded[1]; + } + if (pad > decoded.length) { + throw new Error ('Padding is wrong!'); + } + // All padded bytes must be zero except the first one. + for (var i = padSize; i <= pad; i++) { + if (decoded[i] !== 0) { + throw new Error('Padding is wrong!'); + } + } + return decoded.slice(pad + padSize); + }, }; diff --git a/dom/push/PushService.jsm b/dom/push/PushService.jsm index 7dd50c920302..2028f9256a53 100644 --- a/dom/push/PushService.jsm +++ b/dom/push/PushService.jsm @@ -811,7 +811,8 @@ this.PushService = { cryptoParams.dh, cryptoParams.salt, cryptoParams.rs, - cryptoParams.auth ? record.authenticationSecret : null + cryptoParams.auth ? record.authenticationSecret : null, + cryptoParams.padSize ); } else { decodedPromise = Promise.resolve(null); diff --git a/dom/push/PushServiceAndroidGCM.jsm b/dom/push/PushServiceAndroidGCM.jsm index 153c690b54a6..82bef7af5466 100644 --- a/dom/push/PushServiceAndroidGCM.jsm +++ b/dom/push/PushServiceAndroidGCM.jsm @@ -114,6 +114,7 @@ this.PushServiceAndroidGCM = { encryption_key: data.enckey, crypto_key: data.cryptokey, encryption: data.enc, + encoding: data.con, }; cryptoParams = getCryptoParams(headers); // Ciphertext is (urlsafe) Base 64 encoded. diff --git a/dom/push/PushServiceHttp2.jsm b/dom/push/PushServiceHttp2.jsm index 2c5b8ef092cd..e6906fd3a9f3 100644 --- a/dom/push/PushServiceHttp2.jsm +++ b/dom/push/PushServiceHttp2.jsm @@ -155,6 +155,7 @@ PushChannelListener.prototype = { encryption_key: getHeaderField(aRequest, "Encryption-Key"), crypto_key: getHeaderField(aRequest, "Crypto-Key"), encryption: getHeaderField(aRequest, "Encryption"), + encoding: getHeaderField(aRequest, "Content-Encoding"), }; let cryptoParams = getCryptoParams(headers); let msg = concatArray(this._message); diff --git a/dom/push/test/mockpushserviceparent.js b/dom/push/test/mockpushserviceparent.js index 344dbf249aef..967b686464e6 100644 --- a/dom/push/test/mockpushserviceparent.js +++ b/dom/push/test/mockpushserviceparent.js @@ -48,7 +48,7 @@ MockWebSocketParent.prototype = { }, sendMsg(msg) { - sendAsyncMessage("client-msg", msg); + sendAsyncMessage("socket-client-msg", msg); }, close() { @@ -83,7 +83,7 @@ var pushService = Cc["@mozilla.org/push/Service;1"]. var mockWebSocket; -addMessageListener("setup", function () { +addMessageListener("socket-setup", function () { mockWebSocket = new Promise((resolve, reject) => { var mockSocket = null; pushService.replaceServiceBackend({ @@ -101,15 +101,72 @@ addMessageListener("setup", function () { }); }); -addMessageListener("teardown", function () { +addMessageListener("socket-teardown", function () { mockWebSocket.then(socket => { socket.close(); pushService.restoreServiceBackend(); }); }); -addMessageListener("server-msg", function (msg) { +addMessageListener("socket-server-msg", function (msg) { mockWebSocket.then(socket => { socket.serverSendMsg(msg); }); }); + +var MockService = { + requestID: 1, + resolvers: new Map(), + + sendRequest(name, params) { + return new Promise((resolve, reject) => { + let id = this.requestID++; + this.resolvers.set(id, { resolve, reject }); + sendAsyncMessage("service-request", { + name: name, + id: id, + params: params, + }); + }); + }, + + handleResponse(response) { + if (!this.resolvers.has(response.id)) { + Cu.reportError(`Unexpected response for request ${response.id}`); + return; + } + let resolver = this.resolvers.get(response.id); + this.resolvers.delete(response.id); + if (response.error) { + resolver.reject(response.error); + } else { + resolver.resolve(response.result); + } + }, + + init() {}, + + register(pageRecord) { + return this.sendRequest("register", pageRecord); + }, + + registration(pageRecord) { + return this.sendRequest("registration", pageRecord); + }, + + unregister(pageRecord) { + return this.sendRequest("unregister", pageRecord); + }, +}; + +addMessageListener("service-replace", function () { + pushService.service = MockService; +}); + +addMessageListener("service-restore", function () { + pushService.service = null; +}); + +addMessageListener("service-response", function (response) { + MockService.handleResponse(response); +}); diff --git a/dom/push/test/test_data.html b/dom/push/test/test_data.html index dbdf65c43e43..7dff75e5eb62 100644 --- a/dom/push/test/test_data.html +++ b/dom/push/test/test_data.html @@ -42,7 +42,7 @@ http://creativecommons.org/licenses/publicdomain/ var registration; add_task(function* start() { - yield setupPrefsAndMock(mockSocket); + yield setupPrefsAndMockSocket(mockSocket); yield setPushPermission(true); var url = "worker.js" + "?" + (Math.random()); diff --git a/dom/push/test/test_multiple_register.html b/dom/push/test/test_multiple_register.html index c36067904faa..b9444bf33f1f 100644 --- a/dom/push/test/test_multiple_register.html +++ b/dom/push/test/test_multiple_register.html @@ -119,7 +119,7 @@ http://creativecommons.org/licenses/publicdomain/ }).then(SimpleTest.finish); } - setupPrefsAndMock(new MockWebSocket()).then(_ => runTest()); + setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest()); SpecialPowers.addPermission("desktop-notification", true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_multiple_register_different_scope.html b/dom/push/test/test_multiple_register_different_scope.html index d068844b9dca..a0eb3ef5f31f 100644 --- a/dom/push/test/test_multiple_register_different_scope.html +++ b/dom/push/test/test_multiple_register_different_scope.html @@ -114,7 +114,7 @@ http://creativecommons.org/licenses/publicdomain/ }).then(SimpleTest.finish); } - setupPrefsAndMock(new MockWebSocket()).then(_ => runTest()); + setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest()); SpecialPowers.addPermission("desktop-notification", true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_multiple_register_during_service_activation.html b/dom/push/test/test_multiple_register_during_service_activation.html index 810560bee2b8..d07f3091e8ca 100644 --- a/dom/push/test/test_multiple_register_during_service_activation.html +++ b/dom/push/test/test_multiple_register_during_service_activation.html @@ -57,8 +57,8 @@ http://creativecommons.org/licenses/publicdomain/ function setupMultipleSubscriptions(swr) { // We need to do this to restart service so that a queue will be formed. - teardownMockPushService(); - setupMockPushService(new MockWebSocket()); + teardownMockPushSocket(); + setupMockPushSocket(new MockWebSocket()); return Promise.all([ subscribe(swr), @@ -99,7 +99,7 @@ http://creativecommons.org/licenses/publicdomain/ }).then(SimpleTest.finish); } - setupPrefsAndMock(new MockWebSocket()).then(_ => runTest()); + setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest()); SpecialPowers.addPermission("desktop-notification", true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_permissions.html b/dom/push/test/test_permissions.html index 299d5049837b..c0b50376f280 100644 --- a/dom/push/test/test_permissions.html +++ b/dom/push/test/test_permissions.html @@ -31,7 +31,7 @@ http://creativecommons.org/licenses/publicdomain/ var registration; add_task(function* start() { - yield setupPrefsAndMock(new MockWebSocket()); + yield setupPrefsAndMockSocket(new MockWebSocket()); yield setPushPermission(false); var url = "worker.js" + "?" + Math.random(); diff --git a/dom/push/test/test_register.html b/dom/push/test/test_register.html index 31925dfee4fc..a6680fb1ec5b 100644 --- a/dom/push/test/test_register.html +++ b/dom/push/test/test_register.html @@ -46,7 +46,7 @@ http://creativecommons.org/licenses/publicdomain/ var registration; add_task(function* start() { - yield setupPrefsAndMock(mockSocket); + yield setupPrefsAndMockSocket(mockSocket); yield setPushPermission(true); var url = "worker.js" + "?" + (Math.random()); diff --git a/dom/push/test/test_serviceworker_lifetime.html b/dom/push/test/test_serviceworker_lifetime.html index 369f15582cac..ce61c1cf6d50 100644 --- a/dom/push/test/test_serviceworker_lifetime.html +++ b/dom/push/test/test_serviceworker_lifetime.html @@ -335,7 +335,7 @@ }).then(SimpleTest.finish); } - setupPrefsAndMock(mockSocket).then(_ => runTest()); + setupPrefsAndMockSocket(mockSocket).then(_ => runTest()); SpecialPowers.addPermission('desktop-notification', true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_subscription_change.html b/dom/push/test/test_subscription_change.html index a5814c25cff8..9b72ce6108b0 100644 --- a/dom/push/test/test_subscription_change.html +++ b/dom/push/test/test_subscription_change.html @@ -27,7 +27,7 @@ http://creativecommons.org/licenses/publicdomain/ var registration; add_task(function* start() { - yield setupPrefsAndMock(new MockWebSocket()); + yield setupPrefsAndMockSocket(new MockWebSocket()); yield setPushPermission(true); var url = "worker.js" + "?" + (Math.random()); diff --git a/dom/push/test/test_try_registering_offline_disabled.html b/dom/push/test/test_try_registering_offline_disabled.html index e2a801838c40..625a18b0ce23 100644 --- a/dom/push/test/test_try_registering_offline_disabled.html +++ b/dom/push/test/test_try_registering_offline_disabled.html @@ -296,7 +296,7 @@ http://creativecommons.org/licenses/publicdomain/ .then(SimpleTest.finish); } - setupPrefsAndMock(new MockWebSocket()).then(_ => runTest()); + setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest()); SpecialPowers.addPermission("desktop-notification", true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_unregister.html b/dom/push/test/test_unregister.html index c8e1f7631a8e..6ce0b18cb2f6 100644 --- a/dom/push/test/test_unregister.html +++ b/dom/push/test/test_unregister.html @@ -80,7 +80,7 @@ http://creativecommons.org/licenses/publicdomain/ }).then(SimpleTest.finish); } - setupPrefsAndMock(new MockWebSocket()).then(_ => runTest()); + setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest()); SpecialPowers.addPermission("desktop-notification", true, document); SimpleTest.waitForExplicitFinish(); diff --git a/dom/push/test/test_utils.js b/dom/push/test/test_utils.js index b109ec881863..6fbcdd00080a 100644 --- a/dom/push/test/test_utils.js +++ b/dom/push/test/test_utils.js @@ -4,23 +4,62 @@ let url = SimpleTest.getTestFileURL("mockpushserviceparent.js"); let chromeScript = SpecialPowers.loadChromeScript(url); + /** + * Replaces `PushService.jsm` with a mock implementation that handles requests + * from the DOM API. This allows tests to simulate local errors and error + * reporting, bypassing the `PushService.jsm` machinery. + */ + function replacePushService(mockService) { + chromeScript.sendSyncMessage("service-replace"); + chromeScript.addMessageListener("service-request", function(msg) { + let promise; + try { + let handler = mockService[msg.name]; + promise = Promise.resolve(handler(msg.params)); + } catch (error) { + promise = Promise.reject(error); + } + promise.then(result => { + chromeScript.sendAsyncMessage("service-response", { + id: msg.id, + result: result, + }); + }, error => { + chromeScript.sendAsyncMessage("service-response", { + id: msg.id, + error: error, + }); + }); + }); + } + + function restorePushService() { + chromeScript.sendSyncMessage("service-restore"); + } + let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8"; let currentMockSocket = null; - function setupMockPushService(mockWebSocket) { + /** + * Sets up a mock connection for the WebSocket backend. This only replaces + * the transport layer; `PushService.jsm` still handles DOM API requests, + * observes permission changes, writes to IndexedDB, and notifies service + * workers of incoming push messages. + */ + function setupMockPushSocket(mockWebSocket) { currentMockSocket = mockWebSocket; currentMockSocket._isActive = true; - chromeScript.sendSyncMessage("setup"); - chromeScript.addMessageListener("client-msg", function(msg) { + chromeScript.sendSyncMessage("socket-setup"); + chromeScript.addMessageListener("socket-client-msg", function(msg) { mockWebSocket.handleMessage(msg); }); } - function teardownMockPushService() { + function teardownMockPushSocket() { if (currentMockSocket) { currentMockSocket._isActive = false; - chromeScript.sendSyncMessage("teardown"); + chromeScript.sendSyncMessage("socket-teardown"); } } @@ -90,14 +129,16 @@ serverSendMsg(msg) { if (this._isActive) { - chromeScript.sendAsyncMessage("server-msg", msg); + chromeScript.sendAsyncMessage("socket-server-msg", msg); } }, }; g.MockWebSocket = MockWebSocket; - g.setupMockPushService = setupMockPushService; - g.teardownMockPushService = teardownMockPushService; + g.setupMockPushSocket = setupMockPushSocket; + g.teardownMockPushSocket = teardownMockPushSocket; + g.replacePushService = replacePushService; + g.restorePushService = restorePushService; }(this)); // Remove permissions and prefs when the test finishes. @@ -107,7 +148,8 @@ SimpleTest.registerCleanupFunction(() => { SpecialPowers.flushPrefEnv(resolve); }); }).then(_ => { - teardownMockPushService(); + teardownMockPushSocket(); + restorePushService(); }); }); @@ -119,9 +161,8 @@ function setPushPermission(allow) { }); } -function setupPrefsAndMock(mockSocket) { +function setupPrefs() { return new Promise(resolve => { - setupMockPushService(mockSocket); SpecialPowers.pushPrefEnv({"set": [ ["dom.push.enabled", true], ["dom.push.connection.enabled", true], @@ -132,6 +173,16 @@ function setupPrefsAndMock(mockSocket) { }); } +function setupPrefsAndReplaceService(mockService) { + replacePushService(mockService); + return setupPrefs(); +} + +function setupPrefsAndMockSocket(mockSocket) { + setupMockPushSocket(mockSocket); + return setupPrefs(); +} + function injectControlledFrame(target = document.body) { return new Promise(function(res, rej) { var iframe = document.createElement("iframe"); diff --git a/dom/push/test/xpcshell/head.js b/dom/push/test/xpcshell/head.js index 4d8066b0adff..6847e0fc1756 100644 --- a/dom/push/test/xpcshell/head.js +++ b/dom/push/test/xpcshell/head.js @@ -22,8 +22,6 @@ XPCOMUtils.defineLazyServiceGetter(this, 'PushServiceComponent', const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {}); const servicePrefs = new Preferences('dom.push.'); -const DEFAULT_TIMEOUT = 5000; - const WEBSOCKET_CLOSE_GOING_AWAY = 1001; var isParent = Cc['@mozilla.org/xre/runtime;1'] @@ -98,31 +96,6 @@ function promiseObserverNotification(topic, matchFunc) { }); } -/** - * Waits for a promise to settle. Returns a rejected promise if the promise - * is not resolved or rejected within the given delay. - * - * @param {Promise} promise The pending promise. - * @param {Number} delay The time to wait before rejecting the promise. - * @param {String} [message] The rejection message if the promise times out. - * @returns {Promise} A promise that settles with the value of the pending - * promise, or rejects if the pending promise times out. - */ -function waitForPromise(promise, delay, message = 'Timed out waiting on promise') { - let timeoutDefer = Promise.defer(); - let id = setTimeout(() => timeoutDefer.reject(new Error(message)), delay); - return Promise.race([ - promise.then(value => { - clearTimeout(id); - return value; - }, error => { - clearTimeout(id); - throw error; - }), - timeoutDefer.promise - ]); -} - /** * Wraps an object in a proxy that traps property gets and returns stubs. If * the stub is a function, the original value will be passed as the first diff --git a/dom/push/test/xpcshell/test_clear_origin_data.js b/dom/push/test/xpcshell/test_clear_origin_data.js index 6221f3050ae2..c81c1ced6f66 100644 --- a/dom/push/test/xpcshell/test_clear_origin_data.js +++ b/dom/push/test/xpcshell/test_clear_origin_data.js @@ -137,6 +137,5 @@ add_task(function* test_webapps_cleardata() { yield clearForPattern(testRecords, { inIsolatedMozBrowser: true }); equal(testRecords.length, 0, 'Should remove all test records'); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister'); + yield unregisterPromise; }); diff --git a/dom/push/test/xpcshell/test_crypto.js b/dom/push/test/xpcshell/test_crypto.js new file mode 100644 index 000000000000..58410020c1ed --- /dev/null +++ b/dom/push/test/xpcshell/test_crypto.js @@ -0,0 +1,245 @@ +'use strict'; + +const { + base64UrlDecode, + getCryptoParams, + PushCrypto, +} = Cu.import('resource://gre/modules/PushCrypto.jsm', {}); + +function run_test() { + run_next_test(); +} + +add_task(function* test_crypto_getCryptoParams() { + let testData = [ + // These headers should parse correctly. + { + desc: 'aesgcm with multiple keys', + headers: { + encoding: 'aesgcm', + crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI', + encryption: 'keyid=p256dh;salt=upk1yFkp1xI', + }, + params: { + dh: 'Iy1Je2Kv11A', + salt: 'upk1yFkp1xI', + rs: 4096, + auth: true, + padSize: 2, + }, + }, { + desc: 'aesgcm with quoted key param', + headers: { + encoding: 'aesgcm', + crypto_key: 'dh="byfHbUffc-k"', + encryption: 'salt=C11AvAsp6Gc', + }, + params: { + dh: 'byfHbUffc-k', + salt: 'C11AvAsp6Gc', + rs: 4096, + auth: true, + padSize: 2, + }, + }, { + desc: 'aesgcm with Crypto-Key and rs = 24', + headers: { + encoding: 'aesgcm', + crypto_key: 'dh="ybuT4VDz-Bg"', + encryption: 'salt=H7U7wcIoIKs; rs=24', + }, + params: { + dh: 'ybuT4VDz-Bg', + salt: 'H7U7wcIoIKs', + rs: 24, + auth: true, + padSize: 2, + }, + }, { + desc: 'aesgcm128 with Encryption-Key and rs = 2', + headers: { + encoding: 'aesgcm128', + encryption_key: 'keyid=legacy; dh=LqrDQuVl9lY', + encryption: 'keyid=legacy; salt=YngI8B7YapM; rs=2', + }, + params: { + dh: 'LqrDQuVl9lY', + salt: 'YngI8B7YapM', + rs: 2, + auth: false, + padSize: 1, + }, + }, { + desc: 'aesgcm128 with Encryption-Key', + headers: { + encoding: 'aesgcm128', + encryption_key: 'keyid=v2; dh=VA6wmY1IpiE', + encryption: 'keyid=v2; salt=khtpyXhpDKM', + }, + params: { + dh: 'VA6wmY1IpiE', + salt: 'khtpyXhpDKM', + rs: 4096, + auth: false, + padSize: 1, + } + }, { + desc: 'aesgcm128 with Crypto-Key', + headers: { + encoding: 'aesgcm128', + crypto_key: 'keyid=v2; dh=VA6wmY1IpiE', + encryption: 'keyid=v2; salt=F0Im7RtGgNY', + }, + params: { + dh: 'VA6wmY1IpiE', + salt: 'F0Im7RtGgNY', + rs: 4096, + auth: true, + padSize: 1, + }, + }, + + // These headers should be rejected. + { + desc: 'Invalid encoding', + headers: { + encoding: 'nonexistent', + }, + params: null, + }, { + desc: 'Invalid record size', + headers: { + encoding: 'aesgcm', + crypto_key: 'dh=pbmv1QkcEDY', + encryption: 'dh=Esao8aTBfIk;rs=bad', + }, + params: null, + }, { + desc: 'Insufficiently large record size', + headers: { + encoding: 'aesgcm', + crypto_key: 'dh=fK0EXaw5IU8', + encryption: 'salt=orbLLmlbJfM;rs=1', + }, + params: null, + }, { + desc: 'aesgcm with Encryption-Key', + headers: { + encoding: 'aesgcm', + encryption_key: 'dh=FplK5KkvUF0', + encryption: 'salt=p6YHhFF3BQY', + }, + params: null, + }]; + + for (let test of testData) { + let params = getCryptoParams(test.headers); + deepEqual(params, test.params, test.desc); + } +}); + +add_task(function* test_crypto_decodeMsg() { + let privateKey = { + "crv": "P-256", + "d": "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg", + "ext": true, + "key_ops": ["deriveBits"], + "kty":"EC", + "x":"sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4", + "y":"vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs", + }; + let publicKey = base64UrlDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs'); + + let expectedSuccesses = [{ + desc: 'padSize = 2, rs = 24, pad = 0', + result: 'Some message', + data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU', + senderPublicKey: 'BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo', + salt: 'zCU18Rw3A5aB_Xi-vfixmA', + rs: 24, + authSecret: 'aTDc6JebzR6eScy2oLo4RQ', + padSize: 2, + }, { + desc: 'padSize = 2, rs = 8, pad = 16', + result: 'Yet another message', + data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg', + senderPublicKey: 'BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ', + salt: 'ZFhzj0S-n29g9P2p4-I7tA', + rs: 8, + authSecret: '6plwZnSpVUbF7APDXus3UQ', + padSize: 2, + }, { + desc: 'padSize = 1, rs = 4096, pad = 2', + result: 'aesgcm128 encrypted message', + data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE', + senderPublicKey: 'BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI', + salt: 'btxxUtclbmgcc30b9rT3Bg', + rs: 4096, + padSize: 1, + }, { + desc: 'padSize = 2, rs = 3, pad = 0', + result: 'Small record size', + data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM', + senderPublicKey: 'BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk', + salt: '5LIDBXbvkBvvb7ZdD-T4PQ', + rs: 3, + authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ', + padSize: 2, + }, { + desc: 'padSize = 1, rs = 4096, auth secret, pad = 8', + result: 'aesgcm128 with auth secret', + data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r', + senderPublicKey: 'BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM', + salt: 'aGBpoKklLtrLcAUCcCr7JQ', + rs: 4096, + authSecret: 'Sxb6u0gJIhGEogyLawjmCw', + padSize: 1, + }]; + for (let test of expectedSuccesses) { + let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null; + let result = yield PushCrypto.decodeMsg(base64UrlDecode(test.data), + privateKey, publicKey, + test.senderPublicKey, test.salt, + test.rs, authSecret, test.padSize); + let decoder = new TextDecoder('utf-8'); + equal(decoder.decode(new Uint8Array(result)), test.result, test.desc); + } + + let expectedFailures = [{ + desc: 'Missing padding', + data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg', + senderPublicKey: 'BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4', + salt: 'Czx2i18rar8XWOXAVDnUuw', + rs: 4096, + padSize: 1, + }, { + desc: 'padSize > rs', + data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu', + senderPublicKey: 'BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls', + salt: 'NQVTKhB0rpL7ZzKkotTGlA', + rs: 1, + authSecret: '6plwZnSpVUbF7APDXus3UQ', + padSize: 2, + }, { + desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret', + data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8', + senderPublicKey: 'BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0', + salt: 'c6JQl9eJ0VvwrUVCQDxY7Q', + rs: 4096, + authSecret: 'BhbpNTWyO5wVJmVKTV6XaA', + padSize: 2, + }, { + desc: 'Truncated input', + data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0', + rs: 25, + }]; + for (let test of expectedFailures) { + let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null; + yield rejects( + PushCrypto.decodeMsg(base64UrlDecode(test.data), privateKey, publicKey, + test.senderPublicKey, test.salt, test.rs, + authSecret, test.padSize), + test.desc + ); + } +}); diff --git a/dom/push/test/xpcshell/test_drop_expired.js b/dom/push/test/xpcshell/test_drop_expired.js index 2258e797e610..a5502235d555 100644 --- a/dom/push/test/xpcshell/test_drop_expired.js +++ b/dom/push/test/xpcshell/test_drop_expired.js @@ -120,8 +120,7 @@ add_task(function* setUp() { }, }); - yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for subscription change event on startup'); + yield subChangePromise; }); add_task(function* test_site_visited() { @@ -133,8 +132,7 @@ add_task(function* test_site_visited() { yield visitURI(quotaURI, Date.now()); PushService.observe(null, 'idle-daily', ''); - yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for subscription change event after visit'); + yield subChangePromise; }); add_task(function* test_perm_restored() { @@ -146,6 +144,5 @@ add_task(function* test_perm_restored() { Services.perms.add(permURI, 'desktop-notification', Ci.nsIPermissionManager.ALLOW_ACTION); - yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for subscription change event after permission'); + yield subChangePromise; }); diff --git a/dom/push/test/xpcshell/test_notification_ack.js b/dom/push/test/xpcshell/test_notification_ack.js index 5ad1ffbdd6af..092a7c38d3fd 100644 --- a/dom/push/test/xpcshell/test_notification_ack.js +++ b/dom/push/test/xpcshell/test_notification_ack.js @@ -118,8 +118,6 @@ add_task(function* test_notification_ack() { } }); - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); - yield waitForPromise(ackPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for multiple acknowledgements'); + yield notifyPromise; + yield ackPromise; }); diff --git a/dom/push/test/xpcshell/test_notification_data.js b/dom/push/test/xpcshell/test_notification_data.js index 74b678488a87..275b3df0e431 100644 --- a/dom/push/test/xpcshell/test_notification_data.js +++ b/dom/push/test/xpcshell/test_notification_data.js @@ -114,8 +114,7 @@ add_task(function* test_notification_ack_data_setup() { }); } }); - yield waitForPromise(setupDonePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); + yield setupDonePromise; }); add_task(function* test_notification_ack_data() { @@ -127,6 +126,7 @@ add_task(function* test_notification_ack_data() { headers: { encryption_key: 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"', encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"', + encoding: 'aesgcm128', }, data: 'NwrrOWPxLE8Sv5Rr0Kep7n0-r_j3rsYrUw_CXPo', version: 'v1', @@ -143,6 +143,7 @@ add_task(function* test_notification_ack_data() { headers: { encryption_key: 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"', encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"', + encoding: 'aesgcm128', }, data: 'Zt9dEdqgHlyAL_l83385aEtb98ZBilz5tgnGgmwEsl5AOCNgesUUJ4p9qUU', }, @@ -158,6 +159,7 @@ add_task(function* test_notification_ack_data() { headers: { encryption_key: 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"', encryption: 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24', + encoding: 'aesgcm128', }, data: 'LKru3ZzxBZuAxYtsaCfaj_fehkrIvqbVd1iSwnwAUgnL-cTeDD-83blxHXTq7r0z9ydTdMtC3UjAcWi8LMnfY-BFzi0qJAjGYIikDA', }, @@ -175,6 +177,7 @@ add_task(function* test_notification_ack_data() { headers: { crypto_key: 'keyid=v4;dh="BHqG01j7rOfp12BEDzxWXxlCaU4cdOx2DZAwCt3QuzEsnXN9lCna9QmZCkVpXsx7sAlaEmtl_VfF1lHlFS7XWcA"', encryption: 'keyid="v4";salt="X5-iy5rzhm4naNmMHdSYJw"', + encoding: 'aesgcm128', }, data: '7YlxyNlZsNX4UNknHxzTqFrcrzz58W95uXBa0iY', }, @@ -183,6 +186,23 @@ add_task(function* test_notification_ack_data() { data: 'Some message' } }, + // A message encoded with `aesgcm` (2 bytes of padding). + { + channelID: 'subscription1', + version: 'v5', + send: { + headers: { + crypto_key: 'dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"', + encryption: 'salt="C14Wb7rQTlXzrgcPHtaUzw"', + encoding: 'aesgcm', + }, + data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe', + }, + receive: { + scope: 'https://example.com/page/1', + data: 'Another message' + } + }, // A message with 17 bytes of padding and rs of 24 { channelID: 'subscription2', @@ -191,6 +211,7 @@ add_task(function* test_notification_ack_data() { headers: { crypto_key: 'keyid="v5"; dh="BJhyKIH5P30YUKn1bolj_LMnael1-KZT_aGXgD2CRspBfv9gcUhVAmpxToZrw7QQEKl9K83b3zcqNY6G_dFhEsI"', encryption: 'keyid=v5;salt="bLmqCy550eK1Ao41tD7orA";rs=24', + encoding: 'aesgcm128', }, data: 'SQDlDg1ftLkM_ruZlmyB2bk9L78HYtkcbA-y4-uAxwL-G4KtOA-J-A_rJ007Vi6NUkQe9K4kSZeIBrIUpmGv', }, @@ -207,6 +228,7 @@ add_task(function* test_notification_ack_data() { headers: { crypto_key: 'dh="BEgnDmVw9Gcn1fWA5t53Jtpsgfewk_pzsjSc_PBPpPmROWGQA2v8ESrSsQgosNXx0o-uMMhi9tDAUeks3380kd8"', encryption: 'salt=T9DM8bNxuMHRVTn4LzkJDQ', + encoding: 'aesgcm128', }, data: '7KUCi0dBBJbWmsYTqEqhFrgTv4ZOo_BmQRQ_2kY', }, @@ -244,10 +266,7 @@ add_task(function* test_notification_ack_data() { return Promise.all([messageReceived, ackReceived]); }; - yield waitForPromise( - allTestData.reduce((p, testData) => { - return p.then(_ => sendAndReceive(testData)); - }, Promise.resolve()), - DEFAULT_TIMEOUT, - 'Timed out waiting for message exchange to complete'); + yield allTestData.reduce((p, testData) => { + return p.then(_ => sendAndReceive(testData)); + }, Promise.resolve()); }); diff --git a/dom/push/test/xpcshell/test_notification_duplicate.js b/dom/push/test/xpcshell/test_notification_duplicate.js index 07b984341cbd..83178d55b05a 100644 --- a/dom/push/test/xpcshell/test_notification_duplicate.js +++ b/dom/push/test/xpcshell/test_notification_duplicate.js @@ -73,10 +73,8 @@ add_task(function* test_notification_duplicate() { } }); - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); - yield waitForPromise(ackPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for stale acknowledgement'); + yield notifyPromise; + yield ackPromise; let staleRecord = yield db.getByKeyID( '8d2d9400-3597-4c5a-8a38-c546b0043bcc'); diff --git a/dom/push/test/xpcshell/test_notification_error.js b/dom/push/test/xpcshell/test_notification_error.js index d0cd12fbf982..4a82c4152741 100644 --- a/dom/push/test/xpcshell/test_notification_error.js +++ b/dom/push/test/xpcshell/test_notification_error.js @@ -87,18 +87,13 @@ add_task(function* test_notification_error() { } }); - yield waitForPromise( - notifyPromise, - DEFAULT_TIMEOUT, - 'Timed out waiting for notifications' - ); + yield notifyPromise; ok(scopes.includes('https://example.com/a'), 'Missing scope for notification A'); ok(scopes.includes('https://example.com/c'), 'Missing scope for notification C'); - yield waitForPromise(ackPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for acknowledgements'); + yield ackPromise; let aRecord = yield db.getByIdentifiers({scope: 'https://example.com/a', originAttributes: originAttributes }); diff --git a/dom/push/test/xpcshell/test_notification_http2.js b/dom/push/test/xpcshell/test_notification_http2.js index bbaefafb294a..5657815e2f02 100644 --- a/dom/push/test/xpcshell/test_notification_http2.js +++ b/dom/push/test/xpcshell/test_notification_http2.js @@ -6,6 +6,7 @@ Cu.import("resource://gre/modules/Services.jsm"); const {PushDB, PushService, PushServiceHttp2} = serviceExports; +const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {}); var prefs; var tlsProfile; @@ -56,6 +57,8 @@ add_task(function* test_pushNotifications() { // length 16. // /pushNotifications/subscription3 will send a message with rs equal 24 and // padding length 16. + // /pushNotifications/subscription4 will send a message with no rs and padding + // length 256. let db = PushServiceHttp2.newPushDB(); do_register_cleanup(() => { @@ -121,6 +124,26 @@ add_task(function* test_pushNotifications() { { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), quota: Infinity, systemRecord: true, + }, { + subscriptionUri: serverURL + '/pushNotifications/subscription4', + pushEndpoint: serverURL + '/pushEndpoint4', + pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4', + scope: 'https://example.com/page/4', + p256dhPublicKey: base64UrlDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'), + p256dhPrivateKey: { + crv: 'P-256', + d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw', + ext: true, + key_ops: ["deriveBits"], + kty: 'EC', + x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ', + y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU' + }, + authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'), + originAttributes: ChromeUtils.originAttributesToSuffix( + { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), + quota: Infinity, + systemRecord: true, }]; for (let record of records) { @@ -148,7 +171,14 @@ add_task(function* test_pushNotifications() { equal(message.text(), "Some message", "decoded message is incorrect"); return true; } - }) + }), + promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) { + var message = subject.QueryInterface(Ci.nsIPushMessage); + if (message && (data == "https://example.com/page/4")){ + equal(message.text(), "Yet another message", "decoded message is incorrect"); + return true; + } + }), ]); PushService.init({ @@ -156,8 +186,7 @@ add_task(function* test_pushNotifications() { db }); - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); + yield notifyPromise; }); add_task(function* test_complete() { diff --git a/dom/push/test/xpcshell/test_notification_incomplete.js b/dom/push/test/xpcshell/test_notification_incomplete.js index 9f2c8dc1a51a..3b73d0d8d968 100644 --- a/dom/push/test/xpcshell/test_notification_incomplete.js +++ b/dom/push/test/xpcshell/test_notification_incomplete.js @@ -108,8 +108,7 @@ add_task(function* test_notification_incomplete() { } }); - yield waitForPromise(notificationPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for incomplete notifications'); + yield notificationPromise; let storeRecords = yield db.getAllKeyIDs(); storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) => diff --git a/dom/push/test/xpcshell/test_notification_version_string.js b/dom/push/test/xpcshell/test_notification_version_string.js index 196a7d9f864e..fa02d6d006f9 100644 --- a/dom/push/test/xpcshell/test_notification_version_string.js +++ b/dom/push/test/xpcshell/test_notification_version_string.js @@ -57,15 +57,10 @@ add_task(function* test_notification_version_string() { } }); - let {subject: notification, data: scope} = yield waitForPromise( - notifyPromise, - DEFAULT_TIMEOUT, - 'Timed out waiting for string notification' - ); + let {subject: notification, data: scope} = yield notifyPromise; equal(notification, null, 'Unexpected data for Simple Push message'); - yield waitForPromise(ackPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for string acknowledgement'); + yield ackPromise; let storeRecord = yield db.getByKeyID( '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b'); diff --git a/dom/push/test/xpcshell/test_permissions.js b/dom/push/test/xpcshell/test_permissions.js index 3f9137add454..4fd8c0cfd2f3 100644 --- a/dom/push/test/xpcshell/test_permissions.js +++ b/dom/push/test/xpcshell/test_permissions.js @@ -124,8 +124,7 @@ add_task(function* setUp() { }); } }); - yield waitForPromise(handshakePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for handshake'); + yield handshakePromise; }); add_task(function* test_permissions_allow_added() { @@ -135,8 +134,7 @@ add_task(function* test_permissions_allow_added() { makePushPermission('https://example.info', 'ALLOW_ACTION'), 'added' ); - let notifiedScopes = yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications after adding allow'); + let notifiedScopes = yield subChangePromise; deepEqual(notifiedScopes, [ 'https://example.info/page/2', @@ -159,8 +157,7 @@ add_task(function* test_permissions_allow_deleted() { 'deleted' ); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister after deleting allow'); + yield unregisterPromise; let record = yield db.getByKeyID('active-allow'); ok(record.isExpired(), @@ -179,8 +176,7 @@ add_task(function* test_permissions_deny_added() { makePushPermission('https://example.net', 'DENY_ACTION'), 'added' ); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications after adding deny'); + yield unregisterPromise; let isExpired = yield allExpired( 'active-deny-added-1', @@ -210,8 +206,7 @@ add_task(function* test_permissions_allow_changed() { 'changed' ); - let notifiedScopes = yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications after changing to allow'); + let notifiedScopes = yield subChangePromise; deepEqual(notifiedScopes, [ 'https://example.net/eggs', @@ -237,8 +232,7 @@ add_task(function* test_permissions_deny_changed() { 'changed' ); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister after changing to deny'); + yield unregisterPromise; let record = yield db.getByKeyID('active-deny-changed'); ok(record.isExpired(), @@ -259,8 +253,7 @@ add_task(function* test_permissions_clear() { yield PushService._onPermissionChange(null, 'cleared'); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister requests after clearing permissions'); + yield unregisterPromise; records = yield db.getAllKeyIDs(); deepEqual(records.map(record => record.keyID).sort(), [ diff --git a/dom/push/test/xpcshell/test_quota_exceeded.js b/dom/push/test/xpcshell/test_quota_exceeded.js index 1e024f2cb9a9..45c5897357e2 100644 --- a/dom/push/test/xpcshell/test_quota_exceeded.js +++ b/dom/push/test/xpcshell/test_quota_exceeded.js @@ -132,11 +132,9 @@ add_task(function* test_expiration_origin_threshold() { }, }); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister request'); + yield unregisterPromise; - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); + yield notifyPromise; let expiredRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349'); strictEqual(expiredRecord.quota, 0, 'Expired record not updated'); diff --git a/dom/push/test/xpcshell/test_quota_observer.js b/dom/push/test/xpcshell/test_quota_observer.js index 5c584c720597..fbd0cd2577ee 100644 --- a/dom/push/test/xpcshell/test_quota_observer.js +++ b/dom/push/test/xpcshell/test_quota_observer.js @@ -98,10 +98,8 @@ add_task(function* test_expiration_history_observer() { } }); - yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for subscription change event on startup'); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister request'); + yield subChangePromise; + yield unregisterPromise; let expiredRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9'); strictEqual(expiredRecord.quota, 0, 'Expired record not updated'); @@ -134,8 +132,7 @@ add_task(function* test_expiration_history_observer() { Services.obs.notifyObservers(null, 'idle-daily', ''); // And we should receive notifications for both scopes. - yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT, - 'Timed out waiting for subscription change events'); + yield subChangePromise; deepEqual(notifiedScopes.sort(), [ 'https://example.com/auctions', 'https://example.com/deals' diff --git a/dom/push/test/xpcshell/test_quota_with_notification.js b/dom/push/test/xpcshell/test_quota_with_notification.js index f4bdf80b6a6c..a38efb6dbe1f 100644 --- a/dom/push/test/xpcshell/test_quota_with_notification.js +++ b/dom/push/test/xpcshell/test_quota_with_notification.js @@ -103,11 +103,9 @@ add_task(function* test_expiration_origin_threshold() { }, }); - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); + yield notifyPromise; - yield waitForPromise(updateQuotaPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for quota to be updated'); + yield updateQuotaPromise; let expiredRecord = yield db.getByKeyID('f56645a9-1f32-4655-92ad-ddc37f6d54fb'); notStrictEqual(expiredRecord.quota, 0, 'Expired record not updated'); diff --git a/dom/push/test/xpcshell/test_reconnect_retry.js b/dom/push/test/xpcshell/test_reconnect_retry.js index 8e7080f68efa..7489181cfff9 100644 --- a/dom/push/test/xpcshell/test_reconnect_retry.js +++ b/dom/push/test/xpcshell/test_reconnect_retry.js @@ -68,7 +68,7 @@ add_task(function* test_reconnect_retry() { originAttributes: ChromeUtils.originAttributesToSuffix( { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), }); - notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request') + notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request'); equal(registers, 3, 'Wrong registration count'); }); diff --git a/dom/push/test/xpcshell/test_register_case.js b/dom/push/test/xpcshell/test_register_case.js index e08ce4785500..af915d8d88f4 100644 --- a/dom/push/test/xpcshell/test_register_case.js +++ b/dom/push/test/xpcshell/test_register_case.js @@ -43,15 +43,11 @@ add_task(function* test_register_case() { } }); - let newRecord = yield waitForPromise( - PushService.register({ - scope: 'https://example.net/case', - originAttributes: ChromeUtils.originAttributesToSuffix( - { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), - }), - DEFAULT_TIMEOUT, - 'Mixed-case register response timed out' - ); + let newRecord = yield PushService.register({ + scope: 'https://example.net/case', + originAttributes: ChromeUtils.originAttributesToSuffix( + { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), + }); equal(newRecord.endpoint, 'https://example.com/update/case', 'Wrong push endpoint in registration record'); diff --git a/dom/push/test/xpcshell/test_register_flush.js b/dom/push/test/xpcshell/test_register_flush.js index 8ff7fe8e2e76..8c0db449410b 100644 --- a/dom/push/test/xpcshell/test_register_flush.js +++ b/dom/push/test/xpcshell/test_register_flush.js @@ -80,12 +80,10 @@ add_task(function* test_register_flush() { equal(newRecord.endpoint, 'https://example.org/update/2', 'Wrong push endpoint in record'); - let {data: scope} = yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notification'); + let {data: scope} = yield notifyPromise; equal(scope, 'https://example.com/page/1', 'Wrong notification scope'); - yield waitForPromise(ackPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for acknowledgements'); + yield ackPromise; let prevRecord = yield db.getByKeyID( '9bcc7efb-86c7-4457-93ea-e24e6eb59b74'); diff --git a/dom/push/test/xpcshell/test_register_invalid_json.js b/dom/push/test/xpcshell/test_register_invalid_json.js index 98cb405bd559..c3c2fe28003e 100644 --- a/dom/push/test/xpcshell/test_register_invalid_json.js +++ b/dom/push/test/xpcshell/test_register_invalid_json.js @@ -54,7 +54,6 @@ add_task(function* test_register_invalid_json() { 'Expected error for invalid JSON response' ); - yield waitForPromise(helloPromise, DEFAULT_TIMEOUT, - 'Reconnect after invalid JSON response timed out'); + yield helloPromise; equal(registers, 1, 'Wrong register count'); }); diff --git a/dom/push/test/xpcshell/test_register_no_id.js b/dom/push/test/xpcshell/test_register_no_id.js index edb2b2ad2246..c0d3edbac746 100644 --- a/dom/push/test/xpcshell/test_register_no_id.js +++ b/dom/push/test/xpcshell/test_register_no_id.js @@ -58,7 +58,6 @@ add_task(function* test_register_no_id() { 'Expected error for incomplete register response' ); - yield waitForPromise(helloPromise, DEFAULT_TIMEOUT, - 'Reconnect after incomplete register response timed out'); + yield helloPromise; equal(registers, 1, 'Wrong register count'); }); diff --git a/dom/push/test/xpcshell/test_register_request_queue.js b/dom/push/test/xpcshell/test_register_request_queue.js index 43a1bbc57d03..02d3b3cbf73c 100644 --- a/dom/push/test/xpcshell/test_register_request_queue.js +++ b/dom/push/test/xpcshell/test_register_request_queue.js @@ -53,11 +53,10 @@ add_task(function* test_register_request_queue() { { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }), }); - yield waitForPromise(Promise.all([ + yield Promise.all([ rejects(firstRegister, 'Should time out the first request'), rejects(secondRegister, 'Should time out the second request') - ]), DEFAULT_TIMEOUT, 'Queued requests did not time out'); + ]); - yield waitForPromise(helloPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for reconnect'); + yield helloPromise; }); diff --git a/dom/push/test/xpcshell/test_register_rollback.js b/dom/push/test/xpcshell/test_register_rollback.js index 20e379bc234f..aa1ddaf8bb9b 100644 --- a/dom/push/test/xpcshell/test_register_rollback.js +++ b/dom/push/test/xpcshell/test_register_rollback.js @@ -81,8 +81,7 @@ add_task(function* test_register_rollback() { ); // Should send an out-of-band unregister request. - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Unregister request timed out'); + yield unregisterPromise; equal(handshakes, 1, 'Wrong handshake count'); equal(registers, 1, 'Wrong register count'); }); diff --git a/dom/push/test/xpcshell/test_register_timeout.js b/dom/push/test/xpcshell/test_register_timeout.js index 7e79a66baf5e..7752c74b8e09 100644 --- a/dom/push/test/xpcshell/test_register_timeout.js +++ b/dom/push/test/xpcshell/test_register_timeout.js @@ -83,10 +83,6 @@ add_task(function* test_register_timeout() { let record = yield db.getByKeyID(channelID); ok(!record, 'Should not store records for timed-out responses'); - yield waitForPromise( - timeoutPromise, - DEFAULT_TIMEOUT, - 'Reconnect timed out' - ); + yield timeoutPromise; equal(registers, 1, 'Should not handle timed-out register requests'); }); diff --git a/dom/push/test/xpcshell/test_register_wrong_id.js b/dom/push/test/xpcshell/test_register_wrong_id.js index ed58d09b3fe2..438239001e35 100644 --- a/dom/push/test/xpcshell/test_register_wrong_id.js +++ b/dom/push/test/xpcshell/test_register_wrong_id.js @@ -64,7 +64,6 @@ add_task(function* test_register_wrong_id() { 'Expected error for mismatched register reply' ); - yield waitForPromise(helloPromise, DEFAULT_TIMEOUT, - 'Reconnect after mismatched register reply timed out'); + yield helloPromise; equal(registers, 1, 'Wrong register count'); }); diff --git a/dom/push/test/xpcshell/test_register_wrong_type.js b/dom/push/test/xpcshell/test_register_wrong_type.js index c90e49605578..5f90b959e5f9 100644 --- a/dom/push/test/xpcshell/test_register_wrong_type.js +++ b/dom/push/test/xpcshell/test_register_wrong_type.js @@ -58,7 +58,6 @@ add_task(function* test_register_wrong_type() { 'Expected error for non-string channel ID' ); - yield waitForPromise(helloPromise, DEFAULT_TIMEOUT, - 'Reconnect after sending non-string channel ID timed out'); + yield helloPromise; equal(registers, 1, 'Wrong register count'); }); diff --git a/dom/push/test/xpcshell/test_registration_success.js b/dom/push/test/xpcshell/test_registration_success.js index 8b2daea9b442..31980c270ccc 100644 --- a/dom/push/test/xpcshell/test_registration_success.js +++ b/dom/push/test/xpcshell/test_registration_success.js @@ -64,11 +64,7 @@ add_task(function* test_registration_success() { } }); - yield waitForPromise( - handshakePromise, - DEFAULT_TIMEOUT, - 'Timed out waiting for handshake' - ); + yield handshakePromise; let registration = yield PushService.registration({ scope: 'https://example.net/a', diff --git a/dom/push/test/xpcshell/test_retry_ws.js b/dom/push/test/xpcshell/test_retry_ws.js index 51733af7a96a..32471a408331 100644 --- a/dom/push/test/xpcshell/test_retry_ws.js +++ b/dom/push/test/xpcshell/test_retry_ws.js @@ -58,11 +58,7 @@ add_task(function* test_ws_retry() { }, }); - yield waitForPromise( - handshakePromise, - 45000, - 'Timed out waiting for successful handshake' - ); + yield handshakePromise; [25, 50, 100, 200, 400, 800, 1600, 3200, 6400, 10000].forEach(function(minDelay, index) { ok(alarmDelays[index] >= minDelay, `Should wait at least ${ minDelay}ms before attempt ${index + 1}`); diff --git a/dom/push/test/xpcshell/test_unregister_error.js b/dom/push/test/xpcshell/test_unregister_error.js index c0fb29c39a20..58be1cbde61b 100644 --- a/dom/push/test/xpcshell/test_unregister_error.js +++ b/dom/push/test/xpcshell/test_unregister_error.js @@ -65,6 +65,5 @@ add_task(function* test_unregister_error() { ok(!result, 'Deleted push record exists'); // Make sure we send a request to the server. - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister'); + yield unregisterPromise; }); diff --git a/dom/push/test/xpcshell/test_unregister_invalid_json.js b/dom/push/test/xpcshell/test_unregister_invalid_json.js index b0a6cdd3b014..d59815c709dc 100644 --- a/dom/push/test/xpcshell/test_unregister_invalid_json.js +++ b/dom/push/test/xpcshell/test_unregister_invalid_json.js @@ -82,6 +82,5 @@ add_task(function* test_unregister_invalid_json() { ok(!record, 'Failed to delete unregistered record after receiving invalid JSON'); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister'); + yield unregisterPromise; }); diff --git a/dom/push/test/xpcshell/test_unregister_success.js b/dom/push/test/xpcshell/test_unregister_success.js index 886ca84e1aa8..2d7992c86dce 100644 --- a/dom/push/test/xpcshell/test_unregister_success.js +++ b/dom/push/test/xpcshell/test_unregister_success.js @@ -60,6 +60,5 @@ add_task(function* test_unregister_success() { let record = yield db.getByKeyID(channelID); ok(!record, 'Unregister did not remove record'); - yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for unregister'); + yield unregisterPromise; }); diff --git a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js index f81644d9e5fd..84923aa78728 100644 --- a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js +++ b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js @@ -67,8 +67,7 @@ add_task(function* test1() { db }); - yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT, - 'Timed out waiting for notifications'); + yield notifyPromise; let aRecord = yield db.getByKeyID(serverURL + '/subscriptionNoKey'); ok(aRecord, 'The record should still be there'); diff --git a/dom/push/test/xpcshell/xpcshell.ini b/dom/push/test/xpcshell/xpcshell.ini index 5338be3ff706..42397c0d917b 100644 --- a/dom/push/test/xpcshell/xpcshell.ini +++ b/dom/push/test/xpcshell/xpcshell.ini @@ -5,6 +5,7 @@ tail = skip-if = toolkit == 'android' [test_clear_origin_data.js] +[test_crypto.js] [test_drop_expired.js] [test_handler_service.js] support-files = PushServiceHandler.js PushServiceHandler.manifest diff --git a/dom/security/nsContentSecurityManager.cpp b/dom/security/nsContentSecurityManager.cpp index 6cf82cc73875..aa004c02fad2 100644 --- a/dom/security/nsContentSecurityManager.cpp +++ b/dom/security/nsContentSecurityManager.cpp @@ -105,6 +105,14 @@ DoCORSChecks(nsIChannel* aChannel, nsILoadInfo* aLoadInfo, nsCOMPtr& aInAndOutListener) { MOZ_RELEASE_ASSERT(aInAndOutListener, "can not perform CORS checks without a listener"); + + // No need to set up CORS if TriggeringPrincipal is the SystemPrincipal. + // For example, allow user stylesheets to load XBL from external files + // without requiring CORS. + if (nsContentUtils::IsSystemPrincipal(aLoadInfo->TriggeringPrincipal())) { + return NS_OK; + } + nsIPrincipal* loadingPrincipal = aLoadInfo->LoadingPrincipal(); RefPtr corsListener = new nsCORSListenerProxy(aInAndOutListener, @@ -476,10 +484,11 @@ nsContentSecurityManager::CheckChannel(nsIChannel* aChannel) return NS_OK; } - // Allow the load if TriggeringPrincipal is the SystemPrincipal which - // is e.g. necessary to allow user user stylesheets to load XBL from - // external files. - if (nsContentUtils::IsSystemPrincipal(loadInfo->TriggeringPrincipal())) { + // Allow subresource loads if TriggeringPrincipal is the SystemPrincipal. + // For example, allow user stylesheets to load XBL from external files. + if (nsContentUtils::IsSystemPrincipal(loadInfo->TriggeringPrincipal()) && + loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_DOCUMENT && + loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_SUBDOCUMENT) { return NS_OK; } diff --git a/dom/webidl/Node.webidl b/dom/webidl/Node.webidl index 232ebb3e26e4..7287286380e3 100644 --- a/dom/webidl/Node.webidl +++ b/dom/webidl/Node.webidl @@ -72,6 +72,8 @@ interface Node : EventTarget { [Throws] Node cloneNode(optional boolean deep = false); [Pure] + boolean isSameNode(Node? node); + [Pure] boolean isEqualNode(Node? node); const unsigned short DOCUMENT_POSITION_DISCONNECTED = 0x01; diff --git a/ipc/glue/BackgroundImpl.cpp b/ipc/glue/BackgroundImpl.cpp index 5e6166d2268a..bdb00c21d3c0 100644 --- a/ipc/glue/BackgroundImpl.cpp +++ b/ipc/glue/BackgroundImpl.cpp @@ -341,11 +341,7 @@ class ChildImpl final : public BackgroundChildImpl // create the background thread after application shutdown has started. static bool sShutdownHasStarted; -#ifdef RELEASE_BUILD -#ifdef DEBUG - nsIThread* mBoundThread; -#endif -#else +#if defined(DEBUG) || !defined(RELEASE_BUILD) nsIThread* mBoundThread; #endif @@ -382,7 +378,9 @@ public: } ChildImpl() +#if defined(DEBUG) || !defined(RELEASE_BUILD) : mBoundThread(nullptr) +#endif #ifdef DEBUG , mActorDestroyed(false) #endif diff --git a/js/examples/jorendb.js b/js/examples/jorendb.js index f1e1291f26cd..7fb9bd3f44f5 100644 --- a/js/examples/jorendb.js +++ b/js/examples/jorendb.js @@ -25,20 +25,91 @@ var topFrame = null; var debuggeeValues = {}; var nextDebuggeeValueIndex = 1; var lastExc = null; +var todo = []; +var activeTask; +var options = { 'pretty': true, + 'emacs': (os.getenv('EMACS') == 't') }; +var rerun = true; // Cleanup functions to run when we next re-enter the repl. var replCleanups = []; +// Redirect debugger printing functions to go to the original output +// destination, unaffected by any redirects done by the debugged script. +var initialOut = os.file.redirect(); +var initialErr = os.file.redirectErr(); + +function wrap(global, name) { + var orig = global[name]; + global[name] = function(...args) { + + var oldOut = os.file.redirect(initialOut); + var oldErr = os.file.redirectErr(initialErr); + try { + return orig.apply(global, args); + } finally { + os.file.redirect(oldOut); + os.file.redirectErr(oldErr); + } + }; +} +wrap(this, 'print'); +wrap(this, 'printErr'); +wrap(this, 'putstr'); + // Convert a debuggee value v to a string. function dvToString(v) { return (typeof v !== 'object' || v === null) ? uneval(v) : "[object " + v.class + "]"; } -function showDebuggeeValue(dv) { +function summaryObject(dv) { + var obj = {}; + for (var name of dv.getOwnPropertyNames()) { + var v = dv.getOwnPropertyDescriptor(name).value; + if (v instanceof Debugger.Object) { + v = "(...)"; + } + obj[name] = v; + } + return obj; +} + +function debuggeeValueToString(dv, style) { var dvrepr = dvToString(dv); + if (!style.pretty || (typeof dv !== 'object')) + return [dvrepr, undefined]; + + if (dv.class == "Error") { + let errval = debuggeeGlobalWrapper.executeInGlobalWithBindings("$" + i + ".toString()", debuggeeValues); + return [dvrepr, errval.return]; + } + + if (style.brief) + return [dvrepr, JSON.stringify(summaryObject(dv), null, 4)]; + + let str = debuggeeGlobalWrapper.executeInGlobalWithBindings("JSON.stringify(v, null, 4)", {v: dv}); + if ('throw' in str) { + if (style.noerror) + return [dvrepr, undefined]; + + let substyle = {}; + Object.assign(substyle, style); + substyle.noerror = true; + return [dvrepr, debuggeeValueToString(str.throw, substyle)]; + } + + return [dvrepr, str.return]; +} + +// Problem! Used to do [object Object] followed by details. Now just details? + +function showDebuggeeValue(dv, style={pretty: options.pretty}) { var i = nextDebuggeeValueIndex++; debuggeeValues["$" + i] = dv; - print("$" + i + " = " + dvrepr); + let [brief, full] = debuggeeValueToString(dv, style); + print("$" + i + " = " + brief); + if (full !== undefined) + print(full); } Object.defineProperty(Debugger.Frame.prototype, "num", { @@ -70,6 +141,16 @@ Debugger.Frame.prototype.positionDescription = function positionDescription() { return null; } +Debugger.Frame.prototype.location = function () { + if (this.script) { + var { lineNumber, columnNumber, isEntryPoint } = this.script.getOffsetLocation(this.offset); + if (this.script.url) + return this.script.url + ":" + lineNumber; + return null; + } + return null; +} + Debugger.Frame.prototype.fullDescription = function fullDescription() { var fr = this.frameDescription(); var pos = this.positionDescription(); @@ -121,6 +202,50 @@ function saveExcursion(fn) { } } +function parseArgs(str) { + return str.split(" "); +} + +function describedRv(r, desc) { + desc = "[" + desc + "] "; + if (r === undefined) { + print(desc + "Returning undefined"); + } else if (r === null) { + print(desc + "Returning null"); + } else if (r.length === undefined) { + print(desc + "Returning object " + JSON.stringify(r)); + } else { + print(desc + "Returning length-" + r.length + " list"); + if (r.length > 0) { + print(" " + r[0]); + } + } + return r; +} + +// Rerun the program (reloading it from the file) +function runCommand(args) { + print("Restarting program"); + if (args) + activeTask.scriptArgs = parseArgs(args); + rerun = true; + for (var f = topFrame; f; f = f.older) { + print(f.script.url + ":" + f.script.getOffsetLine(f.offset) +" was " + f.onPop); + if (f.older) { + f.onPop = function() { + print("Resumifying " + this.script.url + ":" + this.script.getOffsetLine(this.offset)); + return null; + }; + } else { + f.onPop = function() { + return { 'return': 0 }; + }; + } + } + //return describedRv([{ 'return': 0 }], "runCommand"); + return null; +} + // Evaluate an expression in the Debugger global function evalCommand(expr) { eval(expr); @@ -138,12 +263,47 @@ function backtraceCommand() { showFrame(f, i); } -function printCommand(rest) { +function setCommand(rest) { + var space = rest.indexOf(' '); + if (space == -1) { + print("Invalid set