playwright/browser_patches/firefox/patches/bootstrap.diff

7045 lines
245 KiB
Diff
Raw Normal View History

diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h
index c6aa1cf44c8ba339704a18ebe92fe5a7751e52f5..cfe64bdda54d49ee5b11b2368a2f9856cc9ea3cf 100644
--- a/accessible/base/NotificationController.h
+++ b/accessible/base/NotificationController.h
@@ -270,6 +270,8 @@ class NotificationController final : public EventQueue,
}
#endif
+ bool IsUpdatePendingForJugglerAccessibility() { return IsUpdatePending(); }
+
protected:
virtual ~NotificationController();
diff --git a/accessible/interfaces/nsIAccessibleDocument.idl b/accessible/interfaces/nsIAccessibleDocument.idl
index a91df31c96afda66f478a5a38eaa4352039c2a0b..ee777c1746284027fb3aa2f1686f8082af9d89ee 100644
--- a/accessible/interfaces/nsIAccessibleDocument.idl
+++ b/accessible/interfaces/nsIAccessibleDocument.idl
@@ -72,4 +72,9 @@ interface nsIAccessibleDocument : nsISupports
* Return the child document accessible at the given index.
*/
nsIAccessibleDocument getChildDocumentAt(in unsigned long index);
+
+ /**
+ * Return whether it is updating.
+ */
+ readonly attribute boolean isUpdatePendingForJugglerAccessibility;
};
diff --git a/accessible/xpcom/xpcAccessibleDocument.cpp b/accessible/xpcom/xpcAccessibleDocument.cpp
index e3dbe73f22252f11080c3f266b2309f842eba9dc..87f50fe3df7cc8f9bc26dabd5ee571cae270912a 100644
--- a/accessible/xpcom/xpcAccessibleDocument.cpp
+++ b/accessible/xpcom/xpcAccessibleDocument.cpp
@@ -143,6 +143,15 @@ xpcAccessibleDocument::GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) {
return NS_OK;
}
+
+NS_IMETHODIMP
+xpcAccessibleDocument::GetIsUpdatePendingForJugglerAccessibility(bool* updating) {
+ NS_ENSURE_ARG_POINTER(updating);
+ *updating = Intl()->Controller()->IsUpdatePendingForJugglerAccessibility();
+ return NS_OK;
+}
+
+
////////////////////////////////////////////////////////////////////////////////
// xpcAccessibleDocument
diff --git a/accessible/xpcom/xpcAccessibleDocument.h b/accessible/xpcom/xpcAccessibleDocument.h
index f042cc1081850ac60e329b70b5569f8b97d4e4dc..65bcff9b41b9471ef1427e3ea330481c194409bc 100644
--- a/accessible/xpcom/xpcAccessibleDocument.h
+++ b/accessible/xpcom/xpcAccessibleDocument.h
@@ -48,6 +48,8 @@ class xpcAccessibleDocument : public xpcAccessibleHyperText,
nsIAccessibleDocument** aDocument) final;
NS_IMETHOD GetVirtualCursor(nsIAccessiblePivot** aVirtualCursor) final;
+ NS_IMETHOD GetIsUpdatePendingForJugglerAccessibility(bool* aUpdating) final;
+
/**
* Return XPCOM wrapper for the internal accessible.
*/
2019-11-19 05:18:28 +03:00
diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
index 25b1dc48f42fac1182c42f3628c138f97f8b97a0..7da6b76cfbee3ee7a51abc04c6a52bd57d554393 100644
2019-11-19 05:18:28 +03:00
--- a/browser/installer/allowed-dupes.mn
+++ b/browser/installer/allowed-dupes.mn
@@ -139,6 +139,11 @@ browser/chrome/browser/res/payments/formautofill/autofillEditForms.js
2019-11-19 05:18:28 +03:00
# Bug 1451050 - Remote settings empty dumps (will be populated with data eventually)
browser/defaults/settings/pinning/pins.json
browser/defaults/settings/main/example.json
+# Juggler/marionette files
+chrome/juggler/content/content/floating-scrollbars.css
+browser/chrome/devtools/skin/floating-scrollbars-responsive-design.css
+chrome/juggler/content/server/stream-utils.js
+chrome/marionette/content/stream-utils.js
#ifdef MOZ_EME_WIN32_ARTIFACT
gmp-clearkey/0.1/manifest.json
i686/gmp-clearkey/0.1/manifest.json
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index 7e7c8e5535fe8ff1cbf7ed030c406d261257d62d..c385e75c99f88cc09feb508355edee3b7504d403 100644
2019-11-19 05:18:28 +03:00
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -209,6 +209,11 @@
2019-11-19 05:18:28 +03:00
@RESPATH@/components/marionette.js
#endif
+@RESPATH@/chrome/juggler@JAREXT@
+@RESPATH@/chrome/juggler.manifest
+@RESPATH@/components/juggler.manifest
+@RESPATH@/components/juggler.js
+
#if defined(ENABLE_TESTS) && defined(MOZ_DEBUG)
@RESPATH@/components/TestInterfaceJS.js
@RESPATH@/components/TestInterfaceJS.manifest
diff --git a/devtools/server/socket/websocket-server.js b/devtools/server/socket/websocket-server.js
index 040c7b124dec6bb254563bbe74fe50012cb077a3..b4e6b8132786af70e8ad0dce88b67c2835307f88 100644
--- a/devtools/server/socket/websocket-server.js
+++ b/devtools/server/socket/websocket-server.js
@@ -133,13 +133,12 @@ function writeHttpResponse(output, response) {
* Process the WebSocket handshake headers and return the key to be sent in
* Sec-WebSocket-Accept response header.
*/
-function processRequest({ requestLine, headers }) {
+function processRequest({ requestLine, headers }, expectedPath) {
const [method, path] = requestLine.split(" ");
if (method !== "GET") {
throw new Error("The handshake request must use GET method");
}
-
- if (path !== "/") {
+ if (path !== expectedPath) {
throw new Error("The handshake request has unknown path");
}
@@ -189,13 +188,13 @@ function computeKey(key) {
/**
* Perform the server part of a WebSocket opening handshake on an incoming connection.
*/
-const serverHandshake = async function(input, output) {
+const serverHandshake = async function(input, output, expectedPath) {
// Read the request
const request = await readHttpRequest(input);
try {
// Check and extract info from the request
- const { acceptKey } = processRequest(request);
+ const { acceptKey } = processRequest(request, expectedPath);
// Send response headers
await writeHttpResponse(output, [
@@ -217,8 +216,8 @@ const serverHandshake = async function(input, output) {
* Performs the WebSocket handshake and waits for the WebSocket to open.
* Returns Promise with a WebSocket ready to send and receive messages.
*/
-const accept = async function(transport, input, output) {
- await serverHandshake(input, output);
+const accept = async function(transport, input, output, expectedPath) {
+ await serverHandshake(input, output, expectedPath || "/");
const transportProvider = {
setListener(upgradeListener) {
2019-11-19 05:18:28 +03:00
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 514a4f2890a20558afe0d9c1aec697612fc8e873..b1ce2962086b0d93a252f8944d86e1b36fc633b7 100644
2019-11-19 05:18:28 +03:00
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -53,6 +53,7 @@
#include "mozilla/dom/ContentFrameMessageManager.h"
#include "mozilla/dom/DocGroup.h"
#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Geolocation.h"
#include "mozilla/dom/HTMLAnchorElement.h"
#include "mozilla/dom/PerformanceNavigation.h"
#include "mozilla/dom/PermissionMessageUtils.h"
@@ -96,6 +97,7 @@
#include "nsIDocShellTreeItem.h"
#include "nsIDocShellTreeOwner.h"
#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
#include "nsIDocumentLoaderFactory.h"
#include "nsIDOMWindow.h"
#include "nsIEditingSession.h"
@@ -350,6 +352,9 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext,
mUseErrorPages(false),
mObserveErrorPages(true),
mCSSErrorReportingEnabled(false),
+ mFileInputInterceptionEnabled(false),
+ mBypassCSPEnabled(false),
+ mOnlineOverride(nsIDocShell::ONLINE_OVERRIDE_NONE),
mAllowAuth(mItemType == typeContent),
mAllowKeywordFixup(false),
mIsOffScreenBrowser(false),
@@ -1219,6 +1224,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
2019-11-19 05:18:28 +03:00
isSubFrame = mLSHE->GetIsSubFrame();
}
+ FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags);
if (!isSubFrame && !isRoot) {
/*
* We don't want to send OnLocationChange notifications when
@@ -3340,6 +3346,109 @@ nsDocShell::GetMessageManager(ContentFrameMessageManager** aMessageManager) {
return NS_OK;
}
+// =============== Juggler Begin =======================
+
+nsDocShell* nsDocShell::GetRootDocShell() {
+ nsCOMPtr<nsIDocShellTreeItem> rootAsItem;
+ GetInProcessSameTypeRootTreeItem(getter_AddRefs(rootAsItem));
+ nsCOMPtr<nsIDocShell> rootShell = do_QueryInterface(rootAsItem);
+ return nsDocShell::Cast(rootShell);
+}
+
+NS_IMETHODIMP
+nsDocShell::GetBypassCSPEnabled(bool* aEnabled) {
+ MOZ_ASSERT(aEnabled);
+ *aEnabled = mBypassCSPEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetBypassCSPEnabled(bool aEnabled) {
+ mBypassCSPEnabled = aEnabled;
+ return NS_OK;
+}
+
+bool nsDocShell::IsBypassCSPEnabled() {
+ return GetRootDocShell()->mBypassCSPEnabled;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetLanguageOverride(nsAString& aLanguageOverride) {
+ MOZ_ASSERT(aEnabled);
+ aLanguageOverride = GetRootDocShell()->mLanguageOverride;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetLanguageOverride(const nsAString& aLanguageOverride) {
+ mLanguageOverride = aLanguageOverride;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetFileInputInterceptionEnabled(bool* aEnabled) {
+ MOZ_ASSERT(aEnabled);
+ *aEnabled = GetRootDocShell()->mFileInputInterceptionEnabled;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetFileInputInterceptionEnabled(bool aEnabled) {
+ mFileInputInterceptionEnabled = aEnabled;
+ return NS_OK;
+}
+
+bool nsDocShell::IsFileInputInterceptionEnabled() {
+ return GetRootDocShell()->mFileInputInterceptionEnabled;
+}
+
+void nsDocShell::FilePickerShown(mozilla::dom::Element* element) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(
+ ToSupports(element), "juggler-file-picker-shown", nullptr);
+}
+
+RefPtr<nsGeolocationService> nsDocShell::GetGeolocationServiceOverride() {
+ return GetRootDocShell()->mGeolocationServiceOverride;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetGeolocationOverride(nsIDOMGeoPosition* aGeolocationOverride) {
+ if (aGeolocationOverride) {
+ if (!mGeolocationServiceOverride) {
+ mGeolocationServiceOverride = new nsGeolocationService();
+ mGeolocationServiceOverride->Init();
+ }
+ mGeolocationServiceOverride->Update(aGeolocationOverride);
+ } else {
+ mGeolocationServiceOverride = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::GetOnlineOverride(OnlineOverride* aOnlineOverride) {
+ *aOnlineOverride = GetRootDocShell()->mOnlineOverride;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::SetOnlineOverride(OnlineOverride aOnlineOverride) {
+ // We don't have a way to verify this coming from Javascript, so this check is
+ // still needed.
+ if (!(aOnlineOverride == ONLINE_OVERRIDE_NONE ||
+ aOnlineOverride == ONLINE_OVERRIDE_ONLINE ||
+ aOnlineOverride == ONLINE_OVERRIDE_OFFLINE)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mOnlineOverride = aOnlineOverride;
+ return NS_OK;
+}
+
+// =============== Juggler End =======================
+
NS_IMETHODIMP
nsDocShell::GetIsNavigating(bool* aOut) {
*aOut = mIsNavigating;
@@ -12137,6 +12246,9 @@ class OnLinkClickEvent : public Runnable {
mNoOpenerImplied, nullptr, nullptr,
mIsUserTriggered, mTriggeringPrincipal, mCsp);
}
+ nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
+ observerService->NotifyObservers(ToSupports(mContent), "juggler-link-click-sync", nullptr);
+
return NS_OK;
}
@@ -12226,6 +12338,9 @@ nsresult nsDocShell::OnLinkClick(
this, aContent, aURI, target, aFileName, aPostDataStream,
aHeadersDataStream, noOpenerImplied, aIsUserTriggered, aIsTrusted,
aTriggeringPrincipal, aCsp);
+
+ nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
+ observerService->NotifyObservers(ToSupports(aContent), "juggler-link-click", nullptr);
return DispatchToTabGroup(TaskCategory::UI, ev.forget());
}
diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
index cc88045201371eb2195a28c60fcd3b6d940e8b72..7fad3529cc7a22b0b2aa8d8cb5ebbb5814aa2490 100644
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -13,6 +13,7 @@
#include "Units.h"
#include "jsapi.h"
#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Geolocation.h"
#include "mozilla/HalScreenConfiguration.h"
#include "mozilla/LinkedList.h"
#include "mozilla/Maybe.h"
@@ -25,6 +26,7 @@
#include "mozilla/UniquePtr.h"
#include "mozilla/WeakPtr.h"
#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Element.h"
#include "mozilla/dom/ChildSHistory.h"
#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
#include "mozilla/dom/WindowProxyHolder.h"
@@ -479,6 +481,13 @@ class nsDocShell final : public nsDocLoader,
void SetWillChangeProcess() { mWillChangeProcess = true; }
+ bool IsFileInputInterceptionEnabled();
+ void FilePickerShown(mozilla::dom::Element* element);
+
+ bool IsBypassCSPEnabled();
+
+ RefPtr<nsGeolocationService> GetGeolocationServiceOverride();
+
// Create a content viewer within this nsDocShell for the given
// `WindowGlobalChild` actor.
nsresult CreateContentViewerForActor(
@@ -1038,6 +1047,8 @@ class nsDocShell final : public nsDocLoader,
bool CSSErrorReportingEnabled() const { return mCSSErrorReportingEnabled; }
+ nsDocShell* GetRootDocShell();
+
// Handles retrieval of subframe session history for nsDocShell::LoadURI. If a
// load is requested in a subframe of the current DocShell, the subframe
// loadType may need to reflect the loadType of the parent document, or in
@@ -1292,6 +1303,12 @@ class nsDocShell final : public nsDocLoader,
bool mUseErrorPages : 1;
bool mObserveErrorPages : 1;
bool mCSSErrorReportingEnabled : 1;
+ bool mFileInputInterceptionEnabled: 1;
+ bool mBypassCSPEnabled : 1;
+ nsString mLanguageOverride;
+ RefPtr<nsGeolocationService> mGeolocationServiceOverride;
+ OnlineOverride mOnlineOverride;
+
bool mAllowAuth : 1;
bool mAllowKeywordFixup : 1;
bool mIsOffScreenBrowser : 1;
diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl
index ee89208c3ada6da09ecda6147e1a372ee0562810..83a70dd59a22abd391f9b2db99837e3e5851db31 100644
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -44,6 +44,7 @@ interface nsIURI;
interface nsIChannel;
interface nsIContentViewer;
interface nsIContentSecurityPolicy;
+interface nsIDOMGeoPosition;
interface nsIDocShellLoadInfo;
interface nsIEditor;
interface nsIEditingSession;
@@ -1129,4 +1130,19 @@ interface nsIDocShell : nsIDocShellTreeItem
* @see nsISHEntry synchronizeLayoutHistoryState().
*/
void synchronizeLayoutHistoryState();
+
+ attribute boolean fileInputInterceptionEnabled;
+
+ attribute boolean bypassCSPEnabled;
+
+ attribute AString languageOverride;
+
+ cenum OnlineOverride: 8 {
+ ONLINE_OVERRIDE_NONE = 0,
+ ONLINE_OVERRIDE_ONLINE = 1,
+ ONLINE_OVERRIDE_OFFLINE = 2,
+ };
+ [infallible] attribute nsIDocShell_OnlineOverride onlineOverride;
+
+ void setGeolocationOverride(in nsIDOMGeoPosition position);
};
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index bc0aeaefb9ca6d7cd15fc5ad189d2d260d519997..e948a5c20308ad8a9f43b3a53c532ee0cf62950c 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -3269,6 +3269,9 @@ void Document::SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages) {
}
void Document::ApplySettingsFromCSP(bool aSpeculative) {
+ if (mDocumentContainer && mDocumentContainer->IsBypassCSPEnabled())
+ return;
+
nsresult rv = NS_OK;
if (!aSpeculative) {
// 1) apply settings from regular CSP
@@ -3318,6 +3321,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) {
return NS_OK;
}
+ nsCOMPtr<nsIDocShell> shell(mDocumentContainer);
+ if (shell && nsDocShell::Cast(shell)->IsBypassCSPEnabled()) {
+ return NS_OK;
+ }
+
// If this is a data document - no need to set CSP.
if (mLoadedAsData) {
return NS_OK;
diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp
index 1388a20edd594c6799e47ed567edb1f7d9cc9224..549026512c86e1e7e81a200d1c45f2cb58b3fc67 100644
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -321,14 +321,18 @@ void Navigator::GetAppName(nsAString& aAppName, CallerType aCallerType) const {
* An empty array will be returned if there is no valid languages.
*/
/* static */
-void Navigator::GetAcceptLanguages(nsTArray<nsString>& aLanguages) {
+void Navigator::GetAcceptLanguages(const nsString* aLanguageOverride, nsTArray<nsString>& aLanguages) {
MOZ_ASSERT(NS_IsMainThread());
aLanguages.Clear();
// E.g. "de-de, en-us,en".
nsAutoString acceptLang;
- Preferences::GetLocalizedString("intl.accept_languages", acceptLang);
+ if (aLanguageOverride && aLanguageOverride->Length())
+ acceptLang = *aLanguageOverride;
+ else
+ Preferences::GetLocalizedString("intl.accept_languages", acceptLang);
+
// Split values on commas.
nsCharSeparatedTokenizer langTokenizer(acceptLang, ',');
@@ -384,7 +388,9 @@ void Navigator::GetLanguage(nsAString& aLanguage) {
}
void Navigator::GetLanguages(nsTArray<nsString>& aLanguages) {
- GetAcceptLanguages(aLanguages);
+ nsString languageOverride;
+ mWindow->GetDocShell()->GetLanguageOverride(languageOverride);
+ GetAcceptLanguages(&languageOverride, aLanguages);
// The returned value is cached by the binding code. The window listen to the
// accept languages change and will clear the cache when needed. It has to
@@ -538,7 +544,13 @@ bool Navigator::CookieEnabled() {
return granted;
}
-bool Navigator::OnLine() { return !NS_IsOffline(); }
+bool Navigator::OnLine() {
+ nsDocShell* docShell = static_cast<nsDocShell*>(GetDocShell());
+ nsIDocShell::OnlineOverride onlineOverride;
+ if (!docShell || docShell->GetOnlineOverride(&onlineOverride) != NS_OK || onlineOverride == nsIDocShell::ONLINE_OVERRIDE_NONE)
+ return !NS_IsOffline();
+ return onlineOverride == nsIDocShell::ONLINE_OVERRIDE_ONLINE;
+}
void Navigator::GetBuildID(nsAString& aBuildID, CallerType aCallerType,
ErrorResult& aRv) const {
diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h
index ab9e3b40096c3b4f453dba6109c2ef7e3134fa53..7a1cdd7d106879446a8631806817eabe797dcdd5 100644
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -218,7 +218,7 @@ class Navigator final : public nsISupports, public nsWrapperCache {
StorageManager* Storage();
- static void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
+ static void GetAcceptLanguages(const nsString* aLanguageOverride, nsTArray<nsString>& aLanguages);
dom::MediaCapabilities* MediaCapabilities();
dom::MediaSession* MediaSession();
diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp
index 51c04d2f40f51c9163183559d6a92ea7b0179e17..72084201c77a4dfeabb9a2a6d42a3348b5aa6485 100644
--- a/dom/geolocation/Geolocation.cpp
+++ b/dom/geolocation/Geolocation.cpp
@@ -23,6 +23,7 @@
#include "nsComponentManagerUtils.h"
#include "nsContentPermissionHelper.h"
#include "nsContentUtils.h"
+#include "nsDocShell.h"
#include "nsGlobalWindow.h"
#include "mozilla/dom/Document.h"
#include "nsINamed.h"
@@ -294,10 +295,8 @@ nsGeolocationRequest::Allow(JS::HandleValue aChoices) {
return NS_OK;
}
- RefPtr<nsGeolocationService> gs =
- nsGeolocationService::GetGeolocationService();
-
- bool canUseCache = false;
+ nsGeolocationService* gs = mLocator->GetGeolocationService();
+ bool canUseCache = gs != nsGeolocationService::sService.get();
CachedPositionAndAccuracy lastPosition = gs->GetCachedPosition();
if (lastPosition.position) {
DOMTimeStamp cachedPositionTime_ms;
@@ -467,8 +466,7 @@ void nsGeolocationRequest::Shutdown() {
// If there are no other high accuracy requests, the geolocation service will
// notify the provider to switch to the default accuracy.
if (mOptions && mOptions->mEnableHighAccuracy) {
- RefPtr<nsGeolocationService> gs =
- nsGeolocationService::GetGeolocationService();
+ nsGeolocationService* gs = mLocator ? mLocator->GetGeolocationService() : nullptr;
if (gs) {
gs->UpdateAccuracy();
}
@@ -745,8 +743,14 @@ void nsGeolocationService::StopDevice() {
StaticRefPtr<nsGeolocationService> nsGeolocationService::sService;
already_AddRefed<nsGeolocationService>
-nsGeolocationService::GetGeolocationService() {
+nsGeolocationService::GetGeolocationService(nsDocShell* docShell) {
RefPtr<nsGeolocationService> result;
+ if (docShell) {
+ result = docShell->GetGeolocationServiceOverride();
+ if (result)
+ return result.forget();
+ }
+
if (nsGeolocationService::sService) {
result = nsGeolocationService::sService;
@@ -838,7 +842,9 @@ nsresult Geolocation::Init(nsPIDOMWindowInner* aContentDom) {
// If no aContentDom was passed into us, we are being used
// by chrome/c++ and have no mOwner, no mPrincipal, and no need
// to prompt.
- mService = nsGeolocationService::GetGeolocationService();
+ nsCOMPtr<Document> doc = aContentDom ? aContentDom->GetDoc() : nullptr;
+ mService = nsGeolocationService::GetGeolocationService(
+ doc ? static_cast<nsDocShell*>(doc->GetDocShell()) : nullptr);
if (mService) {
mService->AddLocator(this);
}
diff --git a/dom/geolocation/Geolocation.h b/dom/geolocation/Geolocation.h
index d92bd1c738016f93c66dbdc449c70937c37b6f9a..a4c1f0ca974470342cb8136705d78cfc00e35083 100644
--- a/dom/geolocation/Geolocation.h
+++ b/dom/geolocation/Geolocation.h
@@ -57,7 +57,7 @@ struct CachedPositionAndAccuracy {
class nsGeolocationService final : public nsIGeolocationUpdate,
public nsIObserver {
public:
- static already_AddRefed<nsGeolocationService> GetGeolocationService();
+ static already_AddRefed<nsGeolocationService> GetGeolocationService(nsDocShell* docShell = nullptr);
static mozilla::StaticRefPtr<nsGeolocationService> sService;
NS_DECL_THREADSAFE_ISUPPORTS
@@ -182,6 +182,8 @@ class Geolocation final : public nsIGeolocationUpdate, public nsWrapperCache {
// null.
static already_AddRefed<Geolocation> NonWindowSingleton();
+ nsGeolocationService* GetGeolocationService() { return mService; };
+
private:
~Geolocation();
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
index 35bcf1e1c19c713a42c62ac4e87ac040e97dc28b..45af1b53fa889368f69f7b45899df8e2d425d011 100644
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -44,6 +44,7 @@
#include "nsMappedAttributes.h"
#include "nsIFormControl.h"
#include "mozilla/dom/Document.h"
+#include "nsDocShell.h"
#include "nsIFormControlFrame.h"
#include "nsITextControlFrame.h"
#include "nsIFrame.h"
@@ -726,6 +727,12 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
return NS_ERROR_FAILURE;
}
+ nsDocShell* docShell = static_cast<nsDocShell*>(win->GetDocShell());
+ if (docShell && docShell->IsFileInputInterceptionEnabled()) {
+ docShell->FilePickerShown(this);
+ return NS_OK;
+ }
+
if (IsPopupBlocked()) {
return NS_OK;
}
2019-11-19 05:18:28 +03:00
diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
index 9e3a1f7ae9f8e5811506107e8b2f058c3c8e3b31..49b9662849eb7e4e5782596cc16907a1bdff4339 100644
2019-11-19 05:18:28 +03:00
--- a/dom/ipc/BrowserChild.cpp
+++ b/dom/ipc/BrowserChild.cpp
@@ -3676,6 +3676,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress,
2019-11-19 05:18:28 +03:00
return NS_OK;
}
+NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsIURI *aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress,
nsIRequest* aRequest,
int32_t aCurSelfProgress,
diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp
index 9e3c1a56d10394d98de9e84fb8cd6ee8e3be5870..8c661de349d6cb64fd8d81d5db9c28f2a2af9138 100644
--- a/dom/script/ScriptSettings.cpp
+++ b/dom/script/ScriptSettings.cpp
@@ -140,6 +140,30 @@ ScriptSettingsStackEntry::~ScriptSettingsStackEntry() {
MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal());
}
+static nsIGlobalObject* UnwrapSandboxGlobal(nsIGlobalObject* global) {
+ if (!global)
+ return global;
+ JSObject* globalObject = global->GetGlobalJSObject();
+ if (!globalObject)
+ return global;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (!cx)
+ return global;
+ JS::Rooted<JSObject*> proto(cx);
+ JS::RootedObject rootedGlobal(cx, globalObject);
+ if (!JS_GetPrototype(cx, rootedGlobal, &proto))
+ return global;
+ if (!proto || !xpc::IsSandboxPrototypeProxy(proto))
+ return global;
+ // If this is a sandbox associated with a DOMWindow via a
+ // sandboxPrototype, use that DOMWindow. This supports GreaseMonkey
+ // and JetPack content scripts.
+ proto = js::CheckedUnwrapDynamic(proto, cx, /* stopAtWindowProxy = */ false);
+ if (!proto)
+ return global;
+ return xpc::WindowGlobalOrNull(proto);
+}
+
// If the entry or incumbent global ends up being something that the subject
// principal doesn't subsume, we don't want to use it. This never happens on
// the web, but can happen with asymmetric privilege relationships (i.e.
@@ -167,7 +191,7 @@ static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) {
NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal());
if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller()
->SubsumesConsideringDomain(globalPrin)) {
- return GetCurrentGlobal();
+ return UnwrapSandboxGlobal(GetCurrentGlobal());
}
return aGlobalOrNull;
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp
index 1782a10a26f51c3bf79efe6713a493c4f058898c..bd2f13802731aa8db42c016d8a91de688dcac80b 100644
--- a/dom/security/nsCSPUtils.cpp
+++ b/dom/security/nsCSPUtils.cpp
@@ -121,6 +121,11 @@ void CSP_ApplyMetaCSPToDoc(mozilla::dom::Document& aDoc,
return;
}
+ if (aDoc.GetDocShell() &&
+ nsDocShell::Cast(aDoc.GetDocShell())->IsBypassCSPEnabled()) {
+ return;
+ }
+
nsAutoString policyStr(
nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>(
aPolicyStr));
diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp
index b20abd71cbb825b17ea3cef1791a2d8b0185b5b2..e7311028af2e8250b8010093549d9e3ff26a6e9a 100644
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -1052,7 +1052,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) {
AssertIsOnMainThread();
nsTArray<nsString> languages;
- Navigator::GetAcceptLanguages(languages);
+ Navigator::GetAcceptLanguages(nullptr, languages);
RuntimeService* runtime = RuntimeService::GetService();
if (runtime) {
@@ -1256,8 +1256,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate* aWorkerPrivate) {
}
// The navigator overridden properties should have already been read.
-
- Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages);
+ Navigator::GetAcceptLanguages(nullptr, mNavigatorProperties.mLanguages);
mNavigatorPropertiesLoaded = true;
}
2019-11-19 05:18:28 +03:00
diff --git a/extensions/permissions/nsPermissionManager.cpp b/extensions/permissions/nsPermissionManager.cpp
index 02f18c7f13c55a16688cee887f586ba3bf97a6fb..1f0c2a3192e35fd71b5fa26fa6822c2b733b7049 100644
2019-11-19 05:18:28 +03:00
--- a/extensions/permissions/nsPermissionManager.cpp
+++ b/extensions/permissions/nsPermissionManager.cpp
@@ -174,7 +174,7 @@ void MaybeStripOAs(bool aForceStrip, OriginAttributes& aOriginAttributes) {
}
if (flags != 0) {
- aOriginAttributes.StripAttributes(flags);
+ // aOriginAttributes.StripAttributes(flags);
}
}
@@ -207,6 +207,8 @@ nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA,
2019-11-19 05:18:28 +03:00
OriginAppendOASuffix(attrs, aForceStripOA, aOrigin);
2019-11-19 05:18:28 +03:00
+ // Disable userContext for permissions.
+ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
return NS_OK;
}
@@ -325,7 +327,7 @@ already_AddRefed<nsIPrincipal> GetNextSubDomainPrincipal(
if (!StaticPrefs::permissions_isolateBy_userContext()) {
// Disable userContext for permissions.
- attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
+ // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
}
nsCOMPtr<nsIPrincipal> principal =
diff --git a/juggler/BrowserContextManager.js b/juggler/BrowserContextManager.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..bd57d338c279f5ab31102e6644f43e133b7f4e25
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/BrowserContextManager.js
@@ -0,0 +1,235 @@
2019-11-19 05:18:28 +03:00
+"use strict";
+
+const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
+const helper = new Helper();
2019-11-19 05:18:28 +03:00
+
+const IDENTITY_NAME = 'JUGGLER ';
+const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
+
+const ALL_PERMISSIONS = [
+ 'geo',
+ 'microphone',
+ 'camera',
+ 'desktop-notifications',
+];
+
+class BrowserContextManager {
+ static instance() {
+ return BrowserContextManager._instance || null;
+ }
+
+ static initialize() {
+ if (BrowserContextManager._instance)
+ return;
+ BrowserContextManager._instance = new BrowserContextManager();
+ }
+
+ constructor() {
+ this._browserContextIdToBrowserContext = new Map();
+ this._userContextIdToBrowserContext = new Map();
2019-11-19 05:18:28 +03:00
+
+ // 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);
2019-11-19 05:18:28 +03:00
+ }
+
+ defaultContext() {
+ return this._defaultContext;
+ }
+
+ createBrowserContext(options) {
+ return new BrowserContext(this, helper.generateId(), options);
2019-11-19 05:18:28 +03:00
+ }
+
+ browserContextForId(browserContextId) {
+ return this._browserContextIdToBrowserContext.get(browserContextId);
+ }
+
+ browserContextForUserContextId(userContextId) {
+ return this._userContextIdToBrowserContext.get(userContextId);
2019-11-19 05:18:28 +03:00
+ }
+
+ getBrowserContexts() {
+ return Array.from(this._browserContextIdToBrowserContext.values());
2019-11-19 05:18:28 +03:00
+ }
+}
2019-11-19 05:18:28 +03:00
+
+class BrowserContext {
+ constructor(manager, browserContextId, options) {
+ EventEmitter.decorate(this);
+
+ this._manager = manager;
+ 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._manager._browserContextIdToBrowserContext.set(this.browserContextId, this);
+ this._manager._userContextIdToBrowserContext.set(this.userContextId, this);
+ this.options = options || {};
+ this.options.scriptsToEvaluateOnNewDocument = [];
+ this.options.bindings = [];
+ this.pages = new Set();
2019-11-19 05:18:28 +03:00
+ }
+
+ destroy() {
+ if (this.userContextId !== 0) {
+ ContextualIdentityService.remove(this.userContextId);
+ ContextualIdentityService.closeContainerTabs(this.userContextId);
+ }
+ this._manager._browserContextIdToBrowserContext.delete(this.browserContextId);
+ this._manager._userContextIdToBrowserContext.delete(this.userContextId);
2019-11-19 05:18:28 +03:00
+ }
+
+ async addScriptToEvaluateOnNewDocument(script) {
+ this.options.scriptsToEvaluateOnNewDocument.push(script);
+ await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
+ }
+
+ async addBinding(name, script) {
+ this.options.bindings.push({ name, script });
+ await Promise.all(Array.from(this.pages).map(page => page.addBinding(name, script)));
+ }
+
+ async setGeolocationOverride(geolocation) {
+ this.options.geolocation = geolocation;
+ await Promise.all(Array.from(this.pages).map(page => page.setGeolocationOverride(geolocation)));
+ }
+
+ async setOnlineOverride(override) {
+ this.options.onlineOverride = override;
+ await Promise.all(Array.from(this.pages).map(page => page.setOnlineOverride(override)));
+ }
+
+ 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(permissions));
+ }
+ }
+ await Promise.all(promises);
2019-11-19 05:18:28 +03:00
+ }
+
+ 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 */);
+ }
2019-11-19 05:18:28 +03:00
+ }
+
+ setCookies(cookies) {
2019-11-19 05:18:28 +03:00
+ 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')
2019-11-19 05:18:28 +03:00
+ 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 */,
2019-11-19 05:18:28 +03:00
+ protocolToSameSite[cookie.sameSite],
+ );
+ }
+ }
+
+ clearCookies() {
+ Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined }));
2019-11-19 05:18:28 +03:00
+ }
+
+ getCookies() {
2019-11-19 05:18:28 +03:00
+ const result = [];
+ const sameSiteToProtocol = {
+ [Ci.nsICookie.SAMESITE_NONE]: 'None',
2019-11-19 05:18:28 +03:00
+ [Ci.nsICookie.SAMESITE_LAX]: 'Lax',
+ [Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
+ };
+ for (let cookie of Services.cookies.cookies) {
+ if (cookie.originAttributes.userContextId !== this.userContextId)
2019-11-19 05:18:28 +03:00
+ continue;
+ if (cookie.host === 'addons.mozilla.org')
2019-11-19 05:18:28 +03:00
+ 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);
+}
+
+var EXPORTED_SYMBOLS = ['BrowserContextManager', 'BrowserContext'];
2019-11-19 05:18:28 +03:00
+this.BrowserContextManager = BrowserContextManager;
+this.BrowserContext = BrowserContext;
2019-11-19 05:18:28 +03:00
+
diff --git a/juggler/Helper.js b/juggler/Helper.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..862c680198bbb503a5f04c19bdb8fdf2cd8c9cef
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/Helper.js
@@ -0,0 +1,102 @@
2019-11-19 05:18:28 +03:00
+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);
2019-11-19 05:18:28 +03:00
+ }
+
+ 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;
+
diff --git a/juggler/NetworkObserver.js b/juggler/NetworkObserver.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..d6ccfe3f9c590969c71630aabad37adf87b274b1
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/NetworkObserver.js
@@ -0,0 +1,773 @@
2019-11-19 05:18:28 +03:00
+"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 {TargetRegistry} = ChromeUtils.import('chrome://juggler/content/TargetRegistry.js');
+const {BrowserContextManager} = ChromeUtils.import('chrome://juggler/content/BrowserContextManager.js');
2019-11-19 05:18:28 +03:00
+
+
+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";
+
+class NetworkObserver {
+ static instance() {
+ return NetworkObserver._instance || null;
+ }
+
+ static initialize() {
+ if (NetworkObserver._instance)
+ return;
+ NetworkObserver._instance = new NetworkObserver();
+ }
+
+ constructor() {
+ EventEmitter.decorate(this);
+ this._browserSessionCount = new Map();
+ 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.
+
2019-11-19 05:18:28 +03:00
+ this._channelSink = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
+ asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
+ this._onRedirect(oldChannel, newChannel, flags);
2019-11-19 05:18:28 +03:00
+ 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._browsersWithEnabledInterception = new Set();
+ this._browserInterceptors = new Map(); // Browser => (requestId => interceptor).
2019-11-19 05:18:28 +03:00
+ this._extraHTTPHeaders = new Map();
+ this._browserResponseStorages = new Map();
+
+ 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'),
+ ];
+ }
+
+ setExtraHTTPHeaders(browser, headers) {
+ if (!headers)
+ this._extraHTTPHeaders.delete(browser);
+ else
+ this._extraHTTPHeaders.set(browser, headers);
+ }
+
+ enableRequestInterception(browser) {
+ this._browsersWithEnabledInterception.add(browser);
2019-11-19 05:18:28 +03:00
+ }
+
+ disableRequestInterception(browser) {
+ this._browsersWithEnabledInterception.delete(browser);
+ const interceptors = this._browserInterceptors.get(browser);
+ if (!interceptors)
2019-11-19 05:18:28 +03:00
+ return;
+ this._browserInterceptors.delete(browser);
+ for (const interceptor of interceptors.values())
+ interceptor._resume();
2019-11-19 05:18:28 +03:00
+ }
+
+ _takeInterceptor(browser, requestId) {
+ const interceptors = this._browserInterceptors.get(browser);
+ if (!interceptors)
2019-11-19 05:18:28 +03:00
+ throw new Error(`Request interception is not enabled`);
+ const interceptor = interceptors.get(requestId);
+ if (!interceptor)
2019-11-19 05:18:28 +03:00
+ throw new Error(`Cannot find request "${requestId}"`);
+ interceptors.delete(requestId);
+ return interceptor;
+ }
+
+ resumeInterceptedRequest(browser, requestId, method, headers, postData) {
+ this._takeInterceptor(browser, requestId)._resume(method, headers, postData);
2019-11-19 05:18:28 +03:00
+ }
+
+ getResponseBody(browser, requestId) {
+ const responseStorage = this._browserResponseStorages.get(browser);
+ if (!responseStorage)
+ throw new Error('Responses are not tracked for the given browser');
+ return responseStorage.getBase64EncodedResponse(requestId);
+ }
+
+ fulfillInterceptedRequest(browser, requestId, status, statusText, headers, base64body) {
+ this._takeInterceptor(browser, requestId)._fulfill(status, statusText, headers, base64body);
2019-11-19 05:18:28 +03:00
+ }
+
+ abortInterceptedRequest(browser, requestId, errorCode) {
+ this._takeInterceptor(browser, requestId)._abort(errorCode);
+ }
+
+ _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))
2019-11-19 05:18:28 +03:00
+ return;
+ const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
+ const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
+ const browser = this._getBrowserForChannel(oldHttpChannel);
+ if (!browser)
2019-11-19 05:18:28 +03:00
+ 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);
+ }
2019-11-19 05:18:28 +03:00
+ }
+
+ 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 browser = this._getBrowserForChannel(httpChannel);
+ if (!browser)
2019-11-19 05:18:28 +03:00
+ return;
+ if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
+ return;
+ if (this._isResumedChannel(httpChannel))
+ return;
+ if (this._requestIdBeforeAuthentication(httpChannel))
+ return;
+ this._sendOnRequestFinished(httpChannel);
+ }
+
+ _getBrowserForChannel(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ if (!loadContext || !this._browserSessionCount.has(loadContext.topFrameElement))
+ return;
+ return loadContext.topFrameElement;
+ }
+
+ _isResumedChannel(httpChannel) {
+ return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + '');
2019-11-19 05:18:28 +03:00
+ }
+
+ _onRequest(channel, topic) {
+ if (!(channel instanceof Ci.nsIHttpChannel))
+ return;
+ const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
+ const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser)
+ return;
+ if (this._isResumedChannel(httpChannel)) {
+ // Ignore onRequest for resumed requests, but listen to their response.
+ new ResponseBodyListener(this, browser, httpChannel);
2019-11-19 05:18:28 +03:00
+ 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 = TargetRegistry.instance().browserContextForBrowser(browser);
+ if (browserContext)
+ this._appendExtraHTTPHeaders(httpChannel, browserContext.options.extraHTTPHeaders);
+ this._appendExtraHTTPHeaders(httpChannel, this._extraHTTPHeaders.get(browser));
+ const requestId = this._requestId(httpChannel);
+ const isRedirect = this._redirectMap.has(requestId);
+ const interceptionEnabled = this._isInterceptionEnabledForBrowser(browser);
+ if (!interceptionEnabled) {
+ new NotificationCallbacks(this, browser, httpChannel, false);
+ this._sendOnRequest(httpChannel, false);
+ new ResponseBodyListener(this, browser, 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 = this._ensureInterceptors(browser);
+ interceptors.set(requestId, {
+ _resume: () => {},
+ _abort: () => {},
+ _fulfill: () => {},
+ });
+ new NotificationCallbacks(this, browser, httpChannel, false);
+ this._sendOnRequest(httpChannel, true);
+ new ResponseBodyListener(this, browser, 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, browser, httpChannel, false);
+ this._sendOnRequest(httpChannel, false);
+ new ResponseBodyListener(this, browser, httpChannel);
+ } else {
+ // We'll issue onRequest once it's intercepted.
+ new NotificationCallbacks(this, browser, httpChannel, true);
+ }
+ } else {
+ // We'll issue onRequest once it's intercepted.
+ new NotificationCallbacks(this, browser, httpChannel, true);
+ }
2019-11-19 05:18:28 +03:00
+ }
+ }
2019-11-19 05:18:28 +03:00
+
+ _isInterceptionEnabledForBrowser(browser) {
+ if (this._browsersWithEnabledInterception.has(browser))
+ return true;
+ const browserContext = TargetRegistry.instance().browserContextForBrowser(browser);
+ if (browserContext && browserContext.options.requestInterceptionEnabled)
+ return true;
+ if (browserContext && browserContext.options.onlineOverride === 'offline')
+ return true;
+ return false;
+ }
+
+ _ensureInterceptors(browser) {
+ let interceptors = this._browserInterceptors.get(browser);
+ if (!interceptors) {
+ interceptors = new Map();
+ this._browserInterceptors.set(browser, interceptors);
+ }
+ return interceptors;
+ }
+
+ _appendExtraHTTPHeaders(httpChannel, headers) {
+ if (!headers)
+ return;
+ for (const header of headers)
+ httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
+ }
+
+ _onIntercepted(httpChannel, interceptor) {
+ const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser) {
+ interceptor._resume();
+ return;
+ }
+
+ const browserContext = TargetRegistry.instance().browserContextForBrowser(browser);
+ if (browserContext && browserContext.options.onlineOverride === 'offline') {
+ interceptor._abort(Cr.NS_ERROR_OFFLINE);
+ return;
+ }
+
+ const interceptionEnabled = this._isInterceptionEnabledForBrowser(browser);
+ this._sendOnRequest(httpChannel, !!interceptionEnabled);
+ if (interceptionEnabled)
+ this._ensureInterceptors(browser).set(this._requestId(httpChannel), interceptor);
+ else
+ interceptor._resume();
+ }
2019-11-19 05:18:28 +03:00
+
+ _sendOnRequest(httpChannel, isIntercepted) {
+ const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser)
+ return;
+ const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
+ const requestId = this._requestId(httpChannel);
+ const redirectedFrom = this._redirectMap.get(requestId);
+ this._redirectMap.delete(requestId);
2019-11-19 05:18:28 +03:00
+ this.emit('request', httpChannel, {
+ url: httpChannel.URI.spec,
+ isIntercepted,
+ requestId,
+ redirectedFrom,
2019-11-19 05:18:28 +03:00
+ postData: readRequestPostData(httpChannel),
+ headers: requestHeaders(httpChannel),
+ method: httpChannel.requestMethod,
+ navigationId: httpChannel.isMainDocumentChannel ? this._requestIdBeforeAuthentication(httpChannel) || this._requestId(httpChannel) : undefined,
2019-11-19 05:18:28 +03:00
+ cause: causeTypeToString(causeType),
+ });
+ }
+
+ _sendOnRequestFinished(httpChannel) {
+ this.emit('requestfinished', httpChannel, {
+ requestId: this._requestId(httpChannel),
+ });
+ this._cleanupChannelState(httpChannel);
+ }
+
+ _sendOnRequestFailed(httpChannel, error) {
+ this.emit('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);
+ }
+
2019-11-19 05:18:28 +03:00
+ _onResponse(fromCache, httpChannel, topic) {
+ const browser = this._getBrowserForChannel(httpChannel);
+ if (!browser)
2019-11-19 05:18:28 +03:00
+ 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.
+ }
+ this.emit('response', httpChannel, {
+ requestId: this._requestId(httpChannel),
2019-11-19 05:18:28 +03:00
+ securityDetails: getSecurityDetails(httpChannel),
+ fromCache,
+ headers,
+ remoteIPAddress,
+ remotePort,
+ status: httpChannel.responseStatus,
+ statusText: httpChannel.responseStatusText,
+ });
+ }
+
+ _onResponseFinished(browser, httpChannel, body) {
+ const responseStorage = this._browserResponseStorages.get(browser);
+ if (!responseStorage)
+ return;
+ responseStorage.addResponseBody(httpChannel, body);
+ this._sendOnRequestFinished(httpChannel);
2019-11-19 05:18:28 +03:00
+ }
+
+ startTrackingBrowserNetwork(browser) {
+ const value = this._browserSessionCount.get(browser) || 0;
+ this._browserSessionCount.set(browser, value + 1);
+ if (value === 0)
+ this._browserResponseStorages.set(browser, new ResponseStorage(this, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10));
2019-11-19 05:18:28 +03:00
+ return () => this.stopTrackingBrowserNetwork(browser);
+ }
+
+ stopTrackingBrowserNetwork(browser) {
+ const value = this._browserSessionCount.get(browser);
+ if (value) {
+ this._browserSessionCount.set(browser, value - 1);
+ } else {
+ this._browserSessionCount.delete(browser);
+ this._browserResponseStorages.delete(browser);
+ this._browsersWithEnabledInterception.delete(browser);
+ this._browserInterceptors.delete(browser);
2019-11-19 05:18:28 +03:00
+ }
+ }
+
+ 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;
2019-11-19 05:18:28 +03:00
+ 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});
2019-11-19 05:18:28 +03:00
+ 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, browser, httpChannel) {
+ this._networkObserver = networkObserver;
+ this._browser = browser;
+ 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);
2019-11-19 05:18:28 +03:00
+ }
+
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ if (this._disposed) {
+ this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount);
+ return;
+ }
+
2019-11-19 05:18:28 +03:00
+ 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;
+
2019-11-19 05:18:28 +03:00
+ const body = this._chunks.join('');
+ delete this._chunks;
+ this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body);
+ this.dispose();
+ }
+
+ dispose() {
+ this._disposed = true;
+ this._networkObserver._bodyListeners.delete(this._httpChannel.channelId + '');
2019-11-19 05:18:28 +03:00
+ }
+}
+
+class NotificationCallbacks {
+ constructor(networkObserver, browser, httpChannel, shouldIntercept) {
+ this._networkObserver = networkObserver;
+ this._browser = browser;
+ 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 = TargetRegistry.instance().browserContextForBrowser(this._browser);
+ const credentials = browserContext ? browserContext.options.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._networkObserver.emit('response', this._httpChannel, {
+ requestId: this._networkObserver._requestId(this._httpChannel),
+ securityDetails: null,
+ fromCache: false,
+ headers,
+ status,
+ statusText,
+ });
+ this._networkObserver._onResponseFinished(this._browser, this._httpChannel, body);
+ }
+
+ _abort(errorCode) {
+ const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
+ this._intercepted.cancelInterception(error);
+ this._networkObserver._sendOnRequestFailed(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,
+};
+
2019-11-19 05:18:28 +03:00
+var EXPORTED_SYMBOLS = ['NetworkObserver'];
+this.NetworkObserver = NetworkObserver;
diff --git a/juggler/SimpleChannel.js b/juggler/SimpleChannel.js
new file mode 100644
index 0000000000000000000000000000000000000000..ba34976ad05e7f5f1a99777f76ac08b171af40b7
--- /dev/null
+++ b/juggler/SimpleChannel.js
@@ -0,0 +1,130 @@
+"use strict";
+// Note: this file should be loadabale with eval() into worker environment.
+// Avoid Components.*, ChromeUtils and global const variables.
+
+const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
+
+class SimpleChannel {
+ static createForMessageManager(name, mm) {
+ const channel = new SimpleChannel(name);
+
+ const messageListener = {
+ receiveMessage: message => channel._onMessage(message.data)
+ };
+ mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
+
+ channel.transport.sendMessage = obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj);
+ channel.transport.dispose = () => {
+ mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
+ };
+ return channel;
+ }
+
+ constructor(name) {
+ this._name = name;
+ this._messageId = 0;
+ this._connectorId = 0;
+ this._pendingMessages = new Map();
+ this._handlers = new Map();
+ this.transport = {
+ sendMessage: null,
+ dispose: null,
+ };
+ this._disposed = false;
+ }
+
+ dispose() {
+ if (this._disposed)
+ return;
+ this._disposed = true;
+ for (const {resolve, reject, methodName} of this._pendingMessages.values())
+ reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
+ this._pendingMessages.clear();
+ this._handlers.clear();
+ this.transport.dispose();
+ }
+
+ _rejectCallbacksFromConnector(connectorId) {
+ for (const [messageId, callback] of this._pendingMessages) {
+ if (callback.connectorId === connectorId) {
+ callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
+ this._pendingMessages.delete(messageId);
+ }
+ }
+ }
+
+ connect(namespace) {
+ const connectorId = ++this._connectorId;
+ return {
+ send: (...args) => this._send(namespace, connectorId, ...args),
+ emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
+ dispose: () => this._rejectCallbacksFromConnector(connectorId),
+ };
+ }
+
+ register(namespace, handler) {
+ if (this._handlers.has(namespace))
+ throw new Error('ERROR: double-register for namespace ' + namespace);
+ this._handlers.set(namespace, handler);
+ return () => this.unregister(namespace);
+ }
+
+ unregister(namespace) {
+ this._handlers.delete(namespace);
+ }
+
+ /**
+ * @param {string} namespace
+ * @param {number} connectorId
+ * @param {string} methodName
+ * @param {...*} params
+ * @return {!Promise<*>}
+ */
+ async _send(namespace, connectorId, methodName, ...params) {
+ if (this._disposed)
+ throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
+ const id = ++this._messageId;
+ const promise = new Promise((resolve, reject) => {
+ this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace});
+ });
+ this.transport.sendMessage({requestId: id, methodName, params, namespace});
+ return promise;
+ }
+
+ async _onMessage(data) {
+ if (data.responseId) {
+ const {resolve, reject} = this._pendingMessages.get(data.responseId);
+ this._pendingMessages.delete(data.responseId);
+ if (data.error)
+ reject(new Error(data.error));
+ else
+ resolve(data.result);
+ } else if (data.requestId) {
+ const namespace = data.namespace;
+ const handler = this._handlers.get(namespace);
+ if (!handler) {
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`});
+ return;
+ }
+ const method = handler[data.methodName];
+ if (!method) {
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
+ return;
+ }
+ try {
+ const result = await method.call(handler, ...data.params);
+ this.transport.sendMessage({responseId: data.requestId, result});
+ } catch (error) {
+ this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
+ return;
+ }
+ } else {
+ dump(`
+ ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)}
+ `);
+ }
+ }
+}
+
+var EXPORTED_SYMBOLS = ['SimpleChannel'];
+this.SimpleChannel = SimpleChannel;
diff --git a/juggler/TargetRegistry.js b/juggler/TargetRegistry.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..e624e3c21a20dd324e0d135598e2a2402c8b62bf
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/TargetRegistry.js
@@ -0,0 +1,277 @@
2019-11-19 05:18:28 +03:00
+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');
2019-11-19 05:18:28 +03:00
+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 TargetRegistry {
+ static instance() {
+ return TargetRegistry._instance || null;
+ }
+
+ static initialize(mainWindow, contextManager) {
+ if (TargetRegistry._instance)
+ return;
+ TargetRegistry._instance = new TargetRegistry(mainWindow, contextManager);
+ }
+
+ constructor(mainWindow, contextManager) {
+ EventEmitter.decorate(this);
+
+ this._mainWindow = mainWindow;
+ this._contextManager = contextManager;
+ this._targets = new Map();
+
+ this._browserTarget = new BrowserTarget();
+ this._targets.set(this._browserTarget.id(), this._browserTarget);
+ this._tabToTarget = new Map();
+
+ for (const tab of this._mainWindow.gBrowser.tabs)
+ this._createTargetForTab(tab);
2019-11-19 05:18:28 +03:00
+ this._mainWindow.gBrowser.tabContainer.addEventListener('TabOpen', event => {
+ const target = this._createTargetForTab(event.target);
+ // If we come here, content will have juggler script from the start,
+ // and we should wait for initial navigation.
+ target._waitForInitialNavigation = true;
+ // For pages created before we attach to them, we don't wait for initial
+ // navigation (target._waitForInitialNavigation is false by default).
2019-11-19 05:18:28 +03:00
+ });
+ this._mainWindow.gBrowser.tabContainer.addEventListener('TabClose', event => {
+ const tab = event.target;
+ const target = this._tabToTarget.get(tab);
+ if (!target)
+ return;
+ this._targets.delete(target.id());
+ this._tabToTarget.delete(tab);
+ target.dispose();
+ this.emit(TargetRegistry.Events.TargetDestroyed, target);
2019-11-19 05:18:28 +03:00
+ });
+ Services.obs.addObserver(this, 'oop-frameloader-crashed');
2019-11-19 05:18:28 +03:00
+ }
+
+ pageTargets(browserContextId) {
+ const browserContext = this._contextManager.browserContextForId(browserContextId);
+ const pageTargets = [...this._targets.values()].filter(target => target instanceof PageTarget);
+ return pageTargets.filter(target => target._browserContext === browserContext);
+ }
+
2019-11-19 05:18:28 +03:00
+ async newPage({browserContextId}) {
+ const browserContext = this._contextManager.browserContextForId(browserContextId);
2019-11-19 05:18:28 +03:00
+ const tab = this._mainWindow.gBrowser.addTab('about:blank', {
+ userContextId: browserContext.userContextId,
2019-11-19 05:18:28 +03:00
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ this._mainWindow.gBrowser.selectedTab = tab;
+ const target = this._tabToTarget.get(tab);
+ await target._contentReadyPromise;
2019-11-19 05:18:28 +03:00
+ return target.id();
+ }
+
+ async closePage(targetId, runBeforeUnload = false) {
+ const tab = this.tabForTarget(targetId);
+ await this._mainWindow.gBrowser.removeTab(tab, {
+ skipPermitUnload: !runBeforeUnload,
+ });
+ }
+
+ targets() {
+ return Array.from(this._targets.values());
2019-11-19 05:18:28 +03:00
+ }
+
+ targetInfo(targetId) {
+ const target = this._targets.get(targetId);
+ return target ? target.info() : null;
+ }
+
+ browserTargetInfo() {
+ return this._browserTarget.info();
+ }
+
+ tabForTarget(targetId) {
+ const target = this._targets.get(targetId);
+ if (!target)
+ throw new Error(`Target "${targetId}" does not exist!`);
+ if (!(target instanceof PageTarget))
+ throw new Error(`Target "${targetId}" is not a page!`);
+ return target._tab;
+ }
+
+ contentChannelForTarget(targetId) {
+ const target = this._targets.get(targetId);
+ if (!target)
+ throw new Error(`Target "${targetId}" does not exist!`);
+ if (!(target instanceof PageTarget))
+ throw new Error(`Target "${targetId}" is not a page!`);
+ return target._channel;
+ }
+
+ targetForId(targetId) {
+ return this._targets.get(targetId);
+ }
2019-11-19 05:18:28 +03:00
+
+ targetForBrowser(browser) {
+ const tab = this._mainWindow.gBrowser.getTabForBrowser(browser);
+ return tab ? this._tabToTarget.get(tab) : undefined;
+ }
+
+ browserContextForBrowser(browser) {
+ const tab = this._mainWindow.gBrowser.getTabForBrowser(browser);
+ return tab ? this._contextManager.browserContextForUserContextId(tab.userContextId) : undefined;
+ }
+
+ _createTargetForTab(tab) {
+ if (this._tabToTarget.has(tab))
+ throw new Error(`Internal error: two targets per tab`);
+ const openerTarget = tab.openerTab ? this._tabToTarget.get(tab.openerTab) : null;
+ const target = new PageTarget(this, tab, this._contextManager.browserContextForUserContextId(tab.userContextId), openerTarget);
2019-11-19 05:18:28 +03:00
+ this._targets.set(target.id(), target);
+ this._tabToTarget.set(tab, target);
+ this.emit(TargetRegistry.Events.TargetCreated, target);
+ return target;
2019-11-19 05:18:28 +03:00
+ }
+
+ observe(subject, topic, data) {
+ if (topic === 'oop-frameloader-crashed') {
+ const browser = subject.ownerElement;
+ if (!browser)
+ return;
+ const target = this.targetForBrowser(browser);
+ if (!target)
+ return;
+ this.emit(TargetRegistry.Events.TargetCrashed, target.id());
+ return;
+ }
+ }
2019-11-19 05:18:28 +03:00
+}
+
+class PageTarget {
+ constructor(registry, tab, browserContext, opener) {
+ this._targetId = helper.generateId();
2019-11-19 05:18:28 +03:00
+ this._registry = registry;
+ this._tab = tab;
+ this._browserContext = browserContext;
+ this._url = '';
2019-11-19 05:18:28 +03:00
+ this._openerId = opener ? opener.id() : undefined;
+ this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, tab.linkedBrowser.messageManager);
2019-11-19 05:18:28 +03:00
+
+ const navigationListener = {
+ QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]),
+ onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
+ };
2019-11-19 05:18:28 +03:00
+ this._eventListeners = [
+ helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
+ helper.addMessageListener(tab.linkedBrowser.messageManager, 'juggler:content-ready', {
+ receiveMessage: () => this._onContentReady()
+ }),
2019-11-19 05:18:28 +03:00
+ ];
+
+ this._contentReadyPromise = new Promise(f => this._contentReadyCallback = f);
+ this._waitForInitialNavigation = false;
+
+ if (browserContext)
+ browserContext.pages.add(this);
+ if (browserContext && browserContext.options.viewport)
+ this.setViewportSize(browserContext.options.viewport.viewportSize);
+ }
+
+ setViewportSize(viewportSize) {
+ if (viewportSize) {
+ const {width, height} = viewportSize;
+ this._tab.linkedBrowser.style.setProperty('min-width', width + 'px');
+ this._tab.linkedBrowser.style.setProperty('min-height', height + 'px');
+ this._tab.linkedBrowser.style.setProperty('max-width', width + 'px');
+ this._tab.linkedBrowser.style.setProperty('max-height', height + 'px');
+ } else {
+ this._tab.linkedBrowser.style.removeProperty('min-width');
+ this._tab.linkedBrowser.style.removeProperty('min-height');
+ this._tab.linkedBrowser.style.removeProperty('max-width');
+ this._tab.linkedBrowser.style.removeProperty('max-height');
+ }
+ const rect = this._tab.linkedBrowser.getBoundingClientRect();
+ return { width: rect.width, height: rect.height };
2019-11-19 05:18:28 +03:00
+ }
+
+ _onContentReady() {
+ const sessionIds = [];
+ const data = { sessionIds, target: this };
+ this._registry.emit(TargetRegistry.Events.PageTargetReady, data);
+ this._contentReadyCallback();
+ return {
+ browserContextOptions: this._browserContext ? this._browserContext.options : {},
+ waitForInitialNavigation: this._waitForInitialNavigation,
+ sessionIds
+ };
+ }
+
2019-11-19 05:18:28 +03:00
+ id() {
+ return this._targetId;
+ }
+
+ info() {
+ return {
+ targetId: this.id(),
+ type: 'page',
+ browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined,
2019-11-19 05:18:28 +03:00
+ openerId: this._openerId,
+ };
+ }
+
+ _onNavigated(aLocation) {
+ this._url = aLocation.spec;
+ this._browserContext.grantPermissionsToOrigin(this._url);
+ }
+
+ async ensurePermissions(permissions) {
+ await this._channel.connect('').send('ensurePermissions', permissions).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 setGeolocationOverride(geolocation) {
+ await this._channel.connect('').send('setGeolocationOverride', geolocation).catch(e => void e);
+ }
+
+ async setOnlineOverride(override) {
+ await this._channel.connect('').send('setOnlineOverride', override).catch(e => void e);
+ }
+
2019-11-19 05:18:28 +03:00
+ dispose() {
+ if (this._browserContext)
+ this._browserContext.pages.delete(this);
2019-11-19 05:18:28 +03:00
+ helper.removeListeners(this._eventListeners);
+ }
+}
+
+class BrowserTarget {
+ id() {
+ return 'target-browser';
+ }
+
+ info() {
+ return {
+ targetId: this.id(),
+ type: 'browser',
+ }
+ }
+}
+
+TargetRegistry.Events = {
+ TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
+ TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
+ TargetCrashed: Symbol('TargetRegistry.Events.TargetCrashed'),
+ PageTargetReady: Symbol('TargetRegistry.Events.PageTargetReady'),
2019-11-19 05:18:28 +03:00
+};
+
+var EXPORTED_SYMBOLS = ['TargetRegistry'];
+this.TargetRegistry = TargetRegistry;
diff --git a/juggler/components/juggler.js b/juggler/components/juggler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..055b032beff4b7d66a9f33d600dd8d2926867a34
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/components/juggler.js
@@ -0,0 +1,116 @@
2019-11-19 05:18:28 +03:00
+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 {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.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();
2019-11-19 05:18:28 +03:00
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+const FRAME_SCRIPT = "chrome://juggler/content/content/main.js";
+
+// Command Line Handler
+function CommandLineHandler() {
+ this._port = -1;
+};
+
+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;
+ this._port = parseInt(jugglerFlag, 10);
+ Services.obs.addObserver(this, 'sessionstore-windows-restored');
+ },
+
+ observe: async function(subject, topic) {
+ Services.obs.removeObserver(this, 'sessionstore-windows-restored');
+
+ const win = await waitForBrowserWindow();
+ BrowserContextManager.initialize();
+ NetworkObserver.initialize();
+ TargetRegistry.initialize(win, BrowserContextManager.instance());
+
+ 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(this._port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
+
+ const token = helper.generateId();
+
2019-11-19 05:18:28 +03:00
+ 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);
2019-11-19 05:18:28 +03:00
+ new Dispatcher(webSocket);
+ }
+ });
+
+ Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
+ dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`);
2019-11-19 05:18:28 +03:00
+ },
+
+ 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]);
+
+/**
+ * @return {!Promise<Ci.nsIDOMChromeWindow>}
+ */
+async function waitForBrowserWindow() {
+ const windowsIt = Services.wm.getEnumerator('navigator:browser');
+ if (windowsIt.hasMoreElements())
+ return waitForWindowLoaded(windowsIt.getNext());
+
+ let fulfill;
+ let promise = new Promise(x => fulfill = x);
+
+ const listener = {
+ onOpenWindow: window => {
+ if (window instanceof Ci.nsIDOMChromeWindow) {
+ Services.wm.removeListener(listener);
+ fulfill(waitForWindowLoaded(window));
+ }
+ },
+ onCloseWindow: () => {}
+ };
+ Services.wm.addListener(listener);
+ return promise;
+
+ /**
+ * @param {!Ci.nsIDOMChromeWindow} window
+ * @return {!Promise<Ci.nsIDOMChromeWindow>}
+ */
+ function waitForWindowLoaded(window) {
+ if (window.document.readyState === 'complete')
+ return window;
+ return new Promise(fulfill => {
+ window.addEventListener('load', function listener() {
+ window.removeEventListener('load', listener);
+ fulfill(window);
+ });
+ });
+ }
+}
diff --git a/juggler/components/juggler.manifest b/juggler/components/juggler.manifest
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..50f8930207563e0d6b8a7878fc602dbca54d77fc
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/components/juggler.manifest
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,3 @@
+component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js
+contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639}
+category command-line-handler m-juggler @mozilla.org/remote/juggler;1
diff --git a/juggler/components/moz.build b/juggler/components/moz.build
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853dd7aeb254
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/components/moz.build
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,9 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_COMPONENTS += [
+ "juggler.js",
+ "juggler.manifest",
+]
+
diff --git a/juggler/content/FrameTree.js b/juggler/content/FrameTree.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..5f2b6b5de4faa91e32c14e53064b9484648ef9eb
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/FrameTree.js
@@ -0,0 +1,452 @@
2019-11-19 05:18:28 +03:00
+"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');
2019-11-19 05:18:28 +03:00
+const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
+const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
2019-11-19 05:18:28 +03:00
+
+const helper = new Helper();
+
+class FrameTree {
+ constructor(rootDocShell, waitForInitialNavigation) {
2019-11-19 05:18:28 +03:00
+ 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();
2019-11-19 05:18:28 +03:00
+ this._docShellToFrame = new Map();
+ this._frameIdToFrame = new Map();
+ this._pageReady = !waitForInitialNavigation;
2019-11-19 05:18:28 +03:00
+ 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);
+
2019-11-19 05:18:28 +03:00
+ const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
+ Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
+ this._eventListeners = [
+ helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
2019-11-19 05:18:28 +03:00
+ 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);
+ }
+
2019-11-19 05:18:28 +03:00
+ 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();
2019-11-19 05:18:28 +03:00
+ 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;
+
+ if (isStart) {
+ // Starting a new navigation.
+ frame._pendingNavigationId = this._channelId(channel);
2019-11-19 05:18:28 +03:00
+ frame._pendingNavigationURL = channel.URI.spec;
+ this.emit(FrameTree.Events.NavigationStarted, frame);
+ } else if (isTransferring || (isStop && frame._pendingNavigationId && !status)) {
+ // Navigation is committed.
+ for (const subframe of frame._children)
+ this._detachFrame(subframe);
+ const navigationId = frame._pendingNavigationId;
+ frame._pendingNavigationId = null;
+ frame._pendingNavigationURL = null;
+ frame._lastCommittedNavigationId = navigationId;
+ frame._url = channel.URI.spec;
+ this.emit(FrameTree.Events.NavigationCommitted, frame);
+ if (frame === this._mainFrame)
+ this.forcePageReady();
2019-11-19 05:18:28 +03:00
+ } else if (isStop && frame._pendingNavigationId && status) {
+ // Navigation is aborted.
+ const navigationId = frame._pendingNavigationId;
+ frame._pendingNavigationId = null;
+ frame._pendingNavigationURL = null;
+ this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, helper.getNetworkErrorStatusText(status));
+ }
+ }
+
+ 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();
+ }
+
2019-11-19 05:18:28 +03:00
+ _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);
2019-11-19 05:18:28 +03:00
+ 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();
2019-11-19 05:18:28 +03:00
+ 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();
2019-11-19 05:18:28 +03:00
+ this.emit(FrameTree.Events.FrameDetached, frame);
+ }
+}
+
+FrameTree.Events = {
+ BindingCalled: 'bindingcalled',
2019-11-19 05:18:28 +03:00
+ FrameAttached: 'frameattached',
+ FrameDetached: 'framedetached',
+ GlobalObjectCreated: 'globalobjectcreated',
+ WorkerCreated: 'workercreated',
+ WorkerDestroyed: 'workerdestroyed',
2019-11-19 05:18:28 +03:00
+ NavigationStarted: 'navigationstarted',
+ NavigationCommitted: 'navigationcommitted',
+ NavigationAborted: 'navigationaborted',
+ SameDocumentNavigation: 'samedocumentnavigation',
+ PageReady: 'pageready',
2019-11-19 05:18:28 +03:00
+};
+
+class Frame {
+ constructor(frameTree, runtime, docShell, parentFrame) {
2019-11-19 05:18:28 +03:00
+ this._frameTree = frameTree;
+ this._runtime = runtime;
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ }
+
+ 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);
+ }
2019-11-19 05:18:28 +03:00
+}
+
+var EXPORTED_SYMBOLS = ['FrameTree'];
+this.FrameTree = FrameTree;
+
diff --git a/juggler/content/NetworkMonitor.js b/juggler/content/NetworkMonitor.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..be70ea364f9534bb3b344f64970366c32e8c11be
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/NetworkMonitor.js
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,62 @@
+"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 = getLoadContext(httpChannel);
+ if (!loadContext)
+ return;
+ const window = loadContext.associatedWindow;
+ const frame = this._frameTree.frameForDocShell(window.docShell);
2019-11-19 05:18:28 +03:00
+ 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);
+ }
+}
+
+function getLoadContext(httpChannel) {
+ let loadContext = null;
+ try {
+ if (httpChannel.notificationCallbacks)
+ loadContext = httpChannel.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) {}
+ try {
+ if (!loadContext && httpChannel.loadGroup)
+ loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (e) { }
+ return loadContext;
+}
+
+
+var EXPORTED_SYMBOLS = ['NetworkMonitor'];
+this.NetworkMonitor = NetworkMonitor;
+
diff --git a/juggler/content/PageAgent.js b/juggler/content/PageAgent.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..643fcfeb7d1084c2a5eb99031d0f626ebee725f9
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/PageAgent.js
@@ -0,0 +1,936 @@
2019-11-19 05:18:28 +03:00
+"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 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();
+ }
+}
+
2019-11-19 05:18:28 +03:00
+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');
2019-11-19 05:18:28 +03:00
+ this._frameTree = frameTree;
+ this._runtime = frameTree.runtime();
2019-11-19 05:18:28 +03:00
+ this._networkMonitor = networkMonitor;
+
+ this._frameData = new Map();
+ this._workerData = new Map();
2019-11-19 05:18:28 +03:00
+ this._scriptsToEvaluateOnNewDocument = new Map();
+ this._isolatedWorlds = new Map();
2019-11-19 05:18:28 +03:00
+
+ this._eventListeners = [
+ browserChannel.register(sessionId + 'page', {
+ addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
+ addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
+ adoptNode: this._adoptNode.bind(this),
+ awaitViewportDimensions: this._awaitViewportDimensions.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),
+ }),
+ ];
2019-11-19 05:18:28 +03:00
+ this._enabled = false;
+
+ const docShell = frameTree.mainFrame().docShell();
+ this._docShell = docShell;
2019-11-19 05:18:28 +03:00
+ this._initialDPPX = docShell.contentViewer.overrideDPPX;
+ this._customScrollbars = null;
+ }
+
+ async _awaitViewportDimensions({width, height}) {
2019-11-19 05:18:28 +03:00
+ const win = this._frameTree.mainFrame().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();
+ }
+ });
+ });
+ }
+
+ _requestDetails({channelId}) {
2019-11-19 05:18:28 +03:00
+ return this._networkMonitor.requestDetails(channelId);
+ }
+
+ async _setEmulatedMedia({type, colorScheme}) {
2019-11-19 05:18:28 +03:00
+ const docShell = this._frameTree.mainFrame().docShell();
+ const cv = docShell.contentViewer;
+ if (type === '')
+ cv.stopEmulatingMedium();
+ else if (type)
+ cv.emulateMedium(type);
+ switch (colorScheme) {
+ case 'light': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_LIGHT); break;
+ case 'dark': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_DARK); break;
+ case 'no-preference': cv.emulatePrefersColorScheme(cv.PREFERS_COLOR_SCHEME_NO_PREFERENCE); break;
+ }
2019-11-19 05:18:28 +03:00
+ }
+
+ _addScriptToEvaluateOnNewDocument({script, worldName}) {
+ if (worldName)
+ return this._createIsolatedWorld({script, worldName});
+ return {scriptId: this._frameTree.addScriptToEvaluateOnNewDocument(script)};
+ }
+
+ _createIsolatedWorld({script, worldName}) {
2019-11-19 05:18:28 +03:00
+ const scriptId = helper.generateId();
+ this._isolatedWorlds.set(scriptId, {script, worldName});
+ for (const frameData of this._frameData.values())
+ frameData.createIsolatedWorld(worldName);
2019-11-19 05:18:28 +03:00
+ return {scriptId};
+ }
+
+ _removeScriptToEvaluateOnNewDocument({scriptId}) {
+ if (this._isolatedWorlds.has(scriptId))
+ this._isolatedWorlds.delete(scriptId);
+ else
+ this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId);
2019-11-19 05:18:28 +03:00
+ }
+
+ _setCacheDisabled({cacheDisabled}) {
2019-11-19 05:18:28 +03:00
+ 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._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)),
2019-11-19 05:18:28 +03:00
+ 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)),
2019-11-19 05:18:28 +03:00
+ 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', {});
2019-11-19 05:18:28 +03:00
+ }
+
+ _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' });
+ }
+
+ _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;
+ });
+ }
+
2019-11-19 05:18:28 +03:00
+ _onDOMContentLoaded(event) {
+ const docShell = event.target.ownerGlobal.docShell;
+ const frame = this._frameTree.frameForDocShell(docShell);
+ if (!frame)
+ return;
+ this._browserPage.emit('pageEventFired', {
2019-11-19 05:18:28 +03:00
+ 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', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ message: errorEvent.message,
+ stack: errorEvent.error ? errorEvent.error.stack : '',
2019-11-19 05:18:28 +03:00
+ });
+ }
+
+ _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'
+ });
+ }
+
2019-11-19 05:18:28 +03:00
+ _onLoad(event) {
+ const docShell = event.target.ownerGlobal.docShell;
+ const frame = this._frameTree.frameForDocShell(docShell);
+ if (!frame)
+ return;
+ this._browserPage.emit('pageEventFired', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ name: 'load'
+ });
+ }
+
+ _onNavigationStarted(frame) {
+ this._browserPage.emit('pageNavigationStarted', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ navigationId: frame.pendingNavigationId(),
+ url: frame.pendingNavigationURL(),
+ });
+ }
+
+ _onNavigationAborted(frame, navigationId, errorText) {
+ this._browserPage.emit('pageNavigationAborted', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ navigationId,
+ errorText,
+ });
+ }
+
+ _onSameDocumentNavigation(frame) {
+ this._browserPage.emit('pageSameDocumentNavigation', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ url: frame.url(),
+ });
+ }
+
+ _onNavigationCommitted(frame) {
+ this._browserPage.emit('pageNavigationCommitted', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ navigationId: frame.lastCommittedNavigationId() || undefined,
2019-11-19 05:18:28 +03:00
+ url: frame.url(),
+ name: frame.name(),
+ });
+ }
+
+ _onGlobalObjectCreated({ frame }) {
+ this._frameData.get(frame).reset();
2019-11-19 05:18:28 +03:00
+ }
+
+ _onFrameAttached(frame) {
+ this._browserPage.emit('pageFrameAttached', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
+ });
+ this._frameData.set(frame, new FrameData(this, this._runtime, frame));
2019-11-19 05:18:28 +03:00
+ }
+
+ _onFrameDetached(frame) {
+ this._frameData.delete(frame);
+ this._browserPage.emit('pageFrameDetached', {
2019-11-19 05:18:28 +03:00
+ frameId: frame.id(),
+ });
+ }
+
+ _onBindingCalled({frame, name, payload}) {
+ this._browserPage.emit('pageBindingCalled', {
+ executionContextId: frame.executionContext().id(),
+ name,
+ payload
+ });
+ }
+
2019-11-19 05:18:28 +03:00
+ 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();
2019-11-19 05:18:28 +03:00
+ helper.removeListeners(this._eventListeners);
+ }
+
+ async _navigate({frameId, url, referer}) {
2019-11-19 05:18:28 +03:00
+ 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(),
2019-11-19 05:18:28 +03:00
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ referrerInfo,
+ postData: null,
+ headers: null,
+ });
+ return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
+ }
+
+ async _reload({frameId, url}) {
2019-11-19 05:18:28 +03:00
+ 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}) {
2019-11-19 05:18:28 +03:00
+ 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}) {
2019-11-19 05:18:28 +03:00
+ 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) };
2019-11-19 05:18:28 +03:00
+ }
+
+ async _setFileInputFiles({objectId, frameId, files}) {
2019-11-19 05:18:28 +03:00
+ 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);
2019-11-19 05:18:28 +03:00
+ 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}) {
2019-11-19 05:18:28 +03:00
+ 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);
2019-11-19 05:18:28 +03:00
+ 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}) {
2019-11-19 05:18:28 +03:00
+ 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,
+ };
2019-11-19 05:18:28 +03:00
+ }
+
+ async _scrollIntoViewIfNeeded({objectId, frameId, rect}) {
2019-11-19 05:18:28 +03:00
+ 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');
+ await this._scrollNodeIntoViewIfNeeded(unsafeObject);
+ const box = this._getNodeBoundingBox(unsafeObject);
+ if (rect) {
+ box.x += rect.x;
+ box.y += rect.y;
+ box.width = rect.width;
+ box.height = rect.height;
+ }
+ this._scrollRectIntoViewIfNeeded(unsafeObject, box);
+ }
+
+ async _scrollNodeIntoViewIfNeeded(node) {
+ if (node.nodeType !== 1)
+ node = node.parentElement;
+ if (!node.ownerDocument || !node.ownerDocument.defaultView)
+ return;
+ const global = node.ownerDocument.defaultView;
+ const visibleRatio = await new Promise(resolve => {
+ const observer = new global.IntersectionObserver(entries => {
+ resolve(entries[0].intersectionRatio);
+ observer.disconnect();
+ });
+ observer.observe(node);
+ // Firefox doesn't call IntersectionObserver callback unless
+ // there are rafs.
+ global.requestAnimationFrame(() => {});
+ });
+ if (visibleRatio !== 1.0)
+ node.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
+ }
+
+ _scrollRectIntoViewIfNeeded(node, rect) {
+ // TODO: implement.
+ }
+
+ _getNodeBoundingBox(unsafeObject) {
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ 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}};
2019-11-19 05:18:28 +03:00
+ }
+
+ async _screenshot({mimeType, fullPage, clip}) {
+ const content = this._messageManager.content;
2019-11-19 05:18:28 +03:00
+ 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}) {
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ tip.keyup(keyEvent, flags);
+ } else {
2019-11-19 05:18:28 +03:00
+ throw new Error(`Unknown type ${type}`);
+ }
2019-11-19 05:18:28 +03:00
+ }
+
+ async _dispatchTouchEvent({type, touchPoints, modifiers}) {
2019-11-19 05:18:28 +03:00
+ const frame = this._frameTree.mainFrame();
+ const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
+ type.toLowerCase(),
+ touchPoints.map((point, id) => id),
+ touchPoints.map(point => point.x),
+ touchPoints.map(point => point.y),
+ touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
+ touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
+ touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
+ touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
+ touchPoints.length,
+ modifiers);
+ return {defaultPrevented};
+ }
+
+ async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
2019-11-19 05:18:28 +03:00
+ const frame = this._frameTree.mainFrame();
+ frame.domWindow().windowUtils.sendMouseEvent(
+ type,
+ x,
+ y,
+ button,
+ clickCount,
+ modifiers,
+ false /*aIgnoreRootScrollFrame*/,
+ undefined /*pressure*/,
+ undefined /*inputSource*/,
+ undefined /*isDOMEventSynthesized*/,
+ undefined /*isWidgetEventSynthesized*/,
+ buttons);
+ 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);
+ }
+ }
+
+ async _insertText({text}) {
2019-11-19 05:18:28 +03:00
+ 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}"`);
+ }
+
2019-11-19 05:18:28 +03:00
+ 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));
+
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ if (!states['enabled'])
+ tree['disabled'] = true;
+
+ const attributes = {};
+ if (accElement.attributes) {
+ for (const { key, value } of accElement.attributes.enumerate()) {
+ attributes[key] = value;
+ }
+ }
+ for (const numericalProperty of ['level']) {
+ if (numericalProperty in attributes)
+ tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
+ }
+ for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) {
+ if (stringProperty in attributes)
+ tree[stringProperty] = attributes[stringProperty];
+ }
+ const children = [];
+
+ for (let child = accElement.firstChild; child; child = child.nextSibling) {
+ children.push(buildNode(child));
+ }
+ if (children.length)
+ tree.children = children;
+ return tree;
+ }
+ await waitForQuiet();
+ return {
+ tree: buildNode(docAcc)
+ };
+ }
+}
+
+function takeScreenshot(win, left, top, width, height, mimeType) {
+ const MAX_SKIA_DIMENSIONS = 32767;
+
+ const scale = win.devicePixelRatio;
+ const canvasWidth = width * scale;
+ const canvasHeight = height * scale;
+
+ if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
+ throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
+
+ const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ let ctx = canvas.getContext('2d');
+ ctx.scale(scale, scale);
+ ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
+ const dataURL = canvas.toDataURL(mimeType);
+ return dataURL.substring(dataURL.indexOf(',') + 1);
+};
+
+var EXPORTED_SYMBOLS = ['PageAgent'];
+this.PageAgent = PageAgent;
+
diff --git a/juggler/content/Runtime.js b/juggler/content/Runtime.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..bd5345b1fab48d798b7e628eed67787a4ba952bb
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/Runtime.js
@@ -0,0 +1,534 @@
2019-11-19 05:18:28 +03:00
+"use strict";
+// Note: this file should be loadabale with eval() into worker environment.
+// Avoid Components.*, ChromeUtils and global const variables.
2019-11-19 05:18:28 +03:00
+
+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);
+}
2019-11-19 05:18:28 +03:00
+
+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) {
2019-11-19 05:18:28 +03:00
+ 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 = {
2019-11-19 05:18:28 +03:00
+ 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;
+ }
2019-11-19 05:18:28 +03:00
+ 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, {
2019-11-19 05:18:28 +03:00
+ 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));
+ }
2019-11-19 05:18:28 +03:00
+
+ _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"));
2019-11-19 05:18:28 +03:00
+ }
+
+ _registerWorkerConsoleHandler() {
+ setConsoleEventHandler(message => {
+ const executionContext = Array.from(this._executionContexts.values())[0];
+ this._onConsoleMessage(executionContext, message);
2019-11-19 05:18:28 +03:00
+ });
+ this._eventListeners.push(() => setConsoleEventHandler(null));
+ }
+
+ _onConsoleMessage(executionContext, message) {
+ const type = consoleLevelToProtocolType[message.level];
+ if (!type)
2019-11-19 05:18:28 +03:00
+ return;
+ const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
+ emitEvent(this.events.onConsoleMessage, {
2019-11-19 05:18:28 +03:00
+ args,
+ type,
+ executionContextId: executionContext.id(),
+ location: {
+ lineNumber: message.lineNumber - 1,
+ columnNumber: message.columnNumber - 1,
+ url: message.filename,
2019-11-19 05:18:28 +03:00
+ },
+ });
+ }
+
+ dispose() {
+ for (const tearDown of this._eventListeners)
+ tearDown.call(null);
+ this._eventListeners = [];
2019-11-19 05:18:28 +03:00
+ }
+
+ 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);
2019-11-19 05:18:28 +03:00
+ this._executionContexts.set(context._id, context);
+ if (domWindow)
+ this._windowToExecutionContext.set(domWindow, context);
+ emitEvent(this.events.onExecutionContextCreated, context);
2019-11-19 05:18:28 +03:00
+ 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;
+ }
+
2019-11-19 05:18:28 +03:00
+ 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);
2019-11-19 05:18:28 +03:00
+ this._executionContexts.delete(destroyedContext._id);
+ if (destroyedContext._domWindow)
+ this._windowToExecutionContext.delete(destroyedContext._domWindow);
+ emitEvent(this.events.onExecutionContextDestroyed, destroyedContext);
2019-11-19 05:18:28 +03:00
+ }
+}
+
+class ExecutionContext {
+ constructor(runtime, domWindow, contextGlobal, global, auxData) {
2019-11-19 05:18:28 +03:00
+ this._runtime = runtime;
+ this._domWindow = domWindow;
+ this._contextGlobal = contextGlobal;
2019-11-19 05:18:28 +03:00
+ this._global = global;
+ this._remoteObjects = new Map();
+ this._id = generateId();
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ }
+
+ id() {
+ return this._id;
+ }
+
+ auxData() {
+ return this._auxData;
+ }
+
2019-11-19 05:18:28 +03:00
+ async evaluateScript(script, exceptionDetails = {}) {
+ const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
+ if (this._domWindow && this._domWindow.document)
+ this._domWindow.document.notifyUserGestureActivation();
+
2019-11-19 05:18:28 +03:00
+ let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
+ userInputHelper && userInputHelper.destruct();
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
+ userInputHelper && userInputHelper.destruct();
2019-11-19 05:18:28 +03:00
+ 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() };
2019-11-19 05:18:28 +03:00
+ }
+
+ 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;
+ }
+
2019-11-19 05:18:28 +03:00
+ _createRemoteObject(debuggerObj) {
+ if (debuggerObj instanceof Debugger.Object) {
+ const objectId = generateId();
2019-11-19 05:18:28 +03:00
+ 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'))
2019-11-19 05:18:28 +03:00
+ subtype = 'node';
+ else if (this._instanceOf(debuggerObj, rawObj, 'RegExp'))
2019-11-19 05:18:28 +03:00
+ subtype = 'regexp';
+ else if (this._instanceOf(debuggerObj, rawObj, 'Date'))
2019-11-19 05:18:28 +03:00
+ subtype = 'date';
+ else if (this._instanceOf(debuggerObj, rawObj, 'Map'))
2019-11-19 05:18:28 +03:00
+ subtype = 'map';
+ else if (this._instanceOf(debuggerObj, rawObj, 'Set'))
2019-11-19 05:18:28 +03:00
+ subtype = 'set';
+ else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap'))
2019-11-19 05:18:28 +03:00
+ subtype = 'weakmap';
+ else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet'))
2019-11-19 05:18:28 +03:00
+ subtype = 'weakset';
+ else if (this._instanceOf(debuggerObj, rawObj, 'Error'))
2019-11-19 05:18:28 +03:00
+ subtype = 'error';
+ else if (this._instanceOf(debuggerObj, rawObj, 'Promise'))
2019-11-19 05:18:28 +03:00
+ 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'))) {
2019-11-19 05:18:28 +03:00
+ subtype = 'typedarray';
+ }
+ return {objectId, type, subtype};
+ }
+ if (typeof debuggerObj === 'symbol') {
+ const objectId = generateId();
2019-11-19 05:18:28 +03:00
+ 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;
2019-11-19 05:18:28 +03:00
+ const properties = {};
+ for (let [key, value] of Object.entries(obj)) {
+ properties[key] = {
+ 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});
2019-11-19 05:18:28 +03:00
+ if (result.throw)
+ throw new Error('Object is not serializable');
+ return result.return === undefined ? undefined : JSON.parse(result.return);
2019-11-19 05:18:28 +03:00
+ }
+
+ disposeObject(objectId) {
+ this._remoteObjects.delete(objectId);
+ }
+
+ getObjectProperties(objectId) {
+ if (!this._remoteObjects.has(objectId))
+ throw new Error('Cannot find object with id = ' + arg.objectId);
+ const result = [];
+ for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) {
+ for (const propertyName of obj.getOwnPropertyNames()) {
+ const descriptor = obj.getOwnPropertyDescriptor(propertyName);
+ if (!descriptor.enumerable)
+ continue;
+ result.push({
+ name: propertyName,
+ value: this._createRemoteObject(descriptor.value),
+ });
+ }
+ }
+ return result;
+ }
+
+ _getResult(completionValue, exceptionDetails = {}) {
+ if (!completionValue) {
+ exceptionDetails.text = 'Evaluation terminated!';
+ exceptionDetails.stack = '';
+ return {success: false, obj: null};
+ }
+ if (completionValue.throw) {
+ if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
+ exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
+ exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
+ } else {
+ exceptionDetails.value = this._serialize(completionValue.throw);
+ }
+ return {success: false, obj: null};
+ }
+ return {success: true, obj: completionValue.return};
+ }
+}
+
+const listenersSymbol = Symbol('listeners');
+
+function createEvent() {
+ const listeners = new Set();
+ const subscribeFunction = listener => {
+ listeners.add(listener);
+ return () => listeners.delete(listener);
+ }
+ subscribeFunction[listenersSymbol] = listeners;
+ return subscribeFunction;
+}
+
+function emitEvent(event, ...args) {
+ let listeners = event[listenersSymbol];
+ if (!listeners || !listeners.size)
+ return;
+ listeners = new Set(listeners);
+ for (const listener of listeners)
+ listener.call(null, ...args);
+}
+
+var EXPORTED_SYMBOLS = ['Runtime'];
+this.Runtime = Runtime;
diff --git a/juggler/content/ScrollbarManager.js b/juggler/content/ScrollbarManager.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c6430983568d
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/ScrollbarManager.js
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,85 @@
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css');
+const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css');
+
+const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
+const helper = new Helper();
+
+class ScrollbarManager {
+ constructor(docShell) {
+ this._docShell = docShell;
+ this._customScrollbars = null;
+ this._contentViewerScrollBars = new Map();
+
+ if (isHeadless)
+ this._setCustomScrollbars(HIDDEN_SCROLLBARS);
+
+ const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+
+ this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']);
+ this._eventListeners = [
+ helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL),
+ ];
+ }
+
+ onLocationChange(webProgress, request, URI, flags) {
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
+ return;
+ this._updateAllDocShells();
+ }
+
+ setFloatingScrollbars(enabled) {
+ if (this._customScrollbars === HIDDEN_SCROLLBARS)
+ return;
+ this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null);
+ }
+
+ _setCustomScrollbars(customScrollbars) {
+ if (this._customScrollbars === customScrollbars)
+ return;
+ this._customScrollbars = customScrollbars;
+ this._updateAllDocShells();
+ }
+
+ _updateAllDocShells() {
+ const allDocShells = [this._docShell];
+ for (let i = 0; i < this._docShell.childCount; i++)
+ allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
+ // At this point, a content viewer might not be loaded for certain docShells.
+ // Scrollbars will be updated in onLocationChange.
+ const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer);
+
+ // Update scrollbar stylesheets.
+ for (const contentViewer of contentViewers) {
+ const oldScrollbars = this._contentViewerScrollBars.get(contentViewer);
+ if (oldScrollbars === this._customScrollbars)
+ continue;
+ const winUtils = contentViewer.DOMDocument.defaultView.windowUtils;
+ if (oldScrollbars)
+ winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET);
+ if (this._customScrollbars)
+ winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET);
+ }
+ // Update state for all *existing* docShells.
+ this._contentViewerScrollBars.clear();
+ for (const contentViewer of contentViewers)
+ this._contentViewerScrollBars.set(contentViewer, this._customScrollbars);
+ }
+
+ dispose() {
+ this._setCustomScrollbars(null);
+ helper.removeListeners(this._eventListeners);
+ }
+}
+
+var EXPORTED_SYMBOLS = ['ScrollbarManager'];
+this.ScrollbarManager = ScrollbarManager;
+
diff --git a/juggler/content/WorkerMain.js b/juggler/content/WorkerMain.js
new file mode 100644
index 0000000000000000000000000000000000000000..a6ed6200364b871ee21ee2cdfd2c9246c9bf0055
--- /dev/null
+++ b/juggler/content/WorkerMain.js
@@ -0,0 +1,69 @@
+"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 */);
+runtime.createExecutionContext(null /* domWindow */, global, {});
+
+class RuntimeAgent {
+ constructor(runtime, channel, sessionId) {
+ this._runtime = runtime;
+ this._browserRuntime = channel.connect(sessionId + 'runtime');
+ this._eventListeners = [
+ channel.register(sessionId + 'runtime', {
+ evaluate: this._runtime.evaluate.bind(this._runtime),
+ callFunction: this._runtime.callFunction.bind(this._runtime),
+ getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
+ disposeObject: this._runtime.disposeObject.bind(this._runtime),
+ }),
+ this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
+ this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
+ this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
+ ];
+ for (const context of this._runtime.executionContexts())
+ this._onExecutionContextCreated(context);
+ }
+
+ _onExecutionContextCreated(executionContext) {
+ this._browserRuntime.emit('runtimeExecutionContextCreated', {
+ executionContextId: executionContext.id(),
+ auxData: executionContext.auxData(),
+ });
+ }
+
+ _onExecutionContextDestroyed(executionContext) {
+ this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
+ executionContextId: executionContext.id(),
+ });
+ }
+
+ dispose() {
+ for (const disposer of this._eventListeners)
+ disposer();
+ this._eventListeners = [];
+ }
+}
+
+channel.register('', {
+ attach: ({sessionId}) => {
+ const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId);
+ runtimeAgents.set(sessionId, runtimeAgent);
+ },
+
+ detach: ({sessionId}) => {
+ const runtimeAgent = runtimeAgents.get(sessionId);
+ runtimeAgents.delete(sessionId);
+ runtimeAgent.dispose();
+ },
+});
+
diff --git a/juggler/content/floating-scrollbars.css b/juggler/content/floating-scrollbars.css
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..7709bdd34c65062fc63684ef17fc792d3991d965
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/floating-scrollbars.css
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,47 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
+ inside a <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;
+}
diff --git a/juggler/content/hidden-scrollbars.css b/juggler/content/hidden-scrollbars.css
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402dfd2ac4f6
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/hidden-scrollbars.css
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,13 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
+ inside a <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;
+}
+
diff --git a/juggler/content/main.js b/juggler/content/main.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..a3d9501d4582bf1428d9e994d609dc54e59b90c7
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/content/main.js
@@ -0,0 +1,174 @@
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
2019-11-19 05:18:28 +03:00
+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');
2019-11-19 05:18:28 +03:00
+
+const ALL_PERMISSIONS = [
+ 'geo',
+ 'microphone',
+ 'camera',
+ 'desktop-notification',
+];
+
2019-11-19 05:18:28 +03:00
+const scrollbarManager = new ScrollbarManager(docShell);
+let frameTree;
+let networkMonitor;
2019-11-19 05:18:28 +03:00
+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();
+}
2019-11-19 05:18:28 +03:00
+
+function setGeolocationOverrideInDocShell(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);
+ }
+}
+
+function setOnlineOverrideInDocShell(override) {
+ if (!override) {
+ docShell.onlineOverride = Ci.nsIDocShell.ONLINE_OVERRIDE_NONE;
+ return;
+ }
+ docShell.onlineOverride = override === 'online' ?
+ Ci.nsIDocShell.ONLINE_OVERRIDE_ONLINE : Ci.nsIDocShell.ONLINE_OVERRIDE_OFFLINE;
+}
+
+function initialize() {
+ let response = sendSyncMessage('juggler:content-ready', {})[0];
+ if (!response)
+ response = { sessionIds: [], browserContextOptions: {}, waitForInitialNavigation: false };
2019-11-19 05:18:28 +03:00
+
+ const { sessionIds, browserContextOptions, waitForInitialNavigation } = response;
+ const { userAgent, bypassCSP, javaScriptDisabled, viewport, scriptsToEvaluateOnNewDocument, bindings, locale, geolocation, onlineOverride } = browserContextOptions;
2019-11-19 05:18:28 +03:00
+
+ if (userAgent !== undefined)
+ docShell.browsingContext.customUserAgent = userAgent;
+ if (bypassCSP !== undefined)
+ docShell.bypassCSPEnabled = bypassCSP;
+ if (javaScriptDisabled !== undefined)
+ docShell.allowJavascript = !javaScriptDisabled;
+ if (locale !== undefined)
+ docShell.languageOverride = locale;
+ if (geolocation !== undefined)
+ setGeolocationOverrideInDocShell(geolocation);
+ if (onlineOverride !== undefined)
+ setOnlineOverrideInDocShell(onlineOverride);
+ if (viewport !== undefined) {
+ docShell.contentViewer.overrideDPPX = viewport.deviceScaleFactor || this._initialDPPX;
+ docShell.deviceSizeIsPageSize = viewport.isMobile;
+ docShell.touchEventsOverride = viewport.hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE;
+ scrollbarManager.setFloatingScrollbars(viewport.isMobile);
+ }
+
+ frameTree = new FrameTree(docShell, waitForInitialNavigation);
+ 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);
2019-11-19 05:18:28 +03:00
+
+ channel.register('', {
+ attach({sessionId}) {
+ createContentSession(channel, sessionId);
+ },
+
+ detach({sessionId}) {
+ disposeContentSession(sessionId);
+ },
+
+ addScriptToEvaluateOnNewDocument(script) {
+ frameTree.addScriptToEvaluateOnNewDocument(script);
+ },
+
+ addBinding(name, script) {
+ frameTree.addBinding(name, script);
+ },
+
+ setGeolocationOverride(geolocation) {
+ setGeolocationOverrideInDocShell(geolocation);
+ },
+
+ setOnlineOverride(override) {
+ setOnlineOverrideInDocShell(override);
+ },
+
+ async ensurePermissions(permissions) {
+ const checkPermissions = () => {
+ for (const permission of ALL_PERMISSIONS) {
+ const actual = Services.perms.testExactPermissionFromPrincipal(this._docShell.domWindow.document.nodePrincipal, permission);
+ const expected = permissions.include(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
+ if (actual !== expected)
+ return false;
+ }
+ return true;
+ }
+
+ if (checkPermissions())
+ return;
+
+ // Track all 'perm-changed' events and wait until permissions are expected.
+ await new Promise(resolve => {
+ const listeners = [helper.addObserver(() => {
+ if (!checkPermission())
+ return;
+ helper.removeListeners(listeners);
+ resolve();
+ }, 'perm-changed')];
+ });
+ },
+
+ dispose() {
+ },
+ });
+
+ const gListeners = [
+ helper.addEventListener(messageManager, 'unload', msg => {
+ helper.removeListeners(gListeners);
+ channel.dispose();
+
+ for (const sessionId of sessions.keys())
+ disposeContentSession(sessionId);
+
+ scrollbarManager.dispose();
+ networkMonitor.dispose();
+ frameTree.dispose();
+ }),
+ ];
+}
+
+initialize();
diff --git a/juggler/jar.mn b/juggler/jar.mn
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..164060acebeaf784d0c38cf161f408e5d141a44e
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/jar.mn
@@ -0,0 +1,29 @@
2019-11-19 05:18:28 +03:00
+# 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/BrowserContextManager.js (BrowserContextManager.js)
+ content/TargetRegistry.js (TargetRegistry.js)
+ content/SimpleChannel.js (SimpleChannel.js)
2019-11-19 05:18:28 +03:00
+ 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)
2019-11-19 05:18:28 +03:00
+ content/content/ScrollbarManager.js (content/ScrollbarManager.js)
+ content/content/floating-scrollbars.css (content/floating-scrollbars.css)
+ content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)
+
diff --git a/juggler/moz.build b/juggler/moz.build
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..1a0a3130bf9509829744fadc692a79754fddd351
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/moz.build
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ["components"]
+
+JAR_MANIFESTS += ["jar.mn"]
+#JS_PREFERENCE_FILES += ["prefs/marionette.js"]
+
+#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"]
+#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Testing", "Juggler")
+
diff --git a/juggler/protocol/AccessibilityHandler.js b/juggler/protocol/AccessibilityHandler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..2f2b7ca247f6b6dff396fb4b644654de87598507
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/AccessibilityHandler.js
@@ -0,0 +1,17 @@
2019-11-19 05:18:28 +03:00
+class AccessibilityHandler {
+ constructor(chromeSession, sessionId, contentChannel) {
2019-11-19 05:18:28 +03:00
+ this._chromeSession = chromeSession;
+ this._contentPage = contentChannel.connect(sessionId + 'page');
2019-11-19 05:18:28 +03:00
+ }
+
+ async getFullAXTree(params) {
+ return await this._contentPage.send('getFullAXTree', params);
2019-11-19 05:18:28 +03:00
+ }
+
+ dispose() {
+ this._contentPage.dispose();
+ }
2019-11-19 05:18:28 +03:00
+}
+
+var EXPORTED_SYMBOLS = ['AccessibilityHandler'];
+this.AccessibilityHandler = AccessibilityHandler;
diff --git a/juggler/protocol/BrowserHandler.js b/juggler/protocol/BrowserHandler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..e225fc81c62bbfac4d071ab1a9d83a754dda46bb
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/BrowserHandler.js
@@ -0,0 +1,178 @@
2019-11-19 05:18:28 +03:00
+"use strict";
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { allowAllCerts } = ChromeUtils.import(
+ "chrome://marionette/content/cert.js"
+);
+const {BrowserContextManager} = ChromeUtils.import("chrome://juggler/content/BrowserContextManager.js");
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+
+const helper = new Helper();
2019-11-19 05:18:28 +03:00
+
+class BrowserHandler {
+ /**
+ * @param {ChromeSession} session
+ */
+ constructor(session) {
+ this._session = session;
2019-11-19 05:18:28 +03:00
+ this._contextManager = BrowserContextManager.instance();
+ this._targetRegistry = TargetRegistry.instance();
+ this._enabled = false;
+ this._attachToDefaultContext = false;
+ this._eventListeners = [];
+ this._createdBrowserContextIds = new Set();
+ }
+
+ 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 sessionId = this._session.dispatcher().createSession(target.id(), true /* shouldConnect */);
+ this._session.emitEvent('Browser.attachedToTarget', {
+ sessionId,
+ targetInfo: target.info()
+ });
+ }
+
+ this._eventListeners = [
+ helper.on(this._targetRegistry, TargetRegistry.Events.PageTargetReady, this._onPageTargetReady.bind(this)),
+ ];
+ }
+
+ async createBrowserContext(options) {
+ if (!this._enabled)
+ throw new Error('Browser domain is not enabled');
+ const browserContext = this._contextManager.createBrowserContext(options);
+ this._createdBrowserContextIds.add(browserContext.browserContextId);
+ return {browserContextId: browserContext.browserContextId};
+ }
+
+ async removeBrowserContext({browserContextId}) {
+ if (!this._enabled)
+ throw new Error('Browser domain is not enabled');
+ this._createdBrowserContextIds.delete(browserContextId);
+ this._contextManager.browserContextForId(browserContextId).destroy();
+ }
+
+ dispose() {
+ helper.removeListeners(this._eventListeners);
+ for (const browserContextId of this._createdBrowserContextIds) {
+ const browserContext = this._contextManager.browserContextForId(browserContextId);
+ if (browserContext.options.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._contextManager.defaultContext();
+ }
+
+ _onPageTargetReady({sessionIds, target}) {
+ if (!this._shouldAttachToTarget(target))
+ return;
+ const sessionId = this._session.dispatcher().createSession(target.id(), false /* shouldConnect */);
+ sessionIds.push(sessionId);
+ this._session.emitEvent('Browser.attachedToTarget', {
+ sessionId,
+ targetInfo: target.info()
+ });
+ }
+
+ async newPage({browserContextId}) {
+ const targetId = await this._targetRegistry.newPage({browserContextId});
+ return {targetId};
2019-11-19 05:18:28 +03:00
+ }
+
+ async close() {
+ let browserWindow = Services.wm.getMostRecentWindow(
+ "navigator:browser"
+ );
+ if (browserWindow && browserWindow.gBrowserInit) {
+ await browserWindow.gBrowserInit.idleTasksFinishedPromise;
+ }
2019-11-19 05:18:28 +03:00
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+
+ async setIgnoreHTTPSErrors({enabled}) {
+ if (!enabled) {
+ allowAllCerts.disable()
2019-11-19 05:18:28 +03:00
+ Services.prefs.setBoolPref('security.mixed_content.block_active_content', true);
+ } else {
+ allowAllCerts.enable()
+ Services.prefs.setBoolPref('security.mixed_content.block_active_content', false);
+ }
+ }
+
+ async grantPermissions({browserContextId, origin, permissions}) {
+ await this._contextManager.browserContextForId(browserContextId).grantPermissions(origin, permissions);
2019-11-19 05:18:28 +03:00
+ }
+
+ resetPermissions({browserContextId}) {
+ this._contextManager.browserContextForId(browserContextId).resetPermissions();
2019-11-19 05:18:28 +03:00
+ }
+
+ setExtraHTTPHeaders({browserContextId, headers}) {
+ this._contextManager.browserContextForId(browserContextId).options.extraHTTPHeaders = headers;
+ }
+
+ setHTTPCredentials({browserContextId, credentials}) {
+ this._contextManager.browserContextForId(browserContextId).options.httpCredentials = credentials;
+ }
+
+ setRequestInterception({browserContextId, enabled}) {
+ this._contextManager.browserContextForId(browserContextId).options.requestInterceptionEnabled = enabled;
+ }
+
+ async setGeolocationOverride({browserContextId, geolocation}) {
+ await this._contextManager.browserContextForId(browserContextId).setGeolocationOverride(geolocation);
+ }
+
+ async setOnlineOverride({browserContextId, override}) {
+ await this._contextManager.browserContextForId(browserContextId).setOnlineOverride(override);
+ }
+
+ async addScriptToEvaluateOnNewDocument({browserContextId, script}) {
+ await this._contextManager.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
+ }
+
+ async addBinding({browserContextId, name, script}) {
+ await this._contextManager.browserContextForId(browserContextId).addBinding(name, script);
+ }
+
2019-11-19 05:18:28 +03:00
+ setCookies({browserContextId, cookies}) {
+ this._contextManager.browserContextForId(browserContextId).setCookies(cookies);
2019-11-19 05:18:28 +03:00
+ }
+
+ clearCookies({browserContextId}) {
+ this._contextManager.browserContextForId(browserContextId).clearCookies();
2019-11-19 05:18:28 +03:00
+ }
+
+ getCookies({browserContextId}) {
+ const cookies = this._contextManager.browserContextForId(browserContextId).getCookies();
+ return {cookies};
2019-11-19 05:18:28 +03:00
+ }
+
+ 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};
+ }
+}
+
+var EXPORTED_SYMBOLS = ['BrowserHandler'];
+this.BrowserHandler = BrowserHandler;
diff --git a/juggler/protocol/Dispatcher.js b/juggler/protocol/Dispatcher.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..b75f20324cb582b6ad85bfe5e7e530ccb8111742
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/Dispatcher.js
@@ -0,0 +1,194 @@
2019-11-19 05:18:28 +03:00
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
+const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js");
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const helper = new Helper();
+const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
2019-11-19 05:18:28 +03:00
+
+const PROTOCOL_HANDLERS = {
+ Page: ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js").PageHandler,
+ Network: ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js").NetworkHandler,
+ Browser: ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js").BrowserHandler,
+ Runtime: ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js").RuntimeHandler,
+ Accessibility: ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js").AccessibilityHandler,
+};
+
+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._targetSessions = new Map();
+ this._sessions = new Map();
+ this._rootSession = new ChromeSession(this, undefined, null /* contentChannel */, TargetRegistry.instance().browserTargetInfo());
2019-11-19 05:18:28 +03:00
+
+ this._eventListeners = [
+ helper.on(TargetRegistry.instance(), TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
+ ];
+ }
+
+ createSession(targetId, shouldConnect) {
2019-11-19 05:18:28 +03:00
+ const targetInfo = TargetRegistry.instance().targetInfo(targetId);
+ if (!targetInfo)
+ throw new Error(`Target "${targetId}" is not found`);
+ let targetSessions = this._targetSessions.get(targetId);
+ if (!targetSessions) {
+ targetSessions = new Map();
+ this._targetSessions.set(targetId, targetSessions);
+ }
+
+ const sessionId = helper.generateId();
+ const contentChannel = targetInfo.type === 'page' ? TargetRegistry.instance().contentChannelForTarget(targetInfo.targetId) : null;
+ if (shouldConnect && contentChannel)
+ contentChannel.connect('').send('attach', {sessionId});
+ const chromeSession = new ChromeSession(this, sessionId, contentChannel, targetInfo);
2019-11-19 05:18:28 +03:00
+ targetSessions.set(sessionId, chromeSession);
+ this._sessions.set(sessionId, chromeSession);
+ return sessionId;
+ }
+
+ _dispose() {
+ helper.removeListeners(this._eventListeners);
+ this._connection.onmessage = null;
+ this._connection.onclose = null;
+ this._rootSession.dispose();
+ this._rootSession = null;
+ for (const session of this._sessions.values())
+ session.dispose();
+ this._sessions.clear();
+ this._targetSessions.clear();
+ }
+
+ _onTargetDestroyed(target) {
+ const targetId = target.id();
2019-11-19 05:18:28 +03:00
+ const sessions = this._targetSessions.get(targetId);
+ if (!sessions)
+ return;
+ this._targetSessions.delete(targetId);
+ for (const [sessionId, session] of sessions) {
+ session.dispose();
+ this._sessions.delete(sessionId);
+ }
+ }
+
+ async _dispatch(event) {
+ const data = JSON.parse(event.data);
+ const id = data.id;
+ const sessionId = data.sessionId;
+ delete data.sessionId;
+ try {
+ const session = sessionId ? this._sessions.get(sessionId) : this._rootSession;
+ if (!session)
+ throw new Error(`ERROR: cannot find session with id "${sessionId}"`);
+ const method = data.method;
+ const params = data.params || {};
+ if (!id)
+ throw new Error(`ERROR: every message must have an 'id' parameter`);
+ if (!method)
+ throw new Error(`ERROR: every message must have a 'method' parameter`);
+
+ const [domain, methodName] = method.split('.');
+ const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null;
+ if (!descriptor)
+ throw new Error(`ERROR: method '${method}' is not supported`);
+ let details = {};
+ if (!checkScheme(descriptor.params || {}, params, details))
+ throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`);
+
+ const result = await session.dispatch(method, params);
+
+ details = {};
+ if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
+ throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`);
+
+ this._connection.send(JSON.stringify({id, sessionId, result}));
+ } catch (e) {
+ this._connection.send(JSON.stringify({id, sessionId, error: {
+ message: e.message,
+ data: e.stack
+ }}));
+ }
+ }
+
+ _emitEvent(sessionId, eventName, params) {
+ const [domain, eName] = eventName.split('.');
+ const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
+ if (!scheme)
+ throw new Error(`ERROR: event '${eventName}' is not supported`);
+ const details = {};
+ if (!checkScheme(scheme, params || {}, details))
+ throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`);
+ this._connection.send(JSON.stringify({method: eventName, params, sessionId}));
+ }
+}
+
+class ChromeSession {
+ /**
+ * @param {Connection} connection
+ */
+ constructor(dispatcher, sessionId, contentChannel, targetInfo) {
2019-11-19 05:18:28 +03:00
+ this._dispatcher = dispatcher;
+ this._sessionId = sessionId;
+ this._contentChannel = contentChannel;
2019-11-19 05:18:28 +03:00
+ this._targetInfo = targetInfo;
+
+ this._handlers = {};
+ for (const [domainName, handlerFactory] of Object.entries(PROTOCOL_HANDLERS)) {
+ if (protocol.domains[domainName].targets.includes(targetInfo.type))
+ this._handlers[domainName] = new handlerFactory(this, sessionId, contentChannel);
2019-11-19 05:18:28 +03:00
+ }
+ const pageHandler = this._handlers['Page'];
+ if (pageHandler)
+ pageHandler.enable();
+ const networkHandler = this._handlers['Network'];
+ if (networkHandler)
+ networkHandler.enable();
2019-11-19 05:18:28 +03:00
+ }
+
+ dispatcher() {
+ return this._dispatcher;
+ }
+
+ targetId() {
+ return this._targetInfo.targetId;
+ }
+
+ dispose() {
+ if (this._contentChannel)
+ this._contentChannel.connect('').emit('detach', {sessionId: this._sessionId});
+ this._contentChannel = null;
2019-11-19 05:18:28 +03:00
+ for (const [domainName, handler] of Object.entries(this._handlers)) {
+ if (!handler.dispose)
+ throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`);
+ handler.dispose();
+ delete this._handlers[domainName];
+ }
+ // Root session don't have sessionId and don't emit detachedFromTarget.
+ if (this._sessionId) {
+ this._dispatcher._emitEvent(this._dispatcher._rootSession._sessionId, 'Browser.detachedFromTarget', {
2019-11-19 05:18:28 +03:00
+ sessionId: this._sessionId,
+ targetId: this.targetId(),
2019-11-19 05:18:28 +03:00
+ });
+ }
+ }
+
+ emitEvent(eventName, params) {
+ this._dispatcher._emitEvent(this._sessionId, eventName, params);
+ }
+
+ async dispatch(method, params) {
+ const [domainName, methodName] = method.split('.');
+ if (!this._handlers[domainName])
+ throw new Error(`Domain "${domainName}" does not exist`);
+ if (!this._handlers[domainName][methodName])
+ throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`);
+ return await this._handlers[domainName][methodName](params);
+ }
+}
+
+this.EXPORTED_SYMBOLS = ['Dispatcher'];
+this.Dispatcher = Dispatcher;
+
diff --git a/juggler/protocol/NetworkHandler.js b/juggler/protocol/NetworkHandler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..e1f1e21a20768d707a92ffffc8a7c114d9bb783b
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/NetworkHandler.js
@@ -0,0 +1,160 @@
2019-11-19 05:18:28 +03:00
+"use strict";
+
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {NetworkObserver} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
+const helper = new Helper();
+
+class NetworkHandler {
+ constructor(chromeSession, sessionId, contentChannel) {
2019-11-19 05:18:28 +03:00
+ this._chromeSession = chromeSession;
+ this._contentPage = contentChannel.connect(sessionId + 'page');
2019-11-19 05:18:28 +03:00
+ this._networkObserver = NetworkObserver.instance();
+ this._httpActivity = new Map();
+ this._enabled = false;
+ this._browser = TargetRegistry.instance().tabForTarget(this._chromeSession.targetId()).linkedBrowser;
+ this._requestInterception = false;
+ this._eventListeners = [];
+ this._pendingRequstWillBeSentEvents = new Set();
+ this._requestIdToFrameId = new Map();
2019-11-19 05:18:28 +03:00
+ }
+
+ async enable() {
+ if (this._enabled)
+ return;
+ this._enabled = true;
+ this._eventListeners = [
+ helper.on(this._networkObserver, 'request', this._onRequest.bind(this)),
+ helper.on(this._networkObserver, 'response', this._onResponse.bind(this)),
+ helper.on(this._networkObserver, 'requestfinished', this._onRequestFinished.bind(this)),
+ helper.on(this._networkObserver, 'requestfailed', this._onRequestFailed.bind(this)),
+ this._networkObserver.startTrackingBrowserNetwork(this._browser),
+ ];
+ }
+
+ async getResponseBody({requestId}) {
+ return this._networkObserver.getResponseBody(this._browser, requestId);
+ }
+
+ async setExtraHTTPHeaders({headers}) {
+ this._networkObserver.setExtraHTTPHeaders(this._browser, headers);
+ }
+
+ async setRequestInterception({enabled}) {
+ if (enabled)
+ this._networkObserver.enableRequestInterception(this._browser);
+ else
+ this._networkObserver.disableRequestInterception(this._browser);
+ // 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._networkObserver.resumeInterceptedRequest(this._browser, requestId, method, headers, postData);
+ }
+
+ async abortInterceptedRequest({requestId, errorCode}) {
+ this._networkObserver.abortInterceptedRequest(this._browser, requestId, errorCode);
+ }
+
+ async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) {
+ this._networkObserver.fulfillInterceptedRequest(this._browser, requestId, status, statusText, headers, base64body);
2019-11-19 05:18:28 +03:00
+ }
+
+ dispose() {
+ this._contentPage.dispose();
2019-11-19 05:18:28 +03:00
+ 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._chromeSession.emitEvent('Network.requestWillBeSent', activity.request);
+ activity._lastSentEvent = 'requestWillBeSent';
+ }
+ if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) {
+ this._chromeSession.emitEvent('Network.responseReceived', activity.response);
+ activity._lastSentEvent = 'responseReceived';
+ }
+ if (activity._lastSentEvent === 'responseReceived' && activity.complete) {
+ this._chromeSession.emitEvent('Network.requestFinished', activity.complete);
+ activity._lastSentEvent = 'requestFinished';
+ }
+ if (activity._lastSentEvent && activity.failed) {
+ this._chromeSession.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});
2019-11-19 05:18:28 +03:00
+ } catch (e) {
+ pendingRequestCallback();
+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
+ return;
2019-11-19 05:18:28 +03:00
+ }
+ // 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);
2019-11-19 05:18:28 +03:00
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
+ activity.request = {
+ frameId,
2019-11-19 05:18:28 +03:00
+ ...eventDetails,
+ };
+ this._reportHTTPAcitivityEvents(activity);
+ pendingRequestCallback();
+ this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
+ }
+
+ async _onResponse(httpChannel, eventDetails) {
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
+ activity.response = eventDetails;
+ this._reportHTTPAcitivityEvents(activity);
+ }
+
+ async _onRequestFinished(httpChannel, eventDetails) {
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
+ activity.complete = eventDetails;
+ this._reportHTTPAcitivityEvents(activity);
+ }
+
+ async _onRequestFailed(httpChannel, eventDetails) {
+ const activity = this._ensureHTTPActivity(eventDetails.requestId);
+ activity.failed = eventDetails;
+ this._reportHTTPAcitivityEvents(activity);
+ }
+}
+
+var EXPORTED_SYMBOLS = ['NetworkHandler'];
+this.NetworkHandler = NetworkHandler;
diff --git a/juggler/protocol/PageHandler.js b/juggler/protocol/PageHandler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..11f9567d816304906df6b6192b3fb71e6c9d53dc
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/PageHandler.js
@@ -0,0 +1,348 @@
2019-11-19 05:18:28 +03:00
+"use strict";
+
+const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
+const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.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(chromeSession, contentChannel, sessionId, workerId) {
+ this._chromeSession = chromeSession;
+ this._sessionId = sessionId;
+ this._contentWorker = contentChannel.connect(sessionId + workerId);
+ this._workerId = workerId;
+
+ const emitWrappedProtocolEvent = eventName => {
+ return params => {
+ this._chromeSession.emitEvent('Page.dispatchMessageFromWorker', {
+ workerId,
+ message: JSON.stringify({method: eventName, params}),
+ });
+ }
+ }
+
+ this._eventListeners = [
+ contentChannel.register(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._chromeSession.emitEvent('Page.dispatchMessageFromWorker', {
+ workerId: this._workerId,
+ message: JSON.stringify({result, id: message.id}),
+ });
+ }
+
+ dispose() {
+ this._contentWorker.dispose();
+ helper.removeListeners(this._eventListeners);
+ }
+}
+
2019-11-19 05:18:28 +03:00
+class PageHandler {
+ constructor(chromeSession, sessionId, contentChannel) {
2019-11-19 05:18:28 +03:00
+ this._chromeSession = chromeSession;
+ this._contentChannel = contentChannel;
+ this._sessionId = sessionId;
+ this._contentPage = contentChannel.connect(sessionId + 'page');
+ this._workers = new Map();
+
+ const emitProtocolEvent = eventName => {
+ return (...args) => this._chromeSession.emitEvent(eventName, ...args);
+ }
+
+ this._eventListeners = [
+ contentChannel.register(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'),
+ 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 = TargetRegistry.instance().targetForId(chromeSession.targetId());
2019-11-19 05:18:28 +03:00
+ this._browser = TargetRegistry.instance().tabForTarget(chromeSession.targetId()).linkedBrowser;
+ this._dialogs = new Map();
+
+ this._enabled = false;
+ }
+
+ _onWorkerCreated({workerId, frameId, url}) {
+ const worker = new WorkerHandler(this._chromeSession, this._contentChannel, this._sessionId, workerId);
+ this._workers.set(workerId, worker);
+ this._chromeSession.emitEvent('Page.workerCreated', {workerId, frameId, url});
+ }
+
+ _onWorkerDestroyed({workerId}) {
+ const worker = this._workers.get(workerId);
+ if (!worker)
+ return;
+ this._workers.delete(workerId);
+ worker.dispose();
+ this._chromeSession.emitEvent('Page.workerDestroyed', {workerId});
+ }
+
2019-11-19 05:18:28 +03:00
+ async close({runBeforeUnload}) {
+ // Postpone target close to deliver response in session.
+ Services.tm.dispatchToMainThread(() => {
+ TargetRegistry.instance().closePage(this._chromeSession.targetId(), runBeforeUnload);
+ });
+ }
+
+ async enable() {
+ if (this._enabled)
+ return;
+ this._enabled = true;
+ this._updateModalDialogs();
+
+ this._eventListeners.push(...[
2019-11-19 05:18:28 +03:00
+ 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(TargetRegistry.instance(), TargetRegistry.Events.TargetCrashed, targetId => {
+ if (targetId === this._chromeSession.targetId())
+ this._chromeSession.emitEvent('Page.crashed', {});
+ }),
+ ]);
2019-11-19 05:18:28 +03:00
+ }
+
+ dispose() {
+ this._contentPage.dispose();
2019-11-19 05:18:28 +03:00
+ helper.removeListeners(this._eventListeners);
+ }
+
+ async setViewportSize({viewportSize}) {
+ const size = this._pageTarget.setViewportSize(viewportSize);
+ await this._contentPage.send('awaitViewportDimensions', {
+ width: size.width,
+ height: size.height
+ });
2019-11-19 05:18:28 +03:00
+ }
+
+ _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._chromeSession.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._chromeSession.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);
2019-11-19 05:18:28 +03:00
+ }
+
+ async setEmulatedMedia(options) {
+ return await this._contentPage.send('setEmulatedMedia', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async setCacheDisabled(options) {
+ return await this._contentPage.send('setCacheDisabled', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async addBinding(options) {
+ return await this._contentPage.send('addBinding', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async adoptNode(options) {
+ return await this._contentPage.send('adoptNode', options);
+ }
+
2019-11-19 05:18:28 +03:00
+ async screenshot(options) {
+ return await this._contentPage.send('screenshot', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async getBoundingBox(options) {
+ return await this._contentPage.send('getBoundingBox', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async getContentQuads(options) {
+ return await this._contentPage.send('getContentQuads', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ /**
+ * @param {{frameId: string, url: string}} options
+ */
+ async navigate(options) {
+ return await this._contentPage.send('navigate', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ /**
+ * @param {{frameId: string, url: string}} options
+ */
+ async goBack(options) {
+ return await this._contentPage.send('goBack', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ /**
+ * @param {{frameId: string, url: string}} options
+ */
+ async goForward(options) {
+ return await this._contentPage.send('goForward', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ /**
+ * @param {{frameId: string, url: string}} options
+ */
+ async reload(options) {
+ return await this._contentPage.send('reload', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async describeNode(options) {
+ return await this._contentPage.send('describeNode', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async scrollIntoViewIfNeeded(options) {
+ return await this._contentPage.send('scrollIntoViewIfNeeded', options);
+ }
+
2019-11-19 05:18:28 +03:00
+ async addScriptToEvaluateOnNewDocument(options) {
+ return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async removeScriptToEvaluateOnNewDocument(options) {
+ return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async dispatchKeyEvent(options) {
+ return await this._contentPage.send('dispatchKeyEvent', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async dispatchTouchEvent(options) {
+ return await this._contentPage.send('dispatchTouchEvent', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async dispatchMouseEvent(options) {
+ return await this._contentPage.send('dispatchMouseEvent', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async insertText(options) {
+ return await this._contentPage.send('insertText', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async crash(options) {
+ return await this._contentPage.send('crash', options);
+ }
+
2019-11-19 05:18:28 +03:00
+ 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));
+ }
2019-11-19 05:18:28 +03:00
+}
+
+class Dialog {
+ static createIfSupported(prompt) {
+ const type = prompt.args.promptType;
+ switch (type) {
+ case 'alert':
+ case 'prompt':
+ case 'confirm':
+ return new Dialog(prompt, type);
+ case 'confirmEx':
+ return new Dialog(prompt, 'beforeunload');
+ default:
+ return null;
+ };
+ }
+
+ constructor(prompt, type) {
+ this._id = helper.generateId();
+ this._type = type;
+ this._prompt = prompt;
+ }
+
+ id() {
+ return this._id;
+ }
+
+ message() {
+ return this._prompt.ui.infoBody.textContent;
+ }
+
+ type() {
+ return this._type;
+ }
+
+ prompt() {
+ return this._prompt;
+ }
+
+ dismiss() {
+ if (this._prompt.ui.button1)
+ this._prompt.ui.button1.click();
+ else
+ this._prompt.ui.button0.click();
+ }
+
+ defaultValue() {
+ return this._prompt.ui.loginTextbox.value;
+ }
+
+ accept(promptValue) {
+ if (typeof promptValue === 'string' && this._type === 'prompt')
+ this._prompt.ui.loginTextbox.value = promptValue;
+ this._prompt.ui.button0.click();
+ }
+}
+
+var EXPORTED_SYMBOLS = ['PageHandler'];
+this.PageHandler = PageHandler;
diff --git a/juggler/protocol/PrimitiveTypes.js b/juggler/protocol/PrimitiveTypes.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07f95a06fa
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/PrimitiveTypes.js
2019-11-19 05:18:28 +03:00
@@ -0,0 +1,143 @@
+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'];
diff --git a/juggler/protocol/Protocol.js b/juggler/protocol/Protocol.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..67df4d5592d66e0db3c7c120ad12f9b360b9c45d
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/Protocol.js
@@ -0,0 +1,778 @@
2019-11-19 05:18:28 +03:00
+const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
+
+// Protocol-specific types.
+const browserTypes = {};
+
+browserTypes.TargetInfo = {
+ type: t.Enum(['page']),
2019-11-19 05:18:28 +03:00
+ 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),
+};
+
+const pageTypes = {};
+pageTypes.DOMPoint = {
2019-11-19 05:18:28 +03:00
+ x: t.Number,
+ y: t.Number,
+};
+
+pageTypes.Rect = {
+ x: t.Number,
+ y: t.Number,
+ width: t.Number,
+ height: t.Number,
2019-11-19 05:18:28 +03:00
+};
+
+pageTypes.Size = {
+ width: t.Number,
+ height: t.Number,
+};
+
+pageTypes.Viewport = {
+ viewportSize: pageTypes.Size,
+ deviceScaleFactor: t.Number,
+ isMobile: t.Boolean,
+ hasTouch: t.Boolean,
+};
+
+pageTypes.DOMQuad = {
+ p1: pageTypes.DOMPoint,
+ p2: pageTypes.DOMPoint,
+ p3: pageTypes.DOMPoint,
+ p4: pageTypes.DOMPoint,
+};
+
+pageTypes.TouchPoint = {
2019-11-19 05:18:28 +03:00
+ 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 = {
2019-11-19 05:18:28 +03:00
+ 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 = {
2019-11-19 05:18:28 +03:00
+ role: t.String,
+ name: t.String,
+ children: t.Optional(t.Array(t.Recursive(axTypes, 'AXTree'))),
2019-11-19 05:18:28 +03:00
+
+ 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),
2019-11-19 05:18:28 +03:00
+}
+
+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,
+};
+
2019-11-19 05:18:28 +03:00
+const Browser = {
+ targets: ['browser'],
+
+ types: browserTypes,
+
+ events: {
+ 'attachedToTarget': {
+ sessionId: t.String,
+ targetInfo: browserTypes.TargetInfo,
+ },
+ 'detachedFromTarget': {
+ sessionId: t.String,
+ targetId: t.String,
+ },
+ },
+
2019-11-19 05:18:28 +03:00
+ methods: {
+ 'enable': {
+ params: {
+ attachToDefaultContext: t.Boolean,
+ },
+ },
+ 'createBrowserContext': {
+ params: {
+ removeOnDetach: t.Optional(t.Boolean),
+ userAgent: t.Optional(t.String),
+ bypassCSP: t.Optional(t.Boolean),
+ javaScriptDisabled: t.Optional(t.Boolean),
+ viewport: t.Optional(pageTypes.Viewport),
+ locale: t.Optional(t.String),
+ },
+ returns: {
+ browserContextId: t.String,
+ },
+ },
+ 'removeBrowserContext': {
+ params: {
+ browserContextId: t.String,
+ },
+ },
+ 'newPage': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ },
+ returns: {
+ targetId: t.String,
+ }
+ },
2019-11-19 05:18:28 +03:00
+ 'close': {},
+ 'getInfo': {
+ returns: {
+ userAgent: t.String,
+ version: t.String,
+ },
+ },
+ 'setIgnoreHTTPSErrors': {
+ params: {
+ enabled: t.Boolean,
+ },
+ },
+ '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),
+ }
+ },
+ 'addScriptToEvaluateOnNewDocument': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ script: t.String,
+ }
+ },
+ 'addBinding': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ name: t.String,
+ script: t.String,
+ },
+ },
2019-11-19 05:18:28 +03:00
+ 'grantPermissions': {
+ params: {
+ origin: t.String,
+ browserContextId: t.Optional(t.String),
+ permissions: t.Array(t.String),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'resetPermissions': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ }
+ },
+ 'setCookies': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ cookies: t.Array(browserTypes.CookieOptions),
2019-11-19 05:18:28 +03:00
+ }
+ },
+ 'clearCookies': {
2019-11-19 05:18:28 +03:00
+ params: {
+ browserContextId: t.Optional(t.String),
+ }
+ },
+ 'getCookies': {
+ params: {
+ browserContextId: t.Optional(t.String)
2019-11-19 05:18:28 +03:00
+ },
+ returns: {
+ cookies: t.Array(browserTypes.Cookie),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'setOnlineOverride': {
+ params: {
+ browserContextId: t.Optional(t.String),
+ override: t.Optional(t.Enum(['online', 'offline'])),
+ }
+ },
2019-11-19 05:18:28 +03:00
+ },
+};
+
+const Network = {
+ targets: ['page'],
+ types: networkTypes,
2019-11-19 05:18:28 +03:00
+ 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,
2019-11-19 05:18:28 +03:00
+ url: t.String,
+ method: t.String,
+ navigationId: t.Optional(t.String),
2019-11-19 05:18:28 +03:00
+ cause: t.String,
+ },
+ 'responseReceived': {
+ securityDetails: t.Nullable(networkTypes.SecurityDetails),
2019-11-19 05:18:28 +03:00
+ 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),
2019-11-19 05:18:28 +03:00
+ },
+ 'requestFinished': {
+ requestId: t.String,
+ },
+ 'requestFailed': {
+ requestId: t.String,
+ errorCode: t.String,
+ },
+ },
+ methods: {
+ 'setRequestInterception': {
+ params: {
+ enabled: t.Boolean,
+ },
+ },
+ 'setExtraHTTPHeaders': {
+ params: {
+ headers: t.Array(networkTypes.HTTPHeader),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'abortInterceptedRequest': {
2019-11-19 05:18:28 +03:00
+ params: {
+ requestId: t.String,
+ errorCode: t.String,
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'resumeInterceptedRequest': {
2019-11-19 05:18:28 +03:00
+ params: {
+ requestId: t.String,
+ method: t.Optional(t.String),
+ headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
+ postData: t.Optional(t.String),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'fulfillInterceptedRequest': {
+ params: {
+ requestId: t.String,
+ status: t.Number,
+ statusText: t.String,
+ headers: t.Array(networkTypes.HTTPHeader),
+ base64body: t.Optional(t.String), // base64-encoded
+ },
+ },
2019-11-19 05:18:28 +03:00
+ 'getResponseBody': {
+ params: {
+ requestId: t.String,
+ },
+ returns: {
+ base64body: t.String,
+ evicted: t.Optional(t.Boolean),
+ },
+ },
+ },
+};
+
+const Runtime = {
+ targets: ['page'],
+ types: runtimeTypes,
2019-11-19 05:18:28 +03:00
+ events: {
+ 'executionContextCreated': {
+ executionContextId: t.String,
+ auxData: t.Any,
+ },
+ 'executionContextDestroyed': {
+ executionContextId: t.String,
+ },
+ 'console': {
+ executionContextId: t.String,
+ args: t.Array(runtimeTypes.RemoteObject),
2019-11-19 05:18:28 +03:00
+ type: t.String,
+ location: runtimeTypes.ScriptLocation,
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 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),
2019-11-19 05:18:28 +03:00
+ }
+ },
+ 'callFunction': {
+ params: {
+ // Pass frameId here.
+ executionContextId: t.String,
+ functionDeclaration: t.String,
+ returnByValue: t.Optional(t.Boolean),
+ args: t.Array(runtimeTypes.CallFunctionArgument),
2019-11-19 05:18:28 +03:00
+ },
+
+ returns: {
+ result: t.Optional(runtimeTypes.RemoteObject),
+ exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
2019-11-19 05:18:28 +03:00
+ }
+ },
+ 'disposeObject': {
+ params: {
+ executionContextId: t.String,
+ objectId: t.String,
+ },
+ },
+
+ 'getObjectProperties': {
+ params: {
+ executionContextId: t.String,
+ objectId: t.String,
+ },
+
+ returns: {
+ properties: t.Array(runtimeTypes.ObjectProperty),
2019-11-19 05:18:28 +03:00
+ }
+ },
+ },
+};
+
+const Page = {
+ targets: ['page'],
+
+ types: pageTypes,
2019-11-19 05:18:28 +03:00
+ events: {
+ 'ready': {
+ },
+ 'crashed': {
+ },
2019-11-19 05:18:28 +03:00
+ '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),
2019-11-19 05:18:28 +03:00
+ 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']),
+ },
+ '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,
+ },
2019-11-19 05:18:28 +03:00
+ },
+
+ 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,
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'setViewportSize': {
2019-11-19 05:18:28 +03:00
+ params: {
+ viewportSize: t.Nullable(pageTypes.Size),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'setEmulatedMedia': {
+ params: {
+ type: t.Optional(t.Enum(['screen', 'print', ''])),
+ colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'setCacheDisabled': {
+ params: {
+ cacheDisabled: t.Boolean,
+ },
+ },
+ 'describeNode': {
2019-11-19 05:18:28 +03:00
+ params: {
+ frameId: t.String,
+ objectId: t.String,
+ },
+ returns: {
+ contentFrameId: t.Optional(t.String),
+ ownerFrameId: t.Optional(t.String),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ 'scrollIntoViewIfNeeded': {
+ params: {
+ frameId: t.String,
+ objectId: t.String,
+ rect: t.Optional(pageTypes.Rect),
+ },
+ },
2019-11-19 05:18:28 +03:00
+ 'addScriptToEvaluateOnNewDocument': {
+ params: {
+ script: t.String,
+ worldName: t.Optional(t.String),
2019-11-19 05:18:28 +03:00
+ },
+ 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),
+ },
2019-11-19 05:18:28 +03:00
+ },
+ 'adoptNode': {
+ params: {
+ frameId: t.String,
+ objectId: t.String,
+ executionContextId: t.String,
+ },
+ returns: {
+ remoteObject: t.Nullable(runtimeTypes.RemoteObject),
+ },
+ },
2019-11-19 05:18:28 +03:00
+ 'screenshot': {
+ params: {
+ mimeType: t.Enum(['image/png', 'image/jpeg']),
+ fullPage: t.Optional(t.Boolean),
+ clip: t.Optional(pageTypes.Clip),
2019-11-19 05:18:28 +03:00
+ },
+ returns: {
+ data: t.String,
+ }
+ },
+ 'getContentQuads': {
+ params: {
+ frameId: t.String,
+ objectId: t.String,
+ },
+ returns: {
+ quads: t.Array(pageTypes.DOMQuad),
2019-11-19 05:18:28 +03:00
+ },
+ },
+ '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),
2019-11-19 05:18:28 +03:00
+ }
+ },
+ 'dispatchTouchEvent': {
+ params: {
+ type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
+ touchPoints: t.Array(pageTypes.TouchPoint),
2019-11-19 05:18:28 +03:00
+ 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: {}
+ },
2019-11-19 05:18:28 +03:00
+ '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,
+ },
+ },
2019-11-19 05:18:28 +03:00
+ },
+};
+
+
+const Accessibility = {
+ targets: ['page'],
+ types: axTypes,
2019-11-19 05:18:28 +03:00
+ events: {},
+ methods: {
+ 'getFullAXTree': {
+ params: {
+ objectId: t.Optional(t.String),
+ },
2019-11-19 05:18:28 +03:00
+ returns: {
+ tree: axTypes.AXTree
2019-11-19 05:18:28 +03:00
+ },
+ }
+ }
+}
+
+this.protocol = {
+ domains: {Browser, Page, Runtime, Network, Accessibility},
2019-11-19 05:18:28 +03:00
+};
+this.checkScheme = checkScheme;
+this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
diff --git a/juggler/protocol/RuntimeHandler.js b/juggler/protocol/RuntimeHandler.js
2019-11-19 05:18:28 +03:00
new file mode 100644
index 0000000000000000000000000000000000000000..5cc68241bdb420668fd14b45f1a702284a43fad7
2019-11-19 05:18:28 +03:00
--- /dev/null
+++ b/juggler/protocol/RuntimeHandler.js
@@ -0,0 +1,52 @@
2019-11-19 05:18:28 +03:00
+"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(chromeSession, sessionId, contentChannel) {
2019-11-19 05:18:28 +03:00
+ this._chromeSession = chromeSession;
+ this._contentRuntime = contentChannel.connect(sessionId + 'runtime');
+
+ const emitProtocolEvent = eventName => {
+ return (...args) => this._chromeSession.emitEvent(eventName, ...args);
+ }
+
+ this._eventListeners = [
+ contentChannel.register(sessionId + 'runtime', {
+ runtimeConsole: emitProtocolEvent('Runtime.console'),
+ runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
+ runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
+ }),
+ ];
2019-11-19 05:18:28 +03:00
+ }
+
+ async evaluate(options) {
+ return await this._contentRuntime.send('evaluate', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async callFunction(options) {
+ return await this._contentRuntime.send('callFunction', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async getObjectProperties(options) {
+ return await this._contentRuntime.send('getObjectProperties', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ async disposeObject(options) {
+ return await this._contentRuntime.send('disposeObject', options);
2019-11-19 05:18:28 +03:00
+ }
+
+ dispose() {
+ this._contentRuntime.dispose();
+ helper.removeListeners(this._eventListeners);
+ }
2019-11-19 05:18:28 +03:00
+}
+
+var EXPORTED_SYMBOLS = ['RuntimeHandler'];
+this.RuntimeHandler = RuntimeHandler;
diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp
index 5bdd250f8061a2fc1f755a4ea82b91e525b88131..9d5d3b92429abc0a8d570b4ea6db67e2bf2ee8f7 100644
--- a/parser/html/nsHtml5TreeOpExecutor.cpp
+++ b/parser/html/nsHtml5TreeOpExecutor.cpp
@@ -1065,9 +1065,12 @@ void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) {
if (!StaticPrefs::security_csp_enable()) {
return;
}
-
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+ if (mDocShell && static_cast<nsDocShell*>(mDocShell.get())->IsBypassCSPEnabled()) {
+ return;
+ }
+
nsresult rv = NS_OK;
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = mDocument->GetPreloadCsp();
if (!preloadCsp) {
diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp
index e27b18249b9dca7fddbd0c45b5af383e75ef3143..cc352957002985d0d168b7045186b389cbc911fb 100644
--- a/security/manager/ssl/nsCertOverrideService.cpp
+++ b/security/manager/ssl/nsCertOverrideService.cpp
@@ -633,7 +633,7 @@ static bool IsDebugger() {
NS_IMETHODIMP
nsCertOverrideService::
SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) {
- if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
+ if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
return NS_ERROR_NOT_AVAILABLE;
}
diff --git a/services/settings/Utils.jsm b/services/settings/Utils.jsm
index 54eb24bceb10eeccdbdf1d0111f2cc0527cb09f8..0efa6e21ee0f32c0092402db60751c9f0674061d 100644
--- a/services/settings/Utils.jsm
+++ b/services/settings/Utils.jsm
@@ -55,7 +55,7 @@ var Utils = {
Ci.nsIEnvironment
);
const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR");
- return AppConstants.RELEASE_OR_BETA && !Cu.isInAutomation && !isXpcshell
+ return false && !Cu.isInAutomation && !isXpcshell
? "https://firefox.settings.services.mozilla.com/v1"
: gServerURL;
},
diff --git a/toolkit/components/startup/nsAppStartup.cpp b/toolkit/components/startup/nsAppStartup.cpp
index bcbd90bae3deadd97f174520ee30859c6fa591f6..845d92bdf61f12b54f4ee3d7a6d8735f7ee81a9d 100644
--- a/toolkit/components/startup/nsAppStartup.cpp
+++ b/toolkit/components/startup/nsAppStartup.cpp
@@ -335,7 +335,7 @@ nsAppStartup::Quit(uint32_t aMode) {
nsCOMPtr<nsISimpleEnumerator> windowEnumerator;
nsCOMPtr<nsIWindowMediator> mediator(
do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
- if (mediator) {
+ if (ferocity != eForceQuit && mediator) {
mediator->GetEnumerator(nullptr, getter_AddRefs(windowEnumerator));
if (windowEnumerator) {
bool more;
2019-11-19 05:18:28 +03:00
diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
index cc1686589f5539b2adc966c240288cc01c4c5c06..c036fe053f819e9e658ea603095102acc8711f59 100644
2019-11-19 05:18:28 +03:00
--- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
+++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
@@ -177,8 +177,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress,
2019-11-19 05:18:28 +03:00
}
NS_IMETHODIMP
-nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress,
- nsIRequest* aRequest,
+nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
+ nsIURI *aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
+ nsIRequest *aRequest,
int32_t aCurSelfProgress,
int32_t aMaxSelfProgress,
int32_t aCurTotalProgress,
diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm
index cae2e4eec0ae281cdeaa07c6d70a8aef30d79332..ab251f81c4a74923940c7e9c051fbef35e2546c4 100644
--- a/toolkit/mozapps/update/UpdateService.jsm
+++ b/toolkit/mozapps/update/UpdateService.jsm
@@ -3077,7 +3077,7 @@ UpdateService.prototype = {
).running;
}
- return (
+ return true || (
(Cu.isInAutomation || marionetteRunning) &&
Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false)
);
2019-11-19 05:18:28 +03:00
diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
index 299230cb3bde5ecd111454ed6f59d1f0504b67a1..09f4ef69776217e5e9f5cc4ad4de939887d8c871 100644
2019-11-19 05:18:28 +03:00
--- a/toolkit/toolkit.mozbuild
+++ b/toolkit/toolkit.mozbuild
@@ -168,6 +168,7 @@ if CONFIG['ENABLE_MARIONETTE']:
2019-11-19 05:18:28 +03:00
DIRS += [
'/testing/firefox-ui',
'/testing/marionette',
+ '/juggler',
2019-11-19 05:18:28 +03:00
'/toolkit/components/telemetry/tests/marionette',
]
diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
index 0a1f12c789762d8b0020aebe933555d99b4a8358..86f29ad8af43d55f973fbcd02c1455510c97b4e4 100644
2019-11-19 05:18:28 +03:00
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -759,6 +759,13 @@ void nsDocLoader::DocLoaderIsEmpty(bool aFlushLayout) {
("DocLoader:%p: Firing load event for document.open\n",
this));
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ nsIPrincipal* principal = doc->NodePrincipal();
+ if (!principal->IsSystemPrincipal())
+ os->NotifyObservers(ToSupports(doc), "juggler-document-open-loaded", nullptr);
+ }
+
// This is a very cut-down version of
// nsDocumentViewer::LoadComplete that doesn't do various things
// that are not relevant here because this wasn't an actual
@@ -1366,6 +1373,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress,
2019-11-19 05:18:28 +03:00
}
}
+void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags) {
+ NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION,
+ nsCOMPtr<nsIWebProgressListener2> listener2 =
+ do_QueryReferent(info.mWeakListener);
+ if (!listener2)
+ continue;
+ listener2->OnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ );
+
+ // Pass the notification up to the parent...
+ if (mParent) {
+ mParent->FireOnFrameLocationChange(aWebProgress, aRequest, aUri, aFlags);
+ }
+}
+
void nsDocLoader::FireOnStatusChange(nsIWebProgress* aWebProgress,
nsIRequest* aRequest, nsresult aStatus,
const char16_t* aMessage) {
diff --git a/uriloader/base/nsDocLoader.h b/uriloader/base/nsDocLoader.h
index 1326a3254115193e1e7670896773b9bfe31d6931..b5134178f4b8332f2f867c66d0761f8685d8070a 100644
2019-11-19 05:18:28 +03:00
--- a/uriloader/base/nsDocLoader.h
+++ b/uriloader/base/nsDocLoader.h
@@ -204,6 +204,11 @@ class nsDocLoader : public nsIDocumentLoader,
2019-11-19 05:18:28 +03:00
void FireOnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest,
nsIURI* aUri, uint32_t aFlags);
+ void FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags);
+
MOZ_MUST_USE bool RefreshAttempted(nsIWebProgress* aWebProgress, nsIURI* aURI,
int32_t aDelay, bool aSameURI);
diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
index b0cde5026dc7c414e8f20300ac2b7d735dbd846e..09ebb0ef6799cf6a74fe529d4d000c6bed2c9497 100644
2019-11-19 05:18:28 +03:00
--- a/uriloader/base/nsIWebProgress.idl
+++ b/uriloader/base/nsIWebProgress.idl
@@ -87,6 +87,10 @@ interface nsIWebProgress : nsISupports
* NOTIFY_REFRESH
* Receive onRefreshAttempted events.
* This is defined on nsIWebProgressListener2.
+ *
+ * NOTIFY_FRAME_LOCATION
+ * Receive onFrameLocationChange events.
+ * This is defined on nsIWebProgressListener2.
*/
const unsigned long NOTIFY_PROGRESS = 0x00000010;
const unsigned long NOTIFY_STATUS = 0x00000020;
@@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports
const unsigned long NOTIFY_LOCATION = 0x00000080;
const unsigned long NOTIFY_REFRESH = 0x00000100;
const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200;
+ const unsigned long NOTIFY_FRAME_LOCATION = 0x00000400;
/**
* This flag enables all notifications.
*/
- const unsigned long NOTIFY_ALL = 0x000003ff;
+ const unsigned long NOTIFY_ALL = 0x000007ff;
/**
* Registers a listener to receive web progress events.
diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
index 87701f8d2cfee8bd84acd28c62b3be4989c9474c..ae1aa85c019cb21d4f7e79c35e8afe72709468a1 100644
2019-11-19 05:18:28 +03:00
--- a/uriloader/base/nsIWebProgressListener2.idl
+++ b/uriloader/base/nsIWebProgressListener2.idl
@@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener {
in nsIURI aRefreshURI,
in long aMillis,
in boolean aSameURI);
+
+ /**
+ * Called when the location of the window or its subframes changes. This is not
+ * when a load is requested, but rather once it is verified that the load is
+ * going to occur in the given window. For instance, a load that starts in a
+ * window might send progress and status messages for the new site, but it
+ * will not send the onLocationChange until we are sure that we are loading
+ * this new page here.
+ *
+ * @param aWebProgress
+ * The nsIWebProgress instance that fired the notification.
+ * @param aRequest
+ * The associated nsIRequest. This may be null in some cases.
+ * @param aLocation
+ * The URI of the location that is being loaded.
+ * @param aFlags
+ * This is a value which explains the situation or the reason why
+ * the location has changed.
+ */
+ void onFrameLocationChange(in nsIWebProgress aWebProgress,
+ in nsIRequest aRequest,
+ in nsIURI aLocation,
+ [optional] in unsigned long aFlags);
};