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:
Andrey Lushnikov 2020-06-02 16:51:13 -07:00 committed by GitHub
parent 8e8f9786a7
commit a3f34fb4b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 6346 additions and 6404 deletions

View file

@ -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

View file

@ -1 +1 @@
1100
1101

View 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;

View 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;

View 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;

View 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;

View 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]);

View file

@ -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

View 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",
]

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();
},
});

View file

@ -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;
}

View file

@ -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;
}

View 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();

View 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)

View 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")

View file

@ -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;

View 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;

View 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;

View 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;

View 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;

View 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'];

View 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'];

View 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

View file

@ -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"

View file

@ -1 +1 @@
1253
1254

View file

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View file

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 982 B