From a3f34fb4b71c46ada5791d3bd5485977f68d8891 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Tue, 2 Jun 2020 16:51:13 -0700 Subject: [PATCH] chore: export juggler as a standalone folder for browser build (#2432) This leaves our firefox diff to gecko instrumentation changes only. Drive-by: rename webkit "src" folder into "embedder". --- browser_patches/export.sh | 18 +- browser_patches/firefox/BUILD_NUMBER | 2 +- browser_patches/firefox/juggler/Helper.js | 119 + .../firefox/juggler/NetworkObserver.js | 800 +++ .../firefox/juggler/SimpleChannel.js | 134 + .../firefox/juggler/TargetRegistry.js | 680 ++ .../firefox/juggler/components/juggler.js | 84 + .../juggler/components/juggler.manifest | 3 + .../firefox/juggler/components/moz.build | 9 + .../firefox/juggler/content/FrameTree.js | 477 ++ .../firefox/juggler/content/NetworkMonitor.js | 52 + .../firefox/juggler/content/PageAgent.js | 981 +++ .../firefox/juggler/content/Runtime.js | 541 ++ .../juggler/content/ScrollbarManager.js | 89 + .../firefox/juggler/content/WorkerMain.js | 87 + .../juggler/content/floating-scrollbars.css | 51 + .../juggler/content/hidden-scrollbars.css | 17 + .../firefox/juggler/content/main.js | 192 + browser_patches/firefox/juggler/jar.mn | 28 + browser_patches/firefox/juggler/moz.build | 15 + .../juggler/protocol/AccessibilityHandler.js | 20 + .../juggler/protocol/BrowserHandler.js | 243 + .../firefox/juggler/protocol/Dispatcher.js | 139 + .../juggler/protocol/NetworkHandler.js | 162 + .../firefox/juggler/protocol/PageHandler.js | 345 + .../juggler/protocol/PrimitiveTypes.js | 147 + .../firefox/juggler/protocol/Protocol.js | 850 +++ .../juggler/protocol/RuntimeHandler.js | 56 + .../firefox/patches/bootstrap.diff | 6389 ----------------- browser_patches/prepare_checkout.sh | 18 +- browser_patches/webkit/BUILD_NUMBER | 2 +- .../Playwright/Configurations/Base.xcconfig | 0 .../Configurations/DebugRelease.xcconfig | 0 .../Configurations/Playwright.xcconfig | 0 .../Configurations/SDKVariant.xcconfig | 0 .../Playwright/MBToolbarItem.h | 0 .../Playwright/MBToolbarItem.m | 0 .../Tools => embedder}/Playwright/Makefile | 0 .../Playwright.xcodeproj/project.pbxproj | 0 .../xcschemes/Playwright.xcscheme | 0 .../Playwright/mac/AppDelegate.h | 0 .../Playwright/mac/AppDelegate.m | 0 .../Playwright/mac/BrowserWindow.xib | 0 .../Playwright/mac/BrowserWindowController.h | 0 .../Playwright/mac/BrowserWindowController.m | 0 .../Playwright/mac/CMakeLists.txt | 0 .../Playwright/mac/Info.plist | 0 .../Playwright/mac/MainMenu.xib | 0 .../Playwright/mac/Playwright_Prefix.pch | 0 .../Tools => embedder}/Playwright/mac/main.m | 0 .../Playwright/win/CMakeLists.txt | 0 .../Playwright/win/Common.cpp | 0 .../Playwright/win/Common.h | 0 .../Playwright/win/DialogHelper.h | 0 .../Playwright/win/MainWindow.cpp | 0 .../Playwright/win/MainWindow.h | 0 .../Playwright/win/Playwright.ico | Bin .../Playwright/win/Playwright.rc | 0 .../Playwright/win/PlaywrightLib.rc | 0 .../Playwright/win/PlaywrightLibResource.h | 0 .../Playwright/win/PlaywrightReplace.h | 0 .../Playwright/win/PlaywrightResource.h | 0 .../Playwright/win/WebKitBrowserWindow.cpp | 0 .../Playwright/win/WebKitBrowserWindow.h | 0 .../Playwright/win/WinMain.cpp | 0 .../Playwright/win/resource.h | 0 .../Playwright/win/stdafx.cpp | 0 .../Playwright/win/stdafx.h | 0 .../Playwright/win/toolbar.bmp | Bin 69 files changed, 6346 insertions(+), 6404 deletions(-) create mode 100644 browser_patches/firefox/juggler/Helper.js create mode 100644 browser_patches/firefox/juggler/NetworkObserver.js create mode 100644 browser_patches/firefox/juggler/SimpleChannel.js create mode 100644 browser_patches/firefox/juggler/TargetRegistry.js create mode 100644 browser_patches/firefox/juggler/components/juggler.js create mode 100644 browser_patches/firefox/juggler/components/juggler.manifest create mode 100644 browser_patches/firefox/juggler/components/moz.build create mode 100644 browser_patches/firefox/juggler/content/FrameTree.js create mode 100644 browser_patches/firefox/juggler/content/NetworkMonitor.js create mode 100644 browser_patches/firefox/juggler/content/PageAgent.js create mode 100644 browser_patches/firefox/juggler/content/Runtime.js create mode 100644 browser_patches/firefox/juggler/content/ScrollbarManager.js create mode 100644 browser_patches/firefox/juggler/content/WorkerMain.js create mode 100644 browser_patches/firefox/juggler/content/floating-scrollbars.css create mode 100644 browser_patches/firefox/juggler/content/hidden-scrollbars.css create mode 100644 browser_patches/firefox/juggler/content/main.js create mode 100644 browser_patches/firefox/juggler/jar.mn create mode 100644 browser_patches/firefox/juggler/moz.build create mode 100644 browser_patches/firefox/juggler/protocol/AccessibilityHandler.js create mode 100644 browser_patches/firefox/juggler/protocol/BrowserHandler.js create mode 100644 browser_patches/firefox/juggler/protocol/Dispatcher.js create mode 100644 browser_patches/firefox/juggler/protocol/NetworkHandler.js create mode 100644 browser_patches/firefox/juggler/protocol/PageHandler.js create mode 100644 browser_patches/firefox/juggler/protocol/PrimitiveTypes.js create mode 100644 browser_patches/firefox/juggler/protocol/Protocol.js create mode 100644 browser_patches/firefox/juggler/protocol/RuntimeHandler.js rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Configurations/Base.xcconfig (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Configurations/DebugRelease.xcconfig (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Configurations/Playwright.xcconfig (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Configurations/SDKVariant.xcconfig (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/MBToolbarItem.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/MBToolbarItem.m (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Makefile (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Playwright.xcodeproj/project.pbxproj (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/Playwright.xcodeproj/xcshareddata/xcschemes/Playwright.xcscheme (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/AppDelegate.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/AppDelegate.m (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/BrowserWindow.xib (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/BrowserWindowController.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/BrowserWindowController.m (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/CMakeLists.txt (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/Info.plist (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/MainMenu.xib (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/Playwright_Prefix.pch (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/mac/main.m (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/CMakeLists.txt (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/Common.cpp (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/Common.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/DialogHelper.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/MainWindow.cpp (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/MainWindow.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/Playwright.ico (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/Playwright.rc (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/PlaywrightLib.rc (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/PlaywrightLibResource.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/PlaywrightReplace.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/PlaywrightResource.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/WebKitBrowserWindow.cpp (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/WebKitBrowserWindow.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/WinMain.cpp (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/resource.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/stdafx.cpp (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/stdafx.h (100%) rename browser_patches/webkit/{src/Tools => embedder}/Playwright/win/toolbar.bmp (100%) diff --git a/browser_patches/export.sh b/browser_patches/export.sh index 3448b03fd5..4f014344fe 100755 --- a/browser_patches/export.sh +++ b/browser_patches/export.sh @@ -38,15 +38,21 @@ FRIENDLY_CHECKOUT_PATH=""; BUILD_NUMBER_UPSTREAM_URL="" CHECKOUT_PATH="" EXPORT_PATH="" +EXTRA_FOLDER_PW_PATH="" +EXTRA_FOLDER_CHECKOUT_RELPATH="" if [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; CHECKOUT_PATH="$PWD/firefox/checkout" + EXTRA_FOLDER_PW_PATH="$PWD/firefox/juggler" + EXTRA_FOLDER_CHECKOUT_RELPATH="juggler" EXPORT_PATH="$PWD/firefox" BUILD_NUMBER_UPSTREAM_URL="https://raw.githubusercontent.com/microsoft/playwright/master/browser_patches/firefox/BUILD_NUMBER" source "./firefox/UPSTREAM_CONFIG.sh" elif [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; CHECKOUT_PATH="$PWD/webkit/checkout" + EXTRA_FOLDER_PW_PATH="$PWD/webkit/embedder/Playwright" + EXTRA_FOLDER_CHECKOUT_RELPATH="Tools/Playwright" EXPORT_PATH="$PWD/webkit" BUILD_NUMBER_UPSTREAM_URL="https://raw.githubusercontent.com/microsoft/playwright/master/browser_patches/webkit/BUILD_NUMBER" source "./webkit/UPSTREAM_CONFIG.sh" @@ -110,7 +116,7 @@ fi CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) NEW_BASE_REVISION=$(git merge-base $REMOTE_BROWSER_UPSTREAM/$BASE_BRANCH $CURRENT_BRANCH) -NEW_DIFF=$(git diff --diff-algorithm=myers --full-index $NEW_BASE_REVISION $CURRENT_BRANCH -- . ":!Tools/Playwright") +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}) @@ -122,12 +128,10 @@ BASE_REVISION=\"$NEW_BASE_REVISION\"" > $EXPORT_PATH/UPSTREAM_CONFIG.sh echo "$NEW_DIFF" > $EXPORT_PATH/patches/$PATCH_NAME echo $BUILD_NUMBER > $EXPORT_PATH/BUILD_NUMBER -if [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then -echo "-- patching WebKit embedders" -rm -rf $EXPORT_PATH/src/* -mkdir $EXPORT_PATH/src/Tools -cp -r Tools/Playwright $EXPORT_PATH/src/Tools/ -fi +echo "-- exporting standalone folder" +rm -rf "${EXTRA_FOLDER_PW_PATH}" +mkdir -p $(dirname "${EXTRA_FOLDER_PW_PATH}") +cp -r "${EXTRA_FOLDER_CHECKOUT_RELPATH}" "${EXTRA_FOLDER_PW_PATH}" NEW_BASE_REVISION_TEXT="$NEW_BASE_REVISION (not changed)" if [[ "$NEW_BASE_REVISION" != "$BASE_REVISION" ]]; then diff --git a/browser_patches/firefox/BUILD_NUMBER b/browser_patches/firefox/BUILD_NUMBER index d878b321af..4f1e6aa1b1 100644 --- a/browser_patches/firefox/BUILD_NUMBER +++ b/browser_patches/firefox/BUILD_NUMBER @@ -1 +1 @@ -1100 +1101 diff --git a/browser_patches/firefox/juggler/Helper.js b/browser_patches/firefox/juggler/Helper.js new file mode 100644 index 0000000000..c26cb6ec57 --- /dev/null +++ b/browser_patches/firefox/juggler/Helper.js @@ -0,0 +1,119 @@ +/* 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); + } + + 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 ''; + } +} + +var EXPORTED_SYMBOLS = [ "Helper" ]; +this.Helper = Helper; + diff --git a/browser_patches/firefox/juggler/NetworkObserver.js b/browser_patches/firefox/juggler/NetworkObserver.js new file mode 100644 index 0000000000..73a7658f52 --- /dev/null +++ b/browser_patches/firefox/juggler/NetworkObserver.js @@ -0,0 +1,800 @@ +/* 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 {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js"); + + +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 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(networkObserver, target) { + let result = target[pageNetworkSymbol]; + if (!result) { + result = new PageNetwork(networkObserver, target); + target[pageNetworkSymbol] = result; + } + return result; + } + + constructor(networkObserver, target) { + EventEmitter.decorate(this); + this._networkObserver = networkObserver; + this._target = target; + this._sessionCount = 0; + this._extraHTTPHeaders = null; + this._responseStorage = null; + this._requestInterceptionEnabled = false; + this._requestIdToInterceptor = null; + } + + addSession() { + if (this._sessionCount === 0) { + this._responseStorage = new ResponseStorage(this._networkObserver, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10); + } + ++this._sessionCount; + return () => this._stopTracking(); + } + + _stopTracking() { + --this._sessionCount; + if (this._sessionCount === 0) { + this._extraHTTPHeaders = null; + this._responseStorage = null; + this._requestInterceptionEnabled = false; + this._requestIdToInterceptor = null; + } + } + + _isActive() { + return this._sessionCount > 0; + } + + setExtraHTTPHeaders(headers) { + this._extraHTTPHeaders = headers; + } + + enableRequestInterception() { + this._requestInterceptionEnabled = true; + } + + disableRequestInterception() { + this._requestInterceptionEnabled = false; + const interceptors = this._requestIdToInterceptor; + if (!interceptors) + return; + this._requestIdToInterceptor = null; + for (const interceptor of interceptors.values()) + interceptor._resume(); + } + + resumeInterceptedRequest(requestId, method, headers, postData) { + this._takeInterceptor(requestId)._resume(method, headers, postData); + } + + fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) { + this._takeInterceptor(requestId)._fulfill(status, statusText, headers, base64body); + } + + abortInterceptedRequest(requestId, errorCode) { + this._takeInterceptor(requestId)._abort(errorCode); + } + + getResponseBody(requestId) { + if (!this._responseStorage) + throw new Error('Responses are not tracked for the given browser'); + return this._responseStorage.getBase64EncodedResponse(requestId); + } + + _ensureInterceptors() { + if (!this._requestIdToInterceptor) + this._requestIdToInterceptor = new Map(); + return this._requestIdToInterceptor; + } + + _takeInterceptor(requestId) { + const interceptors = this._requestIdToInterceptor; + if (!interceptors) + throw new Error(`Request interception is not enabled`); + const interceptor = interceptors.get(requestId); + if (!interceptor) + throw new Error(`Cannot find request "${requestId}"`); + interceptors.delete(requestId); + return interceptor; + } +} + +class NetworkObserver { + static instance() { + return NetworkObserver._instance || null; + } + + constructor(targetRegistry) { + EventEmitter.decorate(this); + NetworkObserver._instance = this; + + this._targetRegistry = targetRegistry; + this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); + this._activityDistributor.addObserver(this); + + this._redirectMap = new Map(); // oldId => newId + this._resumedRequestIdToHeaders = new Map(); // requestId => { headers } + this._postResumeChannelIdToRequestId = new Map(); // post-resume channel id => pre-resume request id + this._pendingAuthentication = new Set(); // pre-auth id + this._postAuthChannelIdToRequestId = new Map(); // pre-auth id => post-auth id + this._bodyListeners = new Map(); // channel id => ResponseBodyListener. + + 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'), + ]; + } + + _requestAuthenticated(httpChannel) { + this._pendingAuthentication.add(httpChannel.channelId + ''); + } + + _requestIdBeforeAuthentication(httpChannel) { + const id = httpChannel.channelId + ''; + return this._postAuthChannelIdToRequestId.has(id) ? id : undefined; + } + + _requestId(httpChannel) { + const id = httpChannel.channelId + ''; + return this._postResumeChannelIdToRequestId.get(id) || this._postAuthChannelIdToRequestId.get(id) || id; + } + + _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); + const pageNetwork = this._pageNetworkForChannel(oldHttpChannel); + if (!pageNetwork) + return; + const oldRequestId = this._requestId(oldHttpChannel); + const newRequestId = this._requestId(newHttpChannel); + if (this._resumedRequestIdToHeaders.has(oldRequestId)) { + // When we call resetInterception on a request, we get a new "redirected" request for it. + const { method, headers, postData } = this._resumedRequestIdToHeaders.get(oldRequestId); + if (headers) { + // Apply new request headers from interception resume. + for (const header of requestHeaders(newChannel)) + newChannel.setRequestHeader(header.name, '', false /* merge */); + for (const header of headers) + newChannel.setRequestHeader(header.name, header.value, false /* merge */); + } + if (method) + newChannel.requestMethod = method; + if (postData && newChannel instanceof Ci.nsIUploadChannel) { + const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + synthesized.data = atob(postData); + newChannel.setUploadStream(synthesized, 'application/octet-stream', -1); + } + // Use the old request id for the new "redirected" request for protocol consistency. + this._resumedRequestIdToHeaders.delete(oldRequestId); + this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId); + } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) { + // Regular (non-internal) redirect. + this._redirectMap.set(newRequestId, oldRequestId); + } + } + + observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) { + if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION) + return; + if (!(channel instanceof Ci.nsIHttpChannel)) + return; + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const pageNetwork = this._pageNetworkForChannel(httpChannel); + if (!pageNetwork) + return; + if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) + return; + if (this._isResumedChannel(httpChannel)) + return; + if (this._requestIdBeforeAuthentication(httpChannel)) + return; + this._sendOnRequestFinished(pageNetwork, httpChannel); + } + + pageNetworkForTarget(target) { + return PageNetwork._forPageTarget(this, target); + } + + _pageNetworkForChannel(httpChannel) { + let loadContext = helper.getLoadContext(httpChannel); + if (!loadContext) + return; + const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement); + if (!target) + return; + const pageNetwork = PageNetwork._forPageTarget(this, target); + if (!pageNetwork._isActive()) + return; + return pageNetwork; + } + + _isResumedChannel(httpChannel) { + return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + ''); + } + + _onRequest(channel, topic) { + if (!(channel instanceof Ci.nsIHttpChannel)) + return; + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const pageNetwork = this._pageNetworkForChannel(httpChannel); + if (!pageNetwork) + return; + if (this._isResumedChannel(httpChannel)) { + // Ignore onRequest for resumed requests, but listen to their response. + new ResponseBodyListener(this, pageNetwork, httpChannel); + return; + } + // Convert pending auth bit into auth mapping. + const channelId = httpChannel.channelId + ''; + if (this._pendingAuthentication.has(channelId)) { + this._postAuthChannelIdToRequestId.set(channelId, channelId + '-auth'); + this._redirectMap.set(channelId + '-auth', channelId); + this._pendingAuthentication.delete(channelId); + const bodyListener = this._bodyListeners.get(channelId); + if (bodyListener) + bodyListener.dispose(); + } + const browserContext = pageNetwork._target.browserContext(); + if (browserContext) + this._appendExtraHTTPHeaders(httpChannel, browserContext.extraHTTPHeaders); + this._appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders); + const requestId = this._requestId(httpChannel); + const isRedirect = this._redirectMap.has(requestId); + const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork); + if (!interceptionEnabled) { + new NotificationCallbacks(this, pageNetwork, httpChannel, false); + this._sendOnRequest(httpChannel, false); + new ResponseBodyListener(this, pageNetwork, httpChannel); + } else if (isRedirect) { + // We pretend that redirect is interceptable in the protocol, although it's actually not + // and therefore we do not instantiate the interceptor. + // TODO: look into REDIRECT_MODE_MANUAL. + const interceptors = pageNetwork._ensureInterceptors(); + interceptors.set(requestId, { + _resume: () => {}, + _abort: () => {}, + _fulfill: () => {}, + }); + new NotificationCallbacks(this, pageNetwork, httpChannel, false); + this._sendOnRequest(httpChannel, true); + new ResponseBodyListener(this, pageNetwork, httpChannel); + } else { + const previousCallbacks = httpChannel.notificationCallbacks; + if (previousCallbacks instanceof Ci.nsIInterfaceRequestor) { + const interceptor = previousCallbacks.getInterface(Ci.nsINetworkInterceptController); + // We assume that interceptor is a service worker if there is one. + if (interceptor && interceptor.shouldPrepareForIntercept(httpChannel.URI, httpChannel)) { + new NotificationCallbacks(this, pageNetwork, httpChannel, false); + this._sendOnRequest(httpChannel, false); + new ResponseBodyListener(this, pageNetwork, httpChannel); + } else { + // We'll issue onRequest once it's intercepted. + new NotificationCallbacks(this, pageNetwork, httpChannel, true); + } + } else { + // We'll issue onRequest once it's intercepted. + new NotificationCallbacks(this, pageNetwork, httpChannel, true); + } + } + } + + _isInterceptionEnabledForPage(pageNetwork) { + if (pageNetwork._requestInterceptionEnabled) + return true; + const browserContext = pageNetwork._target.browserContext(); + if (browserContext && browserContext.requestInterceptionEnabled) + return true; + if (browserContext && browserContext.settings.onlineOverride === 'offline') + return true; + return false; + } + + _appendExtraHTTPHeaders(httpChannel, headers) { + if (!headers) + return; + for (const header of headers) + httpChannel.setRequestHeader(header.name, header.value, false /* merge */); + } + + _onIntercepted(httpChannel, interceptor) { + const pageNetwork = this._pageNetworkForChannel(httpChannel); + if (!pageNetwork) { + interceptor._resume(); + return; + } + const browserContext = pageNetwork._target.browserContext(); + if (browserContext && browserContext.settings.onlineOverride === 'offline') { + interceptor._abort(Cr.NS_ERROR_OFFLINE); + return; + } + + const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork); + this._sendOnRequest(httpChannel, !!interceptionEnabled); + if (interceptionEnabled) + pageNetwork._ensureInterceptors().set(this._requestId(httpChannel), interceptor); + else + interceptor._resume(); + } + + _sendOnRequest(httpChannel, isIntercepted) { + const pageNetwork = this._pageNetworkForChannel(httpChannel); + if (!pageNetwork) + return; + const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; + const internalCauseType = httpChannel.loadInfo ? httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; + const requestId = this._requestId(httpChannel); + const redirectedFrom = this._redirectMap.get(requestId); + this._redirectMap.delete(requestId); + pageNetwork.emit(PageNetwork.Events.Request, httpChannel, { + url: httpChannel.URI.spec, + isIntercepted, + requestId, + redirectedFrom, + postData: readRequestPostData(httpChannel), + headers: requestHeaders(httpChannel), + method: httpChannel.requestMethod, + navigationId: httpChannel.isMainDocumentChannel ? this._requestIdBeforeAuthentication(httpChannel) || this._requestId(httpChannel) : undefined, + cause: causeTypeToString(causeType), + internalCause: causeTypeToString(internalCauseType), + }); + } + + _sendOnRequestFinished(pageNetwork, httpChannel) { + pageNetwork.emit(PageNetwork.Events.RequestFinished, httpChannel, { + requestId: this._requestId(httpChannel), + }); + this._cleanupChannelState(httpChannel); + } + + _sendOnRequestFailed(pageNetwork, httpChannel, error) { + pageNetwork.emit(PageNetwork.Events.RequestFailed, httpChannel, { + requestId: this._requestId(httpChannel), + errorCode: helper.getNetworkErrorStatusText(error), + }); + this._cleanupChannelState(httpChannel); + } + + _cleanupChannelState(httpChannel) { + const id = httpChannel.channelId + ''; + this._postResumeChannelIdToRequestId.delete(id); + this._postAuthChannelIdToRequestId.delete(id); + } + + _onResponse(fromCache, httpChannel, topic) { + const pageNetwork = this._pageNetworkForChannel(httpChannel); + if (!pageNetwork) + return; + httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); + const headers = []; + httpChannel.visitResponseHeaders({ + visitHeader: (name, value) => headers.push({name, value}), + }); + + let remoteIPAddress = undefined; + let remotePort = undefined; + try { + remoteIPAddress = httpChannel.remoteAddress; + remotePort = httpChannel.remotePort; + } catch (e) { + // remoteAddress is not defined for cached requests. + } + pageNetwork.emit(PageNetwork.Events.Response, httpChannel, { + requestId: this._requestId(httpChannel), + securityDetails: getSecurityDetails(httpChannel), + fromCache, + headers, + remoteIPAddress, + remotePort, + status: httpChannel.responseStatus, + statusText: httpChannel.responseStatusText, + }); + } + + _onResponseFinished(pageNetwork, httpChannel, body) { + if (!pageNetwork._isActive()) + return; + pageNetwork._responseStorage.addResponseBody(httpChannel, body); + this._sendOnRequestFinished(pageNetwork, httpChannel); + } + + 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 text = undefined; + try { + text = NetUtil.readInputStreamToString(iStream, iStream.available()); + const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = 'UTF-8'; + text = converter.ConvertToUnicode(text); + } catch (err) { + text = undefined; + } + + // 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 text; +} + +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'; +} + +class ResponseStorage { + constructor(networkObserver, maxTotalSize, maxResponseSize) { + this._networkObserver = networkObserver; + this._totalSize = 0; + this._maxResponseSize = maxResponseSize; + this._maxTotalSize = maxTotalSize; + this._responses = new Map(); + } + + addResponseBody(httpChannel, body) { + if (body.length > this._maxResponseSize) { + this._responses.set(requestId, { + evicted: true, + body: '', + }); + return; + } + let encodings = []; + if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) { + const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); + encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + } + this._responses.set(this._networkObserver._requestId(httpChannel), {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 = CommonUtils.convertString(result, encoding, 'uncompressed'); + } + return {base64body: btoa(result)}; + } +} + +class ResponseBodyListener { + constructor(networkObserver, pageNetwork, httpChannel) { + this._networkObserver = networkObserver; + this._pageNetwork = pageNetwork; + this._httpChannel = httpChannel; + this._chunks = []; + this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]); + httpChannel.QueryInterface(Ci.nsITraceableChannel); + this.originalListener = httpChannel.setNewListener(this); + this._disposed = false; + this._networkObserver._bodyListeners.set(this._httpChannel.channelId + '', this); + } + + onDataAvailable(aRequest, aInputStream, aOffset, aCount) { + if (this._disposed) { + this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount); + return; + } + + 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._chunks.push(data); + + oStream.writeBytes(data, aCount); + this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); + } + + onStartRequest(aRequest) { + this.originalListener.onStartRequest(aRequest); + } + + onStopRequest(aRequest, aStatusCode) { + this.originalListener.onStopRequest(aRequest, aStatusCode); + if (this._disposed) + return; + + if (aStatusCode === 0) { + const body = this._chunks.join(''); + this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body); + } else { + this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, aStatusCode); + } + + delete this._chunks; + this.dispose(); + } + + dispose() { + this._disposed = true; + this._networkObserver._bodyListeners.delete(this._httpChannel.channelId + ''); + } +} + +class NotificationCallbacks { + constructor(networkObserver, pageNetwork, httpChannel, shouldIntercept) { + this._networkObserver = networkObserver; + this._pageNetwork = pageNetwork; + this._shouldIntercept = shouldIntercept; + this._httpChannel = httpChannel; + this._previousCallbacks = httpChannel.notificationCallbacks; + httpChannel.notificationCallbacks = this; + + const qis = [ + Ci.nsIAuthPrompt2, + Ci.nsIAuthPromptProvider, + Ci.nsIInterfaceRequestor, + ]; + if (shouldIntercept) + qis.push(Ci.nsINetworkInterceptController); + this.QueryInterface = ChromeUtils.generateQI(qis); + } + + getInterface(iid) { + if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider)) + return this; + if (this._shouldIntercept && 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; + } + + _forward(iid, method, args) { + if (!this._previousCallbacks) + return; + try { + const impl = this._previousCallbacks.getInterface(iid); + impl[method].apply(impl, args); + } catch (e) { + if (e.result != Cr.NS_ERROR_NO_INTERFACE) + throw e; + } + } + + // 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 browserContext = this._pageNetwork._target.browserContext(); + const credentials = browserContext ? browserContext.httpCredentials : undefined; + if (!credentials) + return false; + authInfo.username = credentials.username; + authInfo.password = credentials.password; + this._networkObserver._requestAuthenticated(this._httpChannel); + return true; + } + + // nsINetworkInterceptController + shouldPrepareForIntercept(aURI, channel) { + if (!(channel instanceof Ci.nsIHttpChannel)) + return false; + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + return httpChannel.channelId === this._httpChannel.channelId; + } + + // nsINetworkInterceptController + channelIntercepted(intercepted) { + this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel); + const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel); + this._networkObserver._onIntercepted(httpChannel, this); + } + + _resume(method, headers, postData) { + this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { method, headers, postData }); + this._intercepted.resetInterception(); + } + + _fulfill(status, statusText, headers, base64body) { + this._intercepted.synthesizeStatus(status, statusText); + for (const header of headers) + this._intercepted.synthesizeHeader(header.name, header.value); + const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); + const body = base64body ? atob(base64body) : ''; + synthesized.data = body; + this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false); + this._intercepted.finishSynthesizedResponse(); + this._pageNetwork.emit(PageNetwork.Events.Response, this._httpChannel, { + requestId: this._networkObserver._requestId(this._httpChannel), + securityDetails: null, + fromCache: false, + headers, + status, + statusText, + }); + this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body); + } + + _abort(errorCode) { + const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; + this._intercepted.cancelInterception(error); + this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, error); + } +} + +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/juggler/SimpleChannel.js b/browser_patches/firefox/juggler/SimpleChannel.js new file mode 100644 index 0000000000..f8dd26a577 --- /dev/null +++ b/browser_patches/firefox/juggler/SimpleChannel.js @@ -0,0 +1,134 @@ +/* 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.transport.sendMessage = obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj); + channel.transport.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.transport = { + sendMessage: null, + dispose: null, + }; + this._disposed = false; + } + + 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); + 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}); + }); + this.transport.sendMessage({requestId: id, methodName, params, namespace}); + return promise; + } + + async _onMessage(data) { + 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.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`}); + 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/juggler/TargetRegistry.js b/browser_patches/firefox/juggler/TargetRegistry.js new file mode 100644 index 0000000000..de19a686fc --- /dev/null +++ b/browser_patches/firefox/juggler/TargetRegistry.js @@ -0,0 +1,680 @@ +/* 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 {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js"); +const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js"); +const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js"); +const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js"); +const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.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(); + helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'); + } + + _onRequest(httpChannel, topic) { + let loadContext = helper.getLoadContext(httpChannel); + if (!loadContext) + return; + if (!loadContext.topFrameElement) + return; + const target = this._registry.targetForBrowser(loadContext.topFrameElement); + if (!target) + return; + target._channelIds.add(httpChannel.channelId); + } + + // + // nsIDownloadInterceptor implementation. + // + interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) { + let pageTarget = this._registry._browserBrowsingContextToTarget.get(browsingContext); + // New page downloads won't have browsing contex. + if (!pageTarget) + pageTarget = this._registry._targetForChannel(request); + 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(); + + // 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('crashed'); + target.dispose(); + this.emit(TargetRegistry.Events.TargetDestroyed, target); + } + }, 'oop-frameloader-crashed'); + + Services.mm.addMessageListener('juggler:content-ready', { + receiveMessage: message => { + const linkedBrowser = message.target; + if (this._browserToTarget.has(linkedBrowser)) + throw new Error(`Internal error: two targets per linkedBrowser`); + + let tab; + let gBrowser; + const windowsIt = Services.wm.getEnumerator('navigator:browser'); + while (windowsIt.hasMoreElements()) { + const window = windowsIt.getNext(); + // gBrowser is always created before tabs. If gBrowser is not + // initialized yet the browser belongs to another window. + if (!window.gBrowser) + continue; + tab = window.gBrowser.getTabForBrowser(linkedBrowser); + if (tab) { + gBrowser = window.gBrowser; + break; + } + } + if (!tab) + return; + + const { userContextId } = message.data; + const openerContext = 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); + } + const browserContext = this._userContextIdToBrowserContext.get(userContextId); + const target = new PageTarget(this, gBrowser, tab, linkedBrowser, browserContext, openerTarget); + + const sessions = []; + const readyData = { sessions, target }; + this.emit(TargetRegistry.Events.TargetCreated, readyData); + sessions.forEach(session => target._initSession(session)); + return { + scriptsToEvaluateOnNewDocument: browserContext ? browserContext.scriptsToEvaluateOnNewDocument : [], + bindings: browserContext ? browserContext.bindings : [], + settings: browserContext ? browserContext.settings : {}, + sessionIds: sessions.map(session => session.sessionId()), + }; + }, + }); + + const onTabOpenListener = event => { + const tab = event.target; + const userContextId = tab.userContextId; + const browserContext = this._userContextIdToBrowserContext.get(userContextId); + if (browserContext && browserContext.defaultViewportSize) + setViewportSizeForBrowser(browserContext.defaultViewportSize, tab.linkedBrowser); + }; + + const onTabCloseListener = event => { + const tab = event.target; + const linkedBrowser = tab.linkedBrowser; + const target = this._browserToTarget.get(linkedBrowser); + if (target) { + target.dispose(); + this.emit(TargetRegistry.Events.TargetDestroyed, target); + } + }; + + Services.wm.addListener({ + onOpenWindow: async window => { + const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); + if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) + return; + if (domWindow.document.readyState !== 'uninitialized') + throw new Error('DOMWindow should not be loaded yet'); + await new Promise(fulfill => { + domWindow.addEventListener('DOMContentLoaded', function listener() { + domWindow.removeEventListener('DOMContentLoaded', listener); + fulfill(); + }); + }); + if (!domWindow.gBrowser) + return; + domWindow.gBrowser.tabContainer.addEventListener('TabOpen', onTabOpenListener); + domWindow.gBrowser.tabContainer.addEventListener('TabClose', onTabCloseListener); + }, + onCloseWindow: window => { + const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); + if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) + return; + if (!domWindow.gBrowser) + return; + domWindow.gBrowser.tabContainer.removeEventListener('TabOpen', onTabOpenListener); + domWindow.gBrowser.tabContainer.removeEventListener('TabClose', onTabCloseListener); + 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)); + } + + defaultContext() { + return this._defaultContext; + } + + createBrowserContext(removeOnDetach) { + return new BrowserContext(this, helper.generateId(), removeOnDetach); + } + + browserContextForId(browserContextId) { + return this._browserContextIdToBrowserContext.get(browserContextId); + } + + async newPage({browserContextId}) { + let window; + let created = false; + const windowsIt = Services.wm.getEnumerator('navigator:browser'); + if (windowsIt.hasMoreElements()) { + window = windowsIt.getNext(); + } else { + const features = "chrome,dialog=no,all"; + const args = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + args.data = 'about:blank'; + window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args); + created = true; + } + if (window.document.readyState !== 'complete') { + await new Promise(fulfill => { + window.addEventListener('load', function listener() { + window.removeEventListener('load', listener); + fulfill(); + }); + }); + } + const browserContext = this.browserContextForId(browserContextId); + const tab = window.gBrowser.addTab('about:blank', { + userContextId: browserContext.userContextId, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + const target = await new Promise(fulfill => { + const listener = helper.on(this, TargetRegistry.Events.TargetCreated, ({target}) => { + if (target._tab === tab) { + helper.removeListeners([listener]); + fulfill(target); + } + }); + }); + if (created) { + window.gBrowser.removeTab(window.gBrowser.getTabForBrowser(window.gBrowser.getBrowserAtIndex(0)), { + skipPermitUnload: true, + }); + } + window.gBrowser.selectedTab = tab; + 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); + } + + _targetForChannel(channel) { + const channelId = channel.channelId; + for (const target of this._browserToTarget.values()) { + if (target._channelIds.has(channelId)) + return target; + } + return null; + } +} + +class PageTarget { + constructor(registry, gBrowser, tab, linkedBrowser, browserContext, opener) { + EventEmitter.decorate(this); + + this._targetId = helper.generateId(); + this._registry = registry; + this._gBrowser = gBrowser; + this._tab = tab; + this._linkedBrowser = linkedBrowser; + this._browserContext = browserContext; + this._viewportSize = undefined; + this._url = 'about:blank'; + this._openerId = opener ? opener.id() : undefined; + this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); + this._channelIds = new Set(); + + const navigationListener = { + QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), + onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), + }; + this._eventListeners = [ + helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), + ]; + + this._disposed = false; + if (browserContext) { + browserContext.pages.add(this); + browserContext._firstPageCallback(); + } + this._registry._browserToTarget.set(this._linkedBrowser, this); + this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this); + } + + linkedBrowser() { + return this._linkedBrowser; + } + + browserContext() { + return this._browserContext; + } + + async setViewportSize(viewportSize) { + this._viewportSize = viewportSize; + const actualSize = setViewportSizeForBrowser(viewportSize, this._linkedBrowser); + await this._channel.connect('').send('awaitViewportDimensions', { + width: actualSize.width, + height: actualSize.height + }); + } + + connectSession(session) { + this._initSession(session); + this._channel.connect('').send('attach', { sessionId: session.sessionId() }); + } + + disconnectSession(session) { + if (!this._disposed) + this._channel.connect('').emit('detach', { sessionId: session.sessionId() }); + } + + async close(runBeforeUnload = false) { + await this._gBrowser.removeTab(this._tab, { + skipPermitUnload: !runBeforeUnload, + }); + } + + _initSession(session) { + const pageHandler = new PageHandler(this, session, this._channel); + const networkHandler = new NetworkHandler(this, session, this._channel); + session.registerHandler('Page', pageHandler); + session.registerHandler('Network', networkHandler); + session.registerHandler('Runtime', new RuntimeHandler(session, this._channel)); + session.registerHandler('Accessibility', new AccessibilityHandler(session, this._channel)); + pageHandler.enable(); + networkHandler.enable(); + } + + id() { + return this._targetId; + } + + info() { + return { + targetId: this.id(), + type: 'page', + browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined, + 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); + } + + dispose() { + this._disposed = true; + if (this._browserContext) + this._browserContext.pages.delete(this); + this._registry._browserToTarget.delete(this._linkedBrowser); + this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext); + helper.removeListeners(this._eventListeners); + } +} + +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.removeOnDetach = removeOnDetach; + this.extraHTTPHeaders = undefined; + this.httpCredentials = undefined; + this.requestInterceptionEnabled = undefined; + this.ignoreHTTPSErrors = undefined; + this.downloadOptions = undefined; + this.defaultViewportSize = undefined; + this.scriptsToEvaluateOnNewDocument = []; + this.bindings = []; + this.settings = {}; + this.pages = new Set(); + this._firstPagePromise = new Promise(f => this._firstPageCallback = f); + } + + async destroy() { + if (this.userContextId !== 0) { + ContextualIdentityService.remove(this.userContextId); + ContextualIdentityService.closeContainerTabs(this.userContextId); + 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); + } + + 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 setDefaultViewport(viewport) { + this.defaultViewportSize = viewport ? viewport.viewportSize : undefined; + if (!this.userContextId) { + // First page in the default context comes before onTabOpenListener + // so we don't set default viewport. Wait for it here and ensure the viewport. + await this._firstPagePromise; + } + const promises = Array.from(this.pages).map(async page => { + // Resize to new default, unless the page has a custom viewport. + if (!page._viewportSize) + await page.setViewportSize(this.defaultViewportSize); + }); + await Promise.all([ + this.applySetting('deviceScaleFactor', viewport ? viewport.deviceScaleFactor : undefined), + ...promises, + ]); + } + + 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], + ); + } + } + + 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; + } +} + +function dirPath(path) { + return path.substring(0, path.lastIndexOf('/') + 1); +} + +function setViewportSizeForBrowser(viewportSize, browser) { + if (viewportSize) { + const {width, height} = viewportSize; + 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']; +this.TargetRegistry = TargetRegistry; diff --git a/browser_patches/firefox/juggler/components/juggler.js b/browser_patches/firefox/juggler/components/juggler.js new file mode 100644 index 0000000000..c0f6163d5f --- /dev/null +++ b/browser_patches/firefox/juggler/components/juggler.js @@ -0,0 +1,84 @@ +/* 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 {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); + if (!jugglerFlag || isNaN(jugglerFlag)) + return; + const port = parseInt(jugglerFlag, 10); + const silent = cmdLine.preventDefault; + if (silent) + Services.startup.enterLastWindowClosingSurvivalArea(); + + const targetRegistry = new TargetRegistry(); + new NetworkObserver(targetRegistry); + + 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(); + + // Force create hidden window here, otherwise its creation later closes the web socket! + Services.appShell.hiddenDOMWindow; + + 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().registerHandler('Browser', browserHandler); + } + }); + + Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */); + dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\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 = XPCOMUtils.generateNSGetFactory([CommandLineHandler]); diff --git a/browser_patches/firefox/juggler/components/juggler.manifest b/browser_patches/firefox/juggler/components/juggler.manifest new file mode 100644 index 0000000000..50f8930207 --- /dev/null +++ b/browser_patches/firefox/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/juggler/components/moz.build b/browser_patches/firefox/juggler/components/moz.build new file mode 100644 index 0000000000..268fbc361d --- /dev/null +++ b/browser_patches/firefox/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/juggler/content/FrameTree.js b/browser_patches/firefox/juggler/content/FrameTree.js new file mode 100644 index 0000000000..ff00af51c2 --- /dev/null +++ b/browser_patches/firefox/juggler/content/FrameTree.js @@ -0,0 +1,477 @@ +/* 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._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(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; + 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); + } + + setColorScheme(colorScheme) { + const docShell = this._mainFrame._docShell; + switch (colorScheme) { + case 'light': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_LIGHT; break; + case 'dark': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_DARK; break; + case 'no-preference': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NO_PREFERENCE; break; + default: docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NONE; break; + } + } + + 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; + } + + const isStart = flag & Ci.nsIWebProgressListener.STATE_START; + const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; + const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; + + let isDownload = false; + try { + isDownload = (channel.contentDisposition === Ci.nsIChannel.DISPOSITION_ATTACHMENT); + } catch(e) { + // The method is expected to throw if it's not an attachment. + } + + if (isStart) { + // Starting a new navigation. + frame._pendingNavigationId = this._channelId(channel); + frame._pendingNavigationURL = channel.URI.spec; + this.emit(FrameTree.Events.NavigationStarted, frame); + } else if (isTransferring || (isStop && frame._pendingNavigationId && !status && !isDownload)) { + // 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 || isDownload)) { + // 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 = isDownload ? 'Will download to file' : helper.getNetworkErrorStatusText(status); + this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText); + if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED && !isDownload) + this.forcePageReady(); + } + } + + 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); + } + } + + _channelId(channel) { + if (channel instanceof Ci.nsIHttpChannel) { + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + return String(httpChannel.channelId); + } + return helper.generateId(); + } + + _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', + NavigationStarted: 'navigationstarted', + NavigationCommitted: 'navigationcommitted', + NavigationAborted: 'navigationaborted', + SameDocumentNavigation: 'samedocumentnavigation', + PageReady: 'pageready', +}; + +class Frame { + constructor(frameTree, runtime, docShell, parentFrame) { + this._frameTree = frameTree; + this._runtime = runtime; + this._docShell = docShell; + this._children = new Set(); + this._frameId = helper.generateId(); + 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; + } + + 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() { + 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.transport = { + 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); + } +} + +var EXPORTED_SYMBOLS = ['FrameTree']; +this.FrameTree = FrameTree; + diff --git a/browser_patches/firefox/juggler/content/NetworkMonitor.js b/browser_patches/firefox/juggler/content/NetworkMonitor.js new file mode 100644 index 0000000000..13b76cd8a5 --- /dev/null +++ b/browser_patches/firefox/juggler/content/NetworkMonitor.js @@ -0,0 +1,52 @@ +/* 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 helper = new Helper(); + +class NetworkMonitor { + constructor(rootDocShell, frameTree) { + this._frameTree = frameTree; + this._requestDetails = new Map(); + + this._eventListeners = [ + helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'), + ]; + } + + _onRequest(channel) { + if (!(channel instanceof Ci.nsIHttpChannel)) + return; + const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + const loadContext = helper.getLoadContext(httpChannel); + if (!loadContext) + return; + const window = loadContext.associatedWindow; + const frame = this._frameTree.frameForDocShell(window.docShell); + if (!frame) + return; + this._requestDetails.set(httpChannel.channelId, { + frameId: frame.id(), + }); + } + + requestDetails(channelId) { + return this._requestDetails.get(channelId) || null; + } + + dispose() { + this._requestDetails.clear(); + helper.removeListeners(this._eventListeners); + } +} + +var EXPORTED_SYMBOLS = ['NetworkMonitor']; +this.NetworkMonitor = NetworkMonitor; + diff --git a/browser_patches/firefox/juggler/content/PageAgent.js b/browser_patches/firefox/juggler/content/PageAgent.js new file mode 100644 index 0000000000..8e312516d4 --- /dev/null +++ b/browser_patches/firefox/juggler/content/PageAgent.js @@ -0,0 +1,981 @@ +/* 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, sessionId, worker) { + this._workerRuntime = worker.channel().connect(sessionId + 'runtime'); + this._browserWorker = browserChannel.connect(sessionId + worker.id()); + this._worker = worker; + this._sessionId = sessionId; + const emit = name => { + return (...args) => this._browserWorker.emit(name, ...args); + }; + this._eventListeners = [ + worker.channel().register(sessionId + 'runtime', { + runtimeConsole: emit('runtimeConsole'), + runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'), + runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'), + }), + browserChannel.register(sessionId + 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), + }), + ]; + worker.channel().connect('').emit('attach', {sessionId}); + } + + dispose() { + this._worker.channel().connect('').emit('detach', {sessionId: this._sessionId}); + 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.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, sessionId, frameTree, networkMonitor) { + this._messageManager = messageManager; + this._browserChannel = browserChannel; + this._sessionId = sessionId; + this._browserPage = browserChannel.connect(sessionId + 'page'); + this._browserRuntime = browserChannel.connect(sessionId + 'runtime'); + this._frameTree = frameTree; + this._runtime = frameTree.runtime(); + this._networkMonitor = networkMonitor; + + this._frameData = new Map(); + this._workerData = new Map(); + this._scriptsToEvaluateOnNewDocument = new Map(); + this._isolatedWorlds = new Map(); + + this._eventListeners = [ + browserChannel.register(sessionId + '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), + getBoundingBox: this._getBoundingBox.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), + requestDetails: this._requestDetails.bind(this), + screenshot: this._screenshot.bind(this), + scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this), + setCacheDisabled: this._setCacheDisabled.bind(this), + setEmulatedMedia: this._setEmulatedMedia.bind(this), + setFileInputFiles: this._setFileInputFiles.bind(this), + setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this), + }), + browserChannel.register(sessionId + '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), + }), + ]; + this._enabled = false; + + const docShell = frameTree.mainFrame().docShell(); + this._docShell = docShell; + this._initialDPPX = docShell.contentViewer.overrideDPPX; + this._customScrollbars = null; + this._dataTransfer = null; + } + + _requestDetails({channelId}) { + return this._networkMonitor.requestDetails(channelId); + } + + async _setEmulatedMedia({type, colorScheme}) { + const docShell = this._frameTree.mainFrame().docShell(); + const cv = docShell.contentViewer; + if (type === '') + cv.stopEmulatingMedium(); + else if (type) + cv.emulateMedium(type); + this._frameTree.setColorScheme(colorScheme); + } + + _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; + } + + enable() { + if (this._enabled) + return; + + this._enabled = true; + // 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); + } + + for (const worker of this._frameTree.workers()) + this._onWorkerCreated(worker); + + this._eventListeners.push(...[ + 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.addEventListener(this._messageManager, 'pageshow', this._onLoad.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, '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.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._browserRuntime.emit('runtimeConsole', msg)), + this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), + this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), + ]); + for (const context of this._runtime.executionContexts()) + this._onExecutionContextCreated(context); + + if (this._frameTree.isPageReady()) + this._browserPage.emit('pageReady', {}); + } + + _onExecutionContextCreated(executionContext) { + this._browserRuntime.emit('runtimeExecutionContextCreated', { + executionContextId: executionContext.id(), + auxData: executionContext.auxData(), + }); + } + + _onExecutionContextDestroyed(executionContext) { + this._browserRuntime.emit('runtimeExecutionContextDestroyed', { + executionContextId: executionContext.id(), + }); + } + + _onWorkerCreated(worker) { + const workerData = new WorkerData(this, this._browserChannel, this._sessionId, 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._browserPage.emit('pageEventFired', { + frameId: this._frameTree.mainFrame().id(), + name: 'DOMContentLoaded', + }); + this._browserPage.emit('pageEventFired', { + frameId: this._frameTree.mainFrame().id(), + name: 'load', + }); + } + } + + _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) { + 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 ? 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(event) { + const docShell = event.target.ownerGlobal.docShell; + const frame = this._frameTree.frameForDocShell(docShell); + if (!frame) + return; + 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, + }); + } + + _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(), + }); + } + + _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); + return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; + } + + async _goBack({frameId, url}) { + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell(); + if (!docShell.canGoBack) + return {navigationId: null, navigationURL: null}; + docShell.goBack(); + return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; + } + + async _goForward({frameId, url}) { + const frame = this._frameTree.frame(frameId); + const docShell = frame.docShell(); + if (!docShell.canGoForward) + return {navigationId: null, navigationURL: null}; + docShell.goForward(); + return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; + } + + 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 type does not support scrollRectIntoViewIfNeeded'); + } + + _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 _getBoundingBox({frameId, objectId}) { + 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 box = this._getNodeBoundingBox(unsafeObject); + if (!box) + return {boundingBox: null}; + return {boundingBox: {x: box.x + frame.domWindow().scrollX, y: box.y + frame.domWindow().scrollY, width: box.width, height: box.height}}; + } + + async _screenshot({mimeType, fullPage, clip}) { + const content = this._messageManager.content; + if (clip) { + const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType); + return {data}; + } + if (fullPage) { + const rect = content.document.documentElement.getBoundingClientRect(); + const width = content.innerWidth + content.scrollMaxX - content.scrollMinX; + const height = content.innerHeight + content.scrollMaxY - content.scrollMinY; + const data = takeScreenshot(content, 0, 0, width, height, mimeType); + return {data}; + } + const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType); + return {data}; + } + + async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) { + // key events don't fire if we are dragging. + if (this._dataTransfer) { + 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}; + } + + _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 */, + this._dataTransfer + ); + + window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event); + if (type === 'drop') + dragService.endDragSession(true); + } + + _cancelDragIfNeeded() { + this._dataTransfer = null; + const sess = dragService.getCurrentSession(); + if (sess) + dragService.endDragSession(false); + } + + async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) { + this._startDragSessionIfNeeded(); + const trapDrag = subject => { + this._dataTransfer = subject.mozCloneForEvent('drop'); + } + + 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._dataTransfer) { + 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) { + const MAX_SKIA_DIMENSIONS = 32767; + + const scale = 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/juggler/content/Runtime.js b/browser_patches/firefox/juggler/content/Runtime.js new file mode 100644 index 0000000000..2a3feb5336 --- /dev/null +++ b/browser_patches/firefox/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.flags & Ci.nsIScriptError.exceptionFlag)) { + 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.windowUtils.currentInnerWindowID === 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/juggler/content/ScrollbarManager.js b/browser_patches/firefox/juggler/content/ScrollbarManager.js new file mode 100644 index 0000000000..9933f0c87b --- /dev/null +++ b/browser_patches/firefox/juggler/content/ScrollbarManager.js @@ -0,0 +1,89 @@ +/* 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 Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +const Cc = Components.classes; + +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css'); +const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css'); + +const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; +const helper = new Helper(); + +class ScrollbarManager { + constructor(docShell) { + this._docShell = docShell; + this._customScrollbars = null; + this._contentViewerScrollBars = new Map(); + + if (isHeadless) + this._setCustomScrollbars(HIDDEN_SCROLLBARS); + + const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + + this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']); + this._eventListeners = [ + helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL), + ]; + } + + onLocationChange(webProgress, request, URI, flags) { + if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + return; + this._updateAllDocShells(); + } + + setFloatingScrollbars(enabled) { + if (this._customScrollbars === HIDDEN_SCROLLBARS) + return; + this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null); + } + + _setCustomScrollbars(customScrollbars) { + if (this._customScrollbars === customScrollbars) + return; + this._customScrollbars = customScrollbars; + this._updateAllDocShells(); + } + + _updateAllDocShells() { + const allDocShells = [this._docShell]; + for (let i = 0; i < this._docShell.childCount; i++) + allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell)); + // At this point, a content viewer might not be loaded for certain docShells. + // Scrollbars will be updated in onLocationChange. + const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer); + + // Update scrollbar stylesheets. + for (const contentViewer of contentViewers) { + const oldScrollbars = this._contentViewerScrollBars.get(contentViewer); + if (oldScrollbars === this._customScrollbars) + continue; + const winUtils = contentViewer.DOMDocument.defaultView.windowUtils; + if (oldScrollbars) + winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET); + if (this._customScrollbars) + winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET); + } + // Update state for all *existing* docShells. + this._contentViewerScrollBars.clear(); + for (const contentViewer of contentViewers) + this._contentViewerScrollBars.set(contentViewer, this._customScrollbars); + } + + dispose() { + this._setCustomScrollbars(null); + helper.removeListeners(this._eventListeners); + } +} + +var EXPORTED_SYMBOLS = ['ScrollbarManager']; +this.ScrollbarManager = ScrollbarManager; + diff --git a/browser_patches/firefox/juggler/content/WorkerMain.js b/browser_patches/firefox/juggler/content/WorkerMain.js new file mode 100644 index 0000000000..975c8e1d39 --- /dev/null +++ b/browser_patches/firefox/juggler/content/WorkerMain.js @@ -0,0 +1,87 @@ +/* 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 runtimeAgents = new Map(); + +const channel = new SimpleChannel('worker::worker'); +const eventListener = event => channel._onMessage(JSON.parse(event.data)); +this.addEventListener('message', eventListener); +channel.transport = { + 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, sessionId) { + this._runtime = runtime; + this._browserRuntime = channel.connect(sessionId + 'runtime'); + this._eventListeners = [ + channel.register(sessionId + '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), + }), + 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)), + ]; + for (const context of this._runtime.executionContexts()) + this._onExecutionContextCreated(context); + } + + _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 = []; + } +} + +channel.register('', { + attach: ({sessionId}) => { + const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId); + runtimeAgents.set(sessionId, runtimeAgent); + }, + + detach: ({sessionId}) => { + const runtimeAgent = runtimeAgents.get(sessionId); + runtimeAgents.delete(sessionId); + runtimeAgent.dispose(); + }, +}); + diff --git a/browser_patches/firefox/juggler/content/floating-scrollbars.css b/browser_patches/firefox/juggler/content/floating-scrollbars.css new file mode 100644 index 0000000000..6df3163f57 --- /dev/null +++ b/browser_patches/firefox/juggler/content/floating-scrollbars.css @@ -0,0 +1,51 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars + inside a . */ +*|*:not(html|select) > scrollbar { + -moz-appearance: none !important; + position: relative; + background-color: transparent; + background-image: none; + z-index: 2147483647; + padding: 2px; + border: none; +} + +/* Scrollbar code will reset the margin to the correct side depending on + where layout actually puts the scrollbar */ +*|*:not(html|select) > scrollbar[orient="vertical"] { + margin-left: -10px; + min-width: 10px; + max-width: 10px; +} + +*|*:not(html|select) > scrollbar[orient="horizontal"] { + margin-top: -10px; + min-height: 10px; + max-height: 10px; +} + +*|*:not(html|select) > scrollbar slider { + -moz-appearance: none !important; +} + +*|*:not(html|select) > scrollbar thumb { + -moz-appearance: none !important; + background-color: rgba(0,0,0,0.2); + border-width: 0px !important; + border-radius: 3px !important; +} + +*|*:not(html|select) > scrollbar scrollbarbutton, +*|*:not(html|select) > scrollbar gripper { + display: none; +} diff --git a/browser_patches/firefox/juggler/content/hidden-scrollbars.css b/browser_patches/firefox/juggler/content/hidden-scrollbars.css new file mode 100644 index 0000000000..7363d7603e --- /dev/null +++ b/browser_patches/firefox/juggler/content/hidden-scrollbars.css @@ -0,0 +1,17 @@ +/* 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/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); + +/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars + inside a . */ +*|*:not(html|select) > scrollbar { + -moz-appearance: none !important; + display: none; +} + diff --git a/browser_patches/firefox/juggler/content/main.js b/browser_patches/firefox/juggler/content/main.js new file mode 100644 index 0000000000..2379ad978f --- /dev/null +++ b/browser_patches/firefox/juggler/content/main.js @@ -0,0 +1,192 @@ +/* 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 {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js'); +const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); +const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); + +const scrollbarManager = new ScrollbarManager(docShell); +let frameTree; +let networkMonitor; +const helper = new Helper(); +const messageManager = this; + +const sessions = new Map(); + +function createContentSession(channel, sessionId) { + const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree, networkMonitor); + sessions.set(sessionId, [pageAgent]); + pageAgent.enable(); +} + +function disposeContentSession(sessionId) { + const handlers = sessions.get(sessionId); + sessions.delete(sessionId); + for (const handler of handlers) + handler.dispose(); +} + +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; + }, + + userAgent: (userAgent) => { + docShell.browsingContext.customUserAgent = userAgent; + }, + + bypassCSP: (bypassCSP) => { + docShell.bypassCSPEnabled = bypassCSP; + }, + + timezoneId: (timezoneId) => { + failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId); + }, + + locale: (locale) => { + docShell.languageOverride = locale; + }, + + javaScriptDisabled: (javaScriptDisabled) => { + docShell.allowJavascript = !javaScriptDisabled; + }, + + hasTouch: (hasTouch) => { + docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; + }, + + colorScheme: (colorScheme) => { + frameTree.setColorScheme(colorScheme); + }, + + deviceScaleFactor: (deviceScaleFactor) => { + docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX; + docShell.deviceSizeIsPageSize = !!deviceScaleFactor; + }, +}; + +function initialize() { + const loadContext = docShell.QueryInterface(Ci.nsILoadContext); + const userContextId = loadContext.originAttributes.userContextId; + + const response = sendSyncMessage('juggler:content-ready', { userContextId })[0]; + const { + sessionIds = [], + scriptsToEvaluateOnNewDocument = [], + bindings = [], + settings = {} + } = response || {}; + + // Enforce focused state for all top level documents. + docShell.overrideHasFocus = 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); + networkMonitor = new NetworkMonitor(docShell, frameTree); + + const channel = SimpleChannel.createForMessageManager('content::page', messageManager); + + for (const sessionId of sessionIds) + createContentSession(channel, sessionId); + + channel.register('', { + attach({sessionId}) { + createContentSession(channel, sessionId); + }, + + detach({sessionId}) { + disposeContentSession(sessionId); + }, + + 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}) { + 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); + channel.dispose(); + + for (const sessionId of sessions.keys()) + disposeContentSession(sessionId); + + scrollbarManager.dispose(); + networkMonitor.dispose(); + frameTree.dispose(); + }), + ]; +} + +initialize(); diff --git a/browser_patches/firefox/juggler/jar.mn b/browser_patches/firefox/juggler/jar.mn new file mode 100644 index 0000000000..ec78981943 --- /dev/null +++ b/browser_patches/firefox/juggler/jar.mn @@ -0,0 +1,28 @@ +# 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/RuntimeHandler.js (protocol/RuntimeHandler.js) + content/protocol/NetworkHandler.js (protocol/NetworkHandler.js) + content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) + content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js) + content/content/main.js (content/main.js) + content/content/FrameTree.js (content/FrameTree.js) + content/content/NetworkMonitor.js (content/NetworkMonitor.js) + content/content/PageAgent.js (content/PageAgent.js) + content/content/Runtime.js (content/Runtime.js) + content/content/WorkerMain.js (content/WorkerMain.js) + content/content/ScrollbarManager.js (content/ScrollbarManager.js) + content/content/floating-scrollbars.css (content/floating-scrollbars.css) + content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) + diff --git a/browser_patches/firefox/juggler/moz.build b/browser_patches/firefox/juggler/moz.build new file mode 100644 index 0000000000..1a0a3130bf --- /dev/null +++ b/browser_patches/firefox/juggler/moz.build @@ -0,0 +1,15 @@ +# 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"] + +JAR_MANIFESTS += ["jar.mn"] +#JS_PREFERENCE_FILES += ["prefs/marionette.js"] + +#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"] +#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +with Files("**"): + BUG_COMPONENT = ("Testing", "Juggler") + diff --git a/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js b/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js new file mode 100644 index 0000000000..dee87b10b2 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/AccessibilityHandler.js @@ -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/. */ + +class AccessibilityHandler { + constructor(session, contentChannel) { + this._contentPage = contentChannel.connect(session.sessionId() + 'page'); + } + + async getFullAXTree(params) { + return await this._contentPage.send('getFullAXTree', params); + } + + dispose() { + this._contentPage.dispose(); + } +} + +var EXPORTED_SYMBOLS = ['AccessibilityHandler']; +this.AccessibilityHandler = AccessibilityHandler; diff --git a/browser_patches/firefox/juggler/protocol/BrowserHandler.js b/browser_patches/firefox/juggler/protocol/BrowserHandler.js new file mode 100644 index 0000000000..bb873638f9 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/BrowserHandler.js @@ -0,0 +1,243 @@ +/* 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 {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js"); +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); + +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 enable({attachToDefaultContext}) { + if (this._enabled) + return; + this._enabled = true; + this._attachToDefaultContext = attachToDefaultContext; + + for (const target of this._targetRegistry.targets()) { + if (!this._shouldAttachToTarget(target)) + continue; + const session = this._dispatcher.createSession(); + target.connectSession(session); + this._attachedSessions.set(target, session); + this._session.emitEvent('Browser.attachedToTarget', { + sessionId: session.sessionId(), + targetInfo: target.info() + }); + } + + 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)), + ]; + } + + async 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 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) { + target.disconnectSession(session); + 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 (!target._browserContext) + return false; + if (this._createdBrowserContextIds.has(target._browserContext.browserContextId)) + return true; + return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext(); + } + + _onTargetCreated({sessions, target}) { + if (!this._shouldAttachToTarget(target)) + return; + const session = this._dispatcher.createSession(); + this._attachedSessions.set(target, session); + this._session.emitEvent('Browser.attachedToTarget', { + sessionId: session.sessionId(), + targetInfo: target.info() + }); + sessions.push(session); + } + + _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 newPage({browserContextId}) { + const targetId = await this._targetRegistry.newPage({browserContextId}); + return {targetId}; + } + + async close() { + this._onclose(); + let browserWindow = Services.wm.getMostRecentWindow( + "navigator:browser" + ); + if (browserWindow && browserWindow.gBrowserInit) { + await browserWindow.gBrowserInit.idleTasksFinishedPromise; + } + Services.startup.quit(Ci.nsIAppStartup.eForceQuit); + } + + async grantPermissions({browserContextId, origin, permissions}) { + await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions); + } + + resetPermissions({browserContextId}) { + this._targetRegistry.browserContextForId(browserContextId).resetPermissions(); + } + + setExtraHTTPHeaders({browserContextId, headers}) { + this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers; + } + + setHTTPCredentials({browserContextId, credentials}) { + this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials); + } + + setRequestInterception({browserContextId, enabled}) { + this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled; + } + + setIgnoreHTTPSErrors({browserContextId, ignoreHTTPSErrors}) { + this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors)); + } + + setDownloadOptions({browserContextId, downloadOptions}) { + this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions); + } + + async setGeolocationOverride({browserContextId, geolocation}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation)); + } + + async setOnlineOverride({browserContextId, override}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override)); + } + + async setColorScheme({browserContextId, colorScheme}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('colorScheme', nullToUndefined(colorScheme)); + } + + async setUserAgentOverride({browserContextId, userAgent}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('userAgent', nullToUndefined(userAgent)); + } + + async setBypassCSP({browserContextId, bypassCSP}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP)); + } + + async setJavaScriptDisabled({browserContextId, javaScriptDisabled}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled)); + } + + async setLocaleOverride({browserContextId, locale}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale)); + } + + async setTimezoneOverride({browserContextId, timezoneId}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId)); + } + + async setTouchOverride({browserContextId, hasTouch}) { + await this._targetRegistry.browserContextForId(browserContextId).applySetting('hasTouch', nullToUndefined(hasTouch)); + } + + async setDefaultViewport({browserContextId, viewport}) { + await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport)); + } + + async addScriptToEvaluateOnNewDocument({browserContextId, script}) { + await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script); + } + + async addBinding({browserContextId, name, script}) { + await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script); + } + + setCookies({browserContextId, cookies}) { + this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies); + } + + clearCookies({browserContextId}) { + this._targetRegistry.browserContextForId(browserContextId).clearCookies(); + } + + getCookies({browserContextId}) { + const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies(); + return {cookies}; + } + + async getInfo() { + const version = Components.classes["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULAppInfo) + .version; + const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] + .getService(Components.interfaces.nsIHttpProtocolHandler) + .userAgent; + return {version: 'Firefox/' + version, userAgent}; + } +} + +function nullToUndefined(value) { + return value === null ? undefined : value; +} + +var EXPORTED_SYMBOLS = ['BrowserHandler']; +this.BrowserHandler = BrowserHandler; diff --git a/browser_patches/firefox/juggler/protocol/Dispatcher.js b/browser_patches/firefox/juggler/protocol/Dispatcher.js new file mode 100644 index 0000000000..2f0413f981 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/Dispatcher.js @@ -0,0 +1,139 @@ +/* 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) { + session.dispose(); + this._sessions.delete(session.sessionId()); + } + + _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(domain, methodName, 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._handlers = new Map(); + } + + sessionId() { + return this._sessionId; + } + + registerHandler(domainName, handler) { + this._handlers.set(domainName, handler); + } + + dispose() { + for (const [domainName, handler] of this._handlers) { + if (typeof handler.dispose !== 'function') + throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`); + handler.dispose(); + } + this._handlers.clear(); + 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(domainName, methodName, params) { + const handler = this._handlers.get(domainName); + if (!handler) + throw new Error(`Domain "${domainName}" does not exist`); + if (!handler[methodName]) + throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`); + return await handler[methodName](params); + } +} + +this.EXPORTED_SYMBOLS = ['Dispatcher']; +this.Dispatcher = Dispatcher; + diff --git a/browser_patches/firefox/juggler/protocol/NetworkHandler.js b/browser_patches/firefox/juggler/protocol/NetworkHandler.js new file mode 100644 index 0000000000..42fa54070d --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/NetworkHandler.js @@ -0,0 +1,162 @@ +/* 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 {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.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(); + +class NetworkHandler { + constructor(target, session, contentChannel) { + this._session = session; + this._contentPage = contentChannel.connect(session.sessionId() + 'page'); + this._httpActivity = new Map(); + this._enabled = false; + this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); + this._requestInterception = false; + this._eventListeners = []; + this._pendingRequstWillBeSentEvents = new Set(); + this._requestIdToFrameId = new Map(); + } + + async enable() { + if (this._enabled) + return; + this._enabled = true; + this._eventListeners = [ + helper.on(this._pageNetwork, PageNetwork.Events.Request, this._onRequest.bind(this)), + helper.on(this._pageNetwork, PageNetwork.Events.Response, this._onResponse.bind(this)), + helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._onRequestFinished.bind(this)), + helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._onRequestFailed.bind(this)), + this._pageNetwork.addSession(), + ]; + } + + async getResponseBody({requestId}) { + return this._pageNetwork.getResponseBody(requestId); + } + + async setExtraHTTPHeaders({headers}) { + this._pageNetwork.setExtraHTTPHeaders(headers); + } + + async setRequestInterception({enabled}) { + if (enabled) + this._pageNetwork.enableRequestInterception(); + else + this._pageNetwork.disableRequestInterception(); + // Right after we enable/disable request interception we need to await all pending + // requestWillBeSent events before successfully returning from the method. + await Promise.all(Array.from(this._pendingRequstWillBeSentEvents)); + } + + async resumeInterceptedRequest({requestId, method, headers, postData}) { + this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData); + } + + async abortInterceptedRequest({requestId, errorCode}) { + this._pageNetwork.abortInterceptedRequest(requestId, errorCode); + } + + async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) { + this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); + } + + dispose() { + this._contentPage.dispose(); + helper.removeListeners(this._eventListeners); + } + + _ensureHTTPActivity(requestId) { + let activity = this._httpActivity.get(requestId); + if (!activity) { + activity = { + _id: requestId, + _lastSentEvent: null, + request: null, + response: null, + complete: null, + failed: null, + }; + this._httpActivity.set(requestId, activity); + } + return activity; + } + + _reportHTTPAcitivityEvents(activity) { + // State machine - sending network events. + if (!activity._lastSentEvent && activity.request) { + this._session.emitEvent('Network.requestWillBeSent', activity.request); + activity._lastSentEvent = 'requestWillBeSent'; + } + if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) { + this._session.emitEvent('Network.responseReceived', activity.response); + activity._lastSentEvent = 'responseReceived'; + } + if (activity._lastSentEvent === 'responseReceived' && activity.complete) { + this._session.emitEvent('Network.requestFinished', activity.complete); + activity._lastSentEvent = 'requestFinished'; + } + if (activity._lastSentEvent && activity.failed) { + this._session.emitEvent('Network.requestFailed', activity.failed); + activity._lastSentEvent = 'requestFailed'; + } + + // Clean up if request lifecycle is over. + if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed') + this._httpActivity.delete(activity._id); + } + + async _onRequest(httpChannel, eventDetails) { + let pendingRequestCallback; + let pendingRequestPromise = new Promise(x => pendingRequestCallback = x); + this._pendingRequstWillBeSentEvents.add(pendingRequestPromise); + let details = null; + try { + details = await this._contentPage.send('requestDetails', {channelId: httpChannel.channelId}); + } catch (e) { + pendingRequestCallback(); + this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); + return; + } + // Inherit frameId for redirects when details are not available. + const frameId = details ? details.frameId : (eventDetails.redirectedFrom ? this._requestIdToFrameId.get(eventDetails.redirectedFrom) : undefined); + this._requestIdToFrameId.set(eventDetails.requestId, frameId); + const activity = this._ensureHTTPActivity(eventDetails.requestId); + activity.request = { + frameId, + ...eventDetails, + }; + this._reportHTTPAcitivityEvents(activity); + pendingRequestCallback(); + this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); + } + + async _onResponse(httpChannel, eventDetails) { + const activity = this._ensureHTTPActivity(eventDetails.requestId); + activity.response = eventDetails; + this._reportHTTPAcitivityEvents(activity); + } + + async _onRequestFinished(httpChannel, eventDetails) { + const activity = this._ensureHTTPActivity(eventDetails.requestId); + activity.complete = eventDetails; + this._reportHTTPAcitivityEvents(activity); + } + + async _onRequestFailed(httpChannel, eventDetails) { + const activity = this._ensureHTTPActivity(eventDetails.requestId); + activity.failed = eventDetails; + this._reportHTTPAcitivityEvents(activity); + } +} + +var EXPORTED_SYMBOLS = ['NetworkHandler']; +this.NetworkHandler = NetworkHandler; diff --git a/browser_patches/firefox/juggler/protocol/PageHandler.js b/browser_patches/firefox/juggler/protocol/PageHandler.js new file mode 100644 index 0000000000..d0b7ac4ac9 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/PageHandler.js @@ -0,0 +1,345 @@ +/* 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 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(); + +class WorkerHandler { + constructor(session, contentChannel, workerId) { + this._session = session; + this._contentWorker = contentChannel.connect(session.sessionId() + workerId); + this._workerId = workerId; + + const emitWrappedProtocolEvent = eventName => { + return params => { + this._session.emitEvent('Page.dispatchMessageFromWorker', { + workerId, + message: JSON.stringify({method: eventName, params}), + }); + } + } + + this._eventListeners = [ + contentChannel.register(session.sessionId() + workerId, { + runtimeConsole: emitWrappedProtocolEvent('Runtime.console'), + 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(session.sessionId() + 'page'); + this._workers = new Map(); + + const emitProtocolEvent = eventName => { + return (...args) => this._session.emitEvent(eventName, ...args); + } + + this._eventListeners = [ + contentChannel.register(session.sessionId() + 'page', { + pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), + pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), + pageEventFired: emitProtocolEvent('Page.eventFired'), + pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), + pageFrameAttached: emitProtocolEvent('Page.frameAttached'), + 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: emitProtocolEvent('Page.ready'), + pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'), + pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), + pageWorkerCreated: this._onWorkerCreated.bind(this), + pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), + }), + ]; + this._pageTarget = target; + this._browser = target.linkedBrowser(); + this._dialogs = new Map(); + + this._enabled = false; + } + + _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}); + } + + async close({runBeforeUnload}) { + // Postpone target close to deliver response in session. + Services.tm.dispatchToMainThread(() => { + this._pageTarget.close(runBeforeUnload); + }); + } + + async enable() { + if (this._enabled) + return; + this._enabled = true; + this._updateModalDialogs(); + + this._eventListeners.push(...[ + helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { + // wait for the dialog to be actually added to DOM. + await Promise.resolve(); + this._updateModalDialogs(); + }), + helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), + helper.on(this._pageTarget, 'crashed', () => { + this._session.emitEvent('Page.crashed', {}); + }), + ]); + } + + dispose() { + this._contentPage.dispose(); + helper.removeListeners(this._eventListeners); + } + + async setViewportSize({viewportSize}) { + await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); + } + + _updateModalDialogs() { + const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []); + for (const dialog of this._dialogs.values()) { + if (!prompts.has(dialog.prompt())) { + this._dialogs.delete(dialog.id()); + this._session.emitEvent('Page.dialogClosed', { + dialogId: dialog.id(), + }); + } 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._session.emitEvent('Page.dialogOpened', { + dialogId: dialog.id(), + type: dialog.type(), + message: dialog.message(), + defaultValue: dialog.defaultValue(), + }); + } + } + + async setFileInputFiles(options) { + return await this._contentPage.send('setFileInputFiles', options); + } + + async setEmulatedMedia(options) { + return await this._contentPage.send('setEmulatedMedia', options); + } + + async setCacheDisabled(options) { + return await this._contentPage.send('setCacheDisabled', options); + } + + async addBinding(options) { + return await this._contentPage.send('addBinding', options); + } + + async adoptNode(options) { + return await this._contentPage.send('adoptNode', options); + } + + async screenshot(options) { + return await this._contentPage.send('screenshot', options); + } + + async getBoundingBox(options) { + return await this._contentPage.send('getBoundingBox', options); + } + + async getContentQuads(options) { + return await this._contentPage.send('getContentQuads', options); + } + + /** + * @param {{frameId: string, url: string}} options + */ + async navigate(options) { + return await this._contentPage.send('navigate', options); + } + + /** + * @param {{frameId: string, url: string}} options + */ + async goBack(options) { + return await this._contentPage.send('goBack', options); + } + + /** + * @param {{frameId: string, url: string}} options + */ + async goForward(options) { + return await this._contentPage.send('goForward', options); + } + + /** + * @param {{frameId: string, url: string}} options + */ + async reload(options) { + return await this._contentPage.send('reload', options); + } + + async describeNode(options) { + return await this._contentPage.send('describeNode', options); + } + + async scrollIntoViewIfNeeded(options) { + return await this._contentPage.send('scrollIntoViewIfNeeded', options); + } + + async addScriptToEvaluateOnNewDocument(options) { + return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options); + } + + async removeScriptToEvaluateOnNewDocument(options) { + return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options); + } + + async dispatchKeyEvent(options) { + return await this._contentPage.send('dispatchKeyEvent', options); + } + + async dispatchTouchEvent(options) { + return await this._contentPage.send('dispatchTouchEvent', options); + } + + async dispatchMouseEvent(options) { + return await this._contentPage.send('dispatchMouseEvent', options); + } + + async insertText(options) { + return await this._contentPage.send('insertText', options); + } + + async crash(options) { + return await this._contentPage.send('crash', options); + } + + async handleDialog({dialogId, accept, promptText}) { + const dialog = this._dialogs.get(dialogId); + if (!dialog) + throw new Error('Failed to find dialog with id = ' + dialogId); + if (accept) + dialog.accept(promptText); + else + dialog.dismiss(); + } + + async setInterceptFileChooserDialog(options) { + return await this._contentPage.send('setInterceptFileChooserDialog', options); + } + + async 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)); + } +} + +class Dialog { + static createIfSupported(prompt) { + const type = prompt.args.promptType; + switch (type) { + case 'alert': + case 'prompt': + case 'confirm': + return new Dialog(prompt, type); + 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(); + } +} + +var EXPORTED_SYMBOLS = ['PageHandler']; +this.PageHandler = PageHandler; diff --git a/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js b/browser_patches/firefox/juggler/protocol/PrimitiveTypes.js new file mode 100644 index 0000000000..5799038f19 --- /dev/null +++ b/browser_patches/firefox/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/juggler/protocol/Protocol.js b/browser_patches/firefox/juggler/protocol/Protocol.js new file mode 100644 index 0000000000..dbc6305719 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/Protocol.js @@ -0,0 +1,850 @@ +/* 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.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), + + value: 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, +}; + + +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.String, + pageTargetId: t.String, + url: t.String, + suggestedFileName: t.String, + }, + 'downloadFinished': { + uuid: t.String, + canceled: t.Optional(t.Boolean), + error: t.Optional(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), + }, + }, + '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'])), + }, + }, + }, +}; + +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), + }, + 'requestFinished': { + requestId: t.String, + }, + '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, + 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, + }, + }, + + 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), + }, + }, + '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: { + navigationId: t.Nullable(t.String), + navigationURL: t.Nullable(t.String), + } + }, + 'goForward': { + params: { + frameId: t.String, + }, + returns: { + navigationId: t.Nullable(t.String), + navigationURL: t.Nullable(t.String), + } + }, + 'reload': { + params: { + frameId: t.String, + }, + returns: { + navigationId: t.String, + navigationURL: t.String, + } + }, + 'getBoundingBox': { + params: { + frameId: t.String, + objectId: t.String, + }, + returns: { + boundingBox: t.Nullable(pageTypes.Rect), + }, + }, + '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']), + fullPage: t.Optional(t.Boolean), + clip: t.Optional(pageTypes.Clip), + }, + 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, + } + }, + '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, + }, + }, + }, +}; + + +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/juggler/protocol/RuntimeHandler.js b/browser_patches/firefox/juggler/protocol/RuntimeHandler.js new file mode 100644 index 0000000000..6082a68935 --- /dev/null +++ b/browser_patches/firefox/juggler/protocol/RuntimeHandler.js @@ -0,0 +1,56 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const helper = new Helper(); + +class RuntimeHandler { + constructor(session, contentChannel) { + const sessionId = session.sessionId(); + this._contentRuntime = contentChannel.connect(sessionId + 'runtime'); + + const emitProtocolEvent = eventName => { + return (...args) => session.emitEvent(eventName, ...args); + } + + this._eventListeners = [ + contentChannel.register(sessionId + 'runtime', { + runtimeConsole: emitProtocolEvent('Runtime.console'), + runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), + runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), + }), + ]; + } + + async evaluate(options) { + return await this._contentRuntime.send('evaluate', options); + } + + async callFunction(options) { + return await this._contentRuntime.send('callFunction', options); + } + + async getObjectProperties(options) { + return await this._contentRuntime.send('getObjectProperties', options); + } + + async disposeObject(options) { + return await this._contentRuntime.send('disposeObject', options); + } + + dispose() { + this._contentRuntime.dispose(); + helper.removeListeners(this._eventListeners); + } +} + +var EXPORTED_SYMBOLS = ['RuntimeHandler']; +this.RuntimeHandler = RuntimeHandler; diff --git a/browser_patches/firefox/patches/bootstrap.diff b/browser_patches/firefox/patches/bootstrap.diff index 5783b207c0..190ce44ca8 100644 --- a/browser_patches/firefox/patches/bootstrap.diff +++ b/browser_patches/firefox/patches/bootstrap.diff @@ -1343,6395 +1343,6 @@ index 25c5b01fc54c8d45da8ceb7cf6ba163bee3c5361..490c5ce49cd9b5f804df59abbfb0450f void updateTimeZone(); void internalResyncICUDefaultTimeZone(); -diff --git a/juggler/Helper.js b/juggler/Helper.js -new file mode 100644 -index 0000000000000000000000000000000000000000..2b1fe7fa712ae210af3ebbccda08404183d19921 ---- /dev/null -+++ b/juggler/Helper.js -@@ -0,0 +1,115 @@ -+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); -+ } -+ -+ 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 ''; -+ } -+} -+ -+var EXPORTED_SYMBOLS = [ "Helper" ]; -+this.Helper = Helper; -+ -diff --git a/juggler/NetworkObserver.js b/juggler/NetworkObserver.js -new file mode 100644 -index 0000000000000000000000000000000000000000..5a9c3232769729fc127c43b24e88713042e577c3 ---- /dev/null -+++ b/juggler/NetworkObserver.js -@@ -0,0 +1,796 @@ -+"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 {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js"); -+ -+ -+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 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(networkObserver, target) { -+ let result = target[pageNetworkSymbol]; -+ if (!result) { -+ result = new PageNetwork(networkObserver, target); -+ target[pageNetworkSymbol] = result; -+ } -+ return result; -+ } -+ -+ constructor(networkObserver, target) { -+ EventEmitter.decorate(this); -+ this._networkObserver = networkObserver; -+ this._target = target; -+ this._sessionCount = 0; -+ this._extraHTTPHeaders = null; -+ this._responseStorage = null; -+ this._requestInterceptionEnabled = false; -+ this._requestIdToInterceptor = null; -+ } -+ -+ addSession() { -+ if (this._sessionCount === 0) { -+ this._responseStorage = new ResponseStorage(this._networkObserver, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10); -+ } -+ ++this._sessionCount; -+ return () => this._stopTracking(); -+ } -+ -+ _stopTracking() { -+ --this._sessionCount; -+ if (this._sessionCount === 0) { -+ this._extraHTTPHeaders = null; -+ this._responseStorage = null; -+ this._requestInterceptionEnabled = false; -+ this._requestIdToInterceptor = null; -+ } -+ } -+ -+ _isActive() { -+ return this._sessionCount > 0; -+ } -+ -+ setExtraHTTPHeaders(headers) { -+ this._extraHTTPHeaders = headers; -+ } -+ -+ enableRequestInterception() { -+ this._requestInterceptionEnabled = true; -+ } -+ -+ disableRequestInterception() { -+ this._requestInterceptionEnabled = false; -+ const interceptors = this._requestIdToInterceptor; -+ if (!interceptors) -+ return; -+ this._requestIdToInterceptor = null; -+ for (const interceptor of interceptors.values()) -+ interceptor._resume(); -+ } -+ -+ resumeInterceptedRequest(requestId, method, headers, postData) { -+ this._takeInterceptor(requestId)._resume(method, headers, postData); -+ } -+ -+ fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) { -+ this._takeInterceptor(requestId)._fulfill(status, statusText, headers, base64body); -+ } -+ -+ abortInterceptedRequest(requestId, errorCode) { -+ this._takeInterceptor(requestId)._abort(errorCode); -+ } -+ -+ getResponseBody(requestId) { -+ if (!this._responseStorage) -+ throw new Error('Responses are not tracked for the given browser'); -+ return this._responseStorage.getBase64EncodedResponse(requestId); -+ } -+ -+ _ensureInterceptors() { -+ if (!this._requestIdToInterceptor) -+ this._requestIdToInterceptor = new Map(); -+ return this._requestIdToInterceptor; -+ } -+ -+ _takeInterceptor(requestId) { -+ const interceptors = this._requestIdToInterceptor; -+ if (!interceptors) -+ throw new Error(`Request interception is not enabled`); -+ const interceptor = interceptors.get(requestId); -+ if (!interceptor) -+ throw new Error(`Cannot find request "${requestId}"`); -+ interceptors.delete(requestId); -+ return interceptor; -+ } -+} -+ -+class NetworkObserver { -+ static instance() { -+ return NetworkObserver._instance || null; -+ } -+ -+ constructor(targetRegistry) { -+ EventEmitter.decorate(this); -+ NetworkObserver._instance = this; -+ -+ this._targetRegistry = targetRegistry; -+ this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor); -+ this._activityDistributor.addObserver(this); -+ -+ this._redirectMap = new Map(); // oldId => newId -+ this._resumedRequestIdToHeaders = new Map(); // requestId => { headers } -+ this._postResumeChannelIdToRequestId = new Map(); // post-resume channel id => pre-resume request id -+ this._pendingAuthentication = new Set(); // pre-auth id -+ this._postAuthChannelIdToRequestId = new Map(); // pre-auth id => post-auth id -+ this._bodyListeners = new Map(); // channel id => ResponseBodyListener. -+ -+ 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'), -+ ]; -+ } -+ -+ _requestAuthenticated(httpChannel) { -+ this._pendingAuthentication.add(httpChannel.channelId + ''); -+ } -+ -+ _requestIdBeforeAuthentication(httpChannel) { -+ const id = httpChannel.channelId + ''; -+ return this._postAuthChannelIdToRequestId.has(id) ? id : undefined; -+ } -+ -+ _requestId(httpChannel) { -+ const id = httpChannel.channelId + ''; -+ return this._postResumeChannelIdToRequestId.get(id) || this._postAuthChannelIdToRequestId.get(id) || id; -+ } -+ -+ _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); -+ const pageNetwork = this._pageNetworkForChannel(oldHttpChannel); -+ if (!pageNetwork) -+ return; -+ const oldRequestId = this._requestId(oldHttpChannel); -+ const newRequestId = this._requestId(newHttpChannel); -+ if (this._resumedRequestIdToHeaders.has(oldRequestId)) { -+ // When we call resetInterception on a request, we get a new "redirected" request for it. -+ const { method, headers, postData } = this._resumedRequestIdToHeaders.get(oldRequestId); -+ if (headers) { -+ // Apply new request headers from interception resume. -+ for (const header of requestHeaders(newChannel)) -+ newChannel.setRequestHeader(header.name, '', false /* merge */); -+ for (const header of headers) -+ newChannel.setRequestHeader(header.name, header.value, false /* merge */); -+ } -+ if (method) -+ newChannel.requestMethod = method; -+ if (postData && newChannel instanceof Ci.nsIUploadChannel) { -+ const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); -+ synthesized.data = atob(postData); -+ newChannel.setUploadStream(synthesized, 'application/octet-stream', -1); -+ } -+ // Use the old request id for the new "redirected" request for protocol consistency. -+ this._resumedRequestIdToHeaders.delete(oldRequestId); -+ this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId); -+ } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) { -+ // Regular (non-internal) redirect. -+ this._redirectMap.set(newRequestId, oldRequestId); -+ } -+ } -+ -+ observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) { -+ if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION) -+ return; -+ if (!(channel instanceof Ci.nsIHttpChannel)) -+ return; -+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); -+ const pageNetwork = this._pageNetworkForChannel(httpChannel); -+ if (!pageNetwork) -+ return; -+ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE) -+ return; -+ if (this._isResumedChannel(httpChannel)) -+ return; -+ if (this._requestIdBeforeAuthentication(httpChannel)) -+ return; -+ this._sendOnRequestFinished(pageNetwork, httpChannel); -+ } -+ -+ pageNetworkForTarget(target) { -+ return PageNetwork._forPageTarget(this, target); -+ } -+ -+ _pageNetworkForChannel(httpChannel) { -+ let loadContext = helper.getLoadContext(httpChannel); -+ if (!loadContext) -+ return; -+ const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement); -+ if (!target) -+ return; -+ const pageNetwork = PageNetwork._forPageTarget(this, target); -+ if (!pageNetwork._isActive()) -+ return; -+ return pageNetwork; -+ } -+ -+ _isResumedChannel(httpChannel) { -+ return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + ''); -+ } -+ -+ _onRequest(channel, topic) { -+ if (!(channel instanceof Ci.nsIHttpChannel)) -+ return; -+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); -+ const pageNetwork = this._pageNetworkForChannel(httpChannel); -+ if (!pageNetwork) -+ return; -+ if (this._isResumedChannel(httpChannel)) { -+ // Ignore onRequest for resumed requests, but listen to their response. -+ new ResponseBodyListener(this, pageNetwork, httpChannel); -+ return; -+ } -+ // Convert pending auth bit into auth mapping. -+ const channelId = httpChannel.channelId + ''; -+ if (this._pendingAuthentication.has(channelId)) { -+ this._postAuthChannelIdToRequestId.set(channelId, channelId + '-auth'); -+ this._redirectMap.set(channelId + '-auth', channelId); -+ this._pendingAuthentication.delete(channelId); -+ const bodyListener = this._bodyListeners.get(channelId); -+ if (bodyListener) -+ bodyListener.dispose(); -+ } -+ const browserContext = pageNetwork._target.browserContext(); -+ if (browserContext) -+ this._appendExtraHTTPHeaders(httpChannel, browserContext.extraHTTPHeaders); -+ this._appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders); -+ const requestId = this._requestId(httpChannel); -+ const isRedirect = this._redirectMap.has(requestId); -+ const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork); -+ if (!interceptionEnabled) { -+ new NotificationCallbacks(this, pageNetwork, httpChannel, false); -+ this._sendOnRequest(httpChannel, false); -+ new ResponseBodyListener(this, pageNetwork, httpChannel); -+ } else if (isRedirect) { -+ // We pretend that redirect is interceptable in the protocol, although it's actually not -+ // and therefore we do not instantiate the interceptor. -+ // TODO: look into REDIRECT_MODE_MANUAL. -+ const interceptors = pageNetwork._ensureInterceptors(); -+ interceptors.set(requestId, { -+ _resume: () => {}, -+ _abort: () => {}, -+ _fulfill: () => {}, -+ }); -+ new NotificationCallbacks(this, pageNetwork, httpChannel, false); -+ this._sendOnRequest(httpChannel, true); -+ new ResponseBodyListener(this, pageNetwork, httpChannel); -+ } else { -+ const previousCallbacks = httpChannel.notificationCallbacks; -+ if (previousCallbacks instanceof Ci.nsIInterfaceRequestor) { -+ const interceptor = previousCallbacks.getInterface(Ci.nsINetworkInterceptController); -+ // We assume that interceptor is a service worker if there is one. -+ if (interceptor && interceptor.shouldPrepareForIntercept(httpChannel.URI, httpChannel)) { -+ new NotificationCallbacks(this, pageNetwork, httpChannel, false); -+ this._sendOnRequest(httpChannel, false); -+ new ResponseBodyListener(this, pageNetwork, httpChannel); -+ } else { -+ // We'll issue onRequest once it's intercepted. -+ new NotificationCallbacks(this, pageNetwork, httpChannel, true); -+ } -+ } else { -+ // We'll issue onRequest once it's intercepted. -+ new NotificationCallbacks(this, pageNetwork, httpChannel, true); -+ } -+ } -+ } -+ -+ _isInterceptionEnabledForPage(pageNetwork) { -+ if (pageNetwork._requestInterceptionEnabled) -+ return true; -+ const browserContext = pageNetwork._target.browserContext(); -+ if (browserContext && browserContext.requestInterceptionEnabled) -+ return true; -+ if (browserContext && browserContext.settings.onlineOverride === 'offline') -+ return true; -+ return false; -+ } -+ -+ _appendExtraHTTPHeaders(httpChannel, headers) { -+ if (!headers) -+ return; -+ for (const header of headers) -+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */); -+ } -+ -+ _onIntercepted(httpChannel, interceptor) { -+ const pageNetwork = this._pageNetworkForChannel(httpChannel); -+ if (!pageNetwork) { -+ interceptor._resume(); -+ return; -+ } -+ const browserContext = pageNetwork._target.browserContext(); -+ if (browserContext && browserContext.settings.onlineOverride === 'offline') { -+ interceptor._abort(Cr.NS_ERROR_OFFLINE); -+ return; -+ } -+ -+ const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork); -+ this._sendOnRequest(httpChannel, !!interceptionEnabled); -+ if (interceptionEnabled) -+ pageNetwork._ensureInterceptors().set(this._requestId(httpChannel), interceptor); -+ else -+ interceptor._resume(); -+ } -+ -+ _sendOnRequest(httpChannel, isIntercepted) { -+ const pageNetwork = this._pageNetworkForChannel(httpChannel); -+ if (!pageNetwork) -+ return; -+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; -+ const internalCauseType = httpChannel.loadInfo ? httpChannel.loadInfo.internalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER; -+ const requestId = this._requestId(httpChannel); -+ const redirectedFrom = this._redirectMap.get(requestId); -+ this._redirectMap.delete(requestId); -+ pageNetwork.emit(PageNetwork.Events.Request, httpChannel, { -+ url: httpChannel.URI.spec, -+ isIntercepted, -+ requestId, -+ redirectedFrom, -+ postData: readRequestPostData(httpChannel), -+ headers: requestHeaders(httpChannel), -+ method: httpChannel.requestMethod, -+ navigationId: httpChannel.isMainDocumentChannel ? this._requestIdBeforeAuthentication(httpChannel) || this._requestId(httpChannel) : undefined, -+ cause: causeTypeToString(causeType), -+ internalCause: causeTypeToString(internalCauseType), -+ }); -+ } -+ -+ _sendOnRequestFinished(pageNetwork, httpChannel) { -+ pageNetwork.emit(PageNetwork.Events.RequestFinished, httpChannel, { -+ requestId: this._requestId(httpChannel), -+ }); -+ this._cleanupChannelState(httpChannel); -+ } -+ -+ _sendOnRequestFailed(pageNetwork, httpChannel, error) { -+ pageNetwork.emit(PageNetwork.Events.RequestFailed, httpChannel, { -+ requestId: this._requestId(httpChannel), -+ errorCode: helper.getNetworkErrorStatusText(error), -+ }); -+ this._cleanupChannelState(httpChannel); -+ } -+ -+ _cleanupChannelState(httpChannel) { -+ const id = httpChannel.channelId + ''; -+ this._postResumeChannelIdToRequestId.delete(id); -+ this._postAuthChannelIdToRequestId.delete(id); -+ } -+ -+ _onResponse(fromCache, httpChannel, topic) { -+ const pageNetwork = this._pageNetworkForChannel(httpChannel); -+ if (!pageNetwork) -+ return; -+ httpChannel.QueryInterface(Ci.nsIHttpChannelInternal); -+ const headers = []; -+ httpChannel.visitResponseHeaders({ -+ visitHeader: (name, value) => headers.push({name, value}), -+ }); -+ -+ let remoteIPAddress = undefined; -+ let remotePort = undefined; -+ try { -+ remoteIPAddress = httpChannel.remoteAddress; -+ remotePort = httpChannel.remotePort; -+ } catch (e) { -+ // remoteAddress is not defined for cached requests. -+ } -+ pageNetwork.emit(PageNetwork.Events.Response, httpChannel, { -+ requestId: this._requestId(httpChannel), -+ securityDetails: getSecurityDetails(httpChannel), -+ fromCache, -+ headers, -+ remoteIPAddress, -+ remotePort, -+ status: httpChannel.responseStatus, -+ statusText: httpChannel.responseStatusText, -+ }); -+ } -+ -+ _onResponseFinished(pageNetwork, httpChannel, body) { -+ if (!pageNetwork._isActive()) -+ return; -+ pageNetwork._responseStorage.addResponseBody(httpChannel, body); -+ this._sendOnRequestFinished(pageNetwork, httpChannel); -+ } -+ -+ 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 text = undefined; -+ try { -+ text = NetUtil.readInputStreamToString(iStream, iStream.available()); -+ const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'] -+ .createInstance(Ci.nsIScriptableUnicodeConverter); -+ converter.charset = 'UTF-8'; -+ text = converter.ConvertToUnicode(text); -+ } catch (err) { -+ text = undefined; -+ } -+ -+ // 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 text; -+} -+ -+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'; -+} -+ -+class ResponseStorage { -+ constructor(networkObserver, maxTotalSize, maxResponseSize) { -+ this._networkObserver = networkObserver; -+ this._totalSize = 0; -+ this._maxResponseSize = maxResponseSize; -+ this._maxTotalSize = maxTotalSize; -+ this._responses = new Map(); -+ } -+ -+ addResponseBody(httpChannel, body) { -+ if (body.length > this._maxResponseSize) { -+ this._responses.set(requestId, { -+ evicted: true, -+ body: '', -+ }); -+ return; -+ } -+ let encodings = []; -+ if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) { -+ const encodingHeader = httpChannel.getResponseHeader("Content-Encoding"); -+ encodings = encodingHeader.split(/\s*\t*,\s*\t*/); -+ } -+ this._responses.set(this._networkObserver._requestId(httpChannel), {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 = CommonUtils.convertString(result, encoding, 'uncompressed'); -+ } -+ return {base64body: btoa(result)}; -+ } -+} -+ -+class ResponseBodyListener { -+ constructor(networkObserver, pageNetwork, httpChannel) { -+ this._networkObserver = networkObserver; -+ this._pageNetwork = pageNetwork; -+ this._httpChannel = httpChannel; -+ this._chunks = []; -+ this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]); -+ httpChannel.QueryInterface(Ci.nsITraceableChannel); -+ this.originalListener = httpChannel.setNewListener(this); -+ this._disposed = false; -+ this._networkObserver._bodyListeners.set(this._httpChannel.channelId + '', this); -+ } -+ -+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) { -+ if (this._disposed) { -+ this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount); -+ return; -+ } -+ -+ 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._chunks.push(data); -+ -+ oStream.writeBytes(data, aCount); -+ this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount); -+ } -+ -+ onStartRequest(aRequest) { -+ this.originalListener.onStartRequest(aRequest); -+ } -+ -+ onStopRequest(aRequest, aStatusCode) { -+ this.originalListener.onStopRequest(aRequest, aStatusCode); -+ if (this._disposed) -+ return; -+ -+ if (aStatusCode === 0) { -+ const body = this._chunks.join(''); -+ this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body); -+ } else { -+ this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, aStatusCode); -+ } -+ -+ delete this._chunks; -+ this.dispose(); -+ } -+ -+ dispose() { -+ this._disposed = true; -+ this._networkObserver._bodyListeners.delete(this._httpChannel.channelId + ''); -+ } -+} -+ -+class NotificationCallbacks { -+ constructor(networkObserver, pageNetwork, httpChannel, shouldIntercept) { -+ this._networkObserver = networkObserver; -+ this._pageNetwork = pageNetwork; -+ this._shouldIntercept = shouldIntercept; -+ this._httpChannel = httpChannel; -+ this._previousCallbacks = httpChannel.notificationCallbacks; -+ httpChannel.notificationCallbacks = this; -+ -+ const qis = [ -+ Ci.nsIAuthPrompt2, -+ Ci.nsIAuthPromptProvider, -+ Ci.nsIInterfaceRequestor, -+ ]; -+ if (shouldIntercept) -+ qis.push(Ci.nsINetworkInterceptController); -+ this.QueryInterface = ChromeUtils.generateQI(qis); -+ } -+ -+ getInterface(iid) { -+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider)) -+ return this; -+ if (this._shouldIntercept && 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; -+ } -+ -+ _forward(iid, method, args) { -+ if (!this._previousCallbacks) -+ return; -+ try { -+ const impl = this._previousCallbacks.getInterface(iid); -+ impl[method].apply(impl, args); -+ } catch (e) { -+ if (e.result != Cr.NS_ERROR_NO_INTERFACE) -+ throw e; -+ } -+ } -+ -+ // 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 browserContext = this._pageNetwork._target.browserContext(); -+ const credentials = browserContext ? browserContext.httpCredentials : undefined; -+ if (!credentials) -+ return false; -+ authInfo.username = credentials.username; -+ authInfo.password = credentials.password; -+ this._networkObserver._requestAuthenticated(this._httpChannel); -+ return true; -+ } -+ -+ // nsINetworkInterceptController -+ shouldPrepareForIntercept(aURI, channel) { -+ if (!(channel instanceof Ci.nsIHttpChannel)) -+ return false; -+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); -+ return httpChannel.channelId === this._httpChannel.channelId; -+ } -+ -+ // nsINetworkInterceptController -+ channelIntercepted(intercepted) { -+ this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel); -+ const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel); -+ this._networkObserver._onIntercepted(httpChannel, this); -+ } -+ -+ _resume(method, headers, postData) { -+ this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { method, headers, postData }); -+ this._intercepted.resetInterception(); -+ } -+ -+ _fulfill(status, statusText, headers, base64body) { -+ this._intercepted.synthesizeStatus(status, statusText); -+ for (const header of headers) -+ this._intercepted.synthesizeHeader(header.name, header.value); -+ const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream); -+ const body = base64body ? atob(base64body) : ''; -+ synthesized.data = body; -+ this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false); -+ this._intercepted.finishSynthesizedResponse(); -+ this._pageNetwork.emit(PageNetwork.Events.Response, this._httpChannel, { -+ requestId: this._networkObserver._requestId(this._httpChannel), -+ securityDetails: null, -+ fromCache: false, -+ headers, -+ status, -+ statusText, -+ }); -+ this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body); -+ } -+ -+ _abort(errorCode) { -+ const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE; -+ this._intercepted.cancelInterception(error); -+ this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, error); -+ } -+} -+ -+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/juggler/SimpleChannel.js b/juggler/SimpleChannel.js -new file mode 100644 -index 0000000000000000000000000000000000000000..ba34976ad05e7f5f1a99777f76ac08b171af40b7 ---- /dev/null -+++ b/juggler/SimpleChannel.js -@@ -0,0 +1,130 @@ -+"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.transport.sendMessage = obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj); -+ channel.transport.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.transport = { -+ sendMessage: null, -+ dispose: null, -+ }; -+ this._disposed = false; -+ } -+ -+ 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); -+ 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}); -+ }); -+ this.transport.sendMessage({requestId: id, methodName, params, namespace}); -+ return promise; -+ } -+ -+ async _onMessage(data) { -+ 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.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`}); -+ 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/juggler/TargetRegistry.js b/juggler/TargetRegistry.js -new file mode 100644 -index 0000000000000000000000000000000000000000..921cea40e11588805a0ab014e810c6aff8998f16 ---- /dev/null -+++ b/juggler/TargetRegistry.js -@@ -0,0 +1,676 @@ -+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 {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js"); -+const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js"); -+const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js"); -+const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js"); -+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.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(); -+ helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'); -+ } -+ -+ _onRequest(httpChannel, topic) { -+ let loadContext = helper.getLoadContext(httpChannel); -+ if (!loadContext) -+ return; -+ if (!loadContext.topFrameElement) -+ return; -+ const target = this._registry.targetForBrowser(loadContext.topFrameElement); -+ if (!target) -+ return; -+ target._channelIds.add(httpChannel.channelId); -+ } -+ -+ // -+ // nsIDownloadInterceptor implementation. -+ // -+ interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) { -+ let pageTarget = this._registry._browserBrowsingContextToTarget.get(browsingContext); -+ // New page downloads won't have browsing contex. -+ if (!pageTarget) -+ pageTarget = this._registry._targetForChannel(request); -+ 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(); -+ -+ // 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('crashed'); -+ target.dispose(); -+ this.emit(TargetRegistry.Events.TargetDestroyed, target); -+ } -+ }, 'oop-frameloader-crashed'); -+ -+ Services.mm.addMessageListener('juggler:content-ready', { -+ receiveMessage: message => { -+ const linkedBrowser = message.target; -+ if (this._browserToTarget.has(linkedBrowser)) -+ throw new Error(`Internal error: two targets per linkedBrowser`); -+ -+ let tab; -+ let gBrowser; -+ const windowsIt = Services.wm.getEnumerator('navigator:browser'); -+ while (windowsIt.hasMoreElements()) { -+ const window = windowsIt.getNext(); -+ // gBrowser is always created before tabs. If gBrowser is not -+ // initialized yet the browser belongs to another window. -+ if (!window.gBrowser) -+ continue; -+ tab = window.gBrowser.getTabForBrowser(linkedBrowser); -+ if (tab) { -+ gBrowser = window.gBrowser; -+ break; -+ } -+ } -+ if (!tab) -+ return; -+ -+ const { userContextId } = message.data; -+ const openerContext = 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); -+ } -+ const browserContext = this._userContextIdToBrowserContext.get(userContextId); -+ const target = new PageTarget(this, gBrowser, tab, linkedBrowser, browserContext, openerTarget); -+ -+ const sessions = []; -+ const readyData = { sessions, target }; -+ this.emit(TargetRegistry.Events.TargetCreated, readyData); -+ sessions.forEach(session => target._initSession(session)); -+ return { -+ scriptsToEvaluateOnNewDocument: browserContext ? browserContext.scriptsToEvaluateOnNewDocument : [], -+ bindings: browserContext ? browserContext.bindings : [], -+ settings: browserContext ? browserContext.settings : {}, -+ sessionIds: sessions.map(session => session.sessionId()), -+ }; -+ }, -+ }); -+ -+ const onTabOpenListener = event => { -+ const tab = event.target; -+ const userContextId = tab.userContextId; -+ const browserContext = this._userContextIdToBrowserContext.get(userContextId); -+ if (browserContext && browserContext.defaultViewportSize) -+ setViewportSizeForBrowser(browserContext.defaultViewportSize, tab.linkedBrowser); -+ }; -+ -+ const onTabCloseListener = event => { -+ const tab = event.target; -+ const linkedBrowser = tab.linkedBrowser; -+ const target = this._browserToTarget.get(linkedBrowser); -+ if (target) { -+ target.dispose(); -+ this.emit(TargetRegistry.Events.TargetDestroyed, target); -+ } -+ }; -+ -+ Services.wm.addListener({ -+ onOpenWindow: async window => { -+ const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); -+ if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) -+ return; -+ if (domWindow.document.readyState !== 'uninitialized') -+ throw new Error('DOMWindow should not be loaded yet'); -+ await new Promise(fulfill => { -+ domWindow.addEventListener('DOMContentLoaded', function listener() { -+ domWindow.removeEventListener('DOMContentLoaded', listener); -+ fulfill(); -+ }); -+ }); -+ if (!domWindow.gBrowser) -+ return; -+ domWindow.gBrowser.tabContainer.addEventListener('TabOpen', onTabOpenListener); -+ domWindow.gBrowser.tabContainer.addEventListener('TabClose', onTabCloseListener); -+ }, -+ onCloseWindow: window => { -+ const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow); -+ if (!(domWindow instanceof Ci.nsIDOMChromeWindow)) -+ return; -+ if (!domWindow.gBrowser) -+ return; -+ domWindow.gBrowser.tabContainer.removeEventListener('TabOpen', onTabOpenListener); -+ domWindow.gBrowser.tabContainer.removeEventListener('TabClose', onTabCloseListener); -+ 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)); -+ } -+ -+ defaultContext() { -+ return this._defaultContext; -+ } -+ -+ createBrowserContext(removeOnDetach) { -+ return new BrowserContext(this, helper.generateId(), removeOnDetach); -+ } -+ -+ browserContextForId(browserContextId) { -+ return this._browserContextIdToBrowserContext.get(browserContextId); -+ } -+ -+ async newPage({browserContextId}) { -+ let window; -+ let created = false; -+ const windowsIt = Services.wm.getEnumerator('navigator:browser'); -+ if (windowsIt.hasMoreElements()) { -+ window = windowsIt.getNext(); -+ } else { -+ const features = "chrome,dialog=no,all"; -+ const args = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); -+ args.data = 'about:blank'; -+ window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args); -+ created = true; -+ } -+ if (window.document.readyState !== 'complete') { -+ await new Promise(fulfill => { -+ window.addEventListener('load', function listener() { -+ window.removeEventListener('load', listener); -+ fulfill(); -+ }); -+ }); -+ } -+ const browserContext = this.browserContextForId(browserContextId); -+ const tab = window.gBrowser.addTab('about:blank', { -+ userContextId: browserContext.userContextId, -+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), -+ }); -+ const target = await new Promise(fulfill => { -+ const listener = helper.on(this, TargetRegistry.Events.TargetCreated, ({target}) => { -+ if (target._tab === tab) { -+ helper.removeListeners([listener]); -+ fulfill(target); -+ } -+ }); -+ }); -+ if (created) { -+ window.gBrowser.removeTab(window.gBrowser.getTabForBrowser(window.gBrowser.getBrowserAtIndex(0)), { -+ skipPermitUnload: true, -+ }); -+ } -+ window.gBrowser.selectedTab = tab; -+ 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); -+ } -+ -+ _targetForChannel(channel) { -+ const channelId = channel.channelId; -+ for (const target of this._browserToTarget.values()) { -+ if (target._channelIds.has(channelId)) -+ return target; -+ } -+ return null; -+ } -+} -+ -+class PageTarget { -+ constructor(registry, gBrowser, tab, linkedBrowser, browserContext, opener) { -+ EventEmitter.decorate(this); -+ -+ this._targetId = helper.generateId(); -+ this._registry = registry; -+ this._gBrowser = gBrowser; -+ this._tab = tab; -+ this._linkedBrowser = linkedBrowser; -+ this._browserContext = browserContext; -+ this._viewportSize = undefined; -+ this._url = 'about:blank'; -+ this._openerId = opener ? opener.id() : undefined; -+ this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager); -+ this._channelIds = new Set(); -+ -+ const navigationListener = { -+ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]), -+ onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation), -+ }; -+ this._eventListeners = [ -+ helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION), -+ ]; -+ -+ this._disposed = false; -+ if (browserContext) { -+ browserContext.pages.add(this); -+ browserContext._firstPageCallback(); -+ } -+ this._registry._browserToTarget.set(this._linkedBrowser, this); -+ this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this); -+ } -+ -+ linkedBrowser() { -+ return this._linkedBrowser; -+ } -+ -+ browserContext() { -+ return this._browserContext; -+ } -+ -+ async setViewportSize(viewportSize) { -+ this._viewportSize = viewportSize; -+ const actualSize = setViewportSizeForBrowser(viewportSize, this._linkedBrowser); -+ await this._channel.connect('').send('awaitViewportDimensions', { -+ width: actualSize.width, -+ height: actualSize.height -+ }); -+ } -+ -+ connectSession(session) { -+ this._initSession(session); -+ this._channel.connect('').send('attach', { sessionId: session.sessionId() }); -+ } -+ -+ disconnectSession(session) { -+ if (!this._disposed) -+ this._channel.connect('').emit('detach', { sessionId: session.sessionId() }); -+ } -+ -+ async close(runBeforeUnload = false) { -+ await this._gBrowser.removeTab(this._tab, { -+ skipPermitUnload: !runBeforeUnload, -+ }); -+ } -+ -+ _initSession(session) { -+ const pageHandler = new PageHandler(this, session, this._channel); -+ const networkHandler = new NetworkHandler(this, session, this._channel); -+ session.registerHandler('Page', pageHandler); -+ session.registerHandler('Network', networkHandler); -+ session.registerHandler('Runtime', new RuntimeHandler(session, this._channel)); -+ session.registerHandler('Accessibility', new AccessibilityHandler(session, this._channel)); -+ pageHandler.enable(); -+ networkHandler.enable(); -+ } -+ -+ id() { -+ return this._targetId; -+ } -+ -+ info() { -+ return { -+ targetId: this.id(), -+ type: 'page', -+ browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined, -+ 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); -+ } -+ -+ dispose() { -+ this._disposed = true; -+ if (this._browserContext) -+ this._browserContext.pages.delete(this); -+ this._registry._browserToTarget.delete(this._linkedBrowser); -+ this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext); -+ helper.removeListeners(this._eventListeners); -+ } -+} -+ -+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.removeOnDetach = removeOnDetach; -+ this.extraHTTPHeaders = undefined; -+ this.httpCredentials = undefined; -+ this.requestInterceptionEnabled = undefined; -+ this.ignoreHTTPSErrors = undefined; -+ this.downloadOptions = undefined; -+ this.defaultViewportSize = undefined; -+ this.scriptsToEvaluateOnNewDocument = []; -+ this.bindings = []; -+ this.settings = {}; -+ this.pages = new Set(); -+ this._firstPagePromise = new Promise(f => this._firstPageCallback = f); -+ } -+ -+ async destroy() { -+ if (this.userContextId !== 0) { -+ ContextualIdentityService.remove(this.userContextId); -+ ContextualIdentityService.closeContainerTabs(this.userContextId); -+ 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); -+ } -+ -+ 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 setDefaultViewport(viewport) { -+ this.defaultViewportSize = viewport ? viewport.viewportSize : undefined; -+ if (!this.userContextId) { -+ // First page in the default context comes before onTabOpenListener -+ // so we don't set default viewport. Wait for it here and ensure the viewport. -+ await this._firstPagePromise; -+ } -+ const promises = Array.from(this.pages).map(async page => { -+ // Resize to new default, unless the page has a custom viewport. -+ if (!page._viewportSize) -+ await page.setViewportSize(this.defaultViewportSize); -+ }); -+ await Promise.all([ -+ this.applySetting('deviceScaleFactor', viewport ? viewport.deviceScaleFactor : undefined), -+ ...promises, -+ ]); -+ } -+ -+ 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], -+ ); -+ } -+ } -+ -+ 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; -+ } -+} -+ -+function dirPath(path) { -+ return path.substring(0, path.lastIndexOf('/') + 1); -+} -+ -+function setViewportSizeForBrowser(viewportSize, browser) { -+ if (viewportSize) { -+ const {width, height} = viewportSize; -+ 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']; -+this.TargetRegistry = TargetRegistry; -diff --git a/juggler/components/juggler.js b/juggler/components/juggler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..4905a1bc7c585d3d1bf33430991d190cee75e332 ---- /dev/null -+++ b/juggler/components/juggler.js -@@ -0,0 +1,80 @@ -+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.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); -+ if (!jugglerFlag || isNaN(jugglerFlag)) -+ return; -+ const port = parseInt(jugglerFlag, 10); -+ const silent = cmdLine.preventDefault; -+ if (silent) -+ Services.startup.enterLastWindowClosingSurvivalArea(); -+ -+ const targetRegistry = new TargetRegistry(); -+ new NetworkObserver(targetRegistry); -+ -+ 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(); -+ -+ // Force create hidden window here, otherwise its creation later closes the web socket! -+ Services.appShell.hiddenDOMWindow; -+ -+ 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().registerHandler('Browser', browserHandler); -+ } -+ }); -+ -+ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */); -+ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\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 = XPCOMUtils.generateNSGetFactory([CommandLineHandler]); -diff --git a/juggler/components/juggler.manifest b/juggler/components/juggler.manifest -new file mode 100644 -index 0000000000000000000000000000000000000000..50f8930207563e0d6b8a7878fc602dbca54d77fc ---- /dev/null -+++ b/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/juggler/components/moz.build b/juggler/components/moz.build -new file mode 100644 -index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853dd7aeb254 ---- /dev/null -+++ b/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/juggler/content/FrameTree.js b/juggler/content/FrameTree.js -new file mode 100644 -index 0000000000000000000000000000000000000000..fe9b67c153cbb6d687bbb98b8882dfdf2bc10c2e ---- /dev/null -+++ b/juggler/content/FrameTree.js -@@ -0,0 +1,473 @@ -+"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._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(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; -+ 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); -+ } -+ -+ setColorScheme(colorScheme) { -+ const docShell = this._mainFrame._docShell; -+ switch (colorScheme) { -+ case 'light': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_LIGHT; break; -+ case 'dark': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_DARK; break; -+ case 'no-preference': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NO_PREFERENCE; break; -+ default: docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NONE; break; -+ } -+ } -+ -+ 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; -+ } -+ -+ const isStart = flag & Ci.nsIWebProgressListener.STATE_START; -+ const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING; -+ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; -+ -+ let isDownload = false; -+ try { -+ isDownload = (channel.contentDisposition === Ci.nsIChannel.DISPOSITION_ATTACHMENT); -+ } catch(e) { -+ // The method is expected to throw if it's not an attachment. -+ } -+ -+ if (isStart) { -+ // Starting a new navigation. -+ frame._pendingNavigationId = this._channelId(channel); -+ frame._pendingNavigationURL = channel.URI.spec; -+ this.emit(FrameTree.Events.NavigationStarted, frame); -+ } else if (isTransferring || (isStop && frame._pendingNavigationId && !status && !isDownload)) { -+ // 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 || isDownload)) { -+ // 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 = isDownload ? 'Will download to file' : helper.getNetworkErrorStatusText(status); -+ this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText); -+ if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED && !isDownload) -+ this.forcePageReady(); -+ } -+ } -+ -+ 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); -+ } -+ } -+ -+ _channelId(channel) { -+ if (channel instanceof Ci.nsIHttpChannel) { -+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); -+ return String(httpChannel.channelId); -+ } -+ return helper.generateId(); -+ } -+ -+ _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', -+ NavigationStarted: 'navigationstarted', -+ NavigationCommitted: 'navigationcommitted', -+ NavigationAborted: 'navigationaborted', -+ SameDocumentNavigation: 'samedocumentnavigation', -+ PageReady: 'pageready', -+}; -+ -+class Frame { -+ constructor(frameTree, runtime, docShell, parentFrame) { -+ this._frameTree = frameTree; -+ this._runtime = runtime; -+ this._docShell = docShell; -+ this._children = new Set(); -+ this._frameId = helper.generateId(); -+ 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; -+ } -+ -+ 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() { -+ 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.transport = { -+ 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); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['FrameTree']; -+this.FrameTree = FrameTree; -+ -diff --git a/juggler/content/NetworkMonitor.js b/juggler/content/NetworkMonitor.js -new file mode 100644 -index 0000000000000000000000000000000000000000..155d0770ddf704728829272a41a31ce8c9509a25 ---- /dev/null -+++ b/juggler/content/NetworkMonitor.js -@@ -0,0 +1,48 @@ -+"use strict"; -+const Ci = Components.interfaces; -+const Cr = Components.results; -+const Cu = Components.utils; -+ -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+ -+const helper = new Helper(); -+ -+class NetworkMonitor { -+ constructor(rootDocShell, frameTree) { -+ this._frameTree = frameTree; -+ this._requestDetails = new Map(); -+ -+ this._eventListeners = [ -+ helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'), -+ ]; -+ } -+ -+ _onRequest(channel) { -+ if (!(channel instanceof Ci.nsIHttpChannel)) -+ return; -+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); -+ const loadContext = helper.getLoadContext(httpChannel); -+ if (!loadContext) -+ return; -+ const window = loadContext.associatedWindow; -+ const frame = this._frameTree.frameForDocShell(window.docShell); -+ if (!frame) -+ return; -+ this._requestDetails.set(httpChannel.channelId, { -+ frameId: frame.id(), -+ }); -+ } -+ -+ requestDetails(channelId) { -+ return this._requestDetails.get(channelId) || null; -+ } -+ -+ dispose() { -+ this._requestDetails.clear(); -+ helper.removeListeners(this._eventListeners); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['NetworkMonitor']; -+this.NetworkMonitor = NetworkMonitor; -+ -diff --git a/juggler/content/PageAgent.js b/juggler/content/PageAgent.js -new file mode 100644 -index 0000000000000000000000000000000000000000..7828bbea9a32fc7bf161c1bc814b900bdc19a2a9 ---- /dev/null -+++ b/juggler/content/PageAgent.js -@@ -0,0 +1,977 @@ -+"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, sessionId, worker) { -+ this._workerRuntime = worker.channel().connect(sessionId + 'runtime'); -+ this._browserWorker = browserChannel.connect(sessionId + worker.id()); -+ this._worker = worker; -+ this._sessionId = sessionId; -+ const emit = name => { -+ return (...args) => this._browserWorker.emit(name, ...args); -+ }; -+ this._eventListeners = [ -+ worker.channel().register(sessionId + 'runtime', { -+ runtimeConsole: emit('runtimeConsole'), -+ runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'), -+ runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'), -+ }), -+ browserChannel.register(sessionId + 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), -+ }), -+ ]; -+ worker.channel().connect('').emit('attach', {sessionId}); -+ } -+ -+ dispose() { -+ this._worker.channel().connect('').emit('detach', {sessionId: this._sessionId}); -+ 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.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, sessionId, frameTree, networkMonitor) { -+ this._messageManager = messageManager; -+ this._browserChannel = browserChannel; -+ this._sessionId = sessionId; -+ this._browserPage = browserChannel.connect(sessionId + 'page'); -+ this._browserRuntime = browserChannel.connect(sessionId + 'runtime'); -+ this._frameTree = frameTree; -+ this._runtime = frameTree.runtime(); -+ this._networkMonitor = networkMonitor; -+ -+ this._frameData = new Map(); -+ this._workerData = new Map(); -+ this._scriptsToEvaluateOnNewDocument = new Map(); -+ this._isolatedWorlds = new Map(); -+ -+ this._eventListeners = [ -+ browserChannel.register(sessionId + '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), -+ getBoundingBox: this._getBoundingBox.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), -+ requestDetails: this._requestDetails.bind(this), -+ screenshot: this._screenshot.bind(this), -+ scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this), -+ setCacheDisabled: this._setCacheDisabled.bind(this), -+ setEmulatedMedia: this._setEmulatedMedia.bind(this), -+ setFileInputFiles: this._setFileInputFiles.bind(this), -+ setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this), -+ }), -+ browserChannel.register(sessionId + '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), -+ }), -+ ]; -+ this._enabled = false; -+ -+ const docShell = frameTree.mainFrame().docShell(); -+ this._docShell = docShell; -+ this._initialDPPX = docShell.contentViewer.overrideDPPX; -+ this._customScrollbars = null; -+ this._dataTransfer = null; -+ } -+ -+ _requestDetails({channelId}) { -+ return this._networkMonitor.requestDetails(channelId); -+ } -+ -+ async _setEmulatedMedia({type, colorScheme}) { -+ const docShell = this._frameTree.mainFrame().docShell(); -+ const cv = docShell.contentViewer; -+ if (type === '') -+ cv.stopEmulatingMedium(); -+ else if (type) -+ cv.emulateMedium(type); -+ this._frameTree.setColorScheme(colorScheme); -+ } -+ -+ _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; -+ } -+ -+ enable() { -+ if (this._enabled) -+ return; -+ -+ this._enabled = true; -+ // 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); -+ } -+ -+ for (const worker of this._frameTree.workers()) -+ this._onWorkerCreated(worker); -+ -+ this._eventListeners.push(...[ -+ 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.addEventListener(this._messageManager, 'pageshow', this._onLoad.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, '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.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._browserRuntime.emit('runtimeConsole', msg)), -+ this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)), -+ this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)), -+ ]); -+ for (const context of this._runtime.executionContexts()) -+ this._onExecutionContextCreated(context); -+ -+ if (this._frameTree.isPageReady()) -+ this._browserPage.emit('pageReady', {}); -+ } -+ -+ _onExecutionContextCreated(executionContext) { -+ this._browserRuntime.emit('runtimeExecutionContextCreated', { -+ executionContextId: executionContext.id(), -+ auxData: executionContext.auxData(), -+ }); -+ } -+ -+ _onExecutionContextDestroyed(executionContext) { -+ this._browserRuntime.emit('runtimeExecutionContextDestroyed', { -+ executionContextId: executionContext.id(), -+ }); -+ } -+ -+ _onWorkerCreated(worker) { -+ const workerData = new WorkerData(this, this._browserChannel, this._sessionId, 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._browserPage.emit('pageEventFired', { -+ frameId: this._frameTree.mainFrame().id(), -+ name: 'DOMContentLoaded', -+ }); -+ this._browserPage.emit('pageEventFired', { -+ frameId: this._frameTree.mainFrame().id(), -+ name: 'load', -+ }); -+ } -+ } -+ -+ _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) { -+ 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 ? 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(event) { -+ const docShell = event.target.ownerGlobal.docShell; -+ const frame = this._frameTree.frameForDocShell(docShell); -+ if (!frame) -+ return; -+ 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, -+ }); -+ } -+ -+ _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(), -+ }); -+ } -+ -+ _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); -+ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; -+ } -+ -+ async _goBack({frameId, url}) { -+ const frame = this._frameTree.frame(frameId); -+ const docShell = frame.docShell(); -+ if (!docShell.canGoBack) -+ return {navigationId: null, navigationURL: null}; -+ docShell.goBack(); -+ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; -+ } -+ -+ async _goForward({frameId, url}) { -+ const frame = this._frameTree.frame(frameId); -+ const docShell = frame.docShell(); -+ if (!docShell.canGoForward) -+ return {navigationId: null, navigationURL: null}; -+ docShell.goForward(); -+ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()}; -+ } -+ -+ 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 type does not support scrollRectIntoViewIfNeeded'); -+ } -+ -+ _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 _getBoundingBox({frameId, objectId}) { -+ 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 box = this._getNodeBoundingBox(unsafeObject); -+ if (!box) -+ return {boundingBox: null}; -+ return {boundingBox: {x: box.x + frame.domWindow().scrollX, y: box.y + frame.domWindow().scrollY, width: box.width, height: box.height}}; -+ } -+ -+ async _screenshot({mimeType, fullPage, clip}) { -+ const content = this._messageManager.content; -+ if (clip) { -+ const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType); -+ return {data}; -+ } -+ if (fullPage) { -+ const rect = content.document.documentElement.getBoundingClientRect(); -+ const width = content.innerWidth + content.scrollMaxX - content.scrollMinX; -+ const height = content.innerHeight + content.scrollMaxY - content.scrollMinY; -+ const data = takeScreenshot(content, 0, 0, width, height, mimeType); -+ return {data}; -+ } -+ const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType); -+ return {data}; -+ } -+ -+ async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) { -+ // key events don't fire if we are dragging. -+ if (this._dataTransfer) { -+ 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}; -+ } -+ -+ _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 */, -+ this._dataTransfer -+ ); -+ -+ window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event); -+ if (type === 'drop') -+ dragService.endDragSession(true); -+ } -+ -+ _cancelDragIfNeeded() { -+ this._dataTransfer = null; -+ const sess = dragService.getCurrentSession(); -+ if (sess) -+ dragService.endDragSession(false); -+ } -+ -+ async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) { -+ this._startDragSessionIfNeeded(); -+ const trapDrag = subject => { -+ this._dataTransfer = subject.mozCloneForEvent('drop'); -+ } -+ -+ 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._dataTransfer) { -+ 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) { -+ const MAX_SKIA_DIMENSIONS = 32767; -+ -+ const scale = 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/juggler/content/Runtime.js b/juggler/content/Runtime.js -new file mode 100644 -index 0000000000000000000000000000000000000000..9ed30684e25c7943a72b37d4289ce1f4629428e0 ---- /dev/null -+++ b/juggler/content/Runtime.js -@@ -0,0 +1,537 @@ -+"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.flags & Ci.nsIScriptError.exceptionFlag)) { -+ 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.windowUtils.currentInnerWindowID === 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/juggler/content/ScrollbarManager.js b/juggler/content/ScrollbarManager.js -new file mode 100644 -index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c6430983568d ---- /dev/null -+++ b/juggler/content/ScrollbarManager.js -@@ -0,0 +1,85 @@ -+const Ci = Components.interfaces; -+const Cr = Components.results; -+const Cu = Components.utils; -+const Cc = Components.classes; -+ -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -+ -+const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css'); -+const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css'); -+ -+const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless; -+const helper = new Helper(); -+ -+class ScrollbarManager { -+ constructor(docShell) { -+ this._docShell = docShell; -+ this._customScrollbars = null; -+ this._contentViewerScrollBars = new Map(); -+ -+ if (isHeadless) -+ this._setCustomScrollbars(HIDDEN_SCROLLBARS); -+ -+ const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor) -+ .getInterface(Ci.nsIWebProgress); -+ -+ this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']); -+ this._eventListeners = [ -+ helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL), -+ ]; -+ } -+ -+ onLocationChange(webProgress, request, URI, flags) { -+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) -+ return; -+ this._updateAllDocShells(); -+ } -+ -+ setFloatingScrollbars(enabled) { -+ if (this._customScrollbars === HIDDEN_SCROLLBARS) -+ return; -+ this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null); -+ } -+ -+ _setCustomScrollbars(customScrollbars) { -+ if (this._customScrollbars === customScrollbars) -+ return; -+ this._customScrollbars = customScrollbars; -+ this._updateAllDocShells(); -+ } -+ -+ _updateAllDocShells() { -+ const allDocShells = [this._docShell]; -+ for (let i = 0; i < this._docShell.childCount; i++) -+ allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell)); -+ // At this point, a content viewer might not be loaded for certain docShells. -+ // Scrollbars will be updated in onLocationChange. -+ const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer); -+ -+ // Update scrollbar stylesheets. -+ for (const contentViewer of contentViewers) { -+ const oldScrollbars = this._contentViewerScrollBars.get(contentViewer); -+ if (oldScrollbars === this._customScrollbars) -+ continue; -+ const winUtils = contentViewer.DOMDocument.defaultView.windowUtils; -+ if (oldScrollbars) -+ winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET); -+ if (this._customScrollbars) -+ winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET); -+ } -+ // Update state for all *existing* docShells. -+ this._contentViewerScrollBars.clear(); -+ for (const contentViewer of contentViewers) -+ this._contentViewerScrollBars.set(contentViewer, this._customScrollbars); -+ } -+ -+ dispose() { -+ this._setCustomScrollbars(null); -+ helper.removeListeners(this._eventListeners); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['ScrollbarManager']; -+this.ScrollbarManager = ScrollbarManager; -+ -diff --git a/juggler/content/WorkerMain.js b/juggler/content/WorkerMain.js -new file mode 100644 -index 0000000000000000000000000000000000000000..fb35b515e58829d86bb74f36eecd8dc885102d69 ---- /dev/null -+++ b/juggler/content/WorkerMain.js -@@ -0,0 +1,83 @@ -+"use strict"; -+loadSubScript('chrome://juggler/content/content/Runtime.js'); -+loadSubScript('chrome://juggler/content/SimpleChannel.js'); -+ -+const runtimeAgents = new Map(); -+ -+const channel = new SimpleChannel('worker::worker'); -+const eventListener = event => channel._onMessage(JSON.parse(event.data)); -+this.addEventListener('message', eventListener); -+channel.transport = { -+ 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, sessionId) { -+ this._runtime = runtime; -+ this._browserRuntime = channel.connect(sessionId + 'runtime'); -+ this._eventListeners = [ -+ channel.register(sessionId + '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), -+ }), -+ 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)), -+ ]; -+ for (const context of this._runtime.executionContexts()) -+ this._onExecutionContextCreated(context); -+ } -+ -+ _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 = []; -+ } -+} -+ -+channel.register('', { -+ attach: ({sessionId}) => { -+ const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId); -+ runtimeAgents.set(sessionId, runtimeAgent); -+ }, -+ -+ detach: ({sessionId}) => { -+ const runtimeAgent = runtimeAgents.get(sessionId); -+ runtimeAgents.delete(sessionId); -+ runtimeAgent.dispose(); -+ }, -+}); -+ -diff --git a/juggler/content/floating-scrollbars.css b/juggler/content/floating-scrollbars.css -new file mode 100644 -index 0000000000000000000000000000000000000000..7709bdd34c65062fc63684ef17fc792d3991d965 ---- /dev/null -+++ b/juggler/content/floating-scrollbars.css -@@ -0,0 +1,47 @@ -+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -+@namespace html url("http://www.w3.org/1999/xhtml"); -+ -+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars -+ inside a . */ -+*|*:not(html|select) > scrollbar { -+ -moz-appearance: none !important; -+ position: relative; -+ background-color: transparent; -+ background-image: none; -+ z-index: 2147483647; -+ padding: 2px; -+ border: none; -+} -+ -+/* Scrollbar code will reset the margin to the correct side depending on -+ where layout actually puts the scrollbar */ -+*|*:not(html|select) > scrollbar[orient="vertical"] { -+ margin-left: -10px; -+ min-width: 10px; -+ max-width: 10px; -+} -+ -+*|*:not(html|select) > scrollbar[orient="horizontal"] { -+ margin-top: -10px; -+ min-height: 10px; -+ max-height: 10px; -+} -+ -+*|*:not(html|select) > scrollbar slider { -+ -moz-appearance: none !important; -+} -+ -+*|*:not(html|select) > scrollbar thumb { -+ -moz-appearance: none !important; -+ background-color: rgba(0,0,0,0.2); -+ border-width: 0px !important; -+ border-radius: 3px !important; -+} -+ -+*|*:not(html|select) > scrollbar scrollbarbutton, -+*|*:not(html|select) > scrollbar gripper { -+ display: none; -+} -diff --git a/juggler/content/hidden-scrollbars.css b/juggler/content/hidden-scrollbars.css -new file mode 100644 -index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402dfd2ac4f6 ---- /dev/null -+++ b/juggler/content/hidden-scrollbars.css -@@ -0,0 +1,13 @@ -+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -+@namespace html url("http://www.w3.org/1999/xhtml"); -+ -+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars -+ inside a . */ -+*|*:not(html|select) > scrollbar { -+ -moz-appearance: none !important; -+ display: none; -+} -+ -diff --git a/juggler/content/main.js b/juggler/content/main.js -new file mode 100644 -index 0000000000000000000000000000000000000000..70c47bb426876f4a89709ba61460149dae370efe ---- /dev/null -+++ b/juggler/content/main.js -@@ -0,0 +1,188 @@ -+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 {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js'); -+const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js'); -+const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js'); -+const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js'); -+ -+const scrollbarManager = new ScrollbarManager(docShell); -+let frameTree; -+let networkMonitor; -+const helper = new Helper(); -+const messageManager = this; -+ -+const sessions = new Map(); -+ -+function createContentSession(channel, sessionId) { -+ const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree, networkMonitor); -+ sessions.set(sessionId, [pageAgent]); -+ pageAgent.enable(); -+} -+ -+function disposeContentSession(sessionId) { -+ const handlers = sessions.get(sessionId); -+ sessions.delete(sessionId); -+ for (const handler of handlers) -+ handler.dispose(); -+} -+ -+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; -+ }, -+ -+ userAgent: (userAgent) => { -+ docShell.browsingContext.customUserAgent = userAgent; -+ }, -+ -+ bypassCSP: (bypassCSP) => { -+ docShell.bypassCSPEnabled = bypassCSP; -+ }, -+ -+ timezoneId: (timezoneId) => { -+ failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId); -+ }, -+ -+ locale: (locale) => { -+ docShell.languageOverride = locale; -+ }, -+ -+ javaScriptDisabled: (javaScriptDisabled) => { -+ docShell.allowJavascript = !javaScriptDisabled; -+ }, -+ -+ hasTouch: (hasTouch) => { -+ docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE; -+ }, -+ -+ colorScheme: (colorScheme) => { -+ frameTree.setColorScheme(colorScheme); -+ }, -+ -+ deviceScaleFactor: (deviceScaleFactor) => { -+ docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX; -+ docShell.deviceSizeIsPageSize = !!deviceScaleFactor; -+ }, -+}; -+ -+function initialize() { -+ const loadContext = docShell.QueryInterface(Ci.nsILoadContext); -+ const userContextId = loadContext.originAttributes.userContextId; -+ -+ const response = sendSyncMessage('juggler:content-ready', { userContextId })[0]; -+ const { -+ sessionIds = [], -+ scriptsToEvaluateOnNewDocument = [], -+ bindings = [], -+ settings = {} -+ } = response || {}; -+ -+ // Enforce focused state for all top level documents. -+ docShell.overrideHasFocus = 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); -+ networkMonitor = new NetworkMonitor(docShell, frameTree); -+ -+ const channel = SimpleChannel.createForMessageManager('content::page', messageManager); -+ -+ for (const sessionId of sessionIds) -+ createContentSession(channel, sessionId); -+ -+ channel.register('', { -+ attach({sessionId}) { -+ createContentSession(channel, sessionId); -+ }, -+ -+ detach({sessionId}) { -+ disposeContentSession(sessionId); -+ }, -+ -+ 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}) { -+ 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); -+ channel.dispose(); -+ -+ for (const sessionId of sessions.keys()) -+ disposeContentSession(sessionId); -+ -+ scrollbarManager.dispose(); -+ networkMonitor.dispose(); -+ frameTree.dispose(); -+ }), -+ ]; -+} -+ -+initialize(); -diff --git a/juggler/jar.mn b/juggler/jar.mn -new file mode 100644 -index 0000000000000000000000000000000000000000..ec78981943bcaa615806b6da18b8c373ba7f23b2 ---- /dev/null -+++ b/juggler/jar.mn -@@ -0,0 +1,28 @@ -+# 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/RuntimeHandler.js (protocol/RuntimeHandler.js) -+ content/protocol/NetworkHandler.js (protocol/NetworkHandler.js) -+ content/protocol/BrowserHandler.js (protocol/BrowserHandler.js) -+ content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js) -+ content/content/main.js (content/main.js) -+ content/content/FrameTree.js (content/FrameTree.js) -+ content/content/NetworkMonitor.js (content/NetworkMonitor.js) -+ content/content/PageAgent.js (content/PageAgent.js) -+ content/content/Runtime.js (content/Runtime.js) -+ content/content/WorkerMain.js (content/WorkerMain.js) -+ content/content/ScrollbarManager.js (content/ScrollbarManager.js) -+ content/content/floating-scrollbars.css (content/floating-scrollbars.css) -+ content/content/hidden-scrollbars.css (content/hidden-scrollbars.css) -+ -diff --git a/juggler/moz.build b/juggler/moz.build -new file mode 100644 -index 0000000000000000000000000000000000000000..1a0a3130bf9509829744fadc692a79754fddd351 ---- /dev/null -+++ b/juggler/moz.build -@@ -0,0 +1,15 @@ -+# 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"] -+ -+JAR_MANIFESTS += ["jar.mn"] -+#JS_PREFERENCE_FILES += ["prefs/marionette.js"] -+ -+#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"] -+#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] -+ -+with Files("**"): -+ BUG_COMPONENT = ("Testing", "Juggler") -+ -diff --git a/juggler/protocol/AccessibilityHandler.js b/juggler/protocol/AccessibilityHandler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..bf37558bccc48f4d90eadc971c1eb3e44d8e92f0 ---- /dev/null -+++ b/juggler/protocol/AccessibilityHandler.js -@@ -0,0 +1,16 @@ -+class AccessibilityHandler { -+ constructor(session, contentChannel) { -+ this._contentPage = contentChannel.connect(session.sessionId() + 'page'); -+ } -+ -+ async getFullAXTree(params) { -+ return await this._contentPage.send('getFullAXTree', params); -+ } -+ -+ dispose() { -+ this._contentPage.dispose(); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['AccessibilityHandler']; -+this.AccessibilityHandler = AccessibilityHandler; -diff --git a/juggler/protocol/BrowserHandler.js b/juggler/protocol/BrowserHandler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..ad4bfcf776c2ae97957c8b0e675f920371955728 ---- /dev/null -+++ b/juggler/protocol/BrowserHandler.js -@@ -0,0 +1,239 @@ -+"use strict"; -+ -+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 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 enable({attachToDefaultContext}) { -+ if (this._enabled) -+ return; -+ this._enabled = true; -+ this._attachToDefaultContext = attachToDefaultContext; -+ -+ for (const target of this._targetRegistry.targets()) { -+ if (!this._shouldAttachToTarget(target)) -+ continue; -+ const session = this._dispatcher.createSession(); -+ target.connectSession(session); -+ this._attachedSessions.set(target, session); -+ this._session.emitEvent('Browser.attachedToTarget', { -+ sessionId: session.sessionId(), -+ targetInfo: target.info() -+ }); -+ } -+ -+ 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)), -+ ]; -+ } -+ -+ async 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 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) { -+ target.disconnectSession(session); -+ 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 (!target._browserContext) -+ return false; -+ if (this._createdBrowserContextIds.has(target._browserContext.browserContextId)) -+ return true; -+ return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext(); -+ } -+ -+ _onTargetCreated({sessions, target}) { -+ if (!this._shouldAttachToTarget(target)) -+ return; -+ const session = this._dispatcher.createSession(); -+ this._attachedSessions.set(target, session); -+ this._session.emitEvent('Browser.attachedToTarget', { -+ sessionId: session.sessionId(), -+ targetInfo: target.info() -+ }); -+ sessions.push(session); -+ } -+ -+ _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 newPage({browserContextId}) { -+ const targetId = await this._targetRegistry.newPage({browserContextId}); -+ return {targetId}; -+ } -+ -+ async close() { -+ this._onclose(); -+ let browserWindow = Services.wm.getMostRecentWindow( -+ "navigator:browser" -+ ); -+ if (browserWindow && browserWindow.gBrowserInit) { -+ await browserWindow.gBrowserInit.idleTasksFinishedPromise; -+ } -+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit); -+ } -+ -+ async grantPermissions({browserContextId, origin, permissions}) { -+ await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions); -+ } -+ -+ resetPermissions({browserContextId}) { -+ this._targetRegistry.browserContextForId(browserContextId).resetPermissions(); -+ } -+ -+ setExtraHTTPHeaders({browserContextId, headers}) { -+ this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers; -+ } -+ -+ setHTTPCredentials({browserContextId, credentials}) { -+ this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials); -+ } -+ -+ setRequestInterception({browserContextId, enabled}) { -+ this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled; -+ } -+ -+ setIgnoreHTTPSErrors({browserContextId, ignoreHTTPSErrors}) { -+ this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors)); -+ } -+ -+ setDownloadOptions({browserContextId, downloadOptions}) { -+ this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions); -+ } -+ -+ async setGeolocationOverride({browserContextId, geolocation}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation)); -+ } -+ -+ async setOnlineOverride({browserContextId, override}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override)); -+ } -+ -+ async setColorScheme({browserContextId, colorScheme}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('colorScheme', nullToUndefined(colorScheme)); -+ } -+ -+ async setUserAgentOverride({browserContextId, userAgent}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('userAgent', nullToUndefined(userAgent)); -+ } -+ -+ async setBypassCSP({browserContextId, bypassCSP}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP)); -+ } -+ -+ async setJavaScriptDisabled({browserContextId, javaScriptDisabled}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled)); -+ } -+ -+ async setLocaleOverride({browserContextId, locale}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale)); -+ } -+ -+ async setTimezoneOverride({browserContextId, timezoneId}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId)); -+ } -+ -+ async setTouchOverride({browserContextId, hasTouch}) { -+ await this._targetRegistry.browserContextForId(browserContextId).applySetting('hasTouch', nullToUndefined(hasTouch)); -+ } -+ -+ async setDefaultViewport({browserContextId, viewport}) { -+ await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport)); -+ } -+ -+ async addScriptToEvaluateOnNewDocument({browserContextId, script}) { -+ await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script); -+ } -+ -+ async addBinding({browserContextId, name, script}) { -+ await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script); -+ } -+ -+ setCookies({browserContextId, cookies}) { -+ this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies); -+ } -+ -+ clearCookies({browserContextId}) { -+ this._targetRegistry.browserContextForId(browserContextId).clearCookies(); -+ } -+ -+ getCookies({browserContextId}) { -+ const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies(); -+ return {cookies}; -+ } -+ -+ async getInfo() { -+ const version = Components.classes["@mozilla.org/xre/app-info;1"] -+ .getService(Components.interfaces.nsIXULAppInfo) -+ .version; -+ const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"] -+ .getService(Components.interfaces.nsIHttpProtocolHandler) -+ .userAgent; -+ return {version: 'Firefox/' + version, userAgent}; -+ } -+} -+ -+function nullToUndefined(value) { -+ return value === null ? undefined : value; -+} -+ -+var EXPORTED_SYMBOLS = ['BrowserHandler']; -+this.BrowserHandler = BrowserHandler; -diff --git a/juggler/protocol/Dispatcher.js b/juggler/protocol/Dispatcher.js -new file mode 100644 -index 0000000000000000000000000000000000000000..0b28a9568877d99967b2ad845df3eb5904a7a508 ---- /dev/null -+++ b/juggler/protocol/Dispatcher.js -@@ -0,0 +1,135 @@ -+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) { -+ session.dispose(); -+ this._sessions.delete(session.sessionId()); -+ } -+ -+ _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(domain, methodName, 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._handlers = new Map(); -+ } -+ -+ sessionId() { -+ return this._sessionId; -+ } -+ -+ registerHandler(domainName, handler) { -+ this._handlers.set(domainName, handler); -+ } -+ -+ dispose() { -+ for (const [domainName, handler] of this._handlers) { -+ if (typeof handler.dispose !== 'function') -+ throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`); -+ handler.dispose(); -+ } -+ this._handlers.clear(); -+ 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(domainName, methodName, params) { -+ const handler = this._handlers.get(domainName); -+ if (!handler) -+ throw new Error(`Domain "${domainName}" does not exist`); -+ if (!handler[methodName]) -+ throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`); -+ return await handler[methodName](params); -+ } -+} -+ -+this.EXPORTED_SYMBOLS = ['Dispatcher']; -+this.Dispatcher = Dispatcher; -+ -diff --git a/juggler/protocol/NetworkHandler.js b/juggler/protocol/NetworkHandler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..10ce1e9eb24879426ca11a21ffeb89f3567ea078 ---- /dev/null -+++ b/juggler/protocol/NetworkHandler.js -@@ -0,0 +1,158 @@ -+"use strict"; -+ -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.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(); -+ -+class NetworkHandler { -+ constructor(target, session, contentChannel) { -+ this._session = session; -+ this._contentPage = contentChannel.connect(session.sessionId() + 'page'); -+ this._httpActivity = new Map(); -+ this._enabled = false; -+ this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target); -+ this._requestInterception = false; -+ this._eventListeners = []; -+ this._pendingRequstWillBeSentEvents = new Set(); -+ this._requestIdToFrameId = new Map(); -+ } -+ -+ async enable() { -+ if (this._enabled) -+ return; -+ this._enabled = true; -+ this._eventListeners = [ -+ helper.on(this._pageNetwork, PageNetwork.Events.Request, this._onRequest.bind(this)), -+ helper.on(this._pageNetwork, PageNetwork.Events.Response, this._onResponse.bind(this)), -+ helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._onRequestFinished.bind(this)), -+ helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._onRequestFailed.bind(this)), -+ this._pageNetwork.addSession(), -+ ]; -+ } -+ -+ async getResponseBody({requestId}) { -+ return this._pageNetwork.getResponseBody(requestId); -+ } -+ -+ async setExtraHTTPHeaders({headers}) { -+ this._pageNetwork.setExtraHTTPHeaders(headers); -+ } -+ -+ async setRequestInterception({enabled}) { -+ if (enabled) -+ this._pageNetwork.enableRequestInterception(); -+ else -+ this._pageNetwork.disableRequestInterception(); -+ // Right after we enable/disable request interception we need to await all pending -+ // requestWillBeSent events before successfully returning from the method. -+ await Promise.all(Array.from(this._pendingRequstWillBeSentEvents)); -+ } -+ -+ async resumeInterceptedRequest({requestId, method, headers, postData}) { -+ this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData); -+ } -+ -+ async abortInterceptedRequest({requestId, errorCode}) { -+ this._pageNetwork.abortInterceptedRequest(requestId, errorCode); -+ } -+ -+ async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) { -+ this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body); -+ } -+ -+ dispose() { -+ this._contentPage.dispose(); -+ helper.removeListeners(this._eventListeners); -+ } -+ -+ _ensureHTTPActivity(requestId) { -+ let activity = this._httpActivity.get(requestId); -+ if (!activity) { -+ activity = { -+ _id: requestId, -+ _lastSentEvent: null, -+ request: null, -+ response: null, -+ complete: null, -+ failed: null, -+ }; -+ this._httpActivity.set(requestId, activity); -+ } -+ return activity; -+ } -+ -+ _reportHTTPAcitivityEvents(activity) { -+ // State machine - sending network events. -+ if (!activity._lastSentEvent && activity.request) { -+ this._session.emitEvent('Network.requestWillBeSent', activity.request); -+ activity._lastSentEvent = 'requestWillBeSent'; -+ } -+ if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) { -+ this._session.emitEvent('Network.responseReceived', activity.response); -+ activity._lastSentEvent = 'responseReceived'; -+ } -+ if (activity._lastSentEvent === 'responseReceived' && activity.complete) { -+ this._session.emitEvent('Network.requestFinished', activity.complete); -+ activity._lastSentEvent = 'requestFinished'; -+ } -+ if (activity._lastSentEvent && activity.failed) { -+ this._session.emitEvent('Network.requestFailed', activity.failed); -+ activity._lastSentEvent = 'requestFailed'; -+ } -+ -+ // Clean up if request lifecycle is over. -+ if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed') -+ this._httpActivity.delete(activity._id); -+ } -+ -+ async _onRequest(httpChannel, eventDetails) { -+ let pendingRequestCallback; -+ let pendingRequestPromise = new Promise(x => pendingRequestCallback = x); -+ this._pendingRequstWillBeSentEvents.add(pendingRequestPromise); -+ let details = null; -+ try { -+ details = await this._contentPage.send('requestDetails', {channelId: httpChannel.channelId}); -+ } catch (e) { -+ pendingRequestCallback(); -+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); -+ return; -+ } -+ // Inherit frameId for redirects when details are not available. -+ const frameId = details ? details.frameId : (eventDetails.redirectedFrom ? this._requestIdToFrameId.get(eventDetails.redirectedFrom) : undefined); -+ this._requestIdToFrameId.set(eventDetails.requestId, frameId); -+ const activity = this._ensureHTTPActivity(eventDetails.requestId); -+ activity.request = { -+ frameId, -+ ...eventDetails, -+ }; -+ this._reportHTTPAcitivityEvents(activity); -+ pendingRequestCallback(); -+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise); -+ } -+ -+ async _onResponse(httpChannel, eventDetails) { -+ const activity = this._ensureHTTPActivity(eventDetails.requestId); -+ activity.response = eventDetails; -+ this._reportHTTPAcitivityEvents(activity); -+ } -+ -+ async _onRequestFinished(httpChannel, eventDetails) { -+ const activity = this._ensureHTTPActivity(eventDetails.requestId); -+ activity.complete = eventDetails; -+ this._reportHTTPAcitivityEvents(activity); -+ } -+ -+ async _onRequestFailed(httpChannel, eventDetails) { -+ const activity = this._ensureHTTPActivity(eventDetails.requestId); -+ activity.failed = eventDetails; -+ this._reportHTTPAcitivityEvents(activity); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['NetworkHandler']; -+this.NetworkHandler = NetworkHandler; -diff --git a/juggler/protocol/PageHandler.js b/juggler/protocol/PageHandler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..2ad037e57ac4b0b97d85c55bcd08489d840205c1 ---- /dev/null -+++ b/juggler/protocol/PageHandler.js -@@ -0,0 +1,341 @@ -+"use strict"; -+ -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -+ -+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(); -+ -+class WorkerHandler { -+ constructor(session, contentChannel, workerId) { -+ this._session = session; -+ this._contentWorker = contentChannel.connect(session.sessionId() + workerId); -+ this._workerId = workerId; -+ -+ const emitWrappedProtocolEvent = eventName => { -+ return params => { -+ this._session.emitEvent('Page.dispatchMessageFromWorker', { -+ workerId, -+ message: JSON.stringify({method: eventName, params}), -+ }); -+ } -+ } -+ -+ this._eventListeners = [ -+ contentChannel.register(session.sessionId() + workerId, { -+ runtimeConsole: emitWrappedProtocolEvent('Runtime.console'), -+ 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(session.sessionId() + 'page'); -+ this._workers = new Map(); -+ -+ const emitProtocolEvent = eventName => { -+ return (...args) => this._session.emitEvent(eventName, ...args); -+ } -+ -+ this._eventListeners = [ -+ contentChannel.register(session.sessionId() + 'page', { -+ pageBindingCalled: emitProtocolEvent('Page.bindingCalled'), -+ pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'), -+ pageEventFired: emitProtocolEvent('Page.eventFired'), -+ pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'), -+ pageFrameAttached: emitProtocolEvent('Page.frameAttached'), -+ 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: emitProtocolEvent('Page.ready'), -+ pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'), -+ pageUncaughtError: emitProtocolEvent('Page.uncaughtError'), -+ pageWorkerCreated: this._onWorkerCreated.bind(this), -+ pageWorkerDestroyed: this._onWorkerDestroyed.bind(this), -+ }), -+ ]; -+ this._pageTarget = target; -+ this._browser = target.linkedBrowser(); -+ this._dialogs = new Map(); -+ -+ this._enabled = false; -+ } -+ -+ _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}); -+ } -+ -+ async close({runBeforeUnload}) { -+ // Postpone target close to deliver response in session. -+ Services.tm.dispatchToMainThread(() => { -+ this._pageTarget.close(runBeforeUnload); -+ }); -+ } -+ -+ async enable() { -+ if (this._enabled) -+ return; -+ this._enabled = true; -+ this._updateModalDialogs(); -+ -+ this._eventListeners.push(...[ -+ helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => { -+ // wait for the dialog to be actually added to DOM. -+ await Promise.resolve(); -+ this._updateModalDialogs(); -+ }), -+ helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()), -+ helper.on(this._pageTarget, 'crashed', () => { -+ this._session.emitEvent('Page.crashed', {}); -+ }), -+ ]); -+ } -+ -+ dispose() { -+ this._contentPage.dispose(); -+ helper.removeListeners(this._eventListeners); -+ } -+ -+ async setViewportSize({viewportSize}) { -+ await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize); -+ } -+ -+ _updateModalDialogs() { -+ const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []); -+ for (const dialog of this._dialogs.values()) { -+ if (!prompts.has(dialog.prompt())) { -+ this._dialogs.delete(dialog.id()); -+ this._session.emitEvent('Page.dialogClosed', { -+ dialogId: dialog.id(), -+ }); -+ } 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._session.emitEvent('Page.dialogOpened', { -+ dialogId: dialog.id(), -+ type: dialog.type(), -+ message: dialog.message(), -+ defaultValue: dialog.defaultValue(), -+ }); -+ } -+ } -+ -+ async setFileInputFiles(options) { -+ return await this._contentPage.send('setFileInputFiles', options); -+ } -+ -+ async setEmulatedMedia(options) { -+ return await this._contentPage.send('setEmulatedMedia', options); -+ } -+ -+ async setCacheDisabled(options) { -+ return await this._contentPage.send('setCacheDisabled', options); -+ } -+ -+ async addBinding(options) { -+ return await this._contentPage.send('addBinding', options); -+ } -+ -+ async adoptNode(options) { -+ return await this._contentPage.send('adoptNode', options); -+ } -+ -+ async screenshot(options) { -+ return await this._contentPage.send('screenshot', options); -+ } -+ -+ async getBoundingBox(options) { -+ return await this._contentPage.send('getBoundingBox', options); -+ } -+ -+ async getContentQuads(options) { -+ return await this._contentPage.send('getContentQuads', options); -+ } -+ -+ /** -+ * @param {{frameId: string, url: string}} options -+ */ -+ async navigate(options) { -+ return await this._contentPage.send('navigate', options); -+ } -+ -+ /** -+ * @param {{frameId: string, url: string}} options -+ */ -+ async goBack(options) { -+ return await this._contentPage.send('goBack', options); -+ } -+ -+ /** -+ * @param {{frameId: string, url: string}} options -+ */ -+ async goForward(options) { -+ return await this._contentPage.send('goForward', options); -+ } -+ -+ /** -+ * @param {{frameId: string, url: string}} options -+ */ -+ async reload(options) { -+ return await this._contentPage.send('reload', options); -+ } -+ -+ async describeNode(options) { -+ return await this._contentPage.send('describeNode', options); -+ } -+ -+ async scrollIntoViewIfNeeded(options) { -+ return await this._contentPage.send('scrollIntoViewIfNeeded', options); -+ } -+ -+ async addScriptToEvaluateOnNewDocument(options) { -+ return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options); -+ } -+ -+ async removeScriptToEvaluateOnNewDocument(options) { -+ return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options); -+ } -+ -+ async dispatchKeyEvent(options) { -+ return await this._contentPage.send('dispatchKeyEvent', options); -+ } -+ -+ async dispatchTouchEvent(options) { -+ return await this._contentPage.send('dispatchTouchEvent', options); -+ } -+ -+ async dispatchMouseEvent(options) { -+ return await this._contentPage.send('dispatchMouseEvent', options); -+ } -+ -+ async insertText(options) { -+ return await this._contentPage.send('insertText', options); -+ } -+ -+ async crash(options) { -+ return await this._contentPage.send('crash', options); -+ } -+ -+ async handleDialog({dialogId, accept, promptText}) { -+ const dialog = this._dialogs.get(dialogId); -+ if (!dialog) -+ throw new Error('Failed to find dialog with id = ' + dialogId); -+ if (accept) -+ dialog.accept(promptText); -+ else -+ dialog.dismiss(); -+ } -+ -+ async setInterceptFileChooserDialog(options) { -+ return await this._contentPage.send('setInterceptFileChooserDialog', options); -+ } -+ -+ async 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)); -+ } -+} -+ -+class Dialog { -+ static createIfSupported(prompt) { -+ const type = prompt.args.promptType; -+ switch (type) { -+ case 'alert': -+ case 'prompt': -+ case 'confirm': -+ return new Dialog(prompt, type); -+ 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(); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['PageHandler']; -+this.PageHandler = PageHandler; -diff --git a/juggler/protocol/PrimitiveTypes.js b/juggler/protocol/PrimitiveTypes.js -new file mode 100644 -index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07f95a06fa ---- /dev/null -+++ b/juggler/protocol/PrimitiveTypes.js -@@ -0,0 +1,143 @@ -+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/juggler/protocol/Protocol.js b/juggler/protocol/Protocol.js -new file mode 100644 -index 0000000000000000000000000000000000000000..71649c3462f82d8b327b5913570612a3d99495b8 ---- /dev/null -+++ b/juggler/protocol/Protocol.js -@@ -0,0 +1,846 @@ -+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.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), -+ -+ value: 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, -+}; -+ -+ -+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.String, -+ pageTargetId: t.String, -+ url: t.String, -+ suggestedFileName: t.String, -+ }, -+ 'downloadFinished': { -+ uuid: t.String, -+ canceled: t.Optional(t.Boolean), -+ error: t.Optional(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), -+ }, -+ }, -+ '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'])), -+ }, -+ }, -+ }, -+}; -+ -+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), -+ }, -+ 'requestFinished': { -+ requestId: t.String, -+ }, -+ '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, -+ 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, -+ }, -+ }, -+ -+ 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), -+ }, -+ }, -+ '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: { -+ navigationId: t.Nullable(t.String), -+ navigationURL: t.Nullable(t.String), -+ } -+ }, -+ 'goForward': { -+ params: { -+ frameId: t.String, -+ }, -+ returns: { -+ navigationId: t.Nullable(t.String), -+ navigationURL: t.Nullable(t.String), -+ } -+ }, -+ 'reload': { -+ params: { -+ frameId: t.String, -+ }, -+ returns: { -+ navigationId: t.String, -+ navigationURL: t.String, -+ } -+ }, -+ 'getBoundingBox': { -+ params: { -+ frameId: t.String, -+ objectId: t.String, -+ }, -+ returns: { -+ boundingBox: t.Nullable(pageTypes.Rect), -+ }, -+ }, -+ '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']), -+ fullPage: t.Optional(t.Boolean), -+ clip: t.Optional(pageTypes.Clip), -+ }, -+ 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, -+ } -+ }, -+ '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, -+ }, -+ }, -+ }, -+}; -+ -+ -+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/juggler/protocol/RuntimeHandler.js b/juggler/protocol/RuntimeHandler.js -new file mode 100644 -index 0000000000000000000000000000000000000000..df2bfabfd2f569ac8ccdf5f65497c5c365cb3842 ---- /dev/null -+++ b/juggler/protocol/RuntimeHandler.js -@@ -0,0 +1,52 @@ -+"use strict"; -+ -+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js'); -+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -+ -+const Cc = Components.classes; -+const Ci = Components.interfaces; -+const Cu = Components.utils; -+const helper = new Helper(); -+ -+class RuntimeHandler { -+ constructor(session, contentChannel) { -+ const sessionId = session.sessionId(); -+ this._contentRuntime = contentChannel.connect(sessionId + 'runtime'); -+ -+ const emitProtocolEvent = eventName => { -+ return (...args) => session.emitEvent(eventName, ...args); -+ } -+ -+ this._eventListeners = [ -+ contentChannel.register(sessionId + 'runtime', { -+ runtimeConsole: emitProtocolEvent('Runtime.console'), -+ runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'), -+ runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'), -+ }), -+ ]; -+ } -+ -+ async evaluate(options) { -+ return await this._contentRuntime.send('evaluate', options); -+ } -+ -+ async callFunction(options) { -+ return await this._contentRuntime.send('callFunction', options); -+ } -+ -+ async getObjectProperties(options) { -+ return await this._contentRuntime.send('getObjectProperties', options); -+ } -+ -+ async disposeObject(options) { -+ return await this._contentRuntime.send('disposeObject', options); -+ } -+ -+ dispose() { -+ this._contentRuntime.dispose(); -+ helper.removeListeners(this._eventListeners); -+ } -+} -+ -+var EXPORTED_SYMBOLS = ['RuntimeHandler']; -+this.RuntimeHandler = RuntimeHandler; diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp index 249349388e925bc1832593d6c0c9df642040eb08..33a7c3b7f4c66a08f0fb9a48fd9a16c1ee3eedb1 100644 --- a/parser/html/nsHtml5TreeOpExecutor.cpp diff --git a/browser_patches/prepare_checkout.sh b/browser_patches/prepare_checkout.sh index 2c5b12ddf2..1eadbcce39 100755 --- a/browser_patches/prepare_checkout.sh +++ b/browser_patches/prepare_checkout.sh @@ -31,18 +31,20 @@ FRIENDLY_CHECKOUT_PATH=""; CHECKOUT_PATH="" PATCHES_PATH="" BUILD_NUMBER="" -PLAYWRIGHT_PATH="" +WEBKIT_EXTRA_FOLDER_PATH="" +FIREFOX_EXTRA_FOLDER_PATH="" if [[ ("$1" == "firefox") || ("$1" == "firefox/") || ("$1" == "ff") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/firefox/checkout"; CHECKOUT_PATH="$PWD/firefox/checkout" PATCHES_PATH="$PWD/firefox/patches" + FIREFOX_EXTRA_FOLDER_PATH="$PWD/firefox/juggler" BUILD_NUMBER=$(cat "$PWD/firefox/BUILD_NUMBER") source "./firefox/UPSTREAM_CONFIG.sh" elif [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then FRIENDLY_CHECKOUT_PATH="//browser_patches/webkit/checkout"; CHECKOUT_PATH="$PWD/webkit/checkout" PATCHES_PATH="$PWD/webkit/patches" - PLAYWRIGHT_PATH="$PWD/webkit/src/Tools/Playwright" + WEBKIT_EXTRA_FOLDER_PATH="$PWD/webkit/embedder/Playwright" BUILD_NUMBER=$(cat "$PWD/webkit/BUILD_NUMBER") source "./webkit/UPSTREAM_CONFIG.sh" else @@ -111,10 +113,14 @@ git checkout -b playwright-build echo "-- applying patches" git apply --index $PATCHES_PATH/* -if [[ ("$1" == "webkit") || ("$1" == "webkit/") || ("$1" == "wk") ]]; then -echo "-- adding WebKit embedders" -cp -r $PLAYWRIGHT_PATH Tools -git add Tools/Playwright +if [[ ! -z "${WEBKIT_EXTRA_FOLDER_PATH}" ]]; then + echo "-- adding WebKit embedders" + cp -r "${WEBKIT_EXTRA_FOLDER_PATH}" ./Tools/Playwright + git add Tools/Playwright +elif [[ ! -z "${FIREFOX_EXTRA_FOLDER_PATH}" ]]; then + echo "-- adding juggler" + cp -r "${FIREFOX_EXTRA_FOLDER_PATH}" ./juggler + git add juggler fi git commit -a --author="playwright-devops " -m "chore: bootstrap build #$BUILD_NUMBER" diff --git a/browser_patches/webkit/BUILD_NUMBER b/browser_patches/webkit/BUILD_NUMBER index 6c4b54876b..1e590faccc 100644 --- a/browser_patches/webkit/BUILD_NUMBER +++ b/browser_patches/webkit/BUILD_NUMBER @@ -1 +1 @@ -1253 +1254 diff --git a/browser_patches/webkit/src/Tools/Playwright/Configurations/Base.xcconfig b/browser_patches/webkit/embedder/Playwright/Configurations/Base.xcconfig similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Configurations/Base.xcconfig rename to browser_patches/webkit/embedder/Playwright/Configurations/Base.xcconfig diff --git a/browser_patches/webkit/src/Tools/Playwright/Configurations/DebugRelease.xcconfig b/browser_patches/webkit/embedder/Playwright/Configurations/DebugRelease.xcconfig similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Configurations/DebugRelease.xcconfig rename to browser_patches/webkit/embedder/Playwright/Configurations/DebugRelease.xcconfig diff --git a/browser_patches/webkit/src/Tools/Playwright/Configurations/Playwright.xcconfig b/browser_patches/webkit/embedder/Playwright/Configurations/Playwright.xcconfig similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Configurations/Playwright.xcconfig rename to browser_patches/webkit/embedder/Playwright/Configurations/Playwright.xcconfig diff --git a/browser_patches/webkit/src/Tools/Playwright/Configurations/SDKVariant.xcconfig b/browser_patches/webkit/embedder/Playwright/Configurations/SDKVariant.xcconfig similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Configurations/SDKVariant.xcconfig rename to browser_patches/webkit/embedder/Playwright/Configurations/SDKVariant.xcconfig diff --git a/browser_patches/webkit/src/Tools/Playwright/MBToolbarItem.h b/browser_patches/webkit/embedder/Playwright/MBToolbarItem.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/MBToolbarItem.h rename to browser_patches/webkit/embedder/Playwright/MBToolbarItem.h diff --git a/browser_patches/webkit/src/Tools/Playwright/MBToolbarItem.m b/browser_patches/webkit/embedder/Playwright/MBToolbarItem.m similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/MBToolbarItem.m rename to browser_patches/webkit/embedder/Playwright/MBToolbarItem.m diff --git a/browser_patches/webkit/src/Tools/Playwright/Makefile b/browser_patches/webkit/embedder/Playwright/Makefile similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Makefile rename to browser_patches/webkit/embedder/Playwright/Makefile diff --git a/browser_patches/webkit/src/Tools/Playwright/Playwright.xcodeproj/project.pbxproj b/browser_patches/webkit/embedder/Playwright/Playwright.xcodeproj/project.pbxproj similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Playwright.xcodeproj/project.pbxproj rename to browser_patches/webkit/embedder/Playwright/Playwright.xcodeproj/project.pbxproj diff --git a/browser_patches/webkit/src/Tools/Playwright/Playwright.xcodeproj/xcshareddata/xcschemes/Playwright.xcscheme b/browser_patches/webkit/embedder/Playwright/Playwright.xcodeproj/xcshareddata/xcschemes/Playwright.xcscheme similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/Playwright.xcodeproj/xcshareddata/xcschemes/Playwright.xcscheme rename to browser_patches/webkit/embedder/Playwright/Playwright.xcodeproj/xcshareddata/xcschemes/Playwright.xcscheme diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/AppDelegate.h b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/AppDelegate.h rename to browser_patches/webkit/embedder/Playwright/mac/AppDelegate.h diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/AppDelegate.m b/browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/AppDelegate.m rename to browser_patches/webkit/embedder/Playwright/mac/AppDelegate.m diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindow.xib b/browser_patches/webkit/embedder/Playwright/mac/BrowserWindow.xib similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindow.xib rename to browser_patches/webkit/embedder/Playwright/mac/BrowserWindow.xib diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindowController.h b/browser_patches/webkit/embedder/Playwright/mac/BrowserWindowController.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindowController.h rename to browser_patches/webkit/embedder/Playwright/mac/BrowserWindowController.h diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindowController.m b/browser_patches/webkit/embedder/Playwright/mac/BrowserWindowController.m similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/BrowserWindowController.m rename to browser_patches/webkit/embedder/Playwright/mac/BrowserWindowController.m diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/CMakeLists.txt b/browser_patches/webkit/embedder/Playwright/mac/CMakeLists.txt similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/CMakeLists.txt rename to browser_patches/webkit/embedder/Playwright/mac/CMakeLists.txt diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/Info.plist b/browser_patches/webkit/embedder/Playwright/mac/Info.plist similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/Info.plist rename to browser_patches/webkit/embedder/Playwright/mac/Info.plist diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/MainMenu.xib b/browser_patches/webkit/embedder/Playwright/mac/MainMenu.xib similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/MainMenu.xib rename to browser_patches/webkit/embedder/Playwright/mac/MainMenu.xib diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/Playwright_Prefix.pch b/browser_patches/webkit/embedder/Playwright/mac/Playwright_Prefix.pch similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/Playwright_Prefix.pch rename to browser_patches/webkit/embedder/Playwright/mac/Playwright_Prefix.pch diff --git a/browser_patches/webkit/src/Tools/Playwright/mac/main.m b/browser_patches/webkit/embedder/Playwright/mac/main.m similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/mac/main.m rename to browser_patches/webkit/embedder/Playwright/mac/main.m diff --git a/browser_patches/webkit/src/Tools/Playwright/win/CMakeLists.txt b/browser_patches/webkit/embedder/Playwright/win/CMakeLists.txt similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/CMakeLists.txt rename to browser_patches/webkit/embedder/Playwright/win/CMakeLists.txt diff --git a/browser_patches/webkit/src/Tools/Playwright/win/Common.cpp b/browser_patches/webkit/embedder/Playwright/win/Common.cpp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/Common.cpp rename to browser_patches/webkit/embedder/Playwright/win/Common.cpp diff --git a/browser_patches/webkit/src/Tools/Playwright/win/Common.h b/browser_patches/webkit/embedder/Playwright/win/Common.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/Common.h rename to browser_patches/webkit/embedder/Playwright/win/Common.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/DialogHelper.h b/browser_patches/webkit/embedder/Playwright/win/DialogHelper.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/DialogHelper.h rename to browser_patches/webkit/embedder/Playwright/win/DialogHelper.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/MainWindow.cpp b/browser_patches/webkit/embedder/Playwright/win/MainWindow.cpp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/MainWindow.cpp rename to browser_patches/webkit/embedder/Playwright/win/MainWindow.cpp diff --git a/browser_patches/webkit/src/Tools/Playwright/win/MainWindow.h b/browser_patches/webkit/embedder/Playwright/win/MainWindow.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/MainWindow.h rename to browser_patches/webkit/embedder/Playwright/win/MainWindow.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/Playwright.ico b/browser_patches/webkit/embedder/Playwright/win/Playwright.ico similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/Playwright.ico rename to browser_patches/webkit/embedder/Playwright/win/Playwright.ico diff --git a/browser_patches/webkit/src/Tools/Playwright/win/Playwright.rc b/browser_patches/webkit/embedder/Playwright/win/Playwright.rc similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/Playwright.rc rename to browser_patches/webkit/embedder/Playwright/win/Playwright.rc diff --git a/browser_patches/webkit/src/Tools/Playwright/win/PlaywrightLib.rc b/browser_patches/webkit/embedder/Playwright/win/PlaywrightLib.rc similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/PlaywrightLib.rc rename to browser_patches/webkit/embedder/Playwright/win/PlaywrightLib.rc diff --git a/browser_patches/webkit/src/Tools/Playwright/win/PlaywrightLibResource.h b/browser_patches/webkit/embedder/Playwright/win/PlaywrightLibResource.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/PlaywrightLibResource.h rename to browser_patches/webkit/embedder/Playwright/win/PlaywrightLibResource.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/PlaywrightReplace.h b/browser_patches/webkit/embedder/Playwright/win/PlaywrightReplace.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/PlaywrightReplace.h rename to browser_patches/webkit/embedder/Playwright/win/PlaywrightReplace.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/PlaywrightResource.h b/browser_patches/webkit/embedder/Playwright/win/PlaywrightResource.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/PlaywrightResource.h rename to browser_patches/webkit/embedder/Playwright/win/PlaywrightResource.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/WebKitBrowserWindow.cpp b/browser_patches/webkit/embedder/Playwright/win/WebKitBrowserWindow.cpp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/WebKitBrowserWindow.cpp rename to browser_patches/webkit/embedder/Playwright/win/WebKitBrowserWindow.cpp diff --git a/browser_patches/webkit/src/Tools/Playwright/win/WebKitBrowserWindow.h b/browser_patches/webkit/embedder/Playwright/win/WebKitBrowserWindow.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/WebKitBrowserWindow.h rename to browser_patches/webkit/embedder/Playwright/win/WebKitBrowserWindow.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/WinMain.cpp b/browser_patches/webkit/embedder/Playwright/win/WinMain.cpp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/WinMain.cpp rename to browser_patches/webkit/embedder/Playwright/win/WinMain.cpp diff --git a/browser_patches/webkit/src/Tools/Playwright/win/resource.h b/browser_patches/webkit/embedder/Playwright/win/resource.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/resource.h rename to browser_patches/webkit/embedder/Playwright/win/resource.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/stdafx.cpp b/browser_patches/webkit/embedder/Playwright/win/stdafx.cpp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/stdafx.cpp rename to browser_patches/webkit/embedder/Playwright/win/stdafx.cpp diff --git a/browser_patches/webkit/src/Tools/Playwright/win/stdafx.h b/browser_patches/webkit/embedder/Playwright/win/stdafx.h similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/stdafx.h rename to browser_patches/webkit/embedder/Playwright/win/stdafx.h diff --git a/browser_patches/webkit/src/Tools/Playwright/win/toolbar.bmp b/browser_patches/webkit/embedder/Playwright/win/toolbar.bmp similarity index 100% rename from browser_patches/webkit/src/Tools/Playwright/win/toolbar.bmp rename to browser_patches/webkit/embedder/Playwright/win/toolbar.bmp