diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index 924a8c484..747d6d941 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -51,6 +51,7 @@ #include "nsJSUtils.h" #include "jsapi.h" // for JSAutoRequest #include "jswrapper.h" +#include "nsCharSeparatedTokenizer.h" #include "nsReadableUtils.h" #include "nsDOMClassInfo.h" #include "nsJSEnvironment.h" @@ -1483,9 +1484,20 @@ void nsGlobalWindow::MaybeForgiveSpamCount() { if (IsOuterWindow() && - IsPopupSpamWindow()) - { - SetPopupSpamWindow(false); + IsPopupSpamWindow()) { + SetIsPopupSpamWindow(false); + } +} + +void +nsGlobalWindow::SetIsPopupSpamWindow(bool aIsPopupSpam) +{ + MOZ_ASSERT(IsOuterWindow()); + + mIsPopupSpam = aIsPopupSpam; + if (aIsPopupSpam) { + ++gOpenPopupSpamCount; + } else { --gOpenPopupSpamCount; NS_ASSERTION(gOpenPopupSpamCount >= 0, "Unbalanced decrement of gOpenPopupSpamCount"); @@ -5718,11 +5730,18 @@ GetCallerDocShellTreeItem() bool nsGlobalWindow::WindowExists(const nsAString& aName, + bool aForceNoOpener, bool aLookForCallerOnJSStack) { NS_PRECONDITION(IsOuterWindow(), "Must be outer window"); NS_PRECONDITION(mDocShell, "Must have docshell"); + if (aForceNoOpener) { + return aName.LowerCaseEqualsLiteral("_self") || + aName.LowerCaseEqualsLiteral("_top") || + aName.LowerCaseEqualsLiteral("_parent"); + } + nsCOMPtr caller; if (aLookForCallerOnJSStack) { caller = GetCallerDocShellTreeItem(); @@ -7532,14 +7551,6 @@ nsGlobalWindow::FirePopupBlockedEvent(nsIDocument* aDoc, aDoc->DispatchEvent(event, &defaultActionEnabled); } -static void FirePopupWindowEvent(nsIDocument* aDoc) -{ - // Fire a "PopupWindow" event - nsContentUtils::DispatchTrustedEvent(aDoc, aDoc, - NS_LITERAL_STRING("PopupWindow"), - true, true); -} - // static bool nsGlobalWindow::CanSetProperty(const char *aPrefName) @@ -7608,13 +7619,9 @@ nsGlobalWindow::RevisePopupAbuseLevel(PopupControlState aControl) return abuse; } -/* If a window open is blocked, fire the appropriate DOM events. - aBlocked signifies we just blocked a popup. - aWindow signifies we just opened what is probably a popup. -*/ +/* If a window open is blocked, fire the appropriate DOM events. */ void -nsGlobalWindow::FireAbuseEvents(bool aBlocked, bool aWindow, - const nsAString &aPopupURL, +nsGlobalWindow::FireAbuseEvents(const nsAString &aPopupURL, const nsAString &aPopupWindowName, const nsAString &aPopupWindowFeatures) { @@ -7645,13 +7652,8 @@ nsGlobalWindow::FireAbuseEvents(bool aBlocked, bool aWindow, ios->NewURI(NS_ConvertUTF16toUTF8(aPopupURL), 0, baseURL, getter_AddRefs(popupURI)); - // fire an event chock full of informative URIs - if (aBlocked) { - FirePopupBlockedEvent(topDoc, popupURI, aPopupWindowName, - aPopupWindowFeatures); - } - if (aWindow) - FirePopupWindowEvent(topDoc); + FirePopupBlockedEvent(topDoc, popupURI, aPopupWindowName, + aPopupWindowFeatures); } already_AddRefed @@ -11333,8 +11335,24 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, nsIPrincipal::APP_STATUS_INSTALLED; } + bool forceNoOpener = false; + // Unlike other window flags, "noopener" comes from splitting on commas with + // HTML whitespace trimming... + nsCharSeparatedTokenizerTemplate tok( + aOptions, ','); + while (tok.hasMoreTokens()) { + if (tok.nextToken().EqualsLiteral("noopener")) { + forceNoOpener = true; + break; + } + } + + // XXXbz When this gets fixed to not use LegacyIsCallerNativeCode() + // (indirectly) maybe we can nix the AutoJSAPI usage OnLinkClickEvent::Run. + // But note that if you change this to GetEntryGlobal(), say, then + // OnLinkClickEvent::Run will need a full-blown AutoEntryScript. const bool checkForPopup = !nsContentUtils::LegacyIsCallerChromeOrNativeCode() && - !isApp && !aDialog && !WindowExists(aName, !aCalledNoScript); + !isApp && !aDialog && !WindowExists(aName, forceNoOpener, !aCalledNoScript); // Note: it's very important that this be an nsXPIDLCString, since we want // .get() on it to return nullptr until we write stuff to it. The window @@ -11377,7 +11395,7 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, } } - FireAbuseEvents(true, false, aUrl, aName, aOptions); + FireAbuseEvents(aUrl, aName, aOptions); return aDoJSFixups ? NS_OK : NS_ERROR_FAILURE; } } @@ -11397,6 +11415,13 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, nsCOMPtr pwwatch(do_QueryInterface(wwatch)); NS_ENSURE_STATE(pwwatch); + MOZ_ASSERT_IF(checkForPopup, abuseLevel < openAbused); + // At this point we should know for a fact that if checkForPopup then + // abuseLevel < openAbused, so we could just check for abuseLevel == + // openControlled. But let's be defensive just in case and treat anything + // that fails the above assert as a spam popup too, if it ever happens. + bool isPopupSpamWindow = checkForPopup && (abuseLevel >= openControlled); + { // Reset popup state while opening a window to prevent the // current state from being active the whole time a modal @@ -11409,6 +11434,8 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, rv = pwwatch->OpenWindow2(this, url.get(), name_ptr, options_ptr, /* aCalledFromScript = */ true, aDialog, aNavigate, nullptr, argv, + isPopupSpamWindow, + forceNoOpener, getter_AddRefs(domReturn)); } else { // Force a system caller here so that the window watcher won't screw us @@ -11425,10 +11452,11 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, nojsapi.emplace(); } - rv = pwwatch->OpenWindow2(this, url.get(), name_ptr, options_ptr, /* aCalledFromScript = */ false, aDialog, aNavigate, nullptr, aExtraArgument, + isPopupSpamWindow, + forceNoOpener, getter_AddRefs(domReturn)); } @@ -11463,18 +11491,6 @@ nsGlobalWindow::OpenInternal(const nsAString& aUrl, const nsAString& aName, } } - if (checkForPopup) { - if (abuseLevel >= openControlled) { - nsGlobalWindow *opened = static_cast(*aReturn); - if (!opened->IsPopupSpamWindow()) { - opened->SetPopupSpamWindow(true); - ++gOpenPopupSpamCount; - } - } - if (abuseLevel >= openAbused) - FireAbuseEvents(false, true, aUrl, aName, aOptions); - } - return rv; } diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index 05dcf49a3..640d081c8 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -1321,6 +1321,7 @@ protected: // Get the parent, returns null if this is a toplevel window nsIDOMWindow* GetParentInternal(); +public: // popup tracking bool IsPopupSpamWindow() { @@ -1331,17 +1332,10 @@ protected: return GetOuterWindowInternal()->mIsPopupSpam; } - void SetPopupSpamWindow(bool aPopup) - { - if (IsInnerWindow() && !mOuterWindow) { - NS_ERROR("SetPopupSpamWindow() called on inner window w/o an outer!"); - - return; - } - - GetOuterWindowInternal()->mIsPopupSpam = aPopup; - } + // Outer windows only. + void SetIsPopupSpamWindow(bool aIsPopupSpam); +protected: // Window Control Functions // Outer windows only. @@ -1471,8 +1465,7 @@ public: bool PopupWhitelisted(); PopupControlState RevisePopupAbuseLevel(PopupControlState); - void FireAbuseEvents(bool aBlocked, bool aWindow, - const nsAString &aPopupURL, + void FireAbuseEvents(const nsAString &aPopupURL, const nsAString &aPopupWindowName, const nsAString &aPopupWindowFeatures); void FireOfflineStatusEventIfChanged(); @@ -1538,7 +1531,8 @@ public: // If aLookForCallerOnJSStack is true, this method will look at the JS stack // to determine who the caller is. If it's false, it'll use |this| as the // caller. - bool WindowExists(const nsAString& aName, bool aLookForCallerOnJSStack); + bool WindowExists(const nsAString& aName, bool aForceNoOpener, + bool aLookForCallerOnJSStack); already_AddRefed GetMainWidget(); nsIWidget* GetNearestWidget() const; diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index b8ff1102b..ace879c71 100755 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -5576,8 +5576,13 @@ ContentParent::RecvCreateWindow(PBrowserParent* aThisTab, const char* name = aName.IsVoid() ? nullptr : NS_ConvertUTF16toUTF8(aName).get(); const char* features = aFeatures.IsVoid() ? nullptr : aFeatures.get(); +#if(0) +// Needs update for issue 522 *aResult = pwwatch->OpenWindow2(parent, uri, name, features, aCalledFromJS, false, false, thisTabParent, nullptr, getter_AddRefs(window)); +#else + MOZ_ASSERT(0); +#endif if (NS_WARN_IF(NS_FAILED(*aResult))) { return true; diff --git a/dom/workers/ServiceWorkerClients.cpp b/dom/workers/ServiceWorkerClients.cpp index 1b267bb30..d20db79b8 100644 --- a/dom/workers/ServiceWorkerClients.cpp +++ b/dom/workers/ServiceWorkerClients.cpp @@ -608,6 +608,12 @@ private: nullptr, nullptr, false, false, true, nullptr, nullptr, + // Not a spammy popup; we got permission, we swear! + /* aIsPopupSpam = */ false, + // Don't force noopener. We're not passing in an + // opener anyway, and we _do_ want the returned + // window. + /* aForceNoOpener = */ false, getter_AddRefs(newWindow)); nsCOMPtr pwindow = do_QueryInterface(newWindow); pwindow.forget(aWindow); diff --git a/embedding/components/windowwatcher/nsPIWindowWatcher.idl b/embedding/components/windowwatcher/nsPIWindowWatcher.idl index a552567e8..44ac81111 100644 --- a/embedding/components/windowwatcher/nsPIWindowWatcher.idl +++ b/embedding/components/windowwatcher/nsPIWindowWatcher.idl @@ -56,6 +56,12 @@ interface nsPIWindowWatcher : nsISupports nsITabParent is a remote tab belonging to aParent. Can be nullptr if this window is not being opened from a tab. @param aArgs Window argument + @param aIsPopupSpam true if the window is a popup spam window; used for + popup blocker internals. + @param aForceNoOpener If true, force noopener behavior. This means not + looking for existing windows with the given name, + not setting an opener on the newly opened window, + and returning null from this method. @return the new window @note This method may examine the JS context stack for purposes of @@ -70,7 +76,9 @@ interface nsPIWindowWatcher : nsISupports in string aName, in string aFeatures, in boolean aCalledFromScript, in boolean aDialog, in boolean aNavigate, in nsITabParent aOpeningTab, - in nsISupports aArgs); + in nsISupports aArgs, + in boolean aIsPopupSpam, + in boolean aForceNoOpener); /** * Find a named docshell tree item amongst all windows registered diff --git a/embedding/components/windowwatcher/nsWindowWatcher.cpp b/embedding/components/windowwatcher/nsWindowWatcher.cpp index 2208d7507..89d1d730c 100644 --- a/embedding/components/windowwatcher/nsWindowWatcher.cpp +++ b/embedding/components/windowwatcher/nsWindowWatcher.cpp @@ -18,6 +18,7 @@ #include "nsJSUtils.h" #include "plstr.h" +#include "nsGlobalWindow.h" #include "nsIBaseWindow.h" #include "nsIBrowserDOMWindow.h" #include "nsIDocShell.h" @@ -366,7 +367,10 @@ nsWindowWatcher::OpenWindow(nsIDOMWindow* aParent, return OpenWindowInternal(aParent, aUrl, aName, aFeatures, /* calledFromJS = */ false, dialog, - /* navigate = */ true, nullptr, argv, aResult); + /* navigate = */ true, nullptr, argv, + /* aIsPopupSpam = */ false, + /* aForceNoOpener = */ false, + aResult); } struct SizeSpec @@ -424,6 +428,8 @@ nsWindowWatcher::OpenWindow2(nsIDOMWindow* aParent, bool aNavigate, nsITabParent* aOpeningTab, nsISupports* aArguments, + bool aIsPopupSpam, + bool aForceNoOpener, nsIDOMWindow** aResult) { nsCOMPtr argv = ConvertArgsToArray(aArguments); @@ -443,7 +449,10 @@ nsWindowWatcher::OpenWindow2(nsIDOMWindow* aParent, return OpenWindowInternal(aParent, aUrl, aName, aFeatures, aCalledFromScript, dialog, - aNavigate, aOpeningTab, argv, aResult); + aNavigate, aOpeningTab, argv, + aIsPopupSpam, + aForceNoOpener, + aResult); } nsresult @@ -456,6 +465,8 @@ nsWindowWatcher::OpenWindowInternal(nsIDOMWindow* aParent, bool aNavigate, nsITabParent* aOpeningTab, nsIArray* aArgv, + bool aIsPopupSpam, + bool aForceNoOpener, nsIDOMWindow** aResult) { nsresult rv = NS_OK; @@ -531,7 +542,8 @@ nsWindowWatcher::OpenWindowInternal(nsIDOMWindow* aParent, // know or care about names - unless we're opening named windows from chrome. if (!aOpeningTab) { // try to find an extant window with the given name - nsCOMPtr foundWindow = SafeGetWindowByName(name, aParent); + nsCOMPtr foundWindow = + SafeGetWindowByName(name, aForceNoOpener, aParent); GetWindowTreeItem(foundWindow, getter_AddRefs(newDocShellItem)); } @@ -828,7 +840,8 @@ nsWindowWatcher::OpenWindowInternal(nsIDOMWindow* aParent, } } - rv = ReadyOpenedDocShellItem(newDocShellItem, aParent, windowIsNew, aResult); + rv = ReadyOpenedDocShellItem(newDocShellItem, aParent, windowIsNew, + aForceNoOpener, aResult); if (NS_FAILED(rv)) { return rv; } @@ -900,6 +913,16 @@ nsWindowWatcher::OpenWindowInternal(nsIDOMWindow* aParent, // SetInitialPrincipalToSubject is safe to call multiple times. if (newWindow) { newWindow->SetInitialPrincipalToSubject(); + if (aIsPopupSpam) { + nsGlobalWindow* globalWin = static_cast(newWindow.get()); + MOZ_ASSERT(!globalWin->IsPopupSpamWindow(), + "Who marked it as popup spam already???"); + if (!globalWin->IsPopupSpamWindow()) { // Make sure we don't mess up our + // counter even if the above + // assert fails. + globalWin->SetIsPopupSpamWindow(true); + } + } } } @@ -1069,6 +1092,10 @@ nsWindowWatcher::OpenWindowInternal(nsIDOMWindow* aParent, } } + if (aForceNoOpener && windowIsNew) { + NS_RELEASE(*aResult); + } + return NS_OK; } @@ -1857,8 +1884,18 @@ nsWindowWatcher::GetCallerTreeItem(nsIDocShellTreeItem* aParentItem) nsPIDOMWindow* nsWindowWatcher::SafeGetWindowByName(const nsAString& aName, + bool aForceNoOpener, nsIDOMWindow* aCurrentWindow) { + if (aForceNoOpener) { + if (!aName.LowerCaseEqualsLiteral("_self") && + !aName.LowerCaseEqualsLiteral("_top") && + !aName.LowerCaseEqualsLiteral("_parent")) { + // Ignore all other names in the noopener case. + return nullptr; + } + } + nsCOMPtr startItem; GetWindowTreeItem(aCurrentWindow, getter_AddRefs(startItem)); @@ -1887,6 +1924,7 @@ nsresult nsWindowWatcher::ReadyOpenedDocShellItem(nsIDocShellTreeItem* aOpenedItem, nsIDOMWindow* aParent, bool aWindowIsNew, + bool aForceNoOpener, nsIDOMWindow** aOpenedWindow) { nsresult rv = NS_ERROR_FAILURE; @@ -1897,7 +1935,9 @@ nsWindowWatcher::ReadyOpenedDocShellItem(nsIDocShellTreeItem* aOpenedItem, nsCOMPtr piOpenedWindow = aOpenedItem->GetWindow(); if (piOpenedWindow) { if (aParent) { - piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit + if (!aForceNoOpener) { + piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit + } if (aWindowIsNew) { #ifdef DEBUG diff --git a/embedding/components/windowwatcher/nsWindowWatcher.h b/embedding/components/windowwatcher/nsWindowWatcher.h index a71bfab80..d683ae5da 100644 --- a/embedding/components/windowwatcher/nsWindowWatcher.h +++ b/embedding/components/windowwatcher/nsWindowWatcher.h @@ -70,7 +70,9 @@ protected: // Unlike GetWindowByName this will look for a caller on the JS // stack, and then fall back on aCurrentWindow if it can't find one. + // It also knows to not look for things if aForceNoOpener is set. nsPIDOMWindow* SafeGetWindowByName(const nsAString& aName, + bool aForceNoOpener, nsIDOMWindow* aCurrentWindow); // Just like OpenWindowJS, but knows whether it got called via OpenWindowJS @@ -84,6 +86,8 @@ protected: bool aNavigate, nsITabParent* aOpeningTab, nsIArray* aArgv, + bool aIsPopupSpam, + bool aForceNoOpener, nsIDOMWindow** aResult); static nsresult URIfromURL(const char* aURL, @@ -105,6 +109,7 @@ protected: static nsresult ReadyOpenedDocShellItem(nsIDocShellTreeItem* aOpenedItem, nsIDOMWindow* aParent, bool aWindowIsNew, + bool aForceNoOpener, nsIDOMWindow** aOpenedWindow); static void SizeOpenedDocShellItem(nsIDocShellTreeItem* aDocShellItem, nsIDOMWindow* aParent,