diff --git a/browser_patches/checkout_build_archive_upload.sh b/browser_patches/checkout_build_archive_upload.sh index dac8771245..2b34da5124 100755 --- a/browser_patches/checkout_build_archive_upload.sh +++ b/browser_patches/checkout_build_archive_upload.sh @@ -188,6 +188,45 @@ elif [[ "$BUILD_FLAVOR" == "firefox-win64" ]]; then BUILD_BLOB_NAME="firefox-win64.zip" +# =============================== +# FIREFOX-STABLE COMPILATION +# =============================== +elif [[ "$BUILD_FLAVOR" == "firefox-stable-ubuntu-18.04" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--full" + EXPECTED_HOST_OS="Ubuntu" + EXPECTED_HOST_OS_VERSION="18.04" + BUILD_BLOB_NAME="firefox-stable-ubuntu-18.04.zip" +elif [[ "$BUILD_FLAVOR" == "firefox-stable-ubuntu-20.04" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--full" + EXPECTED_HOST_OS="Ubuntu" + EXPECTED_HOST_OS_VERSION="20.04" + BUILD_BLOB_NAME="firefox-stable-ubuntu-20.04.zip" +elif [[ "$BUILD_FLAVOR" == "firefox-stable-mac-10.14" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--full" + EXPECTED_HOST_OS="Darwin" + EXPECTED_HOST_OS_VERSION="10.14" + BUILD_BLOB_NAME="firefox-stable-mac-10.14.zip" +elif [[ "$BUILD_FLAVOR" == "firefox-stable-mac-11.0-arm64" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--full" + EXPECTED_HOST_OS="Darwin" + EXPECTED_HOST_OS_VERSION="11.0" + EXPECTED_ARCH="arm64" + BUILD_BLOB_NAME="firefox-stable-mac-11.0-arm64.zip" +elif [[ "$BUILD_FLAVOR" == "firefox-stable-win32" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--full" + EXPECTED_HOST_OS="MINGW" + BUILD_BLOB_NAME="firefox-stable-win32.zip" +elif [[ "$BUILD_FLAVOR" == "firefox-stable-win64" ]]; then + BROWSER_NAME="firefox-stable" + EXTRA_BUILD_ARGS="--win64 --full" + EXPECTED_HOST_OS="MINGW" + BUILD_BLOB_NAME="firefox-stable-win64.zip" + # =========================== # WEBKIT COMPILATION # =========================== diff --git a/browser_patches/export.sh b/browser_patches/export.sh index 3ebcd56be2..3f616491e9 100755 --- a/browser_patches/export.sh +++ b/browser_patches/export.sh @@ -53,6 +53,19 @@ if [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then CHECKOUT_PATH="${FF_CHECKOUT_PATH}" FRIENDLY_CHECKOUT_PATH="" fi +elif [[ ("$1" == "firefox-stable") ]]; then + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox-stable/checkout"; + CHECKOUT_PATH="$PWD/firefox-stable/checkout" + EXTRA_FOLDER_PW_PATH="$PWD/firefox-stable/juggler" + EXTRA_FOLDER_CHECKOUT_RELPATH="juggler" + EXPORT_PATH="$PWD/firefox-stable" + BUILD_NUMBER_UPSTREAM_URL="https://raw.githubusercontent.com/microsoft/playwright/master/browser_patches/firefox-stable/BUILD_NUMBER" + source "./firefox-stable/UPSTREAM_CONFIG.sh" + if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then + echo "WARNING: using checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}" + CHECKOUT_PATH="${FF_CHECKOUT_PATH}" + FRIENDLY_CHECKOUT_PATH="" + fi elif [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; CHECKOUT_PATH="$PWD/webkit/checkout" @@ -129,8 +142,9 @@ NEW_BASE_REVISION=$(git merge-base $REMOTE_BROWSER_UPSTREAM/$BASE_BRANCH $CURREN NEW_DIFF=$(git diff --diff-algorithm=myers --full-index $NEW_BASE_REVISION $CURRENT_BRANCH -- . ":!${EXTRA_FOLDER_CHECKOUT_RELPATH}") # Increment BUILD_NUMBER -BUILD_NUMBER=$(curl ${BUILD_NUMBER_UPSTREAM_URL} | head -1) -BUILD_NUMBER=$((BUILD_NUMBER+1)) +#BUILD_NUMBER=$(curl ${BUILD_NUMBER_UPSTREAM_URL} | head -1) +#BUILD_NUMBER=$((BUILD_NUMBER+1)) +BUILD_NUMBER=1242 echo "REMOTE_URL=\"$REMOTE_URL\" BASE_BRANCH=\"$BASE_BRANCH\" diff --git a/browser_patches/firefox-stable/.gitignore b/browser_patches/firefox-stable/.gitignore new file mode 100644 index 0000000000..5e660dc18e --- /dev/null +++ b/browser_patches/firefox-stable/.gitignore @@ -0,0 +1 @@ +/checkout diff --git a/browser_patches/firefox-stable/BUILD_NUMBER b/browser_patches/firefox-stable/BUILD_NUMBER new file mode 100644 index 0000000000..e826719807 --- /dev/null +++ b/browser_patches/firefox-stable/BUILD_NUMBER @@ -0,0 +1,2 @@ +1242 +Changed: lushnikov@chromium.org Fri 09 Apr 2021 09:56:28 PM PDT diff --git a/browser_patches/firefox-stable/EXPECTED_BUILDS b/browser_patches/firefox-stable/EXPECTED_BUILDS new file mode 100644 index 0000000000..64a6d4fc8f --- /dev/null +++ b/browser_patches/firefox-stable/EXPECTED_BUILDS @@ -0,0 +1,5 @@ +firefox-stable-mac-10.14.zip +firefox-stable-ubuntu-18.04.zip +firefox-stable-ubuntu-20.04.zip +firefox-stable-win32.zip +firefox-stable-win64.zip diff --git a/browser_patches/firefox-stable/UPSTREAM_CONFIG.sh b/browser_patches/firefox-stable/UPSTREAM_CONFIG.sh new file mode 100644 index 0000000000..978ea85053 --- /dev/null +++ b/browser_patches/firefox-stable/UPSTREAM_CONFIG.sh @@ -0,0 +1,3 @@ +REMOTE_URL="https://github.com/mozilla/gecko-dev" +BASE_BRANCH="release" +BASE_REVISION="4068febfd76d9ec557591240d7496be42c27c17f" diff --git a/browser_patches/firefox-stable/archive.sh b/browser_patches/firefox-stable/archive.sh new file mode 100755 index 0000000000..776891f358 --- /dev/null +++ b/browser_patches/firefox-stable/archive.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -e +set +x + +if [[ ("$1" == "-h") || ("$1" == "--help") ]]; then + echo "usage: $(basename $0) [output-absolute-path]" + echo + echo "Generate distributable .zip archive from ./checkout folder that was previously built." + echo + exit 0 +fi + +ZIP_PATH=$1 +if [[ $ZIP_PATH != /* ]]; then + echo "ERROR: path $ZIP_PATH is not absolute" + exit 1 +fi +if [[ $ZIP_PATH != *.zip ]]; then + echo "ERROR: path $ZIP_PATH must have .zip extension" + exit 1 +fi +if [[ -f $ZIP_PATH ]]; then + echo "ERROR: path $ZIP_PATH exists; can't do anything." + exit 1 +fi +if ! [[ -d $(dirname $ZIP_PATH) ]]; then + echo "ERROR: folder for path $($ZIP_PATH) does not exist." + exit 1 +fi + +trap "cd $(pwd -P)" EXIT +cd "$(dirname $0)" +SCRIPT_FOLDER="$(pwd -P)" + +if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then + cd "${FF_CHECKOUT_PATH}" + echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}" +else + cd "checkout" +fi + +OBJ_FOLDER="obj-build-playwright" + +./mach package +node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/$OBJ_FOLDER/dist/firefox + +if ! [[ -d $OBJ_FOLDER/dist/firefox ]]; then + echo "ERROR: cannot find $OBJ_FOLDER/dist/firefox folder in the checkout/. Did you build?" + exit 1; +fi + +# Copy the libstdc++ version we linked against. +# TODO(aslushnikov): this won't be needed with official builds. +if [[ "$(uname)" == "Linux" ]]; then + cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6 $OBJ_FOLDER/dist/firefox/libstdc++.so.6 +fi + +# tar resulting directory and cleanup TMP. +cd $OBJ_FOLDER/dist +zip -r $ZIP_PATH firefox diff --git a/browser_patches/firefox-stable/build.sh b/browser_patches/firefox-stable/build.sh new file mode 100755 index 0000000000..6e42210f90 --- /dev/null +++ b/browser_patches/firefox-stable/build.sh @@ -0,0 +1,126 @@ +#!/bin/bash +set -e +set +x + +RUST_VERSION="1.49.0" +CBINDGEN_VERSION="0.16.0" +# Certain minimal SDK Version is required by firefox +MACOS_SDK_VERSION="10.12" +# XCode version can be determined from https://en.wikipedia.org/wiki/Xcode +XCODE_VERSION_WITH_REQUIRED_SDK_VERSION="8.3.3" + +trap "cd $(pwd -P)" EXIT + +cd "$(dirname $0)" +SCRIPT_FOLDER="$(pwd -P)" + +if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then + cd "${FF_CHECKOUT_PATH}" + echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}" +else + cd "checkout" +fi + +rm -rf .mozconfig + +if [[ "$(uname)" == "Darwin" ]]; then + if [[ $(uname -m) == "arm64" ]]; then + # Building on Apple Silicon requires XCode12.2 and does not require any extra SDKs. + if ! [[ -d "/Applications/Xcode12.2.app" ]]; then + echo "As of Jan 2021, building Firefox on Apple Silicon requires XCode 12.2" + echo "Make sure there's an /Applications/Xcode12.2.app" + echo "Download XCode from https://developer.apple.com/download/more/" + echo "" + exit 1 + fi + export DEVELOPER_DIR=/Applications/Xcode12.2.app/Contents/Developer + else + # Firefox currently does not build on 10.15 out of the box - it requires SDK for 10.12. + # Make sure the SDK is out there. + if ! [[ -d $HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk ]]; then + echo "As of Dec 2020, Firefox does not build on Mac without ${MACOS_SDK_VERSION} SDK." + echo "Download XCode ${XCODE_VERSION_WITH_REQUIRED_SDK_VERSION} from https://developer.apple.com/download/more/ and" + echo "extract SDK to $HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk" + echo "" + echo "More info: https://firefox-source-docs.mozilla.org/setup/macos_build.html" + exit 1 + else + echo "-- configuting .mozconfig with ${MACOS_SDK_VERSION} SDK path" + echo "ac_add_options --with-macos-sdk=$HOME/SDK-archive/MacOSX${MACOS_SDK_VERSION}.sdk/" >> .mozconfig + fi + fi + echo "-- building on Mac" +elif [[ "$(uname)" == "Linux" ]]; then + echo "-- building on Linux" + echo "ac_add_options --disable-av1" >> .mozconfig +elif [[ "$(uname)" == MINGW* ]]; then + echo "ac_add_options --disable-update-agent" >> .mozconfig + echo "ac_add_options --disable-default-browser-agent" >> .mozconfig + + DLL_FILE="" + if [[ $1 == "--win64" ]]; then + echo "-- building win64 build on MINGW" + echo "ac_add_options --target=x86_64-pc-mingw32" >> .mozconfig + echo "ac_add_options --host=x86_64-pc-mingw32" >> .mozconfig + DLL_FILE=$("C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find '**\Redist\MSVC\*\x64\**\vcruntime140.dll') + else + echo "-- building win32 build on MINGW" + DLL_FILE=$("C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -latest -find '**\Redist\MSVC\*\x86\**\vcruntime140.dll') + fi + WIN32_REDIST_DIR=$(dirname "$DLL_FILE") + if ! [[ -d $WIN32_REDIST_DIR ]]; then + echo "ERROR: cannot find MS VS C++ redistributable $WIN32_REDIST_DIR" + exit 1; + fi +else + echo "ERROR: cannot upload on this platform!" 1>&2 + exit 1; +fi + +OBJ_FOLDER="obj-build-playwright" +echo "mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/${OBJ_FOLDER}" >> .mozconfig +echo "ac_add_options --disable-crashreporter" >> .mozconfig + +if [[ $1 == "--full" || $2 == "--full" ]]; then + if [[ "$(uname)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then + ./mach artifact toolchain --from-build macosx64-node + rm -rf "$HOME/.mozbuild/node" + mv node "$HOME/.mozbuild/" + elif [[ "$(uname)" == "Darwin" || "$(uname)" == "Linux" ]]; then + SHELL=/bin/sh ./mach --no-interactive bootstrap --application-choice=browser --no-system-changes + fi + if [[ ! -z "${WIN32_REDIST_DIR}" ]]; then + # Having this option in .mozconfig kills incremental compilation. + echo "export WIN32_REDIST_DIR=\"$WIN32_REDIST_DIR\"" >> .mozconfig + fi +fi + +if ! [[ -f "$HOME/.mozbuild/_virtualenvs/mach/bin/python" ]]; then + ./mach create-mach-environment +fi + +if [[ $1 == "--juggler" ]]; then + ./mach build faster +else + # TODO: rustup is not in the PATH on Windows + if command -v rustup >/dev/null; then + # We manage Rust version ourselves. + echo "-- Using rust v${RUST_VERSION}" + rustup install "${RUST_VERSION}" + rustup default "${RUST_VERSION}" + fi + + # TODO: cargo is not in the PATH on Windows + if command -v cargo >/dev/null; then + echo "-- Using cbindgen v${CBINDGEN_VERSION}" + cargo install cbindgen --version "${CBINDGEN_VERSION}" + fi + ./mach build +fi + +if [[ "$(uname)" == "Darwin" ]]; then + node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/${OBJ_FOLDER}/dist +else + node "${SCRIPT_FOLDER}"/install-preferences.js $PWD/${OBJ_FOLDER}/dist/bin +fi + diff --git a/browser_patches/firefox-stable/clean.sh b/browser_patches/firefox-stable/clean.sh new file mode 100755 index 0000000000..9b3a97c380 --- /dev/null +++ b/browser_patches/firefox-stable/clean.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e +set +x + +trap "cd $(pwd -P)" EXIT +if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then + cd "${FF_CHECKOUT_PATH}" + echo "WARNING: checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}" +else + cd "$(dirname $0)" + cd "checkout" +fi + +OBJ_FOLDER="obj-build-playwright" +if [[ -d $OBJ_FOLDER ]]; then + rm -rf $OBJ_FOLDER +fi + diff --git a/browser_patches/firefox-stable/install-preferences.js b/browser_patches/firefox-stable/install-preferences.js new file mode 100644 index 0000000000..f00f526392 --- /dev/null +++ b/browser_patches/firefox-stable/install-preferences.js @@ -0,0 +1,96 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const util = require('util'); + +const writeFileAsync = util.promisify(fs.writeFile.bind(fs)); +const mkdirAsync = util.promisify(fs.mkdir.bind(fs)); + +// Install browser preferences after downloading and unpacking +// firefox instances. +// Based on: https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Enterprise_deployment_before_60#Configuration +async function installFirefoxPreferences(distpath) { + let executablePath = ''; + if (os.platform() === 'linux') + executablePath = path.join(distpath, 'firefox'); + else if (os.platform() === 'darwin') + executablePath = path.join(distpath, 'Nightly.app', 'Contents', 'MacOS', 'firefox'); + else if (os.platform() === 'win32') + executablePath = path.join(distpath, 'firefox.exe'); + + const firefoxFolder = path.dirname(executablePath); + + let prefPath = ''; + let configPath = ''; + if (os.platform() === 'darwin') { + prefPath = path.join(firefoxFolder, '..', 'Resources', 'defaults', 'pref'); + configPath = path.join(firefoxFolder, '..', 'Resources'); + } else if (os.platform() === 'linux') { + if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults'))) + await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults')); + if (!fs.existsSync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences'))) + await mkdirAsync(path.join(firefoxFolder, 'browser', 'defaults', 'preferences')); + prefPath = path.join(firefoxFolder, 'browser', 'defaults', 'preferences'); + configPath = firefoxFolder; + } else if (os.platform() === 'win32') { + prefPath = path.join(firefoxFolder, 'defaults', 'pref'); + configPath = firefoxFolder; + } else { + throw new Error('Unsupported platform: ' + os.platform()); + } + + await Promise.all([ + copyFile({ + from: path.join(__dirname, 'preferences', '00-playwright-prefs.js'), + to: path.join(prefPath, '00-playwright-prefs.js'), + }), + copyFile({ + from: path.join(__dirname, 'preferences', 'playwright.cfg'), + to: path.join(configPath, 'playwright.cfg'), + }), + ]); +} + +function copyFile({from, to}) { + const rd = fs.createReadStream(from); + const wr = fs.createWriteStream(to); + return new Promise(function(resolve, reject) { + rd.on('error', reject); + wr.on('error', reject); + wr.on('finish', resolve); + rd.pipe(wr); + }).catch(function(error) { + rd.destroy(); + wr.end(); + throw error; + }); +} + +if (process.argv.length !== 3) { + console.log('ERROR: expected a path to the directory with browser build'); + process.exit(1); + return; +} + +installFirefoxPreferences(process.argv[2]).catch(error => { + console.error('ERROR: failed to put preferences!'); + console.error(error); + process.exit(1); +}); diff --git a/browser_patches/firefox-stable/juggler/Helper.js b/browser_patches/firefox-stable/juggler/Helper.js new file mode 100644 index 0000000000..70d8aef0d3 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/Helper.js @@ -0,0 +1,135 @@ +/* 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 uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +class Helper { + + addObserver(handler, topic) { + Services.obs.addObserver(handler, topic); + return () => Services.obs.removeObserver(handler, topic); + } + + addMessageListener(receiver, eventName, handler) { + receiver.addMessageListener(eventName, handler); + return () => receiver.removeMessageListener(eventName, handler); + } + + addEventListener(receiver, eventName, handler) { + receiver.addEventListener(eventName, handler); + return () => receiver.removeEventListener(eventName, handler); + } + + awaitEvent(receiver, eventName) { + return new Promise(resolve => { + receiver.addEventListener(eventName, function listener() { + receiver.removeEventListener(eventName, listener); + resolve(); + }); + }); + } + + on(receiver, eventName, handler) { + // The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument. + // Fire event listeners without it for convenience. + const handlerWrapper = (_, ...args) => handler(...args); + receiver.on(eventName, handlerWrapper); + return () => receiver.off(eventName, handlerWrapper); + } + + addProgressListener(progress, listener, flags) { + progress.addProgressListener(listener, flags); + return () => progress.removeProgressListener(listener); + } + + removeListeners(listeners) { + for (const tearDown of listeners) + tearDown.call(null); + listeners.splice(0, listeners.length); + } + + generateId() { + const string = uuidGen.generateUUID().toString(); + return string.substring(1, string.length - 1); + } + + getLoadContext(channel) { + let loadContext = null; + try { + if (channel.notificationCallbacks) + loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext); + } catch (e) {} + try { + if (!loadContext && channel.loadGroup) + loadContext = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); + } catch (e) { } + return loadContext; + } + + getNetworkErrorStatusText(status) { + if (!status) + return null; + for (const key of Object.keys(Cr)) { + if (Cr[key] === status) + return key; + } + // Security module. The following is taken from + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL + if ((status & 0xff0000) === 0x5a0000) { + // NSS_SEC errors (happen below the base value because of negative vals) + if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) { + // The bases are actually negative, so in our positive numeric space, we + // need to subtract the base off our value. + const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff); + switch (nssErr) { + case 11: + return 'SEC_ERROR_EXPIRED_CERTIFICATE'; + case 12: + return 'SEC_ERROR_REVOKED_CERTIFICATE'; + case 13: + return 'SEC_ERROR_UNKNOWN_ISSUER'; + case 20: + return 'SEC_ERROR_UNTRUSTED_ISSUER'; + case 21: + return 'SEC_ERROR_UNTRUSTED_CERT'; + case 36: + return 'SEC_ERROR_CA_CERT_INVALID'; + case 90: + return 'SEC_ERROR_INADEQUATE_KEY_USAGE'; + case 176: + return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED'; + default: + return 'SEC_ERROR_UNKNOWN'; + } + } + const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff); + switch (sslErr) { + case 3: + return 'SSL_ERROR_NO_CERTIFICATE'; + case 4: + return 'SSL_ERROR_BAD_CERTIFICATE'; + case 8: + return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE'; + case 9: + return 'SSL_ERROR_UNSUPPORTED_VERSION'; + case 12: + return 'SSL_ERROR_BAD_CERT_DOMAIN'; + default: + return 'SSL_ERROR_UNKNOWN'; + } + } + return ''; + } + + browsingContextToFrameId(browsingContext) { + if (!browsingContext) + return undefined; + return 'frame-' + browsingContext.id; + } +} + +var EXPORTED_SYMBOLS = [ "Helper" ]; +this.Helper = Helper; + diff --git a/browser_patches/firefox-stable/juggler/NetworkObserver.js b/browser_patches/firefox-stable/juggler/NetworkObserver.js new file mode 100644 index 0000000000..b97ddc9074 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/NetworkObserver.js @@ -0,0 +1,913 @@ +/* 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"; + +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); + + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; +const Cm = Components.manager; +const CC = Components.Constructor; +const helper = new Helper(); + +const UINT32_MAX = Math.pow(2, 32)-1; + +const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream'); +const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream'); +const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init'); + +// Cap response storage with 100Mb per tracked tab. +const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024; + +/** + * This is a nsIChannelEventSink implementation that monitors channel redirects. + */ +const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink"; +const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}"); +const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; +const SINK_CATEGORY_NAME = "net-channel-event-sinks"; + +const pageNetworkSymbol = Symbol('PageNetwork'); + +class PageNetwork { + static _forPageTarget(target) { + let result = target[pageNetworkSymbol]; + if (!result) { + result = new PageNetwork(target); + target[pageNetworkSymbol] = result; + } + return result; + } + + constructor(target) { + EventEmitter.decorate(this); + this._target = target; + this._extraHTTPHeaders = null; + this._responseStorage = new ResponseStorage(MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10); + this._requestInterceptionEnabled = false; + // This is requestId => NetworkRequest map, only contains requests that are + // awaiting interception action (abort, resume, fulfill) over the protocol. + this._interceptedRequests = new Map(); + } + + setExtraHTTPHeaders(headers) { + this._extraHTTPHeaders = headers; + } + + enableRequestInterception() { + this._requestInterceptionEnabled = true; + } + + disableRequestInterception() { + this._requestInterceptionEnabled = false; + for (const intercepted of this._interceptedRequests.values()) + intercepted.resume(); + this._interceptedRequests.clear(); + } + + resumeInterceptedRequest(requestId, url, method, headers, postData) { + this._takeIntercepted(requestId).resume(url, method, headers, postData); + } + + fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) { + this._takeIntercepted(requestId).fulfill(status, statusText, headers, base64body); + } + + abortInterceptedRequest(requestId, errorCode) { + this._takeIntercepted(requestId).abort(errorCode); + } + + getResponseBody(requestId) { + if (!this._responseStorage) + throw new Error('Responses are not tracked for the given browser'); + return this._responseStorage.getBase64EncodedResponse(requestId); + } + + _takeIntercepted(requestId) { + const intercepted = this._interceptedRequests.get(requestId); + if (!intercepted) + throw new Error(`Cannot find request "${requestId}"`); + this._interceptedRequests.delete(requestId); + return intercepted; + } +} + +class NetworkRequest { + constructor(networkObserver, httpChannel, redirectedFrom) { + this._networkObserver = networkObserver; + this.httpChannel = httpChannel; + this._networkObserver._channelToRequest.set(this.httpChannel, this); + + const loadInfo = this.httpChannel.loadInfo; + let browsingContext = loadInfo?.frameBrowsingContext || loadInfo?.browsingContext; + // TODO: Unfortunately, requests from web workers don't have frameBrowsingContext or + // browsingContext. + // + // We fail to attribute them to the original frames on the browser side, but we + // can use load context top frame to attribute them to the top frame at least. + if (!browsingContext) { + const loadContext = helper.getLoadContext(this.httpChannel); + browsingContext = loadContext?.topFrameElement?.browsingContext; + } + + this._frameId = helper.browsingContextToFrameId(browsingContext); + + this.requestId = httpChannel.channelId + ''; + this.navigationId = httpChannel.isMainDocumentChannel ? this.requestId : undefined; + + const internalCauseType = this.httpChannel.loadInfo ? this.httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; + + this._redirectedIndex = 0; + const ignoredRedirect = redirectedFrom && !redirectedFrom._sentOnResponse; + if (ignoredRedirect) { + // We just ignore redirect that did not hit the network before being redirected. + // This happens, for example, for automatic http->https redirects. + this.navigationId = redirectedFrom.navigationId; + } else if (redirectedFrom) { + this.redirectedFromId = redirectedFrom.requestId; + this._redirectedIndex = redirectedFrom._redirectedIndex + 1; + this.requestId = this.requestId + '-redirect' + this._redirectedIndex; + this.navigationId = redirectedFrom.navigationId; + // Finish previous request now. Since we inherit the listener, we could in theory + // use onStopRequest, but that will only happen after the last redirect has finished. + redirectedFrom._sendOnRequestFinished(); + } + + this._maybeInactivePageNetwork = this._findPageNetwork(); + this._expectingInterception = false; + this._expectingResumedRequest = undefined; // { method, headers, postData } + this._sentOnResponse = false; + + const pageNetwork = this._activePageNetwork(); + if (pageNetwork) { + appendExtraHTTPHeaders(httpChannel, pageNetwork._target.browserContext().extraHTTPHeaders); + appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders); + } + + this._responseBodyChunks = []; + + httpChannel.QueryInterface(Ci.nsITraceableChannel); + this._originalListener = httpChannel.setNewListener(this); + if (redirectedFrom) { + // Listener is inherited for regular redirects, so we'd like to avoid + // calling into previous NetworkRequest. + this._originalListener = redirectedFrom._originalListener; + } + + this._previousCallbacks = httpChannel.notificationCallbacks; + httpChannel.notificationCallbacks = this; + + this.QueryInterface = ChromeUtils.generateQI([ + Ci.nsIAuthPrompt2, + Ci.nsIAuthPromptProvider, + Ci.nsIInterfaceRequestor, + Ci.nsINetworkInterceptController, + Ci.nsIStreamListener, + ]); + + if (this.redirectedFromId) { + // Redirects are not interceptable. + this._sendOnRequest(false); + } + } + + // Public interception API. + resume(url, method, headers, postData) { + this._expectingResumedRequest = { method, headers, postData }; + const newUri = url ? Services.io.newURI(url) : null; + this._interceptedChannel.resetInterceptionWithURI(newUri); + this._interceptedChannel = undefined; + } + + // Public interception API. + abort(errorCode) { + const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; + this._interceptedChannel.cancelInterception(error); + this._interceptedChannel = undefined; + } + + // Public interception API. + fulfill(status, statusText, headers, base64body) { + this._interceptedChannel.synthesizeStatus(status, statusText); + for (const header of headers) { + this._interceptedChannel.synthesizeHeader(header.name, header.value); + if (header.name.toLowerCase() === 'set-cookie') { + Services.cookies.QueryInterface(Ci.nsICookieService); + Services.cookies.setCookieStringFromHttp(this.httpChannel.URI, header.value, this.httpChannel); + } + } + const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + const body = base64body ? atob(base64body) : ''; + synthesized.data = body; + this._interceptedChannel.startSynthesizedResponse(synthesized, null, null, '', false); + this._interceptedChannel.finishSynthesizedResponse(); + this._interceptedChannel = undefined; + } + + // Instrumentation called by NetworkObserver. + _onInternalRedirect(newChannel) { + // Intercepted requests produce "internal redirects" - this is both for our own + // interception and service workers. + // An internal redirect has the same channelId, inherits notificationCallbacks and + // listener, and should be used instead of an old channel. + this._networkObserver._channelToRequest.delete(this.httpChannel); + this.httpChannel = newChannel; + this._networkObserver._channelToRequest.set(this.httpChannel, this); + } + + // Instrumentation called by NetworkObserver. + _onInternalRedirectReady() { + // Resumed request is first internally redirected to a new request, + // and then the new request is ready to be updated. + if (!this._expectingResumedRequest) + return; + const { method, headers, postData } = this._expectingResumedRequest; + this._expectingResumedRequest = undefined; + + if (headers) { + for (const header of requestHeaders(this.httpChannel)) + this.httpChannel.setRequestHeader(header.name, '', false /* merge */); + for (const header of headers) + this.httpChannel.setRequestHeader(header.name, header.value, false /* merge */); + } + if (method) + this.httpChannel.requestMethod = method; + if (postData && this.httpChannel instanceof Ci.nsIUploadChannel2) { + const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + const body = atob(postData); + synthesized.setData(body, body.length); + + const overriddenHeader = (lowerCaseName, defaultValue) => { + if (headers) { + for (const header of headers) { + if (header.name.toLowerCase() === lowerCaseName) { + return header.value; + } + } + } + return defaultValue; + } + // Clear content-length, so that upload stream resets it. + this.httpChannel.setRequestHeader('content-length', overriddenHeader('content-length', ''), false /* merge */); + this.httpChannel.explicitSetUploadStream(synthesized, overriddenHeader('content-type', 'application/octet-stream'), -1, this.httpChannel.requestMethod, false); + } + } + + // Instrumentation called by NetworkObserver. + _onResponse(fromCache) { + this._sendOnResponse(fromCache); + } + + // nsIInterfaceRequestor + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider) || iid.equals(Ci.nsINetworkInterceptController)) + return this; + if (iid.equals(Ci.nsIAuthPrompt)) // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead. + throw Cr.NS_ERROR_NO_INTERFACE; + if (this._previousCallbacks) + return this._previousCallbacks.getInterface(iid); + throw Cr.NS_ERROR_NO_INTERFACE; + } + + // nsIAuthPromptProvider + getAuthPrompt(aPromptReason, iid) { + return this; + } + + // nsIAuthPrompt2 + asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) { + let canceled = false; + Promise.resolve().then(() => { + if (canceled) + return; + const hasAuth = this.promptAuth(aChannel, level, authInfo); + if (hasAuth) + aCallback.onAuthAvailable(aContext, authInfo); + else + aCallback.onAuthCancelled(aContext, true); + }); + return { + QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]), + cancel: () => { + aCallback.onAuthCancelled(aContext, false); + canceled = true; + } + }; + } + + // nsIAuthPrompt2 + promptAuth(aChannel, level, authInfo) { + if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) + return false; + const pageNetwork = this._activePageNetwork(); + if (!pageNetwork) + return false; + let credentials = null; + if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + const proxy = this._networkObserver._targetRegistry.getProxyInfo(aChannel); + credentials = proxy ? {username: proxy.username, password: proxy.password} : null; + } else { + credentials = pageNetwork._target.browserContext().httpCredentials; + } + if (!credentials) + return false; + authInfo.username = credentials.username; + authInfo.password = credentials.password; + // This will produce a new request with respective auth header set. + // It will have the same id as ours. We expect it to arrive as new request and + // will treat it as our own redirect. + this._networkObserver._expectRedirect(this.httpChannel.channelId + '', this); + return true; + } + + // nsINetworkInterceptController + shouldPrepareForIntercept(aURI, channel) { + const interceptController = this._fallThroughInterceptController(); + if (interceptController && interceptController.shouldPrepareForIntercept(aURI, channel)) { + // We assume that interceptController is a service worker if there is one, + // and yield interception to it. We are not going to intercept ourselves, + // so we send onRequest now. + this._sendOnRequest(false); + return true; + } + + if (channel !== this.httpChannel) { + // Not our channel? Just in case this happens, don't do anything. + return false; + } + + // We do not want to intercept any redirects, because we are not able + // to intercept subresource redirects, and it's unreliable for main requests. + // We do not sendOnRequest here, because redirects do that in constructor. + if (this.redirectedFromId) + return false; + + const shouldIntercept = this._shouldIntercept(); + if (!shouldIntercept) { + // We are not intercepting - ready to issue onRequest. + this._sendOnRequest(false); + return false; + } + + this._expectingInterception = true; + return true; + } + + // nsINetworkInterceptController + channelIntercepted(intercepted) { + if (!this._expectingInterception) { + // We are not intercepting, fall-through. + const interceptController = this._fallThroughInterceptController(); + if (interceptController) + interceptController.channelIntercepted(intercepted); + return; + } + + this._expectingInterception = false; + this._interceptedChannel = intercepted.QueryInterface(Ci.nsIInterceptedChannel); + + const pageNetwork = this._activePageNetwork(); + if (!pageNetwork) { + // Just in case we disabled instrumentation while intercepting, resume and forget. + this.resume(); + return; + } + + const browserContext = pageNetwork._target.browserContext(); + if (browserContext.settings.onlineOverride === 'offline') { + // Implement offline. + this.abort(Cr.NS_ERROR_OFFLINE); + return; + } + + // Ok, so now we have intercepted the request, let's issue onRequest. + // If interception has been disabled while we were intercepting, resume and forget. + const interceptionEnabled = this._shouldIntercept(); + this._sendOnRequest(!!interceptionEnabled); + if (interceptionEnabled) + pageNetwork._interceptedRequests.set(this.requestId, this); + else + this.resume(); + } + + // nsIStreamListener + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + // For requests with internal redirect (e.g. intercepted by Service Worker), + // we do not get onResponse normally, but we do get nsIStreamListener notifications. + this._sendOnResponse(false); + + const iStream = new BinaryInputStream(aInputStream); + const sStream = new StorageStream(8192, aCount, null); + const oStream = new BinaryOutputStream(sStream.getOutputStream(0)); + + // Copy received data as they come. + const data = iStream.readBytes(aCount); + this._responseBodyChunks.push(data); + + oStream.writeBytes(data, aCount); + try { + this._originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); + } catch (e) { + // Be ready to original listener exceptions. + } + } + + // nsIStreamListener + onStartRequest(aRequest) { + try { + this._originalListener.onStartRequest(aRequest); + } catch (e) { + // Be ready to original listener exceptions. + } + } + + // nsIStreamListener + onStopRequest(aRequest, aStatusCode) { + try { + this._originalListener.onStopRequest(aRequest, aStatusCode); + } catch (e) { + // Be ready to original listener exceptions. + } + + if (aStatusCode === 0) { + // For requests with internal redirect (e.g. intercepted by Service Worker), + // we do not get onResponse normally, but we do get nsIRequestObserver notifications. + this._sendOnResponse(false); + const body = this._responseBodyChunks.join(''); + const pageNetwork = this._activePageNetwork(); + if (pageNetwork) + pageNetwork._responseStorage.addResponseBody(this, body); + this._sendOnRequestFinished(); + } else { + this._sendOnRequestFailed(aStatusCode); + } + + delete this._responseBodyChunks; + } + + _shouldIntercept() { + const pageNetwork = this._activePageNetwork(); + if (!pageNetwork) + return false; + if (pageNetwork._requestInterceptionEnabled) + return true; + const browserContext = pageNetwork._target.browserContext(); + if (browserContext.requestInterceptionEnabled) + return true; + if (browserContext.settings.onlineOverride === 'offline') + return true; + return false; + } + + _fallThroughInterceptController() { + if (!this._previousCallbacks || !(this._previousCallbacks instanceof Ci.nsINetworkInterceptController)) + return undefined; + return this._previousCallbacks.getInterface(Ci.nsINetworkInterceptController); + } + + _activePageNetwork() { + if (!this._maybeInactivePageNetwork) + return undefined; + return this._maybeInactivePageNetwork; + } + + _findPageNetwork() { + let loadContext = helper.getLoadContext(this.httpChannel); + if (!loadContext) + return; + const target = this._networkObserver._targetRegistry.targetForBrowser(loadContext.topFrameElement); + if (!target) + return; + return PageNetwork._forPageTarget(target); + } + + _sendOnRequest(isIntercepted) { + // Note: we call _sendOnRequest either after we intercepted the request, + // or at the first moment we know that we are not going to intercept. + const pageNetwork = this._activePageNetwork(); + if (!pageNetwork) + return; + const loadInfo = this.httpChannel.loadInfo; + const causeType = loadInfo?.externalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; + const internalCauseType = loadInfo?.internalContentPolicyType || Ci.nsIContentPolicy.TYPE_OTHER; + pageNetwork.emit(PageNetwork.Events.Request, { + url: this.httpChannel.URI.spec, + frameId: this._frameId, + isIntercepted, + requestId: this.requestId, + redirectedFrom: this.redirectedFromId, + postData: readRequestPostData(this.httpChannel), + headers: requestHeaders(this.httpChannel), + method: this.httpChannel.requestMethod, + navigationId: this.navigationId, + cause: causeTypeToString(causeType), + internalCause: causeTypeToString(internalCauseType), + }, this._frameId); + } + + _sendOnResponse(fromCache) { + if (this._sentOnResponse) { + // We can come here twice because of internal redirects, e.g. service workers. + return; + } + this._sentOnResponse = true; + const pageNetwork = this._activePageNetwork(); + if (!pageNetwork) + return; + + this.httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); + this.httpChannel.QueryInterface(Ci.nsITimedChannel); + const timing = { + startTime: this.httpChannel.channelCreationTime, + domainLookupStart: this.httpChannel.domainLookupStartTime, + domainLookupEnd: this.httpChannel.domainLookupEndTime, + connectStart: this.httpChannel.connectStartTime, + secureConnectionStart: this.httpChannel.secureConnectionStartTime, + connectEnd: this.httpChannel.connectEndTime, + requestStart: this.httpChannel.requestStartTime, + responseStart: this.httpChannel.responseStartTime, + }; + + const headers = []; + let status = 0; + let statusText = ''; + try { + status = this.httpChannel.responseStatus; + statusText = this.httpChannel.responseStatusText; + this.httpChannel.visitResponseHeaders({ + visitHeader: (name, value) => headers.push({name, value}), + }); + } catch (e) { + // Response headers, status and/or statusText are not available + // when redirect did not actually hit the network. + } + + let remoteIPAddress = undefined; + let remotePort = undefined; + try { + remoteIPAddress = this.httpChannel.remoteAddress; + remotePort = this.httpChannel.remotePort; + } catch (e) { + // remoteAddress is not defined for cached requests. + } + + pageNetwork.emit(PageNetwork.Events.Response, { + requestId: this.requestId, + securityDetails: getSecurityDetails(this.httpChannel), + fromCache, + headers, + remoteIPAddress, + remotePort, + status, + statusText, + timing, + }, this._frameId); + } + + _sendOnRequestFailed(error) { + const pageNetwork = this._activePageNetwork(); + if (pageNetwork) { + pageNetwork.emit(PageNetwork.Events.RequestFailed, { + requestId: this.requestId, + errorCode: helper.getNetworkErrorStatusText(error), + }, this._frameId); + } + this._networkObserver._channelToRequest.delete(this.httpChannel); + } + + _sendOnRequestFinished() { + const pageNetwork = this._activePageNetwork(); + if (pageNetwork) { + pageNetwork.emit(PageNetwork.Events.RequestFinished, { + requestId: this.requestId, + responseEndTime: this.httpChannel.responseEndTime, + }, this._frameId); + } + this._networkObserver._channelToRequest.delete(this.httpChannel); + } +} + +class NetworkObserver { + static instance() { + return NetworkObserver._instance || null; + } + + constructor(targetRegistry) { + EventEmitter.decorate(this); + NetworkObserver._instance = this; + + this._targetRegistry = targetRegistry; + + this._channelToRequest = new Map(); // http channel -> network request + this._expectedRedirect = new Map(); // expected redirect channel id (string) -> network request + + const protocolProxyService = Cc['@mozilla.org/network/protocol-proxy-service;1'].getService(); + this._channelProxyFilter = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolProxyChannelFilter]), + applyFilter: (channel, defaultProxyInfo, proxyFilter) => { + const proxy = this._targetRegistry.getProxyInfo(channel); + if (!proxy) { + proxyFilter.onProxyFilterResult(defaultProxyInfo); + return; + } + proxyFilter.onProxyFilterResult(protocolProxyService.newProxyInfo( + proxy.type, + proxy.host, + proxy.port, + '', /* aProxyAuthorizationHeader */ + '', /* aConnectionIsolationKey */ + 0, /* aFlags */ + UINT32_MAX, /* aFailoverTimeout */ + null, /* failover proxy */ + )); + }, + }; + protocolProxyService.registerChannelFilter(this._channelProxyFilter, 0 /* position */); + + this._channelSink = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]), + asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => { + this._onRedirect(oldChannel, newChannel, flags); + callback.onRedirectVerifyCallback(Cr.NS_OK); + }, + }; + this._channelSinkFactory = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]), + createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID), + }; + // Register self as ChannelEventSink to track redirects. + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory); + Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true); + + this._eventListeners = [ + helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'), + helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'), + helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'), + helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'), + ]; + } + + _expectRedirect(channelId, previous) { + this._expectedRedirect.set(channelId, previous); + } + + _onRedirect(oldChannel, newChannel, flags) { + if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel)) + return; + const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel); + const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel); + if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) { + const previous = this._channelToRequest.get(oldHttpChannel); + if (previous) + this._expectRedirect(newHttpChannel.channelId + '', previous); + } else { + const request = this._channelToRequest.get(oldHttpChannel); + if (request) + request._onInternalRedirect(newHttpChannel); + } + } + + pageNetworkForTarget(target) { + return PageNetwork._forPageTarget(target); + } + + _onRequest(channel, topic) { + if (!(channel instanceof Ci.nsIHttpChannel)) + return; + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const channelId = httpChannel.channelId + ''; + const redirectedFrom = this._expectedRedirect.get(channelId); + if (redirectedFrom) { + this._expectedRedirect.delete(channelId); + new NetworkRequest(this, httpChannel, redirectedFrom); + } else { + const redirectedRequest = this._channelToRequest.get(httpChannel); + if (redirectedRequest) + redirectedRequest._onInternalRedirectReady(); + else + new NetworkRequest(this, httpChannel); + } + } + + _onResponse(fromCache, httpChannel, topic) { + const request = this._channelToRequest.get(httpChannel); + if (request) + request._onResponse(fromCache); + } + + dispose() { + this._activityDistributor.removeObserver(this); + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory); + Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false); + helper.removeListeners(this._eventListeners); + } +} + +const protocolVersionNames = { + [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1', + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1', + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2', + [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3', +}; + +function getSecurityDetails(httpChannel) { + const securityInfo = httpChannel.securityInfo; + if (!securityInfo) + return null; + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + if (!securityInfo.serverCert) + return null; + return { + protocol: protocolVersionNames[securityInfo.protocolVersion] || '', + subjectName: securityInfo.serverCert.commonName, + issuer: securityInfo.serverCert.issuerCommonName, + // Convert to seconds. + validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000, + validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000, + }; +} + +function readRequestPostData(httpChannel) { + if (!(httpChannel instanceof Ci.nsIUploadChannel)) + return undefined; + const iStream = httpChannel.uploadStream; + if (!iStream) + return undefined; + const isSeekableStream = iStream instanceof Ci.nsISeekableStream; + + let prevOffset; + if (isSeekableStream) { + prevOffset = iStream.tell(); + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + + // Read data from the stream. + let result = undefined; + try { + const buffer = NetUtil.readInputStream(iStream, iStream.available()); + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) + binary += String.fromCharCode(bytes[i]); + result = btoa(binary); + } catch (err) { + result = ''; + } + + // Seek locks the file, so seek to the beginning only if necko hasn't + // read it yet, since necko doesn't seek to 0 before reading (at lest + // not till 459384 is fixed). + if (isSeekableStream && prevOffset == 0) + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + return result; +} + +function requestHeaders(httpChannel) { + const headers = []; + httpChannel.visitRequestHeaders({ + visitHeader: (name, value) => headers.push({name, value}), + }); + return headers; +} + +function causeTypeToString(causeType) { + for (let key in Ci.nsIContentPolicy) { + if (Ci.nsIContentPolicy[key] === causeType) + return key; + } + return 'TYPE_OTHER'; +} + +function appendExtraHTTPHeaders(httpChannel, headers) { + if (!headers) + return; + for (const header of headers) + httpChannel.setRequestHeader(header.name, header.value, false /* merge */); +} + +class ResponseStorage { + constructor(maxTotalSize, maxResponseSize) { + this._totalSize = 0; + this._maxResponseSize = maxResponseSize; + this._maxTotalSize = maxTotalSize; + this._responses = new Map(); + } + + addResponseBody(request, body) { + if (body.length > this._maxResponseSize) { + this._responses.set(request.requestId, { + evicted: true, + body: '', + }); + return; + } + let encodings = []; + if ((request.httpChannel instanceof Ci.nsIEncodedChannel) && request.httpChannel.contentEncodings && !request.httpChannel.applyConversion) { + const encodingHeader = request.httpChannel.getResponseHeader("Content-Encoding"); + encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + } + this._responses.set(request.requestId, {body, encodings}); + this._totalSize += body.length; + if (this._totalSize > this._maxTotalSize) { + for (let [requestId, response] of this._responses) { + this._totalSize -= response.body.length; + response.body = ''; + response.evicted = true; + if (this._totalSize < this._maxTotalSize) + break; + } + } + } + + getBase64EncodedResponse(requestId) { + const response = this._responses.get(requestId); + if (!response) + throw new Error(`Request "${requestId}" is not found`); + if (response.evicted) + return {base64body: '', evicted: true}; + let result = response.body; + if (response.encodings && response.encodings.length) { + for (const encoding of response.encodings) + result = convertString(result, encoding, 'uncompressed'); + } + return {base64body: btoa(result)}; + } +} + +function convertString(s, source, dest) { + const is = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + is.setData(s, s.length); + const listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + let result = []; + listener.init({ + onStreamComplete: function onStreamComplete( + loader, + context, + status, + length, + data + ) { + const array = Array.from(data); + const kChunk = 100000; + for (let i = 0; i < length; i += kChunk) { + const len = Math.min(kChunk, length - i); + const chunk = String.fromCharCode.apply(this, array.slice(i, i + len)); + result.push(chunk); + } + }, + }); + const converter = Cc["@mozilla.org/streamConverters;1"].getService( + Ci.nsIStreamConverterService + ).asyncConvertData( + source, + dest, + listener, + null + ); + converter.onStartRequest(null, null); + converter.onDataAvailable(null, is, 0, s.length); + converter.onStopRequest(null, null, null); + return result.join(''); +} + +const errorMap = { + 'aborted': Cr.NS_ERROR_ABORT, + 'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED, + 'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST, + 'blockedbyclient': Cr.NS_ERROR_FAILURE, + 'blockedbyresponse': Cr.NS_ERROR_FAILURE, + 'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT, + 'connectionclosed': Cr.NS_ERROR_FAILURE, + 'connectionfailed': Cr.NS_ERROR_FAILURE, + 'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED, + 'connectionreset': Cr.NS_ERROR_NET_RESET, + 'internetdisconnected': Cr.NS_ERROR_OFFLINE, + 'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST, + 'timedout': Cr.NS_ERROR_NET_TIMEOUT, + 'failed': Cr.NS_ERROR_FAILURE, +}; + +PageNetwork.Events = { + Request: Symbol('PageNetwork.Events.Request'), + Response: Symbol('PageNetwork.Events.Response'), + RequestFinished: Symbol('PageNetwork.Events.RequestFinished'), + RequestFailed: Symbol('PageNetwork.Events.RequestFailed'), +}; + +var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork']; +this.NetworkObserver = NetworkObserver; +this.PageNetwork = PageNetwork; diff --git a/browser_patches/firefox-stable/juggler/SimpleChannel.js b/browser_patches/firefox-stable/juggler/SimpleChannel.js new file mode 100644 index 0000000000..59b29532ab --- /dev/null +++ b/browser_patches/firefox-stable/juggler/SimpleChannel.js @@ -0,0 +1,180 @@ +/* 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"; +// Note: this file should be loadabale with eval() into worker environment. +// Avoid Components.*, ChromeUtils and global const variables. + +const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel'; + +class SimpleChannel { + static createForMessageManager(name, mm) { + const channel = new SimpleChannel(name); + + const messageListener = { + receiveMessage: message => channel._onMessage(message.data) + }; + mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener); + + channel.setTransport({ + sendMessage: obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj), + dispose: () => mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener), + }); + + return channel; + } + + constructor(name) { + this._name = name; + this._messageId = 0; + this._connectorId = 0; + this._pendingMessages = new Map(); + this._handlers = new Map(); + this._bufferedIncomingMessages = []; + this._bufferedOutgoingMessages = []; + this.transport = { + sendMessage: null, + dispose: null, + }; + this._ready = false; + this._disposed = false; + } + + setTransport(transport) { + this.transport = transport; + // connection handshake: + // 1. There are two channel ends in different processes. + // 2. Both ends start in the `ready = false` state, meaning that they will + // not send any messages over transport. + // 3. Once channel end is created, it sends `READY` message to the other end. + // 4. Eventually, at least one of the ends receives `READY` message and responds with + // `READY_ACK`. We assume at least one of the ends will receive "READY" event from the other, since + // channel ends have a "parent-child" relation, i.e. one end is always created before the other one. + // 5. Once channel end receives either `READY` or `READY_ACK`, it transitions to `ready` state. + this.transport.sendMessage('READY'); + } + + _markAsReady() { + if (this._ready) + return; + this._ready = true; + for (const msg of this._bufferedOutgoingMessages) + this.transport.sendMessage(msg); + this._bufferedOutgoingMessages = []; + } + + dispose() { + if (this._disposed) + return; + this._disposed = true; + for (const {resolve, reject, methodName} of this._pendingMessages.values()) + reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`)); + this._pendingMessages.clear(); + this._handlers.clear(); + this.transport.dispose(); + } + + _rejectCallbacksFromConnector(connectorId) { + for (const [messageId, callback] of this._pendingMessages) { + if (callback.connectorId === connectorId) { + callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`)); + this._pendingMessages.delete(messageId); + } + } + } + + connect(namespace) { + const connectorId = ++this._connectorId; + return { + send: (...args) => this._send(namespace, connectorId, ...args), + emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}), + dispose: () => this._rejectCallbacksFromConnector(connectorId), + }; + } + + register(namespace, handler) { + if (this._handlers.has(namespace)) + throw new Error('ERROR: double-register for namespace ' + namespace); + this._handlers.set(namespace, handler); + // Try to re-deliver all pending messages. + const bufferedRequests = this._bufferedIncomingMessages; + this._bufferedIncomingMessages = []; + for (const data of bufferedRequests) { + this._onMessage(data); + } + return () => this.unregister(namespace); + } + + unregister(namespace) { + this._handlers.delete(namespace); + } + + /** + * @param {string} namespace + * @param {number} connectorId + * @param {string} methodName + * @param {...*} params + * @return {!Promise<*>} + */ + async _send(namespace, connectorId, methodName, ...params) { + if (this._disposed) + throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`); + const id = ++this._messageId; + const promise = new Promise((resolve, reject) => { + this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace}); + }); + const message = {requestId: id, methodName, params, namespace}; + if (this._ready) + this.transport.sendMessage(message); + else + this._bufferedOutgoingMessages.push(message); + return promise; + } + + async _onMessage(data) { + if (data === 'READY') { + this.transport.sendMessage('READY_ACK'); + this._markAsReady(); + return; + } + if (data === 'READY_ACK') { + this._markAsReady(); + return; + } + if (data.responseId) { + const {resolve, reject} = this._pendingMessages.get(data.responseId); + this._pendingMessages.delete(data.responseId); + if (data.error) + reject(new Error(data.error)); + else + resolve(data.result); + } else if (data.requestId) { + const namespace = data.namespace; + const handler = this._handlers.get(namespace); + if (!handler) { + this._bufferedIncomingMessages.push(data); + return; + } + const method = handler[data.methodName]; + if (!method) { + this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`}); + return; + } + try { + const result = await method.call(handler, ...data.params); + this.transport.sendMessage({responseId: data.requestId, result}); + } catch (error) { + this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`}); + return; + } + } else { + dump(` + ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)} + `); + } + } +} + +var EXPORTED_SYMBOLS = ['SimpleChannel']; +this.SimpleChannel = SimpleChannel; diff --git a/browser_patches/firefox-stable/juggler/TargetRegistry.js b/browser_patches/firefox-stable/juggler/TargetRegistry.js new file mode 100644 index 0000000000..fe80fb23ed --- /dev/null +++ b/browser_patches/firefox-stable/juggler/TargetRegistry.js @@ -0,0 +1,914 @@ +/* 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 {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm"); +const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm"); +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); +const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + +const helper = new Helper(); + +const IDENTITY_NAME = 'JUGGLER '; +const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100; + +const ALL_PERMISSIONS = [ + 'geo', + 'desktop-notification', +]; + +class DownloadInterceptor { + constructor(registry) { + this._registry = registry + this._handlerToUuid = new Map(); + } + + // + // nsIDownloadInterceptor implementation. + // + interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) { + if (!(request instanceof Ci.nsIChannel)) + return false; + const channel = request.QueryInterface(Ci.nsIChannel); + let pageTarget = this._registry._browserBrowsingContextToTarget.get(channel.loadInfo.browsingContext); + if (!pageTarget) + return false; + + const browserContext = pageTarget.browserContext(); + const options = browserContext.downloadOptions; + if (!options) + return false; + + const uuid = helper.generateId(); + let file = null; + if (options.behavior === 'saveToDisk') { + file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(options.downloadsDir); + file.append(uuid); + + try { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } catch (e) { + dump(`interceptDownloadRequest failed to create file: ${e}\n`); + return false; + } + } + outFile.value = file; + this._handlerToUuid.set(externalAppHandler, uuid); + const downloadInfo = { + uuid, + browserContextId: browserContext.browserContextId, + pageTargetId: pageTarget.id(), + url: request.name, + suggestedFileName: externalAppHandler.suggestedFileName, + }; + this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo); + return true; + } + + onDownloadComplete(externalAppHandler, canceled, errorName) { + const uuid = this._handlerToUuid.get(externalAppHandler); + if (!uuid) + return; + this._handlerToUuid.delete(externalAppHandler); + const downloadInfo = { + uuid, + }; + if (errorName === 'NS_BINDING_ABORTED') { + downloadInfo.canceled = true; + } else { + downloadInfo.error = errorName; + } + this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo); + } +} + +class TargetRegistry { + constructor() { + EventEmitter.decorate(this); + + this._browserContextIdToBrowserContext = new Map(); + this._userContextIdToBrowserContext = new Map(); + this._browserToTarget = new Map(); + this._browserBrowsingContextToTarget = new Map(); + + this._browserProxy = null; + + // Cleanup containers from previous runs (if any) + for (const identity of ContextualIdentityService.getPublicIdentities()) { + if (identity.name && identity.name.startsWith(IDENTITY_NAME)) { + ContextualIdentityService.remove(identity.userContextId); + ContextualIdentityService.closeContainerTabs(identity.userContextId); + } + } + + this._defaultContext = new BrowserContext(this, undefined, undefined); + + Services.obs.addObserver({ + observe: (subject, topic, data) => { + const browser = subject.ownerElement; + if (!browser) + return; + const target = this._browserToTarget.get(browser); + if (!target) + return; + target.emit(PageTarget.Events.Crashed); + target.dispose(); + } + }, 'oop-frameloader-crashed'); + + Services.mm.addMessageListener('juggler:content-ready', { + receiveMessage: message => { + const linkedBrowser = message.target; + const target = this._browserToTarget.get(linkedBrowser); + if (!target) + return; + + return { + scriptsToEvaluateOnNewDocument: target.browserContext().scriptsToEvaluateOnNewDocument, + bindings: target.browserContext().bindings, + settings: target.browserContext().settings, + }; + }, + }); + + const onTabOpenListener = (appWindow, window, event) => { + const tab = event.target; + const userContextId = tab.userContextId; + const browserContext = this._userContextIdToBrowserContext.get(userContextId); + const hasExplicitSize = appWindow && (appWindow.chromeFlags & Ci.nsIWebBrowserChrome.JUGGLER_WINDOW_EXPLICIT_SIZE) !== 0; + const openerContext = tab.linkedBrowser.browsingContext.opener; + let openerTarget; + if (openerContext) { + // Popups usually have opener context. + openerTarget = this._browserBrowsingContextToTarget.get(openerContext); + } else if (tab.openerTab) { + // Noopener popups from the same window have opener tab instead. + openerTarget = this._browserToTarget.get(tab.openerTab.linkedBrowser); + } + if (!browserContext) + throw new Error(`Internal error: cannot find context for userContextId=${userContextId}`); + const target = new PageTarget(this, window, tab, browserContext, openerTarget); + target.updateUserAgent(); + target.updateTouchOverride(); + target.updateColorSchemeOverride(); + if (!hasExplicitSize) + target.updateViewportSize(); + if (browserContext.screencastOptions) + target._startVideoRecording(browserContext.screencastOptions); + }; + + const onTabCloseListener = event => { + const tab = event.target; + const linkedBrowser = tab.linkedBrowser; + const target = this._browserToTarget.get(linkedBrowser); + if (target) + target.dispose(); + }; + + const domWindowTabListeners = new Map(); + + const onOpenWindow = async (appWindow) => { + + let domWindow; + if (appWindow instanceof Ci.nsIAppWindow) { + domWindow = appWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); + } else { + domWindow = appWindow; + appWindow = null; + } + if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) + return; + // In persistent mode, window might be opened long ago and might be + // already initialized. + // + // In this case, we want to keep this callback synchronous so that we will call + // `onTabOpenListener` synchronously and before the sync IPc message `juggler:content-ready`. + if (domWindow.document.readyState === 'uninitialized' || domWindow.document.readyState === 'loading') { + // For non-initialized windows, DOMContentLoaded initializes gBrowser + // and starts tab loading (see //browser/base/content/browser.js), so we + // are guaranteed to call `onTabOpenListener` before the sync IPC message + // `juggler:content-ready`. + await helper.awaitEvent(domWindow, 'DOMContentLoaded'); + } + + if (!domWindow.gBrowser) + return; + const tabContainer = domWindow.gBrowser.tabContainer; + domWindowTabListeners.set(domWindow, [ + helper.addEventListener(tabContainer, 'TabOpen', event => onTabOpenListener(appWindow, domWindow, event)), + helper.addEventListener(tabContainer, 'TabClose', onTabCloseListener), + ]); + for (const tab of domWindow.gBrowser.tabs) + onTabOpenListener(appWindow, domWindow, { target: tab }); + }; + + const onCloseWindow = window => { + const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); + if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) + return; + if (!domWindow.gBrowser) + return; + + const listeners = domWindowTabListeners.get(domWindow) || []; + domWindowTabListeners.delete(domWindow); + helper.removeListeners(listeners); + for (const tab of domWindow.gBrowser.tabs) + onTabCloseListener({ target: tab }); + }; + + const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService); + extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this)); + + Services.wm.addListener({ onOpenWindow, onCloseWindow }); + for (const win of Services.wm.getEnumerator(null)) + onOpenWindow(win); + } + + setBrowserProxy(proxy) { + this._browserProxy = proxy; + } + + getProxyInfo(channel) { + const originAttributes = channel.loadInfo && channel.loadInfo.originAttributes; + const browserContext = originAttributes ? this.browserContextForUserContextId(originAttributes.userContextId) : null; + // Prefer context proxy and fallback to browser-level proxy. + const proxyInfo = (browserContext && browserContext._proxy) || this._browserProxy; + if (!proxyInfo || proxyInfo.bypass.some(domainSuffix => channel.URI.host.endsWith(domainSuffix))) + return null; + return proxyInfo; + } + + defaultContext() { + return this._defaultContext; + } + + createBrowserContext(removeOnDetach) { + return new BrowserContext(this, helper.generateId(), removeOnDetach); + } + + browserContextForId(browserContextId) { + return this._browserContextIdToBrowserContext.get(browserContextId); + } + + browserContextForUserContextId(userContextId) { + return this._userContextIdToBrowserContext.get(userContextId); + } + + async newPage({browserContextId}) { + const browserContext = this.browserContextForId(browserContextId); + const features = "chrome,dialog=no,all"; + // See _callWithURIToLoad in browser.js for the structure of window.arguments + // window.arguments[1]: unused (bug 871161) + // [2]: referrerInfo (nsIReferrerInfo) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + // [5]: userContextId (int) + // [6]: originPrincipal (nsIPrincipal) + // [7]: originStoragePrincipal (nsIPrincipal) + // [8]: triggeringPrincipal (nsIPrincipal) + // [9]: allowInheritPrincipal (bool) + // [10]: csp (nsIContentSecurityPolicy) + // [11]: nsOpenWindowInfo + const args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + const urlSupports = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + urlSupports.data = 'about:blank'; + args.appendElement(urlSupports); // 0 + args.appendElement(undefined); // 1 + args.appendElement(undefined); // 2 + args.appendElement(undefined); // 3 + args.appendElement(undefined); // 4 + const userContextIdSupports = Cc[ + "@mozilla.org/supports-PRUint32;1" + ].createInstance(Ci.nsISupportsPRUint32); + userContextIdSupports.data = browserContext.userContextId; + args.appendElement(userContextIdSupports); // 5 + args.appendElement(undefined); // 6 + args.appendElement(undefined); // 7 + args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); // 8 + + const window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args); + await waitForWindowReady(window); + if (window.gBrowser.browsers.length !== 1) + throw new Error(`Unexpected number of tabs in the new window: ${window.gBrowser.browsers.length}`); + const browser = window.gBrowser.browsers[0]; + const target = this._browserToTarget.get(browser); + browser.focus(); + if (browserContext.settings.timezoneId) { + if (await target.hasFailedToOverrideTimezone()) + throw new Error('Failed to override timezone'); + } + return target.id(); + } + + targets() { + return Array.from(this._browserToTarget.values()); + } + + targetForBrowser(browser) { + return this._browserToTarget.get(browser); + } +} + +class PageTarget { + constructor(registry, win, tab, browserContext, opener) { + EventEmitter.decorate(this); + + this._targetId = helper.generateId(); + this._registry = registry; + this._window = win; + this._gBrowser = win.gBrowser; + this._tab = tab; + this._linkedBrowser = tab.linkedBrowser; + this._browserContext = browserContext; + this._viewportSize = undefined; + this._initialDPPX = this._linkedBrowser.browsingContext.overrideDPPX; + this._url = 'about:blank'; + this._openerId = opener ? opener.id() : undefined; + this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); + this._screencastInfo = undefined; + this._dialogs = new Map(); + + const navigationListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), + onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), + }; + this._eventListeners = [ + helper.addObserver(this._updateModalDialogs.bind(this), 'tabmodal-dialog-loaded'), + helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), + helper.addEventListener(this._linkedBrowser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), + ]; + + this._disposed = false; + browserContext.pages.add(this); + this._registry._browserToTarget.set(this._linkedBrowser, this); + this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this); + + this._registry.emit(TargetRegistry.Events.TargetCreated, this); + } + + dialog(dialogId) { + return this._dialogs.get(dialogId); + } + + dialogs() { + return [...this._dialogs.values()]; + } + + async windowReady() { + await waitForWindowReady(this._window); + } + + linkedBrowser() { + return this._linkedBrowser; + } + + browserContext() { + return this._browserContext; + } + + updateTouchOverride() { + this._linkedBrowser.browsingContext.touchEventsOverride = this._browserContext.touchOverride ? 'enabled' : 'none'; + } + + updateUserAgent() { + this._linkedBrowser.browsingContext.customUserAgent = this._browserContext.defaultUserAgent; + } + + _updateModalDialogs() { + const prompts = new Set(this._linkedBrowser.tabModalPromptBox ? this._linkedBrowser.tabModalPromptBox.listPrompts() : []); + for (const dialog of this._dialogs.values()) { + if (!prompts.has(dialog.prompt())) { + this._dialogs.delete(dialog.id()); + this.emit(PageTarget.Events.DialogClosed, dialog); + } else { + prompts.delete(dialog.prompt()); + } + } + for (const prompt of prompts) { + const dialog = Dialog.createIfSupported(prompt); + if (!dialog) + continue; + this._dialogs.set(dialog.id(), dialog); + this.emit(PageTarget.Events.DialogOpened, dialog); + } + } + + async updateViewportSize() { + // Viewport size is defined by three arguments: + // 1. default size. Could be explicit if set as part of `window.open` call, e.g. + // `window.open(url, title, 'width=400,height=400')` + // 2. page viewport size + // 3. browserContext viewport size + // + // The "default size" (1) is only respected when the page is opened. + // Otherwise, explicitly set page viewport prevales over browser context + // default viewport. + const viewportSize = this._viewportSize || this._browserContext.defaultViewportSize; + const actualSize = await setViewportSizeForBrowser(viewportSize, this._linkedBrowser, this._window); + this._linkedBrowser.browsingContext.overrideDPPX = this._browserContext.deviceScaleFactor || this._initialDPPX; + await this._channel.connect('').send('awaitViewportDimensions', { + width: actualSize.width, + height: actualSize.height, + deviceSizeIsPageSize: !!this._browserContext.deviceScaleFactor, + }); + } + + setEmulatedMedia(mediumOverride) { + this._linkedBrowser.browsingContext.mediumOverride = mediumOverride || ''; + } + + setColorScheme(colorScheme) { + this.colorScheme = fromProtocolColorScheme(colorScheme); + this.updateColorSchemeOverride(); + } + + updateColorSchemeOverride() { + this._linkedBrowser.browsingContext.prefersColorSchemeOverride = this.colorScheme || this._browserContext.colorScheme || 'none'; + } + + + async setViewportSize(viewportSize) { + this._viewportSize = viewportSize; + await this.updateViewportSize(); + } + + close(runBeforeUnload = false) { + this._gBrowser.removeTab(this._tab, { + skipPermitUnload: !runBeforeUnload, + }); + } + + channel() { + return this._channel; + } + + id() { + return this._targetId; + } + + info() { + return { + targetId: this.id(), + type: 'page', + browserContextId: this._browserContext.browserContextId, + openerId: this._openerId, + }; + } + + _onNavigated(aLocation) { + this._url = aLocation.spec; + this._browserContext.grantPermissionsToOrigin(this._url); + } + + async ensurePermissions() { + await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e); + } + + async addScriptToEvaluateOnNewDocument(script) { + await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e); + } + + async addBinding(name, script) { + await this._channel.connect('').send('addBinding', { name, script }).catch(e => void e); + } + + async applyContextSetting(name, value) { + await this._channel.connect('').send('applyContextSetting', { name, value }).catch(e => void e); + } + + async hasFailedToOverrideTimezone() { + return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true); + } + + async _startVideoRecording({width, height, scale, dir}) { + // On Mac the window may not yet be visible when TargetCreated and its + // NSWindow.windowNumber may be -1, so we wait until the window is known + // to be initialized and visible. + await this.windowReady(); + const file = OS.Path.join(dir, helper.generateId() + '.webm'); + if (width < 10 || width > 10000 || height < 10 || height > 10000) + throw new Error("Invalid size"); + if (scale && (scale <= 0 || scale > 1)) + throw new Error("Unsupported scale"); + + const screencast = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService); + const docShell = this._gBrowser.ownerGlobal.docShell; + // Exclude address bar and navigation control from the video. + const rect = this.linkedBrowser().getBoundingClientRect(); + const devicePixelRatio = this._window.devicePixelRatio; + const videoSessionId = screencast.startVideoRecording(docShell, file, width, height, scale || 0, devicePixelRatio * rect.top); + this._screencastInfo = { videoSessionId, file }; + this.emit(PageTarget.Events.ScreencastStarted); + } + + async _stopVideoRecording() { + if (!this._screencastInfo) + throw new Error('No video recording in progress'); + const screencastInfo = this._screencastInfo; + this._screencastInfo = undefined; + const screencast = Cc['@mozilla.org/juggler/screencast;1'].getService(Ci.nsIScreencastService); + const result = new Promise(resolve => + Services.obs.addObserver(function onStopped(subject, topic, data) { + if (screencastInfo.videoSessionId != data) + return; + + Services.obs.removeObserver(onStopped, 'juggler-screencast-stopped'); + resolve(); + }, 'juggler-screencast-stopped') + ); + screencast.stopVideoRecording(screencastInfo.videoSessionId); + return result; + } + + screencastInfo() { + return this._screencastInfo; + } + + dispose() { + this._disposed = true; + if (this._screencastInfo) + this._stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`)); + this._browserContext.pages.delete(this); + this._registry._browserToTarget.delete(this._linkedBrowser); + this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext); + try { + helper.removeListeners(this._eventListeners); + } catch (e) { + // In some cases, removing listeners from this._linkedBrowser fails + // because it is already half-destroyed. + if (e) + dump(e.message + '\n' + e.stack + '\n'); + } + this._registry.emit(TargetRegistry.Events.TargetDestroyed, this); + } +} + +PageTarget.Events = { + ScreencastStarted: Symbol('PageTarget.ScreencastStarted'), + Crashed: Symbol('PageTarget.Crashed'), + DialogOpened: Symbol('PageTarget.DialogOpened'), + DialogClosed: Symbol('PageTarget.DialogClosed'), +}; + +function fromProtocolColorScheme(colorScheme) { + if (colorScheme === 'light' || colorScheme === 'dark') + return colorScheme; + if (colorScheme === null || colorScheme === 'no-preference') + return undefined; + throw new Error('Unknown color scheme: ' + colorScheme); +} + +class BrowserContext { + constructor(registry, browserContextId, removeOnDetach) { + this._registry = registry; + this.browserContextId = browserContextId; + // Default context has userContextId === 0, but we pass undefined to many APIs just in case. + this.userContextId = 0; + if (browserContextId !== undefined) { + const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId); + this.userContextId = identity.userContextId; + } + this._principals = []; + // Maps origins to the permission lists. + this._permissions = new Map(); + this._registry._browserContextIdToBrowserContext.set(this.browserContextId, this); + this._registry._userContextIdToBrowserContext.set(this.userContextId, this); + this._proxy = null; + this.removeOnDetach = removeOnDetach; + this.extraHTTPHeaders = undefined; + this.httpCredentials = undefined; + this.requestInterceptionEnabled = undefined; + this.ignoreHTTPSErrors = undefined; + this.downloadOptions = undefined; + this.defaultViewportSize = undefined; + this.deviceScaleFactor = undefined; + this.defaultUserAgent = null; + this.touchOverride = false; + this.colorScheme = 'none'; + this.screencastOptions = undefined; + this.scriptsToEvaluateOnNewDocument = []; + this.bindings = []; + this.settings = {}; + this.pages = new Set(); + } + + setColorScheme(colorScheme) { + this.colorScheme = fromProtocolColorScheme(colorScheme); + for (const page of this.pages) + page.updateColorSchemeOverride(); + } + + async destroy() { + if (this.userContextId !== 0) { + ContextualIdentityService.remove(this.userContextId); + for (const page of this.pages) + page.close(); + if (this.pages.size) { + await new Promise(f => { + const listener = helper.on(this._registry, TargetRegistry.Events.TargetDestroyed, () => { + if (!this.pages.size) { + helper.removeListeners([listener]); + f(); + } + }); + }); + } + } + this._registry._browserContextIdToBrowserContext.delete(this.browserContextId); + this._registry._userContextIdToBrowserContext.delete(this.userContextId); + } + + setProxy(proxy) { + // Clear AuthCache. + Services.obs.notifyObservers(null, "net:clear-active-logins"); + this._proxy = proxy; + } + + setIgnoreHTTPSErrors(ignoreHTTPSErrors) { + if (this.ignoreHTTPSErrors === ignoreHTTPSErrors) + return; + this.ignoreHTTPSErrors = ignoreHTTPSErrors; + const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + if (ignoreHTTPSErrors) { + Preferences.set("network.stricttransportsecurity.preloadlist", false); + Preferences.set("security.cert_pinning.enforcement_level", 0); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId); + } else { + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId); + } + } + + async setDefaultUserAgent(userAgent) { + this.defaultUserAgent = userAgent; + for (const page of this.pages) + page.updateUserAgent(); + } + + setTouchOverride(touchOverride) { + this.touchOverride = touchOverride; + for (const page of this.pages) + page.updateTouchOverride(); + } + + async setDefaultViewport(viewport) { + this.defaultViewportSize = viewport ? viewport.viewportSize : undefined; + this.deviceScaleFactor = viewport ? viewport.deviceScaleFactor : undefined; + await Promise.all(Array.from(this.pages).map(page => page.updateViewportSize())); + } + + async addScriptToEvaluateOnNewDocument(script) { + this.scriptsToEvaluateOnNewDocument.push(script); + await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script))); + } + + async addBinding(name, script) { + this.bindings.push({ name, script }); + await Promise.all(Array.from(this.pages).map(page => page.addBinding(name, script))); + } + + async applySetting(name, value) { + this.settings[name] = value; + await Promise.all(Array.from(this.pages).map(page => page.applyContextSetting(name, value))); + } + + async grantPermissions(origin, permissions) { + this._permissions.set(origin, permissions); + const promises = []; + for (const page of this.pages) { + if (origin === '*' || page._url.startsWith(origin)) { + this.grantPermissionsToOrigin(page._url); + promises.push(page.ensurePermissions()); + } + } + await Promise.all(promises); + } + + resetPermissions() { + for (const principal of this._principals) { + for (const permission of ALL_PERMISSIONS) + Services.perms.removeFromPrincipal(principal, permission); + } + this._principals = []; + this._permissions.clear(); + } + + grantPermissionsToOrigin(url) { + let origin = Array.from(this._permissions.keys()).find(key => url.startsWith(key)); + if (!origin) + origin = '*'; + + const permissions = this._permissions.get(origin); + if (!permissions) + return; + + const attrs = { userContextId: this.userContextId || undefined }; + const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(url), attrs); + this._principals.push(principal); + for (const permission of ALL_PERMISSIONS) { + const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION; + Services.perms.addFromPrincipal(principal, permission, action, Ci.nsIPermissionManager.EXPIRE_NEVER, 0 /* expireTime */); + } + } + + setCookies(cookies) { + const protocolToSameSite = { + [undefined]: Ci.nsICookie.SAMESITE_NONE, + 'Lax': Ci.nsICookie.SAMESITE_LAX, + 'Strict': Ci.nsICookie.SAMESITE_STRICT, + }; + for (const cookie of cookies) { + const uri = cookie.url ? NetUtil.newURI(cookie.url) : null; + let domain = cookie.domain; + if (!domain) { + if (!uri) + throw new Error('At least one of the url and domain needs to be specified'); + domain = uri.host; + } + let path = cookie.path; + if (!path) + path = uri ? dirPath(uri.filePath) : '/'; + let secure = false; + if (cookie.secure !== undefined) + secure = cookie.secure; + else if (uri && uri.scheme === 'https') + secure = true; + Services.cookies.add( + domain, + path, + cookie.name, + cookie.value, + secure, + cookie.httpOnly || false, + cookie.expires === undefined || cookie.expires === -1 /* isSession */, + cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires, + { userContextId: this.userContextId || undefined } /* originAttributes */, + protocolToSameSite[cookie.sameSite], + Ci.nsICookie.SCHEME_UNSET + ); + } + } + + clearCookies() { + Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined })); + } + + getCookies() { + const result = []; + const sameSiteToProtocol = { + [Ci.nsICookie.SAMESITE_NONE]: 'None', + [Ci.nsICookie.SAMESITE_LAX]: 'Lax', + [Ci.nsICookie.SAMESITE_STRICT]: 'Strict', + }; + for (let cookie of Services.cookies.cookies) { + if (cookie.originAttributes.userContextId !== this.userContextId) + continue; + if (cookie.host === 'addons.mozilla.org') + continue; + result.push({ + name: cookie.name, + value: cookie.value, + domain: cookie.host, + path: cookie.path, + expires: cookie.isSession ? -1 : cookie.expiry, + size: cookie.name.length + cookie.value.length, + httpOnly: cookie.isHttpOnly, + secure: cookie.isSecure, + session: cookie.isSession, + sameSite: sameSiteToProtocol[cookie.sameSite], + }); + } + return result; + } + + async setScreencastOptions(options) { + this.screencastOptions = options; + if (!options) + return; + const promises = []; + for (const page of this.pages) + promises.push(page._startVideoRecording(options)); + await Promise.all(promises); + } +} + +class Dialog { + static createIfSupported(prompt) { + const type = prompt.args.promptType; + switch (type) { + case 'alert': + case 'alertCheck': + return new Dialog(prompt, 'alert'); + case 'prompt': + return new Dialog(prompt, 'prompt'); + case 'confirm': + case 'confirmCheck': + return new Dialog(prompt, 'confirm'); + case 'confirmEx': + return new Dialog(prompt, 'beforeunload'); + default: + return null; + }; + } + + constructor(prompt, type) { + this._id = helper.generateId(); + this._type = type; + this._prompt = prompt; + } + + id() { + return this._id; + } + + message() { + return this._prompt.ui.infoBody.textContent; + } + + type() { + return this._type; + } + + prompt() { + return this._prompt; + } + + dismiss() { + if (this._prompt.ui.button1) + this._prompt.ui.button1.click(); + else + this._prompt.ui.button0.click(); + } + + defaultValue() { + return this._prompt.ui.loginTextbox.value; + } + + accept(promptValue) { + if (typeof promptValue === 'string' && this._type === 'prompt') + this._prompt.ui.loginTextbox.value = promptValue; + this._prompt.ui.button0.click(); + } +} + + +function dirPath(path) { + return path.substring(0, path.lastIndexOf('/') + 1); +} + +async function waitForWindowReady(window) { + if (window.delayedStartupPromise) { + await window.delayedStartupPromise; + } else { + await new Promise((resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (window == aSubject) { + Services.obs.removeObserver(observer, aTopic); + resolve(); + } + }, "browser-delayed-startup-finished"); + })); + } + if (window.document.readyState !== 'complete') + await helper.awaitEvent(window, 'load'); +} + +async function setViewportSizeForBrowser(viewportSize, browser, window) { + await waitForWindowReady(window); + if (viewportSize) { + const {width, height} = viewportSize; + const rect = browser.getBoundingClientRect(); + window.resizeBy(width - rect.width, height - rect.height); + browser.style.setProperty('min-width', width + 'px'); + browser.style.setProperty('min-height', height + 'px'); + browser.style.setProperty('max-width', width + 'px'); + browser.style.setProperty('max-height', height + 'px'); + } else { + browser.style.removeProperty('min-width'); + browser.style.removeProperty('min-height'); + browser.style.removeProperty('max-width'); + browser.style.removeProperty('max-height'); + } + const rect = browser.getBoundingClientRect(); + return { width: rect.width, height: rect.height }; +} + +TargetRegistry.Events = { + TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'), + TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'), + DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'), + DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'), +}; + +var EXPORTED_SYMBOLS = ['TargetRegistry', 'PageTarget']; +this.TargetRegistry = TargetRegistry; +this.PageTarget = PageTarget; diff --git a/browser_patches/firefox-stable/juggler/components/juggler.js b/browser_patches/firefox-stable/juggler/components/juggler.js new file mode 100644 index 0000000000..7fd1ca3ad6 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/components/juggler.js @@ -0,0 +1,131 @@ +/* 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 {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); +const {ComponentUtils} = ChromeUtils.import("resource://gre/modules/ComponentUtils.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js"); +const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js"); +const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js"); +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const helper = new Helper(); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const FRAME_SCRIPT = "chrome://juggler/content/content/main.js"; + +// Command Line Handler +function CommandLineHandler() { +}; + +CommandLineHandler.prototype = { + classDescription: "Sample command-line handler", + classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'), + contractID: "@mozilla.org/remote/juggler;1", + _xpcom_categories: [{ + category: "command-line-handler", + entry: "m-juggler" + }], + + /* nsICommandLineHandler */ + handle: async function(cmdLine) { + const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false); + const jugglerPipeFlag = cmdLine.handleFlag("juggler-pipe", false); + if (!jugglerPipeFlag && (!jugglerFlag || isNaN(jugglerFlag))) + return; + const silent = cmdLine.preventDefault; + if (silent) + Services.startup.enterLastWindowClosingSurvivalArea(); + + const targetRegistry = new TargetRegistry(); + new NetworkObserver(targetRegistry); + + const loadFrameScript = () => { + Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */); + if (Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless) { + const styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Components.interfaces.nsIStyleSheetService); + const ioService = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService); + const uri = ioService.newURI('chrome://juggler/content/content/hidden-scrollbars.css', null, null); + styleSheetService.loadAndRegisterSheet(uri, styleSheetService.AGENT_SHEET); + } + }; + + // Force create hidden window here, otherwise its creation later closes the web socket! + Services.appShell.hiddenDOMWindow; + + if (jugglerFlag) { + const port = parseInt(jugglerFlag, 10); + const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm"); + const WebSocketServer = require('devtools/server/socket/websocket-server'); + this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket); + this._server.initSpecialConnection(port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4); + const token = helper.generateId(); + this._server.asyncListen({ + onSocketAccepted: async(socket, transport) => { + const input = transport.openInputStream(0, 0, 0); + const output = transport.openOutputStream(0, 0, 0); + const webSocket = await WebSocketServer.accept(transport, input, output, "/" + token); + const dispatcher = new Dispatcher(webSocket); + const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => { + if (silent) + Services.startup.exitLastWindowClosingSurvivalArea(); + }); + dispatcher.rootSession().setHandler(browserHandler); + } + }); + loadFrameScript(); + dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`); + } else if (jugglerPipeFlag) { + let browserHandler; + let pipeStopped = false; + const pipe = Cc['@mozilla.org/juggler/remotedebuggingpipe;1'].getService(Ci.nsIRemoteDebuggingPipe); + const connection = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIRemoteDebuggingPipeClient]), + receiveMessage(message) { + if (this.onmessage) + this.onmessage({ data: message }); + }, + disconnected() { + if (browserHandler) + browserHandler['Browser.close'](); + }, + send(message) { + if (pipeStopped) { + // We are missing the response to Browser.close, + // but everything works fine. Once we actually need it, + // we have to stop the pipe after the response is sent. + return; + } + pipe.sendMessage(message); + }, + }; + pipe.init(connection); + const dispatcher = new Dispatcher(connection); + browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => { + if (silent) + Services.startup.exitLastWindowClosingSurvivalArea(); + connection.onclose(); + pipe.stop(); + pipeStopped = true; + }); + dispatcher.rootSession().setHandler(browserHandler); + loadFrameScript(); + dump(`\nJuggler listening to the pipe\n`); + } + }, + + QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]), + + // CHANGEME: change the help info as appropriate, but + // follow the guidelines in nsICommandLineHandler.idl + // specifically, flag descriptions should start at + // character 24, and lines should be wrapped at + // 72 characters with embedded newlines, + // and finally, the string should end with a newline + helpInfo : " --juggler Enable Juggler automation\n" +}; + +var NSGetFactory = ComponentUtils.generateNSGetFactory([CommandLineHandler]); diff --git a/browser_patches/firefox-stable/juggler/components/juggler.manifest b/browser_patches/firefox-stable/juggler/components/juggler.manifest new file mode 100644 index 0000000000..50f8930207 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/components/juggler.manifest @@ -0,0 +1,3 @@ +component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js +contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639} +category command-line-handler m-juggler @mozilla.org/remote/juggler;1 diff --git a/browser_patches/firefox-stable/juggler/components/moz.build b/browser_patches/firefox-stable/juggler/components/moz.build new file mode 100644 index 0000000000..268fbc361d --- /dev/null +++ b/browser_patches/firefox-stable/juggler/components/moz.build @@ -0,0 +1,9 @@ +# 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/. + +EXTRA_COMPONENTS += [ + "juggler.js", + "juggler.manifest", +] + diff --git a/browser_patches/firefox-stable/juggler/content/FrameTree.js b/browser_patches/firefox-stable/juggler/content/FrameTree.js new file mode 100644 index 0000000000..e2f8828b94 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/FrameTree.js @@ -0,0 +1,574 @@ +/* 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"; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm'); +const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js'); + +const helper = new Helper(); + +class FrameTree { + constructor(rootDocShell) { + EventEmitter.decorate(this); + + this._browsingContextGroup = rootDocShell.browsingContext.group; + if (!this._browsingContextGroup.__jugglerFrameTrees) + this._browsingContextGroup.__jugglerFrameTrees = new Set(); + this._browsingContextGroup.__jugglerFrameTrees.add(this); + this._scriptsToEvaluateOnNewDocument = new Map(); + + this._webSocketEventService = Cc[ + "@mozilla.org/websocketevent/service;1" + ].getService(Ci.nsIWebSocketEventService); + + this._bindings = new Map(); + this._runtime = new Runtime(false /* isWorker */); + this._workers = new Map(); + this._docShellToFrame = new Map(); + this._frameIdToFrame = new Map(); + this._pageReady = false; + this._mainFrame = this._createFrame(rootDocShell); + const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this.QueryInterface = ChromeUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsIWebProgressListener2, + Ci.nsISupportsWeakReference, + ]); + + this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager); + this._wdmListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]), + onRegister: this._onWorkerCreated.bind(this), + onUnregister: this._onWorkerDestroyed.bind(this), + }; + this._wdm.addListener(this._wdmListener); + for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator()) + this._onWorkerCreated(workerDebugger); + + const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT | + Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION; + this._eventListeners = [ + helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'), + helper.addObserver(this._onDOMWindowCreated.bind(this), 'juggler-dom-window-reused'), + helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'), + helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'), + helper.addProgressListener(webProgress, this, flags), + ]; + } + + workers() { + return [...this._workers.values()]; + } + + runtime() { + return this._runtime; + } + + _frameForWorker(workerDebugger) { + if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED) + return null; + if (!workerDebugger.window) + return null; + const docShell = workerDebugger.window.docShell; + return this._docShellToFrame.get(docShell) || null; + } + + _onDOMWindowCreated(window) { + const frame = this._docShellToFrame.get(window.docShell) || null; + if (!frame) + return; + frame._onGlobalObjectCleared(); + this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window }); + } + + _onWorkerCreated(workerDebugger) { + // Note: we do not interoperate with firefox devtools. + if (workerDebugger.isInitialized) + return; + const frame = this._frameForWorker(workerDebugger); + if (!frame) + return; + const worker = new Worker(frame, workerDebugger); + this._workers.set(workerDebugger, worker); + this.emit(FrameTree.Events.WorkerCreated, worker); + } + + _onWorkerDestroyed(workerDebugger) { + const worker = this._workers.get(workerDebugger); + if (!worker) + return; + worker.dispose(); + this._workers.delete(workerDebugger); + this.emit(FrameTree.Events.WorkerDestroyed, worker); + } + + allFramesInBrowsingContextGroup(group) { + const frames = []; + for (const frameTree of (group.__jugglerFrameTrees || [])) + frames.push(...frameTree.frames()); + return frames; + } + + isPageReady() { + return this._pageReady; + } + + forcePageReady() { + if (this._pageReady) + return false; + this._pageReady = true; + this.emit(FrameTree.Events.PageReady); + return true; + } + + addScriptToEvaluateOnNewDocument(script) { + const scriptId = helper.generateId(); + this._scriptsToEvaluateOnNewDocument.set(scriptId, script); + return scriptId; + } + + removeScriptToEvaluateOnNewDocument(scriptId) { + this._scriptsToEvaluateOnNewDocument.delete(scriptId); + } + + addBinding(name, script) { + this._bindings.set(name, script); + for (const frame of this.frames()) + frame._addBinding(name, script); + } + + frameForDocShell(docShell) { + return this._docShellToFrame.get(docShell) || null; + } + + frame(frameId) { + return this._frameIdToFrame.get(frameId) || null; + } + + frames() { + let result = []; + collect(this._mainFrame); + return result; + + function collect(frame) { + result.push(frame); + for (const subframe of frame._children) + collect(subframe); + } + } + + mainFrame() { + return this._mainFrame; + } + + dispose() { + this._browsingContextGroup.__jugglerFrameTrees.delete(this); + this._wdm.removeListener(this._wdmListener); + this._runtime.dispose(); + helper.removeListeners(this._eventListeners); + } + + onStateChange(progress, request, flag, status) { + if (!(request instanceof Ci.nsIChannel)) + return; + const channel = request.QueryInterface(Ci.nsIChannel); + const docShell = progress.DOMWindow.docShell; + const frame = this._docShellToFrame.get(docShell); + if (!frame) { + dump(`ERROR: got a state changed event for un-tracked docshell!\n`); + return; + } + + if (!channel.isDocument) { + // Somehow, we can get worker requests here, + // while we are only interested in frame documents. + return; + } + + const isStart = flag & Ci.nsIWebProgressListener.STATE_START; + const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; + const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; + const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + + if (isStart) { + // Starting a new navigation. + frame._pendingNavigationId = channelId(channel); + frame._pendingNavigationURL = channel.URI.spec; + this.emit(FrameTree.Events.NavigationStarted, frame); + } else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) { + // Navigation is committed. + for (const subframe of frame._children) + this._detachFrame(subframe); + const navigationId = frame._pendingNavigationId; + frame._pendingNavigationId = null; + frame._pendingNavigationURL = null; + frame._lastCommittedNavigationId = navigationId; + frame._url = channel.URI.spec; + this.emit(FrameTree.Events.NavigationCommitted, frame); + if (frame === this._mainFrame) + this.forcePageReady(); + } else if (isStop && frame._pendingNavigationId && status) { + // Navigation is aborted. + const navigationId = frame._pendingNavigationId; + frame._pendingNavigationId = null; + frame._pendingNavigationURL = null; + // Always report download navigation as failure to match other browsers. + const errorText = helper.getNetworkErrorStatusText(status); + this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText); + if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED) + this.forcePageReady(); + } + + if (isStop && isDocument) + this.emit(FrameTree.Events.Load, frame); + } + + onFrameLocationChange(progress, request, location, flags) { + const docShell = progress.DOMWindow.docShell; + const frame = this._docShellToFrame.get(docShell); + const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + if (frame && sameDocumentNavigation) { + frame._url = location.spec; + this.emit(FrameTree.Events.SameDocumentNavigation, frame); + } + } + + _onDocShellCreated(docShell) { + // Bug 1142752: sometimes, the docshell appears to be immediately + // destroyed, bailout early to prevent random exceptions. + if (docShell.isBeingDestroyed()) + return; + // If this docShell doesn't belong to our frame tree - do nothing. + let root = docShell; + while (root.parent) + root = root.parent; + if (root === this._mainFrame._docShell) + this._createFrame(docShell); + } + + _createFrame(docShell) { + const parentFrame = this._docShellToFrame.get(docShell.parent) || null; + const frame = new Frame(this, this._runtime, docShell, parentFrame); + this._docShellToFrame.set(docShell, frame); + this._frameIdToFrame.set(frame.id(), frame); + this.emit(FrameTree.Events.FrameAttached, frame); + // Create execution context **after** reporting frame. + // This is our protocol contract. + if (frame.domWindow()) + frame._onGlobalObjectCleared(); + return frame; + } + + _onDocShellDestroyed(docShell) { + const frame = this._docShellToFrame.get(docShell); + if (frame) + this._detachFrame(frame); + } + + _detachFrame(frame) { + // Detach all children first + for (const subframe of frame._children) + this._detachFrame(subframe); + this._docShellToFrame.delete(frame._docShell); + this._frameIdToFrame.delete(frame.id()); + if (frame._parentFrame) + frame._parentFrame._children.delete(frame); + frame._parentFrame = null; + frame.dispose(); + this.emit(FrameTree.Events.FrameDetached, frame); + } +} + +FrameTree.Events = { + BindingCalled: 'bindingcalled', + FrameAttached: 'frameattached', + FrameDetached: 'framedetached', + GlobalObjectCreated: 'globalobjectcreated', + WorkerCreated: 'workercreated', + WorkerDestroyed: 'workerdestroyed', + WebSocketCreated: 'websocketcreated', + WebSocketOpened: 'websocketopened', + WebSocketClosed: 'websocketclosed', + WebSocketFrameReceived: 'websocketframereceived', + WebSocketFrameSent: 'websocketframesent', + NavigationStarted: 'navigationstarted', + NavigationCommitted: 'navigationcommitted', + NavigationAborted: 'navigationaborted', + SameDocumentNavigation: 'samedocumentnavigation', + PageReady: 'pageready', + Load: 'load', +}; + +class Frame { + constructor(frameTree, runtime, docShell, parentFrame) { + this._frameTree = frameTree; + this._runtime = runtime; + this._docShell = docShell; + this._children = new Set(); + this._frameId = helper.browsingContextToFrameId(this._docShell.browsingContext); + this._parentFrame = null; + this._url = ''; + if (docShell.domWindow && docShell.domWindow.location) + this._url = docShell.domWindow.location.href; + if (parentFrame) { + this._parentFrame = parentFrame; + parentFrame._children.add(this); + } + + this._lastCommittedNavigationId = null; + this._pendingNavigationId = null; + this._pendingNavigationURL = null; + + this._textInputProcessor = null; + this._executionContext = null; + + this._webSocketListenerInnerWindowId = 0; + // WebSocketListener calls frameReceived event before webSocketOpened. + // To avoid this, serialize event reporting. + this._webSocketInfos = new Map(); + + const dispatchWebSocketFrameReceived = (webSocketSerialID, frame) => this._frameTree.emit(FrameTree.Events.WebSocketFrameReceived, { + frameId: this._frameId, + wsid: webSocketSerialID + '', + opcode: frame.opCode, + data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload, + }); + this._webSocketListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIWebSocketEventListener, ]), + + webSocketCreated: (webSocketSerialID, uri, protocols) => { + this._frameTree.emit(FrameTree.Events.WebSocketCreated, { + frameId: this._frameId, + wsid: webSocketSerialID + '', + requestURL: uri, + }); + this._webSocketInfos.set(webSocketSerialID, { + opened: false, + pendingIncomingFrames: [], + }); + }, + + webSocketOpened: (webSocketSerialID, effectiveURI, protocols, extensions, httpChannelId) => { + this._frameTree.emit(FrameTree.Events.WebSocketOpened, { + frameId: this._frameId, + requestId: httpChannelId + '', + wsid: webSocketSerialID + '', + effectiveURL: effectiveURI, + }); + const info = this._webSocketInfos.get(webSocketSerialID); + info.opened = true; + for (const frame of info.pendingIncomingFrames) + dispatchWebSocketFrameReceived(webSocketSerialID, frame); + }, + + webSocketMessageAvailable: (webSocketSerialID, data, messageType) => { + // We don't use this event. + }, + + webSocketClosed: (webSocketSerialID, wasClean, code, reason) => { + this._webSocketInfos.delete(webSocketSerialID); + let error = ''; + if (!wasClean) { + const keys = Object.keys(Ci.nsIWebSocketChannel); + for (const key of keys) { + if (Ci.nsIWebSocketChannel[key] === code) + error = key; + } + } + this._frameTree.emit(FrameTree.Events.WebSocketClosed, { + frameId: this._frameId, + wsid: webSocketSerialID + '', + error, + }); + }, + + frameReceived: (webSocketSerialID, frame) => { + // Report only text and binary frames. + if (frame.opCode !== 1 && frame.opCode !== 2) + return; + const info = this._webSocketInfos.get(webSocketSerialID); + if (info.opened) + dispatchWebSocketFrameReceived(webSocketSerialID, frame); + else + info.pendingIncomingFrames.push(frame); + }, + + frameSent: (webSocketSerialID, frame) => { + // Report only text and binary frames. + if (frame.opCode !== 1 && frame.opCode !== 2) + return; + this._frameTree.emit(FrameTree.Events.WebSocketFrameSent, { + frameId: this._frameId, + wsid: webSocketSerialID + '', + opcode: frame.opCode, + data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload, + }); + }, + }; + } + + dispose() { + if (this._executionContext) + this._runtime.destroyExecutionContext(this._executionContext); + this._executionContext = null; + } + + _addBinding(name, script) { + Cu.exportFunction((...args) => { + this._frameTree.emit(FrameTree.Events.BindingCalled, { + frame: this, + name, + payload: args[0] + }); + }, this.domWindow(), { + defineAs: name, + }); + this.domWindow().eval(script); + } + + _onGlobalObjectCleared() { + const webSocketService = this._frameTree._webSocketEventService; + if (this._webSocketListenerInnerWindowId) + webSocketService.removeListener(this._webSocketListenerInnerWindowId, this._webSocketListener); + this._webSocketListenerInnerWindowId = this.domWindow().windowGlobalChild.innerWindowId; + webSocketService.addListener(this._webSocketListenerInnerWindowId, this._webSocketListener); + + if (this._executionContext) + this._runtime.destroyExecutionContext(this._executionContext); + this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), { + frameId: this._frameId, + name: '', + }); + for (const [name, script] of this._frameTree._bindings) + this._addBinding(name, script); + for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values()) { + try { + const result = this._executionContext.evaluateScript(script); + if (result && result.objectId) + this._executionContext.disposeObject(result.objectId); + } catch (e) { + dump(`ERROR: ${e.message}\n${e.stack}\n`); + } + } + } + + executionContext() { + return this._executionContext; + } + + textInputProcessor() { + if (!this._textInputProcessor) { + this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor); + this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow); + } + return this._textInputProcessor; + } + + pendingNavigationId() { + return this._pendingNavigationId; + } + + pendingNavigationURL() { + return this._pendingNavigationURL; + } + + lastCommittedNavigationId() { + return this._lastCommittedNavigationId; + } + + docShell() { + return this._docShell; + } + + domWindow() { + return this._docShell.domWindow; + } + + name() { + const frameElement = this._docShell.domWindow.frameElement; + let name = ''; + if (frameElement) + name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || ''; + return name; + } + + parentFrame() { + return this._parentFrame; + } + + id() { + return this._frameId; + } + + url() { + return this._url; + } + +} + +class Worker { + constructor(frame, workerDebugger) { + this._frame = frame; + this._workerId = helper.generateId(); + this._workerDebugger = workerDebugger; + + workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js'); + + this._channel = new SimpleChannel(`content::worker[${this._workerId}]`); + this._channel.setTransport({ + sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)), + dispose: () => {}, + }); + this._workerDebuggerListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]), + onMessage: msg => void this._channel._onMessage(JSON.parse(msg)), + onClose: () => void this._channel.dispose(), + onError: (filename, lineno, message) => { + dump(`Error in worker: ${message} @${filename}:${lineno}\n`); + }, + }; + workerDebugger.addListener(this._workerDebuggerListener); + } + + channel() { + return this._channel; + } + + frame() { + return this._frame; + } + + id() { + return this._workerId; + } + + url() { + return this._workerDebugger.url; + } + + dispose() { + this._channel.dispose(); + this._workerDebugger.removeListener(this._workerDebuggerListener); + } +} + +function channelId(channel) { + if (channel instanceof Ci.nsIIdentChannel) { + const identChannel = channel.QueryInterface(Ci.nsIIdentChannel); + return String(identChannel.channelId); + } + return helper.generateId(); +} + + +var EXPORTED_SYMBOLS = ['FrameTree']; +this.FrameTree = FrameTree; + diff --git a/browser_patches/firefox-stable/juggler/content/PageAgent.js b/browser_patches/firefox-stable/juggler/content/PageAgent.js new file mode 100644 index 0000000000..bb278e5f27 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/PageAgent.js @@ -0,0 +1,1028 @@ +/* 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"; +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm'); +const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService +); +const obs = Cc["@mozilla.org/observer-service;1"].getService( + Ci.nsIObserverService +); + +const helper = new Helper(); + +class WorkerData { + constructor(pageAgent, browserChannel, worker) { + this._workerRuntime = worker.channel().connect('runtime'); + this._browserWorker = browserChannel.connect(worker.id()); + this._worker = worker; + const emit = name => { + return (...args) => this._browserWorker.emit(name, ...args); + }; + this._eventListeners = [ + worker.channel().register('runtime', { + runtimeConsole: emit('runtimeConsole'), + runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'), + runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'), + }), + browserChannel.register(worker.id(), { + evaluate: (options) => this._workerRuntime.send('evaluate', options), + callFunction: (options) => this._workerRuntime.send('callFunction', options), + getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options), + disposeObject: (options) => this._workerRuntime.send('disposeObject', options), + }), + ]; + } + + dispose() { + this._workerRuntime.dispose(); + this._browserWorker.dispose(); + helper.removeListeners(this._eventListeners); + } +} + +class FrameData { + constructor(agent, runtime, frame) { + this._agent = agent; + this._runtime = runtime; + this._frame = frame; + this._isolatedWorlds = new Map(); + this._initialNavigationDone = false; + this.reset(); + } + + reset() { + for (const world of this._isolatedWorlds.values()) + this._runtime.destroyExecutionContext(world); + this._isolatedWorlds.clear(); + + for (const {script, worldName} of this._agent._isolatedWorlds.values()) { + const context = worldName ? this.createIsolatedWorld(worldName) : this._frame.executionContext(); + try { + let result = context.evaluateScript(script); + if (result && result.objectId) + context.disposeObject(result.objectId); + } catch (e) { + } + } + } + + createIsolatedWorld(name) { + const principal = [this._frame.domWindow()]; // extended principal + const sandbox = Cu.Sandbox(principal, { + sandboxPrototype: this._frame.domWindow(), + wantComponents: false, + wantExportHelpers: false, + wantXrays: true, + }); + const world = this._runtime.createExecutionContext(this._frame.domWindow(), sandbox, { + frameId: this._frame.id(), + name, + }); + this._isolatedWorlds.set(world.id(), world); + return world; + } + + unsafeObject(objectId) { + const contexts = [this._frame.executionContext(), ...this._isolatedWorlds.values()]; + for (const context of contexts) { + const result = context.unsafeObject(objectId); + if (result) + return result.object; + } + throw new Error('Cannot find object with id = ' + objectId); + } + + dispose() { + for (const world of this._isolatedWorlds.values()) + this._runtime.destroyExecutionContext(world); + this._isolatedWorlds.clear(); + } +} + +class PageAgent { + constructor(messageManager, browserChannel, frameTree) { + this._messageManager = messageManager; + this._browserChannel = browserChannel; + this._browserPage = browserChannel.connect('page'); + this._frameTree = frameTree; + this._runtime = frameTree.runtime(); + + this._frameData = new Map(); + this._workerData = new Map(); + this._scriptsToEvaluateOnNewDocument = new Map(); + this._isolatedWorlds = new Map(); + + const docShell = frameTree.mainFrame().docShell(); + this._docShell = docShell; + this._initialDPPX = docShell.contentViewer.overrideDPPX; + this._customScrollbars = null; + this._dragging = false; + + // Dispatch frameAttached events for all initial frames + for (const frame of this._frameTree.frames()) { + this._onFrameAttached(frame); + if (frame.url()) + this._onNavigationCommitted(frame); + if (frame.pendingNavigationId()) + this._onNavigationStarted(frame); + } + + // Report created workers. + for (const worker of this._frameTree.workers()) + this._onWorkerCreated(worker); + + // Report execution contexts. + for (const context of this._runtime.executionContexts()) + this._onExecutionContextCreated(context); + + if (this._frameTree.isPageReady()) { + this._browserPage.emit('pageReady', {}); + const mainFrame = this._frameTree.mainFrame(); + const domWindow = mainFrame.domWindow(); + const document = domWindow ? domWindow.document : null; + const readyState = document ? document.readyState : null; + // Sometimes we initialize later than the first about:blank page is opened. + // In this case, the page might've been loaded already, and we need to issue + // the `DOMContentLoaded` and `load` events. + if (mainFrame.url() === 'about:blank' && readyState === 'complete') + this._emitAllEvents(this._frameTree.mainFrame()); + } + + this._eventListeners = [ + helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'), + helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'), + helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'), + helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'), + helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)), + helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'), + helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)), + helper.on(this._frameTree, 'load', this._onLoad.bind(this)), + helper.on(this._frameTree, 'bindingcalled', this._onBindingCalled.bind(this)), + helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)), + helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)), + helper.on(this._frameTree, 'globalobjectcreated', this._onGlobalObjectCreated.bind(this)), + helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)), + helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)), + helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)), + helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)), + helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})), + helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)), + helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)), + helper.on(this._frameTree, 'websocketcreated', event => this._browserPage.emit('webSocketCreated', event)), + helper.on(this._frameTree, 'websocketopened', event => this._browserPage.emit('webSocketOpened', event)), + helper.on(this._frameTree, 'websocketframesent', event => this._browserPage.emit('webSocketFrameSent', event)), + helper.on(this._frameTree, 'websocketframereceived', event => this._browserPage.emit('webSocketFrameReceived', event)), + helper.on(this._frameTree, 'websocketclosed', event => this._browserPage.emit('webSocketClosed', event)), + helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'), + this._runtime.events.onErrorFromWorker((domWindow, message, stack) => { + const frame = this._frameTree.frameForDocShell(domWindow.docShell); + if (!frame) + return; + this._browserPage.emit('pageUncaughtError', { + frameId: frame.id(), + message, + stack, + }); + }), + this._runtime.events.onConsoleMessage(msg => this._browserPage.emit('runtimeConsole', msg)), + this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), + this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), + browserChannel.register('page', { + addBinding: ({ name, script }) => this._frameTree.addBinding(name, script), + addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this), + adoptNode: this._adoptNode.bind(this), + crash: this._crash.bind(this), + describeNode: this._describeNode.bind(this), + dispatchKeyEvent: this._dispatchKeyEvent.bind(this), + dispatchMouseEvent: this._dispatchMouseEvent.bind(this), + dispatchTouchEvent: this._dispatchTouchEvent.bind(this), + dispatchTapEvent: this._dispatchTapEvent.bind(this), + getContentQuads: this._getContentQuads.bind(this), + getFullAXTree: this._getFullAXTree.bind(this), + goBack: this._goBack.bind(this), + goForward: this._goForward.bind(this), + insertText: this._insertText.bind(this), + navigate: this._navigate.bind(this), + reload: this._reload.bind(this), + removeScriptToEvaluateOnNewDocument: this._removeScriptToEvaluateOnNewDocument.bind(this), + screenshot: this._screenshot.bind(this), + scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this), + setCacheDisabled: this._setCacheDisabled.bind(this), + setFileInputFiles: this._setFileInputFiles.bind(this), + setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this), + evaluate: this._runtime.evaluate.bind(this._runtime), + callFunction: this._runtime.callFunction.bind(this._runtime), + getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), + disposeObject: this._runtime.disposeObject.bind(this._runtime), + }), + ]; + } + + _addScriptToEvaluateOnNewDocument({script, worldName}) { + if (worldName) + return this._createIsolatedWorld({script, worldName}); + return {scriptId: this._frameTree.addScriptToEvaluateOnNewDocument(script)}; + } + + _createIsolatedWorld({script, worldName}) { + const scriptId = helper.generateId(); + this._isolatedWorlds.set(scriptId, {script, worldName}); + for (const frameData of this._frameData.values()) + frameData.createIsolatedWorld(worldName); + return {scriptId}; + } + + _removeScriptToEvaluateOnNewDocument({scriptId}) { + if (this._isolatedWorlds.has(scriptId)) + this._isolatedWorlds.delete(scriptId); + else + this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId); + } + + _setCacheDisabled({cacheDisabled}) { + const enable = Ci.nsIRequest.LOAD_NORMAL; + const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING; + + const docShell = this._frameTree.mainFrame().docShell(); + docShell.defaultLoadFlags = cacheDisabled ? disable : enable; + } + + _emitAllEvents(frame) { + this._browserPage.emit('pageEventFired', { + frameId: frame.id(), + name: 'DOMContentLoaded', + }); + this._browserPage.emit('pageEventFired', { + frameId: frame.id(), + name: 'load', + }); + } + + _onExecutionContextCreated(executionContext) { + this._browserPage.emit('runtimeExecutionContextCreated', { + executionContextId: executionContext.id(), + auxData: executionContext.auxData(), + }); + } + + _onExecutionContextDestroyed(executionContext) { + this._browserPage.emit('runtimeExecutionContextDestroyed', { + executionContextId: executionContext.id(), + }); + } + + _onWorkerCreated(worker) { + const workerData = new WorkerData(this, this._browserChannel, worker); + this._workerData.set(worker.id(), workerData); + this._browserPage.emit('pageWorkerCreated', { + workerId: worker.id(), + frameId: worker.frame().id(), + url: worker.url(), + }); + } + + _onWorkerDestroyed(worker) { + const workerData = this._workerData.get(worker.id()); + if (!workerData) + return; + this._workerData.delete(worker.id()); + workerData.dispose(); + this._browserPage.emit('pageWorkerDestroyed', { + workerId: worker.id(), + }); + } + + _onWindowOpen(subject) { + if (!(subject instanceof Ci.nsIPropertyBag2)) + return; + const props = subject.QueryInterface(Ci.nsIPropertyBag2); + const hasUrl = props.hasKey('url'); + const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell); + if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady()) + this._emitAllEvents(this._frameTree.mainFrame()); + } + + _setInterceptFileChooserDialog({enabled}) { + this._docShell.fileInputInterceptionEnabled = !!enabled; + } + + _linkClicked(sync, anchorElement) { + if (anchorElement.ownerGlobal.docShell !== this._docShell) + return; + this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' }); + } + + _onWindowOpenInNewContext(docShell) { + // TODO: unify this with _onWindowOpen if possible. + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; + this._browserPage.emit('pageWillOpenNewWindowAsynchronously'); + } + + _filePickerShown(inputElement) { + if (inputElement.ownerGlobal.docShell !== this._docShell) + return; + const frameData = this._findFrameForNode(inputElement); + this._browserPage.emit('pageFileChooserOpened', { + executionContextId: frameData._frame.executionContext().id(), + element: frameData._frame.executionContext().rawValueToRemoteObject(inputElement) + }); + } + + _findFrameForNode(node) { + return Array.from(this._frameData.values()).find(data => { + const doc = data._frame.domWindow().document; + return node === doc || node.ownerDocument === doc; + }); + } + + _onDOMContentLoaded(event) { + if (!event.target.ownerGlobal) + return; + const docShell = event.target.ownerGlobal.docShell; + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; + this._browserPage.emit('pageEventFired', { + frameId: frame.id(), + name: 'DOMContentLoaded', + }); + } + + _onError(errorEvent) { + const docShell = errorEvent.target.ownerGlobal.docShell; + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; + this._browserPage.emit('pageUncaughtError', { + frameId: frame.id(), + message: errorEvent.message, + stack: errorEvent.error && typeof errorEvent.error.stack === 'string' ? errorEvent.error.stack : '', + }); + } + + _onDocumentOpenLoad(document) { + const docShell = document.ownerGlobal.docShell; + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; + this._browserPage.emit('pageEventFired', { + frameId: frame.id(), + name: 'load' + }); + } + + _onLoad(frame) { + this._browserPage.emit('pageEventFired', { + frameId: frame.id(), + name: 'load' + }); + } + + _onNavigationStarted(frame) { + this._browserPage.emit('pageNavigationStarted', { + frameId: frame.id(), + navigationId: frame.pendingNavigationId(), + url: frame.pendingNavigationURL(), + }); + } + + _onNavigationAborted(frame, navigationId, errorText) { + this._browserPage.emit('pageNavigationAborted', { + frameId: frame.id(), + navigationId, + errorText, + }); + const frameData = this._frameData.get(frame); + if (!frameData._initialNavigationDone && frame !== this._frameTree.mainFrame()) + this._emitAllEvents(frame); + frameData._initialNavigationDone = true; + } + + _onSameDocumentNavigation(frame) { + this._browserPage.emit('pageSameDocumentNavigation', { + frameId: frame.id(), + url: frame.url(), + }); + } + + _onNavigationCommitted(frame) { + this._browserPage.emit('pageNavigationCommitted', { + frameId: frame.id(), + navigationId: frame.lastCommittedNavigationId() || undefined, + url: frame.url(), + name: frame.name(), + }); + this._frameData.get(frame)._initialNavigationDone = true; + } + + _onGlobalObjectCreated({ frame }) { + this._frameData.get(frame).reset(); + } + + _onFrameAttached(frame) { + this._browserPage.emit('pageFrameAttached', { + frameId: frame.id(), + parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined, + }); + this._frameData.set(frame, new FrameData(this, this._runtime, frame)); + } + + _onFrameDetached(frame) { + this._frameData.delete(frame); + this._browserPage.emit('pageFrameDetached', { + frameId: frame.id(), + }); + } + + _onBindingCalled({frame, name, payload}) { + this._browserPage.emit('pageBindingCalled', { + executionContextId: frame.executionContext().id(), + name, + payload + }); + } + + dispose() { + for (const workerData of this._workerData.values()) + workerData.dispose(); + this._workerData.clear(); + for (const frameData of this._frameData.values()) + frameData.dispose(); + this._frameData.clear(); + helper.removeListeners(this._eventListeners); + } + + async _navigate({frameId, url, referer}) { + try { + const uri = NetUtil.newURI(url); + } catch (e) { + throw new Error(`Invalid url: "${url}"`); + } + let referrerURI = null; + let referrerInfo = null; + if (referer) { + try { + referrerURI = NetUtil.newURI(referer); + const ReferrerInfo = Components.Constructor( + '@mozilla.org/referrer-info;1', + 'nsIReferrerInfo', + 'init' + ); + referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI); + } catch (e) { + throw new Error(`Invalid referer: "${referer}"`); + } + } + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); + docShell.loadURI(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + referrerInfo, + postData: null, + headers: null, + }); + return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; + } + + async _reload({frameId, url}) { + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation); + docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); + } + + async _goBack({frameId, url}) { + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell(); + if (!docShell.canGoBack) + return {success: false}; + docShell.goBack(); + return {success: true}; + } + + async _goForward({frameId, url}) { + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell(); + if (!docShell.canGoForward) + return {success: false}; + docShell.goForward(); + return {success: true}; + } + + async _adoptNode({frameId, objectId, executionContextId}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); + const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + const context = this._runtime.findExecutionContext(executionContextId); + const fromPrincipal = unsafeObject.nodePrincipal; + const toFrame = this._frameTree.frame(context.auxData().frameId); + const toPrincipal = toFrame.domWindow().document.nodePrincipal; + if (!toPrincipal.subsumes(fromPrincipal)) + return { remoteObject: null }; + return { remoteObject: context.rawValueToRemoteObject(unsafeObject) }; + } + + async _setFileInputFiles({objectId, frameId, files}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); + const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject) + throw new Error('Object is not input!'); + const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath))); + unsafeObject.mozSetFileArray(nsFiles); + } + + _getContentQuads({objectId, frameId}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); + const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject.getBoxQuads) + throw new Error('RemoteObject is not a node'); + const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => { + return { + p1: {x: quad.p1.x, y: quad.p1.y}, + p2: {x: quad.p2.x, y: quad.p2.y}, + p3: {x: quad.p3.x, y: quad.p3.y}, + p4: {x: quad.p4.x, y: quad.p4.y}, + }; + }); + return {quads}; + } + + _describeNode({objectId, frameId}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); + const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + const browsingContextGroup = frame.docShell().browsingContext.group; + const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup); + let contentFrame; + let ownerFrame; + for (const frame of frames) { + if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell) + contentFrame = frame; + const document = frame.domWindow().document; + if (unsafeObject === document || unsafeObject.ownerDocument === document) + ownerFrame = frame; + } + return { + contentFrameId: contentFrame ? contentFrame.id() : undefined, + ownerFrameId: ownerFrame ? ownerFrame.id() : undefined, + }; + } + + async _scrollIntoViewIfNeeded({objectId, frameId, rect}) { + const frame = this._frameTree.frame(frameId); + if (!frame) + throw new Error('Failed to find frame with id = ' + frameId); + const unsafeObject = this._frameData.get(frame).unsafeObject(objectId); + if (!unsafeObject.isConnected) + throw new Error('Node is detached from document'); + if (!rect) + rect = { x: -1, y: -1, width: -1, height: -1}; + if (unsafeObject.scrollRectIntoViewIfNeeded) + unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height); + else + throw new Error('Node does not have a layout object'); + } + + _getNodeBoundingBox(unsafeObject) { + if (!unsafeObject.getBoxQuads) + throw new Error('RemoteObject is not a node'); + const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}); + if (!quads.length) + return; + let x1 = Infinity; + let y1 = Infinity; + let x2 = -Infinity; + let y2 = -Infinity; + for (const quad of quads) { + const boundingBox = quad.getBounds(); + x1 = Math.min(boundingBox.x, x1); + y1 = Math.min(boundingBox.y, y1); + x2 = Math.max(boundingBox.x + boundingBox.width, x2); + y2 = Math.max(boundingBox.y + boundingBox.height, y2); + } + return {x: x1, y: y1, width: x2 - x1, height: y2 - y1}; + } + + async _screenshot({mimeType, clip, omitDeviceScaleFactor}) { + const content = this._messageManager.content; + if (clip) { + const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType, omitDeviceScaleFactor); + return {data}; + } + const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType, omitDeviceScaleFactor); + return {data}; + } + + async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) { + // key events don't fire if we are dragging. + if (this._dragging) { + if (type === 'keydown' && key === 'Escape') + this._cancelDragIfNeeded(); + return; + } + const frame = this._frameTree.mainFrame(); + const tip = frame.textInputProcessor(); + if (key === 'Meta' && Services.appinfo.OS !== 'Darwin') + key = 'OS'; + else if (key === 'OS' && Services.appinfo.OS === 'Darwin') + key = 'Meta'; + let keyEvent = new (frame.domWindow().KeyboardEvent)("", { + key, + code, + location, + repeat, + keyCode + }); + if (type === 'keydown') { + if (text && text !== key) { + tip.commitCompositionWith(text, keyEvent); + } else { + const flags = 0; + tip.keydown(keyEvent, flags); + } + } else if (type === 'keyup') { + if (text) + throw new Error(`keyup does not support text option`); + const flags = 0; + tip.keyup(keyEvent, flags); + } else { + throw new Error(`Unknown type ${type}`); + } + } + + async _dispatchTouchEvent({type, touchPoints, modifiers}) { + const frame = this._frameTree.mainFrame(); + const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent( + type.toLowerCase(), + touchPoints.map((point, id) => id), + touchPoints.map(point => point.x), + touchPoints.map(point => point.y), + touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX), + touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY), + touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle), + touchPoints.map(point => point.force === undefined ? 1.0 : point.force), + touchPoints.length, + modifiers); + return {defaultPrevented}; + } + + async _dispatchTapEvent({x, y, modifiers}) { + // Force a layout at the point in question, because touch events + // do not seem to trigger one like mouse events. + this._frameTree.mainFrame().domWindow().windowUtils.elementFromPoint( + x, + y, + false /* aIgnoreRootScrollFrame */, + true /* aFlushLayout */); + + const {defaultPrevented: startPrevented} = await this._dispatchTouchEvent({ + type: 'touchstart', + modifiers, + touchPoints: [{x, y}] + }); + const {defaultPrevented: endPrevented} = await this._dispatchTouchEvent({ + type: 'touchend', + modifiers, + touchPoints: [{x, y}] + }); + if (startPrevented || endPrevented) + return; + + const frame = this._frameTree.mainFrame(); + frame.domWindow().windowUtils.sendMouseEvent( + 'mousemove', + x, + y, + 0 /*button*/, + 0 /*clickCount*/, + modifiers, + false /*aIgnoreRootScrollFrame*/, + undefined /*pressure*/, + 5 /*inputSource*/, + undefined /*isDOMEventSynthesized*/, + false /*isWidgetEventSynthesized*/, + 0 /*buttons*/, + undefined /*pointerIdentifier*/, + true /*disablePointerEvent*/); + + frame.domWindow().windowUtils.sendMouseEvent( + 'mousedown', + x, + y, + 0 /*button*/, + 1 /*clickCount*/, + modifiers, + false /*aIgnoreRootScrollFrame*/, + undefined /*pressure*/, + 5 /*inputSource*/, + undefined /*isDOMEventSynthesized*/, + false /*isWidgetEventSynthesized*/, + 1 /*buttons*/, + undefined /*pointerIdentifier*/, + true /*disablePointerEvent*/); + + frame.domWindow().windowUtils.sendMouseEvent( + 'mouseup', + x, + y, + 0 /*button*/, + 1 /*clickCount*/, + modifiers, + false /*aIgnoreRootScrollFrame*/, + undefined /*pressure*/, + 5 /*inputSource*/, + undefined /*isDOMEventSynthesized*/, + false /*isWidgetEventSynthesized*/, + 0 /*buttons*/, + undefined /*pointerIdentifier*/, + true /*disablePointerEvent*/); + } + + _startDragSessionIfNeeded() { + const sess = dragService.getCurrentSession(); + if (sess) return; + dragService.startDragSessionForTests( + Ci.nsIDragService.DRAGDROP_ACTION_MOVE | + Ci.nsIDragService.DRAGDROP_ACTION_COPY | + Ci.nsIDragService.DRAGDROP_ACTION_LINK + ); + } + + _simulateDragEvent(type, x, y, modifiers) { + const window = this._frameTree.mainFrame().domWindow(); + const element = window.windowUtils.elementFromPoint(x, y, false, false); + const event = window.document.createEvent('DragEvent'); + + event.initDragEvent( + type, + true /* bubble */, + true /* cancelable */, + window, + 0 /* clickCount */, + window.mozInnerScreenX + x, + window.mozInnerScreenY + y, + x, + y, + modifiers & 2 /* ctrlkey */, + modifiers & 1 /* altKey */, + modifiers & 4 /* shiftKey */, + modifiers & 8 /* metaKey */, + 0 /* button */, // firefox always has the button as 0 on drops, regardless of which was pressed + null /* relatedTarget */, + null, + ); + if (type !== 'drop' || dragService.dragAction) + window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event); + if (type === 'drop') + this._cancelDragIfNeeded(); + } + + _cancelDragIfNeeded() { + this._dragging = false; + const sess = dragService.getCurrentSession(); + if (sess) + dragService.endDragSession(true); + } + + async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) { + this._startDragSessionIfNeeded(); + const trapDrag = subject => { + this._dragging = true; + } + + // Don't send mouse events if there is an active drag + if (!this._dragging) { + const frame = this._frameTree.mainFrame(); + + obs.addObserver(trapDrag, 'on-datatransfer-available'); + frame.domWindow().windowUtils.sendMouseEvent( + type, + x, + y, + button, + clickCount, + modifiers, + false /*aIgnoreRootScrollFrame*/, + undefined /*pressure*/, + undefined /*inputSource*/, + undefined /*isDOMEventSynthesized*/, + undefined /*isWidgetEventSynthesized*/, + buttons); + obs.removeObserver(trapDrag, 'on-datatransfer-available'); + + if (type === 'mousedown' && button === 2) { + frame.domWindow().windowUtils.sendMouseEvent( + 'contextmenu', + x, + y, + button, + clickCount, + modifiers, + false /*aIgnoreRootScrollFrame*/, + undefined /*pressure*/, + undefined /*inputSource*/, + undefined /*isDOMEventSynthesized*/, + undefined /*isWidgetEventSynthesized*/, + buttons); + } + } + + // update drag state + if (this._dragging) { + if (type === 'mousemove') + this._simulateDragEvent('dragover', x, y, modifiers); + else if (type === 'mouseup') // firefox will do drops when any mouse button is released + this._simulateDragEvent('drop', x, y, modifiers); + } else { + this._cancelDragIfNeeded(); + } + } + + async _insertText({text}) { + const frame = this._frameTree.mainFrame(); + frame.textInputProcessor().commitCompositionWith(text); + } + + async _crash() { + dump(`Crashing intentionally\n`); + // This is to intentionally crash the frame. + // We crash by using js-ctypes and dereferencing + // a bad pointer. The crash should happen immediately + // upon loading this frame script. + const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm'); + ChromeUtils.privateNoteIntentionalCrash(); + const zero = new ctypes.intptr_t(8); + const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t)); + badptr.contents; + } + + async _getFullAXTree({objectId}) { + let unsafeObject = null; + if (objectId) { + unsafeObject = this._frameData.get(this._frameTree.mainFrame()).unsafeObject(objectId); + if (!unsafeObject) + throw new Error(`No object found for id "${objectId}"`); + } + + const service = Cc["@mozilla.org/accessibilityService;1"] + .getService(Ci.nsIAccessibilityService); + const document = this._frameTree.mainFrame().domWindow().document; + const docAcc = service.getAccessibleFor(document); + + while (docAcc.document.isUpdatePendingForJugglerAccessibility) + await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x)); + + async function waitForQuiet() { + let state = {}; + docAcc.getState(state, {}); + if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0) + return; + let resolve, reject; + const promise = new Promise((x, y) => {resolve = x, reject = y}); + let eventObserver = { + observe(subject, topic) { + if (topic !== "accessible-event") { + return; + } + + // If event type does not match expected type, skip the event. + let event = subject.QueryInterface(Ci.nsIAccessibleEvent); + if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { + return; + } + + // If event's accessible does not match expected accessible, + // skip the event. + if (event.accessible !== docAcc) { + return; + } + + Services.obs.removeObserver(this, "accessible-event"); + resolve(); + }, + }; + Services.obs.addObserver(eventObserver, "accessible-event"); + return promise; + } + function buildNode(accElement) { + let a = {}, b = {}; + accElement.getState(a, b); + const tree = { + role: service.getStringRole(accElement.role), + name: accElement.name || '', + }; + if (unsafeObject && unsafeObject === accElement.DOMNode) + tree.foundObject = true; + for (const userStringProperty of [ + 'value', + 'description' + ]) { + tree[userStringProperty] = accElement[userStringProperty] || undefined; + } + + const states = {}; + for (const name of service.getStringStates(a.value, b.value)) + states[name] = true; + for (const name of ['selected', + 'focused', + 'pressed', + 'focusable', + 'haspopup', + 'required', + 'invalid', + 'modal', + 'editable', + 'busy', + 'checked', + 'multiselectable']) { + if (states[name]) + tree[name] = true; + } + + if (states['multi line']) + tree['multiline'] = true; + if (states['editable'] && states['readonly']) + tree['readonly'] = true; + if (states['checked']) + tree['checked'] = true; + if (states['mixed']) + tree['checked'] = 'mixed'; + if (states['expanded']) + tree['expanded'] = true; + else if (states['collapsed']) + tree['expanded'] = false; + if (!states['enabled']) + tree['disabled'] = true; + + const attributes = {}; + if (accElement.attributes) { + for (const { key, value } of accElement.attributes.enumerate()) { + attributes[key] = value; + } + } + for (const numericalProperty of ['level']) { + if (numericalProperty in attributes) + tree[numericalProperty] = parseFloat(attributes[numericalProperty]); + } + for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) { + if (stringProperty in attributes) + tree[stringProperty] = attributes[stringProperty]; + } + const children = []; + + for (let child = accElement.firstChild; child; child = child.nextSibling) { + children.push(buildNode(child)); + } + if (children.length) + tree.children = children; + return tree; + } + await waitForQuiet(); + return { + tree: buildNode(docAcc) + }; + } +} + +function takeScreenshot(win, left, top, width, height, mimeType, omitDeviceScaleFactor) { + const MAX_SKIA_DIMENSIONS = 32767; + + const scale = omitDeviceScaleFactor ? 1 : win.devicePixelRatio; + const canvasWidth = width * scale; + const canvasHeight = height * scale; + + if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS) + throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS); + + const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + let ctx = canvas.getContext('2d'); + ctx.scale(scale, scale); + ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET); + const dataURL = canvas.toDataURL(mimeType); + return dataURL.substring(dataURL.indexOf(',') + 1); +}; + +var EXPORTED_SYMBOLS = ['PageAgent']; +this.PageAgent = PageAgent; + diff --git a/browser_patches/firefox-stable/juggler/content/Runtime.js b/browser_patches/firefox-stable/juggler/content/Runtime.js new file mode 100644 index 0000000000..110975ebe1 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/Runtime.js @@ -0,0 +1,541 @@ +/* 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"; +// Note: this file should be loadabale with eval() into worker environment. +// Avoid Components.*, ChromeUtils and global const variables. + +if (!this.Debugger) { + // Worker has a Debugger defined already. + const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {}); + addDebuggerToGlobal(Components.utils.getGlobalForObject(this)); +} + +let lastId = 0; +function generateId() { + return 'id-' + (++lastId); +} + +const consoleLevelToProtocolType = { + 'dir': 'dir', + 'log': 'log', + 'debug': 'debug', + 'info': 'info', + 'error': 'error', + 'warn': 'warning', + 'dirxml': 'dirxml', + 'table': 'table', + 'trace': 'trace', + 'clear': 'clear', + 'group': 'startGroup', + 'groupCollapsed': 'startGroupCollapsed', + 'groupEnd': 'endGroup', + 'assert': 'assert', + 'profile': 'profile', + 'profileEnd': 'profileEnd', + 'count': 'count', + 'countReset': 'countReset', + 'time': null, + 'timeLog': 'timeLog', + 'timeEnd': 'timeEnd', + 'timeStamp': 'timeStamp', +}; + +const disallowedMessageCategories = new Set([ + 'XPConnect JavaScript', + 'component javascript', + 'chrome javascript', + 'chrome registration', + 'XBL', + 'XBL Prototype Handler', + 'XBL Content Sink', + 'xbl javascript', +]); + +class Runtime { + constructor(isWorker = false) { + this._debugger = new Debugger(); + this._pendingPromises = new Map(); + this._executionContexts = new Map(); + this._windowToExecutionContext = new Map(); + this._eventListeners = []; + if (isWorker) { + this._registerWorkerConsoleHandler(); + } else { + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + this._registerConsoleServiceListener(Services); + this._registerConsoleObserver(Services); + } + // We can't use event listener here to be compatible with Worker Global Context. + // Use plain callbacks instead. + this.events = { + onConsoleMessage: createEvent(), + onErrorFromWorker: createEvent(), + onExecutionContextCreated: createEvent(), + onExecutionContextDestroyed: createEvent(), + }; + } + + executionContexts() { + return [...this._executionContexts.values()]; + } + + async evaluate({executionContextId, expression, returnByValue}) { + const executionContext = this.findExecutionContext(executionContextId); + if (!executionContext) + throw new Error('Failed to find execution context with id = ' + executionContextId); + const exceptionDetails = {}; + let result = await executionContext.evaluateScript(expression, exceptionDetails); + if (!result) + return {exceptionDetails}; + if (returnByValue) + result = executionContext.ensureSerializedToValue(result); + return {result}; + } + + async callFunction({executionContextId, functionDeclaration, args, returnByValue}) { + const executionContext = this.findExecutionContext(executionContextId); + if (!executionContext) + throw new Error('Failed to find execution context with id = ' + executionContextId); + const exceptionDetails = {}; + let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails); + if (!result) + return {exceptionDetails}; + if (returnByValue) + result = executionContext.ensureSerializedToValue(result); + return {result}; + } + + async getObjectProperties({executionContextId, objectId}) { + const executionContext = this.findExecutionContext(executionContextId); + if (!executionContext) + throw new Error('Failed to find execution context with id = ' + executionContextId); + return {properties: executionContext.getObjectProperties(objectId)}; + } + + async disposeObject({executionContextId, objectId}) { + const executionContext = this.findExecutionContext(executionContextId); + if (!executionContext) + throw new Error('Failed to find execution context with id = ' + executionContextId); + return executionContext.disposeObject(objectId); + } + + _registerConsoleServiceListener(Services) { + const Ci = Components.interfaces; + const consoleServiceListener = { + QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]), + + observe: message => { + if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID || + !message.category || disallowedMessageCategories.has(message.category)) { + return; + } + const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID); + if (message.category === 'Web Worker' && message.logLevel === Ci.nsIConsoleMessage.error) { + emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack); + return; + } + const executionContext = this._windowToExecutionContext.get(errorWindow); + if (!executionContext) + return; + const typeNames = { + [Ci.nsIConsoleMessage.debug]: 'debug', + [Ci.nsIConsoleMessage.info]: 'info', + [Ci.nsIConsoleMessage.warn]: 'warn', + [Ci.nsIConsoleMessage.error]: 'error', + }; + emitEvent(this.events.onConsoleMessage, { + args: [{ + value: message.message, + }], + type: typeNames[message.logLevel], + executionContextId: executionContext.id(), + location: { + lineNumber: message.lineNumber, + columnNumber: message.columnNumber, + url: message.sourceName, + }, + }); + }, + }; + Services.console.registerListener(consoleServiceListener); + this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener)); + } + + _registerConsoleObserver(Services) { + const consoleObserver = ({wrappedJSObject}, topic, data) => { + const executionContext = Array.from(this._executionContexts.values()).find(context => { + const domWindow = context._domWindow; + return domWindow && domWindow.windowGlobalChild.innerWindowId === wrappedJSObject.innerID; + }); + if (!executionContext) + return; + this._onConsoleMessage(executionContext, wrappedJSObject); + }; + Services.obs.addObserver(consoleObserver, "console-api-log-event"); + this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event")); + } + + _registerWorkerConsoleHandler() { + setConsoleEventHandler(message => { + const executionContext = Array.from(this._executionContexts.values())[0]; + this._onConsoleMessage(executionContext, message); + }); + this._eventListeners.push(() => setConsoleEventHandler(null)); + } + + _onConsoleMessage(executionContext, message) { + const type = consoleLevelToProtocolType[message.level]; + if (!type) + return; + const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg)); + emitEvent(this.events.onConsoleMessage, { + args, + type, + executionContextId: executionContext.id(), + location: { + lineNumber: message.lineNumber - 1, + columnNumber: message.columnNumber - 1, + url: message.filename, + }, + }); + } + + dispose() { + for (const tearDown of this._eventListeners) + tearDown.call(null); + this._eventListeners = []; + } + + async _awaitPromise(executionContext, obj, exceptionDetails = {}) { + if (obj.promiseState === 'fulfilled') + return {success: true, obj: obj.promiseValue}; + if (obj.promiseState === 'rejected') { + const global = executionContext._global; + exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; + exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; + return {success: false, obj: null}; + } + let resolve, reject; + const promise = new Promise((a, b) => { + resolve = a; + reject = b; + }); + this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails}); + if (this._pendingPromises.size === 1) + this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this); + return await promise; + } + + _onPromiseSettled(obj) { + const pendingPromise = this._pendingPromises.get(obj.promiseID); + if (!pendingPromise) + return; + this._pendingPromises.delete(obj.promiseID); + if (!this._pendingPromises.size) + this._debugger.onPromiseSettled = undefined; + + if (obj.promiseState === 'fulfilled') { + pendingPromise.resolve({success: true, obj: obj.promiseValue}); + return; + }; + const global = pendingPromise.executionContext._global; + pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return; + pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return; + pendingPromise.resolve({success: false, obj: null}); + } + + createExecutionContext(domWindow, contextGlobal, auxData) { + // Note: domWindow is null for workers. + const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData); + this._executionContexts.set(context._id, context); + if (domWindow) + this._windowToExecutionContext.set(domWindow, context); + emitEvent(this.events.onExecutionContextCreated, context); + return context; + } + + findExecutionContext(executionContextId) { + const executionContext = this._executionContexts.get(executionContextId); + if (!executionContext) + throw new Error('Failed to find execution context with id = ' + executionContextId); + return executionContext; + } + + destroyExecutionContext(destroyedContext) { + for (const [promiseID, {reject, executionContext}] of this._pendingPromises) { + if (executionContext === destroyedContext) { + reject(new Error('Execution context was destroyed!')); + this._pendingPromises.delete(promiseID); + } + } + if (!this._pendingPromises.size) + this._debugger.onPromiseSettled = undefined; + this._debugger.removeDebuggee(destroyedContext._contextGlobal); + this._executionContexts.delete(destroyedContext._id); + if (destroyedContext._domWindow) + this._windowToExecutionContext.delete(destroyedContext._domWindow); + emitEvent(this.events.onExecutionContextDestroyed, destroyedContext); + } +} + +class ExecutionContext { + constructor(runtime, domWindow, contextGlobal, global, auxData) { + this._runtime = runtime; + this._domWindow = domWindow; + this._contextGlobal = contextGlobal; + this._global = global; + this._remoteObjects = new Map(); + this._id = generateId(); + this._auxData = auxData; + this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => { + const oldToJson = dateProto.toJSON; + dateProto.toJSON = undefined; + let hasSymbol = false; + const result = stringify(object, (key, value) => { + if (typeof value === 'symbol') + hasSymbol = true; + return value; + }); + dateProto.toJSON = oldToJson; + return hasSymbol ? undefined : result; + }).bind(null, JSON.stringify.bind(JSON), Date.prototype)`).return; + } + + id() { + return this._id; + } + + auxData() { + return this._auxData; + } + + async evaluateScript(script, exceptionDetails = {}) { + const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; + if (this._domWindow && this._domWindow.document) + this._domWindow.document.notifyUserGestureActivation(); + + let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails); + userInputHelper && userInputHelper.destruct(); + if (!success) + return null; + if (obj && obj.isPromise) { + const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); + if (!awaitResult.success) + return null; + obj = awaitResult.obj; + } + return this._createRemoteObject(obj); + } + + async evaluateFunction(functionText, args, exceptionDetails = {}) { + const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails); + if (!funEvaluation.success) + return null; + if (!funEvaluation.obj.callable) + throw new Error('functionText does not evaluate to a function!'); + args = args.map(arg => { + if (arg.objectId) { + if (!this._remoteObjects.has(arg.objectId)) + throw new Error('Cannot find object with id = ' + arg.objectId); + return this._remoteObjects.get(arg.objectId); + } + switch (arg.unserializableValue) { + case 'Infinity': return Infinity; + case '-Infinity': return -Infinity; + case '-0': return -0; + case 'NaN': return NaN; + default: return this._toDebugger(arg.value); + } + }); + const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null; + if (this._domWindow && this._domWindow.document) + this._domWindow.document.notifyUserGestureActivation(); + let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails); + userInputHelper && userInputHelper.destruct(); + if (!success) + return null; + if (obj && obj.isPromise) { + const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails); + if (!awaitResult.success) + return null; + obj = awaitResult.obj; + } + return this._createRemoteObject(obj); + } + + unsafeObject(objectId) { + if (!this._remoteObjects.has(objectId)) + return; + return { object: this._remoteObjects.get(objectId).unsafeDereference() }; + } + + rawValueToRemoteObject(rawValue) { + const debuggerObj = this._global.makeDebuggeeValue(rawValue); + return this._createRemoteObject(debuggerObj); + } + + _instanceOf(debuggerObj, rawObj, className) { + if (this._domWindow) + return rawObj instanceof this._domWindow[className]; + return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return; + } + + _createRemoteObject(debuggerObj) { + if (debuggerObj instanceof Debugger.Object) { + const objectId = generateId(); + this._remoteObjects.set(objectId, debuggerObj); + const rawObj = debuggerObj.unsafeDereference(); + const type = typeof rawObj; + let subtype = undefined; + if (debuggerObj.isProxy) + subtype = 'proxy'; + else if (Array.isArray(rawObj)) + subtype = 'array'; + else if (Object.is(rawObj, null)) + subtype = 'null'; + else if (this._instanceOf(debuggerObj, rawObj, 'Node')) + subtype = 'node'; + else if (this._instanceOf(debuggerObj, rawObj, 'RegExp')) + subtype = 'regexp'; + else if (this._instanceOf(debuggerObj, rawObj, 'Date')) + subtype = 'date'; + else if (this._instanceOf(debuggerObj, rawObj, 'Map')) + subtype = 'map'; + else if (this._instanceOf(debuggerObj, rawObj, 'Set')) + subtype = 'set'; + else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap')) + subtype = 'weakmap'; + else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet')) + subtype = 'weakset'; + else if (this._instanceOf(debuggerObj, rawObj, 'Error')) + subtype = 'error'; + else if (this._instanceOf(debuggerObj, rawObj, 'Promise')) + subtype = 'promise'; + else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) || + (this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) || + (this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) || + (this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) || + (this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) { + subtype = 'typedarray'; + } + return {objectId, type, subtype}; + } + if (typeof debuggerObj === 'symbol') { + const objectId = generateId(); + this._remoteObjects.set(objectId, debuggerObj); + return {objectId, type: 'symbol'}; + } + + let unserializableValue = undefined; + if (Object.is(debuggerObj, NaN)) + unserializableValue = 'NaN'; + else if (Object.is(debuggerObj, -0)) + unserializableValue = '-0'; + else if (Object.is(debuggerObj, Infinity)) + unserializableValue = 'Infinity'; + else if (Object.is(debuggerObj, -Infinity)) + unserializableValue = '-Infinity'; + return unserializableValue ? {unserializableValue} : {value: debuggerObj}; + } + + ensureSerializedToValue(protocolObject) { + if (!protocolObject.objectId) + return protocolObject; + const obj = this._remoteObjects.get(protocolObject.objectId); + this._remoteObjects.delete(protocolObject.objectId); + return {value: this._serialize(obj)}; + } + + _toDebugger(obj) { + if (typeof obj !== 'object') + return obj; + if (obj === null) + return obj; + const properties = {}; + for (let [key, value] of Object.entries(obj)) { + properties[key] = { + configurable: true, + writable: true, + enumerable: true, + value: this._toDebugger(value), + }; + } + const baseObject = Array.isArray(obj) ? '([])' : '({})'; + const debuggerObj = this._global.executeInGlobal(baseObject).return; + debuggerObj.defineProperties(properties); + return debuggerObj; + } + + _serialize(obj) { + const result = this._global.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject}); + if (result.throw) + throw new Error('Object is not serializable'); + return result.return === undefined ? undefined : JSON.parse(result.return); + } + + disposeObject(objectId) { + this._remoteObjects.delete(objectId); + } + + getObjectProperties(objectId) { + if (!this._remoteObjects.has(objectId)) + throw new Error('Cannot find object with id = ' + arg.objectId); + const result = []; + for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) { + for (const propertyName of obj.getOwnPropertyNames()) { + const descriptor = obj.getOwnPropertyDescriptor(propertyName); + if (!descriptor.enumerable) + continue; + result.push({ + name: propertyName, + value: this._createRemoteObject(descriptor.value), + }); + } + } + return result; + } + + _getResult(completionValue, exceptionDetails = {}) { + if (!completionValue) { + exceptionDetails.text = 'Evaluation terminated!'; + exceptionDetails.stack = ''; + return {success: false, obj: null}; + } + if (completionValue.throw) { + if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) { + exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return; + exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return; + } else { + exceptionDetails.value = this._serialize(completionValue.throw); + } + return {success: false, obj: null}; + } + return {success: true, obj: completionValue.return}; + } +} + +const listenersSymbol = Symbol('listeners'); + +function createEvent() { + const listeners = new Set(); + const subscribeFunction = listener => { + listeners.add(listener); + return () => listeners.delete(listener); + } + subscribeFunction[listenersSymbol] = listeners; + return subscribeFunction; +} + +function emitEvent(event, ...args) { + let listeners = event[listenersSymbol]; + if (!listeners || !listeners.size) + return; + listeners = new Set(listeners); + for (const listener of listeners) + listener.call(null, ...args); +} + +var EXPORTED_SYMBOLS = ['Runtime']; +this.Runtime = Runtime; diff --git a/browser_patches/firefox-stable/juggler/content/WorkerMain.js b/browser_patches/firefox-stable/juggler/content/WorkerMain.js new file mode 100644 index 0000000000..3d0c1168cb --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/WorkerMain.js @@ -0,0 +1,76 @@ +/* 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"; +loadSubScript('chrome://juggler/content/content/Runtime.js'); +loadSubScript('chrome://juggler/content/SimpleChannel.js'); + +const channel = new SimpleChannel('worker::worker'); +const eventListener = event => channel._onMessage(JSON.parse(event.data)); +this.addEventListener('message', eventListener); +channel.setTransport({ + sendMessage: msg => postMessage(JSON.stringify(msg)), + dispose: () => this.removeEventListener('message', eventListener), +}); + +const runtime = new Runtime(true /* isWorker */); + +(() => { + // Create execution context in the runtime only when the script + // source was actually evaluated in it. + const dbg = new Debugger(global); + if (dbg.findScripts({global}).length) { + runtime.createExecutionContext(null /* domWindow */, global, {}); + } else { + dbg.onNewScript = function(s) { + dbg.onNewScript = undefined; + dbg.removeAllDebuggees(); + runtime.createExecutionContext(null /* domWindow */, global, {}); + }; + } +})(); + +class RuntimeAgent { + constructor(runtime, channel) { + this._runtime = runtime; + this._browserRuntime = channel.connect('runtime'); + + for (const context of this._runtime.executionContexts()) + this._onExecutionContextCreated(context); + + this._eventListeners = [ + this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)), + this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), + this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), + channel.register('runtime', { + evaluate: this._runtime.evaluate.bind(this._runtime), + callFunction: this._runtime.callFunction.bind(this._runtime), + getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime), + disposeObject: this._runtime.disposeObject.bind(this._runtime), + }), + ]; + } + + _onExecutionContextCreated(executionContext) { + this._browserRuntime.emit('runtimeExecutionContextCreated', { + executionContextId: executionContext.id(), + auxData: executionContext.auxData(), + }); + } + + _onExecutionContextDestroyed(executionContext) { + this._browserRuntime.emit('runtimeExecutionContextDestroyed', { + executionContextId: executionContext.id(), + }); + } + + dispose() { + for (const disposer of this._eventListeners) + disposer(); + this._eventListeners = []; + } +} + +new RuntimeAgent(runtime, channel); + diff --git a/browser_patches/firefox-stable/juggler/content/hidden-scrollbars.css b/browser_patches/firefox-stable/juggler/content/hidden-scrollbars.css new file mode 100644 index 0000000000..26fc0db768 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/hidden-scrollbars.css @@ -0,0 +1,7 @@ +/* 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/. */ + +* { + scrollbar-width: none !important; +} diff --git a/browser_patches/firefox-stable/juggler/content/main.js b/browser_patches/firefox-stable/juggler/content/main.js new file mode 100644 index 0000000000..be1ca25746 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/content/main.js @@ -0,0 +1,149 @@ +/* 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 {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); + +let frameTree; +const helper = new Helper(); +const messageManager = this; + +let pageAgent; + +let failedToOverrideTimezone = false; + +const applySetting = { + geolocation: (geolocation) => { + if (geolocation) { + docShell.setGeolocationOverride({ + coords: { + latitude: geolocation.latitude, + longitude: geolocation.longitude, + accuracy: geolocation.accuracy, + altitude: NaN, + altitudeAccuracy: NaN, + heading: NaN, + speed: NaN, + }, + address: null, + timestamp: Date.now() + }); + } else { + docShell.setGeolocationOverride(null); + } + }, + + onlineOverride: (onlineOverride) => { + if (!onlineOverride) { + docShell.onlineOverride = Ci.nsIDocShell.ONLINE_OVERRIDE_NONE; + return; + } + docShell.onlineOverride = onlineOverride === 'online' ? + Ci.nsIDocShell.ONLINE_OVERRIDE_ONLINE : Ci.nsIDocShell.ONLINE_OVERRIDE_OFFLINE; + }, + + bypassCSP: (bypassCSP) => { + docShell.bypassCSPEnabled = bypassCSP; + }, + + timezoneId: (timezoneId) => { + failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId); + }, + + locale: (locale) => { + docShell.languageOverride = locale; + }, + + javaScriptDisabled: (javaScriptDisabled) => { + docShell.allowJavascript = !javaScriptDisabled; + }, + + colorScheme: (colorScheme) => { + frameTree.setColorScheme(colorScheme); + }, +}; + +const channel = SimpleChannel.createForMessageManager('content::page', messageManager); + +function initialize() { + const response = sendSyncMessage('juggler:content-ready')[0]; + // If we didn't get a response, then we don't want to do anything + // as a part of this frame script. + if (!response) + return; + const { + scriptsToEvaluateOnNewDocument = [], + bindings = [], + settings = {} + } = response || {}; + + // Enforce focused state for all top level documents. + docShell.overrideHasFocus = true; + docShell.forceActiveState = true; + frameTree = new FrameTree(docShell); + for (const [name, value] of Object.entries(settings)) { + if (value !== undefined) + applySetting[name](value); + } + for (const script of scriptsToEvaluateOnNewDocument) + frameTree.addScriptToEvaluateOnNewDocument(script); + for (const { name, script } of bindings) + frameTree.addBinding(name, script); + + pageAgent = new PageAgent(messageManager, channel, frameTree); + + channel.register('', { + addScriptToEvaluateOnNewDocument(script) { + frameTree.addScriptToEvaluateOnNewDocument(script); + }, + + addBinding({name, script}) { + frameTree.addBinding(name, script); + }, + + applyContextSetting({name, value}) { + applySetting[name](value); + }, + + ensurePermissions() { + // noop, just a rountrip. + }, + + hasFailedToOverrideTimezone() { + return failedToOverrideTimezone; + }, + + async awaitViewportDimensions({width, height, deviceSizeIsPageSize}) { + docShell.deviceSizeIsPageSize = deviceSizeIsPageSize; + const win = docShell.domWindow; + if (win.innerWidth === width && win.innerHeight === height) + return; + await new Promise(resolve => { + const listener = helper.addEventListener(win, 'resize', () => { + if (win.innerWidth === width && win.innerHeight === height) { + helper.removeListeners([listener]); + resolve(); + } + }); + }); + }, + + dispose() { + }, + }); + + const gListeners = [ + helper.addEventListener(messageManager, 'unload', msg => { + helper.removeListeners(gListeners); + pageAgent.dispose(); + frameTree.dispose(); + channel.dispose(); + }), + ]; +} + +initialize(); diff --git a/browser_patches/firefox-stable/juggler/jar.mn b/browser_patches/firefox-stable/juggler/jar.mn new file mode 100644 index 0000000000..adc90edd79 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/jar.mn @@ -0,0 +1,22 @@ +# 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/. + +juggler.jar: +% content juggler %content/ + content/Helper.js (Helper.js) + content/NetworkObserver.js (NetworkObserver.js) + content/TargetRegistry.js (TargetRegistry.js) + content/SimpleChannel.js (SimpleChannel.js) + content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js) + content/protocol/Protocol.js (protocol/Protocol.js) + content/protocol/Dispatcher.js (protocol/Dispatcher.js) + content/protocol/PageHandler.js (protocol/PageHandler.js) + content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) + content/content/main.js (content/main.js) + content/content/FrameTree.js (content/FrameTree.js) + content/content/PageAgent.js (content/PageAgent.js) + content/content/Runtime.js (content/Runtime.js) + content/content/WorkerMain.js (content/WorkerMain.js) + content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) + diff --git a/browser_patches/firefox-stable/juggler/moz.build b/browser_patches/firefox-stable/juggler/moz.build new file mode 100644 index 0000000000..905c20cc31 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/moz.build @@ -0,0 +1,10 @@ +# 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/. + +DIRS += ["components", "screencast", "pipe"] + +JAR_MANIFESTS += ["jar.mn"] +with Files("**"): + BUG_COMPONENT = ("Testing", "Juggler") + diff --git a/browser_patches/firefox-stable/juggler/pipe/components.conf b/browser_patches/firefox-stable/juggler/pipe/components.conf new file mode 100644 index 0000000000..db13a00ba7 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/pipe/components.conf @@ -0,0 +1,15 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{d69ecefe-3df7-4d11-9dc7-f604edb96da2}', + 'contract_ids': ['@mozilla.org/juggler/remotedebuggingpipe;1'], + 'type': 'nsIRemoteDebuggingPipe', + 'constructor': 'mozilla::nsRemoteDebuggingPipe::GetSingleton', + 'headers': ['/juggler/pipe/nsRemoteDebuggingPipe.h'], + }, +] diff --git a/browser_patches/firefox-stable/juggler/pipe/moz.build b/browser_patches/firefox-stable/juggler/pipe/moz.build new file mode 100644 index 0000000000..b56c697881 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/pipe/moz.build @@ -0,0 +1,24 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIRemoteDebuggingPipe.idl', +] + +XPIDL_MODULE = 'jugglerpipe' + +SOURCES += [ + 'nsRemoteDebuggingPipe.cpp', +] + +XPCOM_MANIFESTS += [ + 'components.conf', +] + +LOCAL_INCLUDES += [ +] + +FINAL_LIBRARY = 'xul' diff --git a/browser_patches/firefox-stable/juggler/pipe/nsIRemoteDebuggingPipe.idl b/browser_patches/firefox-stable/juggler/pipe/nsIRemoteDebuggingPipe.idl new file mode 100644 index 0000000000..ac91b63615 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/pipe/nsIRemoteDebuggingPipe.idl @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(7910c231-971a-4653-abdc-a8599a986c4c)] +interface nsIRemoteDebuggingPipeClient : nsISupports +{ + void receiveMessage(in AString message); + void disconnected(); +}; + +[scriptable, uuid(b7bfb66b-fd46-4aa2-b4ad-396177186d94)] +interface nsIRemoteDebuggingPipe : nsISupports +{ + void init(in nsIRemoteDebuggingPipeClient client); + void sendMessage(in AString message); + void stop(); +}; diff --git a/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.cpp b/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.cpp new file mode 100644 index 0000000000..abcb0a758f --- /dev/null +++ b/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.cpp @@ -0,0 +1,223 @@ +/* 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 "nsRemoteDebuggingPipe.h" + +#include +#if defined(_WIN32) +#include +#include +#else +#include +#include +#include +#endif + +#include "mozilla/StaticPtr.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(nsRemoteDebuggingPipe, nsIRemoteDebuggingPipe) + +namespace { + +StaticRefPtr gPipe; + +const size_t kWritePacketSize = 1 << 16; + +#if defined(_WIN32) +HANDLE readHandle; +HANDLE writeHandle; +#else +const int readFD = 3; +const int writeFD = 4; +#endif + +size_t ReadBytes(void* buffer, size_t size, bool exact_size) +{ + size_t bytesRead = 0; + while (bytesRead < size) { +#if defined(_WIN32) + DWORD sizeRead = 0; + bool hadError = !ReadFile(readHandle, static_cast(buffer) + bytesRead, + size - bytesRead, &sizeRead, nullptr); +#else + int sizeRead = read(readFD, static_cast(buffer) + bytesRead, + size - bytesRead); + if (sizeRead < 0 && errno == EINTR) + continue; + bool hadError = sizeRead <= 0; +#endif + if (hadError) { + return 0; + } + bytesRead += sizeRead; + if (!exact_size) + break; + } + return bytesRead; +} + +void WriteBytes(const char* bytes, size_t size) +{ + size_t totalWritten = 0; + while (totalWritten < size) { + size_t length = size - totalWritten; + if (length > kWritePacketSize) + length = kWritePacketSize; +#if defined(_WIN32) + DWORD bytesWritten = 0; + bool hadError = !WriteFile(writeHandle, bytes + totalWritten, static_cast(length), &bytesWritten, nullptr); +#else + int bytesWritten = write(writeFD, bytes + totalWritten, length); + if (bytesWritten < 0 && errno == EINTR) + continue; + bool hadError = bytesWritten <= 0; +#endif + if (hadError) + return; + totalWritten += bytesWritten; + } +} + +} // namespace + +// static +already_AddRefed nsRemoteDebuggingPipe::GetSingleton() { + if (!gPipe) { + gPipe = new nsRemoteDebuggingPipe(); + } + return do_AddRef(gPipe); +} + +nsRemoteDebuggingPipe::nsRemoteDebuggingPipe() = default; + +nsRemoteDebuggingPipe::~nsRemoteDebuggingPipe() = default; + +nsresult nsRemoteDebuggingPipe::Init(nsIRemoteDebuggingPipeClient* aClient) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); + if (mClient) { + return NS_ERROR_FAILURE; + } + mClient = aClient; + + MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Reader", getter_AddRefs(mReaderThread))); + MOZ_ALWAYS_SUCCEEDS(NS_NewNamedThread("Pipe Writer", getter_AddRefs(mWriterThread))); + +#if defined(_WIN32) + CHAR pipeReadStr[20]; + CHAR pipeWriteStr[20]; + GetEnvironmentVariableA("PW_PIPE_READ", pipeReadStr, 20); + GetEnvironmentVariableA("PW_PIPE_WRITE", pipeWriteStr, 20); + readHandle = reinterpret_cast(atoi(pipeReadStr)); + writeHandle = reinterpret_cast(atoi(pipeWriteStr)); +#endif + + MOZ_ALWAYS_SUCCEEDS(mReaderThread->Dispatch(NewRunnableMethod( + "nsRemoteDebuggingPipe::ReaderLoop", + this, &nsRemoteDebuggingPipe::ReaderLoop), nsIThread::DISPATCH_NORMAL)); + return NS_OK; +} + +nsresult nsRemoteDebuggingPipe::Stop() { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); + if (!mClient) { + return NS_ERROR_FAILURE; + } + m_terminated = true; + mClient = nullptr; + // Cancel pending synchronous read. +#if defined(_WIN32) + CancelIoEx(readHandle, nullptr); + CloseHandle(readHandle); + CloseHandle(writeHandle); +#else + shutdown(readFD, SHUT_RDWR); + shutdown(writeFD, SHUT_RDWR); +#endif + mReaderThread->Shutdown(); + mReaderThread = nullptr; + mWriterThread->Shutdown(); + mWriterThread = nullptr; + return NS_OK; +} + +void nsRemoteDebuggingPipe::ReaderLoop() { + const size_t bufSize = 256 * 1024; + std::vector buffer; + buffer.resize(bufSize); + std::vector line; + while (!m_terminated) { + size_t size = ReadBytes(buffer.data(), bufSize, false); + if (!size) { + nsCOMPtr runnable = NewRunnableMethod<>( + "nsRemoteDebuggingPipe::Disconnected", + this, &nsRemoteDebuggingPipe::Disconnected); + NS_DispatchToMainThread(runnable.forget()); + break; + } + size_t start = 0; + size_t end = line.size(); + line.insert(line.end(), buffer.begin(), buffer.begin() + size); + while (true) { + for (; end < line.size(); ++end) { + if (line[end] == '\0') { + break; + } + } + if (end == line.size()) { + break; + } + if (end > start) { + nsCString message; + message.Append(line.data() + start, end - start); + nsCOMPtr runnable = NewRunnableMethod( + "nsRemoteDebuggingPipe::ReceiveMessage", + this, &nsRemoteDebuggingPipe::ReceiveMessage, std::move(message)); + NS_DispatchToMainThread(runnable.forget()); + } + ++end; + start = end; + } + if (start != 0 && start < line.size()) { + memmove(line.data(), line.data() + start, line.size() - start); + } + line.resize(line.size() - start); + } +} + +void nsRemoteDebuggingPipe::ReceiveMessage(const nsCString& aMessage) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); + if (mClient) { + NS_ConvertUTF8toUTF16 utf16(aMessage); + mClient->ReceiveMessage(utf16); + } +} + +void nsRemoteDebuggingPipe::Disconnected() { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); + if (mClient) + mClient->Disconnected(); +} + +nsresult nsRemoteDebuggingPipe::SendMessage(const nsAString& aMessage) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Remote debugging pipe must be used on the Main thread."); + if (!mClient) { + return NS_ERROR_FAILURE; + } + NS_ConvertUTF16toUTF8 utf8(aMessage); + nsCOMPtr runnable = NS_NewRunnableFunction( + "nsRemoteDebuggingPipe::SendMessage", + [message = std::move(utf8)] { + const nsCString& flat = PromiseFlatCString(message); + WriteBytes(flat.Data(), flat.Length()); + WriteBytes("\0", 1); + }); + MOZ_ALWAYS_SUCCEEDS(mWriterThread->Dispatch(runnable.forget(), nsIThread::DISPATCH_NORMAL)); + return NS_OK; +} + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.h b/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.h new file mode 100644 index 0000000000..be4cb2675e --- /dev/null +++ b/browser_patches/firefox-stable/juggler/pipe/nsRemoteDebuggingPipe.h @@ -0,0 +1,34 @@ +/* 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/. */ + +#pragma once + +#include +#include "nsCOMPtr.h" +#include "nsIRemoteDebuggingPipe.h" +#include "nsThread.h" + +namespace mozilla { + +class nsRemoteDebuggingPipe final : public nsIRemoteDebuggingPipe { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREMOTEDEBUGGINGPIPE + + static already_AddRefed GetSingleton(); + nsRemoteDebuggingPipe(); + + private: + void ReaderLoop(); + void ReceiveMessage(const nsCString& aMessage); + void Disconnected(); + ~nsRemoteDebuggingPipe(); + + RefPtr mClient; + nsCOMPtr mReaderThread; + nsCOMPtr mWriterThread; + std::atomic m_terminated { false }; +}; + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/protocol/BrowserHandler.js b/browser_patches/firefox-stable/juggler/protocol/BrowserHandler.js new file mode 100644 index 0000000000..a084b3389c --- /dev/null +++ b/browser_patches/firefox-stable/juggler/protocol/BrowserHandler.js @@ -0,0 +1,289 @@ +/* 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"; + +const {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js"); +const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); + +const helper = new Helper(); + +class BrowserHandler { + constructor(session, dispatcher, targetRegistry, onclose) { + this._session = session; + this._dispatcher = dispatcher; + this._targetRegistry = targetRegistry; + this._enabled = false; + this._attachToDefaultContext = false; + this._eventListeners = []; + this._createdBrowserContextIds = new Set(); + this._attachedSessions = new Map(); + this._onclose = onclose; + } + + async ['Browser.enable']({attachToDefaultContext}) { + if (this._enabled) + return; + this._enabled = true; + this._attachToDefaultContext = attachToDefaultContext; + + this._eventListeners = [ + helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)), + helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)), + helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)), + helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)), + ]; + + const onScreencastStopped = (subject, topic, data) => { + this._session.emitEvent('Browser.screencastFinished', {screencastId: '' + data}); + }; + Services.obs.addObserver(onScreencastStopped, 'juggler-screencast-stopped'); + this._eventListeners.push(() => Services.obs.removeObserver(onScreencastStopped, 'juggler-screencast-stopped')); + + for (const target of this._targetRegistry.targets()) + this._onTargetCreated(target); + + // Wait to complete initialization of addon manager and search + // service before returning from this method. Failing to do so will result + // in a broken shutdown sequence and multiple errors in browser STDERR log. + // + // NOTE: we have to put this here as well as in the `Browser.close` handler + // since browser shutdown can be initiated when the last tab is closed, e.g. + // with persistent context. + await Promise.all([ + waitForAddonManager(), + waitForSearchService(), + ]); + } + + async ['Browser.createBrowserContext']({removeOnDetach}) { + if (!this._enabled) + throw new Error('Browser domain is not enabled'); + const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach); + this._createdBrowserContextIds.add(browserContext.browserContextId); + return {browserContextId: browserContext.browserContextId}; + } + + async ['Browser.removeBrowserContext']({browserContextId}) { + if (!this._enabled) + throw new Error('Browser domain is not enabled'); + await this._targetRegistry.browserContextForId(browserContextId).destroy(); + this._createdBrowserContextIds.delete(browserContextId); + } + + dispose() { + helper.removeListeners(this._eventListeners); + for (const [target, session] of this._attachedSessions) + this._dispatcher.destroySession(session); + this._attachedSessions.clear(); + for (const browserContextId of this._createdBrowserContextIds) { + const browserContext = this._targetRegistry.browserContextForId(browserContextId); + if (browserContext.removeOnDetach) + browserContext.destroy(); + } + this._createdBrowserContextIds.clear(); + } + + _shouldAttachToTarget(target) { + if (this._createdBrowserContextIds.has(target._browserContext.browserContextId)) + return true; + return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext(); + } + + _onTargetCreated(target) { + if (!this._shouldAttachToTarget(target)) + return; + const channel = target.channel(); + const session = this._dispatcher.createSession(); + this._attachedSessions.set(target, session); + this._session.emitEvent('Browser.attachedToTarget', { + sessionId: session.sessionId(), + targetInfo: target.info() + }); + session.setHandler(new PageHandler(target, session, channel)); + } + + _onTargetDestroyed(target) { + const session = this._attachedSessions.get(target); + if (!session) + return; + this._attachedSessions.delete(target); + this._dispatcher.destroySession(session); + this._session.emitEvent('Browser.detachedFromTarget', { + sessionId: session.sessionId(), + targetId: target.id(), + }); + } + + _onDownloadCreated(downloadInfo) { + this._session.emitEvent('Browser.downloadCreated', downloadInfo); + } + + _onDownloadFinished(downloadInfo) { + this._session.emitEvent('Browser.downloadFinished', downloadInfo); + } + + async ['Browser.newPage']({browserContextId}) { + const targetId = await this._targetRegistry.newPage({browserContextId}); + return {targetId}; + } + + async ['Browser.close']() { + let browserWindow = Services.wm.getMostRecentWindow( + "navigator:browser" + ); + if (browserWindow && browserWindow.gBrowserInit) { + await browserWindow.gBrowserInit.idleTasksFinishedPromise; + } + // Try to fully initialize browser before closing. + // See comment in `Browser.enable`. + await Promise.all([ + waitForAddonManager(), + waitForSearchService(), + ]); + this._onclose(); + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } + + async ['Browser.grantPermissions']({browserContextId, origin, permissions}) { + await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions); + } + + async ['Browser.resetPermissions']({browserContextId}) { + this._targetRegistry.browserContextForId(browserContextId).resetPermissions(); + } + + ['Browser.setExtraHTTPHeaders']({browserContextId, headers}) { + this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers; + } + + ['Browser.setHTTPCredentials']({browserContextId, credentials}) { + this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials); + } + + async ['Browser.setBrowserProxy']({type, host, port, bypass, username, password}) { + this._targetRegistry.setBrowserProxy({ type, host, port, bypass, username, password}); + } + + async ['Browser.setContextProxy']({browserContextId, type, host, port, bypass, username, password}) { + const browserContext = this._targetRegistry.browserContextForId(browserContextId); + browserContext.setProxy({ type, host, port, bypass, username, password }); + } + + ['Browser.setRequestInterception']({browserContextId, enabled}) { + this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled; + } + + ['Browser.setIgnoreHTTPSErrors']({browserContextId, ignoreHTTPSErrors}) { + this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors)); + } + + ['Browser.setDownloadOptions']({browserContextId, downloadOptions}) { + this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions); + } + + async ['Browser.setGeolocationOverride']({browserContextId, geolocation}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation)); + } + + async ['Browser.setOnlineOverride']({browserContextId, override}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override)); + } + + async ['Browser.setColorScheme']({browserContextId, colorScheme}) { + await this._targetRegistry.browserContextForId(browserContextId).setColorScheme(nullToUndefined(colorScheme)); + } + + async ['Browser.setScreencastOptions']({browserContextId, dir, width, height, scale}) { + await this._targetRegistry.browserContextForId(browserContextId).setScreencastOptions({dir, width, height, scale}); + } + + async ['Browser.setUserAgentOverride']({browserContextId, userAgent}) { + await this._targetRegistry.browserContextForId(browserContextId).setDefaultUserAgent(userAgent); + } + + async ['Browser.setBypassCSP']({browserContextId, bypassCSP}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP)); + } + + async ['Browser.setJavaScriptDisabled']({browserContextId, javaScriptDisabled}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled)); + } + + async ['Browser.setLocaleOverride']({browserContextId, locale}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale)); + } + + async ['Browser.setTimezoneOverride']({browserContextId, timezoneId}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId)); + } + + async ['Browser.setTouchOverride']({browserContextId, hasTouch}) { + await this._targetRegistry.browserContextForId(browserContextId).setTouchOverride(nullToUndefined(hasTouch)); + } + + async ['Browser.setDefaultViewport']({browserContextId, viewport}) { + await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport)); + } + + async ['Browser.addScriptToEvaluateOnNewDocument']({browserContextId, script}) { + await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script); + } + + async ['Browser.addBinding']({browserContextId, name, script}) { + await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script); + } + + ['Browser.setCookies']({browserContextId, cookies}) { + this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies); + } + + ['Browser.clearCookies']({browserContextId}) { + this._targetRegistry.browserContextForId(browserContextId).clearCookies(); + } + + ['Browser.getCookies']({browserContextId}) { + const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies(); + return {cookies}; + } + + async ['Browser.getInfo']() { + const version = AppConstants.MOZ_APP_VERSION_DISPLAY; + const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] + .getService(Components.interfaces.nsIHttpProtocolHandler) + .userAgent; + return {version: 'Firefox/' + version, userAgent}; + } +} + +async function waitForSearchService() { + const searchService = Components.classes["@mozilla.org/browser/search-service;1"].getService(Components.interfaces.nsISearchService); + await searchService.init(); +} + +async function waitForAddonManager() { + if (AddonManager.isReady) + return; + await new Promise(resolve => { + let listener = { + onStartup() { + AddonManager.removeManagerListener(listener); + resolve(); + }, + onShutdown() { }, + }; + AddonManager.addManagerListener(listener); + }); +} + +function nullToUndefined(value) { + return value === null ? undefined : value; +} + +var EXPORTED_SYMBOLS = ['BrowserHandler']; +this.BrowserHandler = BrowserHandler; diff --git a/browser_patches/firefox-stable/juggler/protocol/Dispatcher.js b/browser_patches/firefox-stable/juggler/protocol/Dispatcher.js new file mode 100644 index 0000000000..af72f307ac --- /dev/null +++ b/browser_patches/firefox-stable/juggler/protocol/Dispatcher.js @@ -0,0 +1,135 @@ +/* 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 {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); + +const helper = new Helper(); + +class Dispatcher { + /** + * @param {Connection} connection + */ + constructor(connection) { + this._connection = connection; + this._connection.onmessage = this._dispatch.bind(this); + this._connection.onclose = this._dispose.bind(this); + this._sessions = new Map(); + this._rootSession = new ProtocolSession(this, undefined); + } + + rootSession() { + return this._rootSession; + } + + createSession() { + const session = new ProtocolSession(this, helper.generateId()); + this._sessions.set(session.sessionId(), session); + return session; + } + + destroySession(session) { + this._sessions.delete(session.sessionId()); + session._dispose(); + } + + _dispose() { + this._connection.onmessage = null; + this._connection.onclose = null; + this._rootSession._dispose(); + this._rootSession = null; + this._sessions.clear(); + } + + async _dispatch(event) { + const data = JSON.parse(event.data); + const id = data.id; + const sessionId = data.sessionId; + delete data.sessionId; + try { + const session = sessionId ? this._sessions.get(sessionId) : this._rootSession; + if (!session) + throw new Error(`ERROR: cannot find session with id "${sessionId}"`); + const method = data.method; + const params = data.params || {}; + if (!id) + throw new Error(`ERROR: every message must have an 'id' parameter`); + if (!method) + throw new Error(`ERROR: every message must have a 'method' parameter`); + + const [domain, methodName] = method.split('.'); + const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null; + if (!descriptor) + throw new Error(`ERROR: method '${method}' is not supported`); + let details = {}; + if (!checkScheme(descriptor.params || {}, params, details)) + throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`); + + const result = await session.dispatch(method, params); + + details = {}; + if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details)) + throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`); + + this._connection.send(JSON.stringify({id, sessionId, result})); + } catch (e) { + this._connection.send(JSON.stringify({id, sessionId, error: { + message: e.message, + data: e.stack + }})); + } + } + + _emitEvent(sessionId, eventName, params) { + const [domain, eName] = eventName.split('.'); + const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null; + if (!scheme) + throw new Error(`ERROR: event '${eventName}' is not supported`); + const details = {}; + if (!checkScheme(scheme, params || {}, details)) + throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`); + this._connection.send(JSON.stringify({method: eventName, params, sessionId})); + } +} + +class ProtocolSession { + constructor(dispatcher, sessionId) { + this._sessionId = sessionId; + this._dispatcher = dispatcher; + this._handler = null; + } + + sessionId() { + return this._sessionId; + } + + setHandler(handler) { + this._handler = handler; + } + + _dispose() { + if (this._handler) + this._handler.dispose(); + this._handler = null; + this._dispatcher = null; + } + + emitEvent(eventName, params) { + if (!this._dispatcher) + throw new Error(`Session has been disposed.`); + this._dispatcher._emitEvent(this._sessionId, eventName, params); + } + + async dispatch(method, params) { + if (!this._handler) + throw new Error(`Session does not have a handler!`); + if (!this._handler[method]) + throw new Error(`Handler for does not implement method "${method}"`); + return await this._handler[method](params); + } +} + +this.EXPORTED_SYMBOLS = ['Dispatcher']; +this.Dispatcher = Dispatcher; + diff --git a/browser_patches/firefox-stable/juggler/protocol/PageHandler.js b/browser_patches/firefox-stable/juggler/protocol/PageHandler.js new file mode 100644 index 0000000000..b5b52d106d --- /dev/null +++ b/browser_patches/firefox-stable/juggler/protocol/PageHandler.js @@ -0,0 +1,389 @@ +/* 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"; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js'); +const {PageTarget} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js'); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; +const helper = new Helper(); + +function hashConsoleMessage(params) { + return params.location.lineNumber + ':' + params.location.columnNumber + ':' + params.location.url; +} + +class WorkerHandler { + constructor(session, contentChannel, workerId) { + this._session = session; + this._contentWorker = contentChannel.connect(workerId); + this._workerConsoleMessages = new Set(); + this._workerId = workerId; + + const emitWrappedProtocolEvent = eventName => { + return params => { + this._session.emitEvent('Page.dispatchMessageFromWorker', { + workerId, + message: JSON.stringify({method: eventName, params}), + }); + } + } + + this._eventListeners = [ + contentChannel.register(workerId, { + runtimeConsole: (params) => { + this._workerConsoleMessages.add(hashConsoleMessage(params)); + emitWrappedProtocolEvent('Runtime.console')(params); + }, + runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'), + runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'), + }), + ]; + } + + async sendMessage(message) { + const [domain, method] = message.method.split('.'); + if (domain !== 'Runtime') + throw new Error('ERROR: can only dispatch to Runtime domain inside worker'); + const result = await this._contentWorker.send(method, message.params); + this._session.emitEvent('Page.dispatchMessageFromWorker', { + workerId: this._workerId, + message: JSON.stringify({result, id: message.id}), + }); + } + + dispose() { + this._contentWorker.dispose(); + helper.removeListeners(this._eventListeners); + } +} + +class PageHandler { + constructor(target, session, contentChannel) { + this._session = session; + this._contentChannel = contentChannel; + this._contentPage = contentChannel.connect('page'); + this._workers = new Map(); + + this._pageTarget = target; + this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); + + const emitProtocolEvent = eventName => { + return (...args) => this._session.emitEvent(eventName, ...args); + } + + this._reportedFrameIds = new Set(); + this._networkEventsForUnreportedFrameIds = new Map(); + + // `Page.ready` protocol event is emitted whenever page has completed initialization, e.g. + // finished all the transient navigations to the `about:blank`. + // + // We'd like to avoid reporting meaningful events before the `Page.ready` since they are likely + // to be ignored by the protocol clients. + this._isPageReady = false; + + if (this._pageTarget.screencastInfo()) + this._onScreencastStarted(); + + this._eventListeners = [ + helper.on(this._pageTarget, PageTarget.Events.DialogOpened, this._onDialogOpened.bind(this)), + helper.on(this._pageTarget, PageTarget.Events.DialogClosed, this._onDialogClosed.bind(this)), + helper.on(this._pageTarget, PageTarget.Events.Crashed, () => { + this._session.emitEvent('Page.crashed', {}); + }), + helper.on(this._pageTarget, PageTarget.Events.ScreencastStarted, this._onScreencastStarted.bind(this)), + helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')), + helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')), + helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')), + helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')), + contentChannel.register('page', { + pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), + pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), + pageEventFired: emitProtocolEvent('Page.eventFired'), + pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), + pageFrameAttached: this._onFrameAttached.bind(this), + pageFrameDetached: emitProtocolEvent('Page.frameDetached'), + pageLinkClicked: emitProtocolEvent('Page.linkClicked'), + pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'), + pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'), + pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'), + pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'), + pageReady: this._onPageReady.bind(this), + pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'), + pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), + pageWorkerCreated: this._onWorkerCreated.bind(this), + pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), + runtimeConsole: params => { + const consoleMessageHash = hashConsoleMessage(params); + for (const worker of this._workers) { + if (worker._workerConsoleMessages.has(consoleMessageHash)) { + worker._workerConsoleMessages.delete(consoleMessageHash); + return; + } + } + emitProtocolEvent('Runtime.console')(params); + }, + runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), + runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), + + webSocketCreated: emitProtocolEvent('Page.webSocketCreated'), + webSocketOpened: emitProtocolEvent('Page.webSocketOpened'), + webSocketClosed: emitProtocolEvent('Page.webSocketClosed'), + webSocketFrameReceived: emitProtocolEvent('Page.webSocketFrameReceived'), + webSocketFrameSent: emitProtocolEvent('Page.webSocketFrameSent'), + }), + ]; + } + + async dispose() { + this._contentPage.dispose(); + helper.removeListeners(this._eventListeners); + } + + _onScreencastStarted() { + const info = this._pageTarget.screencastInfo(); + this._session.emitEvent('Page.screencastStarted', { screencastId: info.videoSessionId, file: info.file }); + } + + _onPageReady(event) { + this._isPageReady = true; + this._session.emitEvent('Page.ready'); + for (const dialog of this._pageTarget.dialogs()) + this._onDialogOpened(dialog); + } + + _onDialogOpened(dialog) { + if (!this._isPageReady) + return; + this._session.emitEvent('Page.dialogOpened', { + dialogId: dialog.id(), + type: dialog.type(), + message: dialog.message(), + defaultValue: dialog.defaultValue(), + }); + } + + _onDialogClosed(dialog) { + if (!this._isPageReady) + return; + this._session.emitEvent('Page.dialogClosed', { dialogId: dialog.id(), }); + } + + _onWorkerCreated({workerId, frameId, url}) { + const worker = new WorkerHandler(this._session, this._contentChannel, workerId); + this._workers.set(workerId, worker); + this._session.emitEvent('Page.workerCreated', {workerId, frameId, url}); + } + + _onWorkerDestroyed({workerId}) { + const worker = this._workers.get(workerId); + if (!worker) + return; + this._workers.delete(workerId); + worker.dispose(); + this._session.emitEvent('Page.workerDestroyed', {workerId}); + } + + _handleNetworkEvent(protocolEventName, eventDetails, frameId) { + if (!this._reportedFrameIds.has(frameId)) { + let events = this._networkEventsForUnreportedFrameIds.get(frameId); + if (!events) { + events = []; + this._networkEventsForUnreportedFrameIds.set(frameId, events); + } + events.push({eventName: protocolEventName, eventDetails}); + } else { + this._session.emitEvent(protocolEventName, eventDetails); + } + } + + _onFrameAttached({frameId, parentFrameId}) { + this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId}); + this._reportedFrameIds.add(frameId); + const events = this._networkEventsForUnreportedFrameIds.get(frameId) || []; + this._networkEventsForUnreportedFrameIds.delete(frameId); + for (const {eventName, eventDetails} of events) + this._session.emitEvent(eventName, eventDetails); + } + + async ['Page.close']({runBeforeUnload}) { + // Postpone target close to deliver response in session. + Services.tm.dispatchToMainThread(() => { + this._pageTarget.close(runBeforeUnload); + }); + } + + async ['Page.setViewportSize']({viewportSize}) { + await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); + } + + async ['Runtime.evaluate'](options) { + return await this._contentPage.send('evaluate', options); + } + + async ['Runtime.callFunction'](options) { + return await this._contentPage.send('callFunction', options); + } + + async ['Runtime.getObjectProperties'](options) { + return await this._contentPage.send('getObjectProperties', options); + } + + async ['Runtime.disposeObject'](options) { + return await this._contentPage.send('disposeObject', options); + } + + async ['Network.getResponseBody']({requestId}) { + return this._pageNetwork.getResponseBody(requestId); + } + + async ['Network.setExtraHTTPHeaders']({headers}) { + this._pageNetwork.setExtraHTTPHeaders(headers); + } + + async ['Network.setRequestInterception']({enabled}) { + if (enabled) + this._pageNetwork.enableRequestInterception(); + else + this._pageNetwork.disableRequestInterception(); + } + + async ['Network.resumeInterceptedRequest']({requestId, url, method, headers, postData}) { + this._pageNetwork.resumeInterceptedRequest(requestId, url, method, headers, postData); + } + + async ['Network.abortInterceptedRequest']({requestId, errorCode}) { + this._pageNetwork.abortInterceptedRequest(requestId, errorCode); + } + + async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) { + this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); + } + + async ['Accessibility.getFullAXTree'](params) { + return await this._contentPage.send('getFullAXTree', params); + } + + async ['Page.setFileInputFiles'](options) { + return await this._contentPage.send('setFileInputFiles', options); + } + + async ['Page.setEmulatedMedia']({colorScheme, type}) { + this._pageTarget.setColorScheme(colorScheme || null); + this._pageTarget.setEmulatedMedia(type); + } + + async ['Page.bringToFront'](options) { + this._pageTarget._window.focus(); + } + + async ['Page.setCacheDisabled'](options) { + return await this._contentPage.send('setCacheDisabled', options); + } + + async ['Page.addBinding'](options) { + return await this._contentPage.send('addBinding', options); + } + + async ['Page.adoptNode'](options) { + return await this._contentPage.send('adoptNode', options); + } + + async ['Page.screenshot'](options) { + return await this._contentPage.send('screenshot', options); + } + + async ['Page.getContentQuads'](options) { + return await this._contentPage.send('getContentQuads', options); + } + + async ['Page.navigate'](options) { + return await this._contentPage.send('navigate', options); + } + + async ['Page.goBack'](options) { + return await this._contentPage.send('goBack', options); + } + + async ['Page.goForward'](options) { + return await this._contentPage.send('goForward', options); + } + + async ['Page.reload'](options) { + return await this._contentPage.send('reload', options); + } + + async ['Page.describeNode'](options) { + return await this._contentPage.send('describeNode', options); + } + + async ['Page.scrollIntoViewIfNeeded'](options) { + return await this._contentPage.send('scrollIntoViewIfNeeded', options); + } + + async ['Page.addScriptToEvaluateOnNewDocument'](options) { + return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options); + } + + async ['Page.removeScriptToEvaluateOnNewDocument'](options) { + return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options); + } + + async ['Page.dispatchKeyEvent'](options) { + return await this._contentPage.send('dispatchKeyEvent', options); + } + + async ['Page.dispatchTouchEvent'](options) { + return await this._contentPage.send('dispatchTouchEvent', options); + } + + async ['Page.dispatchTapEvent'](options) { + return await this._contentPage.send('dispatchTapEvent', options); + } + + async ['Page.dispatchMouseEvent'](options) { + return await this._contentPage.send('dispatchMouseEvent', options); + } + + async ['Page.insertText'](options) { + return await this._contentPage.send('insertText', options); + } + + async ['Page.crash'](options) { + return await this._contentPage.send('crash', options); + } + + async ['Page.handleDialog']({dialogId, accept, promptText}) { + const dialog = this._pageTarget.dialog(dialogId); + if (!dialog) + throw new Error('Failed to find dialog with id = ' + dialogId); + if (accept) + dialog.accept(promptText); + else + dialog.dismiss(); + } + + async ['Page.setInterceptFileChooserDialog'](options) { + return await this._contentPage.send('setInterceptFileChooserDialog', options); + } + + async ['Page.sendMessageToWorker']({workerId, message}) { + const worker = this._workers.get(workerId); + if (!worker) + throw new Error('ERROR: cannot find worker with id ' + workerId); + return await worker.sendMessage(JSON.parse(message)); + } + + async ['Page.stopVideoRecording']() { + await this._pageTarget.stopVideoRecording(); + } +} + +var EXPORTED_SYMBOLS = ['PageHandler']; +this.PageHandler = PageHandler; diff --git a/browser_patches/firefox-stable/juggler/protocol/PrimitiveTypes.js b/browser_patches/firefox-stable/juggler/protocol/PrimitiveTypes.js new file mode 100644 index 0000000000..5799038f19 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/protocol/PrimitiveTypes.js @@ -0,0 +1,147 @@ +/* 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 t = {}; + +t.String = function(x, details = {}, path = ['']) { + if (typeof x === 'string' || typeof x === 'String') + return true; + details.error = `Expected "${path.join('.')}" to be |string|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; + return false; +} + +t.Number = function(x, details = {}, path = ['']) { + if (typeof x === 'number') + return true; + details.error = `Expected "${path.join('.')}" to be |number|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; + return false; +} + +t.Boolean = function(x, details = {}, path = ['']) { + if (typeof x === 'boolean') + return true; + details.error = `Expected "${path.join('.')}" to be |boolean|; found |${typeof x}| \`${JSON.stringify(x)}\` instead.`; + return false; +} + +t.Null = function(x, details = {}, path = ['']) { + if (Object.is(x, null)) + return true; + details.error = `Expected "${path.join('.')}" to be \`null\`; found \`${JSON.stringify(x)}\` instead.`; + return false; +} + +t.Undefined = function(x, details = {}, path = ['']) { + if (Object.is(x, undefined)) + return true; + details.error = `Expected "${path.join('.')}" to be \`undefined\`; found \`${JSON.stringify(x)}\` instead.`; + return false; +} + +t.Any = x => true, + +t.Enum = function(values) { + return function(x, details = {}, path = ['']) { + if (values.indexOf(x) !== -1) + return true; + details.error = `Expected "${path.join('.')}" to be one of [${values.join(', ')}]; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; + return false; + } +} + +t.Nullable = function(scheme) { + return function(x, details = {}, path = ['']) { + if (Object.is(x, null)) + return true; + return checkScheme(scheme, x, details, path); + } +} + +t.Optional = function(scheme) { + return function(x, details = {}, path = ['']) { + if (Object.is(x, undefined)) + return true; + return checkScheme(scheme, x, details, path); + } +} + +t.Array = function(scheme) { + return function(x, details = {}, path = ['']) { + if (!Array.isArray(x)) { + details.error = `Expected "${path.join('.')}" to be an array; found \`${JSON.stringify(x)}\` (${typeof x}) instead.`; + return false; + } + const lastPathElement = path[path.length - 1]; + for (let i = 0; i < x.length; ++i) { + path[path.length - 1] = lastPathElement + `[${i}]`; + if (!checkScheme(scheme, x[i], details, path)) + return false; + } + path[path.length - 1] = lastPathElement; + return true; + } +} + +t.Recursive = function(types, schemeName) { + return function(x, details = {}, path = ['']) { + const scheme = types[schemeName]; + return checkScheme(scheme, x, details, path); + } +} + +function beauty(path, obj) { + if (path.length === 1) + return `object ${JSON.stringify(obj, null, 2)}`; + return `property "${path.join('.')}" - ${JSON.stringify(obj, null, 2)}`; +} + +function checkScheme(scheme, x, details = {}, path = ['']) { + if (!scheme) + throw new Error(`ILLDEFINED SCHEME: ${path.join('.')}`); + if (typeof scheme === 'object') { + if (!x) { + details.error = `Object "${path.join('.')}" is undefined, but has some scheme`; + return false; + } + for (const [propertyName, aScheme] of Object.entries(scheme)) { + path.push(propertyName); + const result = checkScheme(aScheme, x[propertyName], details, path); + path.pop(); + if (!result) + return false; + } + for (const propertyName of Object.keys(x)) { + if (!scheme[propertyName]) { + path.push(propertyName); + details.error = `Found ${beauty(path, x[propertyName])} which is not described in this scheme`; + return false; + } + } + return true; + } + return scheme(x, details, path); +} + +/* + +function test(scheme, obj) { + const details = {}; + if (!checkScheme(scheme, obj, details)) { + dump(`FAILED: ${JSON.stringify(obj)} + details.error: ${details.error} + `); + } else { + dump(`SUCCESS: ${JSON.stringify(obj)} +`); + } +} + +test(t.Array(t.String), ['a', 'b', 2, 'c']); +test(t.Either(t.String, t.Number), {}); + +*/ + +this.t = t; +this.checkScheme = checkScheme; +this.EXPORTED_SYMBOLS = ['t', 'checkScheme']; diff --git a/browser_patches/firefox-stable/juggler/protocol/Protocol.js b/browser_patches/firefox-stable/juggler/protocol/Protocol.js new file mode 100644 index 0000000000..c9693b162b --- /dev/null +++ b/browser_patches/firefox-stable/juggler/protocol/Protocol.js @@ -0,0 +1,933 @@ +/* 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 {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js'); + +// Protocol-specific types. +const browserTypes = {}; + +browserTypes.TargetInfo = { + type: t.Enum(['page']), + targetId: t.String, + browserContextId: t.Optional(t.String), + // PageId of parent tab, if any. + openerId: t.Optional(t.String), +}; + +browserTypes.CookieOptions = { + name: t.String, + value: t.String, + url: t.Optional(t.String), + domain: t.Optional(t.String), + path: t.Optional(t.String), + secure: t.Optional(t.Boolean), + httpOnly: t.Optional(t.Boolean), + sameSite: t.Optional(t.Enum(['Strict', 'Lax', 'None'])), + expires: t.Optional(t.Number), +}; + +browserTypes.Cookie = { + name: t.String, + domain: t.String, + path: t.String, + value: t.String, + expires: t.Number, + size: t.Number, + httpOnly: t.Boolean, + secure: t.Boolean, + session: t.Boolean, + sameSite: t.Enum(['Strict', 'Lax', 'None']), +}; + +browserTypes.Geolocation = { + latitude: t.Number, + longitude: t.Number, + accuracy: t.Optional(t.Number), +}; + +browserTypes.DownloadOptions = { + behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])), + downloadsDir: t.Optional(t.String), +}; + +const pageTypes = {}; +pageTypes.DOMPoint = { + x: t.Number, + y: t.Number, +}; + +pageTypes.Rect = { + x: t.Number, + y: t.Number, + width: t.Number, + height: t.Number, +}; + +pageTypes.Size = { + width: t.Number, + height: t.Number, +}; + +pageTypes.Viewport = { + viewportSize: pageTypes.Size, + deviceScaleFactor: t.Optional(t.Number), +}; + +pageTypes.DOMQuad = { + p1: pageTypes.DOMPoint, + p2: pageTypes.DOMPoint, + p3: pageTypes.DOMPoint, + p4: pageTypes.DOMPoint, +}; + +pageTypes.TouchPoint = { + x: t.Number, + y: t.Number, + radiusX: t.Optional(t.Number), + radiusY: t.Optional(t.Number), + rotationAngle: t.Optional(t.Number), + force: t.Optional(t.Number), +}; + +pageTypes.Clip = { + x: t.Number, + y: t.Number, + width: t.Number, + height: t.Number, +}; + + +const runtimeTypes = {}; +runtimeTypes.RemoteObject = { + type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])), + subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])), + objectId: t.Optional(t.String), + unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), + value: t.Any +}; + +runtimeTypes.ObjectProperty = { + name: t.String, + value: runtimeTypes.RemoteObject, +}; + +runtimeTypes.ScriptLocation = { + columnNumber: t.Number, + lineNumber: t.Number, + url: t.String, +}; + +runtimeTypes.ExceptionDetails = { + text: t.Optional(t.String), + stack: t.Optional(t.String), + value: t.Optional(t.Any), +}; + +runtimeTypes.CallFunctionArgument = { + objectId: t.Optional(t.String), + unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])), + value: t.Any, +}; + +const axTypes = {}; +axTypes.AXTree = { + role: t.String, + name: t.String, + children: t.Optional(t.Array(t.Recursive(axTypes, 'AXTree'))), + + selected: t.Optional(t.Boolean), + focused: t.Optional(t.Boolean), + pressed: t.Optional(t.Boolean), + focusable: t.Optional(t.Boolean), + haspopup: t.Optional(t.Boolean), + required: t.Optional(t.Boolean), + invalid: t.Optional(t.Boolean), + modal: t.Optional(t.Boolean), + editable: t.Optional(t.Boolean), + busy: t.Optional(t.Boolean), + multiline: t.Optional(t.Boolean), + readonly: t.Optional(t.Boolean), + checked: t.Optional(t.Enum(['mixed', true])), + expanded: t.Optional(t.Boolean), + disabled: t.Optional(t.Boolean), + multiselectable: t.Optional(t.Boolean), + + value: t.Optional(t.String), + description: t.Optional(t.String), + + roledescription: t.Optional(t.String), + valuetext: t.Optional(t.String), + orientation: t.Optional(t.String), + autocomplete: t.Optional(t.String), + keyshortcuts: t.Optional(t.String), + + level: t.Optional(t.Number), + + tag: t.Optional(t.String), + + foundObject: t.Optional(t.Boolean), +} + +const networkTypes = {}; + +networkTypes.HTTPHeader = { + name: t.String, + value: t.String, +}; + +networkTypes.HTTPCredentials = { + username: t.String, + password: t.String, +}; + +networkTypes.SecurityDetails = { + protocol: t.String, + subjectName: t.String, + issuer: t.String, + validFrom: t.Number, + validTo: t.Number, +}; + +networkTypes.ResourceTiming = { + startTime: t.Number, + domainLookupStart: t.Number, + domainLookupEnd: t.Number, + connectStart: t.Number, + secureConnectionStart: t.Number, + connectEnd: t.Number, + requestStart: t.Number, + responseStart: t.Number, +}; + +const Browser = { + targets: ['browser'], + + types: browserTypes, + + events: { + 'attachedToTarget': { + sessionId: t.String, + targetInfo: browserTypes.TargetInfo, + }, + 'detachedFromTarget': { + sessionId: t.String, + targetId: t.String, + }, + 'downloadCreated': { + uuid: t.String, + browserContextId: t.Optional(t.String), + pageTargetId: t.String, + url: t.String, + suggestedFileName: t.String, + }, + 'downloadFinished': { + uuid: t.String, + canceled: t.Optional(t.Boolean), + error: t.Optional(t.String), + }, + 'screencastFinished': { + screencastId: t.String, + }, + }, + + methods: { + 'enable': { + params: { + attachToDefaultContext: t.Boolean, + }, + }, + 'createBrowserContext': { + params: { + removeOnDetach: t.Optional(t.Boolean), + }, + returns: { + browserContextId: t.String, + }, + }, + 'removeBrowserContext': { + params: { + browserContextId: t.String, + }, + }, + 'newPage': { + params: { + browserContextId: t.Optional(t.String), + }, + returns: { + targetId: t.String, + } + }, + 'close': {}, + 'getInfo': { + returns: { + userAgent: t.String, + version: t.String, + }, + }, + 'setExtraHTTPHeaders': { + params: { + browserContextId: t.Optional(t.String), + headers: t.Array(networkTypes.HTTPHeader), + }, + }, + 'setBrowserProxy': { + params: { + type: t.Enum(['http', 'https', 'socks', 'socks4']), + bypass: t.Array(t.String), + host: t.String, + port: t.Number, + username: t.Optional(t.String), + password: t.Optional(t.String), + }, + }, + 'setContextProxy': { + params: { + browserContextId: t.Optional(t.String), + type: t.Enum(['http', 'https', 'socks', 'socks4']), + bypass: t.Array(t.String), + host: t.String, + port: t.Number, + username: t.Optional(t.String), + password: t.Optional(t.String), + }, + }, + 'setHTTPCredentials': { + params: { + browserContextId: t.Optional(t.String), + credentials: t.Nullable(networkTypes.HTTPCredentials), + }, + }, + 'setRequestInterception': { + params: { + browserContextId: t.Optional(t.String), + enabled: t.Boolean, + }, + }, + 'setGeolocationOverride': { + params: { + browserContextId: t.Optional(t.String), + geolocation: t.Nullable(browserTypes.Geolocation), + } + }, + 'setUserAgentOverride': { + params: { + browserContextId: t.Optional(t.String), + userAgent: t.Nullable(t.String), + } + }, + 'setBypassCSP': { + params: { + browserContextId: t.Optional(t.String), + bypassCSP: t.Nullable(t.Boolean), + } + }, + 'setIgnoreHTTPSErrors': { + params: { + browserContextId: t.Optional(t.String), + ignoreHTTPSErrors: t.Nullable(t.Boolean), + } + }, + 'setJavaScriptDisabled': { + params: { + browserContextId: t.Optional(t.String), + javaScriptDisabled: t.Nullable(t.Boolean), + } + }, + 'setLocaleOverride': { + params: { + browserContextId: t.Optional(t.String), + locale: t.Nullable(t.String), + } + }, + 'setTimezoneOverride': { + params: { + browserContextId: t.Optional(t.String), + timezoneId: t.Nullable(t.String), + } + }, + 'setDownloadOptions': { + params: { + browserContextId: t.Optional(t.String), + downloadOptions: t.Nullable(browserTypes.DownloadOptions), + } + }, + 'setTouchOverride': { + params: { + browserContextId: t.Optional(t.String), + hasTouch: t.Nullable(t.Boolean), + } + }, + 'setDefaultViewport': { + params: { + browserContextId: t.Optional(t.String), + viewport: t.Nullable(pageTypes.Viewport), + } + }, + 'addScriptToEvaluateOnNewDocument': { + params: { + browserContextId: t.Optional(t.String), + script: t.String, + } + }, + 'addBinding': { + params: { + browserContextId: t.Optional(t.String), + name: t.String, + script: t.String, + }, + }, + 'grantPermissions': { + params: { + origin: t.String, + browserContextId: t.Optional(t.String), + permissions: t.Array(t.String), + }, + }, + 'resetPermissions': { + params: { + browserContextId: t.Optional(t.String), + } + }, + 'setCookies': { + params: { + browserContextId: t.Optional(t.String), + cookies: t.Array(browserTypes.CookieOptions), + } + }, + 'clearCookies': { + params: { + browserContextId: t.Optional(t.String), + } + }, + 'getCookies': { + params: { + browserContextId: t.Optional(t.String) + }, + returns: { + cookies: t.Array(browserTypes.Cookie), + }, + }, + 'setOnlineOverride': { + params: { + browserContextId: t.Optional(t.String), + override: t.Nullable(t.Enum(['online', 'offline'])), + } + }, + 'setColorScheme': { + params: { + browserContextId: t.Optional(t.String), + colorScheme: t.Nullable(t.Enum(['dark', 'light', 'no-preference'])), + }, + }, + 'setScreencastOptions': { + params: { + browserContextId: t.Optional(t.String), + dir: t.String, + width: t.Number, + height: t.Number, + scale: t.Optional(t.Number), + }, + }, + }, +}; + +const Network = { + targets: ['page'], + types: networkTypes, + events: { + 'requestWillBeSent': { + // frameId may be absent for redirected requests. + frameId: t.Optional(t.String), + requestId: t.String, + // RequestID of redirected request. + redirectedFrom: t.Optional(t.String), + postData: t.Optional(t.String), + headers: t.Array(networkTypes.HTTPHeader), + isIntercepted: t.Boolean, + url: t.String, + method: t.String, + navigationId: t.Optional(t.String), + cause: t.String, + internalCause: t.String, + }, + 'responseReceived': { + securityDetails: t.Nullable(networkTypes.SecurityDetails), + requestId: t.String, + fromCache: t.Boolean, + remoteIPAddress: t.Optional(t.String), + remotePort: t.Optional(t.Number), + status: t.Number, + statusText: t.String, + headers: t.Array(networkTypes.HTTPHeader), + timing: networkTypes.ResourceTiming, + }, + 'requestFinished': { + requestId: t.String, + responseEndTime: t.Number, + }, + 'requestFailed': { + requestId: t.String, + errorCode: t.String, + }, + }, + methods: { + 'setRequestInterception': { + params: { + enabled: t.Boolean, + }, + }, + 'setExtraHTTPHeaders': { + params: { + headers: t.Array(networkTypes.HTTPHeader), + }, + }, + 'abortInterceptedRequest': { + params: { + requestId: t.String, + errorCode: t.String, + }, + }, + 'resumeInterceptedRequest': { + params: { + requestId: t.String, + url: t.Optional(t.String), + method: t.Optional(t.String), + headers: t.Optional(t.Array(networkTypes.HTTPHeader)), + postData: t.Optional(t.String), + }, + }, + 'fulfillInterceptedRequest': { + params: { + requestId: t.String, + status: t.Number, + statusText: t.String, + headers: t.Array(networkTypes.HTTPHeader), + base64body: t.Optional(t.String), // base64-encoded + }, + }, + 'getResponseBody': { + params: { + requestId: t.String, + }, + returns: { + base64body: t.String, + evicted: t.Optional(t.Boolean), + }, + }, + }, +}; + +const Runtime = { + targets: ['page'], + types: runtimeTypes, + events: { + 'executionContextCreated': { + executionContextId: t.String, + auxData: t.Any, + }, + 'executionContextDestroyed': { + executionContextId: t.String, + }, + 'console': { + executionContextId: t.String, + args: t.Array(runtimeTypes.RemoteObject), + type: t.String, + location: runtimeTypes.ScriptLocation, + }, + }, + methods: { + 'evaluate': { + params: { + // Pass frameId here. + executionContextId: t.String, + expression: t.String, + returnByValue: t.Optional(t.Boolean), + }, + + returns: { + result: t.Optional(runtimeTypes.RemoteObject), + exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails), + } + }, + 'callFunction': { + params: { + // Pass frameId here. + executionContextId: t.String, + functionDeclaration: t.String, + returnByValue: t.Optional(t.Boolean), + args: t.Array(runtimeTypes.CallFunctionArgument), + }, + + returns: { + result: t.Optional(runtimeTypes.RemoteObject), + exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails), + } + }, + 'disposeObject': { + params: { + executionContextId: t.String, + objectId: t.String, + }, + }, + + 'getObjectProperties': { + params: { + executionContextId: t.String, + objectId: t.String, + }, + + returns: { + properties: t.Array(runtimeTypes.ObjectProperty), + } + }, + }, +}; + +const Page = { + targets: ['page'], + + types: pageTypes, + events: { + 'ready': { + }, + 'crashed': { + }, + 'eventFired': { + frameId: t.String, + name: t.Enum(['load', 'DOMContentLoaded']), + }, + 'uncaughtError': { + frameId: t.String, + message: t.String, + stack: t.String, + }, + 'frameAttached': { + frameId: t.String, + parentFrameId: t.Optional(t.String), + }, + 'frameDetached': { + frameId: t.String, + }, + 'navigationStarted': { + frameId: t.String, + navigationId: t.String, + url: t.String, + }, + 'navigationCommitted': { + frameId: t.String, + // |navigationId| can only be null in response to enable. + navigationId: t.Optional(t.String), + url: t.String, + // frame.id or frame.name + name: t.String, + }, + 'navigationAborted': { + frameId: t.String, + navigationId: t.String, + errorText: t.String, + }, + 'sameDocumentNavigation': { + frameId: t.String, + url: t.String, + }, + 'dialogOpened': { + dialogId: t.String, + type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']), + message: t.String, + defaultValue: t.Optional(t.String), + }, + 'dialogClosed': { + dialogId: t.String, + }, + 'bindingCalled': { + executionContextId: t.String, + name: t.String, + payload: t.Any, + }, + 'linkClicked': { + phase: t.Enum(['before', 'after']), + }, + 'willOpenNewWindowAsynchronously': {}, + 'fileChooserOpened': { + executionContextId: t.String, + element: runtimeTypes.RemoteObject + }, + 'workerCreated': { + workerId: t.String, + frameId: t.String, + url: t.String, + }, + 'workerDestroyed': { + workerId: t.String, + }, + 'dispatchMessageFromWorker': { + workerId: t.String, + message: t.String, + }, + 'screencastStarted': { + screencastId: t.String, + file: t.String, + }, + 'webSocketCreated': { + frameId: t.String, + wsid: t.String, + requestURL: t.String, + }, + 'webSocketOpened': { + frameId: t.String, + requestId: t.String, + wsid: t.String, + effectiveURL: t.String, + }, + 'webSocketClosed': { + frameId: t.String, + wsid: t.String, + error: t.String, + }, + 'webSocketFrameSent': { + frameId: t.String, + wsid: t.String, + opcode: t.Number, + data: t.String, + }, + 'webSocketFrameReceived': { + frameId: t.String, + wsid: t.String, + opcode: t.Number, + data: t.String, + }, + }, + + methods: { + 'close': { + params: { + runBeforeUnload: t.Optional(t.Boolean), + }, + }, + 'setFileInputFiles': { + params: { + frameId: t.String, + objectId: t.String, + files: t.Array(t.String), + }, + }, + 'addBinding': { + params: { + name: t.String, + script: t.String, + }, + }, + 'setViewportSize': { + params: { + viewportSize: t.Nullable(pageTypes.Size), + }, + }, + 'bringToFront': { + params: { + }, + }, + 'setEmulatedMedia': { + params: { + type: t.Optional(t.Enum(['screen', 'print', ''])), + colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])), + }, + }, + 'setCacheDisabled': { + params: { + cacheDisabled: t.Boolean, + }, + }, + 'describeNode': { + params: { + frameId: t.String, + objectId: t.String, + }, + returns: { + contentFrameId: t.Optional(t.String), + ownerFrameId: t.Optional(t.String), + }, + }, + 'scrollIntoViewIfNeeded': { + params: { + frameId: t.String, + objectId: t.String, + rect: t.Optional(pageTypes.Rect), + }, + }, + 'addScriptToEvaluateOnNewDocument': { + params: { + script: t.String, + worldName: t.Optional(t.String), + }, + returns: { + scriptId: t.String, + } + }, + 'removeScriptToEvaluateOnNewDocument': { + params: { + scriptId: t.String, + }, + }, + 'navigate': { + params: { + frameId: t.String, + url: t.String, + referer: t.Optional(t.String), + }, + returns: { + navigationId: t.Nullable(t.String), + navigationURL: t.Nullable(t.String), + } + }, + 'goBack': { + params: { + frameId: t.String, + }, + returns: { + success: t.Boolean, + }, + }, + 'goForward': { + params: { + frameId: t.String, + }, + returns: { + success: t.Boolean, + }, + }, + 'reload': { + params: { + frameId: t.String, + }, + }, + 'adoptNode': { + params: { + frameId: t.String, + objectId: t.String, + executionContextId: t.String, + }, + returns: { + remoteObject: t.Nullable(runtimeTypes.RemoteObject), + }, + }, + 'screenshot': { + params: { + mimeType: t.Enum(['image/png', 'image/jpeg']), + clip: t.Optional(pageTypes.Clip), + omitDeviceScaleFactor: t.Optional(t.Boolean), + }, + returns: { + data: t.String, + } + }, + 'getContentQuads': { + params: { + frameId: t.String, + objectId: t.String, + }, + returns: { + quads: t.Array(pageTypes.DOMQuad), + }, + }, + 'dispatchKeyEvent': { + params: { + type: t.String, + key: t.String, + keyCode: t.Number, + location: t.Number, + code: t.String, + repeat: t.Boolean, + text: t.Optional(t.String), + } + }, + 'dispatchTouchEvent': { + params: { + type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']), + touchPoints: t.Array(pageTypes.TouchPoint), + modifiers: t.Number, + }, + returns: { + defaultPrevented: t.Boolean, + } + }, + 'dispatchTapEvent': { + params: { + x: t.Number, + y: t.Number, + modifiers: t.Number, + } + }, + 'dispatchMouseEvent': { + params: { + type: t.String, + button: t.Number, + x: t.Number, + y: t.Number, + modifiers: t.Number, + clickCount: t.Optional(t.Number), + buttons: t.Number, + } + }, + 'insertText': { + params: { + text: t.String, + } + }, + 'crash': { + params: {} + }, + 'handleDialog': { + params: { + dialogId: t.String, + accept: t.Boolean, + promptText: t.Optional(t.String), + }, + }, + 'setInterceptFileChooserDialog': { + params: { + enabled: t.Boolean, + }, + }, + 'sendMessageToWorker': { + params: { + frameId: t.String, + workerId: t.String, + message: t.String, + }, + }, + 'startVideoRecording': { + params: { + file: t.String, + width: t.Number, + height: t.Number, + scale: t.Optional(t.Number), + }, + }, + 'stopVideoRecording': { + }, + }, +}; + + +const Accessibility = { + targets: ['page'], + types: axTypes, + events: {}, + methods: { + 'getFullAXTree': { + params: { + objectId: t.Optional(t.String), + }, + returns: { + tree: axTypes.AXTree + }, + } + } +} + +this.protocol = { + domains: {Browser, Page, Runtime, Network, Accessibility}, +}; +this.checkScheme = checkScheme; +this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme']; diff --git a/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.cpp b/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.cpp new file mode 100644 index 0000000000..0caf74d5cb --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.cpp @@ -0,0 +1,113 @@ +/* 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 "HeadlessWindowCapturer.h" + +#include "api/video/i420_buffer.h" +#include "HeadlessWidget.h" +#include "libyuv.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "rtc_base/refcountedobject.h" +#include "rtc_base/scoped_ref_ptr.h" + +using namespace mozilla::widget; +using namespace webrtc; + +namespace mozilla { + +rtc::scoped_refptr HeadlessWindowCapturer::Create(HeadlessWidget* headlessWindow) { + return new rtc::RefCountedObject(headlessWindow); +} + +HeadlessWindowCapturer::HeadlessWindowCapturer(mozilla::widget::HeadlessWidget* window) + : mWindow(window) { +} +HeadlessWindowCapturer::~HeadlessWindowCapturer() { + StopCapture(); +} + + +void HeadlessWindowCapturer::RegisterCaptureDataCallback(rtc::VideoSinkInterface* dataCallback) { + rtc::CritScope lock2(&_callBackCs); + _dataCallBacks.insert(dataCallback); +} +void HeadlessWindowCapturer::DeRegisterCaptureDataCallback(rtc::VideoSinkInterface* dataCallback) { + rtc::CritScope lock2(&_callBackCs); + auto it = _dataCallBacks.find(dataCallback); + if (it != _dataCallBacks.end()) { + _dataCallBacks.erase(it); + } +} + +void HeadlessWindowCapturer::NotifyFrameCaptured(const webrtc::VideoFrame& frame) { + rtc::CritScope lock2(&_callBackCs); + for (auto dataCallBack : _dataCallBacks) + dataCallBack->OnFrame(frame); +} + +int32_t HeadlessWindowCapturer::StopCaptureIfAllClientsClose() { + if (_dataCallBacks.empty()) { + return StopCapture(); + } else { + return 0; + } +} + +int32_t HeadlessWindowCapturer::StartCapture(const VideoCaptureCapability& capability) { + mWindow->SetSnapshotListener([this] (RefPtr&& dataSurface){ + if (!NS_IsInCompositorThread()) { + fprintf(stderr, "SnapshotListener is called not on the Compositor thread!\n"); + return; + } + + if (dataSurface->GetFormat() != gfx::SurfaceFormat::B8G8R8A8) { + fprintf(stderr, "Uexpected snapshot surface format: %hhd\n", dataSurface->GetFormat()); + return; + } + + int width = dataSurface->GetSize().width; + int height = dataSurface->GetSize().height; + rtc::scoped_refptr buffer = I420Buffer::Create(width, height); + + gfx::DataSourceSurface::ScopedMap map(dataSurface.get(), gfx::DataSourceSurface::MapType::READ); + if (!map.IsMapped()) { + fprintf(stderr, "Failed to map snapshot bytes!\n"); + return; + } + +#if MOZ_LITTLE_ENDIAN() + const int conversionResult = libyuv::ARGBToI420( +#else + const int conversionResult = libyuv::BGRAToI420( +#endif + map.GetData(), map.GetStride(), + buffer->MutableDataY(), buffer->StrideY(), + buffer->MutableDataU(), buffer->StrideU(), + buffer->MutableDataV(), buffer->StrideV(), + width, height); + + if (conversionResult != 0) { + fprintf(stderr, "Failed to convert capture frame to I420: %d\n", conversionResult); + return; + } + + VideoFrame captureFrame(buffer, 0, rtc::TimeMillis(), kVideoRotation_0); + NotifyFrameCaptured(captureFrame); + }); + return 0; +} + +int32_t HeadlessWindowCapturer::StopCapture() { + if (!CaptureStarted()) + return 0; + mWindow->SetSnapshotListener(nullptr); + return 0; +} + +bool HeadlessWindowCapturer::CaptureStarted() { + return true; +} + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.h b/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.h new file mode 100644 index 0000000000..86852b1e78 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/HeadlessWindowCapturer.h @@ -0,0 +1,59 @@ +/* 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/. */ + +#pragma once + +#include +#include +#include "api/video/video_frame.h" +#include "media/base/videosinkinterface.h" +#include "modules/video_capture/video_capture.h" +#include "rtc_base/criticalsection.h" + +class nsIWidget; + +namespace mozilla { + +namespace widget { +class HeadlessWidget; +} + +class HeadlessWindowCapturer : public webrtc::VideoCaptureModule { + public: + static rtc::scoped_refptr Create(mozilla::widget::HeadlessWidget*); + + void RegisterCaptureDataCallback( + rtc::VideoSinkInterface* dataCallback) override; + void DeRegisterCaptureDataCallback( + rtc::VideoSinkInterface* dataCallback) override; + int32_t StopCaptureIfAllClientsClose() override; + + int32_t SetCaptureRotation(webrtc::VideoRotation) override { return -1; } + bool SetApplyRotation(bool) override { return false; } + bool GetApplyRotation() override { return true; } + + const char* CurrentDeviceName() const override { return "Headless window"; } + + // Platform dependent + int32_t StartCapture(const webrtc::VideoCaptureCapability& capability) override; + bool FocusOnSelectedSource() override { return false; } + int32_t StopCapture() override; + bool CaptureStarted() override; + int32_t CaptureSettings(webrtc::VideoCaptureCapability& settings) override { + return -1; + } + + protected: + HeadlessWindowCapturer(mozilla::widget::HeadlessWidget*); + ~HeadlessWindowCapturer() override; + + private: + void NotifyFrameCaptured(const webrtc::VideoFrame& frame); + + mozilla::widget::HeadlessWidget* mWindow = nullptr; + rtc::CriticalSection _callBackCs; + std::set*> _dataCallBacks; +}; + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.cpp b/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.cpp new file mode 100644 index 0000000000..d0352f1cfa --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.cpp @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2010, The WebM Project authors. All rights reserved. + * Copyright (c) 2013 The Chromium Authors. All rights reserved. + * Copyright (C) 2020 Microsoft Corporation. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* 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 "ScreencastEncoder.h" + +#include +#include +#include +#include +#include +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "WebMFileWriter.h" +#include "webrtc/api/video/video_frame.h" + +namespace mozilla { + +namespace { + +// Number of timebase unints per one frame. +constexpr int timeScale = 1000; + +// Defines the dimension of a macro block. This is used to compute the active +// map for the encoder. +const int kMacroBlockSize = 16; + +void createImage(unsigned int width, unsigned int height, + std::unique_ptr& out_image, + std::unique_ptr& out_image_buffer, + int& out_buffer_size) { + std::unique_ptr image(new vpx_image_t()); + memset(image.get(), 0, sizeof(vpx_image_t)); + + // libvpx seems to require both to be assigned. + image->d_w = width; + image->w = width; + image->d_h = height; + image->h = height; + + // I420 + image->fmt = VPX_IMG_FMT_YV12; + image->x_chroma_shift = 1; + image->y_chroma_shift = 1; + + // libyuv's fast-path requires 16-byte aligned pointers and strides, so pad + // the Y, U and V planes' strides to multiples of 16 bytes. + const int y_stride = ((image->w - 1) & ~15) + 16; + const int uv_unaligned_stride = y_stride >> image->x_chroma_shift; + const int uv_stride = ((uv_unaligned_stride - 1) & ~15) + 16; + + // libvpx accesses the source image in macro blocks, and will over-read + // if the image is not padded out to the next macroblock: crbug.com/119633. + // Pad the Y, U and V planes' height out to compensate. + // Assuming macroblocks are 16x16, aligning the planes' strides above also + // macroblock aligned them. + static_assert(kMacroBlockSize == 16, "macroblock_size_not_16"); + const int y_rows = ((image->h - 1) & ~(kMacroBlockSize-1)) + kMacroBlockSize; + const int uv_rows = y_rows >> image->y_chroma_shift; + + // Allocate a YUV buffer large enough for the aligned data & padding. + out_buffer_size = y_stride * y_rows + 2*uv_stride * uv_rows; + std::unique_ptr image_buffer(new uint8_t[out_buffer_size]); + + // Reset image value to 128 so we just need to fill in the y plane. + memset(image_buffer.get(), 128, out_buffer_size); + + // Fill in the information for |image_|. + unsigned char* uchar_buffer = + reinterpret_cast(image_buffer.get()); + image->planes[0] = uchar_buffer; + image->planes[1] = image->planes[0] + y_stride * y_rows; + image->planes[2] = image->planes[1] + uv_stride * uv_rows; + image->stride[0] = y_stride; + image->stride[1] = uv_stride; + image->stride[2] = uv_stride; + + out_image = std::move(image); + out_image_buffer = std::move(image_buffer); +} + +} // namespace + +class ScreencastEncoder::VPXFrame { +public: + VPXFrame(rtc::scoped_refptr&& buffer, Maybe scale, const gfx::IntMargin& margin) + : m_frameBuffer(std::move(buffer)) + , m_scale(scale) + , m_margin(margin) + { } + + void setDuration(TimeDuration duration) { m_duration = duration; } + TimeDuration duration() const { return m_duration; } + + void convertToVpxImage(vpx_image_t* image) + { + if (m_frameBuffer->type() != webrtc::VideoFrameBuffer::Type::kI420) { + fprintf(stderr, "convertToVpxImage unexpected frame buffer type: %d\n", m_frameBuffer->type()); + return; + } + + auto src = m_frameBuffer->GetI420(); + const int y_stride = image->stride[VPX_PLANE_Y]; + MOZ_ASSERT(image->stride[VPX_PLANE_U] == image->stride[VPX_PLANE_V]); + const int uv_stride = image->stride[1]; + uint8_t* y_data = image->planes[VPX_PLANE_Y]; + uint8_t* u_data = image->planes[VPX_PLANE_U]; + uint8_t* v_data = image->planes[VPX_PLANE_V]; + + double src_width = src->width() - m_margin.LeftRight(); + double src_height = src->height() - m_margin.top; + + if (m_scale || (src_width > image->w || src_height > image->h)) { + double scale = m_scale ? m_scale.value() : std::min(image->w / src_width, image->h / src_height); + double dst_width = src_width * scale; + if (dst_width > image->w) { + src_width *= image->w / dst_width; + dst_width = image->w; + } + double dst_height = src_height * scale; + if (dst_height > image->h) { + src_height *= image->h / dst_height; + dst_height = image->h; + } + libyuv::I420Scale(src->DataY() + m_margin.top * src->StrideY() + m_margin.left, src->StrideY(), + src->DataU() + (m_margin.top * src->StrideU() + m_margin.left) / 2, src->StrideU(), + src->DataV() + (m_margin.top * src->StrideV() + m_margin.left) / 2, src->StrideV(), + src_width, src_height, + y_data, y_stride, + u_data, uv_stride, + v_data, uv_stride, + dst_width, dst_height, + libyuv::kFilterBilinear); + } else { + int width = std::min(image->w, src_width); + int height = std::min(image->h, src_height); + + libyuv::I420Copy(src->DataY() + m_margin.top * src->StrideY() + m_margin.left, src->StrideY(), + src->DataU() + (m_margin.top * src->StrideU() + m_margin.left) / 2, src->StrideU(), + src->DataV() + (m_margin.top * src->StrideV() + m_margin.left) / 2, src->StrideV(), + y_data, y_stride, + u_data, uv_stride, + v_data, uv_stride, + width, height); + } + } + +private: + rtc::scoped_refptr m_frameBuffer; + Maybe m_scale; + gfx::IntMargin m_margin; + TimeDuration m_duration; +}; + + +class ScreencastEncoder::VPXCodec { +public: + VPXCodec(vpx_codec_ctx_t codec, vpx_codec_enc_cfg_t cfg, FILE* file) + : m_codec(codec) + , m_cfg(cfg) + , m_file(file) + , m_writer(new WebMFileWriter(file, &m_cfg)) + { + nsresult rv = NS_NewNamedThread("Screencast enc", getter_AddRefs(m_encoderQueue)); + if (rv != NS_OK) { + fprintf(stderr, "ScreencastEncoder::VPXCodec failed to spawn thread %d\n", rv); + return; + } + + createImage(cfg.g_w, cfg.g_h, m_image, m_imageBuffer, m_imageBufferSize); + } + + ~VPXCodec() { + m_encoderQueue->Shutdown(); + m_encoderQueue = nullptr; + } + + void encodeFrameAsync(std::unique_ptr&& frame) + { + m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::encodeFrameAsync", [this, frame = std::move(frame)] { + memset(m_imageBuffer.get(), 128, m_imageBufferSize); + frame->convertToVpxImage(m_image.get()); + + double frameCount = frame->duration().ToSeconds() * fps; + // For long duration repeat frame at 1 fps to ensure last frame duration is short enough. + // TODO: figure out why simply passing duration doesn't work well. + for (;frameCount > 1.5; frameCount -= 1) { + encodeFrame(m_image.get(), timeScale); + } + encodeFrame(m_image.get(), std::max(1, frameCount * timeScale)); + })); + } + + void finishAsync(std::function&& callback) + { + m_encoderQueue->Dispatch(NS_NewRunnableFunction("VPXCodec::finishAsync", [this, callback = std::move(callback)] { + finish(); + callback(); + })); + } + +private: + bool encodeFrame(vpx_image_t *img, int duration) + { + vpx_codec_iter_t iter = nullptr; + const vpx_codec_cx_pkt_t *pkt = nullptr; + int flags = 0; + const vpx_codec_err_t res = vpx_codec_encode(&m_codec, img, m_pts, duration, flags, VPX_DL_REALTIME); + if (res != VPX_CODEC_OK) { + fprintf(stderr, "Failed to encode frame: %s\n", vpx_codec_error(&m_codec)); + return false; + } + + bool gotPkts = false; + while ((pkt = vpx_codec_get_cx_data(&m_codec, &iter)) != nullptr) { + gotPkts = true; + + if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) { + m_writer->writeFrame(pkt); + ++m_frameCount; + // fprintf(stderr, " #%03d %spts=%" PRId64 " sz=%zd\n", m_frameCount, (pkt->data.frame.flags & VPX_FRAME_IS_KEY) != 0 ? "[K] " : "", pkt->data.frame.pts, pkt->data.frame.sz); + m_pts += pkt->data.frame.duration; + } + } + + return gotPkts; + } + + void finish() + { + // Flush encoder. + while (encodeFrame(nullptr, 1)) + ++m_frameCount; + + m_writer->finish(); + fclose(m_file); + // fprintf(stderr, "ScreencastEncoder::finish %d frames\n", m_frameCount); + } + + RefPtr m_encoderQueue; + vpx_codec_ctx_t m_codec; + vpx_codec_enc_cfg_t m_cfg; + FILE* m_file { nullptr }; + std::unique_ptr m_writer; + int m_frameCount { 0 }; + int64_t m_pts { 0 }; + std::unique_ptr m_imageBuffer; + int m_imageBufferSize { 0 }; + std::unique_ptr m_image; +}; + +ScreencastEncoder::ScreencastEncoder(std::unique_ptr&& vpxCodec, Maybe scale, const gfx::IntMargin& margin) + : m_vpxCodec(std::move(vpxCodec)) + , m_scale(scale) + , m_margin(margin) +{ +} + +ScreencastEncoder::~ScreencastEncoder() +{ +} + +RefPtr ScreencastEncoder::create(nsCString& errorString, const nsCString& filePath, int width, int height, Maybe scale, const gfx::IntMargin& margin) +{ + vpx_codec_iface_t* codec_interface = vpx_codec_vp8_cx(); + if (!codec_interface) { + errorString = "Codec not found."; + return nullptr; + } + + if (width <= 0 || height <= 0 || (width % 2) != 0 || (height % 2) != 0) { + errorString.AppendPrintf("Invalid frame size: %dx%d", width, height); + return nullptr; + } + + vpx_codec_enc_cfg_t cfg; + memset(&cfg, 0, sizeof(cfg)); + vpx_codec_err_t error = vpx_codec_enc_config_default(codec_interface, &cfg, 0); + if (error) { + errorString.AppendPrintf("Failed to get default codec config: %s", vpx_codec_err_to_string(error)); + return nullptr; + } + + cfg.g_w = width; + cfg.g_h = height; + cfg.g_timebase.num = 1; + cfg.g_timebase.den = fps * timeScale; + cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT; + + vpx_codec_ctx_t codec; + if (vpx_codec_enc_init(&codec, codec_interface, &cfg, 0)) { + errorString.AppendPrintf("Failed to initialize encoder: %s", vpx_codec_error(&codec)); + return nullptr; + } + + FILE* file = fopen(filePath.get(), "wb"); + if (!file) { + errorString.AppendPrintf("Failed to open file '%s' for writing: %s", filePath.get(), strerror(errno)); + return nullptr; + } + + std::unique_ptr vpxCodec(new VPXCodec(codec, cfg, file)); + // fprintf(stderr, "ScreencastEncoder initialized with: %s\n", vpx_codec_iface_name(codec_interface)); + return new ScreencastEncoder(std::move(vpxCodec), scale, margin); +} + +void ScreencastEncoder::flushLastFrame() +{ + TimeStamp now = TimeStamp::Now(); + if (m_lastFrameTimestamp) { + // If previous frame encoding failed for some rason leave the timestampt intact. + if (!m_lastFrame) + return; + + m_lastFrame->setDuration(now - m_lastFrameTimestamp); + m_vpxCodec->encodeFrameAsync(std::move(m_lastFrame)); + } + m_lastFrameTimestamp = now; +} + +void ScreencastEncoder::encodeFrame(const webrtc::VideoFrame& videoFrame) +{ + // fprintf(stderr, "ScreencastEncoder::encodeFrame\n"); + flushLastFrame(); + + m_lastFrame = std::make_unique(videoFrame.video_frame_buffer(), m_scale, m_margin); +} + +void ScreencastEncoder::finish(std::function&& callback) +{ + if (!m_vpxCodec) { + callback(); + return; + } + + flushLastFrame(); + m_vpxCodec->finishAsync([callback = std::move(callback)] () mutable { + NS_DispatchToMainThread(NS_NewRunnableFunction("ScreencastEncoder::finish callback", std::move(callback))); + }); +} + + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.h b/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.h new file mode 100644 index 0000000000..b6827800bc --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/ScreencastEncoder.h @@ -0,0 +1,48 @@ +/* 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/. */ + +#pragma once + +#include +#include +#include "mozilla/gfx/Rect.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "nsISupportsImpl.h" +#include "nsStringFwd.h" + +namespace webrtc { +class VideoFrame; +} + +namespace mozilla { + +class ScreencastEncoder { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ScreencastEncoder) +public: + static constexpr int fps = 25; + + static RefPtr create(nsCString& errorString, const nsCString& filePath, int width, int height, Maybe scale, const gfx::IntMargin& margin); + + class VPXCodec; + ScreencastEncoder(std::unique_ptr&&, Maybe scale, const gfx::IntMargin& margin); + + void encodeFrame(const webrtc::VideoFrame& videoFrame); + + void finish(std::function&& callback); + +private: + ~ScreencastEncoder(); + + void flushLastFrame(); + + std::unique_ptr m_vpxCodec; + Maybe m_scale; + gfx::IntMargin m_margin; + TimeStamp m_lastFrameTimestamp; + class VPXFrame; + std::unique_ptr m_lastFrame; +}; + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.cpp b/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.cpp new file mode 100644 index 0000000000..f720b300f2 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 The WebM project authors. All Rights Reserved. + */ + +/* 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 "WebMFileWriter.h" + +#include +#include "mkvmuxer/mkvmuxerutil.h" + +namespace mozilla { + +WebMFileWriter::WebMFileWriter(FILE* file, vpx_codec_enc_cfg_t* cfg) + : m_cfg(cfg) + , m_writer(new mkvmuxer::MkvWriter(file)) + , m_segment(new mkvmuxer::Segment()) { + m_segment->Init(m_writer.get()); + m_segment->set_mode(mkvmuxer::Segment::kFile); + m_segment->OutputCues(true); + + mkvmuxer::SegmentInfo* info = m_segment->GetSegmentInfo(); + std::string version = "Playwright " + std::string(vpx_codec_version_str()); + info->set_writing_app(version.c_str()); + + // Add vp8 track. + m_videoTrackId = m_segment->AddVideoTrack( + static_cast(m_cfg->g_w), static_cast(m_cfg->g_h), 0); + if (!m_videoTrackId) { + fprintf(stderr, "Failed to add video track\n"); + } +} + +WebMFileWriter::~WebMFileWriter() {} + +void WebMFileWriter::writeFrame(const vpx_codec_cx_pkt_t* pkt) { + int64_t pts_ns = pkt->data.frame.pts * 1000000000ll * m_cfg->g_timebase.num / + m_cfg->g_timebase.den; + m_segment->AddFrame(static_cast(pkt->data.frame.buf), + pkt->data.frame.sz, m_videoTrackId, pts_ns, + pkt->data.frame.flags & VPX_FRAME_IS_KEY); +} + +void WebMFileWriter::finish() { + m_segment->Finalize(); +} + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.h b/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.h new file mode 100644 index 0000000000..4a7fd06e6c --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/WebMFileWriter.h @@ -0,0 +1,32 @@ +/* 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/. */ + +#pragma once + +#include +#include +#include +#include "vpx/vpx_encoder.h" + +#include "mkvmuxer/mkvmuxer.h" +#include "mkvmuxer/mkvwriter.h" + +namespace mozilla { + +class WebMFileWriter { +public: + WebMFileWriter(FILE*, vpx_codec_enc_cfg_t* cfg); + ~WebMFileWriter(); + + void writeFrame(const vpx_codec_cx_pkt_t* pkt); + void finish(); + +private: + vpx_codec_enc_cfg_t* m_cfg = nullptr; + std::unique_ptr m_writer; + std::unique_ptr m_segment; + uint64_t m_videoTrackId = 0; +}; + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/components.conf b/browser_patches/firefox-stable/juggler/screencast/components.conf new file mode 100644 index 0000000000..6298739122 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/components.conf @@ -0,0 +1,15 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{d8c4d9e0-9462-445e-9e43-68d3872ad1de}', + 'contract_ids': ['@mozilla.org/juggler/screencast;1'], + 'type': 'nsIScreencastService', + 'constructor': 'mozilla::nsScreencastService::GetSingleton', + 'headers': ['/juggler/screencast/nsScreencastService.h'], + }, +] diff --git a/browser_patches/firefox-stable/juggler/screencast/moz.build b/browser_patches/firefox-stable/juggler/screencast/moz.build new file mode 100644 index 0000000000..aa5660ddc6 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/moz.build @@ -0,0 +1,49 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIScreencastService.idl', +] + +XPIDL_MODULE = 'jugglerscreencast' + +SOURCES += [ + 'HeadlessWindowCapturer.cpp', + 'nsScreencastService.cpp', + 'ScreencastEncoder.cpp', +] + +XPCOM_MANIFESTS += [ + 'components.conf', +] + +LOCAL_INCLUDES += [ + '/dom/media/systemservices', + '/media/libyuv/libyuv/include', + '/third_party/libwebrtc', + '/third_party/libwebrtc/webrtc', +] + +LOCAL_INCLUDES += [ + '/widget', + '/widget/headless', +] + +LOCAL_INCLUDES += [ + '/third_party/aom/third_party/libwebm', +] + +SOURCES += [ + '/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxer.cc', + '/third_party/aom/third_party/libwebm/mkvmuxer/mkvmuxerutil.cc', + '/third_party/aom/third_party/libwebm/mkvmuxer/mkvwriter.cc', + 'WebMFileWriter.cpp', +] + +include('/dom/media/webrtc/third_party_build/webrtc.mozbuild') +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/browser_patches/firefox-stable/juggler/screencast/nsIScreencastService.idl b/browser_patches/firefox-stable/juggler/screencast/nsIScreencastService.idl new file mode 100644 index 0000000000..d3a82a7cda --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/nsIScreencastService.idl @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIDocShell; + +/** + * Service for recording window video. + */ +[scriptable, uuid(d8c4d9e0-9462-445e-9e43-68d3872ad1de)] +interface nsIScreencastService : nsISupports +{ + AString startVideoRecording(in nsIDocShell docShell, in ACString fileName, in uint32_t width, in uint32_t height, in double scale, in int32_t offset_top); + + /** + * Will emit 'juggler-screencast-stopped' when the video file is saved. + */ + void stopVideoRecording(in AString sessionId); +}; diff --git a/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.cpp b/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.cpp new file mode 100644 index 0000000000..36e4c69e4d --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.cpp @@ -0,0 +1,213 @@ +/* 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 "nsScreencastService.h" + +#include "ScreencastEncoder.h" +#include "HeadlessWidget.h" +#include "HeadlessWindowCapturer.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPtr.h" +#include "nsIDocShell.h" +#include "nsIObserverService.h" +#include "nsIRandomGenerator.h" +#include "nsISupportsPrimitives.h" +#include "nsThreadManager.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "webrtc/modules/desktop_capture/desktop_capturer.h" +#include "webrtc/modules/desktop_capture/desktop_capture_options.h" +#include "webrtc/modules/desktop_capture/desktop_device_info.h" +#include "webrtc/modules/desktop_capture/desktop_frame.h" +#include "webrtc/modules/video_capture/video_capture.h" +#include "mozilla/widget/PlatformWidgetTypes.h" +#include "video_engine/desktop_capture_impl.h" + +using namespace mozilla::widget; + +namespace mozilla { + +NS_IMPL_ISUPPORTS(nsScreencastService, nsIScreencastService) + +namespace { + +StaticRefPtr gScreencastService; + +rtc::scoped_refptr CreateWindowCapturer(nsIWidget* widget) { + if (gfxPlatform::IsHeadless()) { + HeadlessWidget* headlessWidget = static_cast(widget); + return HeadlessWindowCapturer::Create(headlessWidget); + } + uintptr_t rawWindowId = reinterpret_cast(widget->GetNativeData(NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID)); + if (!rawWindowId) { + fprintf(stderr, "Failed to get native window id\n"); + return nullptr; + } + nsCString windowId; + windowId.AppendPrintf("%" PRIuPTR, rawWindowId); + bool captureCursor = false; + static int moduleId = 0; + return webrtc::DesktopCaptureImpl::Create(++moduleId, windowId.get(), webrtc::CaptureDeviceType::Window, captureCursor); +} + +void NotifyScreencastStopped(const nsString& sessionId) { + nsCOMPtr observerService = mozilla::services::GetObserverService(); + if (!observerService) { + fprintf(stderr, "NotifyScreencastStopped error: no observer service\n"); + return; + } + + observerService->NotifyObservers(nullptr, "juggler-screencast-stopped", sessionId.get()); +} + +nsresult generateUid(nsString& uid) { + nsresult rv = NS_OK; + nsCOMPtr rg = do_GetService("@mozilla.org/security/random-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint8_t* buffer; + const int kLen = 16; + rv = rg->GenerateRandomBytes(kLen, &buffer); + NS_ENSURE_SUCCESS(rv, rv); + + for (int i = 0; i < kLen; i++) { + uid.AppendPrintf("%02x", buffer[i]); + } + free(buffer); + return rv; +} +} + +class nsScreencastService::Session : public rtc::VideoSinkInterface { + public: + Session(rtc::scoped_refptr&& capturer, RefPtr&& encoder) + : mCaptureModule(std::move(capturer)) + , mEncoder(std::move(encoder)) { + } + + bool Start() { + webrtc::VideoCaptureCapability capability; + // The size is ignored in fact. + capability.width = 1280; + capability.height = 960; + capability.maxFPS = ScreencastEncoder::fps; + capability.videoType = webrtc::VideoType::kI420; + int error = mCaptureModule->StartCapture(capability); + if (error) { + fprintf(stderr, "StartCapture error %d\n", error); + return false; + } + + mCaptureModule->RegisterCaptureDataCallback(this); + return true; + } + + void Stop(std::function&& callback) { + mCaptureModule->DeRegisterCaptureDataCallback(this); + int error = mCaptureModule->StopCapture(); + if (error) { + fprintf(stderr, "StopCapture error %d\n", error); + } + mEncoder->finish(std::move(callback)); + } + + // These callbacks end up running on the VideoCapture thread. + void OnFrame(const webrtc::VideoFrame& videoFrame) override { + mEncoder->encodeFrame(videoFrame); + } + + private: + rtc::scoped_refptr mCaptureModule; + RefPtr mEncoder; +}; + + +// static +already_AddRefed nsScreencastService::GetSingleton() { + if (gScreencastService) { + return do_AddRef(gScreencastService); + } + + gScreencastService = new nsScreencastService(); + // ClearOnShutdown(&gScreencastService); + return do_AddRef(gScreencastService); +} + +nsScreencastService::nsScreencastService() = default; + +nsScreencastService::~nsScreencastService() { +} + +nsresult nsScreencastService::StartVideoRecording(nsIDocShell* aDocShell, const nsACString& aFileName, uint32_t width, uint32_t height, double scale, int32_t offsetTop, nsAString& sessionId) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Screencast service must be started on the Main thread."); + + PresShell* presShell = aDocShell->GetPresShell(); + if (!presShell) + return NS_ERROR_UNEXPECTED; + nsViewManager* viewManager = presShell->GetViewManager(); + if (!viewManager) + return NS_ERROR_UNEXPECTED; + nsView* view = viewManager->GetRootView(); + if (!view) + return NS_ERROR_UNEXPECTED; + nsIWidget* widget = view->GetWidget(); + + rtc::scoped_refptr capturer = CreateWindowCapturer(widget); + if (!capturer) + return NS_ERROR_FAILURE; + + nsCString error; + Maybe maybeScale; + if (scale) + maybeScale = Some(scale); + + gfx::IntMargin margin; + // On GTK the bottom of the client rect is below the bounds and + // client size is actually equal to the size of the bounds so + // we don't need an adjustment. +#ifndef MOZ_WIDGET_GTK + auto bounds = widget->GetScreenBounds().ToUnknownRect(); + auto clientBounds = widget->GetClientBounds().ToUnknownRect(); + // Crop the image to exclude frame (if any). + margin = bounds - clientBounds; +#endif + // Crop the image to exclude controls. + margin.top += offsetTop; + + RefPtr encoder = ScreencastEncoder::create(error, PromiseFlatCString(aFileName), width, height, maybeScale, margin); + if (!encoder) { + fprintf(stderr, "Failed to create ScreencastEncoder: %s\n", error.get()); + return NS_ERROR_FAILURE; + } + + auto session = std::make_unique(std::move(capturer), std::move(encoder)); + if (!session->Start()) + return NS_ERROR_FAILURE; + + nsString uid; + nsresult rv = generateUid(uid); + NS_ENSURE_SUCCESS(rv, rv); + + sessionId = uid; + mIdToSession.emplace(uid, std::move(session)); + return NS_OK; +} + +nsresult nsScreencastService::StopVideoRecording(const nsAString& aSessionId) { + nsString sessionId(aSessionId); + auto it = mIdToSession.find(sessionId); + if (it == mIdToSession.end()) + return NS_ERROR_INVALID_ARG; + it->second->Stop([sessionId] { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "NotifyScreencastStopped", [sessionId]() -> void { + NotifyScreencastStopped(sessionId); + })); + }); + mIdToSession.erase(it); + return NS_OK; +} + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.h b/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.h new file mode 100644 index 0000000000..082416ca74 --- /dev/null +++ b/browser_patches/firefox-stable/juggler/screencast/nsScreencastService.h @@ -0,0 +1,29 @@ +/* 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/. */ + +#pragma once + +#include +#include +#include "nsIScreencastService.h" + +namespace mozilla { + +class nsScreencastService final : public nsIScreencastService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISCREENCASTSERVICE + + static already_AddRefed GetSingleton(); + + nsScreencastService(); + + private: + ~nsScreencastService(); + + class Session; + std::map> mIdToSession; +}; + +} // namespace mozilla diff --git a/browser_patches/firefox-stable/patches/bootstrap.diff b/browser_patches/firefox-stable/patches/bootstrap.diff new file mode 100644 index 0000000000..0ea3cf1157 --- /dev/null +++ b/browser_patches/firefox-stable/patches/bootstrap.diff @@ -0,0 +1,2499 @@ +diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h +index 07d8011d67c4c830b131275bba051a5f2467c567..c87361238e2be5c080fb04554962c65aa6f11052 100644 +--- a/accessible/base/NotificationController.h ++++ b/accessible/base/NotificationController.h +@@ -274,6 +274,8 @@ class NotificationController final : public EventQueue, + } + #endif + ++ bool IsUpdatePendingForJugglerAccessibility() { return IsUpdatePending(); } ++ + protected: + virtual ~NotificationController(); + +diff --git a/accessible/interfaces/nsIAccessibleDocument.idl b/accessible/interfaces/nsIAccessibleDocument.idl +index a91df31c96afda66f478a5a38eaa4352039c2a0b..ee777c1746284027fb3aa2f1686f8082af9d89ee 100644 +--- a/accessible/interfaces/nsIAccessibleDocument.idl ++++ b/accessible/interfaces/nsIAccessibleDocument.idl +@@ -72,4 +72,9 @@ interface nsIAccessibleDocument : nsISupports + * Return the child document accessible at the given index. + */ + nsIAccessibleDocument getChildDocumentAt(in unsigned long index); ++ ++ /** ++ * Return whether it is updating. ++ */ ++ readonly attribute boolean isUpdatePendingForJugglerAccessibility; + }; +diff --git a/accessible/xpcom/xpcAccessibleDocument.cpp b/accessible/xpcom/xpcAccessibleDocument.cpp +index 57566c4569fba26bb5d74f4eb202e93dcf65778f..7b3305b9fb5ccf4796cc1bd2e0fffdcaa1f81330 100644 +--- a/accessible/xpcom/xpcAccessibleDocument.cpp ++++ b/accessible/xpcom/xpcAccessibleDocument.cpp +@@ -143,6 +143,15 @@ xpcAccessibleDocument::GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) { + return NS_OK; + } + ++ ++NS_IMETHODIMP ++xpcAccessibleDocument::GetIsUpdatePendingForJugglerAccessibility(bool* updating) { ++ NS_ENSURE_ARG_POINTER(updating); ++ *updating = Intl()->Controller()->IsUpdatePendingForJugglerAccessibility(); ++ return NS_OK; ++} ++ ++ + //////////////////////////////////////////////////////////////////////////////// + // xpcAccessibleDocument + +diff --git a/accessible/xpcom/xpcAccessibleDocument.h b/accessible/xpcom/xpcAccessibleDocument.h +index b08377cf1016ee8c24cf8782adf861a5b6dca0ac..cc606615f855026930375079bde89a3ebd580d46 100644 +--- a/accessible/xpcom/xpcAccessibleDocument.h ++++ b/accessible/xpcom/xpcAccessibleDocument.h +@@ -48,6 +48,8 @@ class xpcAccessibleDocument : public xpcAccessibleHyperText, + nsIAccessibleDocument** aDocument) final; + NS_IMETHOD GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) final; + ++ NS_IMETHOD GetIsUpdatePendingForJugglerAccessibility(bool* aUpdating) final; ++ + /** + * Return XPCOM wrapper for the internal accessible. + */ +diff --git a/browser/app/winlauncher/LauncherProcessWin.cpp b/browser/app/winlauncher/LauncherProcessWin.cpp +index 4fa1d742bd5e0c03b1c93b8bf7cca2fcc8cbded1..1af645fdb86c4a803e1a5f0be08c1d09ab6a16cd 100644 +--- a/browser/app/winlauncher/LauncherProcessWin.cpp ++++ b/browser/app/winlauncher/LauncherProcessWin.cpp +@@ -23,6 +23,7 @@ + #include "mozilla/WinHeaderOnlyUtils.h" + #include "nsWindowsHelpers.h" + ++#include + #include + #include + +@@ -327,8 +328,19 @@ Maybe LauncherMain(int& argc, wchar_t* argv[], + HANDLE stdHandles[] = {::GetStdHandle(STD_INPUT_HANDLE), + ::GetStdHandle(STD_OUTPUT_HANDLE), + ::GetStdHandle(STD_ERROR_HANDLE)}; +- + attrs.AddInheritableHandles(stdHandles); ++ // Playwright pipe installation. ++ bool hasJugglerPipe = ++ mozilla::CheckArg(argc, argv, L"juggler-pipe", ++ static_cast(nullptr), ++ mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND; ++ if (hasJugglerPipe) { ++ intptr_t stdio3 = _get_osfhandle(3); ++ intptr_t stdio4 = _get_osfhandle(4); ++ HANDLE pipeHandles[] = {reinterpret_cast(stdio3), ++ reinterpret_cast(stdio4)}; ++ attrs.AddInheritableHandles(pipeHandles); ++ } + + DWORD creationFlags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT; + +diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn +index a01e9a49ecac2134ab2d3b28f920fa564d88935f..1b1073ecf0d3ec061fac6d34f9161d0096167821 100644 +--- a/browser/installer/allowed-dupes.mn ++++ b/browser/installer/allowed-dupes.mn +@@ -65,6 +65,12 @@ browser/defaults/settings/main/example.json + browser/defaults/settings/main/search-default-override-allowlist.json + browser/defaults/settings/main/url-classifier-skip-urls.json + ++# Juggler/marionette files ++chrome/juggler/content/content/floating-scrollbars.css ++browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css ++chrome/juggler/content/server/stream-utils.js ++chrome/marionette/content/stream-utils.js ++ + #ifdef MOZ_EME_WIN32_ARTIFACT + gmp-clearkey/0.1/manifest.json + i686/gmp-clearkey/0.1/manifest.json +diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in +index ec20499166e83c4ceb6d5aa2e30e3e0297b5bcf8..c77dee1d16dce70c07627d509eb529c793df6b2a 100644 +--- a/browser/installer/package-manifest.in ++++ b/browser/installer/package-manifest.in +@@ -218,6 +218,11 @@ + @RESPATH@/components/marionette.js + #endif + ++@RESPATH@/chrome/juggler@JAREXT@ ++@RESPATH@/chrome/juggler.manifest ++@RESPATH@/components/juggler.manifest ++@RESPATH@/components/juggler.js ++ + #if defined(ENABLE_TESTS) && defined(MOZ_DEBUG) + @RESPATH@/components/TestInterfaceJS.js + @RESPATH@/components/TestInterfaceJS.manifest +diff --git a/devtools/server/socket/websocket-server.js b/devtools/server/socket/websocket-server.js +index 040c7b124dec6bb254563bbe74fe50012cb077a3..b4e6b8132786af70e8ad0dce88b67c2835307f88 100644 +--- a/devtools/server/socket/websocket-server.js ++++ b/devtools/server/socket/websocket-server.js +@@ -133,13 +133,12 @@ function writeHttpResponse(output, response) { + * Process the WebSocket handshake headers and return the key to be sent in + * Sec-WebSocket-Accept response header. + */ +-function processRequest({ requestLine, headers }) { ++function processRequest({ requestLine, headers }, expectedPath) { + const [method, path] = requestLine.split(" "); + if (method !== "GET") { + throw new Error("The handshake request must use GET method"); + } +- +- if (path !== "/") { ++ if (path !== expectedPath) { + throw new Error("The handshake request has unknown path"); + } + +@@ -189,13 +188,13 @@ function computeKey(key) { + /** + * Perform the server part of a WebSocket opening handshake on an incoming connection. + */ +-const serverHandshake = async function(input, output) { ++const serverHandshake = async function(input, output, expectedPath) { + // Read the request + const request = await readHttpRequest(input); + + try { + // Check and extract info from the request +- const { acceptKey } = processRequest(request); ++ const { acceptKey } = processRequest(request, expectedPath); + + // Send response headers + await writeHttpResponse(output, [ +@@ -217,8 +216,8 @@ const serverHandshake = async function(input, output) { + * Performs the WebSocket handshake and waits for the WebSocket to open. + * Returns Promise with a WebSocket ready to send and receive messages. + */ +-const accept = async function(transport, input, output) { +- await serverHandshake(input, output); ++const accept = async function(transport, input, output, expectedPath) { ++ await serverHandshake(input, output, expectedPath || "/"); + + const transportProvider = { + setListener(upgradeListener) { +diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp +index fed621a4fe569104d92685500aed6ec729768037..f56b6e9c42c83c18887a1067b9ad40f12ca63222 100644 +--- a/docshell/base/nsDocShell.cpp ++++ b/docshell/base/nsDocShell.cpp +@@ -15,6 +15,12 @@ + # include // for getpid() + #endif + ++#if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU ++# include "unicode/locid.h" ++#endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */ ++ ++#include "js/LocaleSensitive.h" ++ + #include "mozilla/ArrayUtils.h" + #include "mozilla/Attributes.h" + #include "mozilla/AutoRestore.h" +@@ -61,6 +67,7 @@ + #include "mozilla/dom/ContentFrameMessageManager.h" + #include "mozilla/dom/DocGroup.h" + #include "mozilla/dom/Element.h" ++#include "mozilla/dom/Geolocation.h" + #include "mozilla/dom/HTMLAnchorElement.h" + #include "mozilla/dom/HTMLIFrameElement.h" + #include "mozilla/dom/PerformanceNavigation.h" +@@ -82,6 +89,7 @@ + #include "mozilla/dom/LoadURIOptionsBinding.h" + #include "mozilla/dom/JSWindowActorChild.h" + #include "mozilla/ipc/ProtocolUtils.h" ++#include "mozilla/dom/WorkerCommon.h" + #include "mozilla/net/DocumentChannel.h" + #include "mozilla/net/ParentChannelWrapper.h" + #include "mozilla/net/UrlClassifierFeatureFactory.h" +@@ -106,6 +114,7 @@ + #include "nsIDocShellTreeItem.h" + #include "nsIDocShellTreeOwner.h" + #include "mozilla/dom/Document.h" ++#include "mozilla/dom/Element.h" + #include "nsIDocumentLoaderFactory.h" + #include "nsIDOMWindow.h" + #include "nsIEditingSession.h" +@@ -200,6 +209,7 @@ + #include "nsGlobalWindow.h" + #include "nsISearchService.h" + #include "nsJSEnvironment.h" ++#include "nsJSUtils.h" + #include "nsNetCID.h" + #include "nsNetUtil.h" + #include "nsObjectLoadingContent.h" +@@ -395,6 +405,12 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext, + mAllowDNSPrefetch(true), + mAllowWindowControl(true), + mCSSErrorReportingEnabled(false), ++ mFileInputInterceptionEnabled(false), ++ mOverrideHasFocus(false), ++ mBypassCSPEnabled(false), ++ mForceActiveState(false), ++ mOnlineOverride(nsIDocShell::ONLINE_OVERRIDE_NONE), ++ mColorSchemeOverride(COLOR_SCHEME_OVERRIDE_NONE), + mAllowAuth(mItemType == typeContent), + mAllowKeywordFixup(false), + mIsOffScreenBrowser(false), +@@ -1448,6 +1464,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest, + } + } + ++ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags); + if (!isSubFrame && !isRoot) { + /* + * We don't want to send OnLocationChange notifications when +@@ -3276,6 +3293,203 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) { + return NS_OK; + } + ++// =============== Juggler Begin ======================= ++ ++nsDocShell* nsDocShell::GetRootDocShell() { ++ nsCOMPtr rootAsItem; ++ GetInProcessSameTypeRootTreeItem(getter_AddRefs(rootAsItem)); ++ nsCOMPtr rootShell = do_QueryInterface(rootAsItem); ++ return nsDocShell::Cast(rootShell); ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetBypassCSPEnabled(bool* aEnabled) { ++ MOZ_ASSERT(aEnabled); ++ *aEnabled = mBypassCSPEnabled; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetBypassCSPEnabled(bool aEnabled) { ++ mBypassCSPEnabled = aEnabled; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetForceActiveState(bool* aEnabled) { ++ MOZ_ASSERT(aEnabled); ++ *aEnabled = mForceActiveState; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetForceActiveState(bool aEnabled) { ++ mForceActiveState = aEnabled; ++ ActivenessMaybeChanged(); ++ return NS_OK; ++} ++ ++bool nsDocShell::IsBypassCSPEnabled() { ++ return GetRootDocShell()->mBypassCSPEnabled; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetOverrideHasFocus(bool* aEnabled) { ++ MOZ_ASSERT(aEnabled); ++ *aEnabled = mOverrideHasFocus; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetOverrideHasFocus(bool aEnabled) { ++ mOverrideHasFocus = aEnabled; ++ return NS_OK; ++} ++ ++bool nsDocShell::ShouldOverrideHasFocus() const { ++ return mOverrideHasFocus; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetLanguageOverride(nsAString& aLanguageOverride) { ++ aLanguageOverride = GetRootDocShell()->mLanguageOverride; ++ return NS_OK; ++} ++ ++ ++static void SetIcuLocale(const nsAString& aLanguageOverride) { ++ icu::Locale locale(NS_LossyConvertUTF16toASCII(aLanguageOverride).get()); ++ if (icu::Locale::getDefault() == locale) ++ return; ++ UErrorCode error_code = U_ZERO_ERROR; ++ const char* lang = locale.getLanguage(); ++ if (lang != nullptr && *lang != '\0') { ++ icu::Locale::setDefault(locale, error_code); ++ } else { ++ fprintf(stderr, "SetIcuLocale Failed to set the ICU default locale to %s\n", NS_LossyConvertUTF16toASCII(aLanguageOverride).get()); ++ } ++ ++ AutoJSAPI jsapi; ++ jsapi.Init(); ++ JSContext* cx = jsapi.cx(); ++ JS_ResetDefaultLocale(JS_GetRuntime(cx)); ++ ++ ResetDefaultLocaleInAllWorkers(); ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetLanguageOverride(const nsAString& aLanguageOverride) { ++ mLanguageOverride = aLanguageOverride; ++ SetIcuLocale(aLanguageOverride); ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::OverrideTimezone(const nsAString& aTimezoneOverride, ++ bool* aSuccess) { ++ NS_ENSURE_ARG(aSuccess); ++ NS_LossyConvertUTF16toASCII timeZoneId(aTimezoneOverride); ++ *aSuccess = nsJSUtils::SetTimeZoneOverride(timeZoneId.get()); ++ ++ // Set TZ which affects localtime_s(). ++ auto setTimeZoneEnv = [](const char* value) { ++#if defined(_WIN32) ++ return _putenv_s("TZ", value) == 0; ++#else ++ return setenv("TZ", value, true) == 0; ++#endif /* _WIN32 */ ++ }; ++ if (*aSuccess) { ++ *aSuccess = setTimeZoneEnv(timeZoneId.get()); ++ if (!*aSuccess) { ++ fprintf(stderr, "Failed to set 'TZ' to '%s'\n", timeZoneId.get()); ++ } ++ } ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetFileInputInterceptionEnabled(bool* aEnabled) { ++ MOZ_ASSERT(aEnabled); ++ *aEnabled = GetRootDocShell()->mFileInputInterceptionEnabled; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetFileInputInterceptionEnabled(bool aEnabled) { ++ mFileInputInterceptionEnabled = aEnabled; ++ return NS_OK; ++} ++ ++bool nsDocShell::IsFileInputInterceptionEnabled() { ++ return GetRootDocShell()->mFileInputInterceptionEnabled; ++} ++ ++void nsDocShell::FilePickerShown(mozilla::dom::Element* element) { ++ nsCOMPtr observerService = ++ mozilla::services::GetObserverService(); ++ observerService->NotifyObservers( ++ ToSupports(element), "juggler-file-picker-shown", nullptr); ++} ++ ++RefPtr nsDocShell::GetGeolocationServiceOverride() { ++ return GetRootDocShell()->mGeolocationServiceOverride; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetGeolocationOverride(nsIDOMGeoPosition* aGeolocationOverride) { ++ if (aGeolocationOverride) { ++ if (!mGeolocationServiceOverride) { ++ mGeolocationServiceOverride = new nsGeolocationService(); ++ mGeolocationServiceOverride->Init(); ++ } ++ mGeolocationServiceOverride->Update(aGeolocationOverride); ++ } else { ++ mGeolocationServiceOverride = nullptr; ++ } ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetOnlineOverride(OnlineOverride* aOnlineOverride) { ++ *aOnlineOverride = GetRootDocShell()->mOnlineOverride; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetOnlineOverride(OnlineOverride aOnlineOverride) { ++ // We don't have a way to verify this coming from Javascript, so this check is ++ // still needed. ++ if (!(aOnlineOverride == ONLINE_OVERRIDE_NONE || ++ aOnlineOverride == ONLINE_OVERRIDE_ONLINE || ++ aOnlineOverride == ONLINE_OVERRIDE_OFFLINE)) { ++ return NS_ERROR_INVALID_ARG; ++ } ++ ++ mOnlineOverride = aOnlineOverride; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::GetColorSchemeOverride(ColorSchemeOverride* aColorSchemeOverride) { ++ *aColorSchemeOverride = GetRootDocShell()->mColorSchemeOverride; ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsDocShell::SetColorSchemeOverride(ColorSchemeOverride aColorSchemeOverride) { ++ mColorSchemeOverride = aColorSchemeOverride; ++ RefPtr presContext = GetPresContext(); ++ if (presContext) { ++ presContext->MediaFeatureValuesChanged( ++ {MediaFeatureChangeReason::SystemMetricsChange}, ++ MediaFeatureChangePropagation::JustThisDocument); ++ } ++ return NS_OK; ++} ++ ++// =============== Juggler End ======================= ++ + NS_IMETHODIMP + nsDocShell::GetIsNavigating(bool* aOut) { + *aOut = mIsNavigating; +@@ -4914,7 +5128,7 @@ nsDocShell::GetIsOffScreenBrowser(bool* aIsOffScreen) { + } + + void nsDocShell::ActivenessMaybeChanged() { +- bool isActive = mBrowsingContext->IsActive(); ++ bool isActive = mForceActiveState || mBrowsingContext->IsActive(); + if (RefPtr presShell = GetPresShell()) { + presShell->SetIsActive(isActive); + } +@@ -8642,6 +8856,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) { + true, // aForceNoOpener + getter_AddRefs(newBC)); + MOZ_ASSERT(!newBC); ++ if (rv == NS_OK) { ++ nsCOMPtr observerService = mozilla::services::GetObserverService(); ++ if (observerService) { ++ observerService->NotifyObservers(GetAsSupports(this), "juggler-window-open-in-new-context", nullptr); ++ } ++ } + return rv; + } + +@@ -12642,6 +12862,9 @@ class OnLinkClickEvent : public Runnable { + mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied, + mTriggeringPrincipal); + } ++ nsCOMPtr observerService = mozilla::services::GetObserverService(); ++ observerService->NotifyObservers(ToSupports(mContent), "juggler-link-click-sync", nullptr); ++ + return NS_OK; + } + +@@ -12727,6 +12950,8 @@ nsresult nsDocShell::OnLinkClick( + nsCOMPtr ev = + new OnLinkClickEvent(this, aContent, loadState, noOpenerImplied, + aIsTrusted, aTriggeringPrincipal); ++ nsCOMPtr observerService = mozilla::services::GetObserverService(); ++ observerService->NotifyObservers(ToSupports(aContent), "juggler-link-click", nullptr); + return Dispatch(TaskCategory::UI, ev.forget()); + } + +diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h +index 50c1e78a4d985fb5500179594f973477101a17f6..ff1beacdedcedb865c0839440192a77892e199f8 100644 +--- a/docshell/base/nsDocShell.h ++++ b/docshell/base/nsDocShell.h +@@ -14,6 +14,7 @@ + #include "mozilla/UniquePtr.h" + #include "mozilla/WeakPtr.h" + #include "mozilla/dom/BrowsingContext.h" ++#include "mozilla/dom/Element.h" + #include "mozilla/dom/WindowProxyHolder.h" + #include "nsCOMPtr.h" + #include "nsCharsetSource.h" +@@ -75,6 +76,7 @@ class nsCommandManager; + class nsDocShellEditorData; + class nsDOMNavigationTiming; + class nsDSURIContentListener; ++class nsGeolocationService; + class nsGlobalWindowOuter; + + class FramingChecker; +@@ -401,6 +403,15 @@ class nsDocShell final : public nsDocLoader, + void SetWillChangeProcess() { mWillChangeProcess = true; } + bool WillChangeProcess() { return mWillChangeProcess; } + ++ bool IsFileInputInterceptionEnabled(); ++ void FilePickerShown(mozilla::dom::Element* element); ++ ++ bool ShouldOverrideHasFocus() const; ++ ++ bool IsBypassCSPEnabled(); ++ ++ RefPtr GetGeolocationServiceOverride(); ++ + // Create a content viewer within this nsDocShell for the given + // `WindowGlobalChild` actor. + nsresult CreateContentViewerForActor( +@@ -988,6 +999,8 @@ class nsDocShell final : public nsDocLoader, + + bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; } + ++ nsDocShell* GetRootDocShell(); ++ + // Handles retrieval of subframe session history for nsDocShell::LoadURI. If a + // load is requested in a subframe of the current DocShell, the subframe + // loadType may need to reflect the loadType of the parent document, or in +@@ -1225,6 +1238,15 @@ class nsDocShell final : public nsDocLoader, + bool mAllowDNSPrefetch : 1; + bool mAllowWindowControl : 1; + bool mCSSErrorReportingEnabled : 1; ++ bool mFileInputInterceptionEnabled: 1; ++ bool mOverrideHasFocus : 1; ++ bool mBypassCSPEnabled : 1; ++ bool mForceActiveState : 1; ++ nsString mLanguageOverride; ++ RefPtr mGeolocationServiceOverride; ++ OnlineOverride mOnlineOverride; ++ ColorSchemeOverride mColorSchemeOverride; ++ + bool mAllowAuth : 1; + bool mAllowKeywordFixup : 1; + bool mIsOffScreenBrowser : 1; +diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl +index afa1eee3a6107067be52bf635e94be4271facee0..8d3e7bca533da4e55cc43843de552a12036d8340 100644 +--- a/docshell/base/nsIDocShell.idl ++++ b/docshell/base/nsIDocShell.idl +@@ -44,6 +44,7 @@ interface nsIURI; + interface nsIChannel; + interface nsIContentViewer; + interface nsIContentSecurityPolicy; ++interface nsIDOMGeoPosition; + interface nsIDocShellLoadInfo; + interface nsIEditor; + interface nsIEditingSession; +@@ -888,6 +889,35 @@ interface nsIDocShell : nsIDocShellTreeItem + */ + void synchronizeLayoutHistoryState(); + ++ attribute boolean fileInputInterceptionEnabled; ++ ++ attribute boolean overrideHasFocus; ++ ++ attribute boolean bypassCSPEnabled; ++ ++ attribute boolean forceActiveState; ++ ++ attribute AString languageOverride; ++ ++ boolean overrideTimezone(in AString timezoneId); ++ ++ cenum OnlineOverride: 8 { ++ ONLINE_OVERRIDE_NONE = 0, ++ ONLINE_OVERRIDE_ONLINE = 1, ++ ONLINE_OVERRIDE_OFFLINE = 2, ++ }; ++ [infallible] attribute nsIDocShell_OnlineOverride onlineOverride; ++ ++ cenum ColorSchemeOverride : 8 { ++ COLOR_SCHEME_OVERRIDE_LIGHT, ++ COLOR_SCHEME_OVERRIDE_DARK, ++ COLOR_SCHEME_OVERRIDE_NO_PREFERENCE, ++ COLOR_SCHEME_OVERRIDE_NONE, /* This clears the override. */ ++ }; ++ [infallible] attribute nsIDocShell_ColorSchemeOverride colorSchemeOverride; ++ ++ void setGeolocationOverride(in nsIDOMGeoPosition position); ++ + /** + * This attempts to save any applicable layout history state (like + * scroll position) in the nsISHEntry. This is normally done +diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp +index 48af2c30f3604a94e6bc1da7848aed3c8ac11c1e..17df24bfdd2d9752686a46b6bb0afc575b091fcb 100644 +--- a/dom/base/Document.cpp ++++ b/dom/base/Document.cpp +@@ -3490,6 +3490,9 @@ void Document::SendToConsole(nsCOMArray& aMessages) { + } + + void Document::ApplySettingsFromCSP(bool aSpeculative) { ++ if (mDocumentContainer && mDocumentContainer->IsBypassCSPEnabled()) ++ return; ++ + nsresult rv = NS_OK; + if (!aSpeculative) { + // 1) apply settings from regular CSP +@@ -3544,6 +3547,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) { + return NS_OK; + } + ++ nsCOMPtr shell(mDocumentContainer); ++ if (shell && nsDocShell::Cast(shell)->IsBypassCSPEnabled()) { ++ return NS_OK; ++ } ++ + // If this is a data document - no need to set CSP. + if (mLoadedAsData) { + return NS_OK; +@@ -4336,6 +4344,10 @@ bool Document::HasFocus(ErrorResult& rv) const { + return false; + } + ++ if (IsActive() && mDocumentContainer->ShouldOverrideHasFocus()) { ++ return true; ++ } ++ + // Is there a focused DOMWindow? + nsCOMPtr focusedWindow; + fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); +@@ -16821,6 +16833,19 @@ void Document::RemoveToplevelLoadingDocument(Document* aDoc) { + + StylePrefersColorScheme Document::PrefersColorScheme( + IgnoreRFP aIgnoreRFP) const { ++ auto* docShell = static_cast(GetDocShell()); ++ nsIDocShell::ColorSchemeOverride colorScheme; ++ if (docShell && docShell->GetColorSchemeOverride(&colorScheme) == NS_OK && ++ colorScheme != nsIDocShell::COLOR_SCHEME_OVERRIDE_NONE) { ++ switch (colorScheme) { ++ case nsIDocShell::COLOR_SCHEME_OVERRIDE_LIGHT: ++ return StylePrefersColorScheme::Light; ++ case nsIDocShell::COLOR_SCHEME_OVERRIDE_DARK: ++ return StylePrefersColorScheme::Dark; ++ case nsIDocShell::COLOR_SCHEME_OVERRIDE_NO_PREFERENCE: ++ return StylePrefersColorScheme::NoPreference; ++ }; ++ } + if (aIgnoreRFP == IgnoreRFP::No && + nsContentUtils::ShouldResistFingerprinting(this)) { + return StylePrefersColorScheme::Light; +diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp +index 7764a6a690bb97fd4082292c13456a40040fb959..2135ca3d83557a6729907e14ff587ca070ab745e 100644 +--- a/dom/base/Navigator.cpp ++++ b/dom/base/Navigator.cpp +@@ -326,14 +326,18 @@ void Navigator::GetAppName(nsAString& aAppName, CallerType aCallerType) const { + * for more detail. + */ + /* static */ +-void Navigator::GetAcceptLanguages(nsTArray& aLanguages) { ++void Navigator::GetAcceptLanguages(const nsString* aLanguageOverride, nsTArray& aLanguages) { + MOZ_ASSERT(NS_IsMainThread()); + + aLanguages.Clear(); + + // E.g. "de-de, en-us,en". + nsAutoString acceptLang; +- Preferences::GetLocalizedString("intl.accept_languages", acceptLang); ++ if (aLanguageOverride && aLanguageOverride->Length()) ++ acceptLang = *aLanguageOverride; ++ else ++ Preferences::GetLocalizedString("intl.accept_languages", acceptLang); ++ + + // Split values on commas. + for (nsDependentSubstring lang : +@@ -385,7 +389,9 @@ void Navigator::GetLanguage(nsAString& aLanguage) { + } + + void Navigator::GetLanguages(nsTArray& aLanguages) { +- GetAcceptLanguages(aLanguages); ++ nsString languageOverride; ++ mWindow->GetDocShell()->GetLanguageOverride(languageOverride); ++ GetAcceptLanguages(&languageOverride, aLanguages); + + // The returned value is cached by the binding code. The window listens to the + // accept languages change and will clear the cache when needed. It has to +@@ -549,7 +555,13 @@ bool Navigator::CookieEnabled() { + return granted; + } + +-bool Navigator::OnLine() { return !NS_IsOffline(); } ++bool Navigator::OnLine() { ++ nsDocShell* docShell = static_cast(GetDocShell()); ++ nsIDocShell::OnlineOverride onlineOverride; ++ if (!docShell || docShell->GetOnlineOverride(&onlineOverride) != NS_OK || onlineOverride == nsIDocShell::ONLINE_OVERRIDE_NONE) ++ return !NS_IsOffline(); ++ return onlineOverride == nsIDocShell::ONLINE_OVERRIDE_ONLINE; ++} + + void Navigator::GetBuildID(nsAString& aBuildID, CallerType aCallerType, + ErrorResult& aRv) const { +diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h +index 99be251bf05a0252d624c6b0e216e5fa9be58260..cca8719a07ce51aa386ac45c72b01711423ec740 100644 +--- a/dom/base/Navigator.h ++++ b/dom/base/Navigator.h +@@ -221,7 +221,7 @@ class Navigator final : public nsISupports, public nsWrapperCache { + + StorageManager* Storage(); + +- static void GetAcceptLanguages(nsTArray& aLanguages); ++ static void GetAcceptLanguages(const nsString* aLanguageOverride, nsTArray& aLanguages); + + dom::MediaCapabilities* MediaCapabilities(); + dom::MediaSession* MediaSession(); +diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp +index 0b0893e9d363e43761ca7dea849286904a852837..9d8fa6e7f688cb0728797dcbd84996e2a7d45958 100644 +--- a/dom/base/nsContentUtils.cpp ++++ b/dom/base/nsContentUtils.cpp +@@ -8078,7 +8078,8 @@ nsresult nsContentUtils::SendMouseEvent( + bool aIgnoreRootScrollFrame, float aPressure, + unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow, + bool* aPreventDefault, bool aIsDOMEventSynthesized, +- bool aIsWidgetEventSynthesized) { ++ bool aIsWidgetEventSynthesized, ++ bool convertToPointer) { + nsPoint offset; + nsCOMPtr widget = GetWidget(aPresShell, &offset); + if (!widget) return NS_ERROR_FAILURE; +@@ -8135,6 +8136,7 @@ nsresult nsContentUtils::SendMouseEvent( + event.mTime = PR_IntervalNow(); + event.mFlags.mIsSynthesizedForTests = aIsDOMEventSynthesized; + event.mExitFrom = exitFrom; ++ event.convertToPointer = convertToPointer; + + nsPresContext* presContext = aPresShell->GetPresContext(); + if (!presContext) return NS_ERROR_FAILURE; +diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h +index 0826877af91df92b966559c856454305f7176695..aed20ad4ece699c82a96a5e7d63311af90255175 100644 +--- a/dom/base/nsContentUtils.h ++++ b/dom/base/nsContentUtils.h +@@ -2905,7 +2905,7 @@ class nsContentUtils { + int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure, + unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow, + bool* aPreventDefault, bool aIsDOMEventSynthesized, +- bool aIsWidgetEventSynthesized); ++ bool aIsWidgetEventSynthesized, bool convertToPointer = true); + + static void FirePageShowEventForFrameLoaderSwap( + nsIDocShellTreeItem* aItem, +diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp +index cdb923ebc316b14ae6d6c0e3c5fa0c4e33bcea69..5b4f06a789a946c4173b943204ac2758044a758a 100644 +--- a/dom/base/nsDOMWindowUtils.cpp ++++ b/dom/base/nsDOMWindowUtils.cpp +@@ -687,7 +687,7 @@ nsDOMWindowUtils::SendMouseEvent( + int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, + float aPressure, unsigned short aInputSourceArg, + bool aIsDOMEventSynthesized, bool aIsWidgetEventSynthesized, +- int32_t aButtons, uint32_t aIdentifier, uint8_t aOptionalArgCount, ++ int32_t aButtons, uint32_t aIdentifier, bool aDisablePointerEvent, uint8_t aOptionalArgCount, + bool* aPreventDefault) { + return SendMouseEventCommon( + aType, aX, aY, aButton, aClickCount, aModifiers, aIgnoreRootScrollFrame, +@@ -695,7 +695,7 @@ nsDOMWindowUtils::SendMouseEvent( + aOptionalArgCount >= 7 ? aIdentifier : DEFAULT_MOUSE_POINTER_ID, false, + aPreventDefault, aOptionalArgCount >= 4 ? aIsDOMEventSynthesized : true, + aOptionalArgCount >= 5 ? aIsWidgetEventSynthesized : false, +- aOptionalArgCount >= 6 ? aButtons : MOUSE_BUTTONS_NOT_SPECIFIED); ++ aOptionalArgCount >= 6 ? aButtons : MOUSE_BUTTONS_NOT_SPECIFIED, !aDisablePointerEvent); + } + + NS_IMETHODIMP +@@ -722,12 +722,12 @@ nsDOMWindowUtils::SendMouseEventCommon( + int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, + float aPressure, unsigned short aInputSourceArg, uint32_t aPointerId, + bool aToWindow, bool* aPreventDefault, bool aIsDOMEventSynthesized, +- bool aIsWidgetEventSynthesized, int32_t aButtons) { ++ bool aIsWidgetEventSynthesized, int32_t aButtons, bool aConvertToPointer) { + RefPtr presShell = GetPresShell(); + return nsContentUtils::SendMouseEvent( + presShell, aType, aX, aY, aButton, aButtons, aClickCount, aModifiers, + aIgnoreRootScrollFrame, aPressure, aInputSourceArg, aPointerId, aToWindow, +- aPreventDefault, aIsDOMEventSynthesized, aIsWidgetEventSynthesized); ++ aPreventDefault, aIsDOMEventSynthesized, aIsWidgetEventSynthesized, aConvertToPointer); + } + + NS_IMETHODIMP +diff --git a/dom/base/nsDOMWindowUtils.h b/dom/base/nsDOMWindowUtils.h +index 08e81b1c24a17729ec7b6c9e048c2febe57e18dc..cb09fe30de0a42c89da220e3bf8afe5f05923084 100644 +--- a/dom/base/nsDOMWindowUtils.h ++++ b/dom/base/nsDOMWindowUtils.h +@@ -93,7 +93,7 @@ class nsDOMWindowUtils final : public nsIDOMWindowUtils, + int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame, + float aPressure, unsigned short aInputSourceArg, uint32_t aIdentifier, + bool aToWindow, bool* aPreventDefault, bool aIsDOMEventSynthesized, +- bool aIsWidgetEventSynthesized, int32_t aButtons); ++ bool aIsWidgetEventSynthesized, int32_t aButtons, bool aConvertToPointer = true); + + MOZ_CAN_RUN_SCRIPT + nsresult SendTouchEventCommon( +diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp +index b3b54771d09b4ac6af4169f0eac4fc07d5136eb7..beaaf74c915dbd4a17c018fcdb45f2c0cdcc076a 100644 +--- a/dom/base/nsFocusManager.cpp ++++ b/dom/base/nsFocusManager.cpp +@@ -1538,6 +1538,10 @@ void nsFocusManager::SetFocusInner(Element* aNewContent, int32_t aFlags, + (GetActiveBrowsingContext() == newRootBrowsingContext); + } + ++ // In Playwright, we want to send focus events even if the element ++ // isn't actually in the active window. ++ isElementInActiveWindow = true; ++ + // Exit fullscreen if a website focuses another window + if (StaticPrefs::full_screen_api_exit_on_windowRaise() && + !isElementInActiveWindow && +@@ -2768,7 +2772,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow, + } + } + +- if (sTestMode) { ++ // In Playwright, we still want to execte the embedder functions ++ // to actually show / focus windows. ++ if (false && sTestMode) { + // In test mode, emulate raising the window. WindowRaised takes + // care of lowering the present active window. This happens in + // a separate runnable to avoid touching multiple windows in +diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp +index 752c067adb8461df787a67fc9c3a4aeed927d3e0..bc7077973f5958f778df5d5e31f8f81d2fe492bb 100644 +--- a/dom/base/nsGlobalWindowOuter.cpp ++++ b/dom/base/nsGlobalWindowOuter.cpp +@@ -2461,7 +2461,7 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, + &nsGlobalWindowInner::FireOnNewGlobalObject)); + } + +- if (newInnerWindow && !newInnerWindow->mHasNotifiedGlobalCreated && mDoc) { ++ if (newInnerWindow && mDoc) { + // We should probably notify. However if this is the, arguably bad, + // situation when we're creating a temporary non-chrome-about-blank + // document in a chrome docshell, don't notify just yet. Instead wait +@@ -2480,10 +2480,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, + }(); + + if (!isContentAboutBlankInChromeDocshell) { +- newInnerWindow->mHasNotifiedGlobalCreated = true; +- nsContentUtils::AddScriptRunner(NewRunnableMethod( +- "nsGlobalWindowOuter::DispatchDOMWindowCreated", this, +- &nsGlobalWindowOuter::DispatchDOMWindowCreated)); ++ if (!newInnerWindow->mHasNotifiedGlobalCreated) { ++ newInnerWindow->mHasNotifiedGlobalCreated = true; ++ nsContentUtils::AddScriptRunner(NewRunnableMethod( ++ "nsGlobalWindowOuter::DispatchDOMWindowCreated", this, ++ &nsGlobalWindowOuter::DispatchDOMWindowCreated)); ++ } else if (!reUseInnerWindow) { ++ nsContentUtils::AddScriptRunner(NewRunnableMethod( ++ "nsGlobalWindowOuter::JugglerDispatchDOMWindowReused", this, ++ &nsGlobalWindowOuter::JugglerDispatchDOMWindowReused)); ++ } + } + } + +@@ -2607,6 +2613,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() { + } + } + ++void nsGlobalWindowOuter::JugglerDispatchDOMWindowReused() { ++ nsCOMPtr observerService = ++ mozilla::services::GetObserverService(); ++ if (observerService && mDoc) { ++ nsIPrincipal* principal = mDoc->NodePrincipal(); ++ if (!principal->IsSystemPrincipal()) { ++ observerService->NotifyObservers(static_cast(this), ++ "juggler-dom-window-reused", ++ nullptr); ++ } ++ } ++} ++ + void nsGlobalWindowOuter::ClearStatus() { SetStatusOuter(u""_ns); } + + void nsGlobalWindowOuter::SetDocShell(nsDocShell* aDocShell) { +@@ -3841,6 +3860,14 @@ Maybe nsGlobalWindowOuter::GetRDMDeviceSize( + } + } + } ++ if (topInProcessContentDoc) { ++ nsIDocShell* docShell = topInProcessContentDoc->GetDocShell(); ++ if (docShell && docShell->GetDeviceSizeIsPageSize()) { ++ nsPresContext* presContext = docShell->GetPresContext(); ++ if (presContext) ++ return Some(CSSPixel::FromAppUnitsRounded(presContext->GetVisibleArea().Size())); ++ } ++ } + return Nothing(); + } + +diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h +index 44163e0faaff91d4a6fbc32ff8fa9ffbf16e118c..a0bc652f650f5e05615ae9fabf2ccb8eaa949fa7 100644 +--- a/dom/base/nsGlobalWindowOuter.h ++++ b/dom/base/nsGlobalWindowOuter.h +@@ -324,6 +324,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, + + // Outer windows only. + void DispatchDOMWindowCreated(); ++ void JugglerDispatchDOMWindowReused(); + + // Outer windows only. + virtual void EnsureSizeAndPositionUpToDate() override; +diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp +index 9c939cae4dadc45136a115370ec22376af5edc0d..2da7f0eba192e4c8f91c792bec14ccdd429deed9 100644 +--- a/dom/base/nsINode.cpp ++++ b/dom/base/nsINode.cpp +@@ -1290,6 +1290,48 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions, + mozilla::GetBoxQuadsFromWindowOrigin(this, aOptions, aResult, aRv); + } + ++void nsINode::ScrollRectIntoViewIfNeeded(int32_t x, int32_t y, ++ int32_t w, int32_t h, ++ ErrorResult& aRv) { ++ aRv = NS_ERROR_UNEXPECTED; ++ nsCOMPtr document = OwnerDoc(); ++ if (!document) { ++ return aRv.ThrowNotFoundError("Node is detached from document"); ++ } ++ PresShell* presShell = document->GetPresShell(); ++ if (!presShell) { ++ return aRv.ThrowNotFoundError("Node is detached from document"); ++ } ++ if (!IsContent()) { ++ return aRv.ThrowNotFoundError("Node does not have a layout object"); ++ } ++ aRv = NS_OK; ++ nsIFrame* primaryFrame = AsContent()->GetPrimaryFrame(FlushType::Frames); ++ if (!primaryFrame) { ++ return aRv.ThrowNotFoundError("Node does not have a layout object"); ++ } ++ nsRect rect; ++ if (x == -1 && y == -1 && w == -1 && h == -1) { ++ rect = primaryFrame->GetRectRelativeToSelf(); ++ } else { ++ rect = nsRect(nsPresContext::CSSPixelsToAppUnits(x), ++ nsPresContext::CSSPixelsToAppUnits(y), ++ nsPresContext::CSSPixelsToAppUnits(w), ++ nsPresContext::CSSPixelsToAppUnits(h)); ++ } ++ presShell->ScrollFrameRectIntoView( ++ primaryFrame, rect, ++ ScrollAxis(kScrollToCenter, WhenToScroll::Always), ++ ScrollAxis(kScrollToCenter, WhenToScroll::Always), ++ ScrollFlags::ScrollOverflowHidden); ++ // If a _visual_ scroll update is pending, cancel it; otherwise, it will ++ // clobber next scroll (e.g. subsequent window.scrollTo(0, 0) wlll break). ++ if (presShell->GetPendingVisualScrollUpdate()) { ++ presShell->AcknowledgePendingVisualScrollUpdate(); ++ presShell->ClearPendingVisualScrollUpdate(); ++ } ++} ++ + already_AddRefed nsINode::ConvertQuadFromNode( + DOMQuad& aQuad, const GeometryNode& aFrom, + const ConvertCoordinateOptions& aOptions, CallerType aCallerType, +diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h +index 7ba2a9b84d51a2d72eca9624a102554a4ac60260..10644adced58c07e52f8650e35fa49383ec1309c 100644 +--- a/dom/base/nsINode.h ++++ b/dom/base/nsINode.h +@@ -2059,6 +2059,10 @@ class nsINode : public mozilla::dom::EventTarget { + nsTArray>& aResult, + ErrorResult& aRv); + ++ void ScrollRectIntoViewIfNeeded(int32_t x, int32_t y, ++ int32_t w, int32_t h, ++ ErrorResult& aRv); ++ + already_AddRefed ConvertQuadFromNode( + DOMQuad& aQuad, const TextOrElementOrDocument& aFrom, + const ConvertCoordinateOptions& aOptions, CallerType aCallerType, +diff --git a/dom/base/nsJSUtils.cpp b/dom/base/nsJSUtils.cpp +index fb8b8d6db80efb508c1febe5b62f339bbfba8d90..92fd18692f5c2f2cd7c12f35551abe925fc2f657 100644 +--- a/dom/base/nsJSUtils.cpp ++++ b/dom/base/nsJSUtils.cpp +@@ -219,6 +219,11 @@ bool nsJSUtils::GetScopeChainForElement( + return true; + } + ++/* static */ ++bool nsJSUtils::SetTimeZoneOverride(const char* timezoneId) { ++ return JS::SetTimeZoneOverride(timezoneId); ++} ++ + /* static */ + void nsJSUtils::ResetTimeZone() { JS::ResetTimeZone(); } + +diff --git a/dom/base/nsJSUtils.h b/dom/base/nsJSUtils.h +index b53d2f740b5901e4422fa3af84198dc687c535d9..052d15cccb021540cc9c7dfe5a9337d9cd43661b 100644 +--- a/dom/base/nsJSUtils.h ++++ b/dom/base/nsJSUtils.h +@@ -102,6 +102,7 @@ class nsJSUtils { + JSContext* aCx, mozilla::dom::Element* aElement, + JS::MutableHandleVector aScopeChain); + ++ static bool SetTimeZoneOverride(const char* timezoneId); + static void ResetTimeZone(); + + static bool DumpEnabled(); +diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp +index 763192a50cf8dce36eaf1fd2b470c631eecfe65c..436f01a2ca6166f1a97139f1cda7e1832b9df36c 100644 +--- a/dom/geolocation/Geolocation.cpp ++++ b/dom/geolocation/Geolocation.cpp +@@ -23,6 +23,7 @@ + #include "nsComponentManagerUtils.h" + #include "nsContentPermissionHelper.h" + #include "nsContentUtils.h" ++#include "nsDocShell.h" + #include "nsGlobalWindow.h" + #include "mozilla/dom/Document.h" + #include "nsINamed.h" +@@ -254,10 +255,8 @@ nsGeolocationRequest::Allow(JS::HandleValue aChoices) { + return NS_OK; + } + +- RefPtr gs = +- nsGeolocationService::GetGeolocationService(); +- +- bool canUseCache = false; ++ nsGeolocationService* gs = mLocator->GetGeolocationService(); ++ bool canUseCache = gs != nsGeolocationService::sService.get(); + CachedPositionAndAccuracy lastPosition = gs->GetCachedPosition(); + if (lastPosition.position) { + DOMTimeStamp cachedPositionTime_ms; +@@ -431,8 +430,7 @@ void nsGeolocationRequest::Shutdown() { + // If there are no other high accuracy requests, the geolocation service will + // notify the provider to switch to the default accuracy. + if (mOptions && mOptions->mEnableHighAccuracy) { +- RefPtr gs = +- nsGeolocationService::GetGeolocationService(); ++ nsGeolocationService* gs = mLocator ? mLocator->GetGeolocationService() : nullptr; + if (gs) { + gs->UpdateAccuracy(); + } +@@ -708,8 +706,14 @@ void nsGeolocationService::StopDevice() { + StaticRefPtr nsGeolocationService::sService; + + already_AddRefed +-nsGeolocationService::GetGeolocationService() { ++nsGeolocationService::GetGeolocationService(nsDocShell* docShell) { + RefPtr result; ++ if (docShell) { ++ result = docShell->GetGeolocationServiceOverride(); ++ if (result) ++ return result.forget(); ++ } ++ + if (nsGeolocationService::sService) { + result = nsGeolocationService::sService; + +@@ -801,7 +805,9 @@ nsresult Geolocation::Init(nsPIDOMWindowInner* aContentDom) { + // If no aContentDom was passed into us, we are being used + // by chrome/c++ and have no mOwner, no mPrincipal, and no need + // to prompt. +- mService = nsGeolocationService::GetGeolocationService(); ++ nsCOMPtr doc = aContentDom ? aContentDom->GetDoc() : nullptr; ++ mService = nsGeolocationService::GetGeolocationService( ++ doc ? static_cast(doc->GetDocShell()) : nullptr); + if (mService) { + mService->AddLocator(this); + } +diff --git a/dom/geolocation/Geolocation.h b/dom/geolocation/Geolocation.h +index d92bd1c738016f93c66dbdc449c70937c37b6f9a..16fb91cc37b5ce2a8522c56e61e5aed89033a0b8 100644 +--- a/dom/geolocation/Geolocation.h ++++ b/dom/geolocation/Geolocation.h +@@ -31,6 +31,7 @@ + + #include "nsIGeolocationProvider.h" + #include "mozilla/Attributes.h" ++#include "nsDocShell.h" + + class nsGeolocationService; + class nsGeolocationRequest; +@@ -38,6 +39,7 @@ class nsGeolocationRequest; + namespace mozilla { + namespace dom { + class Geolocation; ++ + typedef CallbackObjectHolder + GeoPositionCallback; + typedef CallbackObjectHolder GetGeolocationService(); ++ static already_AddRefed GetGeolocationService(nsDocShell* docShell = nullptr); + static mozilla::StaticRefPtr sService; + + NS_DECL_THREADSAFE_ISUPPORTS +@@ -182,6 +185,8 @@ class Geolocation final : public nsIGeolocationUpdate, public nsWrapperCache { + // null. + static already_AddRefed NonWindowSingleton(); + ++ nsGeolocationService* GetGeolocationService() { return mService; }; ++ + private: + ~Geolocation(); + +diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp +index 8257892ad1763a15936e18adbda304d001f58316..01543dd7ab6eeead5f46dec38d39e123f0d8cd8f 100644 +--- a/dom/html/HTMLInputElement.cpp ++++ b/dom/html/HTMLInputElement.cpp +@@ -52,6 +52,7 @@ + #include "nsMappedAttributes.h" + #include "nsIFormControl.h" + #include "mozilla/dom/Document.h" ++#include "nsDocShell.h" + #include "nsIFormControlFrame.h" + #include "nsITextControlFrame.h" + #include "nsIFrame.h" +@@ -739,6 +740,12 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { + return NS_ERROR_FAILURE; + } + ++ nsDocShell* docShell = static_cast(win->GetDocShell()); ++ if (docShell && docShell->IsFileInputInterceptionEnabled()) { ++ docShell->FilePickerShown(this); ++ return NS_OK; ++ } ++ + if (IsPopupBlocked(doc)) { + return NS_OK; + } +diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl +index f115889acabb06ab41fef26ad6ce9adc3b819275..4a51023178aa934026a306b3a1d197031c8363d7 100644 +--- a/dom/interfaces/base/nsIDOMWindowUtils.idl ++++ b/dom/interfaces/base/nsIDOMWindowUtils.idl +@@ -353,7 +353,8 @@ interface nsIDOMWindowUtils : nsISupports { + [optional] in boolean aIsDOMEventSynthesized, + [optional] in boolean aIsWidgetEventSynthesized, + [optional] in long aButtons, +- [optional] in unsigned long aIdentifier); ++ [optional] in unsigned long aIdentifier, ++ [optional] in boolean aDisablePointerEvent); + + /** Synthesize a touch event. The event types supported are: + * touchstart, touchend, touchmove, and touchcancel +diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp +index 30642165f95fad28396058be5bbad8807e6613cf..6b440732b16e2a4241c1b7e66fa6f32ec77fa755 100644 +--- a/dom/ipc/BrowserChild.cpp ++++ b/dom/ipc/BrowserChild.cpp +@@ -3587,6 +3587,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress, + return NS_OK; + } + ++NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ + NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, +diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.cc b/dom/media/systemservices/video_engine/desktop_capture_impl.cc +index 7bc92fe4408c2878c9d7c8bdb97a7c257258ee31..8885feebedf53c0748cef19d80ce5aa23adc900b 100644 +--- a/dom/media/systemservices/video_engine/desktop_capture_impl.cc ++++ b/dom/media/systemservices/video_engine/desktop_capture_impl.cc +@@ -125,8 +125,9 @@ int32_t ScreenDeviceInfoImpl::GetOrientation(const char* deviceUniqueIdUTF8, + + VideoCaptureModule* DesktopCaptureImpl::Create(const int32_t id, + const char* uniqueId, +- const CaptureDeviceType type) { +- return new rtc::RefCountedObject(id, uniqueId, type); ++ const CaptureDeviceType type, ++ bool captureCursor) { ++ return new rtc::RefCountedObject(id, uniqueId, type, captureCursor); + } + + int32_t WindowDeviceInfoImpl::Init() { +@@ -360,12 +361,16 @@ int32_t DesktopCaptureImpl::Init() { + DesktopCapturer::SourceId sourceId = atoi(_deviceUniqueId.c_str()); + pWindowCapturer->SelectSource(sourceId); + +- MouseCursorMonitor* pMouseCursorMonitor = +- MouseCursorMonitor::CreateForWindow( +- webrtc::DesktopCaptureOptions::CreateDefault(), sourceId); +- desktop_capturer_cursor_composer_ = +- std::unique_ptr(new DesktopAndCursorComposer( +- pWindowCapturer.release(), pMouseCursorMonitor)); ++ if (capture_cursor_) { ++ MouseCursorMonitor* pMouseCursorMonitor = ++ MouseCursorMonitor::CreateForWindow( ++ webrtc::DesktopCaptureOptions::CreateDefault(), sourceId); ++ desktop_capturer_cursor_composer_ = ++ std::unique_ptr(new DesktopAndCursorComposer( ++ pWindowCapturer.release(), pMouseCursorMonitor)); ++ } else { ++ desktop_capturer_cursor_composer_ = std::move(pWindowCapturer); ++ } + } else if (_deviceType == CaptureDeviceType::Browser) { + // XXX We don't capture cursors, so avoid the extra indirection layer. We + // could also pass null for the pMouseCursorMonitor. +@@ -382,7 +387,8 @@ int32_t DesktopCaptureImpl::Init() { + } + + DesktopCaptureImpl::DesktopCaptureImpl(const int32_t id, const char* uniqueId, +- const CaptureDeviceType type) ++ const CaptureDeviceType type, ++ bool captureCursor) + : _id(id), + _deviceUniqueId(uniqueId), + _deviceType(type), +@@ -393,6 +399,7 @@ DesktopCaptureImpl::DesktopCaptureImpl(const int32_t id, const char* uniqueId, + delta_ntp_internal_ms_( + Clock::GetRealTimeClock()->CurrentNtpInMilliseconds() - + last_capture_time_), ++ capture_cursor_(captureCursor), + time_event_(EventWrapper::Create()), + #if defined(_WIN32) + capturer_thread_( +diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.h b/dom/media/systemservices/video_engine/desktop_capture_impl.h +index 75995564e5438261a2886840ecad32d2f1d7663f..dfdabcedcda4e212ed0ffd7bc4def57079218413 100644 +--- a/dom/media/systemservices/video_engine/desktop_capture_impl.h ++++ b/dom/media/systemservices/video_engine/desktop_capture_impl.h +@@ -159,7 +159,8 @@ class DesktopCaptureImpl : public DesktopCapturer::Callback, + /* Create a screen capture modules object + */ + static VideoCaptureModule* Create(const int32_t id, const char* uniqueId, +- const CaptureDeviceType type); ++ const CaptureDeviceType type, ++ bool captureCursor = true); + static VideoCaptureModule::DeviceInfo* CreateDeviceInfo( + const int32_t id, const CaptureDeviceType type); + +@@ -191,7 +192,7 @@ class DesktopCaptureImpl : public DesktopCapturer::Callback, + + protected: + DesktopCaptureImpl(const int32_t id, const char* uniqueId, +- const CaptureDeviceType type); ++ const CaptureDeviceType type, bool captureCursor); + virtual ~DesktopCaptureImpl(); + int32_t DeliverCapturedFrame(webrtc::VideoFrame& captureFrame, + int64_t capture_time); +@@ -239,6 +240,7 @@ class DesktopCaptureImpl : public DesktopCapturer::Callback, + void process(); + + private: ++ bool capture_cursor_ = true; + // This is created on the main thread and accessed on both the main thread + // and the capturer thread. It is created prior to the capturer thread + // starting and is destroyed after it is stopped. +diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp +index 589b46999c7f917c55e9e5e042f45a01cca7e9eb..128eb07822da31d1f6040b2505247c71fc9353b0 100644 +--- a/dom/script/ScriptSettings.cpp ++++ b/dom/script/ScriptSettings.cpp +@@ -195,6 +195,30 @@ ScriptSettingsStackEntry::~ScriptSettingsStackEntry() { + MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal()); + } + ++static nsIGlobalObject* UnwrapSandboxGlobal(nsIGlobalObject* global) { ++ if (!global) ++ return global; ++ JSObject* globalObject = global->GetGlobalJSObject(); ++ if (!globalObject) ++ return global; ++ JSContext* cx = nsContentUtils::GetCurrentJSContext(); ++ if (!cx) ++ return global; ++ JS::Rooted proto(cx); ++ JS::RootedObject rootedGlobal(cx, globalObject); ++ if (!JS_GetPrototype(cx, rootedGlobal, &proto)) ++ return global; ++ if (!proto || !xpc::IsSandboxPrototypeProxy(proto)) ++ return global; ++ // If this is a sandbox associated with a DOMWindow via a ++ // sandboxPrototype, use that DOMWindow. This supports GreaseMonkey ++ // and JetPack content scripts. ++ proto = js::CheckedUnwrapDynamic(proto, cx, /* stopAtWindowProxy = */ false); ++ if (!proto) ++ return global; ++ return xpc::WindowGlobalOrNull(proto); ++} ++ + // If the entry or incumbent global ends up being something that the subject + // principal doesn't subsume, we don't want to use it. This never happens on + // the web, but can happen with asymmetric privilege relationships (i.e. +@@ -222,7 +246,7 @@ static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) { + NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal()); + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller() + ->SubsumesConsideringDomain(globalPrin)) { +- return GetCurrentGlobal(); ++ return UnwrapSandboxGlobal(GetCurrentGlobal()); + } + + return aGlobalOrNull; +diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp +index 73f07786cff86af1ee5252eacbbc1464197fdac8..d93ae3d61f5ffc083bb080467153c06be27093a1 100644 +--- a/dom/security/nsCSPUtils.cpp ++++ b/dom/security/nsCSPUtils.cpp +@@ -127,6 +127,11 @@ void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc, + return; + } + ++ if (aDoc.GetDocShell() && ++ nsDocShell::Cast(aDoc.GetDocShell())->IsBypassCSPEnabled()) { ++ return; ++ } ++ + nsAutoString policyStr( + nsContentUtils::TrimWhitespace( + aPolicyStr)); +diff --git a/dom/webidl/GeometryUtils.webidl b/dom/webidl/GeometryUtils.webidl +index 2f71b284ee5f7e11f117c447834b48355784448c..d996e0a3cbbb19c1dc320c305c6d74037bffa0d3 100644 +--- a/dom/webidl/GeometryUtils.webidl ++++ b/dom/webidl/GeometryUtils.webidl +@@ -27,6 +27,9 @@ interface mixin GeometryUtils { + [Throws, Func="nsINode::HasBoxQuadsSupport", NeedsCallerType] + sequence getBoxQuads(optional BoxQuadOptions options = {}); + ++ [ChromeOnly, Throws, Func="nsINode::HasBoxQuadsSupport"] ++ void scrollRectIntoViewIfNeeded(long x, long y, long w, long h); ++ + /* getBoxQuadsFromWindowOrigin is similar to getBoxQuads, but the + * returned quads are further translated relative to the window + * origin -- which is not the layout origin. Further translation +diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp +index e7df6ded13a38436942414d16161445e74d2a40a..fe4a0fb8f88ce81d87b3377d6ab49a3356edef72 100644 +--- a/dom/workers/RuntimeService.cpp ++++ b/dom/workers/RuntimeService.cpp +@@ -1009,7 +1009,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) { + AssertIsOnMainThread(); + + nsTArray languages; +- Navigator::GetAcceptLanguages(languages); ++ Navigator::GetAcceptLanguages(nullptr, languages); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { +@@ -1212,8 +1212,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) { + } + + // The navigator overridden properties should have already been read. +- +- Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages); ++ Navigator::GetAcceptLanguages(nullptr, mNavigatorProperties.mLanguages); + mNavigatorPropertiesLoaded = true; + } + +@@ -1932,6 +1931,13 @@ void RuntimeService::PropagateStorageAccessPermissionGranted( + } + } + ++void RuntimeService::ResetDefaultLocaleInAllWorkers() { ++ AssertIsOnMainThread(); ++ BroadcastAllWorkers([](auto& worker) { ++ worker.ResetDefaultLocale(); ++ }); ++} ++ + void RuntimeService::NoteIdleThread(SafeRefPtr aThread) { + AssertIsOnMainThread(); + MOZ_ASSERT(aThread); +@@ -2350,6 +2356,14 @@ void PropagateStorageAccessPermissionGrantedToWorkers( + } + } + ++void ResetDefaultLocaleInAllWorkers() { ++ AssertIsOnMainThread(); ++ RuntimeService* runtime = RuntimeService::GetService(); ++ if (runtime) { ++ runtime->ResetDefaultLocaleInAllWorkers(); ++ } ++} ++ + WorkerPrivate* GetWorkerPrivateFromContext(JSContext* aCx) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); +diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h +index 67eac10dabd93da344a1366dee5d1c4428a4352c..1c31651d8087c90983c08fe184b344853af87033 100644 +--- a/dom/workers/RuntimeService.h ++++ b/dom/workers/RuntimeService.h +@@ -121,6 +121,8 @@ class RuntimeService final : public nsIObserver { + void PropagateStorageAccessPermissionGranted( + const nsPIDOMWindowInner& aWindow); + ++ void ResetDefaultLocaleInAllWorkers(); ++ + const NavigatorProperties& GetNavigatorProperties() const { + return mNavigatorProperties; + } +diff --git a/dom/workers/WorkerCommon.h b/dom/workers/WorkerCommon.h +index 8b1b46d69f2c90d851d292c285a1ba9bdbd4d9b7..dea5259b0a82e5e6d3c431fc78e60d5df80b3eda 100644 +--- a/dom/workers/WorkerCommon.h ++++ b/dom/workers/WorkerCommon.h +@@ -45,6 +45,8 @@ void ResumeWorkersForWindow(const nsPIDOMWindowInner& aWindow); + void PropagateStorageAccessPermissionGrantedToWorkers( + const nsPIDOMWindowInner& aWindow); + ++void ResetDefaultLocaleInAllWorkers(); ++ + // All of these are implemented in WorkerScope.cpp + + bool IsWorkerGlobal(JSObject* global); +diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp +index 658542ba0b1f676622c76b683acb31af7ffcad2b..f4602d4d5b64f2cff7a2ade322f421c38ff3d22c 100644 +--- a/dom/workers/WorkerPrivate.cpp ++++ b/dom/workers/WorkerPrivate.cpp +@@ -660,6 +660,18 @@ class UpdateContextOptionsRunnable final : public WorkerControlRunnable { + } + }; + ++class ResetDefaultLocaleRunnable final : public WorkerControlRunnable { ++ public: ++ explicit ResetDefaultLocaleRunnable(WorkerPrivate* aWorkerPrivate) ++ : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) {} ++ ++ virtual bool WorkerRun(JSContext* aCx, ++ WorkerPrivate* aWorkerPrivate) override { ++ aWorkerPrivate->ResetDefaultLocaleInternal(aCx); ++ return true; ++ } ++}; ++ + class UpdateLanguagesRunnable final : public WorkerRunnable { + nsTArray mLanguages; + +@@ -1840,6 +1852,16 @@ void WorkerPrivate::UpdateContextOptions( + } + } + ++void WorkerPrivate::ResetDefaultLocale() { ++ AssertIsOnParentThread(); ++ ++ RefPtr runnable = ++ new ResetDefaultLocaleRunnable(this); ++ if (!runnable->Dispatch()) { ++ NS_WARNING("Failed to reset default locale in worker!"); ++ } ++} ++ + void WorkerPrivate::UpdateLanguages(const nsTArray& aLanguages) { + AssertIsOnParentThread(); + +@@ -4810,6 +4832,15 @@ void WorkerPrivate::UpdateContextOptionsInternal( + } + } + ++void WorkerPrivate::ResetDefaultLocaleInternal(JSContext* aCx) { ++ JS_ResetDefaultLocale(JS_GetRuntime(aCx)); ++ auto data = mWorkerThreadAccessible.Access(); ++ ++ for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) { ++ data->mChildWorkers[index]->ResetDefaultLocale(); ++ } ++} ++ + void WorkerPrivate::UpdateLanguagesInternal( + const nsTArray& aLanguages) { + WorkerGlobalScope* globalScope = GlobalScope(); +diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h +index 49255ca854d9082186fb8055f0a40d7f57e6d6e2..94dc99e306c823ee5d515c469f289c0d1c6dff02 100644 +--- a/dom/workers/WorkerPrivate.h ++++ b/dom/workers/WorkerPrivate.h +@@ -299,6 +299,8 @@ class WorkerPrivate : public RelativeTimeline { + void UpdateContextOptionsInternal(JSContext* aCx, + const JS::ContextOptions& aContextOptions); + ++ void ResetDefaultLocaleInternal(JSContext* aCx); ++ + void UpdateLanguagesInternal(const nsTArray& aLanguages); + + void UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key, +@@ -888,6 +890,8 @@ class WorkerPrivate : public RelativeTimeline { + + void UpdateContextOptions(const JS::ContextOptions& aContextOptions); + ++ void ResetDefaultLocale(); ++ + void UpdateLanguages(const nsTArray& aLanguages); + + void UpdateJSWorkerMemoryParameter(JSGCParamKey key, Maybe value); +diff --git a/js/public/Date.h b/js/public/Date.h +index e7a54d86c44499a3ec2adf1c156b9f9dfb0bc6b4..f56c1b419c4cb52bc371f6b8dbfffba464326fc4 100644 +--- a/js/public/Date.h ++++ b/js/public/Date.h +@@ -56,6 +56,8 @@ namespace JS { + */ + extern JS_PUBLIC_API void ResetTimeZone(); + ++extern JS_PUBLIC_API bool SetTimeZoneOverride(const char* timezoneId); ++ + class ClippedTime; + inline ClippedTime TimeClip(double time); + +diff --git a/js/src/debugger/Object.cpp b/js/src/debugger/Object.cpp +index f18d56852366daaaba853568b74b2b82b4f8ee5d..b2ca28ec8abc3f55694f5409c1a11ecdbc71e132 100644 +--- a/js/src/debugger/Object.cpp ++++ b/js/src/debugger/Object.cpp +@@ -2362,7 +2362,11 @@ Maybe DebuggerObject::call(JSContext* cx, + invokeArgs[i].set(args2[i]); + } + ++ // Disable CSP for the scope of the call. ++ const JSSecurityCallbacks* securityCallbacks = JS_GetSecurityCallbacks(cx); ++ JS_SetSecurityCallbacks(cx, nullptr); + ok = js::Call(cx, calleev, thisv, invokeArgs, &result); ++ JS_SetSecurityCallbacks(cx, securityCallbacks); + } + } + +diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp +index a587c912b36f2a142aef7ed03e245636f8a0100d..95864acc2d9fc4ef9e1ad2bb7a2b97642ada1a22 100644 +--- a/js/src/vm/DateTime.cpp ++++ b/js/src/vm/DateTime.cpp +@@ -169,6 +169,11 @@ void js::DateTimeInfo::internalResetTimeZone(ResetTimeZoneMode mode) { + } + } + ++void js::DateTimeInfo::internalSetTimeZoneOverride(mozilla::UniquePtr timeZone) { ++ timeZoneOverride_ = std::move(timeZone); ++ internalResetTimeZone(ResetTimeZoneMode::ResetEvenIfOffsetUnchanged); ++} ++ + void js::DateTimeInfo::updateTimeZone() { + MOZ_ASSERT(timeZoneStatus_ != TimeZoneStatus::Valid); + +@@ -529,10 +534,27 @@ void js::ResetTimeZoneInternal(ResetTimeZoneMode mode) { + js::DateTimeInfo::resetTimeZone(mode); + } + ++void js::SetTimeZoneOverrideInternal(mozilla::UniquePtr timeZone) { ++ auto guard = js::DateTimeInfo::instance->lock(); ++ guard->internalSetTimeZoneOverride(std::move(timeZone)); ++} ++ + JS_PUBLIC_API void JS::ResetTimeZone() { + js::ResetTimeZoneInternal(js::ResetTimeZoneMode::ResetEvenIfOffsetUnchanged); + } + ++JS_PUBLIC_API bool JS::SetTimeZoneOverride(const char* timeZoneId) { ++ // Validate timezone id. ++ mozilla::UniquePtr timeZone(icu::TimeZone::createTimeZone( ++ icu::UnicodeString(timeZoneId, -1, US_INV))); ++ if (!timeZone || *timeZone == icu::TimeZone::getUnknown()) { ++ fprintf(stderr, "Invalid timezone id: %s\n", timeZoneId); ++ return false; ++ } ++ js::SetTimeZoneOverrideInternal(std::move(timeZone)); ++ return true; ++} ++ + #if defined(XP_WIN) + static bool IsOlsonCompatibleWindowsTimeZoneId(const char* tz) { + // ICU ignores the TZ environment variable on Windows and instead directly +@@ -733,6 +755,11 @@ void js::ResyncICUDefaultTimeZone() { + + void js::DateTimeInfo::internalResyncICUDefaultTimeZone() { + #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU ++ if (timeZoneOverride_) { ++ icu::TimeZone::setDefault(*timeZoneOverride_); ++ return; ++ } ++ + if (const char* tz = std::getenv("TZ")) { + icu::UnicodeString tzid; + +diff --git a/js/src/vm/DateTime.h b/js/src/vm/DateTime.h +index 77b4c4ea3581e3b66b0b40dae33c807b2d5aefd8..84af4461b9e946122527ac974dc30da5fd6b8818 100644 +--- a/js/src/vm/DateTime.h ++++ b/js/src/vm/DateTime.h +@@ -66,6 +66,8 @@ enum class ResetTimeZoneMode : bool { + */ + extern void ResetTimeZoneInternal(ResetTimeZoneMode mode); + ++extern void SetTimeZoneOverrideInternal(mozilla::UniquePtr timeZone); ++ + /** + * ICU's default time zone, used for various date/time formatting operations + * that include the local time in the representation, is allowed to go stale +@@ -205,6 +207,7 @@ class DateTimeInfo { + // and js::ResyncICUDefaultTimeZone(). + friend void js::ResetTimeZoneInternal(ResetTimeZoneMode); + friend void js::ResyncICUDefaultTimeZone(); ++ friend void js::SetTimeZoneOverrideInternal(mozilla::UniquePtr); + + static void resetTimeZone(ResetTimeZoneMode mode) { + auto guard = instance->lock(); +@@ -301,6 +304,8 @@ class DateTimeInfo { + JS::UniqueChars locale_; + JS::UniqueTwoByteChars standardName_; + JS::UniqueTwoByteChars daylightSavingsName_; ++ ++ mozilla::UniquePtr timeZoneOverride_; + #else + // Restrict the data-time range to the minimum required time_t range as + // specified in POSIX. Most operating systems support 64-bit time_t +@@ -316,6 +321,8 @@ class DateTimeInfo { + + void internalResetTimeZone(ResetTimeZoneMode mode); + ++ void internalSetTimeZoneOverride(mozilla::UniquePtr timeZone); ++ + void updateTimeZone(); + + void internalResyncICUDefaultTimeZone(); +diff --git a/netwerk/base/nsINetworkInterceptController.idl b/netwerk/base/nsINetworkInterceptController.idl +index 64a4a71b03b28872f376aac8eee12805bebd1bd8..f6fa7d731f3b0c7c4fcb26babad3fc2cdb29aec1 100644 +--- a/netwerk/base/nsINetworkInterceptController.idl ++++ b/netwerk/base/nsINetworkInterceptController.idl +@@ -56,6 +56,7 @@ interface nsIInterceptedChannel : nsISupports + * network request. + */ + void resetInterception(); ++ void resetInterceptionWithURI(in nsIURI aURI); + + /** + * Set the status and reason for the forthcoming synthesized response. +diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp +index 667990b9d411963f23734652e04c5b5aa5574eef..9408cd2c88443c1a0053b1d9d15146a51900e320 100644 +--- a/netwerk/protocol/http/InterceptedHttpChannel.cpp ++++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp +@@ -602,6 +602,14 @@ void InterceptedHttpChannel::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); + } + ++NS_IMETHODIMP ++InterceptedHttpChannel::ResetInterceptionWithURI(nsIURI* aURI) { ++ if (aURI) { ++ mURI = aURI; ++ } ++ return ResetInterception(); ++} ++ + NS_IMETHODIMP + InterceptedHttpChannel::ResetInterception(void) { + if (mCanceled) { +diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp +index f26b8b48dce213a955c28243a6ade09e3385955c..9574b2c26f202ecbb717f0c80272eda7d722e36f 100644 +--- a/parser/html/nsHtml5TreeOpExecutor.cpp ++++ b/parser/html/nsHtml5TreeOpExecutor.cpp +@@ -1244,9 +1244,12 @@ void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) { + if (!StaticPrefs::security_csp_enable()) { + return; + } +- + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + ++ if (mDocShell && static_cast(mDocShell.get())->IsBypassCSPEnabled()) { ++ return; ++ } ++ + nsresult rv = NS_OK; + nsCOMPtr preloadCsp = mDocument->GetPreloadCsp(); + if (!preloadCsp) { +diff --git a/security/manager/ssl/SSLServerCertVerification.cpp b/security/manager/ssl/SSLServerCertVerification.cpp +index 3c5512c95e9c1f6f06387238063a29c01f7aab64..357aaf8a0f45ee81864d25e2205d6276188ca81e 100644 +--- a/security/manager/ssl/SSLServerCertVerification.cpp ++++ b/security/manager/ssl/SSLServerCertVerification.cpp +@@ -977,8 +977,8 @@ PRErrorCode AuthCertificateParseResults( + return SEC_ERROR_NO_MEMORY; + } + nsresult rv = overrideService->HasMatchingOverride( +- aHostName, aPort, nssCert, &overrideBits, &isTemporaryOverride, +- &haveOverride); ++ aHostName, aPort, aOriginAttributes.mUserContextId, nssCert, ++ &overrideBits, &isTemporaryOverride, &haveOverride); + if (NS_SUCCEEDED(rv) && haveOverride) { + // remove the errors that are already overriden + remainingDisplayErrors &= ~overrideBits; +diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp +index 04fdc8b475dbf14d934bb4b8ffdf9dad2c32a1ab..04f3889910a2fc626da682d1d9c5e04d0d35cd32 100644 +--- a/security/manager/ssl/nsCertOverrideService.cpp ++++ b/security/manager/ssl/nsCertOverrideService.cpp +@@ -522,13 +522,20 @@ nsCertOverrideService::RememberTemporaryValidityOverrideUsingFingerprint( + + NS_IMETHODIMP + nsCertOverrideService::HasMatchingOverride(const nsACString& aHostName, +- int32_t aPort, nsIX509Cert* aCert, ++ int32_t aPort, ++ uint32_t aUserContextId, ++ nsIX509Cert* aCert, + uint32_t* aOverrideBits, + bool* aIsTemporary, bool* _retval) { + bool disableAllSecurityCheck = false; + { + MutexAutoLock lock(mMutex); +- disableAllSecurityCheck = mDisableAllSecurityCheck; ++ if (aUserContextId) { ++ disableAllSecurityCheck = mUserContextIdsWithDisabledSecurityChecks.has( ++ aUserContextId); ++ } else { ++ disableAllSecurityCheck = mDisableAllSecurityCheck; ++ } + } + if (disableAllSecurityCheck) { + nsCertOverride::OverrideBits all = nsCertOverride::OverrideBits::Untrusted | +@@ -740,12 +747,21 @@ static bool IsDebugger() { + + NS_IMETHODIMP + nsCertOverrideService:: +- SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) { +- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) { ++ SetDisableAllSecurityChecksAndLetAttackersInterceptMyData( ++ bool aDisable, uint32_t aUserContextId) { ++ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) { + return NS_ERROR_NOT_AVAILABLE; + } + + MutexAutoLock lock(mMutex); ++ if (aUserContextId) { ++ if (aDisable) { ++ mozilla::Unused << mUserContextIdsWithDisabledSecurityChecks.put(aUserContextId); ++ } else { ++ mUserContextIdsWithDisabledSecurityChecks.remove(aUserContextId); ++ } ++ return NS_OK; ++ } + mDisableAllSecurityCheck = aDisable; + return NS_OK; + } +diff --git a/security/manager/ssl/nsCertOverrideService.h b/security/manager/ssl/nsCertOverrideService.h +index ba995b75e6d8836abf367e26217f1b33c28909f5..092aea36289651d69369a88ec7f7751bbcb31473 100644 +--- a/security/manager/ssl/nsCertOverrideService.h ++++ b/security/manager/ssl/nsCertOverrideService.h +@@ -126,6 +126,7 @@ class nsCertOverrideService final : public nsICertOverrideService, + private: + ~nsCertOverrideService(); + ++ mozilla::HashSet mUserContextIdsWithDisabledSecurityChecks; + mozilla::Mutex mMutex; + bool mDisableAllSecurityCheck; + nsCOMPtr mSettingsFile; +diff --git a/security/manager/ssl/nsICertOverrideService.idl b/security/manager/ssl/nsICertOverrideService.idl +index 23276fbe1933b87eca13f41550c4a9ec78b1c76b..02ad890e2a884f9988ec02eef88727836a92e8d2 100644 +--- a/security/manager/ssl/nsICertOverrideService.idl ++++ b/security/manager/ssl/nsICertOverrideService.idl +@@ -130,6 +130,7 @@ interface nsICertOverrideService : nsISupports { + [must_use] + boolean hasMatchingOverride(in AUTF8String aHostName, + in int32_t aPort, ++ in uint32_t aUserContextId, + in nsIX509Cert aCert, + out uint32_t aOverrideBits, + out boolean aIsTemporary); +@@ -171,5 +172,7 @@ interface nsICertOverrideService : nsISupports { + * @param aDisable If true, disable all security check and make + * hasMatchingOverride always return true. + */ +- void setDisableAllSecurityChecksAndLetAttackersInterceptMyData(in boolean aDisable); ++ void setDisableAllSecurityChecksAndLetAttackersInterceptMyData( ++ in boolean aDisable, ++ [optional] in uint32_t aUserContextId); + }; +diff --git a/services/settings/Utils.jsm b/services/settings/Utils.jsm +index 66df8509044600a0d71eb36bb838f96087a53ef1..e4558874434e3aa57bc26344f0ca89b3ebb837bf 100644 +--- a/services/settings/Utils.jsm ++++ b/services/settings/Utils.jsm +@@ -59,7 +59,7 @@ var Utils = { + Ci.nsIEnvironment + ); + const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR"); +- return AppConstants.RELEASE_OR_BETA && !Cu.isInAutomation && !isXpcshell ++ return false && !Cu.isInAutomation && !isXpcshell + ? "https://firefox.settings.services.mozilla.com/v1" + : gServerURL; + }, +diff --git a/toolkit/components/browser/nsIWebBrowserChrome.idl b/toolkit/components/browser/nsIWebBrowserChrome.idl +index 1e9bea1655af731fc003f8d0cab3ad4d2ad29f5d..5081c0e1ee0c41c6a79bd2ed358a57442e3baa6b 100644 +--- a/toolkit/components/browser/nsIWebBrowserChrome.idl ++++ b/toolkit/components/browser/nsIWebBrowserChrome.idl +@@ -70,6 +70,9 @@ interface nsIWebBrowserChrome : nsISupports + // Whether this window should use out-of-process cross-origin subframes. + const unsigned long CHROME_FISSION_WINDOW = 0x00200000; + ++ // Whether this window has "width" or "height" defined in features ++ const unsigned long JUGGLER_WINDOW_EXPLICIT_SIZE = 0x00400000; ++ + // Prevents new window animations on MacOS and Windows. Currently + // ignored for Linux. + const unsigned long CHROME_SUPPRESS_ANIMATION = 0x01000000; +diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp +index dd475eb83b3bc0206f33807d416fb7b29326986c..207f76bf7e3bf2725a036ecaa280672e39e81005 100644 +--- a/toolkit/components/startup/nsAppStartup.cpp ++++ b/toolkit/components/startup/nsAppStartup.cpp +@@ -341,7 +341,7 @@ nsAppStartup::Quit(uint32_t aMode, int aExitCode, bool* aUserAllowedQuit) { + nsCOMPtr windowEnumerator; + nsCOMPtr mediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); +- if (mediator) { ++ if (ferocity != eForceQuit && mediator) { + mediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator)); + if (windowEnumerator) { + bool more; +diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +index 318037b12e9ea7b8bad92498950ac48ff936fb3c..44db941025a5253da38572600cd0fc57f54ee6bf 100644 +--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp ++++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp +@@ -162,8 +162,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress, + } + + NS_IMETHODIMP +-nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress, +- nsIRequest* aRequest, ++nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, ++ nsIURI *aLocation, ++ uint32_t aFlags) { ++ return NS_OK; ++} ++ ++NS_IMETHODIMP ++nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress, ++ nsIRequest *aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, +diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp +index c6da134e1c4c028420659a1ca26bf51116de71b1..93738c34673bfec06557e856f11ad1993597eb6b 100644 +--- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp ++++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp +@@ -1816,6 +1816,10 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent( + uint32_t chromeFlags = CalculateChromeFlagsHelper( + nsIWebBrowserChrome::CHROME_WINDOW_BORDERS, aFeatures, aSizeSpec); + ++ if (aFeatures.Exists("width") || aFeatures.Exists("height")) { ++ chromeFlags |= nsIWebBrowserChrome::JUGGLER_WINDOW_EXPLICIT_SIZE; ++ } ++ + return EnsureFlagsSafeForContent(chromeFlags); + } + +diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm +index e615f7d6fdc08a3c68ab6b9f4f575b2ddd1038f7..98ce556255487de47171060a3f15aef7b0b89562 100644 +--- a/toolkit/mozapps/update/UpdateService.jsm ++++ b/toolkit/mozapps/update/UpdateService.jsm +@@ -3582,7 +3582,7 @@ UpdateService.prototype = { + ).running; + } + +- return ( ++ return true || ( + (Cu.isInAutomation || marionetteRunning) && + Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false) + ); +diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild +index bbc3c98e4885f23f03f83b7c2aa00e4eb4faaef5..45dcb084904c1d2729ef1e2cd1bef1a4665d8848 100644 +--- a/toolkit/toolkit.mozbuild ++++ b/toolkit/toolkit.mozbuild +@@ -167,6 +167,7 @@ if CONFIG['ENABLE_MARIONETTE']: + DIRS += [ + '/testing/firefox-ui', + '/testing/marionette', ++ '/juggler', + '/toolkit/components/telemetry/tests/marionette', + ] + +diff --git a/toolkit/xre/nsWindowsWMain.cpp b/toolkit/xre/nsWindowsWMain.cpp +index 109c53cac98302d657d2a5a997f2ba687db14515..4d3c4beddaf627441e28f2a49d793d56fe4e2447 100644 +--- a/toolkit/xre/nsWindowsWMain.cpp ++++ b/toolkit/xre/nsWindowsWMain.cpp +@@ -14,8 +14,10 @@ + #endif + + #include "mozilla/Char16.h" ++#include "mozilla/CmdLineAndEnvUtils.h" + #include "nsUTF8Utils.h" + ++#include + #include + + #ifdef __MINGW32__ +@@ -94,6 +96,20 @@ static void FreeAllocStrings(int argc, char** argv) { + int wmain(int argc, WCHAR** argv) { + SanitizeEnvironmentVariables(); + SetDllDirectoryW(L""); ++ bool hasJugglerPipe = ++ mozilla::CheckArg(argc, argv, L"juggler-pipe", ++ static_cast(nullptr), ++ mozilla::CheckArgFlag::None) == mozilla::ARG_FOUND; ++ if (hasJugglerPipe && !mozilla::EnvHasValue("PW_PIPE_READ")) { ++ intptr_t stdio3 = _get_osfhandle(3); ++ intptr_t stdio4 = _get_osfhandle(4); ++ CHAR stdio3str[20]; ++ CHAR stdio4str[20]; ++ itoa(stdio3, stdio3str, 10); ++ itoa(stdio4, stdio4str, 10); ++ SetEnvironmentVariableA("PW_PIPE_READ", stdio3str); ++ SetEnvironmentVariableA("PW_PIPE_WRITE", stdio4str); ++ } + + // Only run this code if LauncherProcessWin.h was included beforehand, thus + // signalling that the hosting process should support launcher mode. +diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp +index 1a3b80cd646c4ed461b2d5d92db523d084028e08..71e2914a4c388cdb6da3a683087770ce8ddfbb07 100644 +--- a/uriloader/base/nsDocLoader.cpp ++++ b/uriloader/base/nsDocLoader.cpp +@@ -790,6 +790,13 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout, + ("DocLoader:%p: Firing load event for document.open\n", + this)); + ++ nsCOMPtr os = mozilla::services::GetObserverService(); ++ if (os) { ++ nsIPrincipal* principal = doc->NodePrincipal(); ++ if (!principal->IsSystemPrincipal()) ++ os->NotifyObservers(ToSupports(doc), "juggler-document-open-loaded", nullptr); ++ } ++ + // This is a very cut-down version of + // nsDocumentViewer::LoadComplete that doesn't do various things + // that are not relevant here because this wasn't an actual +@@ -1350,6 +1357,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress, + } + } + ++void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags) { ++ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION, ++ nsCOMPtr listener2 = ++ do_QueryReferent(info.mWeakListener); ++ if (!listener2) ++ continue; ++ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ ); ++ ++ // Pass the notification up to the parent... ++ if (mParent) { ++ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags); ++ } ++} ++ + void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { +diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h +index 9f39fca7f2c7212c383a02b34079d54ce8cd0fac..71e8ca931656b89863427665eb2e7f17a81566f3 100644 +--- a/uriloader/base/nsDocLoader.h ++++ b/uriloader/base/nsDocLoader.h +@@ -209,6 +209,11 @@ class nsDocLoader : public nsIDocumentLoader, + nsIURI* aURI, int32_t aDelay, + bool aSameURI); + ++ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress, ++ nsIRequest* aRequest, ++ nsIURI *aUri, ++ uint32_t aFlags); ++ + // this function is overridden by the docshell, it is provided so that we + // can pass more information about redirect state (the normal OnStateChange + // doesn't get the new channel). +diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl +index 70079adfff8a1b625ffd5aad5572c960cec5b646..5822ca46fb4022b0b31288f5492fcb4a2ac9baa1 100644 +--- a/uriloader/base/nsIWebProgress.idl ++++ b/uriloader/base/nsIWebProgress.idl +@@ -87,6 +87,10 @@ interface nsIWebProgress : nsISupports + * NOTIFY_REFRESH + * Receive onRefreshAttempted events. + * This is defined on nsIWebProgressListener2. ++ * ++ * NOTIFY_FRAME_LOCATION ++ * Receive onFrameLocationChange events. ++ * This is defined on nsIWebProgressListener2. + */ + const unsigned long NOTIFY_PROGRESS = 0x00000010; + const unsigned long NOTIFY_STATUS = 0x00000020; +@@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports + const unsigned long NOTIFY_LOCATION = 0x00000080; + const unsigned long NOTIFY_REFRESH = 0x00000100; + const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200; ++ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000400; + + /** + * This flag enables all notifications. + */ +- const unsigned long NOTIFY_ALL = 0x000003ff; ++ const unsigned long NOTIFY_ALL = 0x000007ff; + + /** + * Registers a listener to receive web progress events. +diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl +index 87701f8d2cfee8bd84acd28c62b3be4989c9474c..ae1aa85c019cb21d4f7e79c35e8afe72709468a1 100644 +--- a/uriloader/base/nsIWebProgressListener2.idl ++++ b/uriloader/base/nsIWebProgressListener2.idl +@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener { + in nsIURI aRefreshURI, + in long aMillis, + in boolean aSameURI); ++ ++ /** ++ * Called when the location of the window or its subframes changes. This is not ++ * when a load is requested, but rather once it is verified that the load is ++ * going to occur in the given window. For instance, a load that starts in a ++ * window might send progress and status messages for the new site, but it ++ * will not send the onLocationChange until we are sure that we are loading ++ * this new page here. ++ * ++ * @param aWebProgress ++ * The nsIWebProgress instance that fired the notification. ++ * @param aRequest ++ * The associated nsIRequest. This may be null in some cases. ++ * @param aLocation ++ * The URI of the location that is being loaded. ++ * @param aFlags ++ * This is a value which explains the situation or the reason why ++ * the location has changed. ++ */ ++ void onFrameLocationChange(in nsIWebProgress aWebProgress, ++ in nsIRequest aRequest, ++ in nsIURI aLocation, ++ [optional] in unsigned long aFlags); + }; +diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp +index 92927822d792fa2d10a247264e1ec796c3b0cb11..10377d008c034b1b291e4d518c0c16f68a3f0b59 100644 +--- a/uriloader/exthandler/nsExternalHelperAppService.cpp ++++ b/uriloader/exthandler/nsExternalHelperAppService.cpp +@@ -101,6 +101,7 @@ + + #include "mozilla/Components.h" + #include "mozilla/ClearOnShutdown.h" ++#include "mozilla/ErrorNames.h" + #include "mozilla/Preferences.h" + #include "mozilla/ipc/URIUtils.h" + +@@ -903,6 +904,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension( + return NS_OK; + } + ++NS_IMETHODIMP nsExternalHelperAppService::SetDownloadInterceptor( ++ nsIDownloadInterceptor* interceptor) { ++ mInterceptor = interceptor; ++ return NS_OK; ++} ++ + nsresult nsExternalHelperAppService::GetFileTokenForPath( + const char16_t* aPlatformAppPath, nsIFile** aFile) { + nsDependentString platformAppPath(aPlatformAppPath); +@@ -1568,7 +1575,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) { + // Strip off the ".part" from mTempLeafName + mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1); + ++ return CreateSaverForTempFile(); ++} ++ ++nsresult nsExternalAppHandler::CreateSaverForTempFile() { + MOZ_ASSERT(!mSaver, "Output file initialization called more than once!"); ++ nsresult rv; + mSaver = + do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); +@@ -1741,7 +1753,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { + return NS_OK; + } + +- rv = SetUpTempFile(aChannel); ++ bool isIntercepted = false; ++ nsCOMPtr interceptor = mExtProtSvc->mInterceptor; ++ if (interceptor) { ++ nsCOMPtr fileToUse; ++ rv = interceptor->InterceptDownloadRequest(this, request, mBrowsingContext, getter_AddRefs(fileToUse), &isIntercepted); ++ if (!NS_SUCCEEDED(rv)) { ++ LOG((" failed to call nsIDowloadInterceptor.interceptDownloadRequest")); ++ return rv; ++ } ++ if (isIntercepted) { ++ LOG((" request interceped by nsIDowloadInterceptor")); ++ if (fileToUse) { ++ mTempFile = fileToUse; ++ rv = mTempFile->GetLeafName(mTempLeafName); ++ NS_ENSURE_SUCCESS(rv, rv); ++ } else { ++ Cancel(NS_BINDING_ABORTED); ++ return NS_OK; ++ } ++ } ++ } ++ ++ // Temp file is the final destination when download is intercepted. In that ++ // case we only need to create saver (and not create transfer later). Not creating ++ // mTransfer also cuts off all downloads handling logic in the js compoenents and ++ // browser UI. ++ if (isIntercepted) ++ rv = CreateSaverForTempFile(); ++ else ++ rv = SetUpTempFile(aChannel); + if (NS_FAILED(rv)) { + nsresult transferError = rv; + +@@ -1794,6 +1835,11 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) { + + bool alwaysAsk = true; + mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk); ++ ++ if (isIntercepted) { ++ return NS_OK; ++ } ++ + if (alwaysAsk) { + // But we *don't* ask if this mimeInfo didn't come from + // our user configuration datastore and the user has said +@@ -2200,6 +2246,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver, + NotifyTransfer(aStatus); + } + ++ if (!mCanceled) { ++ nsCOMPtr interceptor = mExtProtSvc->mInterceptor; ++ if (interceptor) { ++ nsCString noError; ++ nsresult rv = interceptor->OnDownloadComplete(this, noError); ++ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to call nsIDowloadInterceptor.OnDownloadComplete"); ++ Unused << rv; ++ } ++ } ++ + return NS_OK; + } + +@@ -2587,6 +2643,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) { + } + } + ++ nsCOMPtr interceptor = mExtProtSvc->mInterceptor; ++ if (interceptor) { ++ nsCString errorName; ++ GetErrorName(aReason, errorName); ++ nsresult rv = interceptor->OnDownloadComplete(this, errorName); ++ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed notify nsIDowloadInterceptor about cancel"); ++ Unused << rv; ++ } ++ + // Break our reference cycle with the helper app dialog (set up in + // OnStartRequest) + mDialog = nullptr; +diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h +index 8c87587e9ee70d2786ee0d73d3cce8be387a382d..90acb4cb1075639821c58e4506731b7acab18e61 100644 +--- a/uriloader/exthandler/nsExternalHelperAppService.h ++++ b/uriloader/exthandler/nsExternalHelperAppService.h +@@ -206,6 +206,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService, + mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener); ++ ++ nsCOMPtr mInterceptor; + }; + + /** +@@ -397,6 +399,9 @@ class nsExternalAppHandler final : public nsIStreamListener, + * Upon successful return, both mTempFile and mSaver will be valid. + */ + nsresult SetUpTempFile(nsIChannel* aChannel); ++ ++ nsresult CreateSaverForTempFile(); ++ + /** + * When we download a helper app, we are going to retarget all load + * notifications into our own docloader and load group instead of +diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl +index 657e15bc07426745b9488b903c5a53b8d977fb2d..4f61835e64d537ab7a35c2c2fb059e67cd7cd0fc 100644 +--- a/uriloader/exthandler/nsIExternalHelperAppService.idl ++++ b/uriloader/exthandler/nsIExternalHelperAppService.idl +@@ -6,6 +6,8 @@ + + #include "nsICancelable.idl" + ++webidl BrowsingContext; ++interface nsIHelperAppLauncher; + interface nsIURI; + interface nsIRequest; + interface nsIStreamListener; +@@ -15,6 +17,17 @@ interface nsIWebProgressListener2; + interface nsIInterfaceRequestor; + webidl BrowsingContext; + ++/** ++ * Interceptor interface used by Juggler. ++ */ ++[scriptable, uuid(9a20e9b0-75d0-11ea-bc55-0242ac130003)] ++interface nsIDownloadInterceptor : nsISupports ++{ ++ bool interceptDownloadRequest(in nsIHelperAppLauncher aHandler, in nsIRequest aRequest, in BrowsingContext aBrowsingContext, out nsIFile file); ++ ++ void onDownloadComplete(in nsIHelperAppLauncher aHandler, in ACString aErrorName); ++}; ++ + /** + * The external helper app service is used for finding and launching + * platform specific external applications for a given mime content type. +@@ -43,7 +56,7 @@ interface nsIExternalHelperAppService : nsISupports + in nsIInterfaceRequestor aContentContext, + in boolean aForceSave, + [optional] in nsIInterfaceRequestor aWindowContext); +- ++ + /** + * Binds an external helper application to a stream listener. The caller + * should pump data into the returned stream listener. When the OnStopRequest +@@ -76,6 +89,7 @@ interface nsIExternalHelperAppService : nsISupports + boolean applyDecodingForExtension(in AUTF8String aExtension, + in ACString aEncodingType); + ++ void setDownloadInterceptor(in nsIDownloadInterceptor interceptor); + }; + + /** +diff --git a/widget/InProcessCompositorWidget.cpp b/widget/InProcessCompositorWidget.cpp +index aa52dab7d4b45c42d3dfdc63b9982ac2bab5f21f..ba46d7d54920aba478aeb9ba007991e875184242 100644 +--- a/widget/InProcessCompositorWidget.cpp ++++ b/widget/InProcessCompositorWidget.cpp +@@ -4,7 +4,10 @@ + + #include "InProcessCompositorWidget.h" + ++#include "HeadlessCompositorWidget.h" ++#include "HeadlessWidget.h" + #include "mozilla/VsyncDispatcher.h" ++#include "mozilla/widget/PlatformWidgetTypes.h" + #include "nsBaseWidget.h" + + #if defined(MOZ_WIDGET_ANDROID) && !defined(MOZ_WIDGET_SUPPORTS_OOP_COMPOSITING) +@@ -25,6 +28,12 @@ RefPtr CompositorWidget::CreateLocal( + // only remaining explanation that doesn't involve memory corruption, + // so placing a release assert here. + MOZ_RELEASE_ASSERT(aWidget); ++ if (aInitData.type() == ++ CompositorWidgetInitData::THeadlessCompositorWidgetInitData) { ++ return new HeadlessCompositorWidget( ++ aInitData.get_HeadlessCompositorWidgetInitData(), aOptions, ++ static_cast(aWidget)); ++ } + # ifdef MOZ_WIDGET_ANDROID + return new AndroidCompositorWidget(aOptions, + static_cast(aWidget)); +diff --git a/widget/cocoa/NativeKeyBindings.mm b/widget/cocoa/NativeKeyBindings.mm +index 6a74e57ead4968e5cf5da73b2a2e093e8862f5da..9e44c36a6961a2d7548a874d8934f20f00db1f14 100644 +--- a/widget/cocoa/NativeKeyBindings.mm ++++ b/widget/cocoa/NativeKeyBindings.mm +@@ -491,6 +491,13 @@ void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType, + break; + case KEY_NAME_INDEX_ArrowLeft: + if (aEvent.IsAlt()) { ++ if (aEvent.IsMeta() || aEvent.IsControl()) ++ break; ++ instance->AppendEditCommandsForSelector( ++ !aEvent.IsShift() ++ ? ToObjcSelectorPtr(@selector(moveWordLeft:)) ++ : ToObjcSelectorPtr(@selector(moveWordLeftAndModifySelection:)), ++ aCommands); + break; + } + if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) { +@@ -511,6 +518,13 @@ void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType, + break; + case KEY_NAME_INDEX_ArrowRight: + if (aEvent.IsAlt()) { ++ if (aEvent.IsMeta() || aEvent.IsControl()) ++ break; ++ instance->AppendEditCommandsForSelector( ++ !aEvent.IsShift() ++ ? ToObjcSelectorPtr(@selector(moveWordRight:)) ++ : ToObjcSelectorPtr(@selector(moveWordRightAndModifySelection:)), ++ aCommands); + break; + } + if (aEvent.IsMeta() || (aEvent.IsControl() && aEvent.IsShift())) { +@@ -531,6 +545,10 @@ void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType, + break; + case KEY_NAME_INDEX_ArrowUp: + if (aEvent.IsControl()) { ++ if (aEvent.IsMeta() || aEvent.IsAlt()) ++ break; ++ instance->AppendEditCommandsForSelector( ++ ToObjcSelectorPtr(@selector(scrollPageUp:)), aCommands); + break; + } + if (aEvent.IsMeta()) { +@@ -540,7 +558,7 @@ void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType, + instance->AppendEditCommandsForSelector( + !aEvent.IsShift() + ? ToObjcSelectorPtr(@selector(moveToBeginningOfDocument:)) +- : ToObjcSelectorPtr(@selector(moveToBegginingOfDocumentAndModifySelection:)), ++ : ToObjcSelectorPtr(@selector(moveToBeginningOfDocumentAndModifySelection:)), + aCommands); + break; + } +@@ -563,6 +581,10 @@ void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType, + break; + case KEY_NAME_INDEX_ArrowDown: + if (aEvent.IsControl()) { ++ if (aEvent.IsMeta() || aEvent.IsAlt()) ++ break; ++ instance->AppendEditCommandsForSelector( ++ ToObjcSelectorPtr(@selector(scrollPageDown:)), aCommands); + break; + } + if (aEvent.IsMeta()) { +diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp +index b31a969b7ab3d0fc80912b110d91dfdf3e5991f4..45456bd34713a32695c0fe6a84588f31e40c137d 100644 +--- a/widget/headless/HeadlessCompositorWidget.cpp ++++ b/widget/headless/HeadlessCompositorWidget.cpp +@@ -3,6 +3,7 @@ + * 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/layers/CompositorThread.h" + #include "mozilla/widget/PlatformWidgetTypes.h" + #include "HeadlessCompositorWidget.h" + #include "VsyncDispatcher.h" +@@ -17,6 +18,32 @@ HeadlessCompositorWidget::HeadlessCompositorWidget( + mClientSize = aInitData.InitialClientSize(); + } + ++void HeadlessCompositorWidget::SetSnapshotListener(HeadlessWidget::SnapshotListener&& listener) { ++ MOZ_ASSERT(NS_IsMainThread()); ++ ++ layers::CompositorThread()->Dispatch(NewRunnableMethod( ++ "HeadlessCompositorWidget::SetSnapshotListener", this, ++ &HeadlessCompositorWidget::SetSnapshotListenerOnCompositorThread, ++ std::move(listener))); ++} ++ ++void HeadlessCompositorWidget::SetSnapshotListenerOnCompositorThread( ++ HeadlessWidget::SnapshotListener&& listener) { ++ MOZ_ASSERT(NS_IsInCompositorThread()); ++ mSnapshotListener = std::move(listener); ++ PeriodicSnapshot(); ++} ++ ++already_AddRefed HeadlessCompositorWidget::StartRemoteDrawingInRegion( ++ LayoutDeviceIntRegion& aInvalidRegion, layers::BufferMode* aBufferMode) { ++ if (!mDrawTarget) ++ return nullptr; ++ ++ *aBufferMode = layers::BufferMode::BUFFER_NONE; ++ RefPtr result = mDrawTarget; ++ return result.forget(); ++} ++ + void HeadlessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) { + if (RefPtr cvd = + mWidget->GetCompositorVsyncDispatcher()) { +@@ -29,6 +56,58 @@ nsIWidget* HeadlessCompositorWidget::RealWidget() { return mWidget; } + void HeadlessCompositorWidget::NotifyClientSizeChanged( + const LayoutDeviceIntSize& aClientSize) { + mClientSize = aClientSize; ++ layers::CompositorThread()->Dispatch(NewRunnableMethod( ++ "HeadlessCompositorWidget::UpdateDrawTarget", this, ++ &HeadlessCompositorWidget::UpdateDrawTarget, ++ aClientSize)); ++} ++ ++void HeadlessCompositorWidget::UpdateDrawTarget(const LayoutDeviceIntSize& aClientSize) { ++ MOZ_ASSERT(NS_IsInCompositorThread()); ++ if (aClientSize.IsEmpty()) { ++ mDrawTarget = nullptr; ++ return; ++ } ++ ++ RefPtr old = std::move(mDrawTarget); ++ gfx::SurfaceFormat format = gfx::SurfaceFormat::B8G8R8A8; ++ gfx::IntSize size = aClientSize.ToUnknownSize(); ++ mDrawTarget = mozilla::gfx::Factory::CreateDrawTarget( ++ mozilla::gfx::BackendType::SKIA, size, format); ++ if (old) { ++ RefPtr snapshot = old->Snapshot(); ++ if (snapshot) ++ mDrawTarget->CopySurface(snapshot.get(), old->GetRect(), gfx::IntPoint(0, 0)); ++ } ++} ++ ++void HeadlessCompositorWidget::PeriodicSnapshot() { ++ if (!mSnapshotListener) ++ return; ++ ++ TakeSnapshot(); ++ NS_DelayedDispatchToCurrentThread(NewRunnableMethod( ++ "HeadlessCompositorWidget::PeriodicSnapshot", this, ++ &HeadlessCompositorWidget::PeriodicSnapshot), 40); ++} ++ ++void HeadlessCompositorWidget::TakeSnapshot() { ++ if (!mDrawTarget) ++ return; ++ ++ RefPtr snapshot = mDrawTarget->Snapshot(); ++ if (!snapshot) { ++ fprintf(stderr, "Failed to get snapshot of draw target\n"); ++ return; ++ } ++ ++ RefPtr dataSurface = snapshot->GetDataSurface(); ++ if (!dataSurface) { ++ fprintf(stderr, "Failed to get data surface from snapshot\n"); ++ return; ++ } ++ ++ mSnapshotListener(std::move(dataSurface)); + } + + LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() { +diff --git a/widget/headless/HeadlessCompositorWidget.h b/widget/headless/HeadlessCompositorWidget.h +index 7f91de9e67d7ffa02de3eef1d760e5cfd05e7ad6..b0e3572413f80e5bd125f777c3247b10b8521a73 100644 +--- a/widget/headless/HeadlessCompositorWidget.h ++++ b/widget/headless/HeadlessCompositorWidget.h +@@ -23,9 +23,13 @@ class HeadlessCompositorWidget final : public CompositorWidget, + HeadlessWidget* aWindow); + + void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize); ++ void SetSnapshotListener(HeadlessWidget::SnapshotListener&& listener); + + // CompositorWidget Overrides + ++ already_AddRefed StartRemoteDrawingInRegion( ++ LayoutDeviceIntRegion& aInvalidRegion, layers::BufferMode* aBufferMode) override; ++ + uintptr_t GetWidgetKey() override; + + LayoutDeviceIntSize GetClientSize() override; +@@ -42,9 +46,18 @@ class HeadlessCompositorWidget final : public CompositorWidget, + } + + private: ++ void SetSnapshotListenerOnCompositorThread( ++ HeadlessWidget::SnapshotListener&& listener); ++ void UpdateDrawTarget(const LayoutDeviceIntSize& aClientSize); ++ void PeriodicSnapshot(); ++ void TakeSnapshot(); ++ + HeadlessWidget* mWidget; + + LayoutDeviceIntSize mClientSize; ++ ++ HeadlessWidget::SnapshotListener mSnapshotListener; ++ RefPtr mDrawTarget; + }; + + } // namespace widget +diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp +index 46e24a8c457fac3a90cc7cb37a71be11e86cf00f..98f3266722cecfa39a9caf66eae7a96c993200bf 100644 +--- a/widget/headless/HeadlessWidget.cpp ++++ b/widget/headless/HeadlessWidget.cpp +@@ -107,6 +107,8 @@ void HeadlessWidget::Destroy() { + } + } + ++ SetSnapshotListener(nullptr); ++ + nsBaseWidget::OnDestroy(); + + nsBaseWidget::Destroy(); +@@ -556,5 +558,15 @@ nsresult HeadlessWidget::SynthesizeNativeTouchPadPinch( + DispatchPinchGestureInput(inputToDispatch); + return NS_OK; + } ++ ++void HeadlessWidget::SetSnapshotListener(SnapshotListener&& listener) { ++ if (!mCompositorWidget) { ++ if (listener) ++ fprintf(stderr, "Trying to set SnapshotListener without compositor widget\n"); ++ return; ++ } ++ mCompositorWidget->SetSnapshotListener(std::move(listener)); ++} ++ + } // namespace widget + } // namespace mozilla +diff --git a/widget/headless/HeadlessWidget.h b/widget/headless/HeadlessWidget.h +index 7dbbdca1c34939ce1899046155d3203e6226ffa1..10c3ef76916a88224c77a5c7f1249616391b94e2 100644 +--- a/widget/headless/HeadlessWidget.h ++++ b/widget/headless/HeadlessWidget.h +@@ -157,6 +157,9 @@ class HeadlessWidget : public nsBaseWidget { + TouchpadPinchPhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint, + int32_t aModifierFlags) override; + ++ using SnapshotListener = std::function&&)>; ++ void SetSnapshotListener(SnapshotListener&& listener); ++ + private: + ~HeadlessWidget(); + bool mEnabled; +diff --git a/xpcom/reflect/xptinfo/xptinfo.h b/xpcom/reflect/xptinfo/xptinfo.h +index 33b1f25411fd6a8d02edca9198054347289a1501..ee6ea48f3986a8d7c0e2f351b6d30b9fb706524e 100644 +--- a/xpcom/reflect/xptinfo/xptinfo.h ++++ b/xpcom/reflect/xptinfo/xptinfo.h +@@ -513,7 +513,7 @@ static_assert(sizeof(nsXPTMethodInfo) == 8, "wrong size"); + #if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) + # define PARAM_BUFFER_COUNT 18 + #else +-# define PARAM_BUFFER_COUNT 14 ++# define PARAM_BUFFER_COUNT 15 + #endif + + /** diff --git a/browser_patches/firefox-stable/preferences/00-playwright-prefs.js b/browser_patches/firefox-stable/preferences/00-playwright-prefs.js new file mode 100644 index 0000000000..1895d2d1b7 --- /dev/null +++ b/browser_patches/firefox-stable/preferences/00-playwright-prefs.js @@ -0,0 +1,3 @@ +// Any comment. You must start the file with a single-line comment! +pref("general.config.filename", "playwright.cfg"); +pref("general.config.obscure_value", 0); diff --git a/browser_patches/firefox-stable/preferences/playwright.cfg b/browser_patches/firefox-stable/preferences/playwright.cfg new file mode 100644 index 0000000000..367bece699 --- /dev/null +++ b/browser_patches/firefox-stable/preferences/playwright.cfg @@ -0,0 +1,275 @@ +// Any comment. You must start the file with a comment! + +// Use light theme by default. +pref("ui.systemUsesDarkTheme", 0); + +// @see https://github.com/microsoft/playwright/issues/4297 +pref("browser.tabs.remote.useCrossOriginEmbedderPolicy", false); +pref("browser.tabs.remote.useCrossOriginOpenerPolicy", false); + +// Only allow the old modal dialogs. This should be removed when there is +// support for the new modal UI (see Bug 1686743). +pref("prompts.contentPromptSubDialog", false); + +// Increase max number of child web processes so that new pages +// get a new process by default and we have a process isolation +// between pages from different contexts. If this becomes a performance +// issue we can povide custom '@mozilla.org/ipc/processselector;1' +// +pref("dom.ipc.processCount", 60000); + +// Never reuse processes as they may keep previously overridden values +// (locale, timezone etc.). +pref("dom.ipc.processPrelaunch.enabled", false); + +// Do not use system colors - they are affected by themes. +pref("ui.use_standins_for_native_colors", true); + +// Isolate permissions by user context. +pref("permissions.isolateBy.userContext", true); + +pref("dom.push.serverURL", ""); +pref("services.settings.server", ""); +pref("browser.safebrowsing.provider.mozilla.updateURL", ""); +pref("browser.library.activity-stream.enabled", false); +pref("browser.search.geoSpecificDefaults", false); +pref("browser.search.geoSpecificDefaults.url", ""); +pref("captivedetect.canonicalURL", ""); +pref("network.captive-portal-service.enabled", false); +pref("network.connectivity-service.enabled", false); +pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", ""); + +// Make sure Shield doesn't hit the network. +pref("app.normandy.api_url", ""); +pref("app.normandy.enabled", false); + +// Disable updater +pref("app.update.enabled", false); +// Disable Firefox old build background check +pref("app.update.checkInstallTime", false); +// Disable automatically upgrading Firefox +pref("app.update.disabledForTesting", true); + +// make absolutely sure it is really off +pref("app.update.auto", false); +pref("app.update.mode", 0); +pref("app.update.service.enabled", false); +// Dislabe newtabpage +pref("browser.startup.homepage", "about:blank"); +pref("browser.startup.page", 0); +pref("browser.newtabpage.enabled", false); +// Do not redirect user when a milstone upgrade of Firefox is detected +pref("browser.startup.homepage_override.mstone", "ignore"); + +pref("browser.tabs.remote.separateFileUriProcess", false); +pref("security.sandbox.content.level", 2); + +// Disable topstories +pref("browser.newtabpage.activity-stream.feeds.section.topstories", false); +// DevTools JSONViewer sometimes fails to load dependencies with its require.js. +// This doesn't affect Puppeteer operations, but spams console with a lot of +// unpleasant errors. +// (bug 1424372) +pref("devtools.jsonview.enabled", false); + +// Prevent various error message on the console +pref("browser.contentblocking.features.standard", "-tp,tpPrivate,cookieBehavior0,-cm,-fp"); +pref("network.cookie.cookieBehavior", 0); + +// Increase the APZ content response timeout in tests to 1 minute. +// This is to accommodate the fact that test environments tends to be +// slower than production environments (with the b2g emulator being +// the slowest of them all), resulting in the production timeout value +// sometimes being exceeded and causing false-positive test failures. +// +// (bug 1176798, bug 1177018, bug 1210465) +pref("apz.content_response_timeout", 60000); + +// Allow creating files in content process - required for +// |Page.setFileInputFiles| protocol method. +pref("dom.file.createInChild", true); + +// Indicate that the download panel has been shown once so that +// whichever download test runs first doesn't show the popup +// inconsistently. +pref("browser.download.panel.shown", true); +// Background thumbnails in particular cause grief, and disabling +// thumbnails in general cannot hurt +pref("browser.pagethumbnails.capturing_disabled", true); +// Disable safebrowsing components. +pref("browser.safebrowsing.blockedURIs.enabled", false); +pref("browser.safebrowsing.downloads.enabled", false); +pref("browser.safebrowsing.passwords.enabled", false); +pref("browser.safebrowsing.malware.enabled", false); +pref("browser.safebrowsing.phishing.enabled", false); +// Disable updates to search engines. +pref("browser.search.update", false); +// Do not restore the last open set of tabs if the browser has crashed +pref("browser.sessionstore.resume_from_crash", false); +// Don't check for the default web browser during startup. +pref("browser.shell.checkDefaultBrowser", false); + +// Disable browser animations (tabs, fullscreen, sliding alerts) +pref("toolkit.cosmeticAnimations.enabled", false); + +// Close the window when the last tab gets closed +pref("browser.tabs.closeWindowWithLastTab", true); + +// Do not allow background tabs to be zombified on Android, otherwise for +// tests that open additional tabs, the test harness tab itself might get +// unloaded +pref("browser.tabs.disableBackgroundZombification", false); + +// Do not warn when closing all open tabs +pref("browser.tabs.warnOnClose", false); + +// Do not warn when closing all other open tabs +pref("browser.tabs.warnOnCloseOtherTabs", false); + +// Do not warn when multiple tabs will be opened +pref("browser.tabs.warnOnOpen", false); + +// Disable first run splash page on Windows 10 +pref("browser.usedOnWindows10.introURL", ""); + +// Disable the UI tour. +// +// Should be set in profile. +pref("browser.uitour.enabled", false); + +// Turn off search suggestions in the location bar so as not to trigger +// network connections. +pref("browser.urlbar.suggest.searches", false); + +// Do not warn on quitting Firefox +pref("browser.warnOnQuit", false); + +// Do not show datareporting policy notifications which can +// interfere with tests +pref("datareporting.healthreport.documentServerURI", ""); +pref("datareporting.healthreport.about.reportUrl", ""); +pref("datareporting.healthreport.logging.consoleEnabled", false); +pref("datareporting.healthreport.service.enabled", false); +pref("datareporting.healthreport.service.firstRun", false); +pref("datareporting.healthreport.uploadEnabled", false); +pref("datareporting.policy.dataSubmissionEnabled", false); +pref("datareporting.policy.dataSubmissionPolicyAccepted", false); +pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true); + +// Automatically unload beforeunload alerts +pref("dom.disable_beforeunload", false); + +// Disable popup-blocker +pref("dom.disable_open_during_load", false); + +// Disable the ProcessHangMonitor +pref("dom.ipc.reportProcessHangs", false); +pref("hangmonitor.timeout", 0); + +// Disable slow script dialogues +pref("dom.max_chrome_script_run_time", 0); +pref("dom.max_script_run_time", 0); + +// Only load extensions from the application and user profile +// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION +pref("extensions.autoDisableScopes", 0); +pref("extensions.enabledScopes", 5); + +// Disable metadata caching for installed add-ons by default +pref("extensions.getAddons.cache.enabled", false); + +// Disable installing any distribution extensions or add-ons. +pref("extensions.installDistroAddons", false); + +// Turn off extension updates so they do not bother tests +pref("extensions.update.enabled", false); +pref("extensions.update.notifyUser", false); + +// Make sure opening about:addons will not hit the network +pref("extensions.webservice.discoverURL", ""); + +pref("extensions.screenshots.disabled", true); +pref("extensions.screenshots.upload-disabled", true); + +// Allow the application to have focus even it runs in the background +pref("focusmanager.testmode", true); + +// Disable useragent updates +pref("general.useragent.updates.enabled", false); + +// No ICC color correction. +// See https://developer.mozilla.org/en/docs/Mozilla/Firefox/Releases/3.5/ICC_color_correction_in_Firefox. +pref("gfx.color_management.mode", 0); +pref("gfx.color_management.rendering_intent", 3); + +// Always use network provider for geolocation tests so we bypass the +// macOS dialog raised by the corelocation provider +pref("geo.provider.testing", true); + +// Do not scan Wifi +pref("geo.wifi.scan", false); + +// Show chrome errors and warnings in the error console +pref("javascript.options.showInConsole", true); + +// Disable download and usage of OpenH264: and Widevine plugins +pref("media.gmp-manager.updateEnabled", false); + +// Do not prompt with long usernames or passwords in URLs +pref("network.http.phishy-userpass-length", 255); + +// Do not prompt for temporary redirects +pref("network.http.prompt-temp-redirect", false); + +// Disable speculative connections so they are not reported as leaking +// when they are hanging around +pref("network.http.speculative-parallel-limit", 0); + +// Do not automatically switch between offline and online +pref("network.manage-offline-status", false); + +// Make sure SNTP requests do not hit the network +pref("network.sntp.pools", ""); + +// Disable Flash +pref("plugin.state.flash", 0); + +pref("privacy.trackingprotection.enabled", false); + +pref("security.certerrors.mitm.priming.enabled", false); + +// Local documents have access to all other local documents, +// including directory listings +pref("security.fileuri.strict_origin_policy", false); + +// Tests do not wait for the notification button security delay +pref("security.notification_enable_delay", 0); + +// Ensure blocklist updates do not hit the network +pref("services.settings.server", ""); + +// Do not automatically fill sign-in forms with known usernames and +// passwords +pref("signon.autofillForms", false); + +// Disable password capture, so that tests that include forms are not +// influenced by the presence of the persistent doorhanger notification +pref("signon.rememberSignons", false); + +// Disable first-run welcome page +pref("startup.homepage_welcome_url", "about:blank"); +pref("startup.homepage_welcome_url.additional", ""); + +// Prevent starting into safe mode after application crashes +pref("toolkit.startup.max_resumed_crashes", -1); +lockPref("toolkit.crashreporter.enabled", false); + +pref("toolkit.telemetry.enabled", false); +pref("toolkit.telemetry.server", ""); + +// Disable downloading the list of blocked extensions. +pref("extensions.blocklist.enabled", false); + +// Force Firefox Devtools to open in a separate window. +pref("devtools.toolbox.host", "window"); + diff --git a/browser_patches/prepare_checkout.sh b/browser_patches/prepare_checkout.sh index d700879193..b977600309 100755 --- a/browser_patches/prepare_checkout.sh +++ b/browser_patches/prepare_checkout.sh @@ -10,7 +10,7 @@ REMOTE_BROWSER_UPSTREAM="browser_upstream" BUILD_BRANCH="playwright-build" if [[ ($1 == '--help') || ($1 == '-h') ]]; then - echo "usage: $(basename $0) [firefox|webkit] [custom_checkout_path]" + echo "usage: $(basename $0) [firefox|firefox-stable|webkit] [custom_checkout_path]" echo echo "Prepares browser checkout. The checkout is a GIT repository that:" echo "- has a '$REMOTE_BROWSER_UPSTREAM' remote pointing to a REMOTE_URL from UPSTREAM_CONFIG.sh" @@ -93,6 +93,18 @@ elif [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then CHECKOUT_PATH="${FF_CHECKOUT_PATH}" FRIENDLY_CHECKOUT_PATH="" fi +elif [[ ("$1" == "firefox-stable") ]]; then + FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox-stable/checkout"; + CHECKOUT_PATH="$PWD/firefox-stable/checkout" + PATCHES_PATH="$PWD/firefox-stable/patches" + FIREFOX_EXTRA_FOLDER_PATH="$PWD/firefox-stable/juggler" + BUILD_NUMBER=$(head -1 "$PWD/firefox-stable/BUILD_NUMBER") + source "./firefox-stable/UPSTREAM_CONFIG.sh" + if [[ ! -z "${FF_CHECKOUT_PATH}" ]]; then + echo "WARNING: using checkout path from FF_CHECKOUT_PATH env: ${FF_CHECKOUT_PATH}" + CHECKOUT_PATH="${FF_CHECKOUT_PATH}" + FRIENDLY_CHECKOUT_PATH="" + fi elif [[ ("$1" == "deprecated-webkit-mac-10.14") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/deprecated-webkit-mac-10.14/checkout"; CHECKOUT_PATH="$PWD/deprecated-webkit-mac-10.14/checkout"