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".
This commit is contained in:
parent
8e8f9786a7
commit
a3f34fb4b7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1100
|
||||
1101
|
||||
|
|
|
|||
119
browser_patches/firefox/juggler/Helper.js
Normal file
119
browser_patches/firefox/juggler/Helper.js
Normal file
|
|
@ -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 '<unknown error>';
|
||||
}
|
||||
}
|
||||
|
||||
var EXPORTED_SYMBOLS = [ "Helper" ];
|
||||
this.Helper = Helper;
|
||||
|
||||
800
browser_patches/firefox/juggler/NetworkObserver.js
Normal file
800
browser_patches/firefox/juggler/NetworkObserver.js
Normal file
|
|
@ -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] || '<unknown>',
|
||||
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;
|
||||
134
browser_patches/firefox/juggler/SimpleChannel.js
Normal file
134
browser_patches/firefox/juggler/SimpleChannel.js
Normal file
|
|
@ -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;
|
||||
680
browser_patches/firefox/juggler/TargetRegistry.js
Normal file
680
browser_patches/firefox/juggler/TargetRegistry.js
Normal file
|
|
@ -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;
|
||||
84
browser_patches/firefox/juggler/components/juggler.js
Normal file
84
browser_patches/firefox/juggler/components/juggler.js
Normal file
|
|
@ -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]);
|
||||
|
|
@ -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
|
||||
9
browser_patches/firefox/juggler/components/moz.build
Normal file
9
browser_patches/firefox/juggler/components/moz.build
Normal file
|
|
@ -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",
|
||||
]
|
||||
|
||||
477
browser_patches/firefox/juggler/content/FrameTree.js
Normal file
477
browser_patches/firefox/juggler/content/FrameTree.js
Normal file
|
|
@ -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;
|
||||
|
||||
52
browser_patches/firefox/juggler/content/NetworkMonitor.js
Normal file
52
browser_patches/firefox/juggler/content/NetworkMonitor.js
Normal file
|
|
@ -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;
|
||||
|
||||
981
browser_patches/firefox/juggler/content/PageAgent.js
Normal file
981
browser_patches/firefox/juggler/content/PageAgent.js
Normal file
|
|
@ -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;
|
||||
|
||||
541
browser_patches/firefox/juggler/content/Runtime.js
Normal file
541
browser_patches/firefox/juggler/content/Runtime.js
Normal file
|
|
@ -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;
|
||||
89
browser_patches/firefox/juggler/content/ScrollbarManager.js
Normal file
89
browser_patches/firefox/juggler/content/ScrollbarManager.js
Normal file
|
|
@ -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;
|
||||
|
||||
87
browser_patches/firefox/juggler/content/WorkerMain.js
Normal file
87
browser_patches/firefox/juggler/content/WorkerMain.js
Normal file
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -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 <select> are excluded (including them hides the select arrow on
|
||||
Windows). We want to include both the root scrollbars for the document as
|
||||
well as any overflow: scroll elements within the page, while excluding
|
||||
<select>. */
|
||||
*|*: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;
|
||||
}
|
||||
|
|
@ -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 <select> are excluded (including them hides the select arrow on
|
||||
Windows). We want to include both the root scrollbars for the document as
|
||||
well as any overflow: scroll elements within the page, while excluding
|
||||
<select>. */
|
||||
*|*:not(html|select) > scrollbar {
|
||||
-moz-appearance: none !important;
|
||||
display: none;
|
||||
}
|
||||
|
||||
192
browser_patches/firefox/juggler/content/main.js
Normal file
192
browser_patches/firefox/juggler/content/main.js
Normal file
|
|
@ -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();
|
||||
28
browser_patches/firefox/juggler/jar.mn
Normal file
28
browser_patches/firefox/juggler/jar.mn
Normal file
|
|
@ -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)
|
||||
|
||||
15
browser_patches/firefox/juggler/moz.build
Normal file
15
browser_patches/firefox/juggler/moz.build
Normal file
|
|
@ -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")
|
||||
|
||||
|
|
@ -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;
|
||||
243
browser_patches/firefox/juggler/protocol/BrowserHandler.js
Normal file
243
browser_patches/firefox/juggler/protocol/BrowserHandler.js
Normal file
|
|
@ -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;
|
||||
139
browser_patches/firefox/juggler/protocol/Dispatcher.js
Normal file
139
browser_patches/firefox/juggler/protocol/Dispatcher.js
Normal file
|
|
@ -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;
|
||||
|
||||
162
browser_patches/firefox/juggler/protocol/NetworkHandler.js
Normal file
162
browser_patches/firefox/juggler/protocol/NetworkHandler.js
Normal file
|
|
@ -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;
|
||||
345
browser_patches/firefox/juggler/protocol/PageHandler.js
Normal file
345
browser_patches/firefox/juggler/protocol/PageHandler.js
Normal file
|
|
@ -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;
|
||||
147
browser_patches/firefox/juggler/protocol/PrimitiveTypes.js
Normal file
147
browser_patches/firefox/juggler/protocol/PrimitiveTypes.js
Normal file
|
|
@ -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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
if (Object.is(x, null))
|
||||
return true;
|
||||
return checkScheme(scheme, x, details, path);
|
||||
}
|
||||
}
|
||||
|
||||
t.Optional = function(scheme) {
|
||||
return function(x, details = {}, path = ['<root>']) {
|
||||
if (Object.is(x, undefined))
|
||||
return true;
|
||||
return checkScheme(scheme, x, details, path);
|
||||
}
|
||||
}
|
||||
|
||||
t.Array = function(scheme) {
|
||||
return function(x, details = {}, path = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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 = ['<root>']) {
|
||||
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'];
|
||||
850
browser_patches/firefox/juggler/protocol/Protocol.js
Normal file
850
browser_patches/firefox/juggler/protocol/Protocol.js
Normal file
|
|
@ -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'];
|
||||
56
browser_patches/firefox/juggler/protocol/RuntimeHandler.js
Normal file
56
browser_patches/firefox/juggler/protocol/RuntimeHandler.js
Normal file
|
|
@ -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;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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 <devops@playwright.com>" -m "chore: bootstrap build #$BUILD_NUMBER"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1253
|
||||
1254
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 982 B |
Loading…
Reference in a new issue