diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/remote | |
parent | Initial commit. (diff) | |
download | firefox-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 '')
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> |