summaryrefslogtreecommitdiffstats
path: root/toolkit/components/remote
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/remote
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--toolkit/components/remote/RemoteUtils.cpp102
-rw-r--r--toolkit/components/remote/RemoteUtils.h31
-rw-r--r--toolkit/components/remote/WinRemoteMessage.cpp124
-rw-r--r--toolkit/components/remote/WinRemoteMessage.h69
-rw-r--r--toolkit/components/remote/moz.build53
-rw-r--r--toolkit/components/remote/nsDBusRemoteClient.cpp188
-rw-r--r--toolkit/components/remote/nsDBusRemoteClient.h34
-rw-r--r--toolkit/components/remote/nsDBusRemoteServer.cpp214
-rw-r--r--toolkit/components/remote/nsDBusRemoteServer.h38
-rw-r--r--toolkit/components/remote/nsGTKRemoteServer.cpp69
-rw-r--r--toolkit/components/remote/nsGTKRemoteServer.h37
-rw-r--r--toolkit/components/remote/nsMacRemoteClient.h27
-rw-r--r--toolkit/components/remote/nsMacRemoteClient.mm62
-rw-r--r--toolkit/components/remote/nsMacRemoteServer.h30
-rw-r--r--toolkit/components/remote/nsMacRemoteServer.mm134
-rw-r--r--toolkit/components/remote/nsRemoteClient.h64
-rw-r--r--toolkit/components/remote/nsRemoteServer.h21
-rw-r--r--toolkit/components/remote/nsRemoteService.cpp192
-rw-r--r--toolkit/components/remote/nsRemoteService.h49
-rw-r--r--toolkit/components/remote/nsUnixRemoteServer.cpp113
-rw-r--r--toolkit/components/remote/nsUnixRemoteServer.h28
-rw-r--r--toolkit/components/remote/nsWinRemoteClient.cpp44
-rw-r--r--toolkit/components/remote/nsWinRemoteClient.h25
-rw-r--r--toolkit/components/remote/nsWinRemoteServer.cpp104
-rw-r--r--toolkit/components/remote/nsWinRemoteServer.h27
-rw-r--r--toolkit/components/remote/nsXRemoteClient.cpp654
-rw-r--r--toolkit/components/remote/nsXRemoteClient.h49
-rw-r--r--toolkit/components/remote/nsXRemoteServer.cpp161
-rw-r--r--toolkit/components/remote/nsXRemoteServer.h44
-rw-r--r--toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm178
-rw-r--r--toolkit/components/remotebrowserutils/moz.build14
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs6
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser.ini16
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js220
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js284
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js87
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js414
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js119
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js161
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs58
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/dummy_page.html13
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html5
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/head.js17
-rw-r--r--toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs28
-rw-r--r--toolkit/components/remotepagemanager/MessagePort.jsm280
-rw-r--r--toolkit/components/remotepagemanager/RemotePageManagerChild.jsm86
-rw-r--r--toolkit/components/remotepagemanager/RemotePageManagerParent.jsm350
-rw-r--r--toolkit/components/remotepagemanager/moz.build16
-rw-r--r--toolkit/components/remotepagemanager/tests/browser/browser.ini7
-rw-r--r--toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js570
-rw-r--r--toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html68
-rw-r--r--toolkit/components/remotepagemanager/tests/browser/testremotepagemanager2.html19
52 files changed, 5803 insertions, 0 deletions
diff --git a/toolkit/components/remote/RemoteUtils.cpp b/toolkit/components/remote/RemoteUtils.cpp
new file mode 100644
index 0000000000..f7677f92b9
--- /dev/null
+++ b/toolkit/components/remote/RemoteUtils.cpp
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* vim:set ts=8 sw=2 et cindent: */
+/* 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 <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <limits.h>
+
+#include "RemoteUtils.h"
+
+#ifdef IS_BIG_ENDIAN
+# define TO_LITTLE_ENDIAN32(x) \
+ ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \
+ (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24))
+#else
+# define TO_LITTLE_ENDIAN32(x) (x)
+#endif
+
+#ifndef MAX_PATH
+# ifdef PATH_MAX
+# define MAX_PATH PATH_MAX
+# else
+# define MAX_PATH 1024
+# endif
+#endif
+
+/* like strcpy, but return the char after the final null */
+static char* estrcpy(const char* s, char* d) {
+ while (*s) *d++ = *s++;
+
+ *d++ = '\0';
+ return d;
+}
+
+/* Construct a command line from given args and desktop startup ID.
+ * Returned buffer must be released by free().
+ */
+char* ConstructCommandLine(int32_t argc, char** argv, const char* aStartupToken,
+ int* aCommandLineLength) {
+ char cwdbuf[MAX_PATH];
+ if (!getcwd(cwdbuf, MAX_PATH)) return nullptr;
+
+ // the commandline property is constructed as an array of int32_t
+ // followed by a series of null-terminated strings:
+ //
+ // [argc][offsetargv0][offsetargv1...]<workingdir>\0<argv[0]>\0argv[1]...\0
+ // (offset is from the beginning of the buffer)
+
+ static char startupTokenPrefix[] = " STARTUP_TOKEN=";
+
+ int32_t argvlen = strlen(cwdbuf);
+ for (int i = 0; i < argc; ++i) {
+ int32_t len = strlen(argv[i]);
+ if (i == 0 && aStartupToken) {
+ len += sizeof(startupTokenPrefix) - 1 + strlen(aStartupToken);
+ }
+ argvlen += len;
+ }
+
+ auto* buffer =
+ (int32_t*)malloc(argvlen + argc + 1 + sizeof(int32_t) * (argc + 1));
+ if (!buffer) return nullptr;
+
+ buffer[0] = TO_LITTLE_ENDIAN32(argc);
+
+ auto* bufend = (char*)(buffer + argc + 1);
+
+ bufend = estrcpy(cwdbuf, bufend);
+
+ for (int i = 0; i < argc; ++i) {
+ buffer[i + 1] = TO_LITTLE_ENDIAN32(bufend - ((char*)buffer));
+ bufend = estrcpy(argv[i], bufend);
+ if (i == 0 && aStartupToken) {
+ bufend = estrcpy(startupTokenPrefix, bufend - 1);
+ bufend = estrcpy(aStartupToken, bufend - 1);
+ }
+ }
+
+#ifdef DEBUG_command_line
+ int32_t debug_argc = TO_LITTLE_ENDIAN32(*buffer);
+ char* debug_workingdir = (char*)(buffer + argc + 1);
+
+ printf(
+ "Sending command line:\n"
+ " working dir: %s\n"
+ " argc:\t%i",
+ debug_workingdir, debug_argc);
+
+ int32_t* debug_offset = buffer + 1;
+ for (int debug_i = 0; debug_i < debug_argc; ++debug_i)
+ printf(" argv[%i]:\t%s\n", debug_i,
+ ((char*)buffer) + TO_LITTLE_ENDIAN32(debug_offset[debug_i]));
+#endif
+
+ *aCommandLineLength = bufend - reinterpret_cast<char*>(buffer);
+ return reinterpret_cast<char*>(buffer);
+}
diff --git a/toolkit/components/remote/RemoteUtils.h b/toolkit/components/remote/RemoteUtils.h
new file mode 100644
index 0000000000..2d21293eb5
--- /dev/null
+++ b/toolkit/components/remote/RemoteUtils.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_
+#define TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_
+
+#include "nsString.h"
+#if defined XP_WIN
+# include "WinUtils.h"
+#endif
+
+#if defined XP_WIN || defined XP_MACOSX
+static void BuildClassName(const char* aProgram, const char* aProfile,
+ nsString& aClassName) {
+ aClassName.AppendPrintf("Mozilla_%s", aProgram);
+# if defined XP_WIN
+ nsString pfn = mozilla::widget::WinUtils::GetPackageFamilyName();
+ if (!pfn.IsEmpty()) {
+ aClassName.AppendPrintf("_%S", static_cast<const wchar_t*>(pfn.get()));
+ }
+# endif
+ aClassName.AppendPrintf("_%s_RemoteWindow", aProfile);
+}
+#endif
+
+char* ConstructCommandLine(int32_t argc, char** argv, const char* aStartupToken,
+ int* aCommandLineLength);
+
+#endif // TOOLKIT_COMPONENTS_REMOTE_REMOTEUTILS_H_
diff --git a/toolkit/components/remote/WinRemoteMessage.cpp b/toolkit/components/remote/WinRemoteMessage.cpp
new file mode 100644
index 0000000000..98f425ba1a
--- /dev/null
+++ b/toolkit/components/remote/WinRemoteMessage.cpp
@@ -0,0 +1,124 @@
+/* -*- 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/. */
+
+#include "nsCommandLine.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "WinRemoteMessage.h"
+
+using namespace mozilla;
+
+WinRemoteMessageSender::WinRemoteMessageSender(const char* aCommandLine)
+ : mData({static_cast<DWORD>(WinRemoteMessageVersion::CommandLineOnly)}),
+ mUtf8Buffer(aCommandLine) {
+ mUtf8Buffer.Append('\0');
+
+ char* mutableBuffer;
+ mData.cbData = mUtf8Buffer.GetMutableData(&mutableBuffer);
+ mData.lpData = mutableBuffer;
+}
+
+WinRemoteMessageSender::WinRemoteMessageSender(const char* aCommandLine,
+ const char* aWorkingDir)
+ : mData({static_cast<DWORD>(
+ WinRemoteMessageVersion::CommandLineAndWorkingDir)}),
+ mUtf8Buffer(aCommandLine) {
+ mUtf8Buffer.Append('\0');
+ mUtf8Buffer.Append(aWorkingDir);
+ mUtf8Buffer.Append('\0');
+
+ char* mutableBuffer;
+ mData.cbData = mUtf8Buffer.GetMutableData(&mutableBuffer);
+ mData.lpData = mutableBuffer;
+}
+
+WinRemoteMessageSender::WinRemoteMessageSender(const wchar_t* aCommandLine,
+ const wchar_t* aWorkingDir)
+ : mData({static_cast<DWORD>(
+ WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16)}),
+ mUtf16Buffer(aCommandLine) {
+ mUtf16Buffer.Append(u'\0');
+ mUtf16Buffer.Append(aWorkingDir);
+ mUtf16Buffer.Append(u'\0');
+
+ char16_t* mutableBuffer;
+ mData.cbData = mUtf16Buffer.GetMutableData(&mutableBuffer) * sizeof(char16_t);
+ mData.lpData = mutableBuffer;
+}
+
+COPYDATASTRUCT* WinRemoteMessageSender::CopyData() { return &mData; }
+
+nsresult WinRemoteMessageReceiver::ParseV0(const nsACString& aBuffer) {
+ CommandLineParserWin<char> parser;
+ parser.HandleCommandLine(aBuffer);
+
+ mCommandLine = new nsCommandLine();
+ return mCommandLine->Init(parser.Argc(), parser.Argv(), nullptr,
+ nsICommandLine::STATE_REMOTE_AUTO);
+}
+
+nsresult WinRemoteMessageReceiver::ParseV1(const nsACString& aBuffer) {
+ CommandLineParserWin<char> parser;
+ size_t cch = parser.HandleCommandLine(aBuffer);
+ ++cch; // skip a null char
+
+ nsCOMPtr<nsIFile> workingDir;
+ if (cch < aBuffer.Length()) {
+ NS_NewLocalFile(NS_ConvertUTF8toUTF16(Substring(aBuffer, cch)), false,
+ getter_AddRefs(workingDir));
+ }
+
+ mCommandLine = new nsCommandLine();
+ return mCommandLine->Init(parser.Argc(), parser.Argv(), workingDir,
+ nsICommandLine::STATE_REMOTE_AUTO);
+}
+
+nsresult WinRemoteMessageReceiver::ParseV2(const nsAString& aBuffer) {
+ CommandLineParserWin<char16_t> parser;
+ size_t cch = parser.HandleCommandLine(aBuffer);
+ ++cch; // skip a null char
+
+ nsCOMPtr<nsIFile> workingDir;
+ if (cch < aBuffer.Length()) {
+ NS_NewLocalFile(Substring(aBuffer, cch), false, getter_AddRefs(workingDir));
+ }
+
+ int argc = parser.Argc();
+ Vector<nsAutoCString> utf8args;
+ if (!utf8args.reserve(argc)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ UniquePtr<const char*[]> argv(new const char*[argc]);
+ for (int i = 0; i < argc; ++i) {
+ utf8args.infallibleAppend(NS_ConvertUTF16toUTF8(parser.Argv()[i]));
+ argv[i] = utf8args[i].get();
+ }
+
+ mCommandLine = new nsCommandLine();
+ return mCommandLine->Init(argc, argv.get(), workingDir,
+ nsICommandLine::STATE_REMOTE_AUTO);
+}
+
+nsresult WinRemoteMessageReceiver::Parse(const COPYDATASTRUCT* aMessageData) {
+ switch (static_cast<WinRemoteMessageVersion>(aMessageData->dwData)) {
+ case WinRemoteMessageVersion::CommandLineOnly:
+ return ParseV0(nsDependentCSubstring(
+ reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData));
+ case WinRemoteMessageVersion::CommandLineAndWorkingDir:
+ return ParseV1(nsDependentCSubstring(
+ reinterpret_cast<char*>(aMessageData->lpData), aMessageData->cbData));
+ case WinRemoteMessageVersion::CommandLineAndWorkingDirInUtf16:
+ return ParseV2(nsDependentSubstring(
+ reinterpret_cast<char16_t*>(aMessageData->lpData),
+ aMessageData->cbData / sizeof(char16_t)));
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported message version");
+ return NS_ERROR_FAILURE;
+ }
+}
+
+nsICommandLineRunner* WinRemoteMessageReceiver::CommandLineRunner() {
+ return mCommandLine;
+}
diff --git a/toolkit/components/remote/WinRemoteMessage.h b/toolkit/components/remote/WinRemoteMessage.h
new file mode 100644
index 0000000000..948c6db310
--- /dev/null
+++ b/toolkit/components/remote/WinRemoteMessage.h
@@ -0,0 +1,69 @@
+/* -*- 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/. */
+
+#ifndef __WinRemoteMessage_h__
+#define __WinRemoteMessage_h__
+
+#include <windows.h>
+
+#include "nsICommandLineRunner.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+// This version defines the format of COPYDATASTRUCT::lpData in a message of
+// WM_COPYDATA.
+// Always use the latest version for production use because the older versions
+// have a bug that a non-ascii character in a utf-8 message cannot be parsed
+// correctly (bug 1650637). We keep the older versions for backward
+// compatibility and the testing purpose only.
+enum class WinRemoteMessageVersion : uint32_t {
+ // "<CommandLine>\0" in utf8
+ CommandLineOnly = 0,
+ // "<CommandLine>\0<WorkingDir>\0" in utf8
+ CommandLineAndWorkingDir,
+ // L"<CommandLine>\0<WorkingDir>\0" in utf16
+ CommandLineAndWorkingDirInUtf16,
+};
+
+class WinRemoteMessageSender final {
+ COPYDATASTRUCT mData;
+ nsAutoString mUtf16Buffer;
+ nsAutoCString mUtf8Buffer;
+
+ public:
+ WinRemoteMessageSender(const wchar_t* aCommandLine,
+ const wchar_t* aWorkingDir);
+
+ WinRemoteMessageSender(const WinRemoteMessageSender&) = delete;
+ WinRemoteMessageSender(WinRemoteMessageSender&&) = delete;
+ WinRemoteMessageSender& operator=(const WinRemoteMessageSender&) = delete;
+ WinRemoteMessageSender& operator=(WinRemoteMessageSender&&) = delete;
+
+ COPYDATASTRUCT* CopyData();
+
+ // Constructors for the old formats. Testing purpose only.
+ explicit WinRemoteMessageSender(const char* aCommandLine);
+ WinRemoteMessageSender(const char* aCommandLine, const char* aWorkingDir);
+};
+
+class WinRemoteMessageReceiver final {
+ nsCOMPtr<nsICommandLineRunner> mCommandLine;
+
+ nsresult ParseV0(const nsACString& aBuffer);
+ nsresult ParseV1(const nsACString& aBuffer);
+ nsresult ParseV2(const nsAString& aBuffer);
+
+ public:
+ WinRemoteMessageReceiver() = default;
+ WinRemoteMessageReceiver(const WinRemoteMessageReceiver&) = delete;
+ WinRemoteMessageReceiver(WinRemoteMessageReceiver&&) = delete;
+ WinRemoteMessageReceiver& operator=(const WinRemoteMessageReceiver&) = delete;
+ WinRemoteMessageReceiver& operator=(WinRemoteMessageReceiver&&) = delete;
+
+ nsresult Parse(const COPYDATASTRUCT* aMessageData);
+ nsICommandLineRunner* CommandLineRunner();
+};
+
+#endif // __WinRemoteMessage_h__
diff --git a/toolkit/components/remote/moz.build b/toolkit/components/remote/moz.build
new file mode 100644
index 0000000000..d3bab6cf9a
--- /dev/null
+++ b/toolkit/components/remote/moz.build
@@ -0,0 +1,53 @@
+# -*- 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 = ("Toolkit", "Startup and Profile System")
+
+SOURCES += [
+ "nsRemoteService.cpp",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ SOURCES += [
+ "nsUnixRemoteServer.cpp",
+ "RemoteUtils.cpp",
+ ]
+ if CONFIG["MOZ_ENABLE_DBUS"]:
+ SOURCES += [
+ "nsDBusRemoteClient.cpp",
+ "nsDBusRemoteServer.cpp",
+ ]
+ CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]
+ EXPORTS += [
+ "nsUnixRemoteServer.h",
+ "RemoteUtils.h",
+ ]
+ else:
+ SOURCES += [
+ "nsGTKRemoteServer.cpp",
+ "nsXRemoteClient.cpp",
+ "nsXRemoteServer.cpp",
+ ]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ SOURCES += [
+ "nsWinRemoteClient.cpp",
+ "nsWinRemoteServer.cpp",
+ "WinRemoteMessage.cpp",
+ ]
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += [
+ "nsMacRemoteClient.mm",
+ "nsMacRemoteServer.mm",
+ ]
+
+LOCAL_INCLUDES += [
+ "../../profile",
+ "../../xre",
+]
+FINAL_LIBRARY = "xul"
diff --git a/toolkit/components/remote/nsDBusRemoteClient.cpp b/toolkit/components/remote/nsDBusRemoteClient.cpp
new file mode 100644
index 0000000000..69e8d07c72
--- /dev/null
+++ b/toolkit/components/remote/nsDBusRemoteClient.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* vim:set ts=8 sw=2 et cindent: */
+/* 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 "nsDBusRemoteClient.h"
+#include "RemoteUtils.h"
+#include "mozilla/XREAppData.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Base64.h"
+#include "nsPrintfCString.h"
+
+#include <dlfcn.h>
+#include <dbus/dbus-glib-lowlevel.h>
+
+#undef LOG
+#ifdef MOZ_LOGGING
+static mozilla::LazyLogModule sRemoteLm("nsDBusRemoteClient");
+# define LOG(str, ...) \
+ MOZ_LOG(sRemoteLm, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
+#else
+# define LOG(...)
+#endif
+
+nsDBusRemoteClient::nsDBusRemoteClient() {
+ mConnection = nullptr;
+ LOG("nsDBusRemoteClient::nsDBusRemoteClient");
+}
+
+nsDBusRemoteClient::~nsDBusRemoteClient() {
+ LOG("nsDBusRemoteClient::~nsDBusRemoteClient");
+ Shutdown();
+}
+
+nsresult nsDBusRemoteClient::Init() {
+ LOG("nsDBusRemoteClient::Init");
+
+ if (mConnection) return NS_OK;
+
+ mConnection =
+ already_AddRefed<DBusConnection>(dbus_bus_get(DBUS_BUS_SESSION, nullptr));
+ if (!mConnection) {
+ LOG(" failed to get DBus session");
+ return NS_ERROR_FAILURE;
+ }
+
+ dbus_connection_set_exit_on_disconnect(mConnection, false);
+ dbus_connection_setup_with_g_main(mConnection, nullptr);
+
+ return NS_OK;
+}
+
+void nsDBusRemoteClient::Shutdown(void) {
+ LOG("nsDBusRemoteClient::Shutdown");
+ // This connection is owned by libdbus and we don't need to close it
+ mConnection = nullptr;
+}
+
+nsresult nsDBusRemoteClient::SendCommandLine(
+ const char* aProgram, const char* aProfile, int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse, bool* aWindowFound) {
+ NS_ENSURE_TRUE(aProgram, NS_ERROR_INVALID_ARG);
+
+ LOG("nsDBusRemoteClient::SendCommandLine");
+
+ int commandLineLength;
+ char* commandLine =
+ ConstructCommandLine(argc, argv, aStartupToken, &commandLineLength);
+ if (!commandLine) {
+ LOG(" failed to create command line");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv =
+ DoSendDBusCommandLine(aProgram, aProfile, commandLine, commandLineLength);
+ free(commandLine);
+
+ *aWindowFound = NS_SUCCEEDED(rv);
+
+ LOG("DoSendDBusCommandLine %s", NS_SUCCEEDED(rv) ? "OK" : "FAILED");
+ return rv;
+}
+
+bool nsDBusRemoteClient::GetRemoteDestinationName(const char* aProgram,
+ const char* aProfile,
+ nsCString& aDestinationName) {
+ // We have a profile name - just create the destination.
+ // D-Bus names can contain only [a-z][A-Z][0-9]_
+ // characters so adjust the profile string properly.
+ nsAutoCString profileName;
+ nsresult rv = mozilla::Base64Encode(nsAutoCString(aProfile), profileName);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ mozilla::XREAppData::SanitizeNameForDBus(profileName);
+
+ aDestinationName =
+ nsPrintfCString("org.mozilla.%s.%s", aProgram, profileName.get());
+ if (aDestinationName.Length() > DBUS_MAXIMUM_NAME_LENGTH)
+ aDestinationName.Truncate(DBUS_MAXIMUM_NAME_LENGTH);
+
+ static auto sDBusValidateBusName = (bool (*)(const char*, DBusError*))dlsym(
+ RTLD_DEFAULT, "dbus_validate_bus_name");
+ if (!sDBusValidateBusName) {
+ LOG(" failed to get dbus_validate_bus_name()");
+ return false;
+ }
+
+ if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) {
+ // We don't have a valid busName yet - try to create a default one.
+ aDestinationName =
+ nsPrintfCString("org.mozilla.%s.%s", aProgram, "default");
+ if (!sDBusValidateBusName(aDestinationName.get(), nullptr)) {
+ // We failed completelly to get a valid bus name - just quit
+ // to prevent crash at dbus_bus_request_name().
+ LOG(" failed to validate profile DBus name");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult nsDBusRemoteClient::DoSendDBusCommandLine(const char* aProgram,
+ const char* aProfile,
+ const char* aBuffer,
+ int aLength) {
+ LOG("nsDBusRemoteClient::DoSendDBusCommandLine()");
+
+ nsAutoCString appName(aProgram);
+ mozilla::XREAppData::SanitizeNameForDBus(appName);
+
+ nsAutoCString destinationName;
+ if (!GetRemoteDestinationName(appName.get(), aProfile, destinationName)) {
+ LOG(" failed to get remote destination name");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString pathName;
+ pathName = nsPrintfCString("/org/mozilla/%s/Remote", appName.get());
+
+ static auto sDBusValidatePathName = (bool (*)(const char*, DBusError*))dlsym(
+ RTLD_DEFAULT, "dbus_validate_path");
+ if (!sDBusValidatePathName ||
+ !sDBusValidatePathName(pathName.get(), nullptr)) {
+ LOG(" failed to validate path name");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString remoteInterfaceName;
+ remoteInterfaceName = nsPrintfCString("org.mozilla.%s", appName.get());
+
+ LOG(" DBus destination: %s\n", destinationName.get());
+ LOG(" DBus path: %s\n", pathName.get());
+ LOG(" DBus interface: %s\n", remoteInterfaceName.get());
+
+ RefPtr<DBusMessage> msg =
+ already_AddRefed<DBusMessage>(dbus_message_new_method_call(
+ destinationName.get(),
+ pathName.get(), // object to call on
+ remoteInterfaceName.get(), // interface to call on
+ "OpenURL")); // method name
+ if (!msg) {
+ LOG(" failed to create DBus message");
+ return NS_ERROR_FAILURE;
+ }
+
+ // append arguments
+ if (!dbus_message_append_args(msg, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE, &aBuffer,
+ aLength, DBUS_TYPE_INVALID)) {
+ LOG(" failed to create DBus message");
+ return NS_ERROR_FAILURE;
+ }
+
+ // send message and get a handle for a reply
+ RefPtr<DBusMessage> reply = already_AddRefed<DBusMessage>(
+ dbus_connection_send_with_reply_and_block(mConnection, msg, -1, nullptr));
+
+#ifdef MOZ_LOGGING
+ if (!reply) {
+ LOG(" failed to get DBus reply");
+ }
+#endif
+
+ return reply ? NS_OK : NS_ERROR_FAILURE;
+}
diff --git a/toolkit/components/remote/nsDBusRemoteClient.h b/toolkit/components/remote/nsDBusRemoteClient.h
new file mode 100644
index 0000000000..fdb4dbc665
--- /dev/null
+++ b/toolkit/components/remote/nsDBusRemoteClient.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#ifndef DBusRemoteClient_h__
+#define DBusRemoteClient_h__
+
+#include "nsRemoteClient.h"
+#include "mozilla/DBusHelpers.h"
+#include "mozilla/RefPtr.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+
+class nsDBusRemoteClient : public nsRemoteClient {
+ public:
+ nsDBusRemoteClient();
+ ~nsDBusRemoteClient();
+
+ nsresult Init() override;
+ nsresult SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv, const char* aStartupToken,
+ char** aResponse, bool* aSucceeded) override;
+ void Shutdown();
+
+ private:
+ bool GetRemoteDestinationName(const char* aProgram, const char* aProfile,
+ nsCString& aDestinationName);
+ nsresult DoSendDBusCommandLine(const char* aProgram, const char* aProfile,
+ const char* aBuffer, int aLength);
+ RefPtr<DBusConnection> mConnection;
+};
+
+#endif // DBusRemoteClient_h__
diff --git a/toolkit/components/remote/nsDBusRemoteServer.cpp b/toolkit/components/remote/nsDBusRemoteServer.cpp
new file mode 100644
index 0000000000..34167177d9
--- /dev/null
+++ b/toolkit/components/remote/nsDBusRemoteServer.cpp
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#include "nsDBusRemoteServer.h"
+
+#include "nsCOMPtr.h"
+#include "mozilla/XREAppData.h"
+#include "mozilla/Base64.h"
+#include "mozilla/ScopeExit.h"
+#include "nsPrintfCString.h"
+
+#include "nsGTKToolkit.h"
+
+#include <dbus/dbus.h>
+#include <dbus/dbus-glib-lowlevel.h>
+
+#include <dlfcn.h>
+
+static const char* introspect_template =
+ "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection "
+ "1.0//EN\"\n"
+ "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
+ "<node>\n"
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
+ " <method name=\"Introspect\">\n"
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n"
+ " </method>\n"
+ " </interface>\n"
+ " <interface name=\"org.mozilla.%s\">\n"
+ " <method name=\"OpenURL\">\n"
+ " <arg name=\"url\" direction=\"in\" type=\"ay\"/>\n"
+ " </method>\n"
+ " </interface>\n"
+ "</node>\n";
+
+DBusHandlerResult nsDBusRemoteServer::Introspect(DBusMessage* msg) {
+ DBusMessage* reply = dbus_message_new_method_return(msg);
+ if (!reply) return DBUS_HANDLER_RESULT_NEED_MEMORY;
+
+ nsAutoCString introspect_xml;
+ introspect_xml = nsPrintfCString(introspect_template, mAppName.get());
+
+ const char* message = introspect_xml.get();
+ dbus_message_append_args(reply, DBUS_TYPE_STRING, &message,
+ DBUS_TYPE_INVALID);
+
+ dbus_connection_send(mConnection, reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+DBusHandlerResult nsDBusRemoteServer::OpenURL(DBusMessage* msg) {
+ DBusMessage* reply = nullptr;
+ const char* commandLine;
+ int length;
+
+ if (!dbus_message_get_args(msg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_BYTE,
+ &commandLine, &length, DBUS_TYPE_INVALID) ||
+ length == 0) {
+ nsAutoCString errorMsg;
+ errorMsg = nsPrintfCString("org.mozilla.%s.Error", mAppName.get());
+ reply = dbus_message_new_error(msg, errorMsg.get(), "Wrong argument");
+ } else {
+ guint32 timestamp = gtk_get_current_event_time();
+ if (timestamp == GDK_CURRENT_TIME) {
+ timestamp = guint32(g_get_monotonic_time() / 1000);
+ }
+ HandleCommandLine(commandLine, timestamp);
+ reply = dbus_message_new_method_return(msg);
+ }
+
+ dbus_connection_send(mConnection, reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+DBusHandlerResult nsDBusRemoteServer::HandleDBusMessage(
+ DBusConnection* aConnection, DBusMessage* msg) {
+ NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection.");
+
+ const char* method = dbus_message_get_member(msg);
+ const char* iface = dbus_message_get_interface(msg);
+
+ if ((strcmp("Introspect", method) == 0) &&
+ (strcmp("org.freedesktop.DBus.Introspectable", iface) == 0)) {
+ return Introspect(msg);
+ }
+
+ nsAutoCString ourInterfaceName;
+ ourInterfaceName = nsPrintfCString("org.mozilla.%s", mAppName.get());
+
+ if ((strcmp("OpenURL", method) == 0) &&
+ (strcmp(ourInterfaceName.get(), iface) == 0)) {
+ return OpenURL(msg);
+ }
+
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+void nsDBusRemoteServer::UnregisterDBusInterface(DBusConnection* aConnection) {
+ NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection.");
+ // Not implemented
+}
+
+static DBusHandlerResult message_handler(DBusConnection* conn, DBusMessage* msg,
+ void* user_data) {
+ auto interface = static_cast<nsDBusRemoteServer*>(user_data);
+ return interface->HandleDBusMessage(conn, msg);
+}
+
+static void unregister(DBusConnection* conn, void* user_data) {
+ auto interface = static_cast<nsDBusRemoteServer*>(user_data);
+ interface->UnregisterDBusInterface(conn);
+}
+
+static DBusObjectPathVTable remoteHandlersTable = {
+ .unregister_function = unregister,
+ .message_function = message_handler,
+};
+
+nsresult nsDBusRemoteServer::Startup(const char* aAppName,
+ const char* aProfileName) {
+ if (mConnection && dbus_connection_get_is_connected(mConnection)) {
+ // We're already connected so we don't need to reconnect
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ // Don't even try to start without any application/profile name
+ if (!aAppName || aAppName[0] == '\0' || !aProfileName ||
+ aProfileName[0] == '\0')
+ return NS_ERROR_INVALID_ARG;
+
+ mConnection = dont_AddRef(dbus_bus_get(DBUS_BUS_SESSION, nullptr));
+ if (!mConnection) {
+ return NS_ERROR_FAILURE;
+ }
+ auto releaseDBusConnection =
+ mozilla::MakeScopeExit([&] { mConnection = nullptr; });
+ dbus_connection_set_exit_on_disconnect(mConnection, false);
+ dbus_connection_setup_with_g_main(mConnection, nullptr);
+
+ mAppName = aAppName;
+ mozilla::XREAppData::SanitizeNameForDBus(mAppName);
+
+ nsAutoCString profileName;
+ MOZ_TRY(
+ mozilla::Base64Encode(aProfileName, strlen(aProfileName), profileName));
+
+ mozilla::XREAppData::SanitizeNameForDBus(profileName);
+
+ nsPrintfCString busName("org.mozilla.%s.%s", mAppName.get(),
+ profileName.get());
+ if (busName.Length() > DBUS_MAXIMUM_NAME_LENGTH)
+ busName.Truncate(DBUS_MAXIMUM_NAME_LENGTH);
+
+ static auto sDBusValidateBusName = (bool (*)(const char*, DBusError*))dlsym(
+ RTLD_DEFAULT, "dbus_validate_bus_name");
+ if (!sDBusValidateBusName) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We don't have a valid busName yet - try to create a default one.
+ if (!sDBusValidateBusName(busName.get(), nullptr)) {
+ busName = nsPrintfCString("org.mozilla.%s.%s", mAppName.get(), "default");
+ if (!sDBusValidateBusName(busName.get(), nullptr)) {
+ // We failed completelly to get a valid bus name - just quit
+ // to prevent crash at dbus_bus_request_name().
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ DBusError err;
+ dbus_error_init(&err);
+ dbus_bus_request_name(mConnection, busName.get(), DBUS_NAME_FLAG_DO_NOT_QUEUE,
+ &err);
+ // The interface is already owned - there is another application/profile
+ // instance already running.
+ if (dbus_error_is_set(&err)) {
+ dbus_error_free(&err);
+ return NS_ERROR_FAILURE;
+ }
+
+ mPathName = nsPrintfCString("/org/mozilla/%s/Remote", mAppName.get());
+ static auto sDBusValidatePathName = (bool (*)(const char*, DBusError*))dlsym(
+ RTLD_DEFAULT, "dbus_validate_path");
+ if (!sDBusValidatePathName ||
+ !sDBusValidatePathName(mPathName.get(), nullptr)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!dbus_connection_register_object_path(mConnection, mPathName.get(),
+ &remoteHandlersTable, this)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ releaseDBusConnection.release();
+ return NS_OK;
+}
+
+void nsDBusRemoteServer::Shutdown() {
+ if (!mConnection) {
+ return;
+ }
+
+ dbus_connection_unregister_object_path(mConnection, mPathName.get());
+
+ // dbus_connection_unref() will be called by RefPtr here.
+ mConnection = nullptr;
+}
diff --git a/toolkit/components/remote/nsDBusRemoteServer.h b/toolkit/components/remote/nsDBusRemoteServer.h
new file mode 100644
index 0000000000..ce23a28c36
--- /dev/null
+++ b/toolkit/components/remote/nsDBusRemoteServer.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsDBusRemoteServer_h__
+#define __nsDBusRemoteServer_h__
+
+#include "nsRemoteServer.h"
+#include "nsUnixRemoteServer.h"
+#include "mozilla/DBusHelpers.h"
+
+class nsDBusRemoteServer final : public nsRemoteServer,
+ public nsUnixRemoteServer {
+ public:
+ nsDBusRemoteServer() : mConnection(nullptr), mAppName(nullptr) {}
+ ~nsDBusRemoteServer() override { Shutdown(); }
+
+ nsresult Startup(const char* aAppName, const char* aProfileName) override;
+ void Shutdown() override;
+
+ DBusHandlerResult HandleDBusMessage(DBusConnection* aConnection,
+ DBusMessage* msg);
+ void UnregisterDBusInterface(DBusConnection* aConnection);
+
+ private:
+ DBusHandlerResult OpenURL(DBusMessage* msg);
+ DBusHandlerResult Introspect(DBusMessage* msg);
+
+ // The connection is owned by DBus library
+ RefPtr<DBusConnection> mConnection;
+ nsCString mAppName;
+ nsCString mPathName;
+};
+
+#endif // __nsDBusRemoteServer_h__
diff --git a/toolkit/components/remote/nsGTKRemoteServer.cpp b/toolkit/components/remote/nsGTKRemoteServer.cpp
new file mode 100644
index 0000000000..5b6eda3f89
--- /dev/null
+++ b/toolkit/components/remote/nsGTKRemoteServer.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* 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 "nsGTKRemoteServer.h"
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+
+#include "mozilla/ModuleUtils.h"
+#include "nsAppShellCID.h"
+
+#include "nsCOMPtr.h"
+
+#include "nsGTKToolkit.h"
+
+nsresult nsGTKRemoteServer::Startup(const char* aAppName,
+ const char* aProfileName) {
+ NS_ASSERTION(aAppName, "Don't pass a null appname!");
+
+ if (mServerWindow) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ XRemoteBaseStartup(aAppName, aProfileName);
+
+ mServerWindow = gtk_invisible_new();
+ gtk_widget_realize(mServerWindow);
+ HandleCommandsFor(mServerWindow);
+
+ return NS_OK;
+}
+
+void nsGTKRemoteServer::Shutdown() {
+ if (!mServerWindow) {
+ return;
+ }
+
+ gtk_widget_destroy(mServerWindow);
+ mServerWindow = nullptr;
+}
+
+void nsGTKRemoteServer::HandleCommandsFor(GtkWidget* widget) {
+ g_signal_connect(G_OBJECT(widget), "property_notify_event",
+ G_CALLBACK(HandlePropertyChange), this);
+
+ gtk_widget_add_events(widget, GDK_PROPERTY_CHANGE_MASK);
+
+ Window window = gdk_x11_window_get_xid(gtk_widget_get_window(widget));
+ nsXRemoteServer::HandleCommandsFor(window);
+}
+
+gboolean nsGTKRemoteServer::HandlePropertyChange(GtkWidget* aWidget,
+ GdkEventProperty* pevent,
+ nsGTKRemoteServer* aThis) {
+ if (pevent->state == GDK_PROPERTY_NEW_VALUE) {
+ Atom changedAtom = gdk_x11_atom_to_xatom(pevent->atom);
+
+ XID window = gdk_x11_window_get_xid(gtk_widget_get_window(aWidget));
+ return aThis->HandleNewProperty(
+ window, GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), pevent->time,
+ changedAtom);
+ }
+ return FALSE;
+}
diff --git a/toolkit/components/remote/nsGTKRemoteServer.h b/toolkit/components/remote/nsGTKRemoteServer.h
new file mode 100644
index 0000000000..767f2f54ad
--- /dev/null
+++ b/toolkit/components/remote/nsGTKRemoteServer.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsGTKRemoteServer_h__
+#define __nsGTKRemoteServer_h__
+
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gtk/gtk.h>
+
+#include "nsRemoteServer.h"
+#include "nsXRemoteServer.h"
+#include "mozilla/Attributes.h"
+
+class nsGTKRemoteServer final : public nsXRemoteServer {
+ public:
+ nsGTKRemoteServer() : mServerWindow(nullptr) {}
+ ~nsGTKRemoteServer() override { Shutdown(); }
+
+ nsresult Startup(const char* aAppName, const char* aProfileName) override;
+ void Shutdown() override;
+
+ static gboolean HandlePropertyChange(GtkWidget* widget,
+ GdkEventProperty* event,
+ nsGTKRemoteServer* aData);
+
+ private:
+ void HandleCommandsFor(GtkWidget* aWidget);
+
+ GtkWidget* mServerWindow;
+};
+
+#endif // __nsGTKRemoteService_h__
diff --git a/toolkit/components/remote/nsMacRemoteClient.h b/toolkit/components/remote/nsMacRemoteClient.h
new file mode 100644
index 0000000000..96ad4e2a7f
--- /dev/null
+++ b/toolkit/components/remote/nsMacRemoteClient.h
@@ -0,0 +1,27 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=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/. */
+
+#ifndef TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_
+#define TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_
+
+#import <CoreFoundation/CoreFoundation.h>
+
+#include "nscore.h"
+#include "nsRemoteClient.h"
+
+class nsMacRemoteClient : public nsRemoteClient {
+ public:
+ virtual ~nsMacRemoteClient() = default;
+
+ nsresult Init() override;
+
+ nsresult SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv, const char* aStartupToken,
+ char** aResponse, bool* aSucceeded) override;
+};
+
+#endif // TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTECLIENT_H_
diff --git a/toolkit/components/remote/nsMacRemoteClient.mm b/toolkit/components/remote/nsMacRemoteClient.mm
new file mode 100644
index 0000000000..314eebc030
--- /dev/null
+++ b/toolkit/components/remote/nsMacRemoteClient.mm
@@ -0,0 +1,62 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=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/. */
+
+#import <CoreFoundation/CoreFoundation.h>
+#import <Foundation/Foundation.h>
+
+#include <sys/param.h>
+
+#include "MacAutoreleasePool.h"
+#include "nsMacRemoteClient.h"
+#include "RemoteUtils.h"
+
+using namespace mozilla;
+
+nsresult nsMacRemoteClient::Init() { return NS_OK; }
+
+nsresult nsMacRemoteClient::SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv,
+ const char* aDesktopStartupID, char** aResponse,
+ bool* aSucceeded) {
+ mozilla::MacAutoreleasePool pool;
+
+ *aSucceeded = false;
+
+ nsString className;
+ BuildClassName(aProgram, aProfile, className);
+ NSString* serverNameString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(className.get())
+ length:className.Length()];
+
+ CFMessagePortRef messageServer = CFMessagePortCreateRemote(0, (CFStringRef)serverNameString);
+
+ if (messageServer) {
+ // Getting current process directory
+ char cwdPtr[MAXPATHLEN + 1];
+ getcwd(cwdPtr, MAXPATHLEN + 1);
+
+ NSMutableArray* argumentsArray = [NSMutableArray array];
+ for (int i = 0; i < argc; i++) {
+ NSString* argument = [NSString stringWithUTF8String:argv[i]];
+ [argumentsArray addObject:argument];
+ }
+ NSDictionary* dict = @{@"args" : argumentsArray};
+
+ NSData* data = [NSKeyedArchiver archivedDataWithRootObject:dict];
+
+ CFMessagePortSendRequest(messageServer, 0, (CFDataRef)data, 10.0, 0.0, NULL, NULL);
+
+ CFMessagePortInvalidate(messageServer);
+ CFRelease(messageServer);
+ *aSucceeded = true;
+
+ } else {
+ // Remote Server not found. Doing nothing.
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/remote/nsMacRemoteServer.h b/toolkit/components/remote/nsMacRemoteServer.h
new file mode 100644
index 0000000000..0e2bda8a32
--- /dev/null
+++ b/toolkit/components/remote/nsMacRemoteServer.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_
+#define TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_
+
+#import <CoreFoundation/CoreFoundation.h>
+
+#include "nsRemoteServer.h"
+
+class nsMacRemoteServer final : public nsRemoteServer {
+ public:
+ nsMacRemoteServer() = default;
+ ~nsMacRemoteServer() override { Shutdown(); }
+
+ nsresult Startup(const char* aAppName, const char* aProfileName) override;
+ void Shutdown() override;
+
+ void HandleCommandLine(CFDataRef aData);
+
+ private:
+ CFRunLoopSourceRef mRunLoopSource;
+ CFMessagePortRef mMessageServer;
+};
+
+#endif // TOOLKIT_COMPONENTS_REMOTE_NSMACREMOTESERVER_H_
diff --git a/toolkit/components/remote/nsMacRemoteServer.mm b/toolkit/components/remote/nsMacRemoteServer.mm
new file mode 100644
index 0000000000..0936d4d2fd
--- /dev/null
+++ b/toolkit/components/remote/nsMacRemoteServer.mm
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#import <Cocoa/Cocoa.h>
+#import <CoreServices/CoreServices.h>
+
+#include "MacAutoreleasePool.h"
+#include "nsCOMPtr.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsIWindowMediator.h"
+#include "nsIWidget.h"
+#include "nsICommandLineRunner.h"
+#include "nsICommandLine.h"
+#include "nsCommandLine.h"
+#include "nsIDocShell.h"
+#include "nsMacRemoteServer.h"
+#include "nsXPCOM.h"
+#include "RemoteUtils.h"
+
+CFDataRef messageServerCallback(CFMessagePortRef aLocal, int32_t aMsgid, CFDataRef aData,
+ void* aInfo) {
+ // One of the clients submitted a structure.
+ static_cast<nsMacRemoteServer*>(aInfo)->HandleCommandLine(aData);
+
+ return NULL;
+}
+
+// aData contains serialized Dictionary, which in turn contains command line arguments
+void nsMacRemoteServer::HandleCommandLine(CFDataRef aData) {
+ mozilla::MacAutoreleasePool pool;
+
+ if (aData) {
+ NSDictionary* dict = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData*)aData];
+ if (dict && [dict isKindOfClass:[NSDictionary class]]) {
+ NSArray* args = dict[@"args"];
+ if (!args) {
+ NS_ERROR("Wrong parameters passed to the Remote Server");
+ return;
+ }
+
+ nsCOMPtr<nsICommandLineRunner> cmdLine(new nsCommandLine());
+
+ // Converting Objective-C array into a C array,
+ // which nsICommandLineRunner understands.
+ int argc = [args count];
+ const char** argv = new const char*[argc];
+ for (int i = 0; i < argc; i++) {
+ const char* arg = [[args objectAtIndex:i] UTF8String];
+ argv[i] = arg;
+ }
+
+ nsresult rv = cmdLine->Init(argc, argv, nullptr, nsICommandLine::STATE_REMOTE_AUTO);
+
+ // Cleaning up C array.
+ delete[] argv;
+
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Error initializing command line.");
+ return;
+ }
+
+ // Processing the command line, passed from a remote instance
+ // in the current instance.
+ cmdLine->Run();
+
+ // And bring the app's window to front.
+ [[NSRunningApplication currentApplication]
+ activateWithOptions:NSApplicationActivateIgnoringOtherApps];
+ }
+ }
+}
+
+nsresult nsMacRemoteServer::Startup(const char* aAppName, const char* aProfileName) {
+ // This is the first instance ever.
+ // Let's register a notification listener here,
+ // In case future instances would want to notify us about command line arguments
+ // passed to them. Note, that if mozilla process is restarting, we still need to
+ // register for notifications.
+
+ mozilla::MacAutoreleasePool pool;
+
+ nsString className;
+ BuildClassName(aAppName, aProfileName, className);
+
+ NSString* serverNameString =
+ [NSString stringWithCharacters:reinterpret_cast<const unichar*>(className.get())
+ length:className.Length()];
+
+ CFMessagePortContext context;
+ context.copyDescription = NULL;
+ context.info = this;
+ context.release = NULL;
+ context.retain = NULL;
+ context.version = NULL;
+ mMessageServer = CFMessagePortCreateLocal(NULL, (CFStringRef)serverNameString,
+ messageServerCallback, &context, NULL);
+ if (!mMessageServer) {
+ return NS_ERROR_FAILURE;
+ }
+ mRunLoopSource = CFMessagePortCreateRunLoopSource(NULL, mMessageServer, 0);
+ if (!mRunLoopSource) {
+ CFRelease(mMessageServer);
+ mMessageServer = NULL;
+ return NS_ERROR_FAILURE;
+ }
+ CFRunLoopRef runLoop = CFRunLoopGetMain();
+ CFRunLoopAddSource(runLoop, mRunLoopSource, kCFRunLoopDefaultMode);
+
+ return NS_OK;
+}
+
+void nsMacRemoteServer::Shutdown() {
+ // 1) Invalidate server connection
+ if (mMessageServer) {
+ CFMessagePortInvalidate(mMessageServer);
+ }
+
+ // 2) Release run loop source
+ if (mRunLoopSource) {
+ CFRelease(mRunLoopSource);
+ mRunLoopSource = NULL;
+ }
+
+ // 3) Release server connection
+ if (mMessageServer) {
+ CFRelease(mMessageServer);
+ mMessageServer = NULL;
+ }
+}
diff --git a/toolkit/components/remote/nsRemoteClient.h b/toolkit/components/remote/nsRemoteClient.h
new file mode 100644
index 0000000000..f36c33287b
--- /dev/null
+++ b/toolkit/components/remote/nsRemoteClient.h
@@ -0,0 +1,64 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=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/. */
+
+#ifndef TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_
+#define TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_
+
+#include "nscore.h"
+
+/**
+ * Pure-virtual common base class for remoting implementations.
+ */
+
+class nsRemoteClient {
+ public:
+ virtual ~nsRemoteClient() = default;
+
+ /**
+ * Initializes the client
+ */
+ virtual nsresult Init() = 0;
+
+ /**
+ * Send a complete command line to a running instance.
+ *
+ * @param aProgram This is the preferred program that we want to use
+ * for this particular command.
+ *
+ * @param aUsername This allows someone to only talk to an instance
+ * of the server that's running under a particular username. If
+ * this isn't specified here it's pulled from the LOGNAME
+ * environmental variable if it's set.
+ *
+ * @param aProfile This allows you to specify a particular server
+ * running under a named profile. If it is not specified the
+ * profile is not checked.
+ *
+ * @param argc The number of command-line arguments.
+ *
+ * @param argv The command-line arguments.
+ *
+ * @param aStartupToken the contents of the DESKTOP_STARTUP_ID environment
+ * variable defined by the Startup Notification specification, or the
+ * XDG_ACTIVATION_TOKEN defined by Wayland.
+ * http://standards.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt
+ * https://wayland.app/protocols/xdg-activation-v1
+ *
+ * @param aResponse If there is a response, it will be here. This
+ * includes error messages. The string is allocated using stdlib
+ * string functions, so free it with free().
+ *
+ * @return true if succeeded, false if no running instance was found.
+ *
+ */
+ virtual nsresult SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse,
+ bool* aSucceeded) = 0;
+};
+
+#endif // TOOLKIT_COMPONENTS_REMOTE_NSREMOTECLIENT_H_
diff --git a/toolkit/components/remote/nsRemoteServer.h b/toolkit/components/remote/nsRemoteServer.h
new file mode 100644
index 0000000000..7c24bdce71
--- /dev/null
+++ b/toolkit/components/remote/nsRemoteServer.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsRemoteServer_h__
+#define __nsRemoteServer_h__
+
+#include "nsString.h"
+
+class nsRemoteServer {
+ public:
+ virtual ~nsRemoteServer() = default;
+
+ virtual nsresult Startup(const char* aAppName, const char* aProfileName) = 0;
+ virtual void Shutdown() = 0;
+};
+
+#endif // __nsRemoteServer_h__
diff --git a/toolkit/components/remote/nsRemoteService.cpp b/toolkit/components/remote/nsRemoteService.cpp
new file mode 100644
index 0000000000..3e8e4b5cb8
--- /dev/null
+++ b/toolkit/components/remote/nsRemoteService.cpp
@@ -0,0 +1,192 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* 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/. */
+
+#ifdef MOZ_WIDGET_GTK
+# ifdef MOZ_ENABLE_DBUS
+# include "nsDBusRemoteServer.h"
+# include "nsDBusRemoteClient.h"
+# else
+# include "nsGTKRemoteServer.h"
+# include "nsXRemoteClient.h"
+# endif
+#elif defined(XP_WIN)
+# include "nsWinRemoteServer.h"
+# include "nsWinRemoteClient.h"
+#elif defined(XP_DARWIN)
+# include "nsMacRemoteServer.h"
+# include "nsMacRemoteClient.h"
+#endif
+#include "nsRemoteService.h"
+
+#include "nsIObserverService.h"
+#include "nsString.h"
+#include "nsServiceManagerUtils.h"
+#include "SpecialSystemDirectory.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+
+// Time to wait for the remoting service to start
+#define START_TIMEOUT_SEC 5
+#define START_SLEEP_MSEC 100
+
+using namespace mozilla;
+
+extern int gArgc;
+extern char** gArgv;
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsRemoteService, nsIObserver)
+
+nsRemoteService::nsRemoteService(const char* aProgram) : mProgram(aProgram) {
+ ToLowerCase(mProgram);
+}
+
+void nsRemoteService::SetProfile(nsACString& aProfile) { mProfile = aProfile; }
+
+void nsRemoteService::LockStartup() {
+ nsCOMPtr<nsIFile> mutexDir;
+ nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory,
+ getter_AddRefs(mutexDir));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ rv = mutexDir->AppendNative(mProgram);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ const mozilla::TimeStamp epoch = mozilla::TimeStamp::Now();
+ do {
+ // If we have been waiting for another instance to release the lock it will
+ // have deleted the lock directory when doing so we have to make sure it
+ // exists every time we poll for the lock.
+ rv = mutexDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) {
+ mRemoteLockDir = mutexDir;
+ } else {
+ NS_WARNING("Unable to create startup lock directory.");
+ return;
+ }
+
+ rv = mRemoteLock.Lock(mRemoteLockDir, nullptr);
+ if (NS_SUCCEEDED(rv)) {
+ return;
+ }
+
+ mRemoteLockDir = nullptr;
+ PR_Sleep(START_SLEEP_MSEC);
+ } while ((mozilla::TimeStamp::Now() - epoch) <
+ mozilla::TimeDuration::FromSeconds(START_TIMEOUT_SEC));
+
+ NS_WARNING("Failed to lock for startup, continuing anyway.");
+}
+
+void nsRemoteService::UnlockStartup() {
+ if (mRemoteLockDir) {
+ mRemoteLock.Unlock();
+ mRemoteLock.Cleanup();
+
+ mRemoteLockDir->Remove(false);
+ mRemoteLockDir = nullptr;
+ }
+}
+
+RemoteResult nsRemoteService::StartClient(const char* aStartupToken) {
+ if (mProfile.IsEmpty()) {
+ return REMOTE_NOT_FOUND;
+ }
+
+ UniquePtr<nsRemoteClient> client;
+#ifdef MOZ_WIDGET_GTK
+# if defined(MOZ_ENABLE_DBUS)
+ client = MakeUnique<nsDBusRemoteClient>();
+# else
+ client = MakeUnique<nsXRemoteClient>();
+# endif
+#elif defined(XP_WIN)
+ client = MakeUnique<nsWinRemoteClient>();
+#elif defined(XP_DARWIN)
+ client = MakeUnique<nsMacRemoteClient>();
+#else
+ return REMOTE_NOT_FOUND;
+#endif
+
+ nsresult rv = client ? client->Init() : NS_ERROR_FAILURE;
+ if (NS_FAILED(rv)) return REMOTE_NOT_FOUND;
+
+ nsCString response;
+ bool success = false;
+ rv =
+ client->SendCommandLine(mProgram.get(), mProfile.get(), gArgc, gArgv,
+ aStartupToken, getter_Copies(response), &success);
+ // did the command fail?
+ if (!success) return REMOTE_NOT_FOUND;
+
+ // The "command not parseable" error is returned when the
+ // nsICommandLineHandler throws a NS_ERROR_ABORT.
+ if (response.EqualsLiteral("500 command not parseable"))
+ return REMOTE_ARG_BAD;
+
+ if (NS_FAILED(rv)) return REMOTE_NOT_FOUND;
+
+ return REMOTE_FOUND;
+}
+
+void nsRemoteService::StartupServer() {
+ if (mRemoteServer) {
+ return;
+ }
+
+ if (mProfile.IsEmpty()) {
+ return;
+ }
+
+#ifdef MOZ_WIDGET_GTK
+# if defined(MOZ_ENABLE_DBUS)
+ mRemoteServer = MakeUnique<nsDBusRemoteServer>();
+# else
+ mRemoteServer = MakeUnique<nsGTKRemoteServer>();
+# endif
+#elif defined(XP_WIN)
+ mRemoteServer = MakeUnique<nsWinRemoteServer>();
+#elif defined(XP_DARWIN)
+ mRemoteServer = MakeUnique<nsMacRemoteServer>();
+#else
+ return;
+#endif
+
+ if (!mRemoteServer) {
+ return;
+ }
+
+ nsresult rv = mRemoteServer->Startup(mProgram.get(), mProfile.get());
+
+ if (NS_FAILED(rv)) {
+ mRemoteServer = nullptr;
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obs(
+ do_GetService("@mozilla.org/observer-service;1"));
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ obs->AddObserver(this, "quit-application", false);
+ }
+}
+
+void nsRemoteService::ShutdownServer() { mRemoteServer = nullptr; }
+
+nsRemoteService::~nsRemoteService() {
+ UnlockStartup();
+ ShutdownServer();
+}
+
+NS_IMETHODIMP
+nsRemoteService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ // This can be xpcom-shutdown or quit-application, but it's the same either
+ // way.
+ ShutdownServer();
+ return NS_OK;
+}
diff --git a/toolkit/components/remote/nsRemoteService.h b/toolkit/components/remote/nsRemoteService.h
new file mode 100644
index 0000000000..3105913a7f
--- /dev/null
+++ b/toolkit/components/remote/nsRemoteService.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_
+#define TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_
+
+#include "nsRemoteServer.h"
+#include "nsIObserver.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIFile.h"
+#include "nsProfileLock.h"
+
+enum RemoteResult {
+ REMOTE_NOT_FOUND = 0,
+ REMOTE_FOUND = 1,
+ REMOTE_ARG_BAD = 2
+};
+
+class nsRemoteService final : public nsIObserver {
+ public:
+ // We will be a static singleton, so don't use the ordinary methods.
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ explicit nsRemoteService(const char* aProgram);
+ void SetProfile(nsACString& aProfile);
+
+ void LockStartup();
+ void UnlockStartup();
+
+ RemoteResult StartClient(const char* aStartupToken);
+ void StartupServer();
+ void ShutdownServer();
+
+ private:
+ ~nsRemoteService();
+
+ mozilla::UniquePtr<nsRemoteServer> mRemoteServer;
+ nsProfileLock mRemoteLock;
+ nsCOMPtr<nsIFile> mRemoteLockDir;
+ nsCString mProgram;
+ nsCString mProfile;
+};
+
+#endif // TOOLKIT_COMPONENTS_REMOTE_NSREMOTESERVER_H_
diff --git a/toolkit/components/remote/nsUnixRemoteServer.cpp b/toolkit/components/remote/nsUnixRemoteServer.cpp
new file mode 100644
index 0000000000..f26c90af4a
--- /dev/null
+++ b/toolkit/components/remote/nsUnixRemoteServer.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#include "nsUnixRemoteServer.h"
+#include "nsGTKToolkit.h"
+#include "nsCOMPtr.h"
+#include "nsICommandLineRunner.h"
+#include "nsCommandLine.h"
+#include "nsIFile.h"
+
+// Set desktop startup ID to the passed ID, if there is one, so that any created
+// windows get created with the right window manager metadata, and any windows
+// that get new tabs and are activated also get the right WM metadata.
+// The timestamp will be used if there is no desktop startup ID, or if we're
+// raising an existing window rather than showing a new window for the first
+// time.
+void nsUnixRemoteServer::SetStartupTokenOrTimestamp(
+ const nsACString& aStartupToken, uint32_t aTimestamp) {
+ nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit();
+ if (!toolkit) {
+ return;
+ }
+
+ if (!aStartupToken.IsEmpty()) {
+ toolkit->SetStartupToken(aStartupToken);
+ }
+
+ toolkit->SetFocusTimestamp(aTimestamp);
+}
+
+static bool FindExtensionParameterInCommand(const char* aParameterName,
+ const nsACString& aCommand,
+ char aSeparator,
+ nsACString* aValue) {
+ nsAutoCString searchFor;
+ searchFor.Append(aSeparator);
+ searchFor.Append(aParameterName);
+ searchFor.Append('=');
+
+ nsACString::const_iterator start, end;
+ aCommand.BeginReading(start);
+ aCommand.EndReading(end);
+ if (!FindInReadable(searchFor, start, end)) return false;
+
+ nsACString::const_iterator charStart, charEnd;
+ charStart = end;
+ aCommand.EndReading(charEnd);
+ nsACString::const_iterator idStart = charStart, idEnd;
+ if (FindCharInReadable(aSeparator, charStart, charEnd)) {
+ idEnd = charStart;
+ } else {
+ idEnd = charEnd;
+ }
+ *aValue = nsDependentCSubstring(idStart, idEnd);
+ return true;
+}
+
+const char* nsUnixRemoteServer::HandleCommandLine(const char* aBuffer,
+ uint32_t aTimestamp) {
+ nsCOMPtr<nsICommandLineRunner> cmdline(new nsCommandLine());
+
+ // the commandline property is constructed as an array of int32_t
+ // followed by a series of null-terminated strings:
+ //
+ // [argc][offsetargv0][offsetargv1...]<workingdir>\0<argv[0]>\0argv[1]...\0
+ // (offset is from the beginning of the buffer)
+
+ int32_t argc = TO_LITTLE_ENDIAN32(*reinterpret_cast<const int32_t*>(aBuffer));
+ const char* wd = aBuffer + ((argc + 1) * sizeof(int32_t));
+
+ nsCOMPtr<nsIFile> lf;
+ nsresult rv =
+ NS_NewNativeLocalFile(nsDependentCString(wd), true, getter_AddRefs(lf));
+ if (NS_FAILED(rv)) return "509 internal error";
+
+ nsAutoCString desktopStartupID;
+
+ const char** argv = (const char**)malloc(sizeof(char*) * argc);
+ if (!argv) return "509 internal error";
+
+ const int32_t* offset = reinterpret_cast<const int32_t*>(aBuffer) + 1;
+
+ for (int i = 0; i < argc; ++i) {
+ argv[i] = aBuffer + TO_LITTLE_ENDIAN32(offset[i]);
+
+ if (i == 0) {
+ nsDependentCString cmd(argv[0]);
+ FindExtensionParameterInCommand("STARTUP_TOKEN", cmd, ' ',
+ &desktopStartupID);
+ }
+ }
+
+ rv = cmdline->Init(argc, argv, lf, nsICommandLine::STATE_REMOTE_AUTO);
+
+ free(argv);
+ if (NS_FAILED(rv)) {
+ return "509 internal error";
+ }
+
+ SetStartupTokenOrTimestamp(desktopStartupID, aTimestamp);
+
+ rv = cmdline->Run();
+
+ if (NS_ERROR_ABORT == rv) return "500 command not parseable";
+
+ if (NS_FAILED(rv)) return "509 internal error";
+
+ return "200 executed command";
+}
diff --git a/toolkit/components/remote/nsUnixRemoteServer.h b/toolkit/components/remote/nsUnixRemoteServer.h
new file mode 100644
index 0000000000..003f2efbf8
--- /dev/null
+++ b/toolkit/components/remote/nsUnixRemoteServer.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsUnixRemoteServer_h__
+#define __nsUnixRemoteServer_h__
+
+#include "nsStringFwd.h"
+
+#ifdef IS_BIG_ENDIAN
+# define TO_LITTLE_ENDIAN32(x) \
+ ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \
+ (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24))
+#else
+# define TO_LITTLE_ENDIAN32(x) (x)
+#endif
+
+class nsUnixRemoteServer {
+ protected:
+ void SetStartupTokenOrTimestamp(const nsACString& aStartupToken,
+ uint32_t aTimestamp);
+ const char* HandleCommandLine(const char* aBuffer, uint32_t aTimestamp);
+};
+
+#endif // __nsGTKRemoteService_h__
diff --git a/toolkit/components/remote/nsWinRemoteClient.cpp b/toolkit/components/remote/nsWinRemoteClient.cpp
new file mode 100644
index 0000000000..4428070bfa
--- /dev/null
+++ b/toolkit/components/remote/nsWinRemoteClient.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=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 "nsWinRemoteClient.h"
+#include <windows.h>
+#include "RemoteUtils.h"
+#include "WinRemoteMessage.h"
+
+using namespace mozilla;
+
+nsresult nsWinRemoteClient::Init() { return NS_OK; }
+
+nsresult nsWinRemoteClient::SendCommandLine(
+ const char* aProgram, const char* aProfile, int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse, bool* aSucceeded) {
+ *aSucceeded = false;
+
+ nsString className;
+ BuildClassName(aProgram, aProfile, className);
+
+ HWND handle = ::FindWindowW(className.get(), 0);
+
+ if (!handle) {
+ return NS_OK;
+ }
+
+ WCHAR cwd[MAX_PATH];
+ _wgetcwd(cwd, MAX_PATH);
+ WinRemoteMessageSender sender(::GetCommandLineW(), cwd);
+
+ // Bring the already running Mozilla process to the foreground.
+ // nsWindow will restore the window (if minimized) and raise it.
+ ::SetForegroundWindow(handle);
+ ::SendMessageW(handle, WM_COPYDATA, 0,
+ reinterpret_cast<LPARAM>(sender.CopyData()));
+
+ *aSucceeded = true;
+
+ return NS_OK;
+}
diff --git a/toolkit/components/remote/nsWinRemoteClient.h b/toolkit/components/remote/nsWinRemoteClient.h
new file mode 100644
index 0000000000..64859b69a1
--- /dev/null
+++ b/toolkit/components/remote/nsWinRemoteClient.h
@@ -0,0 +1,25 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=4:tabstop=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/. */
+
+#ifndef nsWinRemoteClient_h__
+#define nsWinRemoteClient_h__
+
+#include "nscore.h"
+#include "nsRemoteClient.h"
+
+class nsWinRemoteClient : public nsRemoteClient {
+ public:
+ virtual ~nsWinRemoteClient() = default;
+
+ nsresult Init() override;
+
+ nsresult SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv, const char* aStartupToken,
+ char** aResponse, bool* aSucceeded) override;
+};
+
+#endif // nsWinRemoteClient_h__
diff --git a/toolkit/components/remote/nsWinRemoteServer.cpp b/toolkit/components/remote/nsWinRemoteServer.cpp
new file mode 100644
index 0000000000..0fa4f5facc
--- /dev/null
+++ b/toolkit/components/remote/nsWinRemoteServer.cpp
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#include "CmdLineAndEnvUtils.h"
+#include "nsWinRemoteServer.h"
+#include "RemoteUtils.h"
+#include "nsCOMPtr.h"
+#include "nsXPCOM.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWindowMediator.h"
+#include "nsIBaseWindow.h"
+#include "nsIWidget.h"
+#include "nsICommandLineRunner.h"
+#include "nsICommandLine.h"
+#include "nsCommandLine.h"
+#include "nsIDocShell.h"
+#include "WinRemoteMessage.h"
+
+HWND hwndForDOMWindow(mozIDOMWindowProxy* window) {
+ if (!window) {
+ return 0;
+ }
+ nsCOMPtr<nsPIDOMWindowOuter> pidomwindow = nsPIDOMWindowOuter::From(window);
+
+ nsCOMPtr<nsIBaseWindow> ppBaseWindow =
+ do_QueryInterface(pidomwindow->GetDocShell());
+ if (!ppBaseWindow) {
+ return 0;
+ }
+
+ nsCOMPtr<nsIWidget> ppWidget;
+ ppBaseWindow->GetMainWidget(getter_AddRefs(ppWidget));
+
+ return (HWND)(ppWidget->GetNativeData(NS_NATIVE_WIDGET));
+}
+
+static nsresult GetMostRecentWindow(mozIDOMWindowProxy** aWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIWindowMediator> med(
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ if (med) return med->GetMostRecentWindow(nullptr, aWindow);
+
+ return NS_ERROR_FAILURE;
+}
+
+LRESULT CALLBACK WindowProc(HWND msgWindow, UINT msg, WPARAM wp, LPARAM lp) {
+ if (msg == WM_COPYDATA) {
+ WinRemoteMessageReceiver receiver;
+ if (NS_SUCCEEDED(receiver.Parse(reinterpret_cast<COPYDATASTRUCT*>(lp)))) {
+ receiver.CommandLineRunner()->Run();
+ } else {
+ NS_ERROR("Error initializing command line.");
+ }
+
+ // Get current window and return its window handle.
+ nsCOMPtr<mozIDOMWindowProxy> win;
+ GetMostRecentWindow(getter_AddRefs(win));
+ return win ? (LRESULT)hwndForDOMWindow(win) : 0;
+ }
+ return DefWindowProcW(msgWindow, msg, wp, lp);
+}
+
+nsresult nsWinRemoteServer::Startup(const char* aAppName,
+ const char* aProfileName) {
+ nsString className;
+ BuildClassName(aAppName, aProfileName, className);
+
+ WNDCLASSW classStruct = {0, // style
+ &WindowProc, // lpfnWndProc
+ 0, // cbClsExtra
+ 0, // cbWndExtra
+ 0, // hInstance
+ 0, // hIcon
+ 0, // hCursor
+ 0, // hbrBackground
+ 0, // lpszMenuName
+ className.get()}; // lpszClassName
+
+ // Register the window class.
+ NS_ENSURE_TRUE(::RegisterClassW(&classStruct), NS_ERROR_FAILURE);
+
+ // Create the window.
+ mHandle = ::CreateWindowW(className.get(),
+ 0, // title
+ WS_CAPTION, // style
+ 0, 0, 0, 0, // x, y, cx, cy
+ 0, // parent
+ 0, // menu
+ 0, // instance
+ 0); // create struct
+
+ return mHandle ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void nsWinRemoteServer::Shutdown() {
+ DestroyWindow(mHandle);
+ mHandle = nullptr;
+}
diff --git a/toolkit/components/remote/nsWinRemoteServer.h b/toolkit/components/remote/nsWinRemoteServer.h
new file mode 100644
index 0000000000..1687e2d44b
--- /dev/null
+++ b/toolkit/components/remote/nsWinRemoteServer.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=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/. */
+
+#ifndef __nsWinRemoteServer_h__
+#define __nsWinRemoteServer_h__
+
+#include "nsRemoteServer.h"
+
+#include <windows.h>
+
+class nsWinRemoteServer final : public nsRemoteServer {
+ public:
+ nsWinRemoteServer() = default;
+ ~nsWinRemoteServer() override { Shutdown(); }
+
+ nsresult Startup(const char* aAppName, const char* aProfileName) override;
+ void Shutdown() override;
+
+ private:
+ HWND mHandle;
+};
+
+#endif // __nsWinRemoteService_h__
diff --git a/toolkit/components/remote/nsXRemoteClient.cpp b/toolkit/components/remote/nsXRemoteClient.cpp
new file mode 100644
index 0000000000..e8af9eaf56
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteClient.cpp
@@ -0,0 +1,654 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* vim:set ts=8 sw=2 et cindent: */
+/* 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 "nsDebug.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Unused.h"
+#include "nsXRemoteClient.h"
+#include "RemoteUtils.h"
+#include "plstr.h"
+#include "prsystem.h"
+#include "mozilla/Logging.h"
+#include "prenv.h"
+#include "prdtoa.h"
+#include <X11/Xatom.h>
+#include <limits.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#define MOZILLA_VERSION_PROP "_MOZILLA_VERSION"
+#define MOZILLA_LOCK_PROP "_MOZILLA_LOCK"
+#define MOZILLA_COMMANDLINE_PROP "_MOZILLA_COMMANDLINE"
+#define MOZILLA_RESPONSE_PROP "_MOZILLA_RESPONSE"
+#define MOZILLA_USER_PROP "_MOZILLA_USER"
+#define MOZILLA_PROFILE_PROP "_MOZILLA_PROFILE"
+#define MOZILLA_PROGRAM_PROP "_MOZILLA_PROGRAM"
+
+#ifdef IS_BIG_ENDIAN
+# define TO_LITTLE_ENDIAN32(x) \
+ ((((x)&0xff000000) >> 24) | (((x)&0x00ff0000) >> 8) | \
+ (((x)&0x0000ff00) << 8) | (((x)&0x000000ff) << 24))
+#else
+# define TO_LITTLE_ENDIAN32(x) (x)
+#endif
+
+#ifndef MAX_PATH
+# ifdef PATH_MAX
+# define MAX_PATH PATH_MAX
+# else
+# define MAX_PATH 1024
+# endif
+#endif
+
+using mozilla::LogLevel;
+using mozilla::Unused;
+
+static mozilla::LazyLogModule sRemoteLm("nsXRemoteClient");
+
+static int (*sOldHandler)(Display*, XErrorEvent*);
+static bool sGotBadWindow;
+
+nsXRemoteClient::nsXRemoteClient() {
+ mDisplay = 0;
+ mInitialized = false;
+ mMozVersionAtom = 0;
+ mMozLockAtom = 0;
+ mMozCommandLineAtom = 0;
+ mMozResponseAtom = 0;
+ mMozWMStateAtom = 0;
+ mMozUserAtom = 0;
+ mMozProfileAtom = 0;
+ mMozProgramAtom = 0;
+ mLockData = 0;
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::nsXRemoteClient"));
+}
+
+nsXRemoteClient::~nsXRemoteClient() {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::~nsXRemoteClient"));
+ if (mInitialized) Shutdown();
+}
+
+// Minimize the roundtrips to the X-server
+static const char* XAtomNames[] = {
+ MOZILLA_VERSION_PROP, MOZILLA_LOCK_PROP, MOZILLA_RESPONSE_PROP,
+ "WM_STATE", MOZILLA_USER_PROP, MOZILLA_PROFILE_PROP,
+ MOZILLA_PROGRAM_PROP, MOZILLA_COMMANDLINE_PROP};
+static Atom XAtoms[MOZ_ARRAY_LENGTH(XAtomNames)];
+
+nsresult nsXRemoteClient::Init() {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::Init"));
+
+ if (mInitialized) return NS_OK;
+
+ // try to open the display
+ mDisplay = XOpenDisplay(0);
+ if (!mDisplay) return NS_ERROR_FAILURE;
+
+ // get our atoms
+ XInternAtoms(mDisplay, const_cast<char**>(XAtomNames),
+ MOZ_ARRAY_LENGTH(XAtomNames), False, XAtoms);
+
+ int i = 0;
+ mMozVersionAtom = XAtoms[i++];
+ mMozLockAtom = XAtoms[i++];
+ mMozResponseAtom = XAtoms[i++];
+ mMozWMStateAtom = XAtoms[i++];
+ mMozUserAtom = XAtoms[i++];
+ mMozProfileAtom = XAtoms[i++];
+ mMozProgramAtom = XAtoms[i++];
+ mMozCommandLineAtom = XAtoms[i];
+
+ mInitialized = true;
+
+ return NS_OK;
+}
+
+void nsXRemoteClient::Shutdown(void) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::Shutdown"));
+
+ if (!mInitialized) return;
+
+ // shut everything down
+ XCloseDisplay(mDisplay);
+ mDisplay = 0;
+ mInitialized = false;
+ if (mLockData) {
+ free(mLockData);
+ mLockData = 0;
+ }
+}
+
+static int HandleBadWindow(Display* display, XErrorEvent* event) {
+ if (event->error_code == BadWindow) {
+ sGotBadWindow = true;
+ return 0; // ignored
+ }
+
+ return (*sOldHandler)(display, event);
+}
+
+nsresult nsXRemoteClient::SendCommandLine(
+ const char* aProgram, const char* aProfile, int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse, bool* aWindowFound) {
+ NS_ENSURE_TRUE(aProgram, NS_ERROR_INVALID_ARG);
+
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("nsXRemoteClient::SendCommandLine"));
+
+ *aWindowFound = false;
+
+ // FindBestWindow() iterates down the window hierarchy, so catch X errors
+ // when windows get destroyed before being accessed.
+ sOldHandler = XSetErrorHandler(HandleBadWindow);
+
+ Window w = FindBestWindow(aProgram, aProfile);
+
+ nsresult rv = NS_OK;
+
+ if (w) {
+ // ok, let the caller know that we at least found a window.
+ *aWindowFound = true;
+
+ // Ignore BadWindow errors up to this point. The last request from
+ // FindBestWindow() was a synchronous XGetWindowProperty(), so no need to
+ // Sync. Leave the error handler installed to detect if w gets destroyed.
+ sGotBadWindow = false;
+
+ // make sure we get the right events on that window
+ XSelectInput(mDisplay, w, (PropertyChangeMask | StructureNotifyMask));
+
+ bool destroyed = false;
+
+ // get the lock on the window
+ rv = GetLock(w, &destroyed);
+
+ if (NS_SUCCEEDED(rv)) {
+ // send our command
+ rv = DoSendCommandLine(w, argc, argv, aStartupToken, aResponse,
+ &destroyed);
+
+ // if the window was destroyed, don't bother trying to free the
+ // lock.
+ if (!destroyed) FreeLock(w); // doesn't really matter what this returns
+ }
+ }
+
+ XSetErrorHandler(sOldHandler);
+
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("SendCommandInternal returning 0x%" PRIx32 "\n",
+ static_cast<uint32_t>(rv)));
+
+ return rv;
+}
+
+Window nsXRemoteClient::CheckWindow(Window aWindow) {
+ Atom type = None;
+ int format;
+ unsigned long nitems, bytesafter;
+ unsigned char* data;
+ Window innerWindow;
+
+ XGetWindowProperty(mDisplay, aWindow, mMozWMStateAtom, 0, 0, False,
+ AnyPropertyType, &type, &format, &nitems, &bytesafter,
+ &data);
+
+ if (type) {
+ XFree(data);
+ return aWindow;
+ }
+
+ // didn't find it here so check the children of this window
+ innerWindow = CheckChildren(aWindow);
+
+ if (innerWindow) return innerWindow;
+
+ return aWindow;
+}
+
+Window nsXRemoteClient::CheckChildren(Window aWindow) {
+ Window root, parent;
+ Window* children;
+ unsigned int nchildren;
+ unsigned int i;
+ Atom type = None;
+ int format;
+ unsigned long nitems, after;
+ unsigned char* data;
+ Window retval = None;
+
+ if (!XQueryTree(mDisplay, aWindow, &root, &parent, &children, &nchildren))
+ return None;
+
+ // scan the list first before recursing into the list of windows
+ // which can get quite deep.
+ for (i = 0; !retval && (i < nchildren); i++) {
+ XGetWindowProperty(mDisplay, children[i], mMozWMStateAtom, 0, 0, False,
+ AnyPropertyType, &type, &format, &nitems, &after, &data);
+ if (type) {
+ XFree(data);
+ retval = children[i];
+ }
+ }
+
+ // otherwise recurse into the list
+ for (i = 0; !retval && (i < nchildren); i++) {
+ retval = CheckChildren(children[i]);
+ }
+
+ if (children) XFree((char*)children);
+
+ return retval;
+}
+
+nsresult nsXRemoteClient::GetLock(Window aWindow, bool* aDestroyed) {
+ bool locked = false;
+ bool waited = false;
+ *aDestroyed = false;
+
+ nsresult rv = NS_OK;
+
+ if (!mLockData) {
+ char pidstr[32];
+ char sysinfobuf[SYS_INFO_BUFFER_LENGTH];
+ SprintfLiteral(pidstr, "pid%d@", getpid());
+ PRStatus status;
+ status =
+ PR_GetSystemInfo(PR_SI_HOSTNAME, sysinfobuf, SYS_INFO_BUFFER_LENGTH);
+ if (status != PR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // allocate enough space for the string plus the terminating
+ // char
+ mLockData = (char*)malloc(strlen(pidstr) + strlen(sysinfobuf) + 1);
+ if (!mLockData) return NS_ERROR_OUT_OF_MEMORY;
+
+ strcpy(mLockData, pidstr);
+ if (!strcat(mLockData, sysinfobuf)) return NS_ERROR_FAILURE;
+ }
+
+ do {
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char* data = 0;
+
+ XGrabServer(mDisplay);
+
+ result = XGetWindowProperty(
+ mDisplay, aWindow, mMozLockAtom, 0, (65536 / sizeof(long)),
+ False, /* don't delete */
+ XA_STRING, &actual_type, &actual_format, &nitems, &bytes_after, &data);
+
+ // aWindow may have been destroyed before XSelectInput was processed, in
+ // which case there may not be any DestroyNotify event in the queue to
+ // tell us. XGetWindowProperty() was synchronous so error responses have
+ // now been processed, setting sGotBadWindow.
+ if (sGotBadWindow) {
+ *aDestroyed = true;
+ rv = NS_ERROR_FAILURE;
+ } else if (result != Success || actual_type == None) {
+ /* It's not now locked - lock it. */
+ XChangeProperty(mDisplay, aWindow, mMozLockAtom, XA_STRING, 8,
+ PropModeReplace, (unsigned char*)mLockData,
+ strlen(mLockData));
+ locked = True;
+ }
+
+ XUngrabServer(mDisplay);
+ XFlush(mDisplay); // ungrab now!
+
+ if (!locked && !NS_FAILED(rv)) {
+ /* We tried to grab the lock this time, and failed because someone
+ else is holding it already. So, wait for a PropertyDelete event
+ to come in, and try again. */
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("window 0x%x is locked by %s; waiting...\n",
+ (unsigned int)aWindow, data));
+ waited = True;
+ while (true) {
+ XEvent event;
+ int poll_retval;
+ struct pollfd pfd;
+
+ pfd.fd = ConnectionNumber(mDisplay);
+ pfd.events = POLLIN;
+
+ poll_retval = poll(&pfd, 1, 10 * 1000);
+ // did we time out?
+ if (poll_retval == 0) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("timed out waiting for window\n"));
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("xevent...\n"));
+ // FIXME check the return value from this?
+ XNextEvent(mDisplay, &event);
+ if (event.xany.type == DestroyNotify &&
+ event.xdestroywindow.window == aWindow) {
+ *aDestroyed = true;
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ if (event.xany.type == PropertyNotify &&
+ event.xproperty.state == PropertyDelete &&
+ event.xproperty.window == aWindow &&
+ event.xproperty.atom == mMozLockAtom) {
+ /* Ok! Someone deleted their lock, so now we can try
+ again. */
+ MOZ_LOG(
+ sRemoteLm, LogLevel::Debug,
+ ("(0x%x unlocked, trying again...)\n", (unsigned int)aWindow));
+ break;
+ }
+ }
+ }
+ if (data) XFree(data);
+ } while (!locked && !NS_FAILED(rv));
+
+ if (waited && locked) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("obtained lock.\n"));
+ } else if (*aDestroyed) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("window 0x%x unexpectedly destroyed.\n", (unsigned int)aWindow));
+ }
+
+ return rv;
+}
+
+Window nsXRemoteClient::FindBestWindow(const char* aProgram,
+ const char* aProfile) {
+ Window root = RootWindowOfScreen(DefaultScreenOfDisplay(mDisplay));
+ Window bestWindow = 0;
+ Window root2, parent, *kids;
+ unsigned int nkids;
+
+ // Get a list of the children of the root window, walk the list
+ // looking for the best window that fits the criteria.
+ if (!XQueryTree(mDisplay, root, &root2, &parent, &kids, &nkids)) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("XQueryTree failed in nsXRemoteClient::FindBestWindow"));
+ return 0;
+ }
+
+ if (!(kids && nkids)) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("root window has no children"));
+ return 0;
+ }
+
+ // We'll walk the list of windows looking for a window that best
+ // fits the criteria here.
+
+ for (unsigned int i = 0; i < nkids; i++) {
+ Atom type;
+ int format;
+ unsigned long nitems, bytesafter;
+ unsigned char* data_return = 0;
+ Window w;
+ w = kids[i];
+ // find the inner window with WM_STATE on it
+ w = CheckWindow(w);
+
+ int status = XGetWindowProperty(
+ mDisplay, w, mMozVersionAtom, 0, (65536 / sizeof(long)), False,
+ XA_STRING, &type, &format, &nitems, &bytesafter, &data_return);
+
+ if (!data_return) continue;
+
+ double version = PR_strtod((char*)data_return, nullptr);
+ XFree(data_return);
+
+ if (!(version >= 5.1 && version < 6)) continue;
+
+ data_return = 0;
+
+ if (status != Success || type == None) continue;
+
+ // Check that this window is from the right program.
+ Unused << XGetWindowProperty(
+ mDisplay, w, mMozProgramAtom, 0, (65536 / sizeof(long)), False,
+ XA_STRING, &type, &format, &nitems, &bytesafter, &data_return);
+
+ // If the return name is not the same as this program name, we don't want
+ // this window.
+ if (data_return) {
+ if (strcmp(aProgram, (const char*)data_return)) {
+ XFree(data_return);
+ continue;
+ }
+
+ // This is actually the success condition.
+ XFree(data_return);
+ } else {
+ // Doesn't support the protocol, even though the user
+ // requested it. So we're not going to use this window.
+ continue;
+ }
+
+ // Check to see if it has the user atom on that window. If there
+ // is then we need to make sure that it matches what we have.
+ const char* username = PR_GetEnv("LOGNAME");
+
+ if (username) {
+ Unused << XGetWindowProperty(
+ mDisplay, w, mMozUserAtom, 0, (65536 / sizeof(long)), False,
+ XA_STRING, &type, &format, &nitems, &bytesafter, &data_return);
+
+ // if there's a username compare it with what we have
+ if (data_return) {
+ // If the IDs aren't equal, we don't want this window.
+ if (strcmp(username, (const char*)data_return)) {
+ XFree(data_return);
+ continue;
+ }
+
+ XFree(data_return);
+ }
+ }
+
+ // Check to see if there's a profile name on this window. If
+ // there is, then we need to make sure it matches what someone
+ // passed in.
+ Unused << XGetWindowProperty(
+ mDisplay, w, mMozProfileAtom, 0, (65536 / sizeof(long)), False,
+ XA_STRING, &type, &format, &nitems, &bytesafter, &data_return);
+
+ // If there's a profile compare it with what we have
+ if (data_return) {
+ // If the profiles aren't equal, we don't want this window.
+ if (strcmp(aProfile, (const char*)data_return)) {
+ XFree(data_return);
+ continue;
+ }
+
+ XFree(data_return);
+ } else {
+ // This isn't the window for this profile.
+ continue;
+ }
+
+ // Check to see if the window supports the new command-line passing
+ // protocol, if that is requested.
+
+ // If we got this far, this is the best window. It passed
+ // all the tests.
+ bestWindow = w;
+ break;
+ }
+
+ if (kids) XFree((char*)kids);
+
+ return bestWindow;
+}
+
+nsresult nsXRemoteClient::FreeLock(Window aWindow) {
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char* data = 0;
+
+ result = XGetWindowProperty(
+ mDisplay, aWindow, mMozLockAtom, 0, (65536 / sizeof(long)),
+ True, /* atomic delete after */
+ XA_STRING, &actual_type, &actual_format, &nitems, &bytes_after, &data);
+ if (result != Success) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("unable to read and delete " MOZILLA_LOCK_PROP " property\n"));
+ return NS_ERROR_FAILURE;
+ }
+ if (!data || !*data) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("invalid data on " MOZILLA_LOCK_PROP " of window 0x%x.\n",
+ (unsigned int)aWindow));
+ return NS_ERROR_FAILURE;
+ } else if (strcmp((char*)data, mLockData)) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ (MOZILLA_LOCK_PROP " was stolen! Expected \"%s\", saw \"%s\"!\n",
+ mLockData, data));
+ return NS_ERROR_FAILURE;
+ }
+
+ if (data) XFree(data);
+ return NS_OK;
+}
+
+nsresult nsXRemoteClient::DoSendCommandLine(Window aWindow, int32_t argc,
+ char** argv,
+ const char* aStartupToken,
+ char** aResponse,
+ bool* aDestroyed) {
+ *aDestroyed = false;
+
+ int commandLineLength;
+ char* commandLine =
+ ConstructCommandLine(argc, argv, aStartupToken, &commandLineLength);
+ XChangeProperty(mDisplay, aWindow, mMozCommandLineAtom, XA_STRING, 8,
+ PropModeReplace, (unsigned char*)commandLine,
+ commandLineLength);
+ free(commandLine);
+
+ if (!WaitForResponse(aWindow, aResponse, aDestroyed, mMozCommandLineAtom))
+ return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+bool nsXRemoteClient::WaitForResponse(Window aWindow, char** aResponse,
+ bool* aDestroyed, Atom aCommandAtom) {
+ bool done = false;
+ bool accepted = false;
+
+ while (!done) {
+ XEvent event;
+ XNextEvent(mDisplay, &event);
+ if (event.xany.type == DestroyNotify &&
+ event.xdestroywindow.window == aWindow) {
+ /* Print to warn user...*/
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("window 0x%x was destroyed.\n", (unsigned int)aWindow));
+ *aResponse = strdup("Window was destroyed while reading response.");
+ *aDestroyed = true;
+ return false;
+ }
+ if (event.xany.type == PropertyNotify &&
+ event.xproperty.state == PropertyNewValue &&
+ event.xproperty.window == aWindow &&
+ event.xproperty.atom == mMozResponseAtom) {
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char* data = 0;
+ Bool result;
+ result = XGetWindowProperty(mDisplay, aWindow, mMozResponseAtom, 0,
+ (65536 / sizeof(long)),
+ True, /* atomic delete after */
+ XA_STRING, &actual_type, &actual_format,
+ &nitems, &bytes_after, &data);
+ if (result != Success) {
+ MOZ_LOG(
+ sRemoteLm, LogLevel::Debug,
+ ("failed reading " MOZILLA_RESPONSE_PROP " from window 0x%0x.\n",
+ (unsigned int)aWindow));
+ *aResponse = strdup("Internal error reading response from window.");
+ done = true;
+ } else if (!data || strlen((char*)data) < 5) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("invalid data on " MOZILLA_RESPONSE_PROP
+ " property of window 0x%0x.\n",
+ (unsigned int)aWindow));
+ *aResponse = strdup("Server returned invalid data in response.");
+ done = true;
+ } else if (*data == '1') { /* positive preliminary reply */
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4));
+ /* keep going */
+ done = false;
+ }
+
+ else if (!strncmp((char*)data, "200", 3)) { /* positive completion */
+ *aResponse = strdup((char*)data);
+ accepted = true;
+ done = true;
+ }
+
+ else if (*data == '2') { /* positive completion */
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4));
+ *aResponse = strdup((char*)data);
+ accepted = true;
+ done = true;
+ }
+
+ else if (*data == '3') { /* positive intermediate reply */
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("internal error: "
+ "server wants more information? (%s)\n",
+ data));
+ *aResponse = strdup((char*)data);
+ done = true;
+ }
+
+ else if (*data == '4' || /* transient negative completion */
+ *data == '5') { /* permanent negative completion */
+ MOZ_LOG(sRemoteLm, LogLevel::Debug, ("%s\n", data + 4));
+ *aResponse = strdup((char*)data);
+ done = true;
+ }
+
+ else {
+ MOZ_LOG(
+ sRemoteLm, LogLevel::Debug,
+ ("unrecognised " MOZILLA_RESPONSE_PROP " from window 0x%x: %s\n",
+ (unsigned int)aWindow, data));
+ *aResponse = strdup((char*)data);
+ done = true;
+ }
+
+ if (data) XFree(data);
+ }
+
+ else if (event.xany.type == PropertyNotify &&
+ event.xproperty.window == aWindow &&
+ event.xproperty.state == PropertyDelete &&
+ event.xproperty.atom == aCommandAtom) {
+ MOZ_LOG(sRemoteLm, LogLevel::Debug,
+ ("(server 0x%x has accepted " MOZILLA_COMMANDLINE_PROP ".)\n",
+ (unsigned int)aWindow));
+ }
+ }
+
+ return accepted;
+}
diff --git a/toolkit/components/remote/nsXRemoteClient.h b/toolkit/components/remote/nsXRemoteClient.h
new file mode 100644
index 0000000000..ad8e7d3787
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteClient.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; 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/. */
+
+#include <X11/X.h>
+#include <X11/Xlib.h>
+
+#include "nsRemoteClient.h"
+
+class nsXRemoteClient : public nsRemoteClient {
+ public:
+ nsXRemoteClient();
+ ~nsXRemoteClient();
+
+ virtual nsresult Init() override;
+ virtual nsresult SendCommandLine(const char* aProgram, const char* aProfile,
+ int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse,
+ bool* aSucceeded) override;
+ void Shutdown();
+
+ private:
+ Window CheckWindow(Window aWindow);
+ Window CheckChildren(Window aWindow);
+ nsresult GetLock(Window aWindow, bool* aDestroyed);
+ nsresult FreeLock(Window aWindow);
+ Window FindBestWindow(const char* aProgram, const char* aProfile);
+ nsresult DoSendCommandLine(Window aWindow, int32_t argc, char** argv,
+ const char* aStartupToken, char** aResponse,
+ bool* aDestroyed);
+ bool WaitForResponse(Window aWindow, char** aResponse, bool* aDestroyed,
+ Atom aCommandAtom);
+
+ Display* mDisplay;
+
+ Atom mMozVersionAtom;
+ Atom mMozLockAtom;
+ Atom mMozCommandLineAtom;
+ Atom mMozResponseAtom;
+ Atom mMozWMStateAtom;
+ Atom mMozUserAtom;
+ Atom mMozProfileAtom;
+ Atom mMozProgramAtom;
+
+ char* mLockData;
+
+ bool mInitialized;
+};
diff --git a/toolkit/components/remote/nsXRemoteServer.cpp b/toolkit/components/remote/nsXRemoteServer.cpp
new file mode 100644
index 0000000000..1f97b18e20
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteServer.cpp
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* 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 "mozilla/ArrayUtils.h"
+
+#include "nsXRemoteServer.h"
+#include "nsCOMPtr.h"
+#include "nsICommandLine.h"
+
+#include "nsIWidget.h"
+#include "nsAppShellCID.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/X11Util.h"
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "prenv.h"
+#include "nsCRT.h"
+
+#include "nsXULAppAPI.h"
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+
+using namespace mozilla;
+
+#define MOZILLA_VERSION_PROP "_MOZILLA_VERSION"
+#define MOZILLA_LOCK_PROP "_MOZILLA_LOCK"
+#define MOZILLA_RESPONSE_PROP "_MOZILLA_RESPONSE"
+#define MOZILLA_USER_PROP "_MOZILLA_USER"
+#define MOZILLA_PROFILE_PROP "_MOZILLA_PROFILE"
+#define MOZILLA_PROGRAM_PROP "_MOZILLA_PROGRAM"
+#define MOZILLA_COMMANDLINE_PROP "_MOZILLA_COMMANDLINE"
+
+const unsigned char kRemoteVersion[] = "5.1";
+
+// Minimize the roundtrips to the X server by getting all the atoms at once
+static const char* XAtomNames[] = {
+ MOZILLA_VERSION_PROP, MOZILLA_LOCK_PROP, MOZILLA_RESPONSE_PROP,
+ MOZILLA_USER_PROP, MOZILLA_PROFILE_PROP, MOZILLA_PROGRAM_PROP,
+ MOZILLA_COMMANDLINE_PROP};
+static Atom XAtoms[MOZ_ARRAY_LENGTH(XAtomNames)];
+
+Atom nsXRemoteServer::sMozVersionAtom;
+Atom nsXRemoteServer::sMozLockAtom;
+Atom nsXRemoteServer::sMozResponseAtom;
+Atom nsXRemoteServer::sMozUserAtom;
+Atom nsXRemoteServer::sMozProfileAtom;
+Atom nsXRemoteServer::sMozProgramAtom;
+Atom nsXRemoteServer::sMozCommandLineAtom;
+
+nsXRemoteServer::nsXRemoteServer() = default;
+
+void nsXRemoteServer::XRemoteBaseStartup(const char* aAppName,
+ const char* aProfileName) {
+ EnsureAtoms();
+
+ mAppName = aAppName;
+ ToLowerCase(mAppName);
+
+ mProfileName = aProfileName;
+}
+
+void nsXRemoteServer::HandleCommandsFor(Window aWindowId) {
+ // set our version
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozVersionAtom,
+ XA_STRING, 8, PropModeReplace, kRemoteVersion,
+ sizeof(kRemoteVersion) - 1);
+
+ // get our username
+ unsigned char* logname;
+ logname = (unsigned char*)PR_GetEnv("LOGNAME");
+ if (logname) {
+ // set the property on the window if it's available
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozUserAtom,
+ XA_STRING, 8, PropModeReplace, logname,
+ strlen((char*)logname));
+ }
+
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozProgramAtom,
+ XA_STRING, 8, PropModeReplace, (unsigned char*)mAppName.get(),
+ mAppName.Length());
+
+ XChangeProperty(mozilla::DefaultXDisplay(), aWindowId, sMozProfileAtom,
+ XA_STRING, 8, PropModeReplace,
+ (unsigned char*)mProfileName.get(), mProfileName.Length());
+}
+
+bool nsXRemoteServer::HandleNewProperty(XID aWindowId, Display* aDisplay,
+ Time aEventTime, Atom aChangedAtom) {
+ if (aChangedAtom == sMozCommandLineAtom) {
+ // We got a new command atom.
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ char* data = 0;
+
+ result = XGetWindowProperty(aDisplay, aWindowId, aChangedAtom,
+ 0, /* long_offset */
+ (65536 / sizeof(long)), /* long_length */
+ X11True, /* atomic delete after */
+ XA_STRING, /* req_type */
+ &actual_type, /* actual_type return */
+ &actual_format, /* actual_format_return */
+ &nitems, /* nitems_return */
+ &bytes_after, /* bytes_after_return */
+ (unsigned char**)&data); /* prop_return
+ (we only care
+ about the first ) */
+
+ // Failed to get property off the window?
+ if (result != Success) return false;
+
+ // Failed to get the data off the window or it was the wrong type?
+ if (!data || !TO_LITTLE_ENDIAN32(*reinterpret_cast<int32_t*>(data)))
+ return false;
+
+ // cool, we got the property data.
+ const char* response = HandleCommandLine(data, aEventTime);
+
+ // put the property onto the window as the response
+ XChangeProperty(aDisplay, aWindowId, sMozResponseAtom, XA_STRING, 8,
+ PropModeReplace, (const unsigned char*)response,
+ strlen(response));
+ XFree(data);
+ return true;
+ }
+
+ if (aChangedAtom == sMozResponseAtom) {
+ // client accepted the response. party on wayne.
+ return true;
+ }
+
+ else if (aChangedAtom == sMozLockAtom) {
+ // someone locked the window
+ return true;
+ }
+
+ return false;
+}
+
+void nsXRemoteServer::EnsureAtoms(void) {
+ if (sMozVersionAtom) return;
+
+ XInternAtoms(mozilla::DefaultXDisplay(), const_cast<char**>(XAtomNames),
+ ArrayLength(XAtomNames), X11False, XAtoms);
+
+ int i = 0;
+ sMozVersionAtom = XAtoms[i++];
+ sMozLockAtom = XAtoms[i++];
+ sMozResponseAtom = XAtoms[i++];
+ sMozUserAtom = XAtoms[i++];
+ sMozProfileAtom = XAtoms[i++];
+ sMozProgramAtom = XAtoms[i++];
+ sMozCommandLineAtom = XAtoms[i];
+}
diff --git a/toolkit/components/remote/nsXRemoteServer.h b/toolkit/components/remote/nsXRemoteServer.h
new file mode 100644
index 0000000000..405bafe789
--- /dev/null
+++ b/toolkit/components/remote/nsXRemoteServer.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=8:
+ */
+/* 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 NSXREMOTESERVER_H
+#define NSXREMOTESERVER_H
+
+#include "nsString.h"
+#include "nsRemoteServer.h"
+#include "nsUnixRemoteServer.h"
+
+#include <X11/Xlib.h>
+#include <X11/X.h>
+
+/**
+ Base class for GTK/Qt remote service
+*/
+class nsXRemoteServer : public nsRemoteServer, public nsUnixRemoteServer {
+ protected:
+ nsXRemoteServer();
+ bool HandleNewProperty(Window aWindowId, Display* aDisplay, Time aEventTime,
+ Atom aChangedAtom);
+ void XRemoteBaseStartup(const char* aAppName, const char* aProfileName);
+ void HandleCommandsFor(Window aWindowId);
+
+ private:
+ void EnsureAtoms();
+
+ nsCString mAppName;
+ nsCString mProfileName;
+
+ static Atom sMozVersionAtom;
+ static Atom sMozLockAtom;
+ static Atom sMozResponseAtom;
+ static Atom sMozUserAtom;
+ static Atom sMozProfileAtom;
+ static Atom sMozProgramAtom;
+ static Atom sMozCommandLineAtom;
+};
+
+#endif // NSXREMOTESERVER_H
diff --git a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
new file mode 100644
index 0000000000..d68fe6c1ad
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
@@ -0,0 +1,178 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 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/.
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+// This object implements the JS parts of nsIWebNavigation.
+class RemoteWebNavigation {
+ constructor(aBrowser) {
+ this._browser = aBrowser;
+ this._cancelContentJSEpoch = 1;
+ this._currentURI = null;
+ this._canGoBack = false;
+ this._canGoForward = false;
+ this.referringURI = null;
+ }
+
+ swapBrowser(aBrowser) {
+ this._browser = aBrowser;
+ }
+
+ maybeCancelContentJSExecution(aNavigationType, aOptions = {}) {
+ const epoch = this._cancelContentJSEpoch++;
+ this._browser.frameLoader.remoteTab.maybeCancelContentJSExecution(
+ aNavigationType,
+ { ...aOptions, epoch }
+ );
+ return epoch;
+ }
+
+ get canGoBack() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ return this._browser.browsingContext.sessionHistory?.index > 0;
+ }
+ return this._canGoBack;
+ }
+
+ get canGoForward() {
+ if (Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = this._browser.browsingContext.sessionHistory;
+ return sessionHistory?.index < sessionHistory?.count - 1;
+ }
+ return this._canGoForward;
+ }
+
+ goBack(requireUserInteraction = false) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_BACK
+ );
+ this._browser.browsingContext.goBack(
+ cancelContentJSEpoch,
+ requireUserInteraction,
+ true
+ );
+ }
+ goForward(requireUserInteraction = false) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_FORWARD
+ );
+ this._browser.browsingContext.goForward(
+ cancelContentJSEpoch,
+ requireUserInteraction,
+ true
+ );
+ }
+ gotoIndex(aIndex) {
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_INDEX,
+ { index: aIndex }
+ );
+ this._browser.browsingContext.goToIndex(aIndex, cancelContentJSEpoch, true);
+ }
+ loadURI(aURI, aLoadURIOptions) {
+ let uri;
+ try {
+ let fixupFlags = Services.uriFixup.webNavigationFlagsToFixupFlags(
+ aURI,
+ aLoadURIOptions.loadFlags
+ );
+ let isBrowserPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(
+ this._browser
+ );
+ if (isBrowserPrivate) {
+ fixupFlags |= Services.uriFixup.FIXUP_FLAG_PRIVATE_CONTEXT;
+ }
+
+ uri = Services.uriFixup.getFixupURIInfo(aURI, fixupFlags).preferredURI;
+
+ // We know the url is going to be loaded, let's start requesting network
+ // connection before the content process asks.
+ // Note that we might have already setup the speculative connection in
+ // some cases, especially when the url is from location bar or its popup
+ // menu.
+ if (uri.schemeIs("http") || uri.schemeIs("https")) {
+ let principal = aLoadURIOptions.triggeringPrincipal;
+ // We usually have a triggeringPrincipal assigned, but in case we
+ // don't have one or if it's a SystemPrincipal, let's create it with OA
+ // inferred from the current context.
+ if (!principal || principal.isSystemPrincipal) {
+ let attrs = {
+ userContextId: this._browser.getAttribute("usercontextid") || 0,
+ privateBrowsingId: isBrowserPrivate ? 1 : 0,
+ };
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ attrs
+ );
+ }
+ Services.io.speculativeConnect(uri, principal, null);
+ }
+ } catch (ex) {
+ // Can't setup speculative connection for this uri string for some
+ // reason (such as failing to parse the URI), just ignore it.
+ }
+
+ let cancelContentJSEpoch = this.maybeCancelContentJSExecution(
+ Ci.nsIRemoteTab.NAVIGATE_URL,
+ { uri }
+ );
+ this._browser.browsingContext.loadURI(aURI, {
+ ...aLoadURIOptions,
+ cancelContentJSEpoch,
+ });
+ }
+ reload(aReloadFlags) {
+ this._browser.browsingContext.reload(aReloadFlags);
+ }
+ stop(aStopFlags) {
+ this._browser.browsingContext.stop(aStopFlags);
+ }
+
+ get document() {
+ return this._browser.contentDocument;
+ }
+
+ get currentURI() {
+ if (!this._currentURI) {
+ this._currentURI = Services.io.newURI("about:blank");
+ }
+ return this._currentURI;
+ }
+ set currentURI(aURI) {
+ // Bug 1498600 verify usages of systemPrincipal here
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ this.loadURI(aURI.spec, loadURIOptions);
+ }
+
+ // Bug 1233803 - accessing the sessionHistory of remote browsers should be
+ // done in content scripts.
+ get sessionHistory() {
+ throw new Components.Exception(
+ "Not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ set sessionHistory(aValue) {
+ throw new Components.Exception(
+ "Not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ _sendMessage(aMessage, aData) {
+ try {
+ this._browser.sendMessageToActor(aMessage, aData, "WebNavigation");
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+}
+
+var EXPORTED_SYMBOLS = ["RemoteWebNavigation"];
diff --git a/toolkit/components/remotebrowserutils/moz.build b/toolkit/components/remotebrowserutils/moz.build
new file mode 100644
index 0000000000..de7a083b1a
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/moz.build
@@ -0,0 +1,14 @@
+# -*- 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", "DOM: Navigation")
+
+EXTRA_JS_MODULES += [
+ "RemoteWebNavigation.jsm",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
diff --git a/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs
new file mode 100644
index 0000000000..1a28b8c5c3
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/307redirect.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 307, "Temporary Redirect");
+ let location = request.queryString;
+ response.setHeader("Location", location, false);
+ response.write("Hello world!");
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser.ini b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
new file mode 100644
index 0000000000..a7d0a4786d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+support-files =
+ dummy_page.html
+ print_postdata.sjs
+ 307redirect.sjs
+ head.js
+ coop_header.sjs
+ file_postmsg_parent.html
+
+[browser_RemoteWebNavigation.js]
+https_first_disabled = true
+[browser_documentChannel.js]
+[browser_httpCrossOriginOpenerPolicy.js]
+[browser_httpToFileHistory.js]
+[browser_oopProcessSwap.js]
+[browser_externalLinkBlanksPage.js]
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
new file mode 100644
index 0000000000..2de59f67a7
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_RemoteWebNavigation.js
@@ -0,0 +1,220 @@
+const SYSTEMPRINCIPAL = Services.scriptSecurityManager.getSystemPrincipal();
+const DUMMY1 =
+ "http://test1.example.org/browser/toolkit/modules/tests/browser/dummy_page.html";
+const DUMMY2 =
+ "http://test2.example.org/browser/toolkit/modules/tests/browser/dummy_page.html";
+const LOAD_URI_OPTIONS = { triggeringPrincipal: SYSTEMPRINCIPAL };
+
+function waitForLoad(uri) {
+ return BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, uri);
+}
+
+function waitForPageShow(browser = gBrowser.selectedBrowser) {
+ return BrowserTestUtils.waitForContentEvent(browser, "pageshow", true);
+}
+
+// Tests that loadURI accepts a referrer and it is included in the load.
+add_task(async function test_referrer() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ let loadURIOptionsWithReferrer = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ referrerInfo: new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(DUMMY2)
+ ),
+ };
+ browser.webNavigation.loadURI(DUMMY1, loadURIOptionsWithReferrer);
+ await waitForLoad(DUMMY1);
+
+ await SpecialPowers.spawn(browser, [[DUMMY1, DUMMY2]], function([
+ dummy1,
+ dummy2,
+ ]) {
+ function getExpectedReferrer(referrer) {
+ let defaultPolicy = Services.prefs.getIntPref(
+ "network.http.referer.defaultPolicy"
+ );
+ ok(
+ [2, 3].indexOf(defaultPolicy) > -1,
+ "default referrer policy should be either strict-origin-when-cross-origin(2) or no-referrer-when-downgrade(3)"
+ );
+ if (defaultPolicy == 2) {
+ return referrer.match(/https?:\/\/[^\/]+\/?/i)[0];
+ }
+ return referrer;
+ }
+
+ is(content.location.href, dummy1, "Should have loaded the right URL");
+ is(
+ content.document.referrer,
+ getExpectedReferrer(dummy2),
+ "Should have the right referrer"
+ );
+ });
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that remote access to webnavigation.sessionHistory works.
+add_task(async function test_history() {
+ async function checkHistoryIndex(browser, n) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(browser, [n], function(n) {
+ let history =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+
+ is(history.index, n, "Should be at the right place in history");
+ });
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+ is(history.index, n, "Should be at the right place in history");
+ return null;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(DUMMY1, LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY1);
+
+ browser.webNavigation.loadURI(DUMMY2, LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY2);
+
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ await SpecialPowers.spawn(browser, [[DUMMY1, DUMMY2]], function([
+ dummy1,
+ dummy2,
+ ]) {
+ let history = docShell.browsingContext.childSessionHistory.legacySHistory;
+
+ is(history.count, 2, "Should be two history items");
+ is(history.index, 1, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(0);
+ is(entry.URI.spec, dummy1, "Should have the right history entry");
+ entry = history.getEntryAtIndex(1);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ });
+ } else {
+ let history = browser.browsingContext.sessionHistory;
+
+ is(history.count, 2, "Should be two history items");
+ is(history.index, 1, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(0);
+ is(entry.URI.spec, DUMMY1, "Should have the right history entry");
+ entry = history.getEntryAtIndex(1);
+ is(entry.URI.spec, DUMMY2, "Should have the right history entry");
+ }
+
+ let promise = waitForPageShow();
+ browser.webNavigation.goBack();
+ await promise;
+ await checkHistoryIndex(browser, 0);
+
+ promise = waitForPageShow();
+ browser.webNavigation.goForward();
+ await promise;
+ await checkHistoryIndex(browser, 1);
+
+ promise = waitForPageShow();
+ browser.webNavigation.gotoIndex(0);
+ await promise;
+ await checkHistoryIndex(browser, 0);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that load flags are passed through to the content process.
+add_task(async function test_flags() {
+ async function checkHistory(browser, { count, index }) {
+ if (!SpecialPowers.Services.appinfo.sessionHistoryInParent) {
+ return SpecialPowers.spawn(browser, [[DUMMY2, count, index]], function([
+ dummy2,
+ count,
+ index,
+ ]) {
+ let history =
+ docShell.browsingContext.childSessionHistory.legacySHistory;
+ is(history.count, count, "Should be one history item");
+ is(history.index, index, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(index);
+ is(entry.URI.spec, dummy2, "Should have the right history entry");
+ });
+ }
+
+ let history = browser.browsingContext.sessionHistory;
+ is(history.count, count, "Should be one history item");
+ is(history.index, index, "Should be at the right place in history");
+ let entry = history.getEntryAtIndex(index);
+ is(entry.URI.spec, DUMMY2, "Should have the right history entry");
+
+ return null;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ browser.webNavigation.loadURI(DUMMY1, LOAD_URI_OPTIONS);
+ await waitForLoad(DUMMY1);
+ let loadURIOptionsReplaceHistory = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ };
+ browser.webNavigation.loadURI(DUMMY2, loadURIOptionsReplaceHistory);
+ await waitForLoad(DUMMY2);
+ await checkHistory(browser, { count: 1, index: 0 });
+ let loadURIOptionsBypassHistory = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ };
+ browser.webNavigation.loadURI(DUMMY1, loadURIOptionsBypassHistory);
+ await waitForLoad(DUMMY1);
+ await checkHistory(browser, { count: 1, index: 0 });
+
+ gBrowser.removeCurrentTab();
+});
+
+// Tests that attempts to use unsupported arguments throw an exception.
+add_task(async function test_badarguments() {
+ if (!gMultiProcessBrowser) {
+ return;
+ }
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ let browser = gBrowser.selectedBrowser;
+
+ try {
+ let loadURIOptionsBadPostData = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ postData: {},
+ };
+ browser.webNavigation.loadURI(DUMMY1, loadURIOptionsBadPostData);
+ ok(
+ false,
+ "Should have seen an exception from trying to pass some postdata"
+ );
+ } catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some postdata");
+ }
+
+ try {
+ let loadURIOptionsBadHeader = {
+ triggeringPrincipal: SYSTEMPRINCIPAL,
+ headers: {},
+ };
+ browser.webNavigation.loadURI(DUMMY1, loadURIOptionsBadHeader);
+ ok(false, "Should have seen an exception from trying to pass some headers");
+ } catch (e) {
+ ok(true, "Should have seen an exception from trying to pass some headers");
+ }
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js
new file mode 100644
index 0000000000..50a39d5d68
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_documentChannel.js
@@ -0,0 +1,284 @@
+/* eslint-env webextensions */
+"use strict";
+
+const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+
+const PRINT_POSTDATA = httpURL("print_postdata.sjs");
+const FILE_DUMMY = fileURL("dummy_page.html");
+const DATA_URL = "data:text/html,Hello%2C World!";
+const DATA_STRING = "Hello, World!";
+
+async function performLoad(browser, opts, action) {
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ opts.url,
+ opts.maybeErrorPage
+ );
+ await action();
+ await loadedPromise;
+}
+
+const EXTENSION_DATA = {
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+
+ permissions: ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
+ },
+
+ files: {
+ "dummy.html": "<html>webext dummy</html>",
+ "redirect.html": "<html>webext redirect</html>",
+ },
+
+ extUrl: "",
+
+ async background() {
+ browser.test.log("background script running");
+ browser.webRequest.onAuthRequired.addListener(
+ async details => {
+ browser.test.log("webRequest onAuthRequired");
+
+ // A blocking request that returns a promise exercises a codepath that
+ // sets the notificationCallbacks on the channel to a JS object that we
+ // can't do directly QueryObject on with expected results.
+ // This triggered a crash which was fixed in bug 1528188.
+ return new Promise((resolve, reject) => {
+ setTimeout(resolve, 0);
+ });
+ },
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ browser.webRequest.onBeforeRequest.addListener(
+ async details => {
+ browser.test.log("webRequest onBeforeRequest");
+ let isRedirect =
+ details.originUrl == browser.runtime.getURL("redirect.html") &&
+ details.url.endsWith("print_postdata.sjs");
+ let url = this.extUrl ? this.extUrl : details.url + "?redirected";
+ return isRedirect ? { redirectUrl: url } : {};
+ },
+ { urls: ["*://*/*"] },
+ ["blocking"]
+ );
+ browser.test.onMessage.addListener(async ({ method, url }) => {
+ if (method == "setRedirectUrl") {
+ this.extUrl = url;
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+};
+
+async function withExtensionDummy(callback) {
+ let extension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
+ await extension.startup();
+ let rv = await callback(`moz-extension://${extension.uuid}/`, extension);
+ await extension.unload();
+ return rv;
+}
+
+async function postFrom(start, target) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ },
+ async function(browser) {
+ info("Test tab ready: postFrom " + start);
+
+ // Create the form element in our loaded URI.
+ await SpecialPowers.spawn(browser, [{ target }], function({ target }) {
+ // eslint-disable-next-line no-unsanitized/property
+ content.document.body.innerHTML = `
+ <form method="post" action="${target}">
+ <input type="text" name="initialRemoteType" value="${Services.appinfo.remoteType}">
+ <input type="submit" id="submit">
+ </form>`;
+ });
+
+ // Perform a form POST submit load.
+ info("Performing POST submission");
+ await performLoad(
+ browser,
+ {
+ url(url) {
+ let enable =
+ url.startsWith(PRINT_POSTDATA) ||
+ url == target ||
+ url == DATA_URL;
+ if (!enable) {
+ info(`url ${url} is invalid to perform load`);
+ }
+ return enable;
+ },
+ maybeErrorPage: true,
+ },
+ async () => {
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.querySelector("#submit").click();
+ });
+ }
+ );
+
+ // Check that the POST data was submitted.
+ info("Fetching results");
+ return SpecialPowers.spawn(browser, [], () => {
+ return {
+ remoteType: Services.appinfo.remoteType,
+ location: "" + content.location.href,
+ body: content.document.body.textContent,
+ };
+ });
+ }
+ );
+}
+
+async function loadAndGetProcessID(browser, target) {
+ info(`Performing GET load: ${target}`);
+ await performLoad(
+ browser,
+ {
+ maybeErrorPage: true,
+ },
+ () => {
+ BrowserTestUtils.loadURI(browser, target);
+ }
+ );
+
+ info(`Navigated to: ${target}`);
+ browser = gBrowser.selectedBrowser;
+ let processID = await SpecialPowers.spawn(browser, [], () => {
+ return Services.appinfo.processID;
+ });
+ return processID;
+}
+
+async function testLoadAndRedirect(
+ target,
+ expectedProcessSwitch,
+ testRedirect
+) {
+ let start = httpURL(`dummy_page.html`);
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ },
+ async function(_browser) {
+ info("Test tab ready: getFrom " + start);
+
+ let browser = gBrowser.selectedBrowser;
+ let firstProcessID = await SpecialPowers.spawn(browser, [], () => {
+ return Services.appinfo.processID;
+ });
+
+ info(`firstProcessID: ${firstProcessID}`);
+
+ let secondProcessID = await loadAndGetProcessID(browser, target);
+
+ info(`secondProcessID: ${secondProcessID}`);
+ Assert.equal(firstProcessID != secondProcessID, expectedProcessSwitch);
+
+ if (!testRedirect) {
+ return;
+ }
+
+ let thirdProcessID = await loadAndGetProcessID(browser, add307(target));
+
+ info(`thirdProcessID: ${thirdProcessID}`);
+ Assert.equal(firstProcessID != thirdProcessID, expectedProcessSwitch);
+ Assert.ok(secondProcessID == thirdProcessID);
+ }
+ );
+}
+
+add_task(async function test_enabled() {
+ // Force only one webIsolated content process to ensure same-origin loads
+ // always end in the same process.
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processCount.webIsolated", 1]],
+ });
+
+ // URIs should correctly switch processes & the POST
+ // should succeed.
+ info("ENABLED -- FILE -- raw URI load");
+ let resp = await postFrom(FILE_DUMMY, PRINT_POSTDATA);
+ ok(E10SUtils.isWebRemoteType(resp.remoteType), "process switch");
+ is(resp.location, PRINT_POSTDATA, "correct location");
+ is(resp.body, "initialRemoteType=file", "correct POST body");
+
+ info("ENABLED -- FILE -- 307-redirect URI load");
+ let resp307 = await postFrom(FILE_DUMMY, add307(PRINT_POSTDATA));
+ ok(E10SUtils.isWebRemoteType(resp307.remoteType), "process switch");
+ is(resp307.location, PRINT_POSTDATA, "correct location");
+ is(resp307.body, "initialRemoteType=file", "correct POST body");
+
+ // Same with extensions
+ await withExtensionDummy(async extOrigin => {
+ info("ENABLED -- EXTENSION -- raw URI load");
+ let respExt = await postFrom(extOrigin + "dummy.html", PRINT_POSTDATA);
+ ok(E10SUtils.isWebRemoteType(respExt.remoteType), "process switch");
+ is(respExt.location, PRINT_POSTDATA, "correct location");
+ is(respExt.body, "initialRemoteType=extension", "correct POST body");
+
+ info("ENABLED -- EXTENSION -- extension-redirect URI load");
+ let respExtRedirect = await postFrom(
+ extOrigin + "redirect.html",
+ PRINT_POSTDATA
+ );
+ ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch");
+ is(
+ respExtRedirect.location,
+ PRINT_POSTDATA + "?redirected",
+ "correct location"
+ );
+ is(
+ respExtRedirect.body,
+ "initialRemoteType=extension?redirected",
+ "correct POST body"
+ );
+
+ info("ENABLED -- EXTENSION -- 307-redirect URI load");
+ let respExt307 = await postFrom(
+ extOrigin + "dummy.html",
+ add307(PRINT_POSTDATA)
+ );
+ ok(E10SUtils.isWebRemoteType(respExt307.remoteType), "process switch");
+ is(respExt307.location, PRINT_POSTDATA, "correct location");
+ is(respExt307.body, "initialRemoteType=extension", "correct POST body");
+ });
+});
+
+async function sendMessage(ext, method, url) {
+ ext.sendMessage({ method, url });
+ await ext.awaitMessage("done");
+}
+
+// TODO: Currently no test framework for ftp://.
+add_task(async function test_protocol() {
+ // TODO: Processes should be switched due to navigation of different origins.
+ await testLoadAndRedirect("data:,foo", false, true);
+
+ // Redirecting to file:// is not allowed.
+ await testLoadAndRedirect(FILE_DUMMY, true, false);
+
+ await withExtensionDummy(async (extOrigin, extension) => {
+ await sendMessage(extension, "setRedirectUrl", DATA_URL);
+
+ let respExtRedirect = await postFrom(
+ extOrigin + "redirect.html",
+ PRINT_POSTDATA
+ );
+
+ ok(E10SUtils.isWebRemoteType(respExtRedirect.remoteType), "process switch");
+ is(respExtRedirect.location, DATA_URL, "correct location");
+ is(respExtRedirect.body, DATA_STRING, "correct POST body");
+ });
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js
new file mode 100644
index 0000000000..0485018fec
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_externalLinkBlanksPage.js
@@ -0,0 +1,87 @@
+/*
+ * Test that following a link with a scheme that opens externally (like
+ * irc:) does not blank the page (Bug 1630757).
+ */
+
+const { HandlerServiceTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/HandlerServiceTestUtils.sys.mjs"
+);
+
+let gHandlerService = Cc["@mozilla.org/uriloader/handler-service;1"].getService(
+ Ci.nsIHandlerService
+);
+
+let Pages = [httpURL("dummy_page.html"), fileURL("dummy_page.html")];
+
+/**
+ * Creates dummy protocol handler
+ */
+function initTestHandlers() {
+ let handlerInfo = HandlerServiceTestUtils.getBlankHandlerInfo(
+ "test-proto://"
+ );
+
+ let appHandler = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ // This is a dir and not executable, but that's enough for here.
+ appHandler.executable = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ handlerInfo.possibleApplicationHandlers.appendElement(appHandler);
+ handlerInfo.preferredApplicationHandler = appHandler;
+ handlerInfo.preferredAction = handlerInfo.useHelperApp;
+ handlerInfo.alwaysAskBeforeHandling = false;
+ gHandlerService.store(handlerInfo);
+
+ registerCleanupFunction(() => {
+ gHandlerService.remove(handlerInfo);
+ });
+}
+
+async function runTest() {
+ initTestHandlers();
+
+ for (let downloadPrefValue of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.download.improvements_to_download_panel", downloadPrefValue],
+ ],
+ });
+ for (let page of Pages) {
+ await BrowserTestUtils.withNewTab(page, async function(aBrowser) {
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let h = content.document.createElement("h1");
+ ok(h);
+ h.innerHTML = "My heading";
+ h.id = "my-heading";
+ content.document.body.append(h);
+ is(content.document.getElementById("my-heading"), h, "h exists");
+
+ let a = content.document.createElement("a");
+ ok(a);
+ a.innerHTML = "my link";
+ a.id = "my-link";
+ content.document.body.append(a);
+ });
+
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ let url = "test-proto://some-thing";
+
+ let a = content.document.getElementById("my-link");
+ ok(a);
+ a.href = url;
+ a.click();
+ });
+
+ await SpecialPowers.spawn(aBrowser, [], async () => {
+ ok(
+ content.document.getElementById("my-heading"),
+ "Page contents not erased"
+ );
+ });
+ });
+ }
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+add_task(runTest);
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
new file mode 100644
index 0000000000..52652001b6
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpCrossOriginOpenerPolicy.js
@@ -0,0 +1,414 @@
+"use strict";
+
+const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+
+const COOP_PREF = "browser.tabs.remote.useCrossOriginOpenerPolicy";
+
+async function setPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[COOP_PREF, true]],
+ });
+}
+
+async function unsetPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[COOP_PREF, false]],
+ });
+}
+
+function httpURL(filename, host = "https://example.com") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ host
+ );
+ return root + filename;
+}
+
+async function performLoad(browser, opts, action) {
+ let loadedPromise = BrowserTestUtils.browserStopped(
+ browser,
+ opts.url,
+ opts.maybeErrorPage
+ );
+ await action();
+ await loadedPromise;
+}
+
+async function test_coop(
+ start,
+ target,
+ expectedProcessSwitch,
+ startRemoteTypeCheck,
+ targetRemoteTypeCheck
+) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: start,
+ waitForStateStop: true,
+ },
+ async function(_browser) {
+ info(`test_coop: Test tab ready: ${start}`);
+
+ let browser = gBrowser.selectedBrowser;
+ let firstRemoteType = browser.remoteType;
+ let firstBC = browser.browsingContext;
+
+ info(`firstBC: ${firstBC.id} remoteType: ${firstRemoteType}`);
+
+ if (startRemoteTypeCheck) {
+ startRemoteTypeCheck(firstRemoteType);
+ }
+
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURI(browser, target)
+ );
+
+ info(`Navigated to: ${target}`);
+ browser = gBrowser.selectedBrowser;
+ let secondRemoteType = browser.remoteType;
+ let secondBC = browser.browsingContext;
+
+ info(`secondBC: ${secondBC.id} remoteType: ${secondRemoteType}`);
+ if (targetRemoteTypeCheck) {
+ targetRemoteTypeCheck(secondRemoteType);
+ }
+ if (expectedProcessSwitch) {
+ Assert.notEqual(firstBC.id, secondBC.id, `from: ${start} to ${target}`);
+ } else {
+ Assert.equal(firstBC.id, secondBC.id, `from: ${start} to ${target}`);
+ }
+ }
+ );
+}
+
+function waitForDownloadWindow() {
+ return new Promise(resolve => {
+ var listener = {
+ onOpenWindow: aXULWindow => {
+ info("Download window shown...");
+ Services.wm.removeListener(listener);
+
+ function downloadOnLoad() {
+ domwindow.removeEventListener("load", downloadOnLoad, true);
+
+ is(
+ domwindow.document.location.href,
+ "chrome://mozapps/content/downloads/unknownContentType.xhtml",
+ "Download page appeared"
+ );
+ resolve(domwindow);
+ }
+
+ var domwindow = aXULWindow.docShell.domWindow;
+ domwindow.addEventListener("load", downloadOnLoad, true);
+ },
+ onCloseWindow: aXULWindow => {},
+ };
+
+ Services.wm.addListener(listener);
+ });
+}
+
+async function waitForDownloadUI() {
+ return BrowserTestUtils.waitForEvent(DownloadsPanel.panel, "popupshown");
+}
+
+async function cleanupDownloads(downloadList) {
+ info("cleaning up downloads");
+ let [download] = await downloadList.getAll();
+ await downloadList.remove(download);
+ await download.finalize(true);
+
+ try {
+ if (Services.appinfo.OS === "WINNT") {
+ // We need to make the file writable to delete it on Windows.
+ await IOUtils.setPermissions(download.target.path, 0o600);
+ }
+ await IOUtils.remove(download.target.path);
+ } catch (error) {
+ info("The file " + download.target.path + " is not removed, " + error);
+ }
+
+ if (DownloadsPanel.panel.state !== "closed") {
+ let hiddenPromise = BrowserTestUtils.waitForEvent(
+ DownloadsPanel.panel,
+ "popuphidden"
+ );
+ DownloadsPanel.hidePanel();
+ await hiddenPromise;
+ }
+ is(
+ DownloadsPanel.panel.state,
+ "closed",
+ "Check that the download panel is closed"
+ );
+}
+
+async function test_download_from(initCoop, downloadCoop) {
+ return BrowserTestUtils.withNewTab("about:blank", async function(_browser) {
+ info(`test_download: Test tab ready`);
+
+ let start = httpURL(
+ "coop_header.sjs?downloadPage&coop=" + initCoop,
+ "https://example.com"
+ );
+ await performLoad(
+ _browser,
+ {
+ url: start,
+ maybeErrorPage: false,
+ },
+ async () => {
+ info(`test_download: Loading download page ${start}`);
+ return BrowserTestUtils.loadURI(_browser, start);
+ }
+ );
+
+ info(`test_download: Download page ready ${start}`);
+ info(`Downloading ${downloadCoop}`);
+
+ let expectDialog = Services.prefs.getBoolPref(
+ "browser.download.always_ask_before_handling_new_types",
+ false
+ );
+ let resultPromise = expectDialog
+ ? waitForDownloadWindow()
+ : waitForDownloadUI();
+ let browser = gBrowser.selectedBrowser;
+ SpecialPowers.spawn(browser, [downloadCoop], downloadCoop => {
+ content.document.getElementById(downloadCoop).click();
+ });
+
+ // if the download page doesn't appear, the promise leads a timeout.
+ if (expectDialog) {
+ let win = await resultPromise;
+ await BrowserTestUtils.closeWindow(win);
+ } else {
+ // verify link target will get automatically downloaded
+ await resultPromise;
+ let downloadList = await Downloads.getList(Downloads.PUBLIC);
+ is((await downloadList.getAll()).length, 1, "Target was downloaded");
+ await cleanupDownloads(downloadList);
+ is((await downloadList.getAll()).length, 0, "Downloads were cleaned up");
+ }
+ });
+}
+
+// Check that multiple navigations of the same tab will only switch processes
+// when it's expected.
+add_task(async function test_multiple_nav_process_switches() {
+ await setPref();
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: httpURL("coop_header.sjs", "https://example.org"),
+ waitForStateStop: true,
+ },
+ async function(browser) {
+ let prevBC = browser.browsingContext;
+
+ let target = httpURL("coop_header.sjs?.", "https://example.org");
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURI(browser, target)
+ );
+
+ Assert.equal(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin",
+ "https://example.org"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURI(browser, target)
+ );
+
+ Assert.notEqual(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin",
+ "https://example.com"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURI(browser, target)
+ );
+
+ Assert.notEqual(prevBC, browser.browsingContext);
+ prevBC = browser.browsingContext;
+
+ target = httpURL(
+ "coop_header.sjs?coop=same-origin&index=4",
+ "https://example.com"
+ );
+ await performLoad(
+ browser,
+ {
+ url: target,
+ maybeErrorPage: false,
+ },
+ async () => BrowserTestUtils.loadURI(browser, target)
+ );
+
+ Assert.equal(prevBC, browser.browsingContext);
+ }
+ );
+});
+
+add_task(async function test_disabled() {
+ await unsetPref();
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs", "https://example.com"),
+ false
+ );
+ await test_coop(
+ httpURL("coop_header.sjs?coop=same-origin", "http://example.com"),
+ httpURL("coop_header.sjs", "http://example.com"),
+ false
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "http://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin", "http://example.com"),
+ false
+ );
+});
+
+add_task(async function test_enabled() {
+ await setPref();
+
+ function checkIsCoopRemoteType(remoteType) {
+ Assert.ok(
+ remoteType.startsWith(E10SUtils.WEB_REMOTE_COOP_COEP_TYPE_PREFIX),
+ `${remoteType} expected to be coop`
+ );
+ }
+
+ function checkIsNotCoopRemoteType(remoteType) {
+ if (gFissionBrowser) {
+ Assert.ok(
+ remoteType.startsWith("webIsolated="),
+ `${remoteType} expected to start with webIsolated=`
+ );
+ } else {
+ Assert.equal(
+ remoteType,
+ E10SUtils.WEB_REMOTE_TYPE,
+ `${remoteType} expected to be web`
+ );
+ }
+ }
+
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs", "https://example.com"),
+ false,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin", "https://example.org"),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.com"),
+ httpURL("coop_header.sjs?coop=same-origin&index=1", "https://example.org"),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL("coop_header.sjs", "https://example.com"),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp",
+ "https://example.com"
+ ),
+ true,
+ checkIsNotCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=2",
+ "https://example.com"
+ ),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=3",
+ "https://example.com"
+ ),
+ false,
+ checkIsCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=4",
+ "https://example.com"
+ ),
+ httpURL("coop_header.sjs", "https://example.com"),
+ true,
+ checkIsCoopRemoteType,
+ checkIsNotCoopRemoteType
+ );
+ await test_coop(
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=5",
+ "https://example.com"
+ ),
+ httpURL(
+ "coop_header.sjs?coop=same-origin&coep=require-corp&index=6",
+ "https://example.org"
+ ),
+ true,
+ checkIsCoopRemoteType,
+ checkIsCoopRemoteType
+ );
+});
+
+add_task(async function test_download() {
+ requestLongerTimeout(4);
+ await setPref();
+
+ let initCoopArray = ["", "same-origin"];
+
+ let downloadCoopArray = [
+ "no-coop",
+ "same-origin",
+ "same-origin-allow-popups",
+ ];
+
+ // If the coop mismatch between current page and download link, clicking the
+ // download link will make the page empty and popup the download window. That
+ // forces us to reload the page every time.
+ for (var initCoop of initCoopArray) {
+ for (var downloadCoop of downloadCoopArray) {
+ await test_download_from(initCoop, downloadCoop);
+ }
+ }
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js
new file mode 100644
index 0000000000..74991b348d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_httpToFileHistory.js
@@ -0,0 +1,119 @@
+const { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+
+const HISTORY = [
+ { url: httpURL("dummy_page.html") },
+ { url: fileURL("dummy_page.html") },
+ { url: httpURL("dummy_page.html") },
+];
+
+function reversed(list) {
+ let copy = list.slice();
+ copy.reverse();
+ return copy;
+}
+
+function butLast(list) {
+ return list.slice(0, -1);
+}
+
+async function runTest() {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async function(aBrowser) {
+ // Perform initial load of each URL in the history.
+ let count = 0;
+ let index = -1;
+ for (let { url } of HISTORY) {
+ BrowserTestUtils.loadURI(aBrowser, url);
+
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ count++;
+ index++;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(
+ docShell.sessionHistory.count,
+ count,
+ "Initial Navigation Count Match"
+ );
+ is(
+ docShell.sessionHistory.index,
+ index,
+ "Initial Navigation Index Match"
+ );
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Initial Navigation URL Scheme");
+ }
+ );
+ }
+
+ // Go back to the first entry.
+ for (let { url } of reversed(HISTORY).slice(1)) {
+ SpecialPowers.spawn(aBrowser, [], () => {
+ content.history.back();
+ });
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ index--;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(docShell.sessionHistory.count, count, "Go Back Count Match");
+ is(docShell.sessionHistory.index, index, "Go Back Index Match");
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Go Back URL Scheme");
+ }
+ );
+ }
+
+ // Go forward to the last entry.
+ for (let { url } of HISTORY.slice(1)) {
+ SpecialPowers.spawn(aBrowser, [], () => {
+ content.history.forward();
+ });
+ await BrowserTestUtils.browserLoaded(aBrowser, false, loaded => {
+ return (
+ Services.io.newURI(loaded).scheme == Services.io.newURI(url).scheme
+ );
+ });
+
+ index++;
+ await SpecialPowers.spawn(
+ aBrowser,
+ [{ count, index, url }],
+ async function({ count, index, url }) {
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+
+ is(docShell.sessionHistory.count, count, "Go Forward Count Match");
+ is(docShell.sessionHistory.index, index, "Go Forward Index Match");
+
+ let real = Services.io.newURI(content.location.href);
+ let expect = Services.io.newURI(url);
+ is(real.scheme, expect.scheme, "Go Forward URL Scheme");
+ }
+ );
+ }
+ });
+}
+
+add_task(runTest);
diff --git a/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js
new file mode 100644
index 0000000000..ccc8e7ef57
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/browser_oopProcessSwap.js
@@ -0,0 +1,161 @@
+add_task(async function oopProcessSwap() {
+ const FILE = fileURL("dummy_page.html");
+ const WEB = httpURL("file_postmsg_parent.html");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: FILE },
+ async browser => {
+ is(browser.browsingContext.children.length, 0);
+
+ info("creating an in-process frame");
+ let frameId = await SpecialPowers.spawn(
+ browser,
+ [{ FILE }],
+ async ({ FILE }) => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", FILE);
+ content.document.body.appendChild(iframe);
+
+ // The nested URI should be same-process
+ ok(iframe.browsingContext.docShell, "Should be in-process");
+
+ return iframe.browsingContext.id;
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ info("navigating to x-process frame");
+ let oopinfo = await SpecialPowers.spawn(
+ browser,
+ [{ WEB }],
+ async ({ WEB }) => {
+ let iframe = content.document.querySelector("iframe");
+
+ iframe.contentWindow.location = WEB;
+
+ let data = await new Promise(resolve => {
+ content.window.addEventListener(
+ "message",
+ function(evt) {
+ info("oop iframe loaded");
+ is(evt.source, iframe.contentWindow);
+ resolve(evt.data);
+ },
+ { once: true }
+ );
+ });
+
+ is(iframe.browsingContext.docShell, null, "Should be out-of-process");
+ is(
+ iframe.browsingContext.embedderElement,
+ iframe,
+ "correct embedder"
+ );
+
+ return {
+ location: data.location,
+ browsingContextId: iframe.browsingContext.id,
+ };
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) {
+ is(
+ frameId,
+ oopinfo.browsingContextId,
+ `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})`
+ );
+ }
+ is(oopinfo.location, WEB, "correct location");
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function oopOriginProcessSwap() {
+ const COM_DUMMY = httpURL("dummy_page.html", "https://example.com/");
+ const ORG_POSTMSG = httpURL(
+ "file_postmsg_parent.html",
+ "https://example.org/"
+ );
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: COM_DUMMY },
+ async browser => {
+ is(browser.browsingContext.children.length, 0);
+
+ info("creating an in-process frame");
+ let frameId = await SpecialPowers.spawn(
+ browser,
+ [{ COM_DUMMY }],
+ async ({ COM_DUMMY }) => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", COM_DUMMY);
+ content.document.body.appendChild(iframe);
+
+ // The nested URI should be same-process
+ ok(iframe.browsingContext.docShell, "Should be in-process");
+
+ return iframe.browsingContext.id;
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+
+ info("navigating to x-process frame");
+ let oopinfo = await SpecialPowers.spawn(
+ browser,
+ [{ ORG_POSTMSG }],
+ async ({ ORG_POSTMSG }) => {
+ let iframe = content.document.querySelector("iframe");
+
+ iframe.contentWindow.location = ORG_POSTMSG;
+
+ let data = await new Promise(resolve => {
+ content.window.addEventListener(
+ "message",
+ function(evt) {
+ info("oop iframe loaded");
+ is(evt.source, iframe.contentWindow);
+ resolve(evt.data);
+ },
+ { once: true }
+ );
+ });
+
+ is(iframe.browsingContext.docShell, null, "Should be out-of-process");
+ is(
+ iframe.browsingContext.embedderElement,
+ iframe,
+ "correct embedder"
+ );
+
+ return {
+ location: data.location,
+ browsingContextId: iframe.browsingContext.id,
+ };
+ }
+ );
+
+ is(browser.browsingContext.children.length, 1);
+ if (Services.prefs.getBoolPref("fission.preserve_browsing_contexts")) {
+ is(
+ frameId,
+ oopinfo.browsingContextId,
+ `BrowsingContext should not have changed (${frameId} != ${oopinfo.browsingContextId})`
+ );
+ }
+ is(oopinfo.location, ORG_POSTMSG, "correct location");
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
new file mode 100644
index 0000000000..c6b537d770
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs
@@ -0,0 +1,58 @@
+function handleRequest(request, response) {
+ Cu.importGlobalProperties(["URLSearchParams"]);
+ let query = new URLSearchParams(request.queryString);
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // The tests for Cross-Origin-Opener-Policy unfortunately depend on
+ // BFCacheInParent not kicking in, as with that enabled, it is not possible to
+ // tell whether the BrowsingContext switch was caused by the BFCache
+ // navigation or by the COOP mismatch. This header disables BFCache for the
+ // coop documents, and should avoid the issue.
+ response.setHeader("Cache-Control", "no-store", false);
+
+ let isDownloadPage = false;
+ let isDownloadFile = false;
+
+ query.forEach((value, name) => {
+ if (name === "downloadPage") {
+ isDownloadPage = true;
+ } else if (name === "downloadFile") {
+ isDownloadFile = true;
+ } else if (name == "coop") {
+ response.setHeader("Cross-Origin-Opener-Policy", unescape(value), false);
+ } else if (name == "coep") {
+ response.setHeader(
+ "Cross-Origin-Embedder-Policy",
+ unescape(value),
+ false
+ );
+ }
+ });
+
+ let downloadHTML = "";
+ if (isDownloadPage) {
+ ["no-coop", "same-origin", "same-origin-allow-popups"].forEach(coop => {
+ downloadHTML +=
+ '<a href="https://example.com/browser/toolkit/components/remotebrowserutils/tests/browser/coop_header.sjs?downloadFile&' +
+ (coop === "no-coop" ? "" : coop) +
+ '" id="' +
+ coop +
+ '" download>' +
+ unescape(coop) +
+ "</a> <br>";
+ });
+ }
+
+ if (isDownloadFile) {
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ response.write("BINARY_DATA");
+ } else {
+ response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+ response.write(
+ "<!DOCTYPE html><html><body><p>Hello world</p> " +
+ downloadHTML +
+ "</body></html>"
+ );
+ }
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
new file mode 100644
index 0000000000..8205e90d5d
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/dummy_page.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<html>
+<body>
+<p>Page</p>
+ <script>
+ // Prevent this page from being stored in the bfcache for the
+ // browser_httpToFileHistory.js test
+ window.blockBFCache = new RTCPeerConnection();
+ </script>
+</body>
+</html>
diff --git a/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html
new file mode 100644
index 0000000000..d5f640775b
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/file_postmsg_parent.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<body onload="parent.postMessage({location: window.location.href}, '*')">
+</body>
+</html>
diff --git a/toolkit/components/remotebrowserutils/tests/browser/head.js b/toolkit/components/remotebrowserutils/tests/browser/head.js
new file mode 100644
index 0000000000..ffdd9375cf
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/head.js
@@ -0,0 +1,17 @@
+function fileURL(filename) {
+ let ifile = getChromeDir(getResolvedURI(gTestPath));
+ ifile.append(filename);
+ return Services.io.newFileURI(ifile).spec;
+}
+
+function httpURL(filename, host = "https://example.com/") {
+ let root = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ host
+ );
+ return root + filename;
+}
+
+function add307(url, host = "https://example.com/") {
+ return httpURL("307redirect.sjs?" + url, host);
+}
diff --git a/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs
new file mode 100644
index 0000000000..bd9c18f7ea
--- /dev/null
+++ b/toolkit/components/remotebrowserutils/tests/browser/print_postdata.sjs
@@ -0,0 +1,28 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/plain", false);
+ if (request.method == "GET") {
+ response.write(request.queryString);
+ } else {
+ var body = new BinaryInputStream(request.bodyInputStream);
+
+ var avail;
+ var bytes = [];
+
+ while ((avail = body.available()) > 0) {
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+ }
+
+ var data = String.fromCharCode.apply(null, bytes);
+ if (request.queryString) {
+ data = data + "?" + request.queryString;
+ }
+ response.bodyOutputStream.write(data, data.length);
+ }
+}
diff --git a/toolkit/components/remotepagemanager/MessagePort.jsm b/toolkit/components/remotepagemanager/MessagePort.jsm
new file mode 100644
index 0000000000..5de2b78a33
--- /dev/null
+++ b/toolkit/components/remotepagemanager/MessagePort.jsm
@@ -0,0 +1,280 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MessagePort", "MessageListener"];
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
+});
+
+class MessageListener {
+ constructor() {
+ this.listeners = new Map();
+ }
+
+ keys() {
+ return this.listeners.keys();
+ }
+
+ has(name) {
+ return this.listeners.has(name);
+ }
+
+ callListeners(message) {
+ let listeners = this.listeners.get(message.name);
+ if (!listeners) {
+ return;
+ }
+
+ for (let listener of listeners.values()) {
+ try {
+ listener(message);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+
+ addMessageListener(name, callback) {
+ if (!this.listeners.has(name)) {
+ this.listeners.set(name, new Set([callback]));
+ } else {
+ this.listeners.get(name).add(callback);
+ }
+ }
+
+ removeMessageListener(name, callback) {
+ if (!this.listeners.has(name)) {
+ return;
+ }
+
+ this.listeners.get(name).delete(callback);
+ }
+}
+
+/*
+ * A message port sits on each side of the process boundary for every remote
+ * page. Each has a port ID that is unique to the message manager it talks
+ * through.
+ *
+ * We roughly implement the same contract as nsIMessageSender and
+ * nsIMessageListenerManager
+ */
+class MessagePort {
+ constructor(messageManagerOrActor, portID) {
+ this.messageManager = messageManagerOrActor;
+ this.portID = portID;
+ this.destroyed = false;
+ this.listener = new MessageListener();
+
+ // This is a sparse array of pending requests. The id of each request is
+ // simply its index in the array. The next id is the current length of the
+ // array (which includes the count of missing indexes).
+ this.requests = [];
+
+ this.message = this.message.bind(this);
+ this.receiveRequest = this.receiveRequest.bind(this);
+ this.receiveResponse = this.receiveResponse.bind(this);
+ this.addMessageListeners();
+ }
+
+ addMessageListeners() {
+ if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
+ return;
+ }
+
+ this.messageManager.addMessageListener("RemotePage:Message", this.message);
+ this.messageManager.addMessageListener(
+ "RemotePage:Request",
+ this.receiveRequest
+ );
+ this.messageManager.addMessageListener(
+ "RemotePage:Response",
+ this.receiveResponse
+ );
+ }
+
+ removeMessageListeners() {
+ if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
+ return;
+ }
+
+ this.messageManager.removeMessageListener(
+ "RemotePage:Message",
+ this.message
+ );
+ this.messageManager.removeMessageListener(
+ "RemotePage:Request",
+ this.receiveRequest
+ );
+ this.messageManager.removeMessageListener(
+ "RemotePage:Response",
+ this.receiveResponse
+ );
+ }
+
+ // Called when the message manager used to connect to the other process has
+ // changed, i.e. when a tab is detached.
+ swapMessageManager(messageManager) {
+ this.removeMessageListeners();
+ this.messageManager = messageManager;
+ this.addMessageListeners();
+ }
+
+ // Sends a request to the other process and returns a promise that completes
+ // once the other process has responded to the request or some error occurs.
+ sendRequest(name, data = null) {
+ if (this.destroyed) {
+ return this.window.Promise.reject(
+ new Error("Message port has been destroyed")
+ );
+ }
+
+ let deferred = lazy.PromiseUtils.defer();
+ this.requests.push(deferred);
+
+ this.messageManager.sendAsyncMessage("RemotePage:Request", {
+ portID: this.portID,
+ requestID: this.requests.length - 1,
+ name,
+ data,
+ });
+
+ return this.wrapPromise(deferred.promise);
+ }
+
+ // Handles an IPC message to perform a request of some kind.
+ async receiveRequest({ data: messagedata }) {
+ if (this.destroyed || messagedata.portID != this.portID) {
+ return;
+ }
+
+ let data = {
+ portID: this.portID,
+ requestID: messagedata.requestID,
+ };
+
+ try {
+ data.resolve = await this.handleRequest(
+ messagedata.name,
+ messagedata.data
+ );
+ } catch (e) {
+ data.reject = e;
+ }
+
+ this.messageManager.sendAsyncMessage("RemotePage:Response", data);
+ }
+
+ // Handles an IPC message with the response of a request.
+ receiveResponse({ data: messagedata }) {
+ if (this.destroyed || messagedata.portID != this.portID) {
+ return;
+ }
+
+ let deferred = this.requests[messagedata.requestID];
+ if (!deferred) {
+ Cu.reportError("Received a response to an unknown request.");
+ return;
+ }
+
+ delete this.requests[messagedata.requestID];
+
+ if ("resolve" in messagedata) {
+ deferred.resolve(messagedata.resolve);
+ } else if ("reject" in messagedata) {
+ deferred.reject(messagedata.reject);
+ } else {
+ deferred.reject(new Error("Internal RPM error."));
+ }
+ }
+
+ // Handles an IPC message containing any message.
+ message({ data: messagedata }) {
+ if (this.destroyed || messagedata.portID != this.portID) {
+ return;
+ }
+
+ this.handleMessage(messagedata);
+ }
+
+ /* Adds a listener for messages. Many callbacks can be registered for the
+ * same message if necessary. An attempt to register the same callback for the
+ * same message twice will be ignored. When called the callback is passed an
+ * object with these properties:
+ * target: This message port
+ * name: The message name
+ * data: Any data sent with the message
+ */
+ addMessageListener(name, callback) {
+ if (this.destroyed) {
+ throw new Error("Message port has been destroyed");
+ }
+
+ this.listener.addMessageListener(name, callback);
+ }
+
+ /*
+ * Removes a listener for messages.
+ */
+ removeMessageListener(name, callback) {
+ if (this.destroyed) {
+ throw new Error("Message port has been destroyed");
+ }
+
+ this.listener.removeMessageListener(name, callback);
+ }
+
+ // Sends a message asynchronously to the other process
+ sendAsyncMessage(name, data = null) {
+ if (this.destroyed) {
+ throw new Error("Message port has been destroyed");
+ }
+
+ let id;
+ if (this.window) {
+ id = this.window.docShell.browsingContext.id;
+ }
+ if (this.messageManager instanceof Ci.nsIMessageSender) {
+ this.messageManager.sendAsyncMessage("RemotePage:Message", {
+ portID: this.portID,
+ browsingContextID: id,
+ name,
+ data,
+ });
+ } else {
+ this.messageManager.sendAsyncMessage(name, data);
+ }
+ }
+
+ // Called to destroy this port
+ destroy() {
+ try {
+ // This can fail in the child process if the tab has already been closed
+ this.removeMessageListeners();
+ } catch (e) {}
+
+ for (let deferred of this.requests) {
+ if (deferred) {
+ deferred.reject(new Error("Message port has been destroyed"));
+ }
+ }
+
+ this.messageManager = null;
+ this.destroyed = true;
+ this.portID = null;
+ this.listener = null;
+ this.requests = [];
+ }
+
+ wrapPromise(promise) {
+ return new this.window.Promise((resolve, reject) =>
+ promise.then(resolve, reject)
+ );
+ }
+}
diff --git a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
new file mode 100644
index 0000000000..afe4f813ec
--- /dev/null
+++ b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ChildMessagePort"];
+
+const { MessagePort } = ChromeUtils.import(
+ "resource://gre/modules/remotepagemanager/MessagePort.jsm"
+);
+
+// The content side of a message port
+class ChildMessagePort extends MessagePort {
+ constructor(window) {
+ let portID =
+ Services.appinfo.processID + ":" + ChildMessagePort.nextPortID++;
+ super(window.docShell.messageManager, portID);
+
+ this.window = window;
+
+ // Add functionality to the content page
+ Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
+ defineAs: "RPMSendAsyncMessage",
+ });
+ Cu.exportFunction(this.addMessageListener.bind(this), window, {
+ defineAs: "RPMAddMessageListener",
+ allowCallbacks: true,
+ });
+ Cu.exportFunction(this.removeMessageListener.bind(this), window, {
+ defineAs: "RPMRemoveMessageListener",
+ allowCallbacks: true,
+ });
+
+ // The actor form only needs the functions set up above. The actor
+ // will send and receive messages directly.
+ if (!(this.messageManager instanceof Ci.nsIMessageSender)) {
+ return;
+ }
+
+ // Send a message for load events
+ let loadListener = () => {
+ this.sendAsyncMessage("RemotePage:Load");
+ window.removeEventListener("load", loadListener);
+ };
+ window.addEventListener("load", loadListener);
+
+ // Destroy the port when the window is unloaded
+ window.addEventListener("unload", () => {
+ try {
+ this.sendAsyncMessage("RemotePage:Unload");
+ } catch (e) {
+ // If the tab has been closed the frame message manager has already been
+ // destroyed
+ }
+ this.destroy();
+ });
+
+ // Tell the main process to set up its side of the message pipe.
+ this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
+ portID,
+ url: window.document.documentURI.replace(/[\#|\?].*$/, ""),
+ });
+ }
+
+ // Called when the content process is requesting some data.
+ async handleRequest(name, data) {
+ throw new Error(`Unknown request ${name}.`);
+ }
+
+ // Called when a message is received from the message manager or actor.
+ handleMessage(messagedata) {
+ let message = {
+ name: messagedata.name,
+ data: messagedata.data,
+ };
+ this.listener.callListeners(Cu.cloneInto(message, this.window));
+ }
+
+ destroy() {
+ this.window = null;
+ super.destroy.call(this);
+ }
+}
+
+ChildMessagePort.nextPortID = 0;
diff --git a/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
new file mode 100644
index 0000000000..0dcedb8fb3
--- /dev/null
+++ b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
@@ -0,0 +1,350 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager"];
+
+/*
+ * Using the RemotePageManager:
+ * * Create a new page listener by calling 'new RemotePages(URI)' which
+ * then injects functions like RPMGetBoolPref() into the registered page.
+ * One can then use those exported functions to communicate between
+ * child and parent.
+ *
+ * * When adding a new consumer of RPM that relies on other functionality
+ * then simple message passing provided by the RPM, then one has to
+ * whitelist permissions for the new URI within the RPMAccessManager
+ * from MessagePort.jsm.
+ */
+
+const { MessageListener, MessagePort } = ChromeUtils.import(
+ "resource://gre/modules/remotepagemanager/MessagePort.jsm"
+);
+
+/**
+ * Creates a RemotePages object which listens for new remote pages of some
+ * particular URLs. A "RemotePage:Init" message will be dispatched to this
+ * object for every page loaded. Message listeners added to this object receive
+ * messages from all loaded pages from the requested urls.
+ */
+class RemotePages {
+ constructor(urls) {
+ this.urls = Array.isArray(urls) ? urls : [urls];
+ this.messagePorts = new Set();
+ this.listener = new MessageListener();
+ this.destroyed = false;
+
+ this.portCreated = this.portCreated.bind(this);
+ this.portMessageReceived = this.portMessageReceived.bind(this);
+
+ for (const url of this.urls) {
+ RemotePageManager.addRemotePageListener(url, this.portCreated);
+ }
+ }
+
+ destroy() {
+ for (const url of this.urls) {
+ RemotePageManager.removeRemotePageListener(url);
+ }
+
+ for (let port of this.messagePorts.values()) {
+ this.removeMessagePort(port);
+ }
+
+ this.messagePorts = null;
+ this.listener = null;
+ this.destroyed = true;
+ }
+
+ // Called when a page matching one of the urls has loaded in a frame.
+ portCreated(port) {
+ this.messagePorts.add(port);
+
+ port.loaded = false;
+ port.addMessageListener("RemotePage:Load", this.portMessageReceived);
+ port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
+
+ for (let name of this.listener.keys()) {
+ this.registerPortListener(port, name);
+ }
+
+ this.listener.callListeners({ target: port, name: "RemotePage:Init" });
+ }
+
+ // A message has been received from one of the pages
+ portMessageReceived(message) {
+ switch (message.name) {
+ case "RemotePage:Load":
+ message.target.loaded = true;
+ break;
+ case "RemotePage:Unload":
+ message.target.loaded = false;
+ this.removeMessagePort(message.target);
+ break;
+ }
+
+ this.listener.callListeners(message);
+ }
+
+ // A page has closed
+ removeMessagePort(port) {
+ for (let name of this.listener.keys()) {
+ port.removeMessageListener(name, this.portMessageReceived);
+ }
+
+ port.removeMessageListener("RemotePage:Load", this.portMessageReceived);
+ port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
+ this.messagePorts.delete(port);
+ }
+
+ registerPortListener(port, name) {
+ port.addMessageListener(name, this.portMessageReceived);
+ }
+
+ // Sends a message to all known pages
+ sendAsyncMessage(name, data = null) {
+ for (let port of this.messagePorts.values()) {
+ try {
+ port.sendAsyncMessage(name, data);
+ } catch (e) {
+ // Unless the port is in the process of unloading, something strange
+ // happened but allow other ports to receive the message
+ if (e.result !== Cr.NS_ERROR_NOT_INITIALIZED) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+
+ addMessageListener(name, callback) {
+ if (this.destroyed) {
+ throw new Error("RemotePages has been destroyed");
+ }
+
+ if (!this.listener.has(name)) {
+ for (let port of this.messagePorts.values()) {
+ this.registerPortListener(port, name);
+ }
+ }
+
+ this.listener.addMessageListener(name, callback);
+ }
+
+ removeMessageListener(name, callback) {
+ if (this.destroyed) {
+ throw new Error("RemotePages has been destroyed");
+ }
+
+ this.listener.removeMessageListener(name, callback);
+ }
+
+ portsForBrowser(browser) {
+ return [...this.messagePorts].filter(port => port.browser == browser);
+ }
+}
+
+// Only exposes the public properties of the MessagePort
+function publicMessagePort(port) {
+ let properties = [
+ "addMessageListener",
+ "removeMessageListener",
+ "sendAsyncMessage",
+ "destroy",
+ ];
+
+ let clean = {};
+ for (let property of properties) {
+ clean[property] = port[property].bind(port);
+ }
+
+ Object.defineProperty(clean, "portID", {
+ enumerable: true,
+ get() {
+ return port.portID;
+ },
+ });
+
+ if (port instanceof ChromeMessagePort) {
+ Object.defineProperty(clean, "browser", {
+ enumerable: true,
+ get() {
+ return port.browser;
+ },
+ });
+
+ Object.defineProperty(clean, "url", {
+ enumerable: true,
+ get() {
+ return port.url;
+ },
+ });
+ }
+
+ return clean;
+}
+
+// The chome side of a message port
+class ChromeMessagePort extends MessagePort {
+ constructor(browser, portID, url) {
+ super(browser.messageManager, portID);
+
+ this._browser = browser;
+ this._permanentKey = browser.permanentKey;
+ this._url = url;
+
+ Services.obs.addObserver(this, "message-manager-disconnect");
+ this.publicPort = publicMessagePort(this);
+
+ this.swapBrowsers = this.swapBrowsers.bind(this);
+ this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
+ }
+
+ get browser() {
+ return this._browser;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ // Called when the docshell is being swapped with another browser. We have to
+ // update to use the new browser's message manager
+ swapBrowsers({ detail: newBrowser }) {
+ // We can see this event for the new browser before the swap completes so
+ // check that the browser we're tracking has our permanentKey.
+ if (this._browser.permanentKey != this._permanentKey) {
+ return;
+ }
+
+ this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
+
+ this._browser = newBrowser;
+ this.swapMessageManager(newBrowser.messageManager);
+
+ this._browser.addEventListener("SwapDocShells", this.swapBrowsers);
+ }
+
+ // Called when a message manager has been disconnected indicating that the
+ // tab has closed or crashed
+ observe(messageManager) {
+ if (messageManager != this.messageManager) {
+ return;
+ }
+
+ this.listener.callListeners({
+ target: this.publicPort,
+ name: "RemotePage:Unload",
+ data: null,
+ });
+ this.destroy();
+ }
+
+ // Called when the content process is requesting some data.
+ async handleRequest(name, data) {
+ throw new Error(`Unknown request ${name}.`);
+ }
+
+ // Called when a message is received from the message manager.
+ handleMessage(messagedata) {
+ let message = {
+ target: this.publicPort,
+ name: messagedata.name,
+ data: messagedata.data,
+ browsingContextID: messagedata.browsingContextID,
+ };
+ this.listener.callListeners(message);
+
+ if (messagedata.name == "RemotePage:Unload") {
+ this.destroy();
+ }
+ }
+
+ destroy() {
+ try {
+ this._browser.removeEventListener("SwapDocShells", this.swapBrowsers);
+ } catch (e) {
+ // It's possible the browser instance is already dead so we can just ignore
+ // this error.
+ }
+
+ this._browser = null;
+ Services.obs.removeObserver(this, "message-manager-disconnect");
+ super.destroy.call(this);
+ }
+}
+
+// Allows callers to register to connect to specific content pages. Registration
+// is done through the addRemotePageListener method
+var RemotePageManagerInternal = {
+ // The currently registered remote pages
+ pages: new Map(),
+
+ // Initialises all the needed listeners
+ init() {
+ Services.mm.addMessageListener(
+ "RemotePage:InitPort",
+ this.initPort.bind(this)
+ );
+ this.updateProcessUrls();
+ },
+
+ updateProcessUrls() {
+ Services.ppmm.sharedData.set(
+ "RemotePageManager:urls",
+ new Set(this.pages.keys())
+ );
+ Services.ppmm.sharedData.flush();
+ },
+
+ // Registers interest in a remote page. A callback is called with a port for
+ // the new page when loading begins (i.e. the page hasn't actually loaded yet).
+ // Only one callback can be registered per URL.
+ addRemotePageListener(url, callback) {
+ if (this.pages.has(url)) {
+ throw new Error("Remote page already registered: " + url);
+ }
+
+ this.pages.set(url, callback);
+ this.updateProcessUrls();
+ },
+
+ // Removes any interest in a remote page.
+ removeRemotePageListener(url) {
+ if (!this.pages.has(url)) {
+ throw new Error("Remote page is not registered: " + url);
+ }
+
+ this.pages.delete(url);
+ this.updateProcessUrls();
+ },
+
+ // A remote page has been created and a port is ready in the content side
+ initPort({ target: browser, data: { url, portID } }) {
+ let callback = this.pages.get(url);
+ if (!callback) {
+ Cu.reportError("Unexpected remote page load: " + url);
+ return;
+ }
+
+ let port = new ChromeMessagePort(browser, portID, url);
+ callback(port.publicPort);
+ },
+};
+
+if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ throw new Error("RemotePageManager can only be used in the main process.");
+}
+
+RemotePageManagerInternal.init();
+
+// The public API for the above object
+var RemotePageManager = {
+ addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(
+ RemotePageManagerInternal
+ ),
+ removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(
+ RemotePageManagerInternal
+ ),
+};
diff --git a/toolkit/components/remotepagemanager/moz.build b/toolkit/components/remotepagemanager/moz.build
new file mode 100644
index 0000000000..85eebd78a6
--- /dev/null
+++ b/toolkit/components/remotepagemanager/moz.build
@@ -0,0 +1,16 @@
+# -*- 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 = ("Toolkit", "General")
+
+EXTRA_JS_MODULES.remotepagemanager = [
+ "MessagePort.jsm",
+ "RemotePageManagerChild.jsm",
+ "RemotePageManagerParent.jsm",
+]
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
diff --git a/toolkit/components/remotepagemanager/tests/browser/browser.ini b/toolkit/components/remotepagemanager/tests/browser/browser.ini
new file mode 100644
index 0000000000..67b6365b37
--- /dev/null
+++ b/toolkit/components/remotepagemanager/tests/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ testremotepagemanager.html
+ testremotepagemanager2.html
+
+[browser_RemotePageManager.js]
+https_first_disabled = true
diff --git a/toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js b/toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js
new file mode 100644
index 0000000000..ac0f3cf83d
--- /dev/null
+++ b/toolkit/components/remotepagemanager/tests/browser/browser_RemotePageManager.js
@@ -0,0 +1,570 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL =
+ "http://www.example.com/browser/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html";
+
+var { RemotePages, RemotePageManager } = ChromeUtils.import(
+ "resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm"
+);
+
+function failOnMessage(message) {
+ ok(false, "Should not have seen message " + message.name);
+}
+
+function waitForMessage(port, message, expectedPort = port) {
+ return new Promise(resolve => {
+ function listener(message) {
+ is(
+ message.target,
+ expectedPort,
+ "Message should be from the right port."
+ );
+
+ port.removeMessageListener(listener);
+ resolve(message);
+ }
+
+ port.addMessageListener(message, listener);
+ });
+}
+
+function waitForPort(url, createTab = true) {
+ return new Promise(resolve => {
+ RemotePageManager.addRemotePageListener(url, port => {
+ RemotePageManager.removeRemotePageListener(url);
+
+ waitForMessage(port, "RemotePage:Load").then(() => resolve(port));
+ });
+
+ if (createTab) {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
+ }
+ });
+}
+
+function waitForPage(pages, url = TEST_URL) {
+ return new Promise(resolve => {
+ function listener({ target }) {
+ pages.removeMessageListener("RemotePage:Init", listener);
+
+ waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
+ }
+
+ pages.addMessageListener("RemotePage:Init", listener);
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, url);
+ });
+}
+
+function swapDocShells(browser1, browser2) {
+ // Swap frameLoaders.
+ browser1.swapDocShells(browser2);
+
+ // Swap permanentKeys.
+ let tmp = browser1.permanentKey;
+ browser1.permanentKey = browser2.permanentKey;
+ browser2.permanentKey = tmp;
+}
+
+add_task(async function sharedData_aka_initialProcessData() {
+ const includesTest = () =>
+ Services.cpmm.sharedData.get("RemotePageManager:urls").has(TEST_URL);
+ is(
+ includesTest(),
+ false,
+ "Shouldn't have test url in initial process data yet"
+ );
+
+ const loadedPort = waitForPort(TEST_URL);
+ is(includesTest(), true, "Should have test url when waiting for it to load");
+
+ await loadedPort;
+ is(includesTest(), false, "Should have test url removed when done listening");
+
+ gBrowser.removeCurrentTab();
+});
+
+// Test that opening a page creates a port, sends the load event and then
+// navigating to a new page sends the unload event. Going back should create a
+// new port
+add_task(async function init_navigate() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ BrowserTestUtils.loadURI(gBrowser, "about:blank");
+
+ await waitForMessage(port, "RemotePage:Unload");
+
+ // Port should be destroyed now
+ try {
+ port.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ port.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ await loaded;
+
+ gBrowser.goBack();
+ port = await waitForPort(TEST_URL, false);
+
+ port.sendAsyncMessage("Ping2");
+ await waitForMessage(port, "Pong2");
+ port.destroy();
+
+ gBrowser.removeCurrentTab();
+});
+
+// Test that opening a page creates a port, sends the load event and then
+// closing the tab sends the unload event
+add_task(async function init_close() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ await unloadPromise;
+
+ // Port should be destroyed now
+ try {
+ port.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ port.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+});
+
+// Tests that we can send messages to individual pages even when more than one
+// is open
+add_task(async function multiple_ports() {
+ let port1 = await waitForPort(TEST_URL);
+ is(
+ port1.browser,
+ gBrowser.selectedBrowser,
+ "Port is for the correct browser"
+ );
+
+ let port2 = await waitForPort(TEST_URL);
+ is(
+ port2.browser,
+ gBrowser.selectedBrowser,
+ "Port is for the correct browser"
+ );
+
+ port2.addMessageListener("Pong", failOnMessage);
+ port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+ let message = await waitForMessage(port1, "Pong");
+ port2.removeMessageListener("Pong", failOnMessage);
+ is(message.data.str, "foobar", "String should pass through");
+ is(message.data.counter, 1, "Counter should be incremented");
+
+ port1.addMessageListener("Pong", failOnMessage);
+ port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 });
+ message = await waitForMessage(port2, "Pong");
+ port1.removeMessageListener("Pong", failOnMessage);
+ is(message.data.str, "foobaz", "String should pass through");
+ is(message.data.counter, 6, "Counter should be incremented");
+
+ let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+ await unloadPromise;
+
+ try {
+ port2.addMessageListener("Pong", failOnMessage);
+ ok(
+ false,
+ "Should not have been able to add a new message listener to a destroyed port."
+ );
+ } catch (e) {
+ ok(
+ true,
+ "Should not have been able to add a new message listener to a destroyed port."
+ );
+ }
+
+ port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+ message = await waitForMessage(port1, "Pong");
+ is(message.data.str, "foobar", "String should pass through");
+ is(message.data.counter, 1, "Counter should be incremented");
+
+ unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+ await unloadPromise;
+});
+
+// Tests that swapping browser docshells doesn't break the ports
+add_task(async function browser_switch() {
+ let port1 = await waitForPort(TEST_URL);
+ is(
+ port1.browser,
+ gBrowser.selectedBrowser,
+ "Port is for the correct browser"
+ );
+ let browser1 = gBrowser.selectedBrowser;
+ port1.sendAsyncMessage("SetCookie", { value: "om nom" });
+
+ let port2 = await waitForPort(TEST_URL);
+ is(
+ port2.browser,
+ gBrowser.selectedBrowser,
+ "Port is for the correct browser"
+ );
+ let browser2 = gBrowser.selectedBrowser;
+ port2.sendAsyncMessage("SetCookie", { value: "om nom nom" });
+
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ let message = await waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = await waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ swapDocShells(browser1, browser2);
+ is(port1.browser, browser2, "Should have noticed the swap");
+ is(port2.browser, browser1, "Should have noticed the swap");
+
+ // Cookies should have stayed the same
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ message = await waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = await waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ swapDocShells(browser1, browser2);
+ is(port1.browser, browser1, "Should have noticed the swap");
+ is(port2.browser, browser2, "Should have noticed the swap");
+
+ // Cookies should have stayed the same
+ port2.addMessageListener("Cookie", failOnMessage);
+ port1.sendAsyncMessage("GetCookie");
+ message = await waitForMessage(port1, "Cookie");
+ port2.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom", "Should have the right cookie");
+
+ port1.addMessageListener("Cookie", failOnMessage);
+ port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+ message = await waitForMessage(port2, "Cookie");
+ port1.removeMessageListener("Cookie", failOnMessage);
+ is(message.data.value, "om nom nom", "Should have the right cookie");
+
+ let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
+ await unloadPromise;
+
+ unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+ gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
+ await unloadPromise;
+});
+
+// Tests that removeMessageListener in chrome works
+add_task(async function remove_chrome_listener() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // This relies on messages sent arriving in the same order. Pong will be
+ // sent back before Pong2 so if removeMessageListener fails the test will fail
+ port.addMessageListener("Pong", failOnMessage);
+ port.removeMessageListener("Pong", failOnMessage);
+ port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 });
+ port.sendAsyncMessage("Ping2");
+ await waitForMessage(port, "Pong2");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ await unloadPromise;
+});
+
+// Tests that removeMessageListener in content works
+add_task(async function remove_content_listener() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // This relies on messages sent arriving in the same order. Pong3 would be
+ // sent back before Pong2 so if removeMessageListener fails the test will fail
+ port.addMessageListener("Pong3", failOnMessage);
+ port.sendAsyncMessage("Ping3");
+ port.sendAsyncMessage("Ping2");
+ await waitForMessage(port, "Pong2");
+
+ let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+ gBrowser.removeCurrentTab();
+ await unloadPromise;
+});
+
+// Test RemotePages works
+add_task(async function remote_pages_basic() {
+ let pages = new RemotePages(TEST_URL);
+ let port = await waitForPage(pages);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ // Listening to global messages should work
+ let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port);
+ gBrowser.removeCurrentTab();
+ await unloadPromise;
+
+ pages.destroy();
+
+ // RemotePages should be destroyed now
+ try {
+ pages.addMessageListener("Foo", failOnMessage);
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+
+ try {
+ pages.sendAsyncMessage("Foo");
+ ok(false, "Should have seen exception");
+ } catch (e) {
+ ok(true, "Should have seen exception");
+ }
+});
+
+// Test that properties exist on the target port provided to listeners
+add_task(async function check_port_properties() {
+ let pages = new RemotePages(TEST_URL);
+
+ const expectedProperties = [
+ "addMessageListener",
+ "browser",
+ "destroy",
+ "loaded",
+ "portID",
+ "removeMessageListener",
+ "sendAsyncMessage",
+ "url",
+ ];
+ function checkProperties(port, description) {
+ const expected = [];
+ const unexpected = [];
+ for (const key in port) {
+ (expectedProperties.includes(key) ? expected : unexpected).push(key);
+ }
+ is(
+ `${expected.sort()}`,
+ `${expectedProperties}`,
+ `${description} has expected keys`
+ );
+ is(
+ `${unexpected.sort()}`,
+ "",
+ `${description} should not have unexpected keys`
+ );
+ }
+
+ function portFrom(message, extraFn = () => {}) {
+ return new Promise(resolve => {
+ function onMessage({ target }) {
+ pages.removeMessageListener(message, onMessage);
+ resolve(target);
+ }
+ pages.addMessageListener(message, onMessage);
+ extraFn();
+ });
+ }
+
+ let portFromInit = await portFrom(
+ "RemotePage:Init",
+ () => (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, TEST_URL))
+ );
+ checkProperties(portFromInit, "inited port");
+ ok(
+ ["about:blank", TEST_URL].includes(portFromInit.browser.currentURI.spec),
+ `inited port browser is either still blank or already at the target url - got ${portFromInit.browser.currentURI.spec}`
+ );
+ is(portFromInit.loaded, false, "inited port has not been loaded yet");
+ is(portFromInit.url, TEST_URL, "got expected url");
+
+ let portFromLoad = await portFrom("RemotePage:Load");
+ is(portFromLoad, portFromInit, "got the same port from init and load");
+ checkProperties(portFromLoad, "loaded port");
+ is(
+ portFromInit.browser.currentURI.spec,
+ TEST_URL,
+ "loaded port has browser with actual url"
+ );
+ is(portFromInit.loaded, true, "loaded port is now loaded");
+ is(portFromInit.url, TEST_URL, "still got expected url");
+
+ let portFromUnload = await portFrom("RemotePage:Unload", () =>
+ BrowserTestUtils.removeTab(gBrowser.selectedTab)
+ );
+ is(portFromUnload, portFromInit, "got the same port from init and unload");
+ checkProperties(portFromUnload, "unloaded port");
+ is(portFromInit.browser, null, "unloaded port has no browser");
+ is(portFromInit.loaded, false, "unloaded port is now not loaded");
+ is(portFromInit.url, TEST_URL, "still got expected url");
+
+ pages.destroy();
+});
+
+// Test sending messages to all remote pages works
+add_task(async function remote_pages_multiple_pages() {
+ let pages = new RemotePages(TEST_URL);
+ let port1 = await waitForPage(pages);
+ let port2 = await waitForPage(pages);
+
+ let pongPorts = [];
+ await new Promise(resolve => {
+ function listener({ name, target, data }) {
+ is(name, "Pong", "Should have seen the right response.");
+ is(data.str, "remote_pages", "String should pass through");
+ is(data.counter, 43, "Counter should be incremented");
+ pongPorts.push(target);
+ if (pongPorts.length == 2) {
+ resolve();
+ }
+ }
+
+ pages.addMessageListener("Pong", listener);
+ pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 });
+ });
+
+ // We don't make any guarantees about which order messages are sent to known
+ // pages so the pongs could have come back in any order.
+ isnot(
+ pongPorts[0],
+ pongPorts[1],
+ "Should have received pongs from different ports"
+ );
+ ok(pongPorts.includes(port1), "Should have seen a pong from port1");
+ ok(pongPorts.includes(port2), "Should have seen a pong from port2");
+
+ // After destroy we should see no messages
+ pages.addMessageListener("RemotePage:Unload", failOnMessage);
+ pages.destroy();
+
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+});
+
+// Test that RemotePages with multiple urls works
+add_task(async function remote_pages_multiple_urls() {
+ const TEST_URLS = [TEST_URL, TEST_URL.replace(".html", "2.html")];
+ const pages = new RemotePages(TEST_URLS);
+
+ const ports = [];
+ // Load two pages for each url
+ for (const [i, url] of TEST_URLS.entries()) {
+ const port = await waitForPage(pages, url);
+ is(
+ port.browser,
+ gBrowser.selectedBrowser,
+ `port${i} is for the correct browser`
+ );
+ ports.push(port);
+ ports.push(await waitForPage(pages, url));
+ }
+
+ let unloadPromise = waitForMessage(pages, "RemotePage:Unload", ports.pop());
+ gBrowser.removeCurrentTab();
+ await unloadPromise;
+
+ const pongPorts = new Set();
+ await new Promise(resolve => {
+ function listener({ name, target, data }) {
+ is(name, "Pong", "Should have seen the right response.");
+ is(data.str, "FAKE_DATA", "String should pass through");
+ is(data.counter, 1235, "Counter should be incremented");
+ pongPorts.add(target);
+ if (pongPorts.size === ports.length) {
+ resolve();
+ }
+ }
+
+ pages.addMessageListener("Pong", listener);
+ pages.sendAsyncMessage("Ping", { str: "FAKE_DATA", counter: 1234 });
+ });
+
+ ports.forEach(port => ok(pongPorts.has(port)));
+
+ pages.destroy();
+ ports.forEach(port =>
+ gBrowser.removeTab(gBrowser.getTabForBrowser(port.browser))
+ );
+});
+
+// Test sending various types of data across the boundary
+add_task(async function send_data() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let data = {
+ integer: 45,
+ real: 45.78,
+ str: "foobar",
+ array: [1, 2, 3, 5, 27],
+ };
+
+ port.sendAsyncMessage("SendData", data);
+ let message = await waitForMessage(port, "ReceivedData");
+
+ ok(message.data.result, message.data.status);
+
+ gBrowser.removeCurrentTab();
+});
+
+// Test sending an object of data across the boundary
+add_task(async function send_data2() {
+ let port = await waitForPort(TEST_URL);
+ is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+ let data = {
+ integer: 45,
+ real: 45.78,
+ str: "foobar",
+ array: [1, 2, 3, 5, 27],
+ };
+
+ port.sendAsyncMessage("SendData2", { data });
+ let message = await waitForMessage(port, "ReceivedData2");
+
+ ok(message.data.result, message.data.status);
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function get_ports_for_browser() {
+ let pages = new RemotePages(TEST_URL);
+ let port = await waitForPage(pages);
+ // waitForPage creates a new tab and selects it by default, so
+ // the selected tab should be the one hosting this port.
+ let browser = gBrowser.selectedBrowser;
+ let foundPorts = pages.portsForBrowser(browser);
+ is(
+ foundPorts.length,
+ 1,
+ "There should only be one port for this simple page"
+ );
+ is(foundPorts[0], port, "Should find the port");
+
+ pages.destroy();
+ gBrowser.removeCurrentTab();
+});
diff --git a/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html b/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html
new file mode 100644
index 0000000000..a1ad6cffb1
--- /dev/null
+++ b/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<script type="text/javascript">
+/* global RPMAddMessageListener, RPMSendAsyncMessage, RPMRemoveMessageListener */
+
+RPMAddMessageListener("Ping", function(message) {
+ RPMSendAsyncMessage("Pong", {
+ str: message.data.str,
+ counter: message.data.counter + 1,
+ });
+});
+
+RPMAddMessageListener("Ping2", function(message) {
+ RPMSendAsyncMessage("Pong2", message.data);
+});
+
+function neverCalled() {
+ RPMSendAsyncMessage("Pong3");
+}
+RPMAddMessageListener("Pong3", neverCalled);
+RPMRemoveMessageListener("Pong3", neverCalled);
+
+function testData(data) {
+ var response = {
+ result: true,
+ status: "All data correctly received",
+ };
+
+ function compare(prop, expected) {
+ if (JSON.stringify(data[prop]) == JSON.stringify(expected))
+ return;
+ if (response.result)
+ response.status = "";
+ response.result = false;
+ response.status += "Property " + prop + " should have been " + expected + " but was " + data[prop] + "\n";
+ }
+
+ compare("integer", 45);
+ compare("real", 45.78);
+ compare("str", "foobar");
+ compare("array", [1, 2, 3, 5, 27]);
+
+ return response;
+}
+
+RPMAddMessageListener("SendData", function(message) {
+ RPMSendAsyncMessage("ReceivedData", testData(message.data));
+});
+
+RPMAddMessageListener("SendData2", function(message) {
+ RPMSendAsyncMessage("ReceivedData2", testData(message.data.data));
+});
+
+var cookie = "nom";
+RPMAddMessageListener("SetCookie", function(message) {
+ cookie = message.data.value;
+});
+
+RPMAddMessageListener("GetCookie", function(message) {
+ RPMSendAsyncMessage("Cookie", { value: cookie });
+});
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager2.html b/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager2.html
new file mode 100644
index 0000000000..70784b5011
--- /dev/null
+++ b/toolkit/components/remotepagemanager/tests/browser/testremotepagemanager2.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<!-- A second page to test that RemotePages works with multiple urls -->
+<html>
+<head>
+<script type="text/javascript">
+/* global RPMAddMessageListener, RPMSendAsyncMessage */
+
+RPMAddMessageListener("Ping", function(message) {
+ RPMSendAsyncMessage("Pong", {
+ str: message.data.str,
+ counter: message.data.counter + 1,
+ });
+});
+
+</script>
+</head>
+<body>
+</body>
+</html>