diff options
Diffstat (limited to 'toolkit/components/windowwatcher')
40 files changed, 5598 insertions, 0 deletions
diff --git a/toolkit/components/windowwatcher/moz.build b/toolkit/components/windowwatcher/moz.build new file mode 100644 index 0000000000..73479b38c9 --- /dev/null +++ b/toolkit/components/windowwatcher/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Window Management") + +TEST_DIRS += ["test"] + +XPIDL_SOURCES += [ + "nsIDialogParamBlock.idl", + "nsIOpenWindowInfo.idl", + "nsIPromptCollection.idl", + "nsIPromptFactory.idl", + "nsIPromptService.idl", + "nsIWindowWatcher.idl", + "nsPIPromptService.idl", + "nsPIWindowWatcher.idl", +] + +XPIDL_MODULE = "windowwatcher" + +EXPORTS += [ + "nsOpenWindowInfo.h", + "nsPromptUtils.h", + "nsWindowWatcher.h", +] + +UNIFIED_SOURCES += [ + "nsAutoWindowStateHelper.cpp", + "nsDialogParamBlock.cpp", + "nsOpenWindowInfo.cpp", + "nsWindowWatcher.cpp", +] + +FINAL_LIBRARY = "xul" +# For nsJSUtils +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/toolkit/components/windowwatcher/nsAutoWindowStateHelper.cpp b/toolkit/components/windowwatcher/nsAutoWindowStateHelper.cpp new file mode 100644 index 0000000000..c735f51e49 --- /dev/null +++ b/toolkit/components/windowwatcher/nsAutoWindowStateHelper.cpp @@ -0,0 +1,67 @@ +/* -*- 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 "nsAutoWindowStateHelper.h" + +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsString.h" + +using namespace mozilla; +using namespace mozilla::dom; + +/**************************************************************** + ****************** nsAutoWindowStateHelper ********************* + ****************************************************************/ + +nsAutoWindowStateHelper::nsAutoWindowStateHelper(nsPIDOMWindowOuter* aWindow) + : mWindow(aWindow), + mDefaultEnabled(DispatchEventToChrome("DOMWillOpenModalDialog")) { + if (mWindow) { + mWindow->EnterModalState(); + } +} + +nsAutoWindowStateHelper::~nsAutoWindowStateHelper() { + if (mWindow) { + mWindow->LeaveModalState(); + } + + if (mDefaultEnabled) { + DispatchEventToChrome("DOMModalDialogClosed"); + } +} + +bool nsAutoWindowStateHelper::DispatchEventToChrome(const char* aEventName) { + // XXXbz should we skip dispatching the event if the inner changed? + // That is, should we store both the inner and the outer? + if (!mWindow) { + return true; + } + + // The functions of nsContentUtils do not provide the required behavior, + // so the following is inlined. + Document* doc = mWindow->GetExtantDoc(); + if (!doc) { + return true; + } + + ErrorResult rv; + RefPtr<Event> event = doc->CreateEvent(u"Events"_ns, CallerType::System, rv); + if (rv.Failed()) { + rv.SuppressException(); + return false; + } + event->InitEvent(NS_ConvertASCIItoUTF16(aEventName), true, true); + event->SetTrusted(true); + event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true; + + nsCOMPtr<EventTarget> target = do_QueryInterface(mWindow); + bool defaultActionEnabled = + target->DispatchEvent(*event, CallerType::System, IgnoreErrors()); + return defaultActionEnabled; +} diff --git a/toolkit/components/windowwatcher/nsAutoWindowStateHelper.h b/toolkit/components/windowwatcher/nsAutoWindowStateHelper.h new file mode 100644 index 0000000000..b18bc4e5e2 --- /dev/null +++ b/toolkit/components/windowwatcher/nsAutoWindowStateHelper.h @@ -0,0 +1,34 @@ +/* -*- 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 __nsAutoWindowStateHelper_h +#define __nsAutoWindowStateHelper_h + +#include "nsCOMPtr.h" +#include "nsPIDOMWindow.h" + +/** + * Helper class for dealing with notifications around opening modal + * windows. + */ + +class nsPIDOMWindowOuter; + +class nsAutoWindowStateHelper { + public: + explicit nsAutoWindowStateHelper(nsPIDOMWindowOuter* aWindow); + ~nsAutoWindowStateHelper(); + + bool DefaultEnabled() { return mDefaultEnabled; } + + protected: + bool DispatchEventToChrome(const char* aEventName); + + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + bool mDefaultEnabled; +}; + +#endif diff --git a/toolkit/components/windowwatcher/nsDialogParamBlock.cpp b/toolkit/components/windowwatcher/nsDialogParamBlock.cpp new file mode 100644 index 0000000000..7aab00f8e1 --- /dev/null +++ b/toolkit/components/windowwatcher/nsDialogParamBlock.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "nsDialogParamBlock.h" +#include "nsString.h" +#include "nsReadableUtils.h" + +NS_IMPL_ISUPPORTS(nsDialogParamBlock, nsIDialogParamBlock) + +nsDialogParamBlock::nsDialogParamBlock() : mNumStrings(0), mString(nullptr) { + for (int32_t i = 0; i < kNumInts; i++) { + mInt[i] = 0; + } +} + +nsDialogParamBlock::~nsDialogParamBlock() { delete[] mString; } + +NS_IMETHODIMP +nsDialogParamBlock::SetNumberStrings(int32_t aNumStrings) { + if (mString) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + mString = new nsString[aNumStrings]; + if (!mString) { + return NS_ERROR_OUT_OF_MEMORY; + } + mNumStrings = aNumStrings; + return NS_OK; +} + +NS_IMETHODIMP +nsDialogParamBlock::GetInt(int32_t aIndex, int32_t* aResult) { + nsresult rv = InBounds(aIndex, kNumInts); + if (rv == NS_OK) { + *aResult = mInt[aIndex]; + } + return rv; +} + +NS_IMETHODIMP +nsDialogParamBlock::SetInt(int32_t aIndex, int32_t aInt) { + nsresult rv = InBounds(aIndex, kNumInts); + if (rv == NS_OK) { + mInt[aIndex] = aInt; + } + return rv; +} + +NS_IMETHODIMP +nsDialogParamBlock::GetString(int32_t aIndex, char16_t** aResult) { + if (mNumStrings == 0) { + SetNumberStrings(kNumStrings); + } + nsresult rv = InBounds(aIndex, mNumStrings); + if (rv == NS_OK) { + *aResult = ToNewUnicode(mString[aIndex]); + } + return rv; +} + +NS_IMETHODIMP +nsDialogParamBlock::SetString(int32_t aIndex, const char16_t* aString) { + if (mNumStrings == 0) { + SetNumberStrings(kNumStrings); + } + nsresult rv = InBounds(aIndex, mNumStrings); + if (rv == NS_OK) { + mString[aIndex] = aString; + } + return rv; +} + +NS_IMETHODIMP +nsDialogParamBlock::GetObjects(nsIMutableArray** aObjects) { + NS_ENSURE_ARG_POINTER(aObjects); + NS_IF_ADDREF(*aObjects = mObjects); + return NS_OK; +} + +NS_IMETHODIMP +nsDialogParamBlock::SetObjects(nsIMutableArray* aObjects) { + mObjects = aObjects; + return NS_OK; +} diff --git a/toolkit/components/windowwatcher/nsDialogParamBlock.h b/toolkit/components/windowwatcher/nsDialogParamBlock.h new file mode 100644 index 0000000000..e411d76eab --- /dev/null +++ b/toolkit/components/windowwatcher/nsDialogParamBlock.h @@ -0,0 +1,46 @@ +/* -*- 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 __nsDialogParamBlock_h +#define __nsDialogParamBlock_h + +#include "nsIDialogParamBlock.h" +#include "nsIMutableArray.h" +#include "nsCOMPtr.h" +#include "nsStringFwd.h" + +// {4E4AAE11-8901-46cc-8217-DAD7C5415873} +#define NS_DIALOGPARAMBLOCK_CID \ + { \ + 0x4e4aae11, 0x8901, 0x46cc, { \ + 0x82, 0x17, 0xda, 0xd7, 0xc5, 0x41, 0x58, 0x73 \ + } \ + } + +class nsDialogParamBlock : public nsIDialogParamBlock { + public: + nsDialogParamBlock(); + + NS_DECL_NSIDIALOGPARAMBLOCK + NS_DECL_ISUPPORTS + + protected: + virtual ~nsDialogParamBlock(); + + private: + enum { kNumInts = 8, kNumStrings = 16 }; + + nsresult InBounds(int32_t aIndex, int32_t aMax) { + return aIndex >= 0 && aIndex < aMax ? NS_OK : NS_ERROR_ILLEGAL_VALUE; + } + + int32_t mInt[kNumInts]; + int32_t mNumStrings; + nsString* mString; + nsCOMPtr<nsIMutableArray> mObjects; +}; + +#endif diff --git a/toolkit/components/windowwatcher/nsIDialogParamBlock.idl b/toolkit/components/windowwatcher/nsIDialogParamBlock.idl new file mode 100644 index 0000000000..2b2b3aafd4 --- /dev/null +++ b/toolkit/components/windowwatcher/nsIDialogParamBlock.idl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +interface nsIMutableArray; + +/** + * An interface to pass strings, integers and nsISupports to a dialog + */ + +[scriptable, uuid(f76c0901-437a-11d3-b7a0-e35db351b4bc)] +interface nsIDialogParamBlock: nsISupports { + + /** Get or set an integer to pass. + * Index must be in the range 0..7 + */ + int32_t GetInt( in int32_t inIndex ); + void SetInt( in int32_t inIndex, in int32_t inInt ); + + /** Set the maximum number of strings to pass. Default is 16. + * Use before setting any string (If you want to change it from the default). + */ + void SetNumberStrings( in int32_t inNumStrings ); + + /** Get or set an string to pass. + * Index starts at 0 + */ + wstring GetString( in int32_t inIndex ); + void SetString( in int32_t inIndex, in wstring inString); + + /** + * A place where you can store an nsIMutableArray to pass nsISupports + */ + attribute nsIMutableArray objects; +}; + +%{C++ +#define NS_DIALOGPARAMBLOCK_CONTRACTID "@mozilla.org/embedcomp/dialogparam;1" +%} diff --git a/toolkit/components/windowwatcher/nsIOpenWindowInfo.idl b/toolkit/components/windowwatcher/nsIOpenWindowInfo.idl new file mode 100644 index 0000000000..a8347c5a1a --- /dev/null +++ b/toolkit/components/windowwatcher/nsIOpenWindowInfo.idl @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl BrowsingContext; + +%{ C++ +namespace mozilla { +class OriginAttributes; +namespace dom { +class BrowserParent; +} // namespace dom +} // namespace mozilla +%} +[ref] native const_OriginAttributes(const mozilla::OriginAttributes); +[ptr] native BrowserParent(mozilla::dom::BrowserParent); + +/* + * nsIBrowsingContextReadyCallback.browsingContextReady() is called within + * nsFrameLoader to indicate that the browsing context for a newly opened + * window/tab is ready. + */ +[uuid(0524ee06-7f4c-4cd3-ab80-084562745cad)] +interface nsIBrowsingContextReadyCallback : nsISupports +{ + void browsingContextReady(in BrowsingContext bc); +}; + +/** + * nsIOpenWindowInfo is a helper type which contains details used when opening + * new content windows. This object is used to correctly create new initial + * content documents when creating a new window. + */ +[scriptable, builtinclass, uuid(30359edb-126c-4f65-ae80-07fb158697f9)] +interface nsIOpenWindowInfo : nsISupports { + /** BrowsingContext which requested the creation of this new window */ + [infallible] + readonly attribute BrowsingContext parent; + + /** If `true`, the content document should be created initially-remote */ + [infallible] + readonly attribute boolean isRemote; + + /** Should |opener| be set on the newly-created content window? */ + [infallible] + readonly attribute boolean forceNoOpener; + + /** Whether this is a window opened for printing */ + [infallible] + readonly attribute boolean isForPrinting; + + /** + * Whether this is a window opened for window.print(). + * When this is true, isForPrinting is necessarily true as well. + */ + [infallible] + readonly attribute boolean isForWindowDotPrint; + + /** BrowserParent instance to use in the new window */ + [notxpcom, nostdcall] + BrowserParent getNextRemoteBrowser(); + + /** Origin Attributes for the to-be-created toplevel BrowsingContext */ + [implicit_jscontext, binaryname(ScriptableOriginAttributes)] + readonly attribute jsval originAttributes; + + [notxpcom, nostdcall, binaryname(GetOriginAttributes)] + const_OriginAttributes binaryGetOriginAttributes(); + + /* Callback to invoke when the browsing context for a new window is ready. */ + [notxpcom, nostdcall] + nsIBrowsingContextReadyCallback browsingContextReadyCallback(); +}; diff --git a/toolkit/components/windowwatcher/nsIPromptCollection.idl b/toolkit/components/windowwatcher/nsIPromptCollection.idl new file mode 100644 index 0000000000..3e3e104407 --- /dev/null +++ b/toolkit/components/windowwatcher/nsIPromptCollection.idl @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl BrowsingContext; + +/** + * This interface contains various specialized prompts that the app can + * implement. + */ +[scriptable, uuid(7913837c-9623-11ea-bb37-0242ac130002)] +interface nsIPromptCollection : nsISupports +{ + /** + * Puts up a dialog for the before unload prompt. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * + * @return Promise which resolves to true if the page should be allowed to + * navigate away + */ + Promise asyncBeforeUnloadCheck(in BrowsingContext aBrowsingContext); + + /** + * Puts up a dialog for the confirm repost prompt. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * + * @return true if the page should be allowed to repost data. + */ + boolean confirmRepost(in BrowsingContext aBrowsingContext); + + /** + * Ask the user for confirmation to upload a selected folder. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param aDirectoryName + * Name of the folder that will be uploaded. + * + * @return true if the user confirmed the upload, false otherwise. + */ + boolean confirmFolderUpload(in BrowsingContext aBrowsingContext, + in AString aDirectoryName); +}; diff --git a/toolkit/components/windowwatcher/nsIPromptFactory.idl b/toolkit/components/windowwatcher/nsIPromptFactory.idl new file mode 100644 index 0000000000..940276ce8a --- /dev/null +++ b/toolkit/components/windowwatcher/nsIPromptFactory.idl @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; + +/** + * This interface allows creating various prompts that have a specific parent. + */ +[scriptable, uuid(2803541c-c96a-4ff1-bd7c-9cb566d46aeb)] +interface nsIPromptFactory : nsISupports +{ + /** + * Returns an object implementing the specified interface that creates + * prompts parented to aParent. + */ + void getPrompt(in mozIDOMWindowProxy aParent, in nsIIDRef iid, + [iid_is(iid),retval] out nsQIResult result); +}; diff --git a/toolkit/components/windowwatcher/nsIPromptService.idl b/toolkit/components/windowwatcher/nsIPromptService.idl new file mode 100644 index 0000000000..66f23ffb5b --- /dev/null +++ b/toolkit/components/windowwatcher/nsIPromptService.idl @@ -0,0 +1,634 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIAuthPromptCallback; +interface nsIAuthInformation; +interface nsICancelable; +interface nsIChannel; + +webidl BrowsingContext; +webidl WindowGlobalParent; + +/** + * This is the interface to the embeddable prompt service; the service that + * implements nsIPrompt. Its interface is designed to be just nsIPrompt, each + * method modified to take a parent window parameter. + * + * Accesskeys can be attached to buttons and checkboxes by inserting an & + * before the accesskey character in the checkbox message or button title. For + * a real &, use && instead. (A "button title" generally refers to the text + * label of a button.) + * + * One note: in all cases, the parent window parameter can be null. However, + * these windows are all intended to have parents. So when no parent is + * specified, the implementation should try hard to find a suitable foster + * parent. + * + * Implementations are free to choose how they present the various button + * types. For example, while prompts that give the user a choice between OK + * and Cancel are required to return a boolean value indicating whether or not + * the user accepted the prompt (pressed OK) or rejected the prompt (pressed + * Cancel), the implementation of this interface could very well speak the + * prompt to the user instead of rendering any visual user-interface. The + * standard button types are merely idioms used to convey the nature of the + * choice the user is to make. + * + * Because implementations of this interface may loosely interpret the various + * button types, it is advised that text messages passed to these prompts do + * not refer to the button types by name. For example, it is inadvisable to + * tell the user to "Press OK to proceed." Instead, such a prompt might be + * rewritten to ask the user: "Would you like to proceed?" + */ +[scriptable, uuid(404ebfa2-d8f4-4c94-8416-e65a55f9df5a)] +interface nsIPromptService : nsISupports +{ + /** + * Puts up an alert dialog with an OK button. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + */ + void alert(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText); + /** + * Like alert, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + void alertBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText); + /** + * Async version of alertBC + * + * @return A promise which resolves when the prompt is dismissed. + */ + Promise asyncAlert(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText); + + /** + * Puts up an alert dialog with an OK button and a labeled checkbox. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aCheckMsg + * Text to appear with the checkbox. + * @param aCheckState + * Contains the initial checked state of the checkbox when this method + * is called and the final checked state after this method returns. + */ + void alertCheck(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Like alertCheck, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + void alertCheckBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Async version of alertCheckBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { checked: boolean } + */ + Promise asyncAlertCheck(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + in boolean aCheckState); + + /** + * Puts up a dialog with OK and Cancel buttons. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * + * @return true for OK, false for Cancel + */ + boolean confirm(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText); + /** + * Like confirm, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean confirmBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText); + /** + * Async version of confirmBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { ok: boolean } + */ + Promise asyncConfirm(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText); + + /** + * Puts up a dialog with OK and Cancel buttons and a labeled checkbox. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aCheckMsg + * Text to appear with the checkbox. + * @param aCheckState + * Contains the initial checked state of the checkbox when this method + * is called and the final checked state after this method returns. + * + * @return true for OK, false for Cancel + */ + boolean confirmCheck(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Like confirmCheck, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean confirmCheckBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Async version of confirmCheckBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { ok: boolean, checked: boolean } + */ + Promise asyncConfirmCheck(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aCheckMsg, + in boolean aCheckState); + + /** + * Button Flags + * + * The following flags are combined to form the aButtonFlags parameter passed + * to confirmEx. See confirmEx for more information on how the flags may be + * combined. + */ + + /** + * Button Position Flags + */ + const unsigned long BUTTON_POS_0 = 1; + const unsigned long BUTTON_POS_1 = 1 << 8; + const unsigned long BUTTON_POS_2 = 1 << 16; + + /** + * Button Title Flags (used to set the labels of buttons in the prompt) + */ + const unsigned long BUTTON_TITLE_OK = 1; + const unsigned long BUTTON_TITLE_CANCEL = 2; + const unsigned long BUTTON_TITLE_YES = 3; + const unsigned long BUTTON_TITLE_NO = 4; + const unsigned long BUTTON_TITLE_SAVE = 5; + const unsigned long BUTTON_TITLE_DONT_SAVE = 6; + const unsigned long BUTTON_TITLE_REVERT = 7; + const unsigned long BUTTON_TITLE_IS_STRING = 127; + + /** + * Button Default Flags (used to select which button is the default one) + */ + const unsigned long BUTTON_POS_0_DEFAULT = 0; + const unsigned long BUTTON_POS_1_DEFAULT = 1 << 24; + const unsigned long BUTTON_POS_2_DEFAULT = 1 << 25; + + /** + * Causes the buttons to be initially disabled. They are enabled after a + * timeout expires. The implementation may interpret this loosely as the + * intent is to ensure that the user does not click through a security dialog + * too quickly. Strictly speaking, the implementation could choose to ignore + * this flag. + */ + const unsigned long BUTTON_DELAY_ENABLE = 1 << 26; + + /** + * Causes a spinner to be displayed in the dialog box. + */ + const unsigned long SHOW_SPINNER = 1 << 27; + + /** + * Selects the standard set of OK/Cancel buttons. + */ + const unsigned long STD_OK_CANCEL_BUTTONS = (BUTTON_TITLE_OK * BUTTON_POS_0) + + (BUTTON_TITLE_CANCEL * BUTTON_POS_1); + + /** + * Selects the standard set of Yes/No buttons. + */ + const unsigned long STD_YES_NO_BUTTONS = (BUTTON_TITLE_YES * BUTTON_POS_0) + + (BUTTON_TITLE_NO * BUTTON_POS_1); + + // Indicates whether a prompt should be shown in-content, on tab level or as a separate window + const unsigned long MODAL_TYPE_CONTENT = 1; + const unsigned long MODAL_TYPE_TAB = 2; + const unsigned long MODAL_TYPE_WINDOW = 3; + // Like MODAL_TYPE_WINDOW, but shown inside a parent window (with similar + // styles as _TAB and _CONTENT types) rather than as a new window: + const unsigned long MODAL_TYPE_INTERNAL_WINDOW = 4; + + /** + * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aButtonFlags + * A combination of Button Flags. + * @param aButton0Title + * Used when button 0 uses TITLE_IS_STRING + * @param aButton1Title + * Used when button 1 uses TITLE_IS_STRING + * @param aButton2Title + * Used when button 2 uses TITLE_IS_STRING + * @param aCheckMsg + * Text to appear with the checkbox. Null if no checkbox. + * @param aCheckState + * Contains the initial checked state of the checkbox when this method + * is called and the final checked state after this method returns. + * + * @return index of the button pressed. + * + * Buttons are numbered 0 - 2. The implementation can decide whether the + * sequence goes from right to left or left to right. Button 0 is the + * default button unless one of the Button Default Flags is specified. + * + * A button may use a predefined title, specified by one of the Button Title + * Flags values. Each title value can be multiplied by a position value to + * assign the title to a particular button. If BUTTON_TITLE_IS_STRING is + * used for a button, the string parameter for that button will be used. If + * the value for a button position is zero, the button will not be shown. + * + * In general, aButtonFlags is constructed per the following example: + * + * aButtonFlags = (BUTTON_POS_0) * (BUTTON_TITLE_AAA) + + * (BUTTON_POS_1) * (BUTTON_TITLE_BBB) + + * BUTTON_POS_1_DEFAULT; + * + * where "AAA" and "BBB" correspond to one of the button titles. + */ + int32_t confirmEx(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + in unsigned long aButtonFlags, + in wstring aButton0Title, + in wstring aButton1Title, + in wstring aButton2Title, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Like confirmEx, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + int32_t confirmExBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in unsigned long aButtonFlags, + in wstring aButton0Title, + in wstring aButton1Title, + in wstring aButton2Title, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Async version of confirmExBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { checked: boolean, buttonNumClicked: int } + */ + Promise asyncConfirmEx(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in unsigned long aButtonFlags, + in wstring aButton0Title, + in wstring aButton1Title, + in wstring aButton2Title, + in wstring aCheckMsg, + in boolean aCheckState, + [optional] in jsval aExtraArgs); + /** + * Puts up a dialog with an edit field and an optional, labeled checkbox. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aValue + * Contains the default value for the dialog field when this method + * is called (null value is ok). Upon return, if the user pressed + * OK, then this parameter contains a newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param aCheckMsg + * Text to appear with the checkbox. If null, check box will not be shown. + * @param aCheckState + * Contains the initial checked state of the checkbox when this method + * is called and the final checked state after this method returns. + * + * @return true for OK, false for Cancel. + */ + boolean prompt(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + inout wstring aValue, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Like prompt, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean promptBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + inout wstring aValue, + in wstring aCheckMsg, + inout boolean aCheckState); + /** + * Async version of promptBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { checked: boolean, value: string, ok: boolean } + */ + Promise asyncPrompt(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aValue, + in wstring aCheckMsg, + in boolean aCheckState); + + /** + * Puts up a dialog with an edit field and a password field. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aUsername + * Contains the default value for the username field when this method + * is called (null value is ok). Upon return, if the user pressed OK, + * then this parameter contains a newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param aPassword + * Contains the default value for the password field when this method + * is called (null value is ok). Upon return, if the user pressed OK, + * then this parameter contains a newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * + * @return true for OK, false for Cancel. + */ + boolean promptUsernameAndPassword(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + inout wstring aUsername, + inout wstring aPassword); + /** + * Like promptUsernameAndPassword, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean promptUsernameAndPasswordBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + inout wstring aUsername, + inout wstring aPassword); + /** + * Async version of promptUsernameAndPasswordBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { user: string, pass: string, ok: boolean } + */ + Promise asyncPromptUsernameAndPassword(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aUsername, + in wstring aPassword); + + /** + * Puts up a dialog with a password field. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aPassword + * Contains the default value for the password field when this method + * is called (null value is ok). Upon return, if the user pressed OK, + * then this parameter contains a newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * + * @return true for OK, false for Cancel. + */ + boolean promptPassword(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + inout wstring aPassword); + /** + * Like promptPassword, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean promptPasswordBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + inout wstring aPassword); + /** + * Async version of promptPasswordBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { pass: string, ok: boolean } + */ + Promise asyncPromptPassword(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in wstring aPassword); + /** + * Puts up a dialog box which has a list box of strings from which the user + * may make a single selection. + * + * @param aParent + * The parent window or null. + * @param aDialogTitle + * Text to appear in the title of the dialog. + * @param aText + * Text to appear in the body of the dialog. + * @param aSelectList + * The list of strings to display. + * @param aOutSelection + * Contains the index of the selected item in the list when this + * method returns true. + * + * @return true for OK, false for Cancel. + */ + boolean select(in mozIDOMWindowProxy aParent, + in wstring aDialogTitle, + in wstring aText, + in Array<AString> aSelectList, + out long aOutSelection); + /** + * Like select, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean selectBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in Array<AString> aSelectList, + out long aOutSelection); + /** + * Async version of selectBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { selected: int, ok: boolean } + */ + Promise asyncSelect(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in wstring aDialogTitle, + in wstring aText, + in Array<AString> aSelectList); + + // NOTE: These functions differ from their nsIAuthPrompt counterparts by + // having additional checkbox parameters + // + // See nsIAuthPrompt2 for documentation on the semantics of the other + // parameters. + boolean promptAuth(in mozIDOMWindowProxy aParent, + in nsIChannel aChannel, + in uint32_t level, + in nsIAuthInformation authInfo); + /** + * Like promptAuth, but with a BrowsingContext as parent. + * + * @param aBrowsingContext + * The browsing context the prompt should be opened for. + * @param modalType + * Whether the prompt should be window, tab or content modal. + */ + boolean promptAuthBC(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in nsIChannel aChannel, + in uint32_t level, + in nsIAuthInformation authInfo); + /** + * Async version of promptAuthBC + * + * @return A promise which resolves when the prompt is dismissed. + * + * @resolves nsIPropertyBag { ok: boolean } + */ + Promise asyncPromptAuth(in BrowsingContext aBrowsingContext, + in unsigned long modalType, + in nsIChannel aChannel, + in uint32_t level, + in nsIAuthInformation authInfo); + + /** + * Displays a contextmenu to get user confirmation for clipboard read. Only + * one context menu can be opened at a time. + * + * @param aWindow + * The window context that initiates the clipboard operation. + * + * @return A promise which resolves when the contextmenu is dismissed. + * + * @resolves nsIPropertyBag { ok: boolean } + */ + Promise confirmUserPaste(in WindowGlobalParent aWindow); +}; diff --git a/toolkit/components/windowwatcher/nsIWindowWatcher.idl b/toolkit/components/windowwatcher/nsIWindowWatcher.idl new file mode 100644 index 0000000000..85a40970d0 --- /dev/null +++ b/toolkit/components/windowwatcher/nsIWindowWatcher.idl @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIObserver; +interface nsIPrompt; +interface nsIAuthPrompt; +interface nsISimpleEnumerator; +interface nsIWebBrowserChrome; +interface nsIWindowCreator; + + +/** + * nsIWindowWatcher is the keeper of Gecko/DOM Windows. It maintains + * a list of open top-level windows, and allows some operations on them. + + * Usage notes: + + * This component has an |activeWindow| property. Clients may expect + * this property to be always current, so to properly integrate this component + * the application will need to keep it current by setting the property + * as the active window changes. + * This component should not keep a (XPCOM) reference to any windows; + * the implementation will claim no ownership. Windows must notify + * this component when they are created or destroyed, so only a weak + * reference is kept. Note that there is no interface for such notifications + * (not a public one, anyway). This is taken care of both in Mozilla and + * by common embedding code. Embedding clients need do nothing special + * about that requirement. + * This component must be initialized at application startup by calling + * setWindowCreator. + */ +[scriptable, uuid(641fe945-6902-4b3f-87c2-0daef32499b3)] +interface nsIWindowWatcher : nsISupports { + + /** Create a new window. It will automatically be added to our list + (via addWindow()). + @param aParent parent window, if any. Null if no parent. If it is + impossible to get to an nsIWebBrowserChrome from aParent, this + method will effectively act as if aParent were null. + @param aURL url to which to open the new window. Must already be + escaped, if applicable. can be null. + @param aName window name from JS window.open. can be null. If a window + with this name already exists, the openWindow call may just load + aUrl in it (if aUrl is not null) and return it. + @param aFeatures window features from JS window.open. can be null. + @param aArguments extra argument(s) to the new window, to be attached + as the |arguments| property. An nsIArray will be + unwound into multiple arguments (but not recursively!). + can be null. + @return the new window + + @note This method may examine the JS context stack for purposes of + determining the security context to use for the search for a given + window named aName. + @note This method should try to set the default charset for the new + window to the default charset of aParent. This is not guaranteed, + however. + @note This method may dispatch a "toplevel-window-ready" notification + via nsIObserverService if the window did not already exist. + */ + mozIDOMWindowProxy openWindow(in mozIDOMWindowProxy aParent, in ACString aUrl, + in ACString aName, in ACString aFeatures, + in nsISupports aArguments); + + /** Clients of this service can register themselves to be notified + when a window is opened or closed (added to or removed from this + service). This method adds an aObserver to the list of objects + to be notified. + @param aObserver the object to be notified when windows are + opened or closed. Its Observe method will be + called with the following parameters: + + aObserver::Observe interprets its parameters so: + aSubject the window being opened or closed, sent as an nsISupports + which can be QIed to an nsIDOMWindow. + aTopic a wstring, either "domwindowopened" or "domwindowclosed". + someData not used. + */ + void registerNotification(in nsIObserver aObserver); + + /** Clients of this service can register themselves to be notified + when a window is opened or closed (added to or removed from this + service). This method removes an aObserver from the list of objects + to be notified. + @param aObserver the observer to be removed. + */ + void unregisterNotification(in nsIObserver aObserver); + + /** Get an iterator for currently open windows in the order they were opened, + guaranteeing that each will be visited exactly once. + @return an enumerator which will itself return nsISupports objects which + can be QIed to an nsIDOMWindow + */ + + nsISimpleEnumerator getWindowEnumerator(); + + /** Return a newly created nsIPrompt implementation. + @param aParent the parent window used for posing alerts. can be null. + @return a new nsIPrompt object + */ + + nsIPrompt getNewPrompter(in mozIDOMWindowProxy aParent); + + /** Return a newly created nsIAuthPrompt implementation. + @param aParent the parent window used for posing alerts. can be null. + @return a new nsIAuthPrompt object + */ + + nsIAuthPrompt getNewAuthPrompter(in mozIDOMWindowProxy aParent); + + /** Set the window creator callback. It must be filled in by the app. + openWindow will use it to create new windows. + @param creator the callback. if null, the callback will be cleared + and window creation capabilities lost. + */ + void setWindowCreator(in nsIWindowCreator creator); + + /** Returns true if a window creator callback has been set, false otherwise. + */ + boolean hasWindowCreator(); + + + /** Retrieve the chrome window mapped to the given DOM window. Window + Watcher keeps a list of all top-level DOM windows currently open, + along with their corresponding chrome interfaces. Since DOM Windows + lack a (public) means of retrieving their corresponding chrome, + this method will do that. + @param aWindow the DOM window whose chrome window the caller needs + @return the corresponding chrome window + */ + nsIWebBrowserChrome getChromeForWindow(in mozIDOMWindowProxy aWindow); + + /** + Retrieve an existing chrome window (or frame). + @param aTargetName the window name + + Note: This method will not consider special names like "_blank", "_top", + "_self", or "_parent", as there is no reference window. + + Note: This method will search all open windows for any window or + frame with the given window name. Make sure you understand the + security implications of this before using this method! + */ + mozIDOMWindowProxy getWindowByName(in AString aTargetName); + + /** + Retrieves the active window from the focus manager. + */ + readonly attribute mozIDOMWindowProxy activeWindow; + +}; + +%{C++ +#define NS_WINDOWWATCHER_CONTRACTID "@mozilla.org/embedcomp/window-watcher;1" +%} diff --git a/toolkit/components/windowwatcher/nsOpenWindowInfo.cpp b/toolkit/components/windowwatcher/nsOpenWindowInfo.cpp new file mode 100644 index 0000000000..4374090075 --- /dev/null +++ b/toolkit/components/windowwatcher/nsOpenWindowInfo.cpp @@ -0,0 +1,89 @@ +/* -*- 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 "nsOpenWindowInfo.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/BrowserParent.h" + +NS_IMPL_ISUPPORTS(nsOpenWindowInfo, nsIOpenWindowInfo) + +NS_IMETHODIMP nsOpenWindowInfo::GetParent( + mozilla::dom::BrowsingContext** aParent) { + *aParent = do_AddRef(mParent).take(); + return NS_OK; +} + +NS_IMETHODIMP nsOpenWindowInfo::GetIsRemote(bool* aIsRemote) { + *aIsRemote = mIsRemote; + return NS_OK; +} + +NS_IMETHODIMP nsOpenWindowInfo::GetIsForWindowDotPrint(bool* aResult) { + *aResult = mIsForWindowDotPrint; + return NS_OK; +} + +NS_IMETHODIMP nsOpenWindowInfo::GetIsForPrinting(bool* aIsForPrinting) { + *aIsForPrinting = mIsForPrinting; + return NS_OK; +} + +NS_IMETHODIMP nsOpenWindowInfo::GetForceNoOpener(bool* aForceNoOpener) { + *aForceNoOpener = mForceNoOpener; + return NS_OK; +} + +NS_IMETHODIMP nsOpenWindowInfo::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle<JS::Value> aAttrs) { + bool ok = ToJSValue(aCx, mOriginAttributes, aAttrs); + NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE); + return NS_OK; +} + +const mozilla::OriginAttributes& nsOpenWindowInfo::GetOriginAttributes() { + return mOriginAttributes; +} + +mozilla::dom::BrowserParent* nsOpenWindowInfo::GetNextRemoteBrowser() { + return mNextRemoteBrowser; +} + +nsIBrowsingContextReadyCallback* +nsOpenWindowInfo::BrowsingContextReadyCallback() { + return mBrowsingContextReadyCallback; +} + +NS_IMPL_ISUPPORTS(nsBrowsingContextReadyCallback, + nsIBrowsingContextReadyCallback) + +nsBrowsingContextReadyCallback::nsBrowsingContextReadyCallback( + RefPtr<mozilla::dom::BrowsingContextCallbackReceivedPromise::Private> + aPromise) + : mPromise(std::move(aPromise)) {} + +nsBrowsingContextReadyCallback::~nsBrowsingContextReadyCallback() { + if (mPromise) { + mPromise->Reject(NS_ERROR_FAILURE, __func__); + } + mPromise = nullptr; +} + +NS_IMETHODIMP nsBrowsingContextReadyCallback::BrowsingContextReady( + mozilla::dom::BrowsingContext* aBC) { + MOZ_DIAGNOSTIC_ASSERT(mPromise, + "The 'browsing context ready' callback is null"); + if (!mPromise) { + return NS_OK; + } + if (aBC) { + mPromise->Resolve(aBC, __func__); + } else { + mPromise->Reject(NS_ERROR_FAILURE, __func__); + } + mPromise = nullptr; + return NS_OK; +} diff --git a/toolkit/components/windowwatcher/nsOpenWindowInfo.h b/toolkit/components/windowwatcher/nsOpenWindowInfo.h new file mode 100644 index 0000000000..8a353dda6b --- /dev/null +++ b/toolkit/components/windowwatcher/nsOpenWindowInfo.h @@ -0,0 +1,50 @@ +/* -*- 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 nsOpenWindowInfo_h +#define nsOpenWindowInfo_h + +#include "nsIOpenWindowInfo.h" +#include "nsISupportsImpl.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/ClientOpenWindowUtils.h" + +class nsOpenWindowInfo : public nsIOpenWindowInfo { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOPENWINDOWINFO + + bool mForceNoOpener = false; + bool mIsRemote = false; + bool mIsForPrinting = false; + bool mIsForWindowDotPrint = false; + RefPtr<mozilla::dom::BrowserParent> mNextRemoteBrowser; + mozilla::OriginAttributes mOriginAttributes; + RefPtr<mozilla::dom::BrowsingContext> mParent; + RefPtr<nsIBrowsingContextReadyCallback> mBrowsingContextReadyCallback; + + private: + virtual ~nsOpenWindowInfo() = default; +}; + +class nsBrowsingContextReadyCallback : public nsIBrowsingContextReadyCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBROWSINGCONTEXTREADYCALLBACK + + explicit nsBrowsingContextReadyCallback( + RefPtr<mozilla::dom::BrowsingContextCallbackReceivedPromise::Private> + aPromise); + + private: + virtual ~nsBrowsingContextReadyCallback(); + + RefPtr<mozilla::dom::BrowsingContextCallbackReceivedPromise::Private> + mPromise; +}; + +#endif // nsOpenWindowInfo_h diff --git a/toolkit/components/windowwatcher/nsPIPromptService.idl b/toolkit/components/windowwatcher/nsPIPromptService.idl new file mode 100644 index 0000000000..3bff27a993 --- /dev/null +++ b/toolkit/components/windowwatcher/nsPIPromptService.idl @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +/* The general dialog posing function within nsPromptService, for + private consumption, only. */ + +#include "nsISupports.idl" + +interface nsIDOMWindow; +interface nsIDialogParamBlock; + +[uuid(C60A1955-6CB3-4827-8EF8-4F5C668AF0B3)] +interface nsPIPromptService : nsISupports +{ +%{C++ + // eOpeningSound is obsolete but we need to support it for the compatibility. + // The implementers should use eSoundEventId instead. + enum {eMsg=0, eCheckboxMsg=1, eIconClass=2, eTitleMessage=3, eEditfield1Msg=4, + eEditfield2Msg=5, eEditfield1Value=6, eEditfield2Value=7, + eButton0Text=8, eButton1Text=9, eButton2Text=10, eButton3Text=11, + eDialogTitle=12, eOpeningSound=13}; + enum {eButtonPressed=0, eCheckboxState=1, eNumberButtons=2, + eNumberEditfields=3, eEditField1Password=4, eDefaultButton=5, + eDelayButtonEnable=6, eSoundEventId=7}; +%} + + void doDialog(in nsIDOMWindow aParent, in nsIDialogParamBlock aParamBlock, in string aChromeURL); +}; diff --git a/toolkit/components/windowwatcher/nsPIWindowWatcher.idl b/toolkit/components/windowwatcher/nsPIWindowWatcher.idl new file mode 100644 index 0000000000..a780c7e251 --- /dev/null +++ b/toolkit/components/windowwatcher/nsPIWindowWatcher.idl @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +/* Private "control" methods on the Window Watcher. These are annoying + bookkeeping methods, not part of the public (embedding) interface. +*/ + +#include "nsISupports.idl" + +%{ C++ +#include "mozilla/dom/UserActivation.h" +class nsDocShellLoadState; +namespace mozilla::dom { +class WindowFeatures; +} +%} + +webidl BrowsingContext; +interface mozIDOMWindowProxy; +interface nsISimpleEnumerator; +interface nsIWebBrowserChrome; +interface nsIDocShellTreeItem; +interface nsIArray; +interface nsIRemoteTab; +interface nsIOpenWindowInfo; +native nsDocShellLoadStatePtr(nsDocShellLoadState*); +[ref] native WindowFeaturesRef(const mozilla::dom::WindowFeatures); +[ref] native UserActivationModifiersRef(const mozilla::dom::UserActivation::Modifiers); + +[uuid(d162f9c4-19d5-4723-931f-f1e51bfa9f68)] +interface nsPIWindowWatcher : nsISupports +{ + /** A window has been created. Add it to our list. + @param aWindow the window to add + @param aChrome the corresponding chrome window. The DOM window + and chrome will be mapped together, and the corresponding + chrome can be retrieved using the (not private) + method getChromeForWindow. If null, any extant mapping + will be cleared. + */ + void addWindow(in mozIDOMWindowProxy aWindow, + in nsIWebBrowserChrome aChrome); + + /** A window has been closed. Remove it from our list. + @param aWindow the window to remove + */ + void removeWindow(in mozIDOMWindowProxy aWindow); + + cenum PrintKind : 8 { + PRINT_NONE, + PRINT_INTERNAL, + PRINT_WINDOW_DOT_PRINT, + }; + + /** Like the public interface's open(), but can handle openDialog-style + arguments and calls which shouldn't result in us navigating the window. + + @param aParent parent window, if any. Null if no parent. If it is + impossible to get to an nsIWebBrowserChrome from aParent, this + method will effectively act as if aParent were null. + @param aURL url to which to open the new window. Must already be + escaped, if applicable. can be null. + @param aName window name from JS window.open. can be null. If a window + with this name already exists, the openWindow call may just load + aUrl in it (if aUrl is not null) and return it. + @param aFeatures window features from JS window.open. can be null. + @param aModifiers The modifiers associated with the user activation, + or UserActivation::Modifiers::None() if this is not initiated by + user activation. This is used to determine where the new window is + located (e.g. new foreground tab, new background tab, new window). + @param aCalledFromScript true if we were called from script. + @param aDialog use dialog defaults (see nsGlobalWindowOuter::OpenInternal) + @param aNavigate true if we should navigate the new window to the + specified URL. + @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. + @param aLoadState if aNavigate is true, this allows the caller to pass in + an nsIDocShellLoadState to use for the navigation. + Callers can pass in null if they want the windowwatcher + to just construct a loadinfo itself. If aNavigate is + false, this argument is ignored. + + @return the new window + + @note This method may examine the JS context stack for purposes of + determining the security context to use for the search for a given + window named aName. + @note This method should try to set the default charset for the new + window to the default charset of the document in the calling window + (which is determined based on the JS stack and the value of + aParent). This is not guaranteed, however. + */ + [noscript] + BrowsingContext openWindow2(in mozIDOMWindowProxy aParent, in ACString aUrl, + in ACString aName, in ACString aFeatures, + in UserActivationModifiersRef aModifiers, + in boolean aCalledFromScript, + in boolean aDialog, + in boolean aNavigate, + in nsISupports aArgs, + in boolean aIsPopupSpam, + in boolean aForceNoOpener, + in boolean aForceNoReferrer, + in nsPIWindowWatcher_PrintKind aPrintKind, + in nsDocShellLoadStatePtr aLoadState); + + /** + * Opens a new window so that the window that aOpeningTab belongs to + * is set as the parent window. The newly opened window will also + * inherit load context information from aOpeningTab. + * + * @param aOpeningTab + * The nsIRemoteTab that is requesting the new window be opened. + * @param aFeatures + * Window features if called with window.open or similar. + * @param aModifiers + * The modifiers associated with the user activation, or + * UserActivation::Modifiers::None() if this is not initiated by + * user activation. + * @param aCalledFromJS + * True if called via window.open or similar. + * @param aOpenerFullZoom + * The current zoom multiplier for the opener tab. This is then + * applied to the newly opened window. + * @param aOpenWindowInfo + * Information used to create the initial content browser in the new + * window. + * + * @return the nsIRemoteTab of the initial browser for the newly opened + * window. + */ + nsIRemoteTab openWindowWithRemoteTab(in nsIRemoteTab aOpeningTab, + in WindowFeaturesRef aFeatures, + in UserActivationModifiersRef aModifiers, + in boolean aCalledFromJS, + in float aOpenerFullZoom, + in nsIOpenWindowInfo aOpenWindowInfo); +}; diff --git a/toolkit/components/windowwatcher/nsPromptUtils.h b/toolkit/components/windowwatcher/nsPromptUtils.h new file mode 100644 index 0000000000..4ff85b00af --- /dev/null +++ b/toolkit/components/windowwatcher/nsPromptUtils.h @@ -0,0 +1,137 @@ +/* -*- 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 NSPROMPTUTILS_H_ +#define NSPROMPTUTILS_H_ + +#include "nsIHttpChannel.h" + +/** + * @file + * This file defines some helper functions that simplify interaction + * with authentication prompts. + */ + +/** + * Given a username (possibly in DOMAIN\user form) and password, parses the + * domain out of the username if necessary and sets domain, username and + * password on the auth information object. + */ +inline void NS_SetAuthInfo(nsIAuthInformation* aAuthInfo, const nsString& aUser, + const nsString& aPassword) { + uint32_t flags; + aAuthInfo->GetFlags(&flags); + if (flags & nsIAuthInformation::NEED_DOMAIN) { + // Domain is separated from username by a backslash + int32_t idx = aUser.FindChar(char16_t('\\')); + if (idx == kNotFound) { + aAuthInfo->SetUsername(aUser); + } else { + aAuthInfo->SetDomain(Substring(aUser, 0, idx)); + aAuthInfo->SetUsername(Substring(aUser, idx + 1)); + } + } else { + aAuthInfo->SetUsername(aUser); + } + aAuthInfo->SetPassword(aPassword); +} + +/** + * Gets the host and port from a channel and authentication info. This is the + * "logical" host and port for this authentication, i.e. for a proxy + * authentication it refers to the proxy, while for a host authentication it + * is the actual host. + * + * @param machineProcessing + * When this parameter is true, the host will be returned in ASCII + * (instead of UTF-8; this is relevant when IDN is used). In addition, + * the port will be returned as the real port even when it was not + * explicitly specified (when false, the port will be returned as -1 in + * this case) + */ +inline void NS_GetAuthHostPort(nsIChannel* aChannel, + nsIAuthInformation* aAuthInfo, + bool aMachineProcessing, nsCString& aHost, + int32_t* aPort) { + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + // Have to distinguish proxy auth and host auth here... + uint32_t flags; + aAuthInfo->GetFlags(&flags); + if (flags & nsIAuthInformation::AUTH_PROXY) { + nsCOMPtr<nsIProxiedChannel> proxied(do_QueryInterface(aChannel)); + NS_ASSERTION(proxied, "proxy auth needs nsIProxiedChannel"); + + nsCOMPtr<nsIProxyInfo> info; + proxied->GetProxyInfo(getter_AddRefs(info)); + NS_ASSERTION(info, "proxy auth needs nsIProxyInfo"); + + nsAutoCString idnhost; + info->GetHost(idnhost); + info->GetPort(aPort); + + if (aMachineProcessing) { + nsCOMPtr<nsIIDNService> idnService = + do_GetService(NS_IDNSERVICE_CONTRACTID); + if (idnService) { + idnService->ConvertUTF8toACE(idnhost, aHost); + } else { + // Not much we can do here... + aHost = idnhost; + } + } else { + aHost = idnhost; + } + } else { + if (aMachineProcessing) { + uri->GetAsciiHost(aHost); + *aPort = NS_GetRealPort(uri); + } else { + uri->GetHost(aHost); + uri->GetPort(aPort); + } + } +} + +/** + * Creates the key for looking up passwords in the password manager. This + * function uses the same format that Gecko functions have always used, thus + * ensuring backwards compatibility. + */ +inline void NS_GetAuthKey(nsIChannel* aChannel, nsIAuthInformation* aAuthInfo, + nsCString& aKey) { + // HTTP does this differently from other protocols + nsCOMPtr<nsIHttpChannel> http(do_QueryInterface(aChannel)); + if (!http) { + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + uri->GetPrePath(aKey); + return; + } + + // NOTE: For backwards-compatibility reasons, this must be the ASCII host. + nsCString host; + int32_t port = -1; + + NS_GetAuthHostPort(aChannel, aAuthInfo, true, host, &port); + + nsAutoString realm; + aAuthInfo->GetRealm(realm); + + // Now assemble the key: host:port (realm) + aKey.Append(host); + aKey.Append(':'); + aKey.AppendInt(port); + aKey.AppendLiteral(" ("); + AppendUTF16toUTF8(realm, aKey); + aKey.Append(')'); +} + +#endif diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp new file mode 100644 index 0000000000..209af157a3 --- /dev/null +++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp @@ -0,0 +1,2548 @@ +/* -*- 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 "nsWindowWatcher.h" +#include "nsAutoWindowStateHelper.h" + +#include "nsCRT.h" +#include "nsDebug.h" +#include "nsNetUtil.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsISimpleEnumerator.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsJSUtils.h" + +#include "nsDocShell.h" +#include "nsGlobalWindowInner.h" +#include "nsGlobalWindowOuter.h" +#include "nsHashPropertyBag.h" +#include "nsIBaseWindow.h" +#include "nsIBrowserDOMWindow.h" +#include "nsIDocShell.h" +#include "nsDocShellLoadState.h" +#include "nsIDocShellTreeItem.h" +#include "nsIDocShellTreeOwner.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowsingContextGroup.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/UserActivation.h" +#include "nsIDragService.h" +#include "nsIPrompt.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIScreen.h" +#include "nsIScreenManager.h" +#include "nsIScriptContext.h" +#include "nsIObserverService.h" +#include "nsXPCOM.h" +#include "nsIURI.h" +#include "nsIWebBrowser.h" +#include "nsIWebBrowserChrome.h" +#include "nsIWebNavigation.h" +#include "nsIWindowCreator.h" +#include "nsIXULRuntime.h" +#include "nsPIDOMWindow.h" +#include "nsIWindowProvider.h" +#include "nsIMutableArray.h" +#include "nsIDOMStorageManager.h" +#include "nsIWidget.h" +#include "nsFocusManager.h" +#include "nsOpenWindowInfo.h" +#include "nsPresContext.h" +#include "nsContentUtils.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsSandboxFlags.h" +#include "nsSimpleEnumerator.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_full_screen_api.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Storage.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/BrowserHost.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/SessionStorageManager.h" +#include "nsIAppWindow.h" +#include "nsIXULBrowserWindow.h" +#include "ReferrerInfo.h" + +using namespace mozilla; +using namespace mozilla::dom; + +/**************************************************************** + ******************** nsWatcherWindowEntry ********************** + ****************************************************************/ + +class nsWindowWatcher; + +struct nsWatcherWindowEntry { + nsWatcherWindowEntry(mozIDOMWindowProxy* aWindow, + nsIWebBrowserChrome* aChrome) + : mChrome(nullptr) { + mWindow = aWindow; + nsCOMPtr<nsISupportsWeakReference> supportsweak(do_QueryInterface(aChrome)); + if (supportsweak) { + supportsweak->GetWeakReference(getter_AddRefs(mChromeWeak)); + } else { + mChrome = aChrome; + mChromeWeak = nullptr; + } + ReferenceSelf(); + } + ~nsWatcherWindowEntry() = default; + + void InsertAfter(nsWatcherWindowEntry* aOlder); + void Unlink(); + void ReferenceSelf(); + + mozIDOMWindowProxy* mWindow; + nsIWebBrowserChrome* mChrome; + nsWeakPtr mChromeWeak; + // each struct is in a circular, doubly-linked list + nsWatcherWindowEntry* mYounger; // next younger in sequence + nsWatcherWindowEntry* mOlder; +}; + +void nsWatcherWindowEntry::InsertAfter(nsWatcherWindowEntry* aOlder) { + if (aOlder) { + mOlder = aOlder; + mYounger = aOlder->mYounger; + mOlder->mYounger = this; + if (mOlder->mOlder == mOlder) { + mOlder->mOlder = this; + } + mYounger->mOlder = this; + if (mYounger->mYounger == mYounger) { + mYounger->mYounger = this; + } + } +} + +void nsWatcherWindowEntry::Unlink() { + mOlder->mYounger = mYounger; + mYounger->mOlder = mOlder; + ReferenceSelf(); +} + +void nsWatcherWindowEntry::ReferenceSelf() { + mYounger = this; + mOlder = this; +} + +/**************************************************************** + ****************** nsWatcherWindowEnumerator ******************* + ****************************************************************/ + +class nsWatcherWindowEnumerator : public nsSimpleEnumerator { + public: + explicit nsWatcherWindowEnumerator(nsWindowWatcher* aWatcher); + NS_IMETHOD HasMoreElements(bool* aResult) override; + NS_IMETHOD GetNext(nsISupports** aResult) override; + + protected: + ~nsWatcherWindowEnumerator() override; + + private: + friend class nsWindowWatcher; + + nsWatcherWindowEntry* FindNext(); + void WindowRemoved(nsWatcherWindowEntry* aInfo); + + nsWindowWatcher* mWindowWatcher; + nsWatcherWindowEntry* mCurrentPosition; +}; + +nsWatcherWindowEnumerator::nsWatcherWindowEnumerator(nsWindowWatcher* aWatcher) + : mWindowWatcher(aWatcher), mCurrentPosition(aWatcher->mOldestWindow) { + mWindowWatcher->AddEnumerator(this); + mWindowWatcher->AddRef(); +} + +nsWatcherWindowEnumerator::~nsWatcherWindowEnumerator() { + mWindowWatcher->RemoveEnumerator(this); + mWindowWatcher->Release(); +} + +NS_IMETHODIMP +nsWatcherWindowEnumerator::HasMoreElements(bool* aResult) { + if (!aResult) { + return NS_ERROR_INVALID_ARG; + } + + *aResult = !!mCurrentPosition; + return NS_OK; +} + +NS_IMETHODIMP +nsWatcherWindowEnumerator::GetNext(nsISupports** aResult) { + if (!aResult) { + return NS_ERROR_INVALID_ARG; + } + + *aResult = nullptr; + + if (mCurrentPosition) { + CallQueryInterface(mCurrentPosition->mWindow, aResult); + mCurrentPosition = FindNext(); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +nsWatcherWindowEntry* nsWatcherWindowEnumerator::FindNext() { + nsWatcherWindowEntry* info; + + if (!mCurrentPosition) { + return nullptr; + } + + info = mCurrentPosition->mYounger; + return info == mWindowWatcher->mOldestWindow ? nullptr : info; +} + +// if a window is being removed adjust the iterator's current position +void nsWatcherWindowEnumerator::WindowRemoved(nsWatcherWindowEntry* aInfo) { + if (mCurrentPosition == aInfo) { + mCurrentPosition = + mCurrentPosition != aInfo->mYounger ? aInfo->mYounger : nullptr; + } +} + +/**************************************************************** + *********************** nsWindowWatcher ************************ + ****************************************************************/ + +NS_IMPL_ADDREF(nsWindowWatcher) +NS_IMPL_RELEASE(nsWindowWatcher) +NS_IMPL_QUERY_INTERFACE(nsWindowWatcher, nsIWindowWatcher, nsIPromptFactory, + nsPIWindowWatcher) + +nsWindowWatcher::nsWindowWatcher() + : mOldestWindow(nullptr), mListLock("nsWindowWatcher.mListLock") {} + +nsWindowWatcher::~nsWindowWatcher() { + // delete data + while (mOldestWindow) { + RemoveWindow(mOldestWindow); + } +} + +nsresult nsWindowWatcher::Init() { return NS_OK; } + +/** + * Convert aArguments into either an nsIArray or nullptr. + * + * - If aArguments is nullptr, return nullptr. + * - If aArguments is an nsArray, return nullptr if it's empty, or otherwise + * return the array. + * - If aArguments is an nsIArray, return nullptr if it's empty, or + * otherwise just return the array. + * - Otherwise, return an nsIArray with one element: aArguments. + */ +static already_AddRefed<nsIArray> ConvertArgsToArray(nsISupports* aArguments) { + if (!aArguments) { + return nullptr; + } + + nsCOMPtr<nsIArray> array = do_QueryInterface(aArguments); + if (array) { + uint32_t argc = 0; + array->GetLength(&argc); + if (argc == 0) { + return nullptr; + } + + return array.forget(); + } + + nsCOMPtr<nsIMutableArray> singletonArray = + do_CreateInstance(NS_ARRAY_CONTRACTID); + NS_ENSURE_TRUE(singletonArray, nullptr); + + nsresult rv = singletonArray->AppendElement(aArguments); + NS_ENSURE_SUCCESS(rv, nullptr); + + return singletonArray.forget(); +} + +NS_IMETHODIMP +nsWindowWatcher::OpenWindow(mozIDOMWindowProxy* aParent, const nsACString& aUrl, + const nsACString& aName, + const nsACString& aFeatures, + nsISupports* aArguments, + mozIDOMWindowProxy** aResult) { + nsCOMPtr<nsIArray> argv = ConvertArgsToArray(aArguments); + + uint32_t argc = 0; + if (argv) { + argv->GetLength(&argc); + } + bool dialog = (argc != 0); + + RefPtr<BrowsingContext> bc; + MOZ_TRY(OpenWindowInternal(aParent, aUrl, aName, aFeatures, + mozilla::dom::UserActivation::Modifiers::None(), + /* calledFromJS = */ false, dialog, + /* navigate = */ true, argv, + /* aIsPopupSpam = */ false, + /* aForceNoOpener = */ false, + /* aForceNoReferrer = */ false, PRINT_NONE, + /* aLoadState = */ nullptr, getter_AddRefs(bc))); + if (bc) { + nsCOMPtr<mozIDOMWindowProxy> win(bc->GetDOMWindow()); + win.forget(aResult); + } + return NS_OK; +} + +struct SizeSpec { + SizeSpec() = default; + + // We store these in screen-independent pixels. + Maybe<DesktopIntCoord> mLeft; + Maybe<DesktopIntCoord> mTop; + Maybe<CSSIntCoord> mOuterWidth; // Total window width + Maybe<CSSIntCoord> mOuterHeight; // Total window height + Maybe<CSSIntCoord> mInnerWidth; // Content area width + Maybe<CSSIntCoord> mInnerHeight; // Content area height + + bool mLockAspectRatio = false; + + bool PositionSpecified() const { return mLeft.isSome() || mTop.isSome(); } + + bool SizeSpecified() const { return WidthSpecified() || HeightSpecified(); } + + bool WidthSpecified() const { + return mOuterWidth.isSome() || mInnerWidth.isSome(); + } + + bool HeightSpecified() const { + return mOuterHeight.isSome() || mInnerHeight.isSome(); + } + + void ScaleBy(float aOpenerZoom) { + if (aOpenerZoom == 1.0f) { + return; + } + auto Scale = [&aOpenerZoom](auto& aValue) { + if (aValue) { + *aValue = NSToIntRound(*aValue * aOpenerZoom); + } + }; + // Scaling the position is needed to make sure that the window position is + // what the caller expects. + Scale(mLeft); + Scale(mTop); + + // Scaling these CSS sizes by the zoom factor might be a bit dubious, as the + // created window should not be zoomed, but we've done that historically... + Scale(mOuterWidth); + Scale(mOuterHeight); + Scale(mInnerWidth); + Scale(mInnerHeight); + } +}; + +static void SizeOpenedWindow(nsIDocShellTreeOwner* aTreeOwner, + mozIDOMWindowProxy* aParent, bool aIsCallerChrome, + const SizeSpec&); +static SizeSpec CalcSizeSpec(const WindowFeatures&, bool aHasChromeParent, + CSSToDesktopScale); + +NS_IMETHODIMP +nsWindowWatcher::OpenWindow2( + mozIDOMWindowProxy* aParent, const nsACString& aUrl, + const nsACString& aName, const nsACString& aFeatures, + const UserActivation::Modifiers& aModifiers, bool aCalledFromScript, + bool aDialog, bool aNavigate, nsISupports* aArguments, bool aIsPopupSpam, + bool aForceNoOpener, bool aForceNoReferrer, PrintKind aPrintKind, + nsDocShellLoadState* aLoadState, BrowsingContext** aResult) { + nsCOMPtr<nsIArray> argv = ConvertArgsToArray(aArguments); + + uint32_t argc = 0; + if (argv) { + argv->GetLength(&argc); + } + + // This is extremely messed up, but this behavior is necessary because + // callers lie about whether they're a dialog window and whether they're + // called from script. Fixing this is bug 779939. + bool dialog = aDialog; + if (!aCalledFromScript) { + dialog = argc > 0; + } + + return OpenWindowInternal(aParent, aUrl, aName, aFeatures, aModifiers, + aCalledFromScript, dialog, aNavigate, argv, + aIsPopupSpam, aForceNoOpener, aForceNoReferrer, + aPrintKind, aLoadState, aResult); +} + +// This static function checks if the aDocShell uses an UserContextId equal to +// the userContextId of subjectPrincipal, if not null. +static bool CheckUserContextCompatibility(nsIDocShell* aDocShell) { + MOZ_ASSERT(aDocShell); + + uint32_t userContextId = + static_cast<nsDocShell*>(aDocShell)->GetOriginAttributes().mUserContextId; + + nsCOMPtr<nsIPrincipal> subjectPrincipal = + nsContentUtils::GetCurrentJSContext() ? nsContentUtils::SubjectPrincipal() + : nullptr; + + // If we don't have a valid principal, probably we are in e10s mode, parent + // side. + if (!subjectPrincipal) { + return true; + } + + // DocShell can have UsercontextID set but loading a document with system + // principal. In this case, we consider everything ok. + if (subjectPrincipal->IsSystemPrincipal()) { + return true; + } + + return subjectPrincipal->GetUserContextId() == userContextId; +} + +nsresult nsWindowWatcher::CreateChromeWindow(nsIWebBrowserChrome* aParentChrome, + uint32_t aChromeFlags, + nsIOpenWindowInfo* aOpenWindowInfo, + nsIWebBrowserChrome** aResult) { + if (NS_WARN_IF(!mWindowCreator)) { + return NS_ERROR_UNEXPECTED; + } + + bool cancel = false; + if (aChromeFlags & nsIWebBrowserChrome::CHROME_OPENAS_DIALOG) { + // If there are any drag and drop operations in flight, try to end them. + nsCOMPtr<nsIDragService> ds = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (ds) { + ds->EndDragSession(true, 0); + } + } + nsCOMPtr<nsIWebBrowserChrome> newWindowChrome; + nsresult rv = mWindowCreator->CreateChromeWindow( + aParentChrome, aChromeFlags, aOpenWindowInfo, &cancel, + getter_AddRefs(newWindowChrome)); + + if (NS_SUCCEEDED(rv) && cancel) { + newWindowChrome = nullptr; + return NS_ERROR_ABORT; + } + + newWindowChrome.forget(aResult); + return NS_OK; +} + +/** + * Disable persistence of size/position in popups (determined by + * determining whether the features parameter specifies width or height + * in any way). We consider any overriding of the window's size or position + * in the open call as disabling persistence of those attributes. + * Popup windows (which should not persist size or position) generally set + * the size. + * + * @param aFeatures + * The features that was used to open the window. + * @param aTreeOwner + * The nsIDocShellTreeOwner of the newly opened window. If null, + * this function is a no-op. + */ +static void MaybeDisablePersistence(const SizeSpec& aSizeSpec, + nsIDocShellTreeOwner* aTreeOwner) { + if (!aTreeOwner) { + return; + } + + if (aSizeSpec.SizeSpecified()) { + aTreeOwner->SetPersistence(false, false, false); + } +} + +NS_IMETHODIMP +nsWindowWatcher::OpenWindowWithRemoteTab( + nsIRemoteTab* aRemoteTab, const WindowFeatures& aFeatures, + const UserActivation::Modifiers& aModifiers, bool aCalledFromJS, + float aOpenerFullZoom, nsIOpenWindowInfo* aOpenWindowInfo, + nsIRemoteTab** aResult) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(mWindowCreator); + + if (!nsContentUtils::IsSafeToRunScript()) { + nsContentUtils::WarnScriptWasIgnored(nullptr); + return NS_ERROR_FAILURE; + } + + if (NS_WARN_IF(!mWindowCreator)) { + return NS_ERROR_UNEXPECTED; + } + + bool isFissionWindow = FissionAutostart(); + bool isPrivateBrowsingWindow = + StaticPrefs::browser_privatebrowsing_autostart(); + + nsCOMPtr<nsPIDOMWindowOuter> parentWindowOuter; + RefPtr<BrowsingContext> parentBC = aOpenWindowInfo->GetParent(); + if (parentBC) { + RefPtr<Element> browserElement = parentBC->Top()->GetEmbedderElement(); + if (browserElement && browserElement->GetOwnerGlobal() && + browserElement->GetOwnerGlobal()->GetAsInnerWindow()) { + parentWindowOuter = browserElement->GetOwnerGlobal() + ->GetAsInnerWindow() + ->GetOuterWindow(); + } + + isFissionWindow = parentBC->UseRemoteSubframes(); + isPrivateBrowsingWindow = + isPrivateBrowsingWindow || parentBC->UsePrivateBrowsing(); + } + + if (!parentWindowOuter) { + // We couldn't find a browser window for the opener, so either we + // never were passed aRemoteTab, the window is closed, + // or it's in the process of closing. Either way, we'll use + // the most recently opened browser window instead. + parentWindowOuter = nsContentUtils::GetMostRecentNonPBWindow(); + } + + if (NS_WARN_IF(!parentWindowOuter)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIDocShellTreeOwner> parentTreeOwner = + parentWindowOuter->GetTreeOwner(); + if (NS_WARN_IF(!parentTreeOwner)) { + return NS_ERROR_UNEXPECTED; + } + + if (NS_WARN_IF(!mWindowCreator)) { + return NS_ERROR_UNEXPECTED; + } + + // get various interfaces for aDocShellItem, used throughout this method + CSSToDesktopScale cssToDesktopScale(1.0f); + if (nsCOMPtr<nsIBaseWindow> win = do_QueryInterface(parentTreeOwner)) { + cssToDesktopScale = win->GetUnscaledCSSToDesktopScale(); + } + SizeSpec sizeSpec = CalcSizeSpec(aFeatures, false, cssToDesktopScale); + sizeSpec.ScaleBy(aOpenerFullZoom); + + // This is not initiated by window.open call in content context, and we + // don't need to propagate isPopupRequested out-parameter to the resulting + // browsing context. + bool unused = false; + uint32_t chromeFlags = + CalculateChromeFlagsForContent(aFeatures, aModifiers, &unused); + + if (isPrivateBrowsingWindow) { + chromeFlags |= nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW; + } + + // A content process has asked for a new window, which implies + // that the new window will need to be remote. + chromeFlags |= nsIWebBrowserChrome::CHROME_REMOTE_WINDOW; + + if (isFissionWindow) { + chromeFlags |= nsIWebBrowserChrome::CHROME_FISSION_WINDOW; + } + + nsCOMPtr<nsIWebBrowserChrome> parentChrome(do_GetInterface(parentTreeOwner)); + nsCOMPtr<nsIWebBrowserChrome> newWindowChrome; + + CreateChromeWindow(parentChrome, chromeFlags, aOpenWindowInfo, + getter_AddRefs(newWindowChrome)); + + if (NS_WARN_IF(!newWindowChrome)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIDocShellTreeItem> chromeTreeItem = + do_GetInterface(newWindowChrome); + if (NS_WARN_IF(!chromeTreeItem)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIDocShellTreeOwner> chromeTreeOwner; + chromeTreeItem->GetTreeOwner(getter_AddRefs(chromeTreeOwner)); + if (NS_WARN_IF(!chromeTreeOwner)) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsILoadContext> chromeContext = do_QueryInterface(chromeTreeItem); + if (NS_WARN_IF(!chromeContext)) { + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(chromeContext->UsePrivateBrowsing() == isPrivateBrowsingWindow); + MOZ_ASSERT(chromeContext->UseRemoteSubframes() == isFissionWindow); + + // Tabs opened from a content process can only open new windows + // that will also run with out-of-process tabs. + MOZ_ASSERT(chromeContext->UseRemoteTabs()); + + MaybeDisablePersistence(sizeSpec, chromeTreeOwner); + + SizeOpenedWindow(chromeTreeOwner, parentWindowOuter, false, sizeSpec); + + nsCOMPtr<nsIRemoteTab> newBrowserParent; + chromeTreeOwner->GetPrimaryRemoteTab(getter_AddRefs(newBrowserParent)); + if (NS_WARN_IF(!newBrowserParent)) { + return NS_ERROR_UNEXPECTED; + } + + newBrowserParent.forget(aResult); + return NS_OK; +} + +nsresult nsWindowWatcher::OpenWindowInternal( + mozIDOMWindowProxy* aParent, const nsACString& aUrl, + const nsACString& aName, const nsACString& aFeatures, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool aCalledFromJS, bool aDialog, bool aNavigate, nsIArray* aArgv, + bool aIsPopupSpam, bool aForceNoOpener, bool aForceNoReferrer, + PrintKind aPrintKind, nsDocShellLoadState* aLoadState, + BrowsingContext** aResult) { + MOZ_ASSERT_IF(aForceNoReferrer, aForceNoOpener); + + nsresult rv = NS_OK; + bool isNewToplevelWindow = false; + bool windowIsNew = false; + bool windowNeedsName = false; + bool windowIsModal = false; + bool uriToLoadIsChrome = false; + + uint32_t chromeFlags; + nsAutoString name; // string version of aName + nsCOMPtr<nsIURI> uriToLoad; // from aUrl, if any + nsCOMPtr<nsIDocShellTreeOwner> + parentTreeOwner; // from the parent window, if any + RefPtr<BrowsingContext> targetBC; // from the new window + + nsCOMPtr<nsPIDOMWindowOuter> parentOuterWin = + aParent ? nsPIDOMWindowOuter::From(aParent) : nullptr; + + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + if (!nsContentUtils::IsSafeToRunScript()) { + nsContentUtils::WarnScriptWasIgnored(nullptr); + return NS_ERROR_FAILURE; + } + + if (parentOuterWin) { + parentTreeOwner = parentOuterWin->GetTreeOwner(); + } + + // We expect BrowserParent to have provided us the absolute URI of the window + // we're to open, so there's no need to call URIfromURL (or more importantly, + // to check for a chrome URI, which cannot be opened from a remote tab). + if (!aUrl.IsVoid()) { + rv = URIfromURL(aUrl, aParent, getter_AddRefs(uriToLoad)); + if (NS_FAILED(rv)) { + return rv; + } + uriToLoadIsChrome = uriToLoad->SchemeIs("chrome"); + } + + bool nameSpecified = false; + if (!aName.IsEmpty()) { + CopyUTF8toUTF16(aName, name); + nameSpecified = true; + } else { + name.SetIsVoid(true); + } + + WindowFeatures features; + nsAutoCString featuresStr; + if (!aFeatures.IsEmpty()) { + featuresStr.Assign(aFeatures); + features.Tokenize(featuresStr); + } else { + featuresStr.SetIsVoid(true); + } + + RefPtr<BrowsingContext> parentBC( + parentOuterWin ? parentOuterWin->GetBrowsingContext() : nullptr); + nsCOMPtr<nsIDocShell> parentDocShell(parentBC ? parentBC->GetDocShell() + : nullptr); + RefPtr<Document> parentDoc(parentOuterWin ? parentOuterWin->GetDoc() + : nullptr); + nsCOMPtr<nsPIDOMWindowInner> parentInnerWin( + parentOuterWin ? parentOuterWin->GetCurrentInnerWindow() : nullptr); + + // Return null for any attempt to trigger a load from a discarded browsing + // context. The spec is non-normative, and doesn't specify what should happen + // when window.open is called on a window with a null browsing context, but it + // does give us broad discretion over when we can decide to ignore an open + // request and return null. + // + // Regardless, we cannot trigger a cross-process load from a discarded + // browsing context, and ideally we should behave consistently whether a load + // is same-process or cross-process. + if (parentBC && parentBC->IsDiscarded()) { + return NS_ERROR_ABORT; + } + + // If no parent, consider it chrome when running in the parent process. + bool hasChromeParent = !XRE_IsContentProcess(); + if (aParent) { + // Check if the parent document has chrome privileges. + hasChromeParent = parentDoc && nsContentUtils::IsChromeDoc(parentDoc); + } + + bool isCallerChrome = nsContentUtils::LegacyIsCallerChromeOrNativeCode(); + + // try to find an extant browsing context with the given name + if (!name.IsEmpty() && + (!aForceNoOpener || nsContentUtils::IsSpecialName(name))) { + if (parentInnerWin && parentInnerWin->GetWindowGlobalChild()) { + // If we have a parent window, perform the look-up relative to the parent + // inner window. + targetBC = + parentInnerWin->GetWindowGlobalChild()->FindBrowsingContextWithName( + name); + } else if (hasChromeParent && isCallerChrome && + !nsContentUtils::IsSpecialName(name)) { + // Otherwise, if this call is from chrome, perform the lookup relative + // to the system group. + nsCOMPtr<mozIDOMWindowProxy> chromeWindow; + MOZ_ALWAYS_SUCCEEDS(GetWindowByName(name, getter_AddRefs(chromeWindow))); + if (chromeWindow) { + targetBC = nsPIDOMWindowOuter::From(chromeWindow)->GetBrowsingContext(); + } + } + } + + // Do sandbox checks here, instead of waiting until nsIDocShell::LoadURI. + // The state of the window can change before this call and if we are blocked + // because of sandboxing, we wouldn't want that to happen. + if (parentBC && parentBC->IsSandboxedFrom(targetBC)) { + return NS_ERROR_DOM_INVALID_ACCESS_ERR; + } + + // If our target BrowsingContext is still pending initialization, ignore the + // navigation request targeting it. + if (targetBC && NS_WARN_IF(targetBC->GetPendingInitialization())) { + return NS_ERROR_ABORT; + } + + // no extant window? make a new one. + + CSSToDesktopScale cssToDesktopScale(1.0); + if (nsCOMPtr<nsIBaseWindow> win = do_QueryInterface(parentDocShell)) { + cssToDesktopScale = win->GetUnscaledCSSToDesktopScale(); + } + SizeSpec sizeSpec = + CalcSizeSpec(features, hasChromeParent, cssToDesktopScale); + sizeSpec.ScaleBy(parentBC ? parentBC->FullZoom() : 1.0f); + + bool isPopupRequested = false; + + // Make sure we calculate the chromeFlags *before* we push the + // callee context onto the context stack so that + // the calculation sees the actual caller when doing its + // security checks. + if (hasChromeParent && isCallerChrome && XRE_IsParentProcess()) { + chromeFlags = + CalculateChromeFlagsForSystem(features, aDialog, uriToLoadIsChrome); + } else { + MOZ_DIAGNOSTIC_ASSERT(parentBC && parentBC->IsContent(), + "content caller must provide content parent"); + chromeFlags = + CalculateChromeFlagsForContent(features, aModifiers, &isPopupRequested); + + if (aDialog) { + MOZ_ASSERT(XRE_IsParentProcess()); + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_DIALOG; + } + } + + bool windowTypeIsChrome = + chromeFlags & nsIWebBrowserChrome::CHROME_OPENAS_CHROME; + + if (parentBC && !aForceNoOpener) { + if (parentBC->IsChrome() && !windowTypeIsChrome) { + NS_WARNING( + "Content windows may never have chrome windows as their openers."); + return NS_ERROR_INVALID_ARG; + } + if (parentBC->IsContent() && windowTypeIsChrome) { + NS_WARNING( + "Chrome windows may never have content windows as their openers."); + return NS_ERROR_INVALID_ARG; + } + } + + // If we're opening a content window from a content window, we need to exactly + // inherit fission and e10s status flags from parentBC. Only new toplevel + // windows may change these options. + if (parentBC && parentBC->IsContent() && !windowTypeIsChrome) { + chromeFlags &= ~(nsIWebBrowserChrome::CHROME_REMOTE_WINDOW | + nsIWebBrowserChrome::CHROME_FISSION_WINDOW); + if (parentBC->UseRemoteTabs()) { + chromeFlags |= nsIWebBrowserChrome::CHROME_REMOTE_WINDOW; + } + if (parentBC->UseRemoteSubframes()) { + chromeFlags |= nsIWebBrowserChrome::CHROME_FISSION_WINDOW; + } + } + + // XXXbz Why is an AutoJSAPI good enough here? Wouldn't AutoEntryScript (so + // we affect the entry global) make more sense? Or do we just want to affect + // GetSubjectPrincipal()? + dom::AutoJSAPI jsapiChromeGuard; + + if (isCallerChrome && !hasChromeParent && !windowTypeIsChrome) { + // open() is called from chrome on a non-chrome window, initialize an + // AutoJSAPI with the callee to prevent the caller's privileges from leaking + // into code that runs while opening the new window. + // + // The reasoning for this is in bug 289204. Basically, chrome sometimes does + // someContentWindow.open(untrustedURL), and wants to be insulated from + // nasty javascript: URLs and such. But there are also cases where we create + // a window parented to a content window (such as a download dialog), + // usually directly with nsIWindowWatcher. In those cases, we want the + // principal of the initial about:blank document to be system, so that the + // subsequent XUL load can reuse the inner window and avoid blowing away + // expandos. As such, we decide whether to load with the principal of the + // caller or of the parent based on whether the docshell type is chrome or + // content. + + nsCOMPtr<nsIGlobalObject> parentGlobalObject = do_QueryInterface(aParent); + if (!aParent) { + jsapiChromeGuard.Init(); + } else if (NS_WARN_IF(!jsapiChromeGuard.Init(parentGlobalObject))) { + return NS_ERROR_UNEXPECTED; + } + } + + // Now that the jsapiChromeGuard has been set, fetch the system principal + // potentially configured by it. We want to make sure to respect any principal + // changes imposed by that guard throughout this function. + // + // Note: The check for the current JSContext isn't necessarily sensical. + // It's just designed to preserve old semantics during a mass-conversion + // patch. + // Bug 1498605 verify usages of systemPrincipal here + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + nsCOMPtr<nsIPrincipal> subjectPrincipal = + cx ? nsContentUtils::SubjectPrincipal() + : nsContentUtils::GetSystemPrincipal(); + MOZ_ASSERT(subjectPrincipal); + + nsCOMPtr<nsIPrincipal> newWindowPrincipal; + if (!targetBC) { + if (windowTypeIsChrome) { + // If we are creating a chrome window, we must be called with a system + // principal, and should inherit that for the new chrome window. + MOZ_RELEASE_ASSERT(subjectPrincipal->IsSystemPrincipal(), + "Only system principals can create chrome windows"); + newWindowPrincipal = subjectPrincipal; + } else if (nsContentUtils::IsSystemOrExpandedPrincipal(subjectPrincipal)) { + // Don't allow initial about:blank documents to inherit a system or + // expanded principal, instead replace it with a null principal. We can't + // inherit origin attributes from the system principal, so use the parent + // BC if it's available. + if (parentBC) { + newWindowPrincipal = + NullPrincipal::Create(parentBC->OriginAttributesRef()); + } else { + newWindowPrincipal = NullPrincipal::CreateWithoutOriginAttributes(); + } + } else if (aForceNoOpener) { + // If we're opening a new window with noopener, create a new opaque + // principal for the new window, rather than re-using the existing + // principal. + newWindowPrincipal = + NullPrincipal::CreateWithInheritedAttributes(subjectPrincipal); + } else { + // Finally, if there's an opener relationship and it's not a special + // principal, we should inherit that principal for the new window. + newWindowPrincipal = subjectPrincipal; + } + } + + // Information used when opening new content windows. This object will be + // passed through to the inner nsFrameLoader. + RefPtr<nsOpenWindowInfo> openWindowInfo; + if (!targetBC && !windowTypeIsChrome) { + openWindowInfo = new nsOpenWindowInfo(); + openWindowInfo->mForceNoOpener = aForceNoOpener; + openWindowInfo->mParent = parentBC; + openWindowInfo->mIsForPrinting = aPrintKind != PRINT_NONE; + openWindowInfo->mIsForWindowDotPrint = aPrintKind == PRINT_WINDOW_DOT_PRINT; + + // We're going to want the window to be immediately available, meaning we + // want it to match the current remoteness. + openWindowInfo->mIsRemote = XRE_IsContentProcess(); + + // Inherit our OriginAttributes from the computed new window principal. + MOZ_ASSERT( + newWindowPrincipal && + !nsContentUtils::IsSystemOrExpandedPrincipal(newWindowPrincipal)); + openWindowInfo->mOriginAttributes = + newWindowPrincipal->OriginAttributesRef(); + + MOZ_DIAGNOSTIC_ASSERT( + !parentBC || openWindowInfo->mOriginAttributes.EqualsIgnoringFPD( + parentBC->OriginAttributesRef()), + "subject principal origin attributes doesn't match opener"); + } + + uint32_t activeDocsSandboxFlags = 0; + nsCOMPtr<nsIContentSecurityPolicy> cspToInheritForAboutBlank; + Maybe<nsILoadInfo::CrossOriginEmbedderPolicy> coepToInheritForAboutBlank; + if (!targetBC) { + // We're going to either open up a new window ourselves or ask a + // nsIWindowProvider for one. In either case, we'll want to set the right + // name on it. + windowNeedsName = true; + + // If the parent trying to open a new window is sandboxed + // without 'allow-popups', this is not allowed and we fail here. + if (parentDoc) { + // Save sandbox flags for copying to new browsing context (docShell). + activeDocsSandboxFlags = parentDoc->GetSandboxFlags(); + + if (!aForceNoOpener) { + cspToInheritForAboutBlank = parentDoc->GetCsp(); + coepToInheritForAboutBlank = parentDoc->GetEmbedderPolicy(); + } + + // Check to see if this frame is allowed to navigate, but don't check if + // we're printing, as that's not a real navigation. + if (aPrintKind == PRINT_NONE && + (activeDocsSandboxFlags & SANDBOXED_AUXILIARY_NAVIGATION)) { + return NS_ERROR_DOM_INVALID_ACCESS_ERR; + } + } + + // Now check whether it's ok to ask a window provider for a window. Don't + // do it if we're opening a dialog or if our parent is a chrome window or + // if we're opening something that has modal, dialog, or chrome flags set. + if (parentTreeOwner && !aDialog && parentBC->IsContent() && + !(chromeFlags & (nsIWebBrowserChrome::CHROME_MODAL | + nsIWebBrowserChrome::CHROME_OPENAS_DIALOG | + nsIWebBrowserChrome::CHROME_OPENAS_CHROME))) { + MOZ_ASSERT(openWindowInfo); + + nsCOMPtr<nsIWindowProvider> provider = do_GetInterface(parentTreeOwner); + if (provider) { + rv = provider->ProvideWindow(openWindowInfo, chromeFlags, aCalledFromJS, + uriToLoad, name, featuresStr, aModifiers, + aForceNoOpener, aForceNoReferrer, + isPopupRequested, aLoadState, &windowIsNew, + getter_AddRefs(targetBC)); + + if (NS_SUCCEEDED(rv) && targetBC) { + nsCOMPtr<nsIDocShell> newDocShell = targetBC->GetDocShell(); + + // If this is a new window, but it's incompatible with the current + // userContextId, we ignore it and we pretend that nothing has been + // returned by ProvideWindow. + if (!windowIsNew && newDocShell) { + if (!CheckUserContextCompatibility(newDocShell)) { + targetBC = nullptr; + windowIsNew = false; + } + } + + } else if (rv == NS_ERROR_ABORT) { + // NS_ERROR_ABORT means the window provider has flat-out rejected + // the open-window call and we should bail. Don't return an error + // here, because our caller may propagate that error, which might + // cause e.g. window.open to throw! Just return null for our out + // param. + return NS_OK; + } + } + } + } + + bool newWindowShouldBeModal = false; + bool parentIsModal = false; + if (!targetBC) { + if (XRE_IsContentProcess()) { + // If our window provider failed to provide a window in the content + // process, we cannot recover. Reject the window open request and bail. + return NS_OK; + } + + windowIsNew = true; + isNewToplevelWindow = true; + + nsCOMPtr<nsIWebBrowserChrome> parentChrome( + do_GetInterface(parentTreeOwner)); + + // is the parent (if any) modal? if so, we must be, too. + bool weAreModal = (chromeFlags & nsIWebBrowserChrome::CHROME_MODAL) != 0; + newWindowShouldBeModal = weAreModal; + if (!weAreModal && parentChrome) { + parentChrome->IsWindowModal(&weAreModal); + parentIsModal = weAreModal; + } + + if (weAreModal) { + windowIsModal = true; + // in case we added this because weAreModal + chromeFlags |= nsIWebBrowserChrome::CHROME_MODAL | + nsIWebBrowserChrome::CHROME_DEPENDENT; + } + + // Make sure to not create modal windows if our parent is invisible and + // isn't a chrome window. Otherwise we can end up in a bizarre situation + // where we can't shut down because an invisible window is open. If + // someone tries to do this, throw. + if (!hasChromeParent && (chromeFlags & nsIWebBrowserChrome::CHROME_MODAL)) { + nsCOMPtr<nsIBaseWindow> parentWindow(do_GetInterface(parentTreeOwner)); + nsCOMPtr<nsIWidget> parentWidget; + if (parentWindow) { + parentWindow->GetMainWidget(getter_AddRefs(parentWidget)); + } + // NOTE: the logic for this visibility check is duplicated in + // nsIDOMWindowUtils::isParentWindowMainWidgetVisible - if we change + // how a window is determined "visible" in this context then we should + // also adjust that attribute and/or any consumers of it... + if (parentWidget && !parentWidget->IsVisible()) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + NS_ASSERTION(mWindowCreator, + "attempted to open a new window with no WindowCreator"); + rv = NS_ERROR_FAILURE; + if (mWindowCreator) { + nsCOMPtr<nsIWebBrowserChrome> newChrome; + + nsCOMPtr<nsPIDOMWindowInner> parentTopInnerWindow; + if (parentOuterWin) { + nsCOMPtr<nsPIDOMWindowOuter> parentTopWindow = + parentOuterWin->GetInProcessTop(); + if (parentTopWindow) { + parentTopInnerWindow = parentTopWindow->GetCurrentInnerWindow(); + } + } + + if (parentTopInnerWindow) { + parentTopInnerWindow->Suspend(); + } + + /* We can give the window creator some hints. The only hint at this time + is whether the opening window is in a situation that's likely to mean + this is an unrequested popup window we're creating. However we're not + completely honest: we clear that indicator if the opener is chrome, so + that the downstream consumer can treat the indicator to mean simply + that the new window is subject to popup control. */ + rv = CreateChromeWindow(parentChrome, chromeFlags, openWindowInfo, + getter_AddRefs(newChrome)); + if (parentTopInnerWindow) { + parentTopInnerWindow->Resume(); + } + + if (newChrome) { + /* It might be a chrome AppWindow, in which case it won't have + an nsIDOMWindow (primary content shell). But in that case, it'll + be able to hand over an nsIDocShellTreeItem directly. */ + nsCOMPtr<nsPIDOMWindowOuter> newWindow(do_GetInterface(newChrome)); + nsCOMPtr<nsIDocShellTreeItem> newDocShellItem; + if (newWindow) { + newDocShellItem = newWindow->GetDocShell(); + } + if (!newDocShellItem) { + newDocShellItem = do_GetInterface(newChrome); + } + if (!newDocShellItem) { + rv = NS_ERROR_FAILURE; + } + targetBC = newDocShellItem->GetBrowsingContext(); + } + } + } + + // better have a window to use by this point + if (!targetBC) { + return rv; + } + + // If our parent is sandboxed, set it as the one permitted sandboxed navigator + // on the new window we're opening. + if (activeDocsSandboxFlags && parentBC) { + MOZ_ALWAYS_SUCCEEDS(targetBC->SetOnePermittedSandboxedNavigator(parentBC)); + } + + if (!aForceNoOpener && parentBC) { + // If we've created a new content window, its opener should have been set + // when its BrowsingContext was created, in order to ensure that the context + // is loaded within the correct BrowsingContextGroup. + if (windowIsNew && targetBC->IsContent()) { + if (parentBC->IsDiscarded()) { + // If the parent BC was discarded in a nested event loop before we got + // to this point, we can't set it as the opener. Ideally we would still + // set `HadOriginalOpener()` in that case, but that's somewhat + // nontrivial, and not worth the effort given the nature of the corner + // case (see comment in `nsFrameLoader::CreateBrowsingContext`. + MOZ_RELEASE_ASSERT(targetBC->GetOpenerId() == parentBC->Id() || + targetBC->GetOpenerId() == 0); + } else { + MOZ_RELEASE_ASSERT(targetBC->GetOpenerId() == parentBC->Id()); + MOZ_RELEASE_ASSERT(targetBC->HadOriginalOpener()); + } + } else { + // Update the opener for an existing or chrome BC. + targetBC->SetOpener(parentBC); + } + } + + RefPtr<nsDocShell> targetDocShell(nsDocShell::Cast(targetBC->GetDocShell())); + + // As required by spec, new windows always start out same-process, even if the + // URL being loaded will eventually load in a new process. + MOZ_DIAGNOSTIC_ASSERT(!windowIsNew || targetDocShell); + // New top-level windows are only opened in the parent process and are, by + // definition, always in-process. + MOZ_DIAGNOSTIC_ASSERT(!isNewToplevelWindow || targetDocShell); + + // Copy sandbox flags to the new window if activeDocsSandboxFlags says to do + // so. Note that it's only nonzero if the window is new, so clobbering + // sandbox flags on the window makes sense in that case. + if (activeDocsSandboxFlags & + SANDBOX_PROPAGATES_TO_AUXILIARY_BROWSING_CONTEXTS) { + MOZ_ASSERT(windowIsNew, "Should only get here for new windows"); + MOZ_ALWAYS_SUCCEEDS(targetBC->SetSandboxFlags(activeDocsSandboxFlags)); + MOZ_ALWAYS_SUCCEEDS( + targetBC->SetInitialSandboxFlags(targetBC->GetSandboxFlags())); + } + + RefPtr<nsGlobalWindowOuter> targetOuterWin( + nsGlobalWindowOuter::Cast(targetBC->GetDOMWindow())); +#ifdef DEBUG + if (targetOuterWin && windowIsNew) { + // Assert that we're not loading things right now. If we are, when + // that load completes it will clobber whatever principals we set up + // on this new window! + nsCOMPtr<nsIChannel> chan; + targetDocShell->GetDocumentChannel(getter_AddRefs(chan)); + MOZ_ASSERT(!chan, "Why is there a document channel?"); + + if (RefPtr<Document> doc = targetOuterWin->GetExtantDoc()) { + MOZ_ASSERT(doc->IsInitialDocument(), + "New window's document should be an initial document"); + } + } +#endif + + MOZ_ASSERT(targetOuterWin || !windowIsNew, + "New windows are always created in-process"); + + *aResult = do_AddRef(targetBC).take(); + + if (isNewToplevelWindow) { + nsCOMPtr<nsIDocShellTreeOwner> newTreeOwner; + targetDocShell->GetTreeOwner(getter_AddRefs(newTreeOwner)); + MaybeDisablePersistence(sizeSpec, newTreeOwner); + } + + if (aDialog && aArgv) { + MOZ_ASSERT(targetOuterWin); + NS_ENSURE_TRUE(targetOuterWin, NS_ERROR_UNEXPECTED); + + // Set the args on the new window. + MOZ_TRY(targetOuterWin->SetArguments(aArgv)); + } + + /* allow a window that we found by name to keep its name (important for cases + like _self where the given name is different (and invalid)). Also, _blank + is not a window name. */ + if (windowNeedsName) { + if (nameSpecified && !name.LowerCaseEqualsLiteral("_blank")) { + MOZ_ALWAYS_SUCCEEDS(targetBC->SetName(name)); + } else { + MOZ_ALWAYS_SUCCEEDS(targetBC->SetName(u""_ns)); + } + } + + // Now we have to set the right opener principal on the new window. Note + // that we have to do this _before_ starting any URI loads, thanks to the + // sync nature of javascript: loads. + + if (windowIsNew) { + MOZ_DIAGNOSTIC_ASSERT( + !targetBC->IsContent() || + newWindowPrincipal->OriginAttributesRef().EqualsIgnoringFPD( + targetBC->OriginAttributesRef())); + + bool autoPrivateBrowsing = StaticPrefs::browser_privatebrowsing_autostart(); + + if (!autoPrivateBrowsing && + (chromeFlags & nsIWebBrowserChrome::CHROME_NON_PRIVATE_WINDOW)) { + if (targetBC->IsChrome()) { + targetBC->SetUsePrivateBrowsing(false); + } + MOZ_DIAGNOSTIC_ASSERT( + !targetBC->UsePrivateBrowsing(), + "CHROME_NON_PRIVATE_WINDOW passed, but got private window"); + } else if (autoPrivateBrowsing || + (chromeFlags & nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW)) { + if (targetBC->IsChrome()) { + targetBC->SetUsePrivateBrowsing(true); + } + MOZ_DIAGNOSTIC_ASSERT( + targetBC->UsePrivateBrowsing(), + "CHROME_PRIVATE_WINDOW passed, but got non-private window"); + } + + // Now set the opener principal on the new window. Note that we need to do + // this no matter whether we were opened from JS; if there is nothing on + // the JS stack, just use the principal of our parent window. In those + // cases we do _not_ set the parent window principal as the owner of the + // load--since we really don't know who the owner is, just leave it null. + NS_ASSERTION(targetOuterWin == targetDocShell->GetWindow(), + "Different windows??"); + + // Initialize the principal of the initial about:blank document. For + // toplevel windows, this call may have already happened when the window was + // created, but SetInitialPrincipal is safe to call multiple times. + if (targetOuterWin) { + MOZ_ASSERT(windowIsNew); + MOZ_ASSERT(!targetOuterWin->GetSameProcessOpener() || + targetOuterWin->GetSameProcessOpener() == aParent); + targetOuterWin->SetInitialPrincipal(newWindowPrincipal, + cspToInheritForAboutBlank, + coepToInheritForAboutBlank); + + if (aIsPopupSpam) { + MOZ_ASSERT(!targetBC->GetIsPopupSpam(), + "Who marked it as popup spam already???"); + // Make sure we don't mess up our counter even if the above assert + // fails. + if (!targetBC->GetIsPopupSpam()) { + MOZ_ALWAYS_SUCCEEDS(targetBC->SetIsPopupSpam(true)); + } + } + } + + // Copy the current session storage for the current domain. Don't perform + // the copy if we're forcing noopener, however. + if (!aForceNoOpener && subjectPrincipal && parentDocShell && + targetDocShell) { + const RefPtr<SessionStorageManager> parentStorageManager = + parentDocShell->GetBrowsingContext()->GetSessionStorageManager(); + const RefPtr<SessionStorageManager> newStorageManager = + targetDocShell->GetBrowsingContext()->GetSessionStorageManager(); + + if (parentStorageManager && newStorageManager) { + RefPtr<Storage> storage; + parentStorageManager->GetStorage( + parentInnerWin, subjectPrincipal, subjectPrincipal, + targetBC->UsePrivateBrowsing(), getter_AddRefs(storage)); + if (storage) { + newStorageManager->CloneStorage(storage); + } + } + } + } + + // We rely on CalculateChromeFlags to decide whether remote (out-of-process) + // tabs should be used. + MOZ_DIAGNOSTIC_ASSERT( + targetBC->UseRemoteTabs() == + !!(chromeFlags & nsIWebBrowserChrome::CHROME_REMOTE_WINDOW)); + MOZ_DIAGNOSTIC_ASSERT( + targetBC->UseRemoteSubframes() == + !!(chromeFlags & nsIWebBrowserChrome::CHROME_FISSION_WINDOW)); + + RefPtr<nsDocShellLoadState> loadState = aLoadState; + if (uriToLoad && loadState) { + // If a URI was passed to this function, open that, not what was passed in + // the original LoadState. See Bug 1515433. + loadState->SetURI(uriToLoad); + } else if (uriToLoad && aNavigate && !loadState) { + RefPtr<WindowContext> context = + parentInnerWin ? parentInnerWin->GetWindowContext() : nullptr; + loadState = new nsDocShellLoadState(uriToLoad); + + loadState->SetSourceBrowsingContext(parentBC); + loadState->SetAllowFocusMove(true); + loadState->SetHasValidUserGestureActivation( + context && context->HasValidTransientUserGestureActivation()); + if (parentBC) { + loadState->SetTriggeringSandboxFlags(parentBC->GetSandboxFlags()); + } + + if (parentInnerWin) { + loadState->SetTriggeringWindowId(parentInnerWin->WindowID()); + loadState->SetTriggeringStorageAccess( + parentInnerWin->UsingStorageAccess()); + } + + if (subjectPrincipal) { + loadState->SetTriggeringPrincipal(subjectPrincipal); + } +#ifndef ANDROID + MOZ_ASSERT(subjectPrincipal, + "nsWindowWatcher: triggeringPrincipal required"); +#endif + + if (!aForceNoReferrer) { + /* use the URL from the *extant* document, if any. The usual accessor + GetDocument will synchronously create an about:blank document if + it has no better answer, and we only care about a real document. + Also using GetDocument to force document creation seems to + screw up focus in the hidden window; see bug 36016. + */ + RefPtr<Document> doc = GetEntryDocument(); + if (!doc) { + doc = parentDoc; + } + if (doc) { + auto referrerInfo = MakeRefPtr<ReferrerInfo>(*doc); + loadState->SetReferrerInfo(referrerInfo); + } + } + } + + if (loadState && cx) { + nsGlobalWindowInner* win = xpc::CurrentWindowOrNull(cx); + if (win) { + nsCOMPtr<nsIContentSecurityPolicy> csp = win->GetCsp(); + loadState->SetCsp(csp); + } + } + + if (isNewToplevelWindow) { + // Notify observers that the window is open and ready. + // The window has not yet started to load a document. + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->NotifyObservers(ToSupports(targetOuterWin), + "toplevel-window-ready", nullptr); + } + } + + // Before loading the URI we want to be 100% sure that we use the correct + // userContextId. + MOZ_ASSERT_IF(targetDocShell, CheckUserContextCompatibility(targetDocShell)); + + // If this tab or window has been opened by a window.open call, we have to + // provide all the data needed to send a + // webNavigation.onCreatedNavigationTarget event. + if (parentDocShell && windowIsNew) { + nsCOMPtr<nsIObserverService> obsSvc = + mozilla::services::GetObserverService(); + + if (obsSvc) { + RefPtr<nsHashPropertyBag> props = new nsHashPropertyBag(); + + if (uriToLoad) { + // The url notified in the webNavigation.onCreatedNavigationTarget + // event. + props->SetPropertyAsACString(u"url"_ns, uriToLoad->GetSpecOrDefault()); + } + + props->SetPropertyAsInterface(u"sourceTabDocShell"_ns, parentDocShell); + props->SetPropertyAsInterface(u"createdTabDocShell"_ns, + ToSupports(targetDocShell)); + + obsSvc->NotifyObservers(static_cast<nsIPropertyBag2*>(props), + "webNavigation-createdNavigationTarget-from-js", + nullptr); + } + } + + if (uriToLoad && aNavigate) { + uint32_t loadFlags = nsIWebNavigation::LOAD_FLAGS_NONE; + if (windowIsNew) { + loadFlags |= nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD; + + // Per spec, the explicit navigation to about:blank after the initial + // about:blank document in a new window does not occur, so there is no + // opportunity for it to inherit the source document's principal. This + // doesn't perfectly model this, as a noopener creation of `about:blank` + // will replace the global due to a principal mismatch, but it should be + // unobservable (bug 1694993). + // This isn't set for chrome windows, as an about:blank chrome document + // needs to inherit its principal from its opener. + if (aForceNoOpener && !windowTypeIsChrome) { + loadFlags |= nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + } + loadState->SetLoadFlags(loadFlags); + loadState->SetFirstParty(true); + + // Should this pay attention to errors returned by LoadURI? + targetBC->LoadURI(loadState); + } + + if (isNewToplevelWindow) { + nsCOMPtr<nsIDocShellTreeOwner> newTreeOwner; + targetDocShell->GetTreeOwner(getter_AddRefs(newTreeOwner)); + SizeOpenedWindow(newTreeOwner, aParent, isCallerChrome, sizeSpec); + } + + if (windowIsModal) { + NS_ENSURE_TRUE(targetDocShell, NS_ERROR_NOT_IMPLEMENTED); + + nsCOMPtr<nsIDocShellTreeOwner> newTreeOwner; + targetDocShell->GetTreeOwner(getter_AddRefs(newTreeOwner)); + nsCOMPtr<nsIWebBrowserChrome> newChrome(do_GetInterface(newTreeOwner)); + + // Throw an exception here if no web browser chrome is available, + // we need that to show a modal window. + NS_ENSURE_TRUE(newChrome, NS_ERROR_NOT_AVAILABLE); + + // Dispatch dialog events etc, but we only want to do that if + // we're opening a modal content window (the helper classes are + // no-ops if given no window), for chrome dialogs we don't want to + // do any of that (it's done elsewhere for us). + // Make sure we maintain the state on an outer window, because + // that's where it lives; inner windows assert if you try to + // maintain the state on them. + nsAutoWindowStateHelper windowStateHelper(parentOuterWin); + + if (!windowStateHelper.DefaultEnabled()) { + // Default to cancel not opening the modal window. + NS_RELEASE(*aResult); + + return NS_OK; + } + + bool isAppModal = false; + nsCOMPtr<nsIBaseWindow> parentWindow(do_GetInterface(newTreeOwner)); + nsCOMPtr<nsIWidget> parentWidget; + if (parentWindow) { + parentWindow->GetMainWidget(getter_AddRefs(parentWidget)); + if (parentWidget) { + isAppModal = parentWidget->IsRunningAppModal(); + } + } + if (parentWidget && + ((!newWindowShouldBeModal && parentIsModal) || isAppModal)) { + parentWidget->SetFakeModal(true); + } else { + // Reset popup state while opening a modal dialog, and firing + // events about the dialog, to prevent the current state from + // being active the whole time a modal dialog is open. + AutoPopupStatePusher popupStatePusher(PopupBlocker::openAbused); + + auto rv = newChrome->ShowAsModal(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + // If a website opens a popup exit DOM fullscreen + if (StaticPrefs::full_screen_api_exit_on_windowOpen() && aCalledFromJS && + !hasChromeParent && !isCallerChrome && parentOuterWin) { + Document::AsyncExitFullscreen(parentOuterWin->GetDoc()); + } + + if (aForceNoOpener && windowIsNew) { + NS_RELEASE(*aResult); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::RegisterNotification(nsIObserver* aObserver) { + // just a convenience method; it delegates to nsIObserverService + + if (!aObserver) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (!os) { + return NS_ERROR_FAILURE; + } + + nsresult rv = os->AddObserver(aObserver, "domwindowopened", false); + if (NS_SUCCEEDED(rv)) { + rv = os->AddObserver(aObserver, "domwindowclosed", false); + } + + return rv; +} + +NS_IMETHODIMP +nsWindowWatcher::UnregisterNotification(nsIObserver* aObserver) { + // just a convenience method; it delegates to nsIObserverService + + if (!aObserver) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (!os) { + return NS_ERROR_FAILURE; + } + + os->RemoveObserver(aObserver, "domwindowopened"); + os->RemoveObserver(aObserver, "domwindowclosed"); + + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::GetWindowEnumerator(nsISimpleEnumerator** aResult) { + if (!aResult) { + return NS_ERROR_INVALID_ARG; + } + + MutexAutoLock lock(mListLock); + RefPtr<nsWatcherWindowEnumerator> enumerator = + new nsWatcherWindowEnumerator(this); + enumerator.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::GetNewPrompter(mozIDOMWindowProxy* aParent, + nsIPrompt** aResult) { + // This is for backwards compat only. Callers should just use the prompt + // service directly. + nsresult rv; + nsCOMPtr<nsIPromptFactory> factory = + do_GetService("@mozilla.org/prompter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return factory->GetPrompt(aParent, NS_GET_IID(nsIPrompt), + reinterpret_cast<void**>(aResult)); +} + +NS_IMETHODIMP +nsWindowWatcher::GetNewAuthPrompter(mozIDOMWindowProxy* aParent, + nsIAuthPrompt** aResult) { + // This is for backwards compat only. Callers should just use the prompt + // service directly. + nsresult rv; + nsCOMPtr<nsIPromptFactory> factory = + do_GetService("@mozilla.org/prompter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return factory->GetPrompt(aParent, NS_GET_IID(nsIAuthPrompt), + reinterpret_cast<void**>(aResult)); +} + +NS_IMETHODIMP +nsWindowWatcher::GetPrompt(mozIDOMWindowProxy* aParent, const nsIID& aIID, + void** aResult) { + // This is for backwards compat only. Callers should just use the prompt + // service directly. + nsresult rv; + nsCOMPtr<nsIPromptFactory> factory = + do_GetService("@mozilla.org/prompter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = factory->GetPrompt(aParent, aIID, aResult); + + // Allow for an embedding implementation to not support nsIAuthPrompt2. + if (rv == NS_NOINTERFACE && aIID.Equals(NS_GET_IID(nsIAuthPrompt2))) { + nsCOMPtr<nsIAuthPrompt> oldPrompt; + rv = factory->GetPrompt(aParent, NS_GET_IID(nsIAuthPrompt), + getter_AddRefs(oldPrompt)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_WrapAuthPrompt(oldPrompt, reinterpret_cast<nsIAuthPrompt2**>(aResult)); + if (!*aResult) { + rv = NS_ERROR_NOT_AVAILABLE; + } + } + return rv; +} + +NS_IMETHODIMP +nsWindowWatcher::SetWindowCreator(nsIWindowCreator* aCreator) { + mWindowCreator = aCreator; + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::HasWindowCreator(bool* aResult) { + *aResult = mWindowCreator; + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::GetActiveWindow(mozIDOMWindowProxy** aActiveWindow) { + *aActiveWindow = nullptr; + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm) { + return fm->GetActiveWindow(aActiveWindow); + } + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::AddWindow(mozIDOMWindowProxy* aWindow, + nsIWebBrowserChrome* aChrome) { + if (!aWindow) { + return NS_ERROR_INVALID_ARG; + } + + { + nsWatcherWindowEntry* info; + MutexAutoLock lock(mListLock); + + // if we already have an entry for this window, adjust + // its chrome mapping and return + info = FindWindowEntry(aWindow); + if (info) { + nsCOMPtr<nsISupportsWeakReference> supportsweak( + do_QueryInterface(aChrome)); + if (supportsweak) { + supportsweak->GetWeakReference(getter_AddRefs(info->mChromeWeak)); + } else { + info->mChrome = aChrome; + info->mChromeWeak = nullptr; + } + return NS_OK; + } + + // create a window info struct and add it to the list of windows + info = new nsWatcherWindowEntry(aWindow, aChrome); + if (!info) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mOldestWindow) { + info->InsertAfter(mOldestWindow->mOlder); + } else { + mOldestWindow = info; + } + } // leave the mListLock + + // a window being added to us signifies a newly opened window. + // send notifications. + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (!os) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISupports> domwin(do_QueryInterface(aWindow)); + return os->NotifyObservers(domwin, "domwindowopened", nullptr); +} + +NS_IMETHODIMP +nsWindowWatcher::RemoveWindow(mozIDOMWindowProxy* aWindow) { + // find the corresponding nsWatcherWindowEntry, remove it + + if (!aWindow) { + return NS_ERROR_INVALID_ARG; + } + + nsWatcherWindowEntry* info = FindWindowEntry(aWindow); + if (info) { + RemoveWindow(info); + return NS_OK; + } + NS_WARNING("requested removal of nonexistent window"); + return NS_ERROR_INVALID_ARG; +} + +nsWatcherWindowEntry* nsWindowWatcher::FindWindowEntry( + mozIDOMWindowProxy* aWindow) { + // find the corresponding nsWatcherWindowEntry + nsWatcherWindowEntry* info; + nsWatcherWindowEntry* listEnd; + + info = mOldestWindow; + listEnd = nullptr; + while (info != listEnd) { + if (info->mWindow == aWindow) { + return info; + } + info = info->mYounger; + listEnd = mOldestWindow; + } + return nullptr; +} + +nsresult nsWindowWatcher::RemoveWindow(nsWatcherWindowEntry* aInfo) { + uint32_t count = mEnumeratorList.Length(); + + { + // notify the enumerators + MutexAutoLock lock(mListLock); + for (uint32_t ctr = 0; ctr < count; ++ctr) { + mEnumeratorList[ctr]->WindowRemoved(aInfo); + } + + // remove the element from the list + if (aInfo == mOldestWindow) { + mOldestWindow = + aInfo->mYounger == mOldestWindow ? nullptr : aInfo->mYounger; + } + aInfo->Unlink(); + } + + // a window being removed from us signifies a newly closed window. + // send notifications. + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + nsCOMPtr<nsISupports> domwin(do_QueryInterface(aInfo->mWindow)); + os->NotifyObservers(domwin, "domwindowclosed", nullptr); + } + + delete aInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::GetChromeForWindow(mozIDOMWindowProxy* aWindow, + nsIWebBrowserChrome** aResult) { + if (!aWindow || !aResult) { + return NS_ERROR_INVALID_ARG; + } + *aResult = nullptr; + + MutexAutoLock lock(mListLock); + nsWatcherWindowEntry* info = FindWindowEntry(aWindow); + if (info) { + if (info->mChromeWeak) { + return info->mChromeWeak->QueryReferent( + NS_GET_IID(nsIWebBrowserChrome), reinterpret_cast<void**>(aResult)); + } + *aResult = info->mChrome; + NS_IF_ADDREF(*aResult); + } + return NS_OK; +} + +NS_IMETHODIMP +nsWindowWatcher::GetWindowByName(const nsAString& aTargetName, + mozIDOMWindowProxy** aResult) { + if (!aResult) { + return NS_ERROR_INVALID_ARG; + } + + *aResult = nullptr; + + // We won't be able to find any windows with a special or empty name. + if (aTargetName.IsEmpty() || nsContentUtils::IsSpecialName(aTargetName)) { + return NS_OK; + } + + // Search each toplevel in the chrome BrowsingContextGroup for a window with + // the given name. + for (const RefPtr<BrowsingContext>& toplevel : + BrowsingContextGroup::GetChromeGroup()->Toplevels()) { + BrowsingContext* context = + toplevel->FindWithNameInSubtree(aTargetName, nullptr); + if (context) { + *aResult = do_AddRef(context->GetDOMWindow()).take(); + MOZ_ASSERT(*aResult); + return NS_OK; + } + } + + return NS_OK; +} + +bool nsWindowWatcher::AddEnumerator(nsWatcherWindowEnumerator* aEnumerator) { + // (requires a lock; assumes it's called by someone holding the lock) + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + mEnumeratorList.AppendElement(aEnumerator); + return true; +} + +bool nsWindowWatcher::RemoveEnumerator(nsWatcherWindowEnumerator* aEnumerator) { + // (requires a lock; assumes it's called by someone holding the lock) + return mEnumeratorList.RemoveElement(aEnumerator); +} + +nsresult nsWindowWatcher::URIfromURL(const nsACString& aURL, + mozIDOMWindowProxy* aParent, + nsIURI** aURI) { + // Build the URI relative to the entry global. + nsCOMPtr<nsPIDOMWindowInner> baseWindow = do_QueryInterface(GetEntryGlobal()); + + // failing that, build it relative to the parent window, if possible + if (!baseWindow && aParent) { + baseWindow = nsPIDOMWindowOuter::From(aParent)->GetCurrentInnerWindow(); + } + + // failing that, use the given URL unmodified. It had better not be relative. + + nsIURI* baseURI = nullptr; + + // get baseWindow's document URI + if (baseWindow) { + if (Document* doc = baseWindow->GetDoc()) { + baseURI = doc->GetDocBaseURI(); + } + } + + // build and return the absolute URI + return NS_NewURI(aURI, aURL, nullptr, baseURI); +} + +// static +bool nsWindowWatcher::ShouldOpenPopup(const WindowFeatures& aFeatures) { + if (aFeatures.IsEmpty()) { + return false; + } + + // NOTE: This is different than chrome-only "popup" feature that is handled + // in nsWindowWatcher::CalculateChromeFlagsForSystem. + if (aFeatures.Exists("popup")) { + return aFeatures.GetBool("popup"); + } + + if (!aFeatures.GetBoolWithDefault("location", false) && + !aFeatures.GetBoolWithDefault("toolbar", false)) { + return true; + } + + if (!aFeatures.GetBoolWithDefault("menubar", false)) { + return true; + } + + if (!aFeatures.GetBoolWithDefault("resizable", true)) { + return true; + } + + if (!aFeatures.GetBoolWithDefault("scrollbars", false)) { + return true; + } + + if (!aFeatures.GetBoolWithDefault("status", false)) { + return true; + } + + return false; +} + +/** + * Calculate the chrome bitmask from a string list of features requested + * from a child process. The feature string can only control whether to open a + * new tab or a new popup. + * @param aFeatures a string containing a list of named features + * @param aIsPopupRequested an out parameter that indicates whether a popup + * is requested by aFeatures + * @return the chrome bitmask + */ +// static +uint32_t nsWindowWatcher::CalculateChromeFlagsForContent( + const WindowFeatures& aFeatures, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool* aIsPopupRequested) { + if (aFeatures.IsEmpty() || !ShouldOpenPopup(aFeatures)) { + // Open the current/new tab in the current/new window + // (depends on browser.link.open_newwindow). + return nsIWebBrowserChrome::CHROME_ALL; + } + + int32_t unused; + if (IsWindowOpenLocationModified(aModifiers, &unused)) { + // If modifier keys are held when `window.open` is called, open a new + // foreground/background tab in the current window, or open a new tab in a + // new window, depending on the modifiers combination. + return nsIWebBrowserChrome::CHROME_ALL; + } + + // Open a minimal popup. + *aIsPopupRequested = true; + return nsIWebBrowserChrome::CHROME_MINIMAL_POPUP; +} + +/** + * Calculate the chrome bitmask from a string list of features for a new + * privileged window. + * @param aFeatures a string containing a list of named chrome features + * @param aDialog affects the assumptions made about unnamed features + * @param aChromeURL true if the window is being sent to a chrome:// URL + * @return the chrome bitmask + */ +// static +uint32_t nsWindowWatcher::CalculateChromeFlagsForSystem( + const WindowFeatures& aFeatures, bool aDialog, bool aChromeURL) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(nsContentUtils::LegacyIsCallerChromeOrNativeCode()); + + uint32_t chromeFlags = 0; + + // The features string is made void by OpenWindowInternal + // if nullptr was originally passed as the features string. + if (aFeatures.IsEmpty()) { + chromeFlags = nsIWebBrowserChrome::CHROME_ALL; + if (aDialog) { + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_DIALOG | + nsIWebBrowserChrome::CHROME_OPENAS_CHROME; + } + } else { + chromeFlags = nsIWebBrowserChrome::CHROME_WINDOW_BORDERS; + } + + /* This function has become complicated since browser windows and + dialogs diverged. The difference is, browser windows assume all + chrome not explicitly mentioned is off, if the features string + is not null. Exceptions are some OS border chrome new with Mozilla. + Dialogs interpret a (mostly) empty features string to mean + "OS's choice," and also support an "all" flag explicitly disallowed + in the standards-compliant window.(normal)open. */ + + bool presenceFlag = false; + if (aDialog && aFeatures.GetBoolWithDefault("all", false, &presenceFlag)) { + chromeFlags = nsIWebBrowserChrome::CHROME_ALL; + } + + /* Next, allow explicitly named options to override the initial settings */ + if (aFeatures.GetBoolWithDefault("titlebar", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_TITLEBAR; + } + if (aFeatures.GetBoolWithDefault("close", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_CLOSE; + } + if (aFeatures.GetBoolWithDefault("toolbar", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_TOOLBAR; + } + if (aFeatures.GetBoolWithDefault("location", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_LOCATIONBAR; + } + if (aFeatures.GetBoolWithDefault("personalbar", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_PERSONAL_TOOLBAR; + } + if (aFeatures.GetBoolWithDefault("status", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_STATUSBAR; + } + if (aFeatures.GetBoolWithDefault("menubar", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_MENUBAR; + } + if (aFeatures.GetBoolWithDefault("resizable", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_RESIZE; + } + if (aFeatures.GetBoolWithDefault("minimizable", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_MINIMIZE; + } + if (aFeatures.GetBoolWithDefault("scrollbars", true, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_SCROLLBARS; + } + + // Determine whether the window is a private browsing window + if (aFeatures.GetBoolWithDefault("private", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW; + } + if (aFeatures.GetBoolWithDefault("non-private", false, &presenceFlag)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_NON_PRIVATE_WINDOW; + } + + // Determine whether the window should have remote tabs. + bool remote = BrowserTabsRemoteAutostart(); + + if (remote) { + remote = !aFeatures.GetBoolWithDefault("non-remote", false, &presenceFlag); + } else { + remote = aFeatures.GetBoolWithDefault("remote", false, &presenceFlag); + } + + if (remote) { + chromeFlags |= nsIWebBrowserChrome::CHROME_REMOTE_WINDOW; + } + + // Determine whether the window should have remote subframes + bool fission = FissionAutostart(); + + if (fission) { + fission = + !aFeatures.GetBoolWithDefault("non-fission", false, &presenceFlag); + } else { + fission = aFeatures.GetBoolWithDefault("fission", false, &presenceFlag); + } + + if (fission) { + chromeFlags |= nsIWebBrowserChrome::CHROME_FISSION_WINDOW; + } + + /* OK. + Normal browser windows, in spite of a stated pattern of turning off + all chrome not mentioned explicitly, will want the new OS chrome (window + borders, titlebars, closebox) on, unless explicitly turned off. + Dialogs, on the other hand, take the absence of any explicit settings + to mean "OS' choice." */ + + // default titlebar and closebox to "on," if not mentioned at all + if (!aFeatures.Exists("titlebar")) { + chromeFlags |= nsIWebBrowserChrome::CHROME_TITLEBAR; + } + if (!aFeatures.Exists("close")) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_CLOSE; + } + + if (aDialog && !aFeatures.IsEmpty() && !presenceFlag) { + chromeFlags = nsIWebBrowserChrome::CHROME_DEFAULT; + } + + /* Finally, once all the above normal chrome has been divined, deal + with the features that are more operating hints than appearance + instructions. (Note modality implies dependence.) */ + + if (aFeatures.GetBoolWithDefault("alwayslowered", false) || + aFeatures.GetBoolWithDefault("z-lock", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_LOWERED; + } else if (aFeatures.GetBoolWithDefault("alwaysraised", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_WINDOW_RAISED; + } + + if (aFeatures.GetBoolWithDefault("suppressanimation", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_SUPPRESS_ANIMATION; + } + if (aFeatures.GetBoolWithDefault("alwaysontop", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_ALWAYS_ON_TOP; + } + if (aFeatures.GetBoolWithDefault("chrome", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_CHROME; + } + if (aFeatures.GetBoolWithDefault("extrachrome", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_EXTRA; + } + if (aFeatures.GetBoolWithDefault("centerscreen", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_CENTER_SCREEN; + } + if (aFeatures.GetBoolWithDefault("dependent", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_DEPENDENT; + } + if (aFeatures.GetBoolWithDefault("modal", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_MODAL | + nsIWebBrowserChrome::CHROME_DEPENDENT; + } + if (aFeatures.GetBoolWithDefault("dialog", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_DIALOG; + } + if (aFeatures.GetBoolWithDefault("alert", false)) { + chromeFlags |= nsIWebBrowserChrome::CHROME_ALERT; + } + + /* dialogs need to have the last word. assume dialogs are dialogs, + and opened as chrome, unless explicitly told otherwise. */ + if (aDialog) { + if (!aFeatures.Exists("dialog")) { + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_DIALOG; + } + if (!aFeatures.Exists("chrome")) { + chromeFlags |= nsIWebBrowserChrome::CHROME_OPENAS_CHROME; + } + } + + return chromeFlags; +} + +// public static +bool nsWindowWatcher::HaveSpecifiedSize(const WindowFeatures& features) { + return CalcSizeSpec(features, false, CSSToDesktopScale()).SizeSpecified(); +} + +// static +SizeSpec CalcSizeSpec(const WindowFeatures& aFeatures, bool aHasChromeParent, + CSSToDesktopScale aCSSToDesktopScale) { + SizeSpec result; + // https://drafts.csswg.org/cssom-view/#set-up-browsing-context-features + // To set up browsing context features for a browsing context `target` given + // a map `tokenizedFeatures`: + + // Step 1. Let `x` be null. + // (implicit) + + // Step 2. Let `y` be null. + // (implicit) + + // Step 3. Let `width` be null. + // (implicit) + + // Step 4. Let `height` be null. + // (implicit) + + // Step 5. If `tokenizedFeatures["left"]` exists: + if (aFeatures.Exists("left")) { + // Step 5.1. Set `x` to the result of invoking the rules for parsing + // integers on `tokenizedFeatures["left"]`. + // + // Step 5.2. If `x` is an error, set `x` to 0. + int32_t x = aFeatures.GetInt("left"); + + // Step 5.3. Optionally, clamp `x` in a user-agent-defined manner so that + // the window does not move outside the Web-exposed available screen area. + // (done later) + + // Step 5.4. Optionally, move `target`’s window such that the window’s + // left edge is at the horizontal coordinate `x` relative to the left edge + // of the Web-exposed screen area, measured in CSS pixels of target. + // The positive axis is rightward. + result.mLeft.emplace((CSSCoord(x) * aCSSToDesktopScale).Rounded()); + } + + // Step 6. If `tokenizedFeatures["top"]` exists: + if (aFeatures.Exists("top")) { + // Step 6.1. Set `y` to the result of invoking the rules for parsing + // integers on `tokenizedFeatures["top"]`. + // + // Step 6.2. If `y` is an error, set `y` to 0. + int32_t y = aFeatures.GetInt("top"); + + // Step 6.3. Optionally, clamp `y` in a user-agent-defined manner so that + // the window does not move outside the Web-exposed available screen area. + // (done later) + + // Step 6.4. Optionally, move `target`’s window such that the window’s top + // edge is at the vertical coordinate `y` relative to the top edge of the + // Web-exposed screen area, measured in CSS pixels of target. The positive + // axis is downward. + result.mTop.emplace((CSSCoord(y) * aCSSToDesktopScale).Rounded()); + } + + // Non-standard extension. + // Not exposed to web content. + if (aHasChromeParent && aFeatures.Exists("outerwidth")) { + int32_t width = aFeatures.GetInt("outerwidth"); + if (width) { + result.mOuterWidth.emplace(width); + } + } + + if (result.mOuterWidth.isNothing()) { + // Step 7. If `tokenizedFeatures["width"]` exists: + if (aFeatures.Exists("width")) { + // Step 7.1. Set `width` to the result of invoking the rules for parsing + // integers on `tokenizedFeatures["width"]`. + // + // Step 7.2. If `width` is an error, set `width` to 0. + int32_t width = aFeatures.GetInt("width"); + + // Step 7.3. If `width` is not 0: + if (width) { + // Step 7.3.1. Optionally, clamp `width` in a user-agent-defined manner + // so that the window does not get too small or bigger than the + // Web-exposed available screen area. + // (done later) + + // Step 7.3.2. Optionally, size `target`’s window by moving its right + // edge such that the distance between the left and right edges of the + // viewport are `width` CSS pixels of target. + result.mInnerWidth.emplace(width); + + // Step 7.3.3. Optionally, move target’s window in a user-agent-defined + // manner so that it does not grow outside the Web-exposed available + // screen area. + // (done later) + } + } + } + + // Non-standard extension. + // Not exposed to web content. + if (aHasChromeParent && aFeatures.Exists("outerheight")) { + int32_t height = aFeatures.GetInt("outerheight"); + if (height) { + result.mOuterHeight.emplace(height); + } + } + + if (result.mOuterHeight.isNothing()) { + // Step 8. If `tokenizedFeatures["height"]` exists: + if (aFeatures.Exists("height")) { + // Step 8.1. Set `height` to the result of invoking the rules for parsing + // integers on `tokenizedFeatures["height"]`. + // + // Step 8.2. If `height` is an error, set `height` to 0. + int32_t height = aFeatures.GetInt("height"); + + // Step 8.3. If `height` is not 0: + if (height) { + // Step 8.3.1. Optionally, clamp `height` in a user-agent-defined manner + // so that the window does not get too small or bigger than the + // Web-exposed available screen area. + // (done later) + + // Step 8.3.2. Optionally, size `target`’s window by moving its bottom + // edge such that the distance between the top and bottom edges of the + // viewport are `height` CSS pixels of target. + result.mInnerHeight.emplace(height); + + // Step 8.3.3. Optionally, move target’s window in a user-agent-defined + // manner so that it does not grow outside the Web-exposed available + // screen area. + // (done later) + } + } + } + + // NOTE: The value is handled only on chrome-priv code. + // See nsWindowWatcher::SizeOpenedWindow. + result.mLockAspectRatio = + aFeatures.GetBoolWithDefault("lockaspectratio", false); + return result; +} + +/* Size and position a new window according to aSizeSpec. This method + is assumed to be called after the window has already been given + a default position and size; thus its current position and size are + accurate defaults. The new window is made visible at method end. + @param aTreeOwner + The top-level nsIDocShellTreeOwner of the newly opened window. + @param aParent + The parent window, used to do security checks. + @param aIsCallerChrome + True if the code requesting the new window is privileged. + @param aSizeSpec + The size that the new window should be. +*/ +static void SizeOpenedWindow(nsIDocShellTreeOwner* aTreeOwner, + mozIDOMWindowProxy* aParent, bool aIsCallerChrome, + const SizeSpec& aSizeSpec) { + // We should only be sizing top-level windows if we're in the parent + // process. + MOZ_ASSERT(XRE_IsParentProcess()); + + // get various interfaces for aDocShellItem, used throughout this method + nsCOMPtr<nsIBaseWindow> treeOwnerAsWin(do_QueryInterface(aTreeOwner)); + if (!treeOwnerAsWin) { // we'll need this to actually size the docshell + return; + } + + // The current position and size will be unchanged if not specified + // (and they fit entirely onscreen). Also, calculate the difference + // between chrome and content sizes on aDocShellItem's window. + // This latter point becomes important if chrome and content + // specifications are mixed in aFeatures, and when bringing the window + // back from too far off the right or bottom edges of the screen. + DesktopIntCoord left = 0; + DesktopIntCoord top = 0; + CSSIntCoord width = 0; + CSSIntCoord height = 0; + // difference between chrome and content size + CSSIntCoord chromeWidth = 0; + CSSIntCoord chromeHeight = 0; + // whether the window size spec refers to chrome or content + bool sizeChromeWidth = true; + bool sizeChromeHeight = true; + + { + CSSToLayoutDeviceScale cssToDevScale = + treeOwnerAsWin->UnscaledDevicePixelsPerCSSPixel(); + DesktopToLayoutDeviceScale devToDesktopScale = + treeOwnerAsWin->DevicePixelsPerDesktopPixel(); + + LayoutDeviceIntRect devPxRect = treeOwnerAsWin->GetPositionAndSize(); + width = (LayoutDeviceCoord(devPxRect.width) / cssToDevScale).Rounded(); + height = (LayoutDeviceCoord(devPxRect.height) / cssToDevScale).Rounded(); + left = (LayoutDeviceCoord(devPxRect.x) / devToDesktopScale).Rounded(); + top = (LayoutDeviceCoord(devPxRect.y) / devToDesktopScale).Rounded(); + + LayoutDeviceIntSize contentSize; + bool hasPrimaryContent = false; + aTreeOwner->GetHasPrimaryContent(&hasPrimaryContent); + if (hasPrimaryContent) { + aTreeOwner->GetPrimaryContentSize(&contentSize.width, + &contentSize.height); + } else { + aTreeOwner->GetRootShellSize(&contentSize.width, &contentSize.height); + } + + CSSIntSize contentSizeCSS = RoundedToInt(contentSize / cssToDevScale); + chromeWidth = width - contentSizeCSS.width; + chromeHeight = height - contentSizeCSS.height; + } + + // Set up left/top + if (aSizeSpec.mLeft) { + left = *aSizeSpec.mLeft; + } + + if (aSizeSpec.mTop) { + top = *aSizeSpec.mTop; + } + + // Set up width + if (aSizeSpec.mOuterWidth) { + width = *aSizeSpec.mOuterWidth; + } else if (aSizeSpec.mInnerWidth) { + sizeChromeWidth = false; + width = *aSizeSpec.mInnerWidth; + } + + // Set up height + if (aSizeSpec.mOuterHeight) { + height = *aSizeSpec.mOuterHeight; + } else if (aSizeSpec.mInnerHeight) { + sizeChromeHeight = false; + height = *aSizeSpec.mInnerHeight; + } + + bool positionSpecified = aSizeSpec.PositionSpecified(); + + // Check security state for use in determining window dimensions + bool enabled = false; + if (aIsCallerChrome) { + // Only enable special privileges for chrome when chrome calls + // open() on a chrome window + enabled = !aParent || nsGlobalWindowOuter::Cast(aParent)->IsChromeWindow(); + } + + const CSSIntCoord extraWidth = sizeChromeWidth ? CSSIntCoord(0) : chromeWidth; + const CSSIntCoord extraHeight = + sizeChromeHeight ? CSSIntCoord(0) : chromeHeight; + + if (!enabled) { + // Security check failed. Ensure all args meet minimum reqs. + + int32_t oldTop = top, oldLeft = left; + + // We'll also need the screen dimensions + nsCOMPtr<nsIScreen> screen; + nsCOMPtr<nsIScreenManager> screenMgr( + do_GetService("@mozilla.org/gfx/screenmanager;1")); + if (screenMgr) { + // XXX width, height are in CSS rather than desktop pixels here. + screenMgr->ScreenForRect(left, top, width, height, + getter_AddRefs(screen)); + } + if (screen) { + CSSIntCoord winWidth = width + extraWidth; + CSSIntCoord winHeight = height + extraHeight; + + auto screenCssToDesktopScale = screen->GetCSSToDesktopScale(); + + const DesktopIntRect screenDesktopRect = screen->GetAvailRectDisplayPix(); + // Get screen dimensions (in CSS pixels) + const CSSSize screenCssSize = + screenDesktopRect.Size() / screenCssToDesktopScale; + + if (aSizeSpec.SizeSpecified()) { + if (!nsContentUtils::ShouldResistFingerprinting( + "When RFP is enabled, we unconditionally round new window " + "sizes. The code paths that create new windows are " + "complicated, and this is a conservative behavior to avoid " + "exempting something that shouldn't be. It also presents a " + "uniform behavior for something that's very browser-related.", + RFPTarget::RoundWindowSize)) { + /* Unlike position, force size out-of-bounds check only if + size actually was specified. Otherwise, intrinsically sized + windows are broken. */ + if (height < 100) { + height = 100; + winHeight = height + extraHeight; + } + if (winHeight > screenCssSize.height) { + height = static_cast<int32_t>(screenCssSize.height - extraHeight); + } + if (width < 100) { + width = 100; + winWidth = width + extraWidth; + } + if (winWidth > screenCssSize.width) { + width = static_cast<int32_t>(screenCssSize.width - extraWidth); + } + } else { + int32_t targetContentWidth = 0; + int32_t targetContentHeight = 0; + + nsContentUtils::CalcRoundedWindowSizeForResistingFingerprinting( + chromeWidth, chromeHeight, screenCssSize.width, + screenCssSize.height, width, height, sizeChromeWidth, + sizeChromeHeight, &targetContentWidth, &targetContentHeight); + + if (aSizeSpec.mInnerWidth || aSizeSpec.mOuterWidth) { + width = targetContentWidth; + winWidth = width + extraWidth; + } + + if (aSizeSpec.mInnerHeight || aSizeSpec.mOuterHeight) { + height = targetContentHeight; + winHeight = height + extraHeight; + } + } + } + + const DesktopIntCoord desktopWinWidth = + (CSSCoord(winWidth) * screenCssToDesktopScale).Rounded(); + const DesktopIntCoord desktopWinHeight = + (CSSCoord(winHeight) * screenCssToDesktopScale).Rounded(); + CheckedInt<int32_t> leftPlusWinWidth = int32_t(left); + leftPlusWinWidth += int32_t(desktopWinWidth); + if (!leftPlusWinWidth.isValid() || + leftPlusWinWidth.value() > screenDesktopRect.XMost()) { + left = screenDesktopRect.XMost() - desktopWinWidth; + } + if (left < screenDesktopRect.x) { + left = screenDesktopRect.x; + } + + CheckedInt<int32_t> topPlusWinHeight = int32_t(top); + topPlusWinHeight += int32_t(desktopWinHeight); + if (!topPlusWinHeight.isValid() || + topPlusWinHeight.value() > screenDesktopRect.YMost()) { + top = screenDesktopRect.YMost() - desktopWinHeight; + } + if (top < screenDesktopRect.y) { + top = screenDesktopRect.y; + } + + if (top != oldTop || left != oldLeft) { + positionSpecified = true; + } + } + } + + // size and position the window + + if (positionSpecified) { + treeOwnerAsWin->SetPositionDesktopPix(left, top); + } + + if (aSizeSpec.SizeSpecified()) { + const CSSToLayoutDeviceScale scale = + treeOwnerAsWin->UnscaledDevicePixelsPerCSSPixel(); + + /* Prefer to trust the interfaces, which think in terms of pure + chrome or content sizes. If we have a mix, use the chrome size + adjusted by the chrome/content differences calculated earlier. */ + if (!sizeChromeWidth && !sizeChromeHeight) { + const LayoutDeviceIntCoord widthDevPx = + (CSSCoord(width) * scale).Rounded(); + const LayoutDeviceIntCoord heightDevPx = + (CSSCoord(height) * scale).Rounded(); + bool hasPrimaryContent = false; + aTreeOwner->GetHasPrimaryContent(&hasPrimaryContent); + if (hasPrimaryContent) { + aTreeOwner->SetPrimaryContentSize(widthDevPx, heightDevPx); + } else { + aTreeOwner->SetRootShellSize(widthDevPx, heightDevPx); + } + } else { + const LayoutDeviceIntCoord widthDevPx = + (CSSCoord(width + extraWidth) * scale).Rounded(); + const LayoutDeviceIntCoord heightDevPx = + (CSSCoord(height + extraHeight) * scale).Rounded(); + treeOwnerAsWin->SetSize(widthDevPx, heightDevPx, false); + } + } + + if (aIsCallerChrome) { + nsCOMPtr<nsIAppWindow> appWin = do_GetInterface(treeOwnerAsWin); + if (appWin && aSizeSpec.mLockAspectRatio) { + appWin->LockAspectRatio(true); + } + } + + treeOwnerAsWin->SetVisibility(true); +} + +/* static */ +bool nsWindowWatcher::IsWindowOpenLocationModified( + const mozilla::dom::UserActivation::Modifiers& aModifiers, + int32_t* aLocation) { + // Perform the subset of BrowserUtils.whereToOpenLink in + // toolkit/modules/BrowserUtils.sys.mjs +#ifdef XP_MACOSX + bool metaKey = aModifiers.IsMeta(); +#else + bool metaKey = aModifiers.IsControl(); +#endif + bool shiftKey = aModifiers.IsShift(); + if (metaKey) { + if (shiftKey) { + *aLocation = nsIBrowserDOMWindow::OPEN_NEWTAB; + return true; + } + *aLocation = nsIBrowserDOMWindow::OPEN_NEWTAB_BACKGROUND; + return true; + } + if (shiftKey) { + *aLocation = nsIBrowserDOMWindow::OPEN_NEWWINDOW; + return true; + } + + return false; +} + +/* static */ +int32_t nsWindowWatcher::GetWindowOpenLocation( + nsPIDOMWindowOuter* aParent, uint32_t aChromeFlags, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool aCalledFromJS, bool aIsForPrinting) { + // These windows are not actually visible to the user, so we return the thing + // that we can always handle. + if (aIsForPrinting) { + return nsIBrowserDOMWindow::OPEN_PRINT_BROWSER; + } + + int32_t modifiedLocation = 0; + if (IsWindowOpenLocationModified(aModifiers, &modifiedLocation)) { + return modifiedLocation; + } + + // Where should we open this? + int32_t containerPref; + if (NS_FAILED( + Preferences::GetInt("browser.link.open_newwindow", &containerPref))) { + // We couldn't read the user preference, so fall back on the default. + return nsIBrowserDOMWindow::OPEN_NEWTAB; + } + + bool isDisabledOpenNewWindow = + aParent->GetFullScreen() && + Preferences::GetBool( + "browser.link.open_newwindow.disabled_in_fullscreen"); + + if (isDisabledOpenNewWindow && + (containerPref == nsIBrowserDOMWindow::OPEN_NEWWINDOW)) { + containerPref = nsIBrowserDOMWindow::OPEN_NEWTAB; + } + + if (containerPref != nsIBrowserDOMWindow::OPEN_NEWTAB && + containerPref != nsIBrowserDOMWindow::OPEN_CURRENTWINDOW) { + // Just open a window normally + return nsIBrowserDOMWindow::OPEN_NEWWINDOW; + } + + if (aCalledFromJS) { + /* Now check our restriction pref. The restriction pref is a power-user's + fine-tuning pref. values: + 0: no restrictions - divert everything + 1: don't divert window.open at all + 2: don't divert window.open with features + */ + int32_t restrictionPref = + Preferences::GetInt("browser.link.open_newwindow.restriction", 2); + if (restrictionPref < 0 || restrictionPref > 2) { + restrictionPref = 2; // Sane default behavior + } + + if (isDisabledOpenNewWindow) { + // In browser fullscreen, the window should be opened + // in the current window with no features (see bug 803675) + restrictionPref = 0; + } + + if (restrictionPref == 1) { + return nsIBrowserDOMWindow::OPEN_NEWWINDOW; + } + + if (restrictionPref == 2) { + // Only continue if there is no special chrome flags - with the exception + // of the remoteness and private flags, which might have been + // automatically flipped by Gecko. + int32_t uiChromeFlags = aChromeFlags; + uiChromeFlags &= ~(nsIWebBrowserChrome::CHROME_REMOTE_WINDOW | + nsIWebBrowserChrome::CHROME_FISSION_WINDOW | + nsIWebBrowserChrome::CHROME_PRIVATE_WINDOW | + nsIWebBrowserChrome::CHROME_NON_PRIVATE_WINDOW | + nsIWebBrowserChrome::CHROME_PRIVATE_LIFETIME); + if (uiChromeFlags != nsIWebBrowserChrome::CHROME_ALL) { + return nsIBrowserDOMWindow::OPEN_NEWWINDOW; + } + } + } + + return containerPref; +} diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.h b/toolkit/components/windowwatcher/nsWindowWatcher.h new file mode 100644 index 0000000000..d5921fcf58 --- /dev/null +++ b/toolkit/components/windowwatcher/nsWindowWatcher.h @@ -0,0 +1,114 @@ +/* -*- 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 __nsWindowWatcher_h__ +#define __nsWindowWatcher_h__ + +// {a21bfa01-f349-4394-a84c-8de5cf0737d0} +#define NS_WINDOWWATCHER_CID \ + { \ + 0xa21bfa01, 0xf349, 0x4394, { \ + 0xa8, 0x4c, 0x8d, 0xe5, 0xcf, 0x7, 0x37, 0xd0 \ + } \ + } + +#include "nsCOMPtr.h" +#include "Units.h" +#include "mozilla/Mutex.h" +#include "mozilla/Maybe.h" +#include "nsIWindowCreator.h" // for stupid compilers +#include "nsIWindowWatcher.h" +#include "nsIOpenWindowInfo.h" +#include "nsIPromptFactory.h" +#include "nsIRemoteTab.h" +#include "nsPIWindowWatcher.h" +#include "nsTArray.h" +#include "mozilla/dom/UserActivation.h" // mozilla::dom::UserActivation +#include "mozilla/dom/WindowFeatures.h" // mozilla::dom::WindowFeatures + +class nsIURI; +class nsIDocShellTreeItem; +class nsIDocShellTreeOwner; +class nsPIDOMWindowOuter; +class nsWatcherWindowEnumerator; +class nsPromptService; +struct nsWatcherWindowEntry; + +class nsWindowWatcher : public nsIWindowWatcher, + public nsPIWindowWatcher, + public nsIPromptFactory { + friend class nsWatcherWindowEnumerator; + + public: + nsWindowWatcher(); + + nsresult Init(); + + NS_DECL_ISUPPORTS + + NS_DECL_NSIWINDOWWATCHER + NS_DECL_NSPIWINDOWWATCHER + NS_DECL_NSIPROMPTFACTORY + + static bool IsWindowOpenLocationModified( + const mozilla::dom::UserActivation::Modifiers& aModifiers, + int32_t* aLocation); + + static int32_t GetWindowOpenLocation( + nsPIDOMWindowOuter* aParent, uint32_t aChromeFlags, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool aCalledFromJS, bool aIsForPrinting); + + static bool HaveSpecifiedSize(const mozilla::dom::WindowFeatures& features); + + protected: + virtual ~nsWindowWatcher(); + + friend class nsPromptService; + bool AddEnumerator(nsWatcherWindowEnumerator* aEnumerator); + bool RemoveEnumerator(nsWatcherWindowEnumerator* aEnumerator); + + nsWatcherWindowEntry* FindWindowEntry(mozIDOMWindowProxy* aWindow); + nsresult RemoveWindow(nsWatcherWindowEntry* aInfo); + + // Just like OpenWindowJS, but knows whether it got called via OpenWindowJS + // (which means called from script) or called via OpenWindow. + nsresult OpenWindowInternal( + mozIDOMWindowProxy* aParent, const nsACString& aUrl, + const nsACString& aName, const nsACString& aFeatures, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool aCalledFromJS, bool aDialog, bool aNavigate, nsIArray* aArgv, + bool aIsPopupSpam, bool aForceNoOpener, bool aForceNoReferrer, PrintKind, + nsDocShellLoadState* aLoadState, mozilla::dom::BrowsingContext** aResult); + + static nsresult URIfromURL(const nsACString& aURL, + mozIDOMWindowProxy* aParent, nsIURI** aURI); + + static bool ShouldOpenPopup(const mozilla::dom::WindowFeatures& aFeatures); + + static uint32_t CalculateChromeFlagsForContent( + const mozilla::dom::WindowFeatures& aFeatures, + const mozilla::dom::UserActivation::Modifiers& aModifiers, + bool* aIsPopupRequested); + + static uint32_t CalculateChromeFlagsForSystem( + const mozilla::dom::WindowFeatures& aFeatures, bool aDialog, + bool aChromeURL); + + private: + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult CreateChromeWindow( + nsIWebBrowserChrome* aParentChrome, uint32_t aChromeFlags, + nsIOpenWindowInfo* aOpenWindowInfo, nsIWebBrowserChrome** aResult); + + protected: + nsTArray<nsWatcherWindowEnumerator*> mEnumeratorList; + nsWatcherWindowEntry* mOldestWindow; + mozilla::Mutex mListLock MOZ_UNANNOTATED; + + nsCOMPtr<nsIWindowCreator> mWindowCreator; +}; + +#endif diff --git a/toolkit/components/windowwatcher/test/browser.toml b/toolkit/components/windowwatcher/test/browser.toml new file mode 100644 index 0000000000..90882dc378 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser.toml @@ -0,0 +1,35 @@ +[DEFAULT] +tags = "openwindow" +support-files = ["head.js"] + +["browser_new_content_window_chromeflags.js"] + +["browser_new_content_window_from_chrome_principal.js"] + +["browser_new_remote_window_flags.js"] + +["browser_new_sized_window.js"] +skip-if = ["os == 'win'"] # Bug 1276802 - Opening windows from content on Windows might not get the size right + +["browser_non_popup_from_popup.js"] + +["browser_popup_condition_current.js"] +skip-if = [ + "debug", # Opening many windows takes a long time on slow builds + "tsan", # Opening many windows takes a long time on slow builds + "socketprocess_networking", +] + +["browser_popup_condition_tab.js"] +skip-if = [ + "debug", # Opening many windows takes a long time on slow builds + "tsan", # Opening many windows takes a long time on slow builds + "socketprocess_networking", +] + +["browser_popup_condition_window.js"] +skip-if = [ + "debug", # Opening many windows takes a long time on slow builds + "tsan", # Opening many windows takes a long time on slow builds + "socketprocess_networking", +] diff --git a/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js b/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js new file mode 100644 index 0000000000..4eff3b46f0 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_new_content_window_chromeflags.js @@ -0,0 +1,278 @@ +/** + * Tests that chromeFlags are set properly on windows that are + * being opened from content. + */ + +// The following are not allowed from web content. +// +// <feature string>: { +// flag: <associated nsIWebBrowserChrome flag>, +// defaults_to: <what this feature defaults to normally> +// } +const DISALLOWED = { + location: { + flag: Ci.nsIWebBrowserChrome.CHROME_LOCATIONBAR, + defaults_to: true, + }, + chrome: { + flag: Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME, + defaults_to: false, + }, + dialog: { + flag: Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG, + defaults_to: false, + }, + private: { + flag: Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW, + defaults_to: false, + }, + "non-private": { + flag: Ci.nsIWebBrowserChrome.CHROME_NON_PRIVATE_WINDOW, + defaults_to: false, + }, + // "all": + // checked manually, since this is an aggregate + // flag. + // + // "remote": + // checked manually, since its default value will + // depend on whether or not e10s is enabled by default. + alwaysLowered: { + flag: Ci.nsIWebBrowserChrome.CHROME_WINDOW_LOWERED, + defaults_to: false, + }, + "z-lock": { + flag: Ci.nsIWebBrowserChrome.CHROME_WINDOW_LOWERED, // Renamed to alwaysLowered + defaults_to: false, + }, + alwaysRaised: { + flag: Ci.nsIWebBrowserChrome.CHROME_WINDOW_RAISED, + defaults_to: false, + }, + alwaysOnTop: { + flag: Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP, + defaults_to: false, + }, + suppressanimation: { + flag: Ci.nsIWebBrowserChrome.CHROME_SUPPRESS_ANIMATION, + defaults_to: false, + }, + extrachrome: { + flag: Ci.nsIWebBrowserChrome.CHROME_EXTRA, + defaults_to: false, + }, + centerscreen: { + flag: Ci.nsIWebBrowserChrome.CHROME_CENTER_SCREEN, + defaults_to: false, + }, + dependent: { + flag: Ci.nsIWebBrowserChrome.CHROME_DEPENDENT, + defaults_to: false, + }, + modal: { + flag: Ci.nsIWebBrowserChrome.CHROME_MODAL, + defaults_to: false, + }, + titlebar: { + flag: Ci.nsIWebBrowserChrome.CHROME_TITLEBAR, + defaults_to: true, + }, + close: { + flag: Ci.nsIWebBrowserChrome.CHROME_WINDOW_CLOSE, + defaults_to: true, + }, + resizable: { + flag: Ci.nsIWebBrowserChrome.CHROME_WINDOW_RESIZE, + defaults_to: true, + }, + status: { + flag: Ci.nsIWebBrowserChrome.CHROME_STATUSBAR, + defaults_to: true, + }, +}; + +// This magic value of 2 means that by default, when content tries +// to open a new window, it'll actually open in a new window instead +// of a new tab. +Services.prefs.setIntPref("browser.link.open_newwindow", 2); +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.link.open_newwindow"); +}); + +/** + * Given some nsIDOMWindow for a window running in the parent process, + * asynchronously return the nsIWebBrowserChrome chrome flags for the + * associated content window. + * + * @param win (nsIDOMWindow) + * @returns int + */ +function getContentChromeFlags(win) { + let b = win.gBrowser.selectedBrowser; + return SpecialPowers.spawn(b, [], async function () { + // Content scripts provide docShell as a global. + /* global docShell */ + docShell.QueryInterface(Ci.nsIInterfaceRequestor); + try { + // This will throw if we're not a remote browser. + return docShell + .getInterface(Ci.nsIBrowserChild) + .QueryInterface(Ci.nsIWebBrowserChrome).chromeFlags; + } catch (e) { + // This must be a non-remote browser... + return docShell.treeOwner.QueryInterface( + Ci.nsIWebBrowserChrome + ).chromeFlags; + } + }); +} + +/** + * For some chromeFlags, ensures that flags in the DISALLOWED + * group were not modified. + * + * @param chromeFlags (int) + * Some chromeFlags to check. + */ +function assertContentFlags(chromeFlags, isPopup) { + for (let feature in DISALLOWED) { + let flag = DISALLOWED[feature].flag; + Assert.ok(flag, "Expected flag to be a non-zeroish value"); + if (DISALLOWED[feature].defaults_to) { + // The feature is supposed to default to true, so it should + // stay true. + Assert.ok( + chromeFlags & flag, + `Expected feature ${feature} to be unchanged` + ); + } else { + // The feature is supposed to default to false, so it should + // stay false. + Assert.ok( + !(chromeFlags & flag), + `Expected feature ${feature} to be unchanged` + ); + } + } +} + +/** + * Opens a window from content using window.open with the + * features computed from DISALLOWED. The computed feature string attempts to + * flip every feature away from their default. + */ +add_task(async function test_disallowed_flags() { + // Construct a features string that flips all DISALLOWED features + // to not be their defaults. + const DISALLOWED_STRING = Object.keys(DISALLOWED) + .map(feature => { + let toValue = DISALLOWED[feature].defaults_to ? "no" : "yes"; + return `${feature}=${toValue}`; + }) + .join(","); + + const FEATURES = [DISALLOWED_STRING].join(","); + + const SCRIPT_PAGE = `data:text/html,<script>window.open("about:blank", "_blank", "${FEATURES}");</script>`; + const SCRIPT_PAGE_FOR_CHROME_ALL = `data:text/html,<script>window.open("about:blank", "_blank", "all");</script>`; + + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SCRIPT_PAGE, + }, + async function (browser) { + let win = await newWinPromise; + let parentChromeFlags = getParentChromeFlags(win); + assertContentFlags(parentChromeFlags); + + if (win.gMultiProcessBrowser) { + Assert.ok( + parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, + "Should be remote by default" + ); + } else { + Assert.ok( + !(parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW), + "Should not be remote by default" + ); + } + + // Confusingly, chromeFlags also exist in the content process + // as part of the BrowserChild, so we have to check those too. + let contentChromeFlags = await getContentChromeFlags(win); + assertContentFlags(contentChromeFlags); + + Assert.equal( + parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, + contentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, + "Should have matching remote value in parent and content" + ); + + Assert.equal( + parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW, + contentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW, + "Should have matching fission value in parent and content" + ); + + await BrowserTestUtils.closeWindow(win); + } + ); + + // We check "all" manually, since that's an aggregate flag + // and doesn't fit nicely into the ALLOWED / DISALLOWED scheme + newWinPromise = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SCRIPT_PAGE_FOR_CHROME_ALL, + }, + async function (browser) { + let win = await newWinPromise; + let parentChromeFlags = getParentChromeFlags(win); + Assert.notEqual( + parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_ALL, + Ci.nsIWebBrowserChrome.CHROME_ALL, + "Should not have been able to set CHROME_ALL" + ); + await BrowserTestUtils.closeWindow(win); + } + ); +}); + +/** + * Opens a window with some chrome flags specified, which should not affect + * scrollbars flag which defaults to true when not disabled explicitly. + */ +add_task(async function test_scrollbars_flag() { + const SCRIPT = 'window.open("about:blank", "_blank", "toolbar=0");'; + const SCRIPT_PAGE = `data:text/html,<script>${SCRIPT}</script>`; + + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SCRIPT_PAGE, + }, + async function (browser) { + let win = await newWinPromise; + + let parentChromeFlags = getParentChromeFlags(win); + Assert.ok( + parentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS, + "Should have scrollbars when not disabled explicitly" + ); + + let contentChromeFlags = await getContentChromeFlags(win); + Assert.ok( + contentChromeFlags & Ci.nsIWebBrowserChrome.CHROME_SCROLLBARS, + "Should have scrollbars when not disabled explicitly" + ); + + await BrowserTestUtils.closeWindow(win); + } + ); +}); diff --git a/toolkit/components/windowwatcher/test/browser_new_content_window_from_chrome_principal.js b/toolkit/components/windowwatcher/test/browser_new_content_window_from_chrome_principal.js new file mode 100644 index 0000000000..28c1c23e60 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_new_content_window_from_chrome_principal.js @@ -0,0 +1,44 @@ +"use strict"; + +/** + * Tests that if chrome-privileged code calls .open() on an + * unprivileged window, that the principal in the newly + * opened window is appropriately set. + */ +add_task(async function test_chrome_opens_window() { + // This magic value of 2 means that by default, when content tries + // to open a new window, it'll actually open in a new window instead + // of a new tab. + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 2]], + }); + + let newWinPromise = BrowserTestUtils.waitForNewWindow({ + url: "https://example.com/", + }); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.open("https://example.com/", "_blank"); + }); + + let win = await newWinPromise; + let browser = win.gBrowser.selectedBrowser; + Assert.ok( + E10SUtils.isWebRemoteType(browser.remoteType), + "Should have the default content remote type." + ); + + await SpecialPowers.spawn(browser, [], async function () { + Assert.ok( + !content.document.nodePrincipal.isSystemPrincipal, + "We should not have a system principal." + ); + Assert.equal( + content.document.nodePrincipal.origin, + "https://example.com", + "Should have the example.com principal" + ); + }); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js b/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js new file mode 100644 index 0000000000..06a15d123c --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_new_remote_window_flags.js @@ -0,0 +1,85 @@ +/** + * Tests that when a remote browser opens a new window that the + * newly opened window is also remote. + */ + +const ANCHOR_PAGE = `data:text/html,<a href="about:blank" target="_blank">Click me!</a>`; +const SCRIPT_PAGE = `data:text/html,<script>window.open("about:blank", "_blank");</script>`; + +// This magic value of 2 means that by default, when content tries +// to open a new window, it'll actually open in a new window instead +// of a new tab. +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 2]], + }); +}); + +function assertFlags(win) { + let docShell = win.docShell; + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let chromeFlags = docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags; + Assert.ok( + loadContext.useRemoteTabs, + "Should be using remote tabs on the load context" + ); + Assert.ok( + chromeFlags & Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW, + "Should have the remoteness chrome flag on the window" + ); +} + +/** + * Content can open a window using a target="_blank" link + */ +add_task(async function test_new_remote_window_flags_target_blank() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: ANCHOR_PAGE, + }, + async function (browser) { + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + await BrowserTestUtils.synthesizeMouseAtCenter("a", {}, browser); + let win = await newWinPromise; + assertFlags(win); + await BrowserTestUtils.closeWindow(win); + } + ); +}); + +/** + * Content can open a window using window.open + */ +add_task(async function test_new_remote_window_flags_window_open() { + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: SCRIPT_PAGE, + }, + async function (browser) { + let win = await newWinPromise; + assertFlags(win); + await BrowserTestUtils.closeWindow(win); + } + ); +}); + +/** + * Privileged content scripts can also open new windows + * using content.open. + */ +add_task(async function test_new_remote_window_flags_content_open() { + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + content.open("about:blank", "_blank"); + }); + + let win = await newWinPromise; + assertFlags(win); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/toolkit/components/windowwatcher/test/browser_new_sized_window.js b/toolkit/components/windowwatcher/test/browser_new_sized_window.js new file mode 100644 index 0000000000..0a1e63a8c8 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_new_sized_window.js @@ -0,0 +1,67 @@ +"use strict"; + +/** + * Tests that content can open windows at requested dimensions + * of height and width. + */ + +/** + * This utility function does most of the actual testing. We + * construct a feature string suitable for the passed in width + * and height, and then run that script in content to open the + * new window. When the new window comes up, this function tests + * to ensure that the content area of the initial browser is the + * requested dimensions. Finally, we also ensure that we're not + * persisting the position, size or sizemode of the new browser + * window. + */ +function test_dimensions({ width, height }) { + let features = []; + if (width) { + features.push(`width=${width}`); + } + if (height) { + features.push(`height=${height}`); + } + const FEATURE_STR = features.join(","); + const SCRIPT_PAGE = `data:text/html,<script>window.open("about:blank", "_blank", "${FEATURE_STR}");</script>`; + + let newWinPromise = BrowserTestUtils.waitForNewWindow(); + + return BrowserTestUtils.withNewTab( + { + gBrowser, + url: SCRIPT_PAGE, + }, + async function (browser) { + let win = await newWinPromise; + let rect = win.gBrowser.selectedBrowser.getBoundingClientRect(); + + if (width) { + Assert.equal(rect.width, width, "Should have the requested width"); + } + + if (height) { + Assert.equal(rect.height, height, "Should have the requested height"); + } + + let treeOwner = win.docShell.treeOwner; + let persistPosition = {}; + let persistSize = {}; + let persistSizeMode = {}; + treeOwner.getPersistence(persistPosition, persistSize, persistSizeMode); + + Assert.ok(!persistPosition.value, "Should not persist position"); + Assert.ok(!persistSize.value, "Should not persist size"); + Assert.ok(!persistSizeMode.value, "Should not persist size mode"); + + await BrowserTestUtils.closeWindow(win); + } + ); +} + +add_task(async function test_new_sized_window() { + await test_dimensions({ width: 100 }); + await test_dimensions({ height: 150 }); + await test_dimensions({ width: 300, height: 200 }); +}); diff --git a/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js b/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js new file mode 100644 index 0000000000..9c905bacc1 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_non_popup_from_popup.js @@ -0,0 +1,53 @@ +"use strict"; + +// Opening non-popup from a popup should open a new tab in the most recent +// non-popup window. +add_task(async function test_non_popup_from_popup() { + const BLANK_PAGE = "data:text/html,"; + + // A page opened in a new tab. + const OPEN_PAGE = "data:text/plain,hello"; + + // A page opened in a new popup. + // This opens a new non-popup page with OPEN_PAGE, + // tha should be opened in a new tab in most recent window. + const NON_POPUP_OPENER = btoa( + `data:text/html,<script>window.open('${OPEN_PAGE}', '', '')</script>` + ); + + // A page opened in a new tab. + // This opens a popup with NON_POPUP_OPENER. + const POPUP_OPENER = `data:text/html,<script>window.open(atob("${NON_POPUP_OPENER}"), "", "width=500");</script>`; + + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 3]], + }); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: BLANK_PAGE, + }, + async function (browser) { + // Wait for a popup opened by POPUP_OPENER. + const newPopupPromise = BrowserTestUtils.waitForNewWindow(); + + // Wait for a new tab opened by NON_POPUP_OPENER. + const newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, OPEN_PAGE); + + // Open a new tab that opens a popup with NON_POPUP_OPENER. + BrowserTestUtils.startLoadingURIString(gBrowser, POPUP_OPENER); + + let win = await newPopupPromise; + Assert.ok(true, "popup is opened"); + + let tab = await newTabPromise; + Assert.ok(true, "new tab is opened in the recent window"); + + BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + } + ); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/windowwatcher/test/browser_popup_condition_current.js b/toolkit/components/windowwatcher/test/browser_popup_condition_current.js new file mode 100644 index 0000000000..a033c6738a --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_popup_condition_current.js @@ -0,0 +1,13 @@ +"use strict"; + +/** + * Opens windows from content using window.open with several features patterns. + */ +add_task(async function test_popup_conditions_current() { + // Non-popup is opened in a current tab. + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 1]], + }); + await testPopupPatterns("current"); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/windowwatcher/test/browser_popup_condition_tab.js b/toolkit/components/windowwatcher/test/browser_popup_condition_tab.js new file mode 100644 index 0000000000..36a98fd511 --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_popup_condition_tab.js @@ -0,0 +1,13 @@ +"use strict"; + +/** + * Opens windows from content using window.open with several features patterns. + */ +add_task(async function test_popup_conditions_tab() { + // Non-popup is opened in a new tab (default behavior). + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 3]], + }); + await testPopupPatterns("tab"); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/windowwatcher/test/browser_popup_condition_window.js b/toolkit/components/windowwatcher/test/browser_popup_condition_window.js new file mode 100644 index 0000000000..6f66124a1a --- /dev/null +++ b/toolkit/components/windowwatcher/test/browser_popup_condition_window.js @@ -0,0 +1,13 @@ +"use strict"; + +/** + * Opens windows from content using window.open with several features patterns. + */ +add_task(async function test_popup_conditions_window() { + // Non-popup is opened in a new window. + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", 2]], + }); + await testPopupPatterns("window"); + await SpecialPowers.popPrefEnv(); +}); diff --git a/toolkit/components/windowwatcher/test/chrome.toml b/toolkit/components/windowwatcher/test/chrome.toml new file mode 100644 index 0000000000..24dd1019d7 --- /dev/null +++ b/toolkit/components/windowwatcher/test/chrome.toml @@ -0,0 +1,10 @@ +[DEFAULT] +tags = "openwindow" + +["test_alwaysOnTop_windows.html"] +skip-if = ["os != 'win'"] + +["test_dialog_arguments.html"] +support-files = ["file_test_dialog.html"] + +["test_modal_windows.html"] diff --git a/toolkit/components/windowwatcher/test/file_named_window.html b/toolkit/components/windowwatcher/test/file_named_window.html new file mode 100644 index 0000000000..c66d89cd34 --- /dev/null +++ b/toolkit/components/windowwatcher/test/file_named_window.html @@ -0,0 +1,5 @@ +<html> +<body> +test_named_window.html new window +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/file_storage_copied.html b/toolkit/components/windowwatcher/test/file_storage_copied.html new file mode 100644 index 0000000000..e32709315a --- /dev/null +++ b/toolkit/components/windowwatcher/test/file_storage_copied.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<!-- +This page is opened in a new window by test_storage_copied.html. +We need to return the sessionStorage value for the item "test-item", +by way of postMessage. +--> +<head> +<body>Opened!</body> +<script> + window.postMessage(window.sessionStorage.getItem("test-item"), "*"); +</script> +</html> diff --git a/toolkit/components/windowwatcher/test/file_test_dialog.html b/toolkit/components/windowwatcher/test/file_test_dialog.html new file mode 100644 index 0000000000..efb11d0077 --- /dev/null +++ b/toolkit/components/windowwatcher/test/file_test_dialog.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<!-- +This page is opened in a new window by test_dialog_arguments. It is +a dialog which expects a Symbol to be passed in the dialog arguments. +Once we load, we call back into the opener with the argument we were +passed. +--> +<head> +<body>Opened!</body> +<script> + window.arguments[1].done(window.arguments[0]); +</script> +</html> diff --git a/toolkit/components/windowwatcher/test/head.js b/toolkit/components/windowwatcher/test/head.js new file mode 100644 index 0000000000..f72344bcd0 --- /dev/null +++ b/toolkit/components/windowwatcher/test/head.js @@ -0,0 +1,207 @@ +/** + * Given some nsIDOMWindow for a window running in the parent + * process, return the nsIWebBrowserChrome chrome flags for + * the associated XUL window. + * + * @param win (nsIDOMWindow) + * Some window in the parent process. + * @returns int + */ +function getParentChromeFlags(win) { + return win.docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow).chromeFlags; +} + +const WINDOW_OPEN_FEATURES_PATTERNS = [ + { features: "", popup: false }, + + // If features isn't empty, the following should be true to open tab/window: + // * location or toolbar (defaults to false) + // * menubar (defaults to false) + // * resizable (defaults to true) + // * scrollbars (defaults to false) + // * status (defaults to false) + { features: "location,menubar,resizable,scrollbars,status", popup: false }, + { features: "toolbar,menubar,resizable,scrollbars,status", popup: false }, + + // resizable defaults to true. + { features: "location,menubar,scrollbars,status", popup: false }, + + // The following testcases use "location,menubar,scrollbars,status" + // as the base non-popup case, and test the boundary between popup + // vs non-popup. + + // If either location or toolbar is true, not popup. + { + features: "toolbar,menubar,resizable,scrollbars,status", + popup: false, + }, + { + features: "location,menubar,resizable,scrollbars,status", + popup: false, + }, + + // If both location and toolbar are false, popup. + { features: "menubar,scrollbars,status", popup: true }, + + // If menubar is false, popup. + { features: "location,resizable,scrollbars,status", popup: true }, + + // If resizable is true, not popup. + { + features: "location,menubar,resizable=yes,scrollbars,status", + popup: false, + }, + + // If resizable is false, popup. + { features: "location,menubar,resizable=0,scrollbars,status", popup: true }, + + // If scrollbars is false, popup. + { features: "location,menubar,resizable,status", popup: true }, + + // If status is false, popup. + { features: "location,menubar,resizable,scrollbars", popup: true }, + + // position and size have no effect. + { + features: + "location,menubar,scrollbars,status," + + "left=100,screenX=100,top=100,screenY=100," + + "width=100,innerWidth=100,outerWidth=100," + + "height=100,innerHeight=100,outerHeight=100", + popup: false, + }, + + // Most feature defaults to false if the feature is not empty. + // Specifying only some of them opens a popup. + { features: "location,toolbar,menubar", popup: true }, + { features: "resizable,scrollbars,status", popup: true }, + + // Specifying unknown feature makes the feature not empty. + { features: "someunknownfeature", popup: true }, + + // noopener and noreferrer are removed before testing if feature is empty. + { features: "noopener,noreferrer", popup: false }, +]; + +const WINDOW_CHROME_FLAGS = { + CHROME_WINDOW_BORDERS: true, + CHROME_WINDOW_CLOSE: true, + CHROME_WINDOW_RESIZE: true, + CHROME_LOCATIONBAR: true, + CHROME_STATUSBAR: true, + CHROME_SCROLLBARS: true, + CHROME_TITLEBAR: true, + + CHROME_MENUBAR: true, + CHROME_TOOLBAR: true, + CHROME_PERSONAL_TOOLBAR: true, +}; + +const POPUP_CHROME_FLAGS = { + CHROME_WINDOW_BORDERS: true, + CHROME_WINDOW_CLOSE: true, + CHROME_WINDOW_RESIZE: true, + CHROME_LOCATIONBAR: true, + CHROME_STATUSBAR: true, + CHROME_SCROLLBARS: true, + CHROME_TITLEBAR: true, + + CHROME_MENUBAR: false, + CHROME_TOOLBAR: false, + CHROME_PERSONAL_TOOLBAR: false, +}; + +async function testPopupPatterns(nonPopup) { + for (const { features, popup } of WINDOW_OPEN_FEATURES_PATTERNS) { + const BLANK_PAGE = "data:text/html,"; + const OPEN_PAGE = "data:text/plain,hello"; + const SCRIPT_PAGE = `data:text/html,<script>window.open("${OPEN_PAGE}", "", "${features}");</script>`; + + async function testNewWindow(flags) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: BLANK_PAGE, + }, + async function (browser) { + const newWinPromise = BrowserTestUtils.waitForNewWindow(); + BrowserTestUtils.startLoadingURIString(gBrowser, SCRIPT_PAGE); + + const win = await newWinPromise; + const parentChromeFlags = getParentChromeFlags(win); + + for (const [name, visible] of Object.entries(flags)) { + if (visible) { + Assert.equal( + !!(parentChromeFlags & Ci.nsIWebBrowserChrome[name]), + true, + `${name} should be present for features "${features}"` + ); + } else { + Assert.equal( + !!(parentChromeFlags & Ci.nsIWebBrowserChrome[name]), + false, + `${name} should not be present for features "${features}"` + ); + } + } + + await BrowserTestUtils.closeWindow(win); + } + ); + } + + async function testNewTab() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: BLANK_PAGE, + }, + async function (browser) { + const newTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + OPEN_PAGE + ); + BrowserTestUtils.startLoadingURIString(gBrowser, SCRIPT_PAGE); + + let tab = await newTabPromise; + BrowserTestUtils.removeTab(tab); + } + ); + } + + async function testCurrentTab() { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: BLANK_PAGE, + }, + async function (browser) { + const pagePromise = BrowserTestUtils.browserLoaded( + browser, + false, + OPEN_PAGE + ); + BrowserTestUtils.startLoadingURIString(gBrowser, SCRIPT_PAGE); + + await pagePromise; + } + ); + } + + if (!popup) { + if (nonPopup == "window") { + await testNewWindow(WINDOW_CHROME_FLAGS); + } else if (nonPopup == "tab") { + await testNewTab(); + } else { + // current tab + await testCurrentTab(); + } + } else { + await testNewWindow(POPUP_CHROME_FLAGS); + } + } +} diff --git a/toolkit/components/windowwatcher/test/mochitest.toml b/toolkit/components/windowwatcher/test/mochitest.toml new file mode 100644 index 0000000000..67b0f80eb8 --- /dev/null +++ b/toolkit/components/windowwatcher/test/mochitest.toml @@ -0,0 +1,11 @@ +[DEFAULT] +tags = "openwindow" +skip-if = ["os == 'android'"] # Fennec doesn't support web content opening new windows (See bug 1277544 for details) + +["test_blank_named_window.html"] + +["test_named_window.html"] +support-files = ["file_named_window.html"] + +["test_storage_copied.html"] +support-files = ["file_storage_copied.html"] diff --git a/toolkit/components/windowwatcher/test/moz.build b/toolkit/components/windowwatcher/test/moz.build new file mode 100644 index 0000000000..0333f1deda --- /dev/null +++ b/toolkit/components/windowwatcher/test/moz.build @@ -0,0 +1,17 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +BROWSER_CHROME_MANIFESTS += [ + "browser.toml", +] + +MOCHITEST_MANIFESTS += [ + "mochitest.toml", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "chrome.toml", +] diff --git a/toolkit/components/windowwatcher/test/test_alwaysOnTop_windows.html b/toolkit/components/windowwatcher/test/test_alwaysOnTop_windows.html new file mode 100644 index 0000000000..05c768c93f --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_alwaysOnTop_windows.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +Tests the alwaysOnTop window feature for the Windows OS. +--> +<head> + <meta charset="utf-8"> + <title>Test an alwaysOnTop window on Windows</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript"> + + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + const {ctypes} = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + function assertAlwaysOnTop(win, expected) { + let docShell = win.docShell; + let chromeFlags = docShell.treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow) + .chromeFlags; + let hasFlag = !!(chromeFlags & Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP); + is(hasFlag, expected, "Window should have CHROME_ALWAYS_ON_TOP flag."); + + const hWND = Number(win.docShell.treeOwner.nsIBaseWindow.nativeHandle); + const WS_EX_TOPMOST = 0x00000008; + const GWL_EXSTYLE = -20; + + let lib = ctypes.open("user32.dll"); + // On 32-bit systems, the function we need to call is GetWindowLongW. On + // 64-bit systems, the function is GetWindowLongPtrW. Interestingly, + // the MSDN page[1] for GetWindowLongPtrW claims that calling it should work + // on both 32-bit and 64-bit, but that didn't appear to be the case here + // with local testing, hence the conditional. + // + // [1]: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowlongptrw + let GetWindowFuncSymbol = Services.appinfo.is64Bit ? "GetWindowLongPtrW" + : "GetWindowLongW"; + let GetWindowFunc = lib.declare(GetWindowFuncSymbol, ctypes.winapi_abi, + ctypes.intptr_t, ctypes.uintptr_t, + ctypes.int); + + let styles = GetWindowFunc(hWND, GWL_EXSTYLE); + let isAlwaysOnTop = !!(styles & WS_EX_TOPMOST); + is(isAlwaysOnTop, expected, "Window should be always on top."); + lib.close(); + } + + if (Services.appinfo.OS != "WINNT") { + ok(false, "This test is only designed to run on Windows."); + } else { + add_task(async function() { + let normalWinOpened = BrowserTestUtils.domWindowOpenedAndLoaded(); + window.openDialog("about:blank", + "_blank", "chrome,width=100,height=100,noopener", null); + let normalWin = await normalWinOpened; + ok(normalWin, "Normal window opened"); + assertAlwaysOnTop(normalWin, false); + await BrowserTestUtils.closeWindow(normalWin); + + let alwaysOnTopWinOpened = BrowserTestUtils.domWindowOpenedAndLoaded(); + window.openDialog("about:blank", + "_blank", "chrome,width=100,height=100,alwaysOnTop,noopener", null); + let alwaysOnTopWin = await alwaysOnTopWinOpened; + ok(alwaysOnTopWin, "AlwaysOnTop window opened"); + assertAlwaysOnTop(alwaysOnTopWin, true); + await BrowserTestUtils.closeWindow(alwaysOnTopWin); + }); + } + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/test_blank_named_window.html b/toolkit/components/windowwatcher/test/test_blank_named_window.html new file mode 100644 index 0000000000..f8472d216a --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_blank_named_window.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that when opening a window with the reserved name _blank that the new +window does not get that name, and that subsequent window openings with that +name result in new windows being opened. +--> +<head> + <meta charset="utf-8"> + <title>Test named windows</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js" type="application/javascript"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + "use strict"; + add_task(async function() { + // This magic value of 2 means that by default, when content tries + // to open a new window, it'll actually open in a new window instead + // of a new tab. + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.link.open_newwindow", 2], + ]}); + + let win1 = window.open("data:text/html,<p>This is window 1 for test_blank_named_window.html</p>", "_blank"); + + let name = SpecialPowers.wrap(win1).docShell.name; + + is(name, "", "Should have no name"); + + let win2 = window.open("data:text/html,<p>This is window 2 for test_blank_named_window.html</p>", "_blank"); + isnot(win1, win2, "Should not have gotten back the same window"); + + win1.close(); + win2.close(); + }); + </script> +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/test_dialog_arguments.html b/toolkit/components/windowwatcher/test/test_dialog_arguments.html new file mode 100644 index 0000000000..6e937c6d01 --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_dialog_arguments.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that arguments can be passed to dialogs. +--> +<head> + <meta charset="utf-8"> + <title>Test a modal window</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript"> + + const TEST_ITEM = Symbol("test-item"); + + function done(returnedItem) { + is(returnedItem, TEST_ITEM, + "Dialog should have received test item"); + win.close(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + let win = window.browsingContext.topChromeWindow.openDialog("file_test_dialog.html", "_blank", "width=100,height=100", TEST_ITEM, window); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/test_modal_windows.html b/toolkit/components/windowwatcher/test/test_modal_windows.html new file mode 100644 index 0000000000..3c0aa6ebb5 --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_modal_windows.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that the parent can open modal windows, and that the modal window +that is opened reports itself as being modal. +--> +<head> + <meta charset="utf-8"> + <title>Test a modal window</title> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> + + <script type="application/javascript"> + + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + add_task(async function() { + BrowserTestUtils.domWindowOpened().then((win) => { + let treeOwner = win.docShell.treeOwner; + let chromeFlags = treeOwner.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow) + .chromeFlags; + ok(chromeFlags & Ci.nsIWebBrowserChrome.CHROME_MODAL, + "Should have the modal chrome flag"); + + let wbc = treeOwner.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserChrome); + ok(wbc.isWindowModal(), "Should report as modal"); + + win.close(); + }); + + window.openDialog("data:text/html,<p>This is a modal window for test_modal_windows.html</p>", + "_blank", "modal,noopener", null); + // Since the modal runs a nested event loop, just to be on the safe side, + // we'll wait a tick of the main event loop before resolving the task. + await new Promise(resolve => setTimeout(resolve, 0)); + }); + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/test_named_window.html b/toolkit/components/windowwatcher/test/test_named_window.html new file mode 100644 index 0000000000..af3536dd5e --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_named_window.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test that when content opens a new window with a name, that the +newly opened window actually gets that name, and that subsequent +attempts to open a window with that name will target the same +window. +--> +<head> + <meta charset="utf-8"> + <title>Test named windows</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js" type="application/javascript"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a href="#" id="link">Click me</a> + + <script type="application/javascript"> + "use strict"; + + const NAME = "my_window"; + const TARGET_URL = location.href.replace("test_named_window.html", + "file_named_window.html"); + const TARGET_URL_2 = TARGET_URL + "#2"; + const TARGET_URL_3 = TARGET_URL + "#3"; + + /** + * Returns a Promise that resolves once some target has had + * some event dispatched on it. + * + * @param target + * The thing to wait for the event to be dispatched + * through. + * @param eventName + * The name of the event to wait for. + * @returns Promise + */ + function promiseEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, function(e) { + resolve(e); + }, {capture: true, once: true}); + }); + } + + add_task(async function() { + // This magic value of 2 means that by default, when content tries + // to open a new window, it'll actually open in a new window instead + // of a new tab. + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.link.open_newwindow", 2], + ]}); + + let win1 = window.open(TARGET_URL, "my_window"); + await promiseEvent(win1, "load"); + + let name = SpecialPowers.wrap(win1).docShell.name; + + is(name, NAME, "Should have the expected name"); + is(win1.location.href, new URL(TARGET_URL).href, + "Should have loaded target TARGET_URL in the original window"); + + let hashChange = promiseEvent(win1, "hashchange"); + let win2 = window.open(TARGET_URL_2, "my_window"); + await hashChange; + + is(win1, win2, "Should have gotten back the same window"); + is(win1.location.href, new URL(TARGET_URL_2).href, + "Should have re-targeted pre-existing window"); + + hashChange = promiseEvent(win1, "hashchange"); + let link = document.getElementById("link"); + link.setAttribute("target", NAME); + link.setAttribute("href", TARGET_URL_3); + link.click(); + + await hashChange; + + is(win1.location.href, new URL(TARGET_URL_3).href, + "Should have re-targeted pre-existing window"); + + win1.close(); + }); + </script> +</body> +</html> diff --git a/toolkit/components/windowwatcher/test/test_storage_copied.html b/toolkit/components/windowwatcher/test/test_storage_copied.html new file mode 100644 index 0000000000..de05cd6855 --- /dev/null +++ b/toolkit/components/windowwatcher/test/test_storage_copied.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test sessionStorage is copied over when a new window opens to the +same domain as the opener. +--> +<head> + <meta charset="utf-8"> + <title>Test storage copied</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js" type="application/javascript"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + "use strict"; + + function waitForMessage(win) { + return new Promise(resolve => { + win.addEventListener("message", function(event) { + resolve(event.data); + }, {once: true}); + }); + } + + add_task(async function() { + const TEST_VALUE = "test-value"; + // This magic value of 2 means that by default, when content tries + // to open a new window, it'll actually open in a new window instead + // of a new tab. + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.link.open_newwindow", 2], + ]}); + + window.sessionStorage.setItem("test-item", TEST_VALUE); + let win = window.open("file_storage_copied.html", "my_window"); + let data = await waitForMessage(win); + is(data, TEST_VALUE, "Should have cloned the test value"); + win.close(); + }); + </script> +</body> +</html> |