#522: basic window.open=noopener, refactor popup blocker M1267338 M1267339

This commit is contained in:
Cameron Kaiser 2018-09-19 19:01:21 -07:00
parent 3660eb7e23
commit 46bf256a1f
7 changed files with 132 additions and 58 deletions

View File

@ -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<nsIDocShellTreeItem> 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<nsIDOMWindow>
@ -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<nsContentUtils::IsHTMLWhitespace> 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<nsPIWindowWatcher> 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<nsGlobalWindow *>(*aReturn);
if (!opened->IsPopupSpamWindow()) {
opened->SetPopupSpamWindow(true);
++gOpenPopupSpamCount;
}
}
if (abuseLevel >= openAbused)
FireAbuseEvents(false, true, aUrl, aName, aOptions);
}
return rv;
}

View File

@ -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<nsIWidget> GetMainWidget();
nsIWidget* GetNearestWidget() const;

View File

@ -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;

View File

@ -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<nsPIDOMWindow> pwindow = do_QueryInterface(newWindow);
pwindow.forget(aWindow);

View File

@ -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

View File

@ -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<nsIArray> 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<nsIDOMWindow> foundWindow = SafeGetWindowByName(name, aParent);
nsCOMPtr<nsIDOMWindow> 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<nsGlobalWindow*>(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<nsIDocShellTreeItem> 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<nsPIDOMWindow> piOpenedWindow = aOpenedItem->GetWindow();
if (piOpenedWindow) {
if (aParent) {
piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit
if (!aForceNoOpener) {
piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit
}
if (aWindowIsNew) {
#ifdef DEBUG

View File

@ -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,