8233 lines
		
	
	
		
			286 KiB
		
	
	
	
		
			Diff
		
	
	
	
			
		
		
	
	
			8233 lines
		
	
	
		
			286 KiB
		
	
	
	
		
			Diff
		
	
	
	
| diff --git a/accessible/base/NotificationController.h b/accessible/base/NotificationController.h
 | |
| index f239731e0ff06cb01a4c0e8cf0ba4ff5014f88e0..53447ef12eb59bd065abbfd031bd6336a60020a2 100644
 | |
| --- a/accessible/base/NotificationController.h
 | |
| +++ b/accessible/base/NotificationController.h
 | |
| @@ -284,6 +284,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.
 | |
|     */
 | |
| diff --git a/browser/installer/allowed-dupes.mn b/browser/installer/allowed-dupes.mn
 | |
| index 1154516f0f452def338110bd7407ae144e916506..f85db060474ad705f86739d64c7f18fc1c48fd02 100644
 | |
| --- a/browser/installer/allowed-dupes.mn
 | |
| +++ b/browser/installer/allowed-dupes.mn
 | |
| @@ -63,6 +63,12 @@ browser/chrome/browser/res/payments/formautofill/autofillEditForms.js
 | |
|  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 e011b74cc4b4014362397137e92a1f27e29b43c7..c3ac23c2416363ce897741ca457fa9086b4cdd05 100644
 | |
| --- a/browser/installer/package-manifest.in
 | |
| +++ b/browser/installer/package-manifest.in
 | |
| @@ -211,6 +211,11 @@
 | |
|  @RESPATH@/components/marionette.js
 | |
|  #endif
 | |
|  
 | |
| +@RESPATH@/chrome/juggler@JAREXT@
 | |
| +@RESPATH@/chrome/juggler.manifest
 | |
| +@RESPATH@/components/juggler.manifest
 | |
| +@RESPATH@/components/juggler.js
 | |
| +
 | |
|  #if defined(ENABLE_TESTS) && defined(MOZ_DEBUG)
 | |
|  @RESPATH@/components/TestInterfaceJS.js
 | |
|  @RESPATH@/components/TestInterfaceJS.manifest
 | |
| diff --git a/devtools/server/socket/websocket-server.js b/devtools/server/socket/websocket-server.js
 | |
| index 040c7b124dec6bb254563bbe74fe50012cb077a3..b4e6b8132786af70e8ad0dce88b67c2835307f88 100644
 | |
| --- a/devtools/server/socket/websocket-server.js
 | |
| +++ b/devtools/server/socket/websocket-server.js
 | |
| @@ -133,13 +133,12 @@ function writeHttpResponse(output, response) {
 | |
|   * Process the WebSocket handshake headers and return the key to be sent in
 | |
|   * Sec-WebSocket-Accept response header.
 | |
|   */
 | |
| -function processRequest({ requestLine, headers }) {
 | |
| +function processRequest({ requestLine, headers }, expectedPath) {
 | |
|    const [method, path] = requestLine.split(" ");
 | |
|    if (method !== "GET") {
 | |
|      throw new Error("The handshake request must use GET method");
 | |
|    }
 | |
| -
 | |
| -  if (path !== "/") {
 | |
| +  if (path !== expectedPath) {
 | |
|      throw new Error("The handshake request has unknown path");
 | |
|    }
 | |
|  
 | |
| @@ -189,13 +188,13 @@ function computeKey(key) {
 | |
|  /**
 | |
|   * Perform the server part of a WebSocket opening handshake on an incoming connection.
 | |
|   */
 | |
| -const serverHandshake = async function(input, output) {
 | |
| +const serverHandshake = async function(input, output, expectedPath) {
 | |
|    // Read the request
 | |
|    const request = await readHttpRequest(input);
 | |
|  
 | |
|    try {
 | |
|      // Check and extract info from the request
 | |
| -    const { acceptKey } = processRequest(request);
 | |
| +    const { acceptKey } = processRequest(request, expectedPath);
 | |
|  
 | |
|      // Send response headers
 | |
|      await writeHttpResponse(output, [
 | |
| @@ -217,8 +216,8 @@ const serverHandshake = async function(input, output) {
 | |
|   * Performs the WebSocket handshake and waits for the WebSocket to open.
 | |
|   * Returns Promise with a WebSocket ready to send and receive messages.
 | |
|   */
 | |
| -const accept = async function(transport, input, output) {
 | |
| -  await serverHandshake(input, output);
 | |
| +const accept = async function(transport, input, output, expectedPath) {
 | |
| +  await serverHandshake(input, output, expectedPath || "/");
 | |
|  
 | |
|    const transportProvider = {
 | |
|      setListener(upgradeListener) {
 | |
| diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
 | |
| index 52b04ca19b0f8db30ffec0a1cf06ab2f6df0443e..c65ad4d019f7089cdcc168d018e0389e661d0bcc 100644
 | |
| --- a/docshell/base/nsDocShell.cpp
 | |
| +++ b/docshell/base/nsDocShell.cpp
 | |
| @@ -15,6 +15,12 @@
 | |
|  #  include <unistd.h>  // for getpid()
 | |
|  #endif
 | |
|  
 | |
| +#if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
 | |
| +#  include "unicode/locid.h"
 | |
| +#endif /* JS_HAS_INTL_API && !MOZ_SYSTEM_ICU */
 | |
| +
 | |
| +#include "js/LocaleSensitive.h"
 | |
| +
 | |
|  #include "mozilla/ArrayUtils.h"
 | |
|  #include "mozilla/Attributes.h"
 | |
|  #include "mozilla/AutoRestore.h"
 | |
| @@ -56,6 +62,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"
 | |
| @@ -73,6 +80,7 @@
 | |
|  #include "mozilla/dom/nsCSPContext.h"
 | |
|  #include "mozilla/dom/LoadURIOptionsBinding.h"
 | |
|  #include "mozilla/dom/JSWindowActorChild.h"
 | |
| +#include "mozilla/dom/WorkerCommon.h"
 | |
|  #include "nsSHEntry.h"
 | |
|  #include "mozilla/net/DocumentChannel.h"
 | |
|  #include "mozilla/net/UrlClassifierFeatureFactory.h"
 | |
| @@ -97,6 +105,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"
 | |
| @@ -185,6 +194,7 @@
 | |
|  #include "nsGlobalWindow.h"
 | |
|  #include "nsISearchService.h"
 | |
|  #include "nsJSEnvironment.h"
 | |
| +#include "nsJSUtils.h"
 | |
|  #include "nsNetCID.h"
 | |
|  #include "nsNetUtil.h"
 | |
|  #include "nsObjectLoadingContent.h"
 | |
| @@ -370,6 +380,11 @@ nsDocShell::nsDocShell(BrowsingContext* aBrowsingContext,
 | |
|        mAllowWindowControl(true),
 | |
|        mUseErrorPages(true),
 | |
|        mCSSErrorReportingEnabled(false),
 | |
| +      mFileInputInterceptionEnabled(false),
 | |
| +      mOverrideHasFocus(false),
 | |
| +      mBypassCSPEnabled(false),
 | |
| +      mOnlineOverride(nsIDocShell::ONLINE_OVERRIDE_NONE),
 | |
| +      mColorSchemeOverride(COLOR_SCHEME_OVERRIDE_NONE),
 | |
|        mAllowAuth(mItemType == typeContent),
 | |
|        mAllowKeywordFixup(false),
 | |
|        mIsOffScreenBrowser(false),
 | |
| @@ -1245,6 +1260,7 @@ bool nsDocShell::SetCurrentURI(nsIURI* aURI, nsIRequest* aRequest,
 | |
|      isSubFrame = mLSHE->GetIsSubFrame();
 | |
|    }
 | |
|  
 | |
| +  FireOnFrameLocationChange(this, aRequest, aURI, aLocationFlags);
 | |
|    if (!isSubFrame && !isRoot) {
 | |
|      /*
 | |
|       * We don't want to send OnLocationChange notifications when
 | |
| @@ -3125,6 +3141,184 @@ 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::GetOverrideHasFocus(bool* aEnabled) {
 | |
| +  MOZ_ASSERT(aEnabled);
 | |
| +  *aEnabled = mOverrideHasFocus;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::SetOverrideHasFocus(bool aEnabled) {
 | |
| +  mOverrideHasFocus = aEnabled;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +bool nsDocShell::ShouldOverrideHasFocus() const {
 | |
| +  return mOverrideHasFocus;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::GetLanguageOverride(nsAString& aLanguageOverride) {
 | |
| +  MOZ_ASSERT(aEnabled);
 | |
| +  aLanguageOverride = GetRootDocShell()->mLanguageOverride;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +
 | |
| +static void SetIcuLocale(const nsAString& aLanguageOverride) {
 | |
| +  icu::Locale locale(NS_LossyConvertUTF16toASCII(aLanguageOverride).get());
 | |
| +  if (icu::Locale::getDefault() == locale)
 | |
| +    return;
 | |
| +  UErrorCode error_code = U_ZERO_ERROR;
 | |
| +  const char* lang = locale.getLanguage();
 | |
| +  if (lang != nullptr && *lang != '\0') {
 | |
| +    icu::Locale::setDefault(locale, error_code);
 | |
| +  } else {
 | |
| +    fprintf(stderr, "SetIcuLocale Failed to set the ICU default locale to %s\n", NS_LossyConvertUTF16toASCII(aLanguageOverride).get());
 | |
| +  }
 | |
| +
 | |
| +  AutoJSAPI jsapi;
 | |
| +  jsapi.Init();
 | |
| +  JSContext* cx = jsapi.cx();
 | |
| +  JS_ResetDefaultLocale(JS_GetRuntime(cx));
 | |
| +
 | |
| +  ResetDefaultLocaleInAllWorkers();
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::SetLanguageOverride(const nsAString& aLanguageOverride) {
 | |
| +  mLanguageOverride = aLanguageOverride;
 | |
| +  SetIcuLocale(aLanguageOverride);
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::OverrideTimezone(const nsAString& aTimezoneOverride,
 | |
| +                             bool* aSuccess) {
 | |
| +  NS_ENSURE_ARG(aSuccess);
 | |
| +  NS_LossyConvertUTF16toASCII timeZoneId(aTimezoneOverride);
 | |
| +  *aSuccess = nsJSUtils::SetTimeZoneOverride(timeZoneId.get());
 | |
| +
 | |
| +  // Set TZ which affects localtime_s().
 | |
| +  auto setTimeZoneEnv = [](const char* value) {
 | |
| +#if defined(_WIN32)
 | |
| +    return _putenv_s("TZ", value) == 0;
 | |
| +#else
 | |
| +    return setenv("TZ", value, true) == 0;
 | |
| +#endif /* _WIN32 */
 | |
| +  };
 | |
| +  if (*aSuccess) {
 | |
| +    *aSuccess = setTimeZoneEnv(timeZoneId.get());
 | |
| +    if (!*aSuccess) {
 | |
| +      fprintf(stderr, "Failed to set 'TZ' to '%s'\n", timeZoneId.get());
 | |
| +    }
 | |
| +  }
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::GetFileInputInterceptionEnabled(bool* aEnabled) {
 | |
| +  MOZ_ASSERT(aEnabled);
 | |
| +  *aEnabled = GetRootDocShell()->mFileInputInterceptionEnabled;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::SetFileInputInterceptionEnabled(bool aEnabled) {
 | |
| +  mFileInputInterceptionEnabled = aEnabled;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +bool nsDocShell::IsFileInputInterceptionEnabled() {
 | |
| +  return GetRootDocShell()->mFileInputInterceptionEnabled;
 | |
| +}
 | |
| +
 | |
| +void nsDocShell::FilePickerShown(mozilla::dom::Element* element) {
 | |
| +  nsCOMPtr<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;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::GetColorSchemeOverride(ColorSchemeOverride* aColorSchemeOverride) {
 | |
| +  *aColorSchemeOverride = GetRootDocShell()->mColorSchemeOverride;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsDocShell::SetColorSchemeOverride(ColorSchemeOverride aColorSchemeOverride) {
 | |
| +  mColorSchemeOverride = aColorSchemeOverride;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +// =============== Juggler End =======================
 | |
| +
 | |
|  NS_IMETHODIMP
 | |
|  nsDocShell::GetIsNavigating(bool* aOut) {
 | |
|    *aOut = mIsNavigating;
 | |
| @@ -8236,6 +8430,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState,
 | |
|                       true,  // aForceNoOpener
 | |
|                       getter_AddRefs(newBC));
 | |
|        MOZ_ASSERT(!newBC);
 | |
| +      if (rv == NS_OK) {
 | |
| +        nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
 | |
| +        if (observerService) {
 | |
| +          observerService->NotifyObservers(GetAsSupports(this), "juggler-window-open-in-new-context", nullptr);
 | |
| +        }
 | |
| +      }
 | |
|        return rv;
 | |
|      }
 | |
|  
 | |
| @@ -11777,6 +11977,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;
 | |
|    }
 | |
|  
 | |
| @@ -11866,6 +12069,8 @@ 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 Dispatch(TaskCategory::UI, ev.forget());
 | |
|  }
 | |
|  
 | |
| diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
 | |
| index 9343cac02675aeec3a6491b53fd979b2d9ae6e0f..31355beae06e3c04f7cefe524e9e02e9932e8f14 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"
 | |
| @@ -476,6 +478,15 @@ class nsDocShell final : public nsDocLoader,
 | |
|  
 | |
|    void SetWillChangeProcess() { mWillChangeProcess = true; }
 | |
|  
 | |
| +  bool IsFileInputInterceptionEnabled();
 | |
| +  void FilePickerShown(mozilla::dom::Element* element);
 | |
| +
 | |
| +  bool ShouldOverrideHasFocus() const;
 | |
| +
 | |
| +  bool IsBypassCSPEnabled();
 | |
| +
 | |
| +  RefPtr<nsGeolocationService> GetGeolocationServiceOverride();
 | |
| +
 | |
|    // Create a content viewer within this nsDocShell for the given
 | |
|    // `WindowGlobalChild` actor.
 | |
|    nsresult CreateContentViewerForActor(
 | |
| @@ -1018,6 +1029,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
 | |
| @@ -1258,6 +1271,14 @@ class nsDocShell final : public nsDocLoader,
 | |
|    bool mAllowWindowControl : 1;
 | |
|    bool mUseErrorPages : 1;
 | |
|    bool mCSSErrorReportingEnabled : 1;
 | |
| +  bool mFileInputInterceptionEnabled: 1;
 | |
| +  bool mOverrideHasFocus : 1;
 | |
| +  bool mBypassCSPEnabled : 1;
 | |
| +  nsString mLanguageOverride;
 | |
| +  RefPtr<nsGeolocationService> mGeolocationServiceOverride;
 | |
| +  OnlineOverride mOnlineOverride;
 | |
| +  ColorSchemeOverride mColorSchemeOverride;
 | |
| +
 | |
|    bool mAllowAuth : 1;
 | |
|    bool mAllowKeywordFixup : 1;
 | |
|    bool mIsOffScreenBrowser : 1;
 | |
| diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl
 | |
| index 8a8159ad55d530f70de4a86b39793459cca2ccd9..3d9bd79a5d0c38adc8d9fa3135fc270253b6f586 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;
 | |
| @@ -1073,6 +1074,33 @@ interface nsIDocShell : nsIDocShellTreeItem
 | |
|     */
 | |
|    void synchronizeLayoutHistoryState();
 | |
|  
 | |
| +  attribute boolean fileInputInterceptionEnabled;
 | |
| +
 | |
| +  attribute boolean overrideHasFocus;
 | |
| +
 | |
| +  attribute boolean bypassCSPEnabled;
 | |
| +
 | |
| +  attribute AString languageOverride;
 | |
| +
 | |
| +  boolean overrideTimezone(in AString timezoneId);
 | |
| +
 | |
| +  cenum OnlineOverride: 8 {
 | |
| +    ONLINE_OVERRIDE_NONE = 0,
 | |
| +    ONLINE_OVERRIDE_ONLINE = 1,
 | |
| +    ONLINE_OVERRIDE_OFFLINE = 2,
 | |
| +  };
 | |
| +  [infallible] attribute nsIDocShell_OnlineOverride onlineOverride;
 | |
| +
 | |
| +  cenum ColorSchemeOverride : 8 {
 | |
| +    COLOR_SCHEME_OVERRIDE_LIGHT,
 | |
| +    COLOR_SCHEME_OVERRIDE_DARK,
 | |
| +    COLOR_SCHEME_OVERRIDE_NO_PREFERENCE,
 | |
| +    COLOR_SCHEME_OVERRIDE_NONE, /* This clears the override. */
 | |
| +  };
 | |
| +  [infallible] attribute nsIDocShell_ColorSchemeOverride colorSchemeOverride;
 | |
| +
 | |
| +  void setGeolocationOverride(in nsIDOMGeoPosition position);
 | |
| +
 | |
|    /**
 | |
|     * This attempts to save any applicable layout history state (like
 | |
|     * scroll position) in the nsISHEntry. This is normally done
 | |
| diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
 | |
| index d920acae67811acedf7d087e485a5c2c3cd01454..621a4109c7f2b1905274cfa0499b4bae129ae0d6 100644
 | |
| --- a/dom/base/Document.cpp
 | |
| +++ b/dom/base/Document.cpp
 | |
| @@ -3232,6 +3232,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
 | |
| @@ -3286,6 +3289,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;
 | |
| @@ -4024,6 +4032,10 @@ bool Document::HasFocus(ErrorResult& rv) const {
 | |
|      return false;
 | |
|    }
 | |
|  
 | |
| +  if (IsActive() && mDocumentContainer->ShouldOverrideHasFocus()) {
 | |
| +    return true;
 | |
| +  }
 | |
| +
 | |
|    // Is there a focused DOMWindow?
 | |
|    nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
 | |
|    fm->GetFocusedWindow(getter_AddRefs(focusedWindow));
 | |
| @@ -16292,6 +16304,20 @@ void Document::RemoveToplevelLoadingDocument(Document* aDoc) {
 | |
|  }
 | |
|  
 | |
|  StylePrefersColorScheme Document::PrefersColorScheme() const {
 | |
| +  auto* docShell = static_cast<nsDocShell*>(GetDocShell());
 | |
| +  nsIDocShell::ColorSchemeOverride colorScheme;
 | |
| +  if (docShell->GetColorSchemeOverride(&colorScheme) == NS_OK &&
 | |
| +      colorScheme != nsIDocShell::COLOR_SCHEME_OVERRIDE_NONE) {
 | |
| +    switch (colorScheme) {
 | |
| +      case nsIDocShell::COLOR_SCHEME_OVERRIDE_LIGHT:
 | |
| +        return StylePrefersColorScheme::Light;
 | |
| +      case nsIDocShell::COLOR_SCHEME_OVERRIDE_DARK:
 | |
| +        return StylePrefersColorScheme::Dark;
 | |
| +      case nsIDocShell::COLOR_SCHEME_OVERRIDE_NO_PREFERENCE:
 | |
| +        return StylePrefersColorScheme::NoPreference;
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
|    if (nsContentUtils::ShouldResistFingerprinting(this)) {
 | |
|      return StylePrefersColorScheme::Light;
 | |
|    }
 | |
| diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp
 | |
| index e49bbceefb219c55e9643a7cf82dc42b605b7988..ad31c040498f8f11cdf07d38fb14630dd92a4f8d 100644
 | |
| --- a/dom/base/Navigator.cpp
 | |
| +++ b/dom/base/Navigator.cpp
 | |
| @@ -326,14 +326,18 @@ void Navigator::GetAppName(nsAString& aAppName, CallerType aCallerType) const {
 | |
|   * for more detail.
 | |
|   */
 | |
|  /* static */
 | |
| -void Navigator::GetAcceptLanguages(nsTArray<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, ',');
 | |
| @@ -389,7 +393,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 listens to the
 | |
|    // accept languages change and will clear the cache when needed. It has to
 | |
| @@ -540,7 +546,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 e268e2bbe8add1b43f6e4d6507cc7810d707a344..a34a7a292a02ea8d94042475a43ae3a05710c207 100644
 | |
| --- a/dom/base/Navigator.h
 | |
| +++ b/dom/base/Navigator.h
 | |
| @@ -216,7 +216,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/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
 | |
| index b3ab072d7826fd00637a1ea5db605a98bae7d615..96cb32e3235950610f1cde4765ca14592b03f077 100644
 | |
| --- a/dom/base/nsGlobalWindowOuter.cpp
 | |
| +++ b/dom/base/nsGlobalWindowOuter.cpp
 | |
| @@ -3924,6 +3924,14 @@ Maybe<CSSIntSize> nsGlobalWindowOuter::GetRDMDeviceSize(
 | |
|        }
 | |
|      }
 | |
|    }
 | |
| +  if (topInProcessContentDoc) {
 | |
| +    nsIDocShell* docShell = topInProcessContentDoc->GetDocShell();
 | |
| +    if (docShell && docShell->GetDeviceSizeIsPageSize()) {
 | |
| +      nsPresContext* presContext = docShell->GetPresContext();
 | |
| +      if (presContext)
 | |
| +        return Some(CSSPixel::FromAppUnitsRounded(presContext->GetVisibleArea().Size()));
 | |
| +    }
 | |
| +  }
 | |
|    return Nothing();
 | |
|  }
 | |
|  
 | |
| diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp
 | |
| index ddd7d6a76bf0d974090b60f0a79929168ec57fac..195787295fd03d034a2d5110d58cd20856787845 100644
 | |
| --- a/dom/base/nsINode.cpp
 | |
| +++ b/dom/base/nsINode.cpp
 | |
| @@ -1260,6 +1260,48 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions,
 | |
|    mozilla::GetBoxQuadsFromWindowOrigin(this, aOptions, aResult, aRv);
 | |
|  }
 | |
|  
 | |
| +void nsINode::ScrollRectIntoViewIfNeeded(int32_t x, int32_t y,
 | |
| +                                         int32_t w, int32_t h,
 | |
| +                                         ErrorResult& aRv) {
 | |
| +  aRv = NS_ERROR_UNEXPECTED;
 | |
| +  nsCOMPtr<Document> document = OwnerDoc();
 | |
| +  if (!document) {
 | |
| +    return;
 | |
| +  }
 | |
| +  PresShell* presShell = document->GetPresShell();
 | |
| +  if (!presShell) {
 | |
| +    return;
 | |
| +  }
 | |
| +  if (!IsContent()) {
 | |
| +    return;
 | |
| +  }
 | |
| +  aRv = NS_OK;
 | |
| +  nsIFrame* primaryFrame = AsContent()->GetPrimaryFrame(FlushType::Frames);
 | |
| +  if (!primaryFrame){
 | |
| +    return;
 | |
| +  }
 | |
| +  nsRect rect;
 | |
| +  if (x == -1 && y == -1 && w == -1 && h == -1) {
 | |
| +    rect = primaryFrame->GetRectRelativeToSelf();
 | |
| +  } else {
 | |
| +    rect = nsRect(nsPresContext::CSSPixelsToAppUnits(x),
 | |
| +                  nsPresContext::CSSPixelsToAppUnits(y),
 | |
| +                  nsPresContext::CSSPixelsToAppUnits(w),
 | |
| +                  nsPresContext::CSSPixelsToAppUnits(h));
 | |
| +  }
 | |
| +  presShell->ScrollFrameRectIntoView(
 | |
| +      primaryFrame, rect,
 | |
| +      ScrollAxis(kScrollToCenter, WhenToScroll::Always),
 | |
| +      ScrollAxis(kScrollToCenter, WhenToScroll::Always),
 | |
| +      ScrollFlags::ScrollOverflowHidden);
 | |
| +  // If a _visual_ scroll update is pending, cancel it; otherwise, it will
 | |
| +  // clobber next scroll (e.g. subsequent window.scrollTo(0, 0) wlll break).
 | |
| +  if (presShell->GetPendingVisualScrollUpdate()) {
 | |
| +    presShell->AcknowledgePendingVisualScrollUpdate();
 | |
| +    presShell->ClearPendingVisualScrollUpdate();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
|  already_AddRefed<DOMQuad> nsINode::ConvertQuadFromNode(
 | |
|      DOMQuad& aQuad, const GeometryNode& aFrom,
 | |
|      const ConvertCoordinateOptions& aOptions, CallerType aCallerType,
 | |
| diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h
 | |
| index f97908bcc7d4d30b7b8135078b37843d7394d308..2c61dedfcae19d09f85e8ce81a73f408e0572ce8 100644
 | |
| --- a/dom/base/nsINode.h
 | |
| +++ b/dom/base/nsINode.h
 | |
| @@ -2014,6 +2014,10 @@ class nsINode : public mozilla::dom::EventTarget {
 | |
|                                     nsTArray<RefPtr<DOMQuad>>& aResult,
 | |
|                                     ErrorResult& aRv);
 | |
|  
 | |
| +  void ScrollRectIntoViewIfNeeded(int32_t x, int32_t y,
 | |
| +                                  int32_t w, int32_t h,
 | |
| +                                  ErrorResult& aRv);
 | |
| +
 | |
|    already_AddRefed<DOMQuad> ConvertQuadFromNode(
 | |
|        DOMQuad& aQuad, const TextOrElementOrDocument& aFrom,
 | |
|        const ConvertCoordinateOptions& aOptions, CallerType aCallerType,
 | |
| diff --git a/dom/base/nsJSUtils.cpp b/dom/base/nsJSUtils.cpp
 | |
| index cb2dfa0cb0b83c3c53257c20ee9e9af9ecba15d3..f8f8459281769a7bc1b3118ace1be27c5353d4f5 100644
 | |
| --- a/dom/base/nsJSUtils.cpp
 | |
| +++ b/dom/base/nsJSUtils.cpp
 | |
| @@ -585,6 +585,11 @@ bool nsJSUtils::GetScopeChainForElement(
 | |
|    return true;
 | |
|  }
 | |
|  
 | |
| +/* static */
 | |
| +bool nsJSUtils::SetTimeZoneOverride(const char* timezoneId) {
 | |
| +  return JS::SetTimeZoneOverride(timezoneId);
 | |
| +}
 | |
| +
 | |
|  /* static */
 | |
|  void nsJSUtils::ResetTimeZone() { JS::ResetTimeZone(); }
 | |
|  
 | |
| diff --git a/dom/base/nsJSUtils.h b/dom/base/nsJSUtils.h
 | |
| index 2b654b490e53d0e258bcd4edb0470cb2e7cc9452..462e60d7fa84fb3bd09f7009c0a0cb3783caf651 100644
 | |
| --- a/dom/base/nsJSUtils.h
 | |
| +++ b/dom/base/nsJSUtils.h
 | |
| @@ -241,6 +241,7 @@ class nsJSUtils {
 | |
|        JSContext* aCx, mozilla::dom::Element* aElement,
 | |
|        JS::MutableHandleVector<JSObject*> aScopeChain);
 | |
|  
 | |
| +  static bool SetTimeZoneOverride(const char* timezoneId);
 | |
|    static void ResetTimeZone();
 | |
|  
 | |
|    static bool DumpEnabled();
 | |
| diff --git a/dom/geolocation/Geolocation.cpp b/dom/geolocation/Geolocation.cpp
 | |
| index 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 036577742e32c513590f8e2980544deffb254483..8bf5b8644dc9db2ecbefc0c1310c63316c93d383 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"
 | |
| @@ -706,6 +707,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;
 | |
|    }
 | |
| diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
 | |
| index b0f10a8873507b580348ee5f161b9e7e939cb059..2d4d300d61fba40473a358f973658fe0dece870f 100644
 | |
| --- a/dom/ipc/BrowserChild.cpp
 | |
| +++ b/dom/ipc/BrowserChild.cpp
 | |
| @@ -3581,6 +3581,13 @@ NS_IMETHODIMP BrowserChild::OnStateChange(nsIWebProgress* aWebProgress,
 | |
|    return NS_OK;
 | |
|  }
 | |
|  
 | |
| +NS_IMETHODIMP BrowserChild::OnFrameLocationChange(nsIWebProgress *aWebProgress,
 | |
| +                                             nsIRequest *aRequest,
 | |
| +                                             nsIURI *aLocation,
 | |
| +                                             uint32_t aFlags) {
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
|  NS_IMETHODIMP BrowserChild::OnProgressChange(nsIWebProgress* aWebProgress,
 | |
|                                               nsIRequest* aRequest,
 | |
|                                               int32_t aCurSelfProgress,
 | |
| diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp
 | |
| index e4c33aed753aab79d7e9d29abdce0a4d71d51466..d8fc1e7563dffa3fbe1f5281279774907549b311 100644
 | |
| --- a/dom/script/ScriptSettings.cpp
 | |
| +++ b/dom/script/ScriptSettings.cpp
 | |
| @@ -141,6 +141,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.
 | |
| @@ -168,7 +192,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 6908be00b3087df175508270f2097e0d908c370b..7b999d2f26454fb3cab50060cf1823566579a116 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/webidl/GeometryUtils.webidl b/dom/webidl/GeometryUtils.webidl
 | |
| index 2f71b284ee5f7e11f117c447834b48355784448c..d996e0a3cbbb19c1dc320c305c6d74037bffa0d3 100644
 | |
| --- a/dom/webidl/GeometryUtils.webidl
 | |
| +++ b/dom/webidl/GeometryUtils.webidl
 | |
| @@ -27,6 +27,9 @@ interface mixin GeometryUtils {
 | |
|    [Throws, Func="nsINode::HasBoxQuadsSupport", NeedsCallerType]
 | |
|    sequence<DOMQuad> getBoxQuads(optional BoxQuadOptions options = {});
 | |
|  
 | |
| +  [ChromeOnly, Throws, Func="nsINode::HasBoxQuadsSupport"]
 | |
| +  void scrollRectIntoViewIfNeeded(long x, long y, long w, long h);
 | |
| +
 | |
|    /* getBoxQuadsFromWindowOrigin is similar to getBoxQuads, but the
 | |
|     * returned quads are further translated relative to the window
 | |
|     * origin -- which is not the layout origin. Further translation
 | |
| diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp
 | |
| index a20debb462588c36e53971318bbfe9f45b8c7bf3..63d90418ac52fb52a86875a40d173428b0392f7d 100644
 | |
| --- a/dom/workers/RuntimeService.cpp
 | |
| +++ b/dom/workers/RuntimeService.cpp
 | |
| @@ -1014,7 +1014,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) {
 | |
| @@ -1213,8 +1213,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;
 | |
|      }
 | |
|  
 | |
| @@ -1960,6 +1959,11 @@ void RuntimeService::PropagateFirstPartyStorageAccessGranted(
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void RuntimeService::ResetDefaultLocaleInAllWorkers() {
 | |
| +  AssertIsOnMainThread();
 | |
| +  BROADCAST_ALL_WORKERS(ResetDefaultLocale);
 | |
| +}
 | |
| +
 | |
|  void RuntimeService::NoteIdleThread(WorkerThread* aThread) {
 | |
|    AssertIsOnMainThread();
 | |
|    MOZ_ASSERT(aThread);
 | |
| @@ -2377,6 +2381,14 @@ void PropagateFirstPartyStorageAccessGrantedToWorkers(
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void ResetDefaultLocaleInAllWorkers() {
 | |
| +  AssertIsOnMainThread();
 | |
| +  RuntimeService* runtime = RuntimeService::GetService();
 | |
| +  if (runtime) {
 | |
| +    runtime->ResetDefaultLocaleInAllWorkers();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
|  WorkerPrivate* GetWorkerPrivateFromContext(JSContext* aCx) {
 | |
|    MOZ_ASSERT(!NS_IsMainThread());
 | |
|    MOZ_ASSERT(aCx);
 | |
| diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h
 | |
| index ee237dcc59f056c31e3d16c5bb0f00a70efb3649..299575f4d72742d256f51077d12409cfc7d61572 100644
 | |
| --- a/dom/workers/RuntimeService.h
 | |
| +++ b/dom/workers/RuntimeService.h
 | |
| @@ -117,6 +117,8 @@ class RuntimeService final : public nsIObserver {
 | |
|  
 | |
|    void PropagateFirstPartyStorageAccessGranted(nsPIDOMWindowInner* aWindow);
 | |
|  
 | |
| +  void ResetDefaultLocaleInAllWorkers();
 | |
| +
 | |
|    const NavigatorProperties& GetNavigatorProperties() const {
 | |
|      return mNavigatorProperties;
 | |
|    }
 | |
| diff --git a/dom/workers/WorkerCommon.h b/dom/workers/WorkerCommon.h
 | |
| index f5e5c232d424e25607fb2fcf089c747708e02104..ada9c56f9d31d8d1c7c4c918403f14279358a4a8 100644
 | |
| --- a/dom/workers/WorkerCommon.h
 | |
| +++ b/dom/workers/WorkerCommon.h
 | |
| @@ -47,6 +47,8 @@ void ResumeWorkersForWindow(nsPIDOMWindowInner* aWindow);
 | |
|  void PropagateFirstPartyStorageAccessGrantedToWorkers(
 | |
|      nsPIDOMWindowInner* aWindow);
 | |
|  
 | |
| +void ResetDefaultLocaleInAllWorkers();
 | |
| +
 | |
|  // All of these are implemented in WorkerScope.cpp
 | |
|  
 | |
|  bool IsWorkerGlobal(JSObject* global);
 | |
| diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp
 | |
| index 2b071cc1d60eb8aa76e9a2c88635ffddcd85a589..cf339e8fbf63ad158754c27443e3d3c9f937cf7e 100644
 | |
| --- a/dom/workers/WorkerPrivate.cpp
 | |
| +++ b/dom/workers/WorkerPrivate.cpp
 | |
| @@ -649,6 +649,18 @@ class UpdateContextOptionsRunnable final : public WorkerControlRunnable {
 | |
|    }
 | |
|  };
 | |
|  
 | |
| +class ResetDefaultLocaleRunnable final : public WorkerControlRunnable {
 | |
| + public:
 | |
| +  explicit ResetDefaultLocaleRunnable(WorkerPrivate* aWorkerPrivate)
 | |
| +      : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) {}
 | |
| +
 | |
| +  virtual bool WorkerRun(JSContext* aCx,
 | |
| +                         WorkerPrivate* aWorkerPrivate) override {
 | |
| +    aWorkerPrivate->ResetDefaultLocaleInternal(aCx);
 | |
| +    return true;
 | |
| +  }
 | |
| +};
 | |
| +
 | |
|  class UpdateLanguagesRunnable final : public WorkerRunnable {
 | |
|    nsTArray<nsString> mLanguages;
 | |
|  
 | |
| @@ -1828,6 +1840,16 @@ void WorkerPrivate::UpdateContextOptions(
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void WorkerPrivate::ResetDefaultLocale() {
 | |
| +  AssertIsOnParentThread();
 | |
| +
 | |
| +  RefPtr<ResetDefaultLocaleRunnable> runnable =
 | |
| +      new ResetDefaultLocaleRunnable(this);
 | |
| +  if (!runnable->Dispatch()) {
 | |
| +    NS_WARNING("Failed to reset default locale in worker!");
 | |
| +  }
 | |
| +}
 | |
| +
 | |
|  void WorkerPrivate::UpdateLanguages(const nsTArray<nsString>& aLanguages) {
 | |
|    AssertIsOnParentThread();
 | |
|  
 | |
| @@ -4737,6 +4759,15 @@ void WorkerPrivate::UpdateContextOptionsInternal(
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void WorkerPrivate::ResetDefaultLocaleInternal(JSContext* aCx) {
 | |
| +  JS_ResetDefaultLocale(JS_GetRuntime(aCx));
 | |
| +
 | |
| +  MOZ_ACCESS_THREAD_BOUND(mWorkerThreadAccessible, data);
 | |
| +  for (uint32_t index = 0; index < data->mChildWorkers.Length(); index++) {
 | |
| +    data->mChildWorkers[index]->ResetDefaultLocale();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
|  void WorkerPrivate::UpdateLanguagesInternal(
 | |
|      const nsTArray<nsString>& aLanguages) {
 | |
|    WorkerGlobalScope* globalScope = GlobalScope();
 | |
| diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h
 | |
| index 35a2fbeff49e3e2b54854fc43b208c0a92f182d2..632d34ec768ebdf8ccbfc5f62187dc0d86fa5124 100644
 | |
| --- a/dom/workers/WorkerPrivate.h
 | |
| +++ b/dom/workers/WorkerPrivate.h
 | |
| @@ -281,6 +281,8 @@ class WorkerPrivate : public RelativeTimeline {
 | |
|    void UpdateContextOptionsInternal(JSContext* aCx,
 | |
|                                      const JS::ContextOptions& aContextOptions);
 | |
|  
 | |
| +  void ResetDefaultLocaleInternal(JSContext* aCx);
 | |
| +
 | |
|    void UpdateLanguagesInternal(const nsTArray<nsString>& aLanguages);
 | |
|  
 | |
|    void UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key,
 | |
| @@ -859,6 +861,8 @@ class WorkerPrivate : public RelativeTimeline {
 | |
|  
 | |
|    void UpdateContextOptions(const JS::ContextOptions& aContextOptions);
 | |
|  
 | |
| +  void ResetDefaultLocale();
 | |
| +
 | |
|    void UpdateLanguages(const nsTArray<nsString>& aLanguages);
 | |
|  
 | |
|    void UpdateJSWorkerMemoryParameter(JSGCParamKey key, Maybe<uint32_t> value);
 | |
| diff --git a/extensions/permissions/Permission.cpp b/extensions/permissions/Permission.cpp
 | |
| index 72ed1de82fd322ba2cafffbad5622e2fdb6aa677..0dd907ff05d1df313dd3b8f8561c4ab3296e2931 100644
 | |
| --- a/extensions/permissions/Permission.cpp
 | |
| +++ b/extensions/permissions/Permission.cpp
 | |
| @@ -34,7 +34,7 @@ already_AddRefed<nsIPrincipal> Permission::ClonePrincipalForPermission(
 | |
|  
 | |
|    mozilla::OriginAttributes attrs = aPrincipal->OriginAttributesRef();
 | |
|    if (!StaticPrefs::permissions_isolateBy_userContext()) {
 | |
| -    attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 | |
| +    // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 | |
|    }
 | |
|  
 | |
|    nsAutoCString originNoSuffix;
 | |
| diff --git a/extensions/permissions/PermissionManager.cpp b/extensions/permissions/PermissionManager.cpp
 | |
| index aee1bf97dc55c66287dddfe449e495a1783dbcb4..153a2ec3e2177051283bfb008a2b94cf80018d03 100644
 | |
| --- a/extensions/permissions/PermissionManager.cpp
 | |
| +++ b/extensions/permissions/PermissionManager.cpp
 | |
| @@ -195,7 +195,7 @@ void MaybeStripOAs(bool aForceStrip, OriginAttributes& aOriginAttributes) {
 | |
|    }
 | |
|  
 | |
|    if (flags != 0) {
 | |
| -    aOriginAttributes.StripAttributes(flags);
 | |
| +    // aOriginAttributes.StripAttributes(flags);
 | |
|    }
 | |
|  }
 | |
|  
 | |
| @@ -228,6 +228,8 @@ nsresult GetOriginFromPrincipal(nsIPrincipal* aPrincipal, bool aForceStripOA,
 | |
|  
 | |
|    OriginAppendOASuffix(attrs, aForceStripOA, aOrigin);
 | |
|  
 | |
| +  // Disable userContext for permissions.
 | |
| +  // attrs.StripAttributes(mozilla::OriginAttributes::STRIP_USER_CONTEXT_ID);
 | |
|    return NS_OK;
 | |
|  }
 | |
|  
 | |
| diff --git a/image/DecoderFactory.cpp b/image/DecoderFactory.cpp
 | |
| index 2e444341783a09da055f33a44f51f2718609a04c..7e992310d96295950adff58b98f116a875515fb6 100644
 | |
| --- a/image/DecoderFactory.cpp
 | |
| +++ b/image/DecoderFactory.cpp
 | |
| @@ -20,7 +20,9 @@
 | |
|  #include "nsICODecoder.h"
 | |
|  #include "nsIconDecoder.h"
 | |
|  #include "nsWebPDecoder.h"
 | |
| +#ifdef MOZ_AV1
 | |
|  #include "nsAVIFDecoder.h"
 | |
| +#endif
 | |
|  
 | |
|  namespace mozilla {
 | |
|  
 | |
| @@ -79,9 +81,11 @@ DecoderType DecoderFactory::GetDecoderType(const char* aMimeType) {
 | |
|      type = DecoderType::WEBP;
 | |
|  
 | |
|      // AVIF
 | |
| +#ifdef MOZ_AV1
 | |
|    } else if (!strcmp(aMimeType, IMAGE_AVIF) &&
 | |
|               StaticPrefs::image_avif_enabled()) {
 | |
|      type = DecoderType::AVIF;
 | |
| +#endif
 | |
|    }
 | |
|  
 | |
|    return type;
 | |
| @@ -121,9 +125,11 @@ already_AddRefed<Decoder> DecoderFactory::GetDecoder(DecoderType aType,
 | |
|      case DecoderType::WEBP:
 | |
|        decoder = new nsWebPDecoder(aImage);
 | |
|        break;
 | |
| +#ifdef MOZ_AV1
 | |
|      case DecoderType::AVIF:
 | |
|        decoder = new nsAVIFDecoder(aImage);
 | |
|        break;
 | |
| +#endif
 | |
|      default:
 | |
|        MOZ_ASSERT_UNREACHABLE("Unknown decoder type");
 | |
|    }
 | |
| diff --git a/image/decoders/moz.build b/image/decoders/moz.build
 | |
| index a14e571b6cc778dc520f7fa0894cb01b0542aaa9..a974396aab043086b083aa17e3b7f9677415391c 100644
 | |
| --- a/image/decoders/moz.build
 | |
| +++ b/image/decoders/moz.build
 | |
| @@ -22,7 +22,6 @@ elif toolkit == 'android':
 | |
|  UNIFIED_SOURCES += [
 | |
|      'EXIF.cpp',
 | |
|      'iccjpeg.c',
 | |
| -    'nsAVIFDecoder.cpp',
 | |
|      'nsBMPDecoder.cpp',
 | |
|      'nsGIFDecoder2.cpp',
 | |
|      'nsICODecoder.cpp',
 | |
| @@ -32,6 +31,11 @@ UNIFIED_SOURCES += [
 | |
|      'nsWebPDecoder.cpp',
 | |
|  ]
 | |
|  
 | |
| +if CONFIG['MOZ_AV1']:
 | |
| +    UNIFIED_SOURCES += [
 | |
| +        'nsAVIFDecoder.cpp',
 | |
| +    ]
 | |
| +
 | |
|  include('/ipc/chromium/chromium-config.mozbuild')
 | |
|  
 | |
|  LOCAL_INCLUDES += [
 | |
| diff --git a/js/public/Date.h b/js/public/Date.h
 | |
| index e7a54d86c44499a3ec2adf1c156b9f9dfb0bc6b4..f56c1b419c4cb52bc371f6b8dbfffba464326fc4 100644
 | |
| --- a/js/public/Date.h
 | |
| +++ b/js/public/Date.h
 | |
| @@ -56,6 +56,8 @@ namespace JS {
 | |
|   */
 | |
|  extern JS_PUBLIC_API void ResetTimeZone();
 | |
|  
 | |
| +extern JS_PUBLIC_API bool SetTimeZoneOverride(const char* timezoneId);
 | |
| +
 | |
|  class ClippedTime;
 | |
|  inline ClippedTime TimeClip(double time);
 | |
|  
 | |
| diff --git a/js/src/debugger/Object.cpp b/js/src/debugger/Object.cpp
 | |
| index add42a1c2da3fca6a4469435498a4c6d824d8f47..72a7f8eff3302586d6567287380ec5de9c1c5165 100644
 | |
| --- a/js/src/debugger/Object.cpp
 | |
| +++ b/js/src/debugger/Object.cpp
 | |
| @@ -2385,7 +2385,11 @@ Maybe<Completion> DebuggerObject::call(JSContext* cx,
 | |
|          invokeArgs[i].set(args2[i]);
 | |
|        }
 | |
|  
 | |
| +      // Disable CSP for the scope of the call.
 | |
| +      const JSSecurityCallbacks* securityCallbacks = JS_GetSecurityCallbacks(cx);
 | |
| +      JS_SetSecurityCallbacks(cx, nullptr);
 | |
|        ok = js::Call(cx, calleev, thisv, invokeArgs, &result);
 | |
| +      JS_SetSecurityCallbacks(cx, securityCallbacks);
 | |
|      }
 | |
|    }
 | |
|  
 | |
| diff --git a/js/src/vm/DateTime.cpp b/js/src/vm/DateTime.cpp
 | |
| index d27ba46016e0a01bd57dde7acc219eaaee9e65ca..3cfa6ec99272339238a4494b62b008de7036d426 100644
 | |
| --- a/js/src/vm/DateTime.cpp
 | |
| +++ b/js/src/vm/DateTime.cpp
 | |
| @@ -168,6 +168,11 @@ void js::DateTimeInfo::internalResetTimeZone(ResetTimeZoneMode mode) {
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void js::DateTimeInfo::internalSetTimeZoneOverride(mozilla::UniquePtr<icu::TimeZone> timeZone) {
 | |
| +  timeZoneOverride_ = std::move(timeZone);
 | |
| +  internalResetTimeZone(ResetTimeZoneMode::ResetEvenIfOffsetUnchanged);
 | |
| +}
 | |
| +
 | |
|  void js::DateTimeInfo::updateTimeZone() {
 | |
|    MOZ_ASSERT(timeZoneStatus_ != TimeZoneStatus::Valid);
 | |
|  
 | |
| @@ -528,10 +533,27 @@ void js::ResetTimeZoneInternal(ResetTimeZoneMode mode) {
 | |
|    js::DateTimeInfo::resetTimeZone(mode);
 | |
|  }
 | |
|  
 | |
| +void js::SetTimeZoneOverrideInternal(mozilla::UniquePtr<icu::TimeZone> timeZone) {
 | |
| +  auto guard = js::DateTimeInfo::instance->lock();
 | |
| +  guard->internalSetTimeZoneOverride(std::move(timeZone));
 | |
| +}
 | |
| +
 | |
|  JS_PUBLIC_API void JS::ResetTimeZone() {
 | |
|    js::ResetTimeZoneInternal(js::ResetTimeZoneMode::ResetEvenIfOffsetUnchanged);
 | |
|  }
 | |
|  
 | |
| +JS_PUBLIC_API bool JS::SetTimeZoneOverride(const char* timeZoneId) {
 | |
| +  // Validate timezone id.
 | |
| +  mozilla::UniquePtr<icu::TimeZone> timeZone(icu::TimeZone::createTimeZone(
 | |
| +      icu::UnicodeString(timeZoneId, -1, US_INV)));
 | |
| +  if (!timeZone || *timeZone == icu::TimeZone::getUnknown()) {
 | |
| +    fprintf(stderr, "Invalid timezone id: %s\n", timeZoneId);
 | |
| +    return false;
 | |
| +  }
 | |
| +  js::SetTimeZoneOverrideInternal(std::move(timeZone));
 | |
| +  return true;
 | |
| +}
 | |
| +
 | |
|  #if defined(XP_WIN)
 | |
|  static bool IsOlsonCompatibleWindowsTimeZoneId(const char* tz) {
 | |
|    // ICU ignores the TZ environment variable on Windows and instead directly
 | |
| @@ -726,6 +748,11 @@ void js::ResyncICUDefaultTimeZone() {
 | |
|  
 | |
|  void js::DateTimeInfo::internalResyncICUDefaultTimeZone() {
 | |
|  #if JS_HAS_INTL_API && !MOZ_SYSTEM_ICU
 | |
| +  if (timeZoneOverride_) {
 | |
| +    icu::TimeZone::setDefault(*timeZoneOverride_);
 | |
| +    return;
 | |
| +  }
 | |
| +
 | |
|    if (const char* tz = std::getenv("TZ")) {
 | |
|      icu::UnicodeString tzid;
 | |
|  
 | |
| diff --git a/js/src/vm/DateTime.h b/js/src/vm/DateTime.h
 | |
| index 25c5b01fc54c8d45da8ceb7cf6ba163bee3c5361..490c5ce49cd9b5f804df59abbfb0450fb9d1f877 100644
 | |
| --- a/js/src/vm/DateTime.h
 | |
| +++ b/js/src/vm/DateTime.h
 | |
| @@ -67,6 +67,8 @@ enum class ResetTimeZoneMode : bool {
 | |
|   */
 | |
|  extern void ResetTimeZoneInternal(ResetTimeZoneMode mode);
 | |
|  
 | |
| +extern void SetTimeZoneOverrideInternal(mozilla::UniquePtr<icu::TimeZone> timeZone);
 | |
| +
 | |
|  /**
 | |
|   * ICU's default time zone, used for various date/time formatting operations
 | |
|   * that include the local time in the representation, is allowed to go stale
 | |
| @@ -206,6 +208,7 @@ class DateTimeInfo {
 | |
|    // and js::ResyncICUDefaultTimeZone().
 | |
|    friend void js::ResetTimeZoneInternal(ResetTimeZoneMode);
 | |
|    friend void js::ResyncICUDefaultTimeZone();
 | |
| +  friend void js::SetTimeZoneOverrideInternal(mozilla::UniquePtr<icu::TimeZone>);
 | |
|  
 | |
|    static void resetTimeZone(ResetTimeZoneMode mode) {
 | |
|      auto guard = instance->lock();
 | |
| @@ -302,6 +305,8 @@ class DateTimeInfo {
 | |
|    JS::UniqueChars locale_;
 | |
|    JS::UniqueTwoByteChars standardName_;
 | |
|    JS::UniqueTwoByteChars daylightSavingsName_;
 | |
| +
 | |
| +  mozilla::UniquePtr<icu::TimeZone> timeZoneOverride_;
 | |
|  #else
 | |
|    // Restrict the data-time range to the minimum required time_t range as
 | |
|    // specified in POSIX. Most operating systems support 64-bit time_t
 | |
| @@ -317,6 +322,8 @@ class DateTimeInfo {
 | |
|  
 | |
|    void internalResetTimeZone(ResetTimeZoneMode mode);
 | |
|  
 | |
| +  void internalSetTimeZoneOverride(mozilla::UniquePtr<icu::TimeZone> timeZone);
 | |
| +
 | |
|    void updateTimeZone();
 | |
|  
 | |
|    void internalResyncICUDefaultTimeZone();
 | |
| diff --git a/juggler/Helper.js b/juggler/Helper.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..2b1fe7fa712ae210af3ebbccda08404183d19921
 | |
| --- /dev/null
 | |
| +++ b/juggler/Helper.js
 | |
| @@ -0,0 +1,115 @@
 | |
| +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +
 | |
| +class Helper {
 | |
| +  addObserver(handler, topic) {
 | |
| +    Services.obs.addObserver(handler, topic);
 | |
| +    return () => Services.obs.removeObserver(handler, topic);
 | |
| +  }
 | |
| +
 | |
| +  addMessageListener(receiver, eventName, handler) {
 | |
| +    receiver.addMessageListener(eventName, handler);
 | |
| +    return () => receiver.removeMessageListener(eventName, handler);
 | |
| +  }
 | |
| +
 | |
| +  addEventListener(receiver, eventName, handler) {
 | |
| +    receiver.addEventListener(eventName, handler);
 | |
| +    return () => receiver.removeEventListener(eventName, handler);
 | |
| +  }
 | |
| +
 | |
| +  on(receiver, eventName, handler) {
 | |
| +    // The toolkit/modules/EventEmitter.jsm dispatches event name as a first argument.
 | |
| +    // Fire event listeners without it for convenience.
 | |
| +    const handlerWrapper = (_, ...args) => handler(...args);
 | |
| +    receiver.on(eventName, handlerWrapper);
 | |
| +    return () => receiver.off(eventName, handlerWrapper);
 | |
| +  }
 | |
| +
 | |
| +  addProgressListener(progress, listener, flags) {
 | |
| +    progress.addProgressListener(listener, flags);
 | |
| +    return () => progress.removeProgressListener(listener);
 | |
| +  }
 | |
| +
 | |
| +  removeListeners(listeners) {
 | |
| +    for (const tearDown of listeners)
 | |
| +      tearDown.call(null);
 | |
| +    listeners.splice(0, listeners.length);
 | |
| +  }
 | |
| +
 | |
| +  generateId() {
 | |
| +    const string = uuidGen.generateUUID().toString();
 | |
| +    return string.substring(1, string.length - 1);
 | |
| +  }
 | |
| +
 | |
| +  getLoadContext(channel) {
 | |
| +    let loadContext = null;
 | |
| +    try {
 | |
| +      if (channel.notificationCallbacks)
 | |
| +        loadContext = channel.notificationCallbacks.getInterface(Ci.nsILoadContext);
 | |
| +    } catch (e) {}
 | |
| +    try {
 | |
| +      if (!loadContext && channel.loadGroup)
 | |
| +        loadContext = channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
 | |
| +    } catch (e) { }
 | |
| +    return loadContext;
 | |
| +  }
 | |
| +
 | |
| +  getNetworkErrorStatusText(status) {
 | |
| +    if (!status)
 | |
| +      return null;
 | |
| +    for (const key of Object.keys(Cr)) {
 | |
| +      if (Cr[key] === status)
 | |
| +        return key;
 | |
| +    }
 | |
| +    // Security module. The following is taken from
 | |
| +    // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/How_to_check_the_secruity_state_of_an_XMLHTTPRequest_over_SSL
 | |
| +    if ((status & 0xff0000) === 0x5a0000) {
 | |
| +      // NSS_SEC errors (happen below the base value because of negative vals)
 | |
| +      if ((status & 0xffff) < Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE)) {
 | |
| +        // The bases are actually negative, so in our positive numeric space, we
 | |
| +        // need to subtract the base off our value.
 | |
| +        const nssErr = Math.abs(Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE) - (status & 0xffff);
 | |
| +        switch (nssErr) {
 | |
| +          case 11:
 | |
| +            return 'SEC_ERROR_EXPIRED_CERTIFICATE';
 | |
| +          case 12:
 | |
| +            return 'SEC_ERROR_REVOKED_CERTIFICATE';
 | |
| +          case 13:
 | |
| +            return 'SEC_ERROR_UNKNOWN_ISSUER';
 | |
| +          case 20:
 | |
| +            return 'SEC_ERROR_UNTRUSTED_ISSUER';
 | |
| +          case 21:
 | |
| +            return 'SEC_ERROR_UNTRUSTED_CERT';
 | |
| +          case 36:
 | |
| +            return 'SEC_ERROR_CA_CERT_INVALID';
 | |
| +          case 90:
 | |
| +            return 'SEC_ERROR_INADEQUATE_KEY_USAGE';
 | |
| +          case 176:
 | |
| +            return 'SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED';
 | |
| +          default:
 | |
| +            return 'SEC_ERROR_UNKNOWN';
 | |
| +        }
 | |
| +      }
 | |
| +      const sslErr = Math.abs(Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE) - (status & 0xffff);
 | |
| +      switch (sslErr) {
 | |
| +        case 3:
 | |
| +          return 'SSL_ERROR_NO_CERTIFICATE';
 | |
| +        case 4:
 | |
| +          return 'SSL_ERROR_BAD_CERTIFICATE';
 | |
| +        case 8:
 | |
| +          return 'SSL_ERROR_UNSUPPORTED_CERTIFICATE_TYPE';
 | |
| +        case 9:
 | |
| +          return 'SSL_ERROR_UNSUPPORTED_VERSION';
 | |
| +        case 12:
 | |
| +          return 'SSL_ERROR_BAD_CERT_DOMAIN';
 | |
| +        default:
 | |
| +          return 'SSL_ERROR_UNKNOWN';
 | |
| +      }
 | |
| +    }
 | |
| +    return '<unknown error>';
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = [ "Helper" ];
 | |
| +this.Helper = Helper;
 | |
| +
 | |
| diff --git a/juggler/NetworkObserver.js b/juggler/NetworkObserver.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..f803071bea317e4d3ce7341573622f6bddaaf547
 | |
| --- /dev/null
 | |
| +++ b/juggler/NetworkObserver.js
 | |
| @@ -0,0 +1,794 @@
 | |
| +"use strict";
 | |
| +
 | |
| +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
 | |
| +const {CommonUtils} = ChromeUtils.import("resource://services-common/utils.js");
 | |
| +
 | |
| +
 | |
| +const Cc = Components.classes;
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cu = Components.utils;
 | |
| +const Cr = Components.results;
 | |
| +const Cm = Components.manager;
 | |
| +const CC = Components.Constructor;
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +const BinaryInputStream = CC('@mozilla.org/binaryinputstream;1', 'nsIBinaryInputStream', 'setInputStream');
 | |
| +const BinaryOutputStream = CC('@mozilla.org/binaryoutputstream;1', 'nsIBinaryOutputStream', 'setOutputStream');
 | |
| +const StorageStream = CC('@mozilla.org/storagestream;1', 'nsIStorageStream', 'init');
 | |
| +
 | |
| +// Cap response storage with 100Mb per tracked tab.
 | |
| +const MAX_RESPONSE_STORAGE_SIZE = 100 * 1024 * 1024;
 | |
| +
 | |
| +/**
 | |
| + * This is a nsIChannelEventSink implementation that monitors channel redirects.
 | |
| + */
 | |
| +const SINK_CLASS_DESCRIPTION = "Juggler NetworkMonitor Channel Event Sink";
 | |
| +const SINK_CLASS_ID = Components.ID("{c2b4c83e-607a-405a-beab-0ef5dbfb7617}");
 | |
| +const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1";
 | |
| +const SINK_CATEGORY_NAME = "net-channel-event-sinks";
 | |
| +
 | |
| +const pageNetworkSymbol = Symbol('PageNetwork');
 | |
| +
 | |
| +class PageNetwork {
 | |
| +  static _forPageTarget(networkObserver, target) {
 | |
| +    let result = target[pageNetworkSymbol];
 | |
| +    if (!result) {
 | |
| +      result = new PageNetwork(networkObserver, target);
 | |
| +      target[pageNetworkSymbol] = result;
 | |
| +    }
 | |
| +    return result;
 | |
| +  }
 | |
| +
 | |
| +  constructor(networkObserver, target) {
 | |
| +    EventEmitter.decorate(this);
 | |
| +    this._networkObserver = networkObserver;
 | |
| +    this._target = target;
 | |
| +    this._sessionCount = 0;
 | |
| +    this._extraHTTPHeaders = null;
 | |
| +    this._responseStorage = null;
 | |
| +    this._requestInterceptionEnabled = false;
 | |
| +    this._requestIdToInterceptor = null;
 | |
| +  }
 | |
| +
 | |
| +  addSession() {
 | |
| +    if (this._sessionCount === 0) {
 | |
| +      this._responseStorage = new ResponseStorage(this._networkObserver, MAX_RESPONSE_STORAGE_SIZE, MAX_RESPONSE_STORAGE_SIZE / 10);
 | |
| +    }
 | |
| +    ++this._sessionCount;
 | |
| +    return () => this._stopTracking();
 | |
| +  }
 | |
| +
 | |
| +  _stopTracking() {
 | |
| +    --this._sessionCount;
 | |
| +    if (this._sessionCount === 0) {
 | |
| +      this._extraHTTPHeaders = null;
 | |
| +      this._responseStorage = null;
 | |
| +      this._requestInterceptionEnabled = false;
 | |
| +      this._requestIdToInterceptor = null;
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  _isActive() {
 | |
| +    return this._sessionCount > 0;
 | |
| +  }
 | |
| +
 | |
| +  setExtraHTTPHeaders(headers) {
 | |
| +    this._extraHTTPHeaders = headers;
 | |
| +  }
 | |
| +
 | |
| +  enableRequestInterception() {
 | |
| +    this._requestInterceptionEnabled = true;
 | |
| +  }
 | |
| +
 | |
| +  disableRequestInterception() {
 | |
| +    this._requestInterceptionEnabled = false;
 | |
| +    const interceptors = this._requestIdToInterceptor;
 | |
| +    if (!interceptors)
 | |
| +      return;
 | |
| +    this._requestIdToInterceptor = null;
 | |
| +    for (const interceptor of interceptors.values())
 | |
| +      interceptor._resume();
 | |
| +  }
 | |
| +
 | |
| +  resumeInterceptedRequest(requestId, method, headers, postData) {
 | |
| +    this._takeInterceptor(requestId)._resume(method, headers, postData);
 | |
| +  }
 | |
| +
 | |
| +  fulfillInterceptedRequest(requestId, status, statusText, headers, base64body) {
 | |
| +    this._takeInterceptor(requestId)._fulfill(status, statusText, headers, base64body);
 | |
| +  }
 | |
| +
 | |
| +  abortInterceptedRequest(requestId, errorCode) {
 | |
| +    this._takeInterceptor(requestId)._abort(errorCode);
 | |
| +  }
 | |
| +
 | |
| +  getResponseBody(requestId) {
 | |
| +    if (!this._responseStorage)
 | |
| +      throw new Error('Responses are not tracked for the given browser');
 | |
| +    return this._responseStorage.getBase64EncodedResponse(requestId);
 | |
| +  }
 | |
| +
 | |
| +  _ensureInterceptors() {
 | |
| +    if (!this._requestIdToInterceptor)
 | |
| +      this._requestIdToInterceptor = new Map();
 | |
| +    return this._requestIdToInterceptor;
 | |
| +  }
 | |
| +
 | |
| +  _takeInterceptor(requestId) {
 | |
| +    const interceptors = this._requestIdToInterceptor;
 | |
| +    if (!interceptors)
 | |
| +      throw new Error(`Request interception is not enabled`);
 | |
| +    const interceptor = interceptors.get(requestId);
 | |
| +    if (!interceptor)
 | |
| +      throw new Error(`Cannot find request "${requestId}"`);
 | |
| +    interceptors.delete(requestId);
 | |
| +    return interceptor;
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class NetworkObserver {
 | |
| +  static instance() {
 | |
| +    return NetworkObserver._instance || null;
 | |
| +  }
 | |
| +
 | |
| +  constructor(targetRegistry) {
 | |
| +    EventEmitter.decorate(this);
 | |
| +    NetworkObserver._instance = this;
 | |
| +
 | |
| +    this._targetRegistry = targetRegistry;
 | |
| +    this._activityDistributor = Cc["@mozilla.org/network/http-activity-distributor;1"].getService(Ci.nsIHttpActivityDistributor);
 | |
| +    this._activityDistributor.addObserver(this);
 | |
| +
 | |
| +    this._redirectMap = new Map();  // oldId => newId
 | |
| +    this._resumedRequestIdToHeaders = new Map();  // requestId => { headers }
 | |
| +    this._postResumeChannelIdToRequestId = new Map();  // post-resume channel id => pre-resume request id
 | |
| +    this._pendingAuthentication = new Set();  // pre-auth id
 | |
| +    this._postAuthChannelIdToRequestId = new Map();  // pre-auth id => post-auth id
 | |
| +    this._bodyListeners = new Map();  // channel id => ResponseBodyListener.
 | |
| +
 | |
| +    this._channelSink = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsIChannelEventSink]),
 | |
| +      asyncOnChannelRedirect: (oldChannel, newChannel, flags, callback) => {
 | |
| +        this._onRedirect(oldChannel, newChannel, flags);
 | |
| +        callback.onRedirectVerifyCallback(Cr.NS_OK);
 | |
| +      },
 | |
| +    };
 | |
| +    this._channelSinkFactory = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsIFactory]),
 | |
| +      createInstance: (aOuter, aIID) => this._channelSink.QueryInterface(aIID),
 | |
| +    };
 | |
| +    // Register self as ChannelEventSink to track redirects.
 | |
| +    const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
 | |
| +    registrar.registerFactory(SINK_CLASS_ID, SINK_CLASS_DESCRIPTION, SINK_CONTRACT_ID, this._channelSinkFactory);
 | |
| +    Services.catMan.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, SINK_CONTRACT_ID, false, true);
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request'),
 | |
| +      helper.addObserver(this._onResponse.bind(this, false /* fromCache */), 'http-on-examine-response'),
 | |
| +      helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-cached-response'),
 | |
| +      helper.addObserver(this._onResponse.bind(this, true /* fromCache */), 'http-on-examine-merged-response'),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  _requestAuthenticated(httpChannel) {
 | |
| +    this._pendingAuthentication.add(httpChannel.channelId + '');
 | |
| +  }
 | |
| +
 | |
| +  _requestIdBeforeAuthentication(httpChannel) {
 | |
| +    const id = httpChannel.channelId + '';
 | |
| +    return this._postAuthChannelIdToRequestId.has(id) ? id : undefined;
 | |
| +  }
 | |
| +
 | |
| +  _requestId(httpChannel) {
 | |
| +    const id = httpChannel.channelId + '';
 | |
| +    return this._postResumeChannelIdToRequestId.get(id) || this._postAuthChannelIdToRequestId.get(id) || id;
 | |
| +  }
 | |
| +
 | |
| +  _onRedirect(oldChannel, newChannel, flags) {
 | |
| +    if (!(oldChannel instanceof Ci.nsIHttpChannel) || !(newChannel instanceof Ci.nsIHttpChannel))
 | |
| +      return;
 | |
| +    const oldHttpChannel = oldChannel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    const newHttpChannel = newChannel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    const pageNetwork = this._pageNetworkForChannel(oldHttpChannel);
 | |
| +    if (!pageNetwork)
 | |
| +      return;
 | |
| +    const oldRequestId = this._requestId(oldHttpChannel);
 | |
| +    const newRequestId = this._requestId(newHttpChannel);
 | |
| +    if (this._resumedRequestIdToHeaders.has(oldRequestId)) {
 | |
| +      // When we call resetInterception on a request, we get a new "redirected" request for it.
 | |
| +      const { method, headers, postData } = this._resumedRequestIdToHeaders.get(oldRequestId);
 | |
| +      if (headers) {
 | |
| +        // Apply new request headers from interception resume.
 | |
| +        for (const header of requestHeaders(newChannel))
 | |
| +          newChannel.setRequestHeader(header.name, '', false /* merge */);
 | |
| +        for (const header of headers)
 | |
| +          newChannel.setRequestHeader(header.name, header.value, false /* merge */);
 | |
| +      }
 | |
| +      if (method)
 | |
| +        newChannel.requestMethod = method;
 | |
| +      if (postData && newChannel instanceof Ci.nsIUploadChannel) {
 | |
| +        const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
 | |
| +        synthesized.data = atob(postData);
 | |
| +        newChannel.setUploadStream(synthesized, 'application/octet-stream', -1);
 | |
| +      }
 | |
| +      // Use the old request id for the new "redirected" request for protocol consistency.
 | |
| +      this._resumedRequestIdToHeaders.delete(oldRequestId);
 | |
| +      this._postResumeChannelIdToRequestId.set(newRequestId, oldRequestId);
 | |
| +    } else if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) {
 | |
| +      // Regular (non-internal) redirect.
 | |
| +      this._redirectMap.set(newRequestId, oldRequestId);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  observeActivity(channel, activityType, activitySubtype, timestamp, extraSizeData, extraStringData) {
 | |
| +    if (activityType !== Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION)
 | |
| +      return;
 | |
| +    if (!(channel instanceof Ci.nsIHttpChannel))
 | |
| +      return;
 | |
| +    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    const pageNetwork = this._pageNetworkForChannel(httpChannel);
 | |
| +    if (!pageNetwork)
 | |
| +      return;
 | |
| +    if (activitySubtype !== Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
 | |
| +      return;
 | |
| +    if (this._isResumedChannel(httpChannel))
 | |
| +      return;
 | |
| +    if (this._requestIdBeforeAuthentication(httpChannel))
 | |
| +      return;
 | |
| +    this._sendOnRequestFinished(pageNetwork, httpChannel);
 | |
| +  }
 | |
| +
 | |
| +  pageNetworkForTarget(target) {
 | |
| +    return PageNetwork._forPageTarget(this, target);
 | |
| +  }
 | |
| +
 | |
| +  _pageNetworkForChannel(httpChannel) {
 | |
| +    let loadContext = helper.getLoadContext(httpChannel);
 | |
| +    if (!loadContext)
 | |
| +      return;
 | |
| +    const target = this._targetRegistry.targetForBrowser(loadContext.topFrameElement);
 | |
| +    if (!target)
 | |
| +      return;
 | |
| +    const pageNetwork = PageNetwork._forPageTarget(this, target);
 | |
| +    if (!pageNetwork._isActive())
 | |
| +      return;
 | |
| +    return pageNetwork;
 | |
| +  }
 | |
| +
 | |
| +  _isResumedChannel(httpChannel) {
 | |
| +    return this._postResumeChannelIdToRequestId.has(httpChannel.channelId + '');
 | |
| +  }
 | |
| +
 | |
| +  _onRequest(channel, topic) {
 | |
| +    if (!(channel instanceof Ci.nsIHttpChannel))
 | |
| +      return;
 | |
| +    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    const pageNetwork = this._pageNetworkForChannel(httpChannel);
 | |
| +    if (!pageNetwork)
 | |
| +      return;
 | |
| +    if (this._isResumedChannel(httpChannel)) {
 | |
| +      // Ignore onRequest for resumed requests, but listen to their response.
 | |
| +      new ResponseBodyListener(this, pageNetwork, httpChannel);
 | |
| +      return;
 | |
| +    }
 | |
| +    // Convert pending auth bit into auth mapping.
 | |
| +    const channelId = httpChannel.channelId + '';
 | |
| +    if (this._pendingAuthentication.has(channelId)) {
 | |
| +      this._postAuthChannelIdToRequestId.set(channelId, channelId + '-auth');
 | |
| +      this._redirectMap.set(channelId + '-auth', channelId);
 | |
| +      this._pendingAuthentication.delete(channelId);
 | |
| +      const bodyListener = this._bodyListeners.get(channelId);
 | |
| +      if (bodyListener)
 | |
| +        bodyListener.dispose();
 | |
| +    }
 | |
| +    const browserContext = pageNetwork._target.browserContext();
 | |
| +    if (browserContext)
 | |
| +      this._appendExtraHTTPHeaders(httpChannel, browserContext.extraHTTPHeaders);
 | |
| +    this._appendExtraHTTPHeaders(httpChannel, pageNetwork._extraHTTPHeaders);
 | |
| +    const requestId = this._requestId(httpChannel);
 | |
| +    const isRedirect = this._redirectMap.has(requestId);
 | |
| +    const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork);
 | |
| +    if (!interceptionEnabled) {
 | |
| +      new NotificationCallbacks(this, pageNetwork, httpChannel, false);
 | |
| +      this._sendOnRequest(httpChannel, false);
 | |
| +      new ResponseBodyListener(this, pageNetwork, httpChannel);
 | |
| +    } else if (isRedirect) {
 | |
| +      // We pretend that redirect is interceptable in the protocol, although it's actually not
 | |
| +      // and therefore we do not instantiate the interceptor.
 | |
| +      // TODO: look into REDIRECT_MODE_MANUAL.
 | |
| +      const interceptors = pageNetwork._ensureInterceptors();
 | |
| +      interceptors.set(requestId, {
 | |
| +        _resume: () => {},
 | |
| +        _abort: () => {},
 | |
| +        _fulfill: () => {},
 | |
| +      });
 | |
| +      new NotificationCallbacks(this, pageNetwork, httpChannel, false);
 | |
| +      this._sendOnRequest(httpChannel, true);
 | |
| +      new ResponseBodyListener(this, pageNetwork, httpChannel);
 | |
| +    } else {
 | |
| +      const previousCallbacks = httpChannel.notificationCallbacks;
 | |
| +      if (previousCallbacks instanceof Ci.nsIInterfaceRequestor) {
 | |
| +        const interceptor = previousCallbacks.getInterface(Ci.nsINetworkInterceptController);
 | |
| +        // We assume that interceptor is a service worker if there is one.
 | |
| +        if (interceptor && interceptor.shouldPrepareForIntercept(httpChannel.URI, httpChannel)) {
 | |
| +          new NotificationCallbacks(this, pageNetwork, httpChannel, false);
 | |
| +          this._sendOnRequest(httpChannel, false);
 | |
| +          new ResponseBodyListener(this, pageNetwork, httpChannel);
 | |
| +        } else {
 | |
| +          // We'll issue onRequest once it's intercepted.
 | |
| +          new NotificationCallbacks(this, pageNetwork, httpChannel, true);
 | |
| +        }
 | |
| +      } else {
 | |
| +        // We'll issue onRequest once it's intercepted.
 | |
| +        new NotificationCallbacks(this, pageNetwork, httpChannel, true);
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  _isInterceptionEnabledForPage(pageNetwork) {
 | |
| +    if (pageNetwork._requestInterceptionEnabled)
 | |
| +      return true;
 | |
| +    const browserContext = pageNetwork._target.browserContext();
 | |
| +    if (browserContext && browserContext.requestInterceptionEnabled)
 | |
| +      return true;
 | |
| +    if (browserContext && browserContext.settings.onlineOverride === 'offline')
 | |
| +      return true;
 | |
| +    return false;
 | |
| +  }
 | |
| +
 | |
| +  _appendExtraHTTPHeaders(httpChannel, headers) {
 | |
| +    if (!headers)
 | |
| +      return;
 | |
| +    for (const header of headers)
 | |
| +      httpChannel.setRequestHeader(header.name, header.value, false /* merge */);
 | |
| +  }
 | |
| +
 | |
| +  _onIntercepted(httpChannel, interceptor) {
 | |
| +    const pageNetwork = this._pageNetworkForChannel(httpChannel);
 | |
| +    if (!pageNetwork) {
 | |
| +      interceptor._resume();
 | |
| +      return;
 | |
| +    }
 | |
| +    const browserContext = pageNetwork._target.browserContext();
 | |
| +    if (browserContext && browserContext.settings.onlineOverride === 'offline') {
 | |
| +      interceptor._abort(Cr.NS_ERROR_OFFLINE);
 | |
| +      return;
 | |
| +    }
 | |
| +
 | |
| +    const interceptionEnabled = this._isInterceptionEnabledForPage(pageNetwork);
 | |
| +    this._sendOnRequest(httpChannel, !!interceptionEnabled);
 | |
| +    if (interceptionEnabled)
 | |
| +      pageNetwork._ensureInterceptors().set(this._requestId(httpChannel), interceptor);
 | |
| +    else
 | |
| +      interceptor._resume();
 | |
| +  }
 | |
| +
 | |
| +  _sendOnRequest(httpChannel, isIntercepted) {
 | |
| +    const pageNetwork = this._pageNetworkForChannel(httpChannel);
 | |
| +    if (!pageNetwork)
 | |
| +      return;
 | |
| +    const causeType = httpChannel.loadInfo ? httpChannel.loadInfo.externalContentPolicyType : Ci.nsIContentPolicy.TYPE_OTHER;
 | |
| +    const requestId = this._requestId(httpChannel);
 | |
| +    const redirectedFrom = this._redirectMap.get(requestId);
 | |
| +    this._redirectMap.delete(requestId);
 | |
| +    pageNetwork.emit(PageNetwork.Events.Request, httpChannel, {
 | |
| +      url: httpChannel.URI.spec,
 | |
| +      isIntercepted,
 | |
| +      requestId,
 | |
| +      redirectedFrom,
 | |
| +      postData: readRequestPostData(httpChannel),
 | |
| +      headers: requestHeaders(httpChannel),
 | |
| +      method: httpChannel.requestMethod,
 | |
| +      navigationId: httpChannel.isMainDocumentChannel ? this._requestIdBeforeAuthentication(httpChannel) || this._requestId(httpChannel) : undefined,
 | |
| +      cause: causeTypeToString(causeType),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _sendOnRequestFinished(pageNetwork, httpChannel) {
 | |
| +    pageNetwork.emit(PageNetwork.Events.RequestFinished, httpChannel, {
 | |
| +      requestId: this._requestId(httpChannel),
 | |
| +    });
 | |
| +    this._cleanupChannelState(httpChannel);
 | |
| +  }
 | |
| +
 | |
| +  _sendOnRequestFailed(pageNetwork, httpChannel, error) {
 | |
| +    pageNetwork.emit(PageNetwork.Events.RequestFailed, httpChannel, {
 | |
| +      requestId: this._requestId(httpChannel),
 | |
| +      errorCode: helper.getNetworkErrorStatusText(error),
 | |
| +    });
 | |
| +    this._cleanupChannelState(httpChannel);
 | |
| +  }
 | |
| +
 | |
| +  _cleanupChannelState(httpChannel) {
 | |
| +    const id = httpChannel.channelId + '';
 | |
| +    this._postResumeChannelIdToRequestId.delete(id);
 | |
| +    this._postAuthChannelIdToRequestId.delete(id);
 | |
| +  }
 | |
| +
 | |
| +  _onResponse(fromCache, httpChannel, topic) {
 | |
| +    const pageNetwork = this._pageNetworkForChannel(httpChannel);
 | |
| +    if (!pageNetwork)
 | |
| +      return;
 | |
| +    httpChannel.QueryInterface(Ci.nsIHttpChannelInternal);
 | |
| +    const headers = [];
 | |
| +    httpChannel.visitResponseHeaders({
 | |
| +      visitHeader: (name, value) => headers.push({name, value}),
 | |
| +    });
 | |
| +
 | |
| +    let remoteIPAddress = undefined;
 | |
| +    let remotePort = undefined;
 | |
| +    try {
 | |
| +      remoteIPAddress = httpChannel.remoteAddress;
 | |
| +      remotePort = httpChannel.remotePort;
 | |
| +    } catch (e) {
 | |
| +      // remoteAddress is not defined for cached requests.
 | |
| +    }
 | |
| +    pageNetwork.emit(PageNetwork.Events.Response, httpChannel, {
 | |
| +      requestId: this._requestId(httpChannel),
 | |
| +      securityDetails: getSecurityDetails(httpChannel),
 | |
| +      fromCache,
 | |
| +      headers,
 | |
| +      remoteIPAddress,
 | |
| +      remotePort,
 | |
| +      status: httpChannel.responseStatus,
 | |
| +      statusText: httpChannel.responseStatusText,
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onResponseFinished(pageNetwork, httpChannel, body) {
 | |
| +    if (!pageNetwork._isActive())
 | |
| +      return;
 | |
| +    pageNetwork._responseStorage.addResponseBody(httpChannel, body);
 | |
| +    this._sendOnRequestFinished(pageNetwork, httpChannel);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._activityDistributor.removeObserver(this);
 | |
| +    const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
 | |
| +    registrar.unregisterFactory(SINK_CLASS_ID, this._channelSinkFactory);
 | |
| +    Services.catMan.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, false);
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +const protocolVersionNames = {
 | |
| +  [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: 'TLS 1',
 | |
| +  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: 'TLS 1.1',
 | |
| +  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: 'TLS 1.2',
 | |
| +  [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: 'TLS 1.3',
 | |
| +};
 | |
| +
 | |
| +function getSecurityDetails(httpChannel) {
 | |
| +  const securityInfo = httpChannel.securityInfo;
 | |
| +  if (!securityInfo)
 | |
| +    return null;
 | |
| +  securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
 | |
| +  if (!securityInfo.serverCert)
 | |
| +    return null;
 | |
| +  return {
 | |
| +    protocol: protocolVersionNames[securityInfo.protocolVersion] || '<unknown>',
 | |
| +    subjectName: securityInfo.serverCert.commonName,
 | |
| +    issuer: securityInfo.serverCert.issuerCommonName,
 | |
| +    // Convert to seconds.
 | |
| +    validFrom: securityInfo.serverCert.validity.notBefore / 1000 / 1000,
 | |
| +    validTo: securityInfo.serverCert.validity.notAfter / 1000 / 1000,
 | |
| +  };
 | |
| +}
 | |
| +
 | |
| +function readRequestPostData(httpChannel) {
 | |
| +  if (!(httpChannel instanceof Ci.nsIUploadChannel))
 | |
| +    return undefined;
 | |
| +  const iStream = httpChannel.uploadStream;
 | |
| +  if (!iStream)
 | |
| +    return undefined;
 | |
| +  const isSeekableStream = iStream instanceof Ci.nsISeekableStream;
 | |
| +
 | |
| +  let prevOffset;
 | |
| +  if (isSeekableStream) {
 | |
| +    prevOffset = iStream.tell();
 | |
| +    iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
 | |
| +  }
 | |
| +
 | |
| +  // Read data from the stream.
 | |
| +  let text = undefined;
 | |
| +  try {
 | |
| +    text = NetUtil.readInputStreamToString(iStream, iStream.available());
 | |
| +    const converter = Cc['@mozilla.org/intl/scriptableunicodeconverter']
 | |
| +        .createInstance(Ci.nsIScriptableUnicodeConverter);
 | |
| +    converter.charset = 'UTF-8';
 | |
| +    text = converter.ConvertToUnicode(text);
 | |
| +  } catch (err) {
 | |
| +    text = undefined;
 | |
| +  }
 | |
| +
 | |
| +  // Seek locks the file, so seek to the beginning only if necko hasn't
 | |
| +  // read it yet, since necko doesn't seek to 0 before reading (at lest
 | |
| +  // not till 459384 is fixed).
 | |
| +  if (isSeekableStream && prevOffset == 0)
 | |
| +    iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
 | |
| +  return text;
 | |
| +}
 | |
| +
 | |
| +function requestHeaders(httpChannel) {
 | |
| +  const headers = [];
 | |
| +  httpChannel.visitRequestHeaders({
 | |
| +    visitHeader: (name, value) => headers.push({name, value}),
 | |
| +  });
 | |
| +  return headers;
 | |
| +}
 | |
| +
 | |
| +function causeTypeToString(causeType) {
 | |
| +  for (let key in Ci.nsIContentPolicy) {
 | |
| +    if (Ci.nsIContentPolicy[key] === causeType)
 | |
| +      return key;
 | |
| +  }
 | |
| +  return 'TYPE_OTHER';
 | |
| +}
 | |
| +
 | |
| +class ResponseStorage {
 | |
| +  constructor(networkObserver, maxTotalSize, maxResponseSize) {
 | |
| +    this._networkObserver = networkObserver;
 | |
| +    this._totalSize = 0;
 | |
| +    this._maxResponseSize = maxResponseSize;
 | |
| +    this._maxTotalSize = maxTotalSize;
 | |
| +    this._responses = new Map();
 | |
| +  }
 | |
| +
 | |
| +  addResponseBody(httpChannel, body) {
 | |
| +    if (body.length > this._maxResponseSize) {
 | |
| +      this._responses.set(requestId, {
 | |
| +        evicted: true,
 | |
| +        body: '',
 | |
| +      });
 | |
| +      return;
 | |
| +    }
 | |
| +    let encodings = [];
 | |
| +    if ((httpChannel instanceof Ci.nsIEncodedChannel) && httpChannel.contentEncodings && !httpChannel.applyConversion) {
 | |
| +      const encodingHeader = httpChannel.getResponseHeader("Content-Encoding");
 | |
| +      encodings = encodingHeader.split(/\s*\t*,\s*\t*/);
 | |
| +    }
 | |
| +    this._responses.set(this._networkObserver._requestId(httpChannel), {body, encodings});
 | |
| +    this._totalSize += body.length;
 | |
| +    if (this._totalSize > this._maxTotalSize) {
 | |
| +      for (let [requestId, response] of this._responses) {
 | |
| +        this._totalSize -= response.body.length;
 | |
| +        response.body = '';
 | |
| +        response.evicted = true;
 | |
| +        if (this._totalSize < this._maxTotalSize)
 | |
| +          break;
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  getBase64EncodedResponse(requestId) {
 | |
| +    const response = this._responses.get(requestId);
 | |
| +    if (!response)
 | |
| +      throw new Error(`Request "${requestId}" is not found`);
 | |
| +    if (response.evicted)
 | |
| +      return {base64body: '', evicted: true};
 | |
| +    let result = response.body;
 | |
| +    if (response.encodings && response.encodings.length) {
 | |
| +      for (const encoding of response.encodings)
 | |
| +        result = CommonUtils.convertString(result, encoding, 'uncompressed');
 | |
| +    }
 | |
| +    return {base64body: btoa(result)};
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class ResponseBodyListener {
 | |
| +  constructor(networkObserver, pageNetwork, httpChannel) {
 | |
| +    this._networkObserver = networkObserver;
 | |
| +    this._pageNetwork = pageNetwork;
 | |
| +    this._httpChannel = httpChannel;
 | |
| +    this._chunks = [];
 | |
| +    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIStreamListener]);
 | |
| +    httpChannel.QueryInterface(Ci.nsITraceableChannel);
 | |
| +    this.originalListener = httpChannel.setNewListener(this);
 | |
| +    this._disposed = false;
 | |
| +    this._networkObserver._bodyListeners.set(this._httpChannel.channelId + '', this);
 | |
| +  }
 | |
| +
 | |
| +  onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
 | |
| +    if (this._disposed) {
 | |
| +      this.originalListener.onDataAvailable(aRequest, aInputStream, aOffset, aCount);
 | |
| +      return;
 | |
| +    }
 | |
| +
 | |
| +    const iStream = new BinaryInputStream(aInputStream);
 | |
| +    const sStream = new StorageStream(8192, aCount, null);
 | |
| +    const oStream = new BinaryOutputStream(sStream.getOutputStream(0));
 | |
| +
 | |
| +    // Copy received data as they come.
 | |
| +    const data = iStream.readBytes(aCount);
 | |
| +    this._chunks.push(data);
 | |
| +
 | |
| +    oStream.writeBytes(data, aCount);
 | |
| +    this.originalListener.onDataAvailable(aRequest, sStream.newInputStream(0), aOffset, aCount);
 | |
| +  }
 | |
| +
 | |
| +  onStartRequest(aRequest) {
 | |
| +    this.originalListener.onStartRequest(aRequest);
 | |
| +  }
 | |
| +
 | |
| +  onStopRequest(aRequest, aStatusCode) {
 | |
| +    this.originalListener.onStopRequest(aRequest, aStatusCode);
 | |
| +    if (this._disposed)
 | |
| +      return;
 | |
| +
 | |
| +    if (aStatusCode === 0) {
 | |
| +      const body = this._chunks.join('');
 | |
| +      this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body);
 | |
| +    } else {
 | |
| +      this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, aStatusCode);
 | |
| +    }
 | |
| +
 | |
| +    delete this._chunks;
 | |
| +    this.dispose();
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._disposed = true;
 | |
| +    this._networkObserver._bodyListeners.delete(this._httpChannel.channelId + '');
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class NotificationCallbacks {
 | |
| +  constructor(networkObserver, pageNetwork, httpChannel, shouldIntercept) {
 | |
| +    this._networkObserver = networkObserver;
 | |
| +    this._pageNetwork = pageNetwork;
 | |
| +    this._shouldIntercept = shouldIntercept;
 | |
| +    this._httpChannel = httpChannel;
 | |
| +    this._previousCallbacks = httpChannel.notificationCallbacks;
 | |
| +    httpChannel.notificationCallbacks = this;
 | |
| +
 | |
| +    const qis = [
 | |
| +      Ci.nsIAuthPrompt2,
 | |
| +      Ci.nsIAuthPromptProvider,
 | |
| +      Ci.nsIInterfaceRequestor,
 | |
| +    ];
 | |
| +    if (shouldIntercept)
 | |
| +      qis.push(Ci.nsINetworkInterceptController);
 | |
| +    this.QueryInterface = ChromeUtils.generateQI(qis);
 | |
| +  }
 | |
| +
 | |
| +  getInterface(iid) {
 | |
| +    if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPromptProvider))
 | |
| +      return this;
 | |
| +    if (this._shouldIntercept && iid.equals(Ci.nsINetworkInterceptController))
 | |
| +      return this;
 | |
| +    if (iid.equals(Ci.nsIAuthPrompt))  // Block nsIAuthPrompt - we want nsIAuthPrompt2 to be used instead.
 | |
| +      throw Cr.NS_ERROR_NO_INTERFACE;
 | |
| +    if (this._previousCallbacks)
 | |
| +      return this._previousCallbacks.getInterface(iid);
 | |
| +    throw Cr.NS_ERROR_NO_INTERFACE;
 | |
| +  }
 | |
| +
 | |
| +  _forward(iid, method, args) {
 | |
| +    if (!this._previousCallbacks)
 | |
| +      return;
 | |
| +    try {
 | |
| +      const impl = this._previousCallbacks.getInterface(iid);
 | |
| +      impl[method].apply(impl, args);
 | |
| +    } catch (e) {
 | |
| +      if (e.result != Cr.NS_ERROR_NO_INTERFACE)
 | |
| +        throw e;
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  // nsIAuthPromptProvider
 | |
| +  getAuthPrompt(aPromptReason, iid) {
 | |
| +    return this;
 | |
| +  }
 | |
| +
 | |
| +  // nsIAuthPrompt2
 | |
| +  asyncPromptAuth(aChannel, aCallback, aContext, level, authInfo) {
 | |
| +    let canceled = false;
 | |
| +    Promise.resolve().then(() => {
 | |
| +      if (canceled)
 | |
| +        return;
 | |
| +      const hasAuth = this.promptAuth(aChannel, level, authInfo);
 | |
| +      if (hasAuth)
 | |
| +        aCallback.onAuthAvailable(aContext, authInfo);
 | |
| +      else
 | |
| +        aCallback.onAuthCancelled(aContext, true);
 | |
| +    });
 | |
| +    return {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsICancelable]),
 | |
| +      cancel: () => {
 | |
| +        aCallback.onAuthCancelled(aContext, false);
 | |
| +        canceled = true;
 | |
| +      }
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  // nsIAuthPrompt2
 | |
| +  promptAuth(aChannel, level, authInfo) {
 | |
| +    if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED)
 | |
| +      return false;
 | |
| +    const browserContext = this._pageNetwork._target.browserContext();
 | |
| +    const credentials = browserContext ? browserContext.httpCredentials : undefined;
 | |
| +    if (!credentials)
 | |
| +      return false;
 | |
| +    authInfo.username = credentials.username;
 | |
| +    authInfo.password = credentials.password;
 | |
| +    this._networkObserver._requestAuthenticated(this._httpChannel);
 | |
| +    return true;
 | |
| +  }
 | |
| +
 | |
| +  // nsINetworkInterceptController
 | |
| +  shouldPrepareForIntercept(aURI, channel) {
 | |
| +    if (!(channel instanceof Ci.nsIHttpChannel))
 | |
| +      return false;
 | |
| +    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    return httpChannel.channelId === this._httpChannel.channelId;
 | |
| +  }
 | |
| +
 | |
| +  // nsINetworkInterceptController
 | |
| +  channelIntercepted(intercepted) {
 | |
| +    this._intercepted = intercepted.QueryInterface(Ci.nsIInterceptedChannel);
 | |
| +    const httpChannel = this._intercepted.channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    this._networkObserver._onIntercepted(httpChannel, this);
 | |
| +  }
 | |
| +
 | |
| +  _resume(method, headers, postData) {
 | |
| +    this._networkObserver._resumedRequestIdToHeaders.set(this._networkObserver._requestId(this._httpChannel), { method, headers, postData });
 | |
| +    this._intercepted.resetInterception();
 | |
| +  }
 | |
| +
 | |
| +  _fulfill(status, statusText, headers, base64body) {
 | |
| +    this._intercepted.synthesizeStatus(status, statusText);
 | |
| +    for (const header of headers)
 | |
| +      this._intercepted.synthesizeHeader(header.name, header.value);
 | |
| +    const synthesized = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
 | |
| +    const body = base64body ? atob(base64body) : '';
 | |
| +    synthesized.data = body;
 | |
| +    this._intercepted.startSynthesizedResponse(synthesized, null, null, '', false);
 | |
| +    this._intercepted.finishSynthesizedResponse();
 | |
| +    this._pageNetwork.emit(PageNetwork.Events.Response, this._httpChannel, {
 | |
| +      requestId: this._networkObserver._requestId(this._httpChannel),
 | |
| +      securityDetails: null,
 | |
| +      fromCache: false,
 | |
| +      headers,
 | |
| +      status,
 | |
| +      statusText,
 | |
| +    });
 | |
| +    this._networkObserver._onResponseFinished(this._pageNetwork, this._httpChannel, body);
 | |
| +  }
 | |
| +
 | |
| +  _abort(errorCode) {
 | |
| +    const error = errorMap[errorCode] || Cr.NS_ERROR_FAILURE;
 | |
| +    this._intercepted.cancelInterception(error);
 | |
| +    this._networkObserver._sendOnRequestFailed(this._pageNetwork, this._httpChannel, error);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +const errorMap = {
 | |
| +  'aborted': Cr.NS_ERROR_ABORT,
 | |
| +  'accessdenied': Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED,
 | |
| +  'addressunreachable': Cr.NS_ERROR_UNKNOWN_HOST,
 | |
| +  'blockedbyclient': Cr.NS_ERROR_FAILURE,
 | |
| +  'blockedbyresponse': Cr.NS_ERROR_FAILURE,
 | |
| +  'connectionaborted': Cr.NS_ERROR_NET_INTERRUPT,
 | |
| +  'connectionclosed': Cr.NS_ERROR_FAILURE,
 | |
| +  'connectionfailed': Cr.NS_ERROR_FAILURE,
 | |
| +  'connectionrefused': Cr.NS_ERROR_CONNECTION_REFUSED,
 | |
| +  'connectionreset': Cr.NS_ERROR_NET_RESET,
 | |
| +  'internetdisconnected': Cr.NS_ERROR_OFFLINE,
 | |
| +  'namenotresolved': Cr.NS_ERROR_UNKNOWN_HOST,
 | |
| +  'timedout': Cr.NS_ERROR_NET_TIMEOUT,
 | |
| +  'failed': Cr.NS_ERROR_FAILURE,
 | |
| +};
 | |
| +
 | |
| +PageNetwork.Events = {
 | |
| +  Request: Symbol('PageNetwork.Events.Request'),
 | |
| +  Response: Symbol('PageNetwork.Events.Response'),
 | |
| +  RequestFinished: Symbol('PageNetwork.Events.RequestFinished'),
 | |
| +  RequestFailed: Symbol('PageNetwork.Events.RequestFailed'),
 | |
| +};
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['NetworkObserver', 'PageNetwork'];
 | |
| +this.NetworkObserver = NetworkObserver;
 | |
| +this.PageNetwork = PageNetwork;
 | |
| diff --git a/juggler/SimpleChannel.js b/juggler/SimpleChannel.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..ba34976ad05e7f5f1a99777f76ac08b171af40b7
 | |
| --- /dev/null
 | |
| +++ b/juggler/SimpleChannel.js
 | |
| @@ -0,0 +1,130 @@
 | |
| +"use strict";
 | |
| +// Note: this file should be loadabale with eval() into worker environment.
 | |
| +// Avoid Components.*, ChromeUtils and global const variables.
 | |
| +
 | |
| +const SIMPLE_CHANNEL_MESSAGE_NAME = 'juggler:simplechannel';
 | |
| +
 | |
| +class SimpleChannel {
 | |
| +  static createForMessageManager(name, mm) {
 | |
| +    const channel = new SimpleChannel(name);
 | |
| +
 | |
| +    const messageListener = {
 | |
| +      receiveMessage: message => channel._onMessage(message.data)
 | |
| +    };
 | |
| +    mm.addMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
 | |
| +
 | |
| +    channel.transport.sendMessage = obj => mm.sendAsyncMessage(SIMPLE_CHANNEL_MESSAGE_NAME, obj);
 | |
| +    channel.transport.dispose = () => {
 | |
| +      mm.removeMessageListener(SIMPLE_CHANNEL_MESSAGE_NAME, messageListener);
 | |
| +    };
 | |
| +    return channel;
 | |
| +  }
 | |
| +
 | |
| +  constructor(name) {
 | |
| +    this._name = name;
 | |
| +    this._messageId = 0;
 | |
| +    this._connectorId = 0;
 | |
| +    this._pendingMessages = new Map();
 | |
| +    this._handlers = new Map();
 | |
| +    this.transport = {
 | |
| +      sendMessage: null,
 | |
| +      dispose: null,
 | |
| +    };
 | |
| +    this._disposed = false;
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    if (this._disposed)
 | |
| +      return;
 | |
| +    this._disposed = true;
 | |
| +    for (const {resolve, reject, methodName} of this._pendingMessages.values())
 | |
| +      reject(new Error(`Failed "${methodName}": ${this._name} is disposed.`));
 | |
| +    this._pendingMessages.clear();
 | |
| +    this._handlers.clear();
 | |
| +    this.transport.dispose();
 | |
| +  }
 | |
| +
 | |
| +  _rejectCallbacksFromConnector(connectorId) {
 | |
| +    for (const [messageId, callback] of this._pendingMessages) {
 | |
| +      if (callback.connectorId === connectorId) {
 | |
| +        callback.reject(new Error(`Failed "${callback.methodName}": connector for namespace "${callback.namespace}" in channel "${this._name}" is disposed.`));
 | |
| +        this._pendingMessages.delete(messageId);
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  connect(namespace) {
 | |
| +    const connectorId = ++this._connectorId;
 | |
| +    return {
 | |
| +      send: (...args) => this._send(namespace, connectorId, ...args),
 | |
| +      emit: (...args) => void this._send(namespace, connectorId, ...args).catch(e => {}),
 | |
| +      dispose: () => this._rejectCallbacksFromConnector(connectorId),
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  register(namespace, handler) {
 | |
| +    if (this._handlers.has(namespace))
 | |
| +      throw new Error('ERROR: double-register for namespace ' + namespace);
 | |
| +    this._handlers.set(namespace, handler);
 | |
| +    return () => this.unregister(namespace);
 | |
| +  }
 | |
| +
 | |
| +  unregister(namespace) {
 | |
| +    this._handlers.delete(namespace);
 | |
| +  }
 | |
| +
 | |
| +  /**
 | |
| +   * @param {string} namespace
 | |
| +   * @param {number} connectorId
 | |
| +   * @param {string} methodName
 | |
| +   * @param {...*} params
 | |
| +   * @return {!Promise<*>}
 | |
| +   */
 | |
| +  async _send(namespace, connectorId, methodName, ...params) {
 | |
| +    if (this._disposed)
 | |
| +      throw new Error(`ERROR: channel ${this._name} is already disposed! Cannot send "${methodName}" to "${namespace}"`);
 | |
| +    const id = ++this._messageId;
 | |
| +    const promise = new Promise((resolve, reject) => {
 | |
| +      this._pendingMessages.set(id, {connectorId, resolve, reject, methodName, namespace});
 | |
| +    });
 | |
| +    this.transport.sendMessage({requestId: id, methodName, params, namespace});
 | |
| +    return promise;
 | |
| +  }
 | |
| +
 | |
| +  async _onMessage(data) {
 | |
| +    if (data.responseId) {
 | |
| +      const {resolve, reject} = this._pendingMessages.get(data.responseId);
 | |
| +      this._pendingMessages.delete(data.responseId);
 | |
| +      if (data.error)
 | |
| +        reject(new Error(data.error));
 | |
| +      else
 | |
| +        resolve(data.result);
 | |
| +    } else if (data.requestId) {
 | |
| +      const namespace = data.namespace;
 | |
| +      const handler = this._handlers.get(namespace);
 | |
| +      if (!handler) {
 | |
| +        this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No handler for namespace "${namespace}"`});
 | |
| +        return;
 | |
| +      }
 | |
| +      const method = handler[data.methodName];
 | |
| +      if (!method) {
 | |
| +        this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": No method "${data.methodName}" in namespace "${namespace}"`});
 | |
| +        return;
 | |
| +      }
 | |
| +      try {
 | |
| +        const result = await method.call(handler, ...data.params);
 | |
| +        this.transport.sendMessage({responseId: data.requestId, result});
 | |
| +      } catch (error) {
 | |
| +        this.transport.sendMessage({responseId: data.requestId, error: `error in channel "${this._name}": exception while running method "${data.methodName}" in namespace "${namespace}": ${error.message} ${error.stack}`});
 | |
| +        return;
 | |
| +      }
 | |
| +    } else {
 | |
| +      dump(`
 | |
| +        ERROR: unknown message in channel "${this._name}": ${JSON.stringify(data)}
 | |
| +      `);
 | |
| +    }
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['SimpleChannel'];
 | |
| +this.SimpleChannel = SimpleChannel;
 | |
| diff --git a/juggler/TargetRegistry.js b/juggler/TargetRegistry.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..bbd2983755282e514c17824c210e9ab49a1bbad0
 | |
| --- /dev/null
 | |
| +++ b/juggler/TargetRegistry.js
 | |
| @@ -0,0 +1,676 @@
 | |
| +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const {Preferences} = ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 | |
| +const {ContextualIdentityService} = ChromeUtils.import("resource://gre/modules/ContextualIdentityService.jsm");
 | |
| +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
 | |
| +const {PageHandler} = ChromeUtils.import("chrome://juggler/content/protocol/PageHandler.js");
 | |
| +const {NetworkHandler} = ChromeUtils.import("chrome://juggler/content/protocol/NetworkHandler.js");
 | |
| +const {RuntimeHandler} = ChromeUtils.import("chrome://juggler/content/protocol/RuntimeHandler.js");
 | |
| +const {AccessibilityHandler} = ChromeUtils.import("chrome://juggler/content/protocol/AccessibilityHandler.js");
 | |
| +const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +const IDENTITY_NAME = 'JUGGLER ';
 | |
| +const HUNDRED_YEARS = 60 * 60 * 24 * 365 * 100;
 | |
| +
 | |
| +const ALL_PERMISSIONS = [
 | |
| +  'geo',
 | |
| +  'desktop-notification',
 | |
| +];
 | |
| +
 | |
| +class DownloadInterceptor {
 | |
| +  constructor(registry) {
 | |
| +    this._registry = registry
 | |
| +    this._handlerToUuid = new Map();
 | |
| +    helper.addObserver(this._onRequest.bind(this), 'http-on-modify-request');
 | |
| +  }
 | |
| +
 | |
| +  _onRequest(httpChannel, topic) {
 | |
| +    let loadContext = helper.getLoadContext(httpChannel);
 | |
| +    if (!loadContext)
 | |
| +      return;
 | |
| +    if (!loadContext.topFrameElement)
 | |
| +      return;
 | |
| +    const target = this._registry.targetForBrowser(loadContext.topFrameElement);
 | |
| +    if (!target)
 | |
| +      return;
 | |
| +    target._channelIds.add(httpChannel.channelId);
 | |
| +  }
 | |
| +
 | |
| +  //
 | |
| +  // nsIDownloadInterceptor implementation.
 | |
| +  //
 | |
| +  interceptDownloadRequest(externalAppHandler, request, browsingContext, outFile) {
 | |
| +    let pageTarget = this._registry._browserBrowsingContextToTarget.get(browsingContext);
 | |
| +    // New page downloads won't have browsing contex.
 | |
| +    if (!pageTarget)
 | |
| +      pageTarget = this._registry._targetForChannel(request);
 | |
| +    if (!pageTarget)
 | |
| +      return false;
 | |
| +
 | |
| +    const browserContext = pageTarget.browserContext();
 | |
| +    const options = browserContext.downloadOptions;
 | |
| +    if (!options)
 | |
| +      return false;
 | |
| +
 | |
| +    const uuid = helper.generateId();
 | |
| +    let file = null;
 | |
| +    if (options.behavior === 'saveToDisk') {
 | |
| +      file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
 | |
| +      file.initWithPath(options.downloadsDir);
 | |
| +      file.append(uuid);
 | |
| +
 | |
| +      try {
 | |
| +        file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
 | |
| +      } catch (e) {
 | |
| +        dump(`interceptDownloadRequest failed to create file: ${e}\n`);
 | |
| +        return false;
 | |
| +      }
 | |
| +    }
 | |
| +    outFile.value = file;
 | |
| +    this._handlerToUuid.set(externalAppHandler, uuid);
 | |
| +    const downloadInfo = {
 | |
| +      uuid,
 | |
| +      browserContextId: browserContext.browserContextId,
 | |
| +      pageTargetId: pageTarget.id(),
 | |
| +      url: request.name,
 | |
| +      suggestedFileName: externalAppHandler.suggestedFileName,
 | |
| +    };
 | |
| +    this._registry.emit(TargetRegistry.Events.DownloadCreated, downloadInfo);
 | |
| +    return true;
 | |
| +  }
 | |
| +
 | |
| +  onDownloadComplete(externalAppHandler, canceled, errorName) {
 | |
| +    const uuid = this._handlerToUuid.get(externalAppHandler);
 | |
| +    if (!uuid)
 | |
| +      return;
 | |
| +    this._handlerToUuid.delete(externalAppHandler);
 | |
| +    const downloadInfo = {
 | |
| +      uuid,
 | |
| +    };
 | |
| +    if (errorName === 'NS_BINDING_ABORTED') {
 | |
| +      downloadInfo.canceled = true;
 | |
| +    } else {
 | |
| +      downloadInfo.error = errorName;
 | |
| +    }
 | |
| +    this._registry.emit(TargetRegistry.Events.DownloadFinished, downloadInfo);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class TargetRegistry {
 | |
| +  constructor() {
 | |
| +    EventEmitter.decorate(this);
 | |
| +
 | |
| +    this._browserContextIdToBrowserContext = new Map();
 | |
| +    this._userContextIdToBrowserContext = new Map();
 | |
| +    this._browserToTarget = new Map();
 | |
| +    this._browserBrowsingContextToTarget = new Map();
 | |
| +
 | |
| +    // Cleanup containers from previous runs (if any)
 | |
| +    for (const identity of ContextualIdentityService.getPublicIdentities()) {
 | |
| +      if (identity.name && identity.name.startsWith(IDENTITY_NAME)) {
 | |
| +        ContextualIdentityService.remove(identity.userContextId);
 | |
| +        ContextualIdentityService.closeContainerTabs(identity.userContextId);
 | |
| +      }
 | |
| +    }
 | |
| +
 | |
| +    this._defaultContext = new BrowserContext(this, undefined, undefined);
 | |
| +
 | |
| +    Services.obs.addObserver({
 | |
| +      observe: (subject, topic, data) => {
 | |
| +        const browser = subject.ownerElement;
 | |
| +        if (!browser)
 | |
| +          return;
 | |
| +        const target = this._browserToTarget.get(browser);
 | |
| +        if (!target)
 | |
| +          return;
 | |
| +        target.emit('crashed');
 | |
| +        target.dispose();
 | |
| +        this.emit(TargetRegistry.Events.TargetDestroyed, target);
 | |
| +      }
 | |
| +    }, 'oop-frameloader-crashed');
 | |
| +
 | |
| +    Services.mm.addMessageListener('juggler:content-ready', {
 | |
| +      receiveMessage: message => {
 | |
| +        const linkedBrowser = message.target;
 | |
| +        if (this._browserToTarget.has(linkedBrowser))
 | |
| +          throw new Error(`Internal error: two targets per linkedBrowser`);
 | |
| +
 | |
| +        let tab;
 | |
| +        let gBrowser;
 | |
| +        const windowsIt = Services.wm.getEnumerator('navigator:browser');
 | |
| +        while (windowsIt.hasMoreElements()) {
 | |
| +          const window = windowsIt.getNext();
 | |
| +          // gBrowser is always created before tabs. If gBrowser is not
 | |
| +          // initialized yet the browser belongs to another window.
 | |
| +          if (!window.gBrowser)
 | |
| +            continue;
 | |
| +          tab = window.gBrowser.getTabForBrowser(linkedBrowser);
 | |
| +          if (tab) {
 | |
| +            gBrowser = window.gBrowser;
 | |
| +            break;
 | |
| +          }
 | |
| +        }
 | |
| +        if (!tab)
 | |
| +          return;
 | |
| +
 | |
| +        const { userContextId } = message.data;
 | |
| +        const openerContext = linkedBrowser.browsingContext.opener;
 | |
| +        let openerTarget;
 | |
| +        if (openerContext) {
 | |
| +          // Popups usually have opener context.
 | |
| +          openerTarget = this._browserBrowsingContextToTarget.get(openerContext);
 | |
| +        } else if (tab.openerTab) {
 | |
| +          // Noopener popups from the same window have opener tab instead.
 | |
| +          openerTarget = this._browserToTarget.get(tab.openerTab.linkedBrowser);
 | |
| +        }
 | |
| +        const browserContext = this._userContextIdToBrowserContext.get(userContextId);
 | |
| +        const target = new PageTarget(this, gBrowser, tab, linkedBrowser, browserContext, openerTarget);
 | |
| +
 | |
| +        const sessions = [];
 | |
| +        const readyData = { sessions, target };
 | |
| +        this.emit(TargetRegistry.Events.TargetCreated, readyData);
 | |
| +        sessions.forEach(session => target._initSession(session));
 | |
| +        return {
 | |
| +          scriptsToEvaluateOnNewDocument: browserContext ? browserContext.scriptsToEvaluateOnNewDocument : [],
 | |
| +          bindings: browserContext ? browserContext.bindings : [],
 | |
| +          settings: browserContext ? browserContext.settings : {},
 | |
| +          sessionIds: sessions.map(session => session.sessionId()),
 | |
| +        };
 | |
| +      },
 | |
| +    });
 | |
| +
 | |
| +    const onTabOpenListener = event => {
 | |
| +      const tab = event.target;
 | |
| +      const userContextId = tab.userContextId;
 | |
| +      const browserContext = this._userContextIdToBrowserContext.get(userContextId);
 | |
| +      if (browserContext && browserContext.defaultViewportSize)
 | |
| +        setViewportSizeForBrowser(browserContext.defaultViewportSize, tab.linkedBrowser);
 | |
| +    };
 | |
| +
 | |
| +    const onTabCloseListener = event => {
 | |
| +      const tab = event.target;
 | |
| +      const linkedBrowser = tab.linkedBrowser;
 | |
| +      const target = this._browserToTarget.get(linkedBrowser);
 | |
| +      if (target) {
 | |
| +        target.dispose();
 | |
| +        this.emit(TargetRegistry.Events.TargetDestroyed, target);
 | |
| +      }
 | |
| +    };
 | |
| +
 | |
| +    Services.wm.addListener({
 | |
| +      onOpenWindow: async window => {
 | |
| +        const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
 | |
| +        if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
 | |
| +          return;
 | |
| +        if (domWindow.document.readyState !== 'uninitialized')
 | |
| +          throw new Error('DOMWindow should not be loaded yet');
 | |
| +        await new Promise(fulfill => {
 | |
| +          domWindow.addEventListener('DOMContentLoaded', function listener() {
 | |
| +            domWindow.removeEventListener('DOMContentLoaded', listener);
 | |
| +            fulfill();
 | |
| +          });
 | |
| +        });
 | |
| +        if (!domWindow.gBrowser)
 | |
| +          return;
 | |
| +        domWindow.gBrowser.tabContainer.addEventListener('TabOpen', onTabOpenListener);
 | |
| +        domWindow.gBrowser.tabContainer.addEventListener('TabClose', onTabCloseListener);
 | |
| +      },
 | |
| +      onCloseWindow: window => {
 | |
| +        const domWindow = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
 | |
| +        if (!(domWindow instanceof Ci.nsIDOMChromeWindow))
 | |
| +          return;
 | |
| +        if (!domWindow.gBrowser)
 | |
| +          return;
 | |
| +        domWindow.gBrowser.tabContainer.removeEventListener('TabOpen', onTabOpenListener);
 | |
| +        domWindow.gBrowser.tabContainer.removeEventListener('TabClose', onTabCloseListener);
 | |
| +        for (const tab of domWindow.gBrowser.tabs)
 | |
| +          onTabCloseListener({ target: tab });
 | |
| +      },
 | |
| +    });
 | |
| +
 | |
| +    const extHelperAppSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].getService(Ci.nsIExternalHelperAppService);
 | |
| +    extHelperAppSvc.setDownloadInterceptor(new DownloadInterceptor(this));
 | |
| +  }
 | |
| +
 | |
| +  defaultContext() {
 | |
| +    return this._defaultContext;
 | |
| +  }
 | |
| +
 | |
| +  createBrowserContext(removeOnDetach) {
 | |
| +    return new BrowserContext(this, helper.generateId(), removeOnDetach);
 | |
| +  }
 | |
| +
 | |
| +  browserContextForId(browserContextId) {
 | |
| +    return this._browserContextIdToBrowserContext.get(browserContextId);
 | |
| +  }
 | |
| +
 | |
| +  async newPage({browserContextId}) {
 | |
| +    let window;
 | |
| +    let created = false;
 | |
| +    const windowsIt = Services.wm.getEnumerator('navigator:browser');
 | |
| +    if (windowsIt.hasMoreElements()) {
 | |
| +      window = windowsIt.getNext();
 | |
| +    } else {
 | |
| +      const features = "chrome,dialog=no,all";
 | |
| +      const args = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
 | |
| +      args.data = 'about:blank';
 | |
| +      window = Services.ww.openWindow(null, AppConstants.BROWSER_CHROME_URL, '_blank', features, args);
 | |
| +      created = true;
 | |
| +    }
 | |
| +    if (window.document.readyState !== 'complete') {
 | |
| +      await new Promise(fulfill => {
 | |
| +        window.addEventListener('load', function listener() {
 | |
| +          window.removeEventListener('load', listener);
 | |
| +          fulfill();
 | |
| +        });
 | |
| +      });
 | |
| +    }
 | |
| +    const browserContext = this.browserContextForId(browserContextId);
 | |
| +    const tab = window.gBrowser.addTab('about:blank', {
 | |
| +      userContextId: browserContext.userContextId,
 | |
| +      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
 | |
| +    });
 | |
| +    const target = await new Promise(fulfill => {
 | |
| +      const listener = helper.on(this, TargetRegistry.Events.TargetCreated, ({target}) => {
 | |
| +        if (target._tab === tab) {
 | |
| +          helper.removeListeners([listener]);
 | |
| +          fulfill(target);
 | |
| +        }
 | |
| +      });
 | |
| +    });
 | |
| +    if (created) {
 | |
| +      window.gBrowser.removeTab(window.gBrowser.getTabForBrowser(window.gBrowser.getBrowserAtIndex(0)), {
 | |
| +        skipPermitUnload: true,
 | |
| +      });
 | |
| +    }
 | |
| +    window.gBrowser.selectedTab = tab;
 | |
| +    if (browserContext.settings.timezoneId) {
 | |
| +      if (await target.hasFailedToOverrideTimezone())
 | |
| +        throw new Error('Failed to override timezone');
 | |
| +    }
 | |
| +    return target.id();
 | |
| +  }
 | |
| +
 | |
| +  targets() {
 | |
| +    return Array.from(this._browserToTarget.values());
 | |
| +  }
 | |
| +
 | |
| +  targetForBrowser(browser) {
 | |
| +    return this._browserToTarget.get(browser);
 | |
| +  }
 | |
| +
 | |
| +  _targetForChannel(channel) {
 | |
| +    const channelId = channel.channelId;
 | |
| +    for (const target of this._browserToTarget.values()) {
 | |
| +      if (target._channelIds.has(channelId))
 | |
| +        return target;
 | |
| +    }
 | |
| +    return null;
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class PageTarget {
 | |
| +  constructor(registry, gBrowser, tab, linkedBrowser, browserContext, opener) {
 | |
| +    EventEmitter.decorate(this);
 | |
| +
 | |
| +    this._targetId = helper.generateId();
 | |
| +    this._registry = registry;
 | |
| +    this._gBrowser = gBrowser;
 | |
| +    this._tab = tab;
 | |
| +    this._linkedBrowser = linkedBrowser;
 | |
| +    this._browserContext = browserContext;
 | |
| +    this._viewportSize = undefined;
 | |
| +    this._url = '';
 | |
| +    this._openerId = opener ? opener.id() : undefined;
 | |
| +    this._channel = SimpleChannel.createForMessageManager(`browser::page[${this._targetId}]`, this._linkedBrowser.messageManager);
 | |
| +    this._channelIds = new Set();
 | |
| +
 | |
| +    const navigationListener = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([ Ci.nsIWebProgressListener]),
 | |
| +      onLocationChange: (aWebProgress, aRequest, aLocation) => this._onNavigated(aLocation),
 | |
| +    };
 | |
| +    this._eventListeners = [
 | |
| +      helper.addProgressListener(tab.linkedBrowser, navigationListener, Ci.nsIWebProgress.NOTIFY_LOCATION),
 | |
| +    ];
 | |
| +
 | |
| +    this._disposed = false;
 | |
| +    if (browserContext) {
 | |
| +      browserContext.pages.add(this);
 | |
| +      browserContext._firstPageCallback();
 | |
| +    }
 | |
| +    this._registry._browserToTarget.set(this._linkedBrowser, this);
 | |
| +    this._registry._browserBrowsingContextToTarget.set(this._linkedBrowser.browsingContext, this);
 | |
| +  }
 | |
| +
 | |
| +  linkedBrowser() {
 | |
| +    return this._linkedBrowser;
 | |
| +  }
 | |
| +
 | |
| +  browserContext() {
 | |
| +    return this._browserContext;
 | |
| +  }
 | |
| +
 | |
| +  async setViewportSize(viewportSize) {
 | |
| +    this._viewportSize = viewportSize;
 | |
| +    const actualSize = setViewportSizeForBrowser(viewportSize, this._linkedBrowser);
 | |
| +    await this._channel.connect('').send('awaitViewportDimensions', {
 | |
| +      width: actualSize.width,
 | |
| +      height: actualSize.height
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  connectSession(session) {
 | |
| +    this._initSession(session);
 | |
| +    this._channel.connect('').send('attach', { sessionId: session.sessionId() });
 | |
| +  }
 | |
| +
 | |
| +  disconnectSession(session) {
 | |
| +    if (!this._disposed)
 | |
| +      this._channel.connect('').emit('detach', { sessionId: session.sessionId() });
 | |
| +  }
 | |
| +
 | |
| +  async close(runBeforeUnload = false) {
 | |
| +    await this._gBrowser.removeTab(this._tab, {
 | |
| +      skipPermitUnload: !runBeforeUnload,
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _initSession(session) {
 | |
| +    const pageHandler = new PageHandler(this, session, this._channel);
 | |
| +    const networkHandler = new NetworkHandler(this, session, this._channel);
 | |
| +    session.registerHandler('Page', pageHandler);
 | |
| +    session.registerHandler('Network', networkHandler);
 | |
| +    session.registerHandler('Runtime', new RuntimeHandler(session, this._channel));
 | |
| +    session.registerHandler('Accessibility', new AccessibilityHandler(session, this._channel));
 | |
| +    pageHandler.enable();
 | |
| +    networkHandler.enable();
 | |
| +  }
 | |
| +
 | |
| +  id() {
 | |
| +    return this._targetId;
 | |
| +  }
 | |
| +
 | |
| +  info() {
 | |
| +    return {
 | |
| +      targetId: this.id(),
 | |
| +      type: 'page',
 | |
| +      browserContextId: this._browserContext ? this._browserContext.browserContextId : undefined,
 | |
| +      openerId: this._openerId,
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  _onNavigated(aLocation) {
 | |
| +    this._url = aLocation.spec;
 | |
| +    this._browserContext.grantPermissionsToOrigin(this._url);
 | |
| +  }
 | |
| +
 | |
| +  async ensurePermissions() {
 | |
| +    await this._channel.connect('').send('ensurePermissions', {}).catch(e => void e);
 | |
| +  }
 | |
| +
 | |
| +  async addScriptToEvaluateOnNewDocument(script) {
 | |
| +    await this._channel.connect('').send('addScriptToEvaluateOnNewDocument', script).catch(e => void e);
 | |
| +  }
 | |
| +
 | |
| +  async addBinding(name, script) {
 | |
| +    await this._channel.connect('').send('addBinding', { name, script }).catch(e => void e);
 | |
| +  }
 | |
| +
 | |
| +  async applyContextSetting(name, value) {
 | |
| +    await this._channel.connect('').send('applyContextSetting', { name, value }).catch(e => void e);
 | |
| +  }
 | |
| +
 | |
| +  async hasFailedToOverrideTimezone() {
 | |
| +    return await this._channel.connect('').send('hasFailedToOverrideTimezone').catch(e => true);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._disposed = true;
 | |
| +    if (this._browserContext)
 | |
| +      this._browserContext.pages.delete(this);
 | |
| +    this._registry._browserToTarget.delete(this._linkedBrowser);
 | |
| +    this._registry._browserBrowsingContextToTarget.delete(this._linkedBrowser.browsingContext);
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class BrowserContext {
 | |
| +  constructor(registry, browserContextId, removeOnDetach) {
 | |
| +    this._registry = registry;
 | |
| +    this.browserContextId = browserContextId;
 | |
| +    // Default context has userContextId === 0, but we pass undefined to many APIs just in case.
 | |
| +    this.userContextId = 0;
 | |
| +    if (browserContextId !== undefined) {
 | |
| +      const identity = ContextualIdentityService.create(IDENTITY_NAME + browserContextId);
 | |
| +      this.userContextId = identity.userContextId;
 | |
| +    }
 | |
| +    this._principals = [];
 | |
| +    // Maps origins to the permission lists.
 | |
| +    this._permissions = new Map();
 | |
| +    this._registry._browserContextIdToBrowserContext.set(this.browserContextId, this);
 | |
| +    this._registry._userContextIdToBrowserContext.set(this.userContextId, this);
 | |
| +    this.removeOnDetach = removeOnDetach;
 | |
| +    this.extraHTTPHeaders = undefined;
 | |
| +    this.httpCredentials = undefined;
 | |
| +    this.requestInterceptionEnabled = undefined;
 | |
| +    this.ignoreHTTPSErrors = undefined;
 | |
| +    this.downloadOptions = undefined;
 | |
| +    this.defaultViewportSize = undefined;
 | |
| +    this.scriptsToEvaluateOnNewDocument = [];
 | |
| +    this.bindings = [];
 | |
| +    this.settings = {};
 | |
| +    this.pages = new Set();
 | |
| +    this._firstPagePromise = new Promise(f => this._firstPageCallback = f);
 | |
| +  }
 | |
| +
 | |
| +  async destroy() {
 | |
| +    if (this.userContextId !== 0) {
 | |
| +      ContextualIdentityService.remove(this.userContextId);
 | |
| +      ContextualIdentityService.closeContainerTabs(this.userContextId);
 | |
| +      if (this.pages.size) {
 | |
| +        await new Promise(f => {
 | |
| +          const listener = helper.on(this._registry, TargetRegistry.Events.TargetDestroyed, () => {
 | |
| +            if (!this.pages.size) {
 | |
| +              helper.removeListeners([listener]);
 | |
| +              f();
 | |
| +            }
 | |
| +          });
 | |
| +        });
 | |
| +      }
 | |
| +    }
 | |
| +    this._registry._browserContextIdToBrowserContext.delete(this.browserContextId);
 | |
| +    this._registry._userContextIdToBrowserContext.delete(this.userContextId);
 | |
| +  }
 | |
| +
 | |
| +  setIgnoreHTTPSErrors(ignoreHTTPSErrors) {
 | |
| +    if (this.ignoreHTTPSErrors === ignoreHTTPSErrors)
 | |
| +      return;
 | |
| +    this.ignoreHTTPSErrors = ignoreHTTPSErrors;
 | |
| +    const certOverrideService = Cc[
 | |
| +      "@mozilla.org/security/certoverride;1"
 | |
| +    ].getService(Ci.nsICertOverrideService);
 | |
| +    if (ignoreHTTPSErrors) {
 | |
| +      Preferences.set("network.stricttransportsecurity.preloadlist", false);
 | |
| +      Preferences.set("security.cert_pinning.enforcement_level", 0);
 | |
| +      certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(true, this.userContextId);
 | |
| +    } else {
 | |
| +      certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(false, this.userContextId);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  async setDefaultViewport(viewport) {
 | |
| +    this.defaultViewportSize = viewport ? viewport.viewportSize : undefined;
 | |
| +    if (!this.userContextId) {
 | |
| +      // First page in the default context comes before onTabOpenListener
 | |
| +      // so we don't set default viewport. Wait for it here and ensure the viewport.
 | |
| +      await this._firstPagePromise;
 | |
| +    }
 | |
| +    const promises = Array.from(this.pages).map(async page => {
 | |
| +      // Resize to new default, unless the page has a custom viewport.
 | |
| +      if (!page._viewportSize)
 | |
| +        await page.setViewportSize(this.defaultViewportSize);
 | |
| +    });
 | |
| +    await Promise.all([
 | |
| +      this.applySetting('deviceScaleFactor', viewport ? viewport.deviceScaleFactor : undefined),
 | |
| +      ...promises,
 | |
| +    ]);
 | |
| +  }
 | |
| +
 | |
| +  async addScriptToEvaluateOnNewDocument(script) {
 | |
| +    this.scriptsToEvaluateOnNewDocument.push(script);
 | |
| +    await Promise.all(Array.from(this.pages).map(page => page.addScriptToEvaluateOnNewDocument(script)));
 | |
| +  }
 | |
| +
 | |
| +  async addBinding(name, script) {
 | |
| +    this.bindings.push({ name, script });
 | |
| +    await Promise.all(Array.from(this.pages).map(page => page.addBinding(name, script)));
 | |
| +  }
 | |
| +
 | |
| +  async applySetting(name, value) {
 | |
| +    this.settings[name] = value;
 | |
| +    await Promise.all(Array.from(this.pages).map(page => page.applyContextSetting(name, value)));
 | |
| +  }
 | |
| +
 | |
| +  async grantPermissions(origin, permissions) {
 | |
| +    this._permissions.set(origin, permissions);
 | |
| +    const promises = [];
 | |
| +    for (const page of this.pages) {
 | |
| +      if (origin === '*' || page._url.startsWith(origin)) {
 | |
| +        this.grantPermissionsToOrigin(page._url);
 | |
| +        promises.push(page.ensurePermissions());
 | |
| +      }
 | |
| +    }
 | |
| +    await Promise.all(promises);
 | |
| +  }
 | |
| +
 | |
| +  resetPermissions() {
 | |
| +    for (const principal of this._principals) {
 | |
| +      for (const permission of ALL_PERMISSIONS)
 | |
| +        Services.perms.removeFromPrincipal(principal, permission);
 | |
| +    }
 | |
| +    this._principals = [];
 | |
| +    this._permissions.clear();
 | |
| +  }
 | |
| +
 | |
| +  grantPermissionsToOrigin(url) {
 | |
| +    let origin = Array.from(this._permissions.keys()).find(key => url.startsWith(key));
 | |
| +    if (!origin)
 | |
| +      origin = '*';
 | |
| +
 | |
| +    const permissions = this._permissions.get(origin);
 | |
| +    if (!permissions)
 | |
| +      return;
 | |
| +
 | |
| +    const attrs = { userContextId: this.userContextId || undefined };
 | |
| +    const principal = Services.scriptSecurityManager.createContentPrincipal(NetUtil.newURI(url), attrs);
 | |
| +    this._principals.push(principal);
 | |
| +    for (const permission of ALL_PERMISSIONS) {
 | |
| +      const action = permissions.includes(permission) ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
 | |
| +      Services.perms.addFromPrincipal(principal, permission, action, Ci.nsIPermissionManager.EXPIRE_NEVER, 0 /* expireTime */);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  setCookies(cookies) {
 | |
| +    const protocolToSameSite = {
 | |
| +      [undefined]: Ci.nsICookie.SAMESITE_NONE,
 | |
| +      'Lax': Ci.nsICookie.SAMESITE_LAX,
 | |
| +      'Strict': Ci.nsICookie.SAMESITE_STRICT,
 | |
| +    };
 | |
| +    for (const cookie of cookies) {
 | |
| +      const uri = cookie.url ? NetUtil.newURI(cookie.url) : null;
 | |
| +      let domain = cookie.domain;
 | |
| +      if (!domain) {
 | |
| +        if (!uri)
 | |
| +          throw new Error('At least one of the url and domain needs to be specified');
 | |
| +        domain = uri.host;
 | |
| +      }
 | |
| +      let path = cookie.path;
 | |
| +      if (!path)
 | |
| +        path = uri ? dirPath(uri.filePath) : '/';
 | |
| +      let secure = false;
 | |
| +      if (cookie.secure !== undefined)
 | |
| +        secure = cookie.secure;
 | |
| +      else if (uri && uri.scheme === 'https')
 | |
| +        secure = true;
 | |
| +      Services.cookies.add(
 | |
| +        domain,
 | |
| +        path,
 | |
| +        cookie.name,
 | |
| +        cookie.value,
 | |
| +        secure,
 | |
| +        cookie.httpOnly || false,
 | |
| +        cookie.expires === undefined || cookie.expires === -1 /* isSession */,
 | |
| +        cookie.expires === undefined ? Date.now() + HUNDRED_YEARS : cookie.expires,
 | |
| +        { userContextId: this.userContextId || undefined } /* originAttributes */,
 | |
| +        protocolToSameSite[cookie.sameSite],
 | |
| +      );
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  clearCookies() {
 | |
| +    Services.cookies.removeCookiesWithOriginAttributes(JSON.stringify({ userContextId: this.userContextId || undefined }));
 | |
| +  }
 | |
| +
 | |
| +  getCookies() {
 | |
| +    const result = [];
 | |
| +    const sameSiteToProtocol = {
 | |
| +      [Ci.nsICookie.SAMESITE_NONE]: 'None',
 | |
| +      [Ci.nsICookie.SAMESITE_LAX]: 'Lax',
 | |
| +      [Ci.nsICookie.SAMESITE_STRICT]: 'Strict',
 | |
| +    };
 | |
| +    for (let cookie of Services.cookies.cookies) {
 | |
| +      if (cookie.originAttributes.userContextId !== this.userContextId)
 | |
| +        continue;
 | |
| +      if (cookie.host === 'addons.mozilla.org')
 | |
| +        continue;
 | |
| +      result.push({
 | |
| +        name: cookie.name,
 | |
| +        value: cookie.value,
 | |
| +        domain: cookie.host,
 | |
| +        path: cookie.path,
 | |
| +        expires: cookie.isSession ? -1 : cookie.expiry,
 | |
| +        size: cookie.name.length + cookie.value.length,
 | |
| +        httpOnly: cookie.isHttpOnly,
 | |
| +        secure: cookie.isSecure,
 | |
| +        session: cookie.isSession,
 | |
| +        sameSite: sameSiteToProtocol[cookie.sameSite],
 | |
| +      });
 | |
| +    }
 | |
| +    return result;
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +function dirPath(path) {
 | |
| +  return path.substring(0, path.lastIndexOf('/') + 1);
 | |
| +}
 | |
| +
 | |
| +function setViewportSizeForBrowser(viewportSize, browser) {
 | |
| +  if (viewportSize) {
 | |
| +    const {width, height} = viewportSize;
 | |
| +    browser.style.setProperty('min-width', width + 'px');
 | |
| +    browser.style.setProperty('min-height', height + 'px');
 | |
| +    browser.style.setProperty('max-width', width + 'px');
 | |
| +    browser.style.setProperty('max-height', height + 'px');
 | |
| +  } else {
 | |
| +    browser.style.removeProperty('min-width');
 | |
| +    browser.style.removeProperty('min-height');
 | |
| +    browser.style.removeProperty('max-width');
 | |
| +    browser.style.removeProperty('max-height');
 | |
| +  }
 | |
| +  const rect = browser.getBoundingClientRect();
 | |
| +  return { width: rect.width, height: rect.height };
 | |
| +}
 | |
| +
 | |
| +TargetRegistry.Events = {
 | |
| +  TargetCreated: Symbol('TargetRegistry.Events.TargetCreated'),
 | |
| +  TargetDestroyed: Symbol('TargetRegistry.Events.TargetDestroyed'),
 | |
| +  DownloadCreated: Symbol('TargetRegistry.Events.DownloadCreated'),
 | |
| +  DownloadFinished: Symbol('TargetRegistry.Events.DownloadFinished'),
 | |
| +};
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['TargetRegistry'];
 | |
| +this.TargetRegistry = TargetRegistry;
 | |
| diff --git a/juggler/components/juggler.js b/juggler/components/juggler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..4905a1bc7c585d3d1bf33430991d190cee75e332
 | |
| --- /dev/null
 | |
| +++ b/juggler/components/juggler.js
 | |
| @@ -0,0 +1,80 @@
 | |
| +const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const {Dispatcher} = ChromeUtils.import("chrome://juggler/content/protocol/Dispatcher.js");
 | |
| +const {BrowserHandler} = ChromeUtils.import("chrome://juggler/content/protocol/BrowserHandler.js");
 | |
| +const {NetworkObserver} = ChromeUtils.import("chrome://juggler/content/NetworkObserver.js");
 | |
| +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +const Cc = Components.classes;
 | |
| +const Ci = Components.interfaces;
 | |
| +
 | |
| +const FRAME_SCRIPT = "chrome://juggler/content/content/main.js";
 | |
| +
 | |
| +// Command Line Handler
 | |
| +function CommandLineHandler() {
 | |
| +};
 | |
| +
 | |
| +CommandLineHandler.prototype = {
 | |
| +  classDescription: "Sample command-line handler",
 | |
| +  classID: Components.ID('{f7a74a33-e2ab-422d-b022-4fb213dd2639}'),
 | |
| +  contractID: "@mozilla.org/remote/juggler;1",
 | |
| +  _xpcom_categories: [{
 | |
| +    category: "command-line-handler",
 | |
| +    entry: "m-juggler"
 | |
| +  }],
 | |
| +
 | |
| +  /* nsICommandLineHandler */
 | |
| +  handle: async function(cmdLine) {
 | |
| +    const jugglerFlag = cmdLine.handleFlagWithParam("juggler", false);
 | |
| +    if (!jugglerFlag || isNaN(jugglerFlag))
 | |
| +      return;
 | |
| +    const port = parseInt(jugglerFlag, 10);
 | |
| +    const silent = cmdLine.preventDefault;
 | |
| +    if (silent)
 | |
| +      Services.startup.enterLastWindowClosingSurvivalArea();
 | |
| +
 | |
| +    const targetRegistry = new TargetRegistry();
 | |
| +    new NetworkObserver(targetRegistry);
 | |
| +
 | |
| +    const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
 | |
| +    const WebSocketServer = require('devtools/server/socket/websocket-server');
 | |
| +    this._server = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
 | |
| +    this._server.initSpecialConnection(port, Ci.nsIServerSocket.KeepWhenOffline | Ci.nsIServerSocket.LoopbackOnly, 4);
 | |
| +
 | |
| +    const token = helper.generateId();
 | |
| +
 | |
| +    // Force create hidden window here, otherwise its creation later closes the web socket!
 | |
| +    Services.appShell.hiddenDOMWindow;
 | |
| +
 | |
| +    this._server.asyncListen({
 | |
| +      onSocketAccepted: async(socket, transport) => {
 | |
| +        const input = transport.openInputStream(0, 0, 0);
 | |
| +        const output = transport.openOutputStream(0, 0, 0);
 | |
| +        const webSocket = await WebSocketServer.accept(transport, input, output, "/" + token);
 | |
| +        const dispatcher = new Dispatcher(webSocket);
 | |
| +        const browserHandler = new BrowserHandler(dispatcher.rootSession(), dispatcher, targetRegistry, () => {
 | |
| +          if (silent)
 | |
| +            Services.startup.exitLastWindowClosingSurvivalArea();
 | |
| +        });
 | |
| +        dispatcher.rootSession().registerHandler('Browser', browserHandler);
 | |
| +      }
 | |
| +    });
 | |
| +
 | |
| +    Services.mm.loadFrameScript(FRAME_SCRIPT, true /* aAllowDelayedLoad */);
 | |
| +    dump(`Juggler listening on ws://127.0.0.1:${this._server.port}/${token}\n`);
 | |
| +  },
 | |
| +
 | |
| +  QueryInterface: ChromeUtils.generateQI([ Ci.nsICommandLineHandler ]),
 | |
| +
 | |
| +  // CHANGEME: change the help info as appropriate, but
 | |
| +  // follow the guidelines in nsICommandLineHandler.idl
 | |
| +  // specifically, flag descriptions should start at
 | |
| +  // character 24, and lines should be wrapped at
 | |
| +  // 72 characters with embedded newlines,
 | |
| +  // and finally, the string should end with a newline
 | |
| +  helpInfo : "  --juggler            Enable Juggler automation\n"
 | |
| +};
 | |
| +
 | |
| +var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandLineHandler]);
 | |
| diff --git a/juggler/components/juggler.manifest b/juggler/components/juggler.manifest
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..50f8930207563e0d6b8a7878fc602dbca54d77fc
 | |
| --- /dev/null
 | |
| +++ b/juggler/components/juggler.manifest
 | |
| @@ -0,0 +1,3 @@
 | |
| +component {f7a74a33-e2ab-422d-b022-4fb213dd2639} juggler.js
 | |
| +contract @mozilla.org/remote/juggler;1 {f7a74a33-e2ab-422d-b022-4fb213dd2639}
 | |
| +category command-line-handler m-juggler @mozilla.org/remote/juggler;1
 | |
| diff --git a/juggler/components/moz.build b/juggler/components/moz.build
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..268fbc361d8053182bb6c27f626e853dd7aeb254
 | |
| --- /dev/null
 | |
| +++ b/juggler/components/moz.build
 | |
| @@ -0,0 +1,9 @@
 | |
| +# This Source Code Form is subject to the terms of the Mozilla Public
 | |
| +# License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| +
 | |
| +EXTRA_COMPONENTS += [
 | |
| +    "juggler.js",
 | |
| +    "juggler.manifest",
 | |
| +]
 | |
| +
 | |
| diff --git a/juggler/content/FrameTree.js b/juggler/content/FrameTree.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..fe9b67c153cbb6d687bbb98b8882dfdf2bc10c2e
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/FrameTree.js
 | |
| @@ -0,0 +1,473 @@
 | |
| +"use strict";
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cr = Components.results;
 | |
| +const Cu = Components.utils;
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
 | |
| +const {EventEmitter} = ChromeUtils.import('resource://gre/modules/EventEmitter.jsm');
 | |
| +const {Runtime} = ChromeUtils.import('chrome://juggler/content/content/Runtime.js');
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class FrameTree {
 | |
| +  constructor(rootDocShell) {
 | |
| +    EventEmitter.decorate(this);
 | |
| +
 | |
| +    this._browsingContextGroup = rootDocShell.browsingContext.group;
 | |
| +    if (!this._browsingContextGroup.__jugglerFrameTrees)
 | |
| +      this._browsingContextGroup.__jugglerFrameTrees = new Set();
 | |
| +    this._browsingContextGroup.__jugglerFrameTrees.add(this);
 | |
| +    this._scriptsToEvaluateOnNewDocument = new Map();
 | |
| +
 | |
| +    this._bindings = new Map();
 | |
| +    this._runtime = new Runtime(false /* isWorker */);
 | |
| +    this._workers = new Map();
 | |
| +    this._docShellToFrame = new Map();
 | |
| +    this._frameIdToFrame = new Map();
 | |
| +    this._pageReady = false;
 | |
| +    this._mainFrame = this._createFrame(rootDocShell);
 | |
| +    const webProgress = rootDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
| +                                .getInterface(Ci.nsIWebProgress);
 | |
| +    this.QueryInterface = ChromeUtils.generateQI([
 | |
| +      Ci.nsIWebProgressListener,
 | |
| +      Ci.nsIWebProgressListener2,
 | |
| +      Ci.nsISupportsWeakReference,
 | |
| +    ]);
 | |
| +
 | |
| +    this._wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"].createInstance(Ci.nsIWorkerDebuggerManager);
 | |
| +    this._wdmListener = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerManagerListener]),
 | |
| +      onRegister: this._onWorkerCreated.bind(this),
 | |
| +      onUnregister: this._onWorkerDestroyed.bind(this),
 | |
| +    };
 | |
| +    this._wdm.addListener(this._wdmListener);
 | |
| +    for (const workerDebugger of this._wdm.getWorkerDebuggerEnumerator())
 | |
| +      this._onWorkerCreated(workerDebugger);
 | |
| +
 | |
| +    const flags = Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
 | |
| +                  Ci.nsIWebProgress.NOTIFY_FRAME_LOCATION;
 | |
| +    this._eventListeners = [
 | |
| +      helper.addObserver(this._onDOMWindowCreated.bind(this), 'content-document-global-created'),
 | |
| +      helper.addObserver(subject => this._onDocShellCreated(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-create'),
 | |
| +      helper.addObserver(subject => this._onDocShellDestroyed(subject.QueryInterface(Ci.nsIDocShell)), 'webnavigation-destroy'),
 | |
| +      helper.addProgressListener(webProgress, this, flags),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  workers() {
 | |
| +    return [...this._workers.values()];
 | |
| +  }
 | |
| +
 | |
| +  runtime() {
 | |
| +    return this._runtime;
 | |
| +  }
 | |
| +
 | |
| +  _frameForWorker(workerDebugger) {
 | |
| +    if (workerDebugger.type !== Ci.nsIWorkerDebugger.TYPE_DEDICATED)
 | |
| +      return null;
 | |
| +    const docShell = workerDebugger.window.docShell;
 | |
| +    return this._docShellToFrame.get(docShell) || null;
 | |
| +  }
 | |
| +
 | |
| +  _onDOMWindowCreated(window) {
 | |
| +    const frame = this._docShellToFrame.get(window.docShell) || null;
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    frame._onGlobalObjectCleared();
 | |
| +    this.emit(FrameTree.Events.GlobalObjectCreated, { frame, window });
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerCreated(workerDebugger) {
 | |
| +    // Note: we do not interoperate with firefox devtools.
 | |
| +    if (workerDebugger.isInitialized)
 | |
| +      return;
 | |
| +    const frame = this._frameForWorker(workerDebugger);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    const worker = new Worker(frame, workerDebugger);
 | |
| +    this._workers.set(workerDebugger, worker);
 | |
| +    this.emit(FrameTree.Events.WorkerCreated, worker);
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerDestroyed(workerDebugger) {
 | |
| +    const worker = this._workers.get(workerDebugger);
 | |
| +    if (!worker)
 | |
| +      return;
 | |
| +    worker.dispose();
 | |
| +    this._workers.delete(workerDebugger);
 | |
| +    this.emit(FrameTree.Events.WorkerDestroyed, worker);
 | |
| +  }
 | |
| +
 | |
| +  allFramesInBrowsingContextGroup(group) {
 | |
| +    const frames = [];
 | |
| +    for (const frameTree of (group.__jugglerFrameTrees || []))
 | |
| +      frames.push(...frameTree.frames());
 | |
| +    return frames;
 | |
| +  }
 | |
| +
 | |
| +  isPageReady() {
 | |
| +    return this._pageReady;
 | |
| +  }
 | |
| +
 | |
| +  forcePageReady() {
 | |
| +    if (this._pageReady)
 | |
| +      return false;
 | |
| +    this._pageReady = true;
 | |
| +    this.emit(FrameTree.Events.PageReady);
 | |
| +    return true;
 | |
| +  }
 | |
| +
 | |
| +  addScriptToEvaluateOnNewDocument(script) {
 | |
| +    const scriptId = helper.generateId();
 | |
| +    this._scriptsToEvaluateOnNewDocument.set(scriptId, script);
 | |
| +    return scriptId;
 | |
| +  }
 | |
| +
 | |
| +  removeScriptToEvaluateOnNewDocument(scriptId) {
 | |
| +    this._scriptsToEvaluateOnNewDocument.delete(scriptId);
 | |
| +  }
 | |
| +
 | |
| +  addBinding(name, script) {
 | |
| +    this._bindings.set(name, script);
 | |
| +    for (const frame of this.frames())
 | |
| +      frame._addBinding(name, script);
 | |
| +  }
 | |
| +
 | |
| +  setColorScheme(colorScheme) {
 | |
| +    const docShell = this._mainFrame._docShell;
 | |
| +    switch (colorScheme) {
 | |
| +      case 'light': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_LIGHT; break;
 | |
| +      case 'dark': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_DARK; break;
 | |
| +      case 'no-preference': docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NO_PREFERENCE; break;
 | |
| +      default: docShell.colorSchemeOverride = Ci.nsIDocShell.COLOR_SCHEME_OVERRIDE_NONE; break;
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  frameForDocShell(docShell) {
 | |
| +    return this._docShellToFrame.get(docShell) || null;
 | |
| +  }
 | |
| +
 | |
| +  frame(frameId) {
 | |
| +    return this._frameIdToFrame.get(frameId) || null;
 | |
| +  }
 | |
| +
 | |
| +  frames() {
 | |
| +    let result = [];
 | |
| +    collect(this._mainFrame);
 | |
| +    return result;
 | |
| +
 | |
| +    function collect(frame) {
 | |
| +      result.push(frame);
 | |
| +      for (const subframe of frame._children)
 | |
| +        collect(subframe);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  mainFrame() {
 | |
| +    return this._mainFrame;
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._browsingContextGroup.__jugglerFrameTrees.delete(this);
 | |
| +    this._wdm.removeListener(this._wdmListener);
 | |
| +    this._runtime.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +
 | |
| +  onStateChange(progress, request, flag, status) {
 | |
| +    if (!(request instanceof Ci.nsIChannel))
 | |
| +      return;
 | |
| +    const channel = request.QueryInterface(Ci.nsIChannel);
 | |
| +    const docShell = progress.DOMWindow.docShell;
 | |
| +    const frame = this._docShellToFrame.get(docShell);
 | |
| +    if (!frame) {
 | |
| +      dump(`ERROR: got a state changed event for un-tracked docshell!\n`);
 | |
| +      return;
 | |
| +    }
 | |
| +
 | |
| +    const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
 | |
| +    const isTransferring = flag & Ci.nsIWebProgressListener.STATE_TRANSFERRING;
 | |
| +    const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
 | |
| +
 | |
| +    let isDownload = false;
 | |
| +    try {
 | |
| +      isDownload = (channel.contentDisposition === Ci.nsIChannel.DISPOSITION_ATTACHMENT);
 | |
| +    } catch(e) {
 | |
| +      // The method is expected to throw if it's not an attachment.
 | |
| +    }
 | |
| +
 | |
| +    if (isStart) {
 | |
| +      // Starting a new navigation.
 | |
| +      frame._pendingNavigationId = this._channelId(channel);
 | |
| +      frame._pendingNavigationURL = channel.URI.spec;
 | |
| +      this.emit(FrameTree.Events.NavigationStarted, frame);
 | |
| +    } else if (isTransferring || (isStop && frame._pendingNavigationId && !status && !isDownload)) {
 | |
| +      // Navigation is committed.
 | |
| +      for (const subframe of frame._children)
 | |
| +        this._detachFrame(subframe);
 | |
| +      const navigationId = frame._pendingNavigationId;
 | |
| +      frame._pendingNavigationId = null;
 | |
| +      frame._pendingNavigationURL = null;
 | |
| +      frame._lastCommittedNavigationId = navigationId;
 | |
| +      frame._url = channel.URI.spec;
 | |
| +      this.emit(FrameTree.Events.NavigationCommitted, frame);
 | |
| +      if (frame === this._mainFrame)
 | |
| +        this.forcePageReady();
 | |
| +    } else if (isStop && frame._pendingNavigationId && (status || isDownload)) {
 | |
| +      // Navigation is aborted.
 | |
| +      const navigationId = frame._pendingNavigationId;
 | |
| +      frame._pendingNavigationId = null;
 | |
| +      frame._pendingNavigationURL = null;
 | |
| +      // Always report download navigation as failure to match other browsers.
 | |
| +      const errorText = isDownload ? 'Will download to file' : helper.getNetworkErrorStatusText(status);
 | |
| +      this.emit(FrameTree.Events.NavigationAborted, frame, navigationId, errorText);
 | |
| +      if (frame === this._mainFrame && status !== Cr.NS_BINDING_ABORTED && !isDownload)
 | |
| +        this.forcePageReady();
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  onFrameLocationChange(progress, request, location, flags) {
 | |
| +    const docShell = progress.DOMWindow.docShell;
 | |
| +    const frame = this._docShellToFrame.get(docShell);
 | |
| +    const sameDocumentNavigation = !!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
 | |
| +    if (frame && sameDocumentNavigation) {
 | |
| +      frame._url = location.spec;
 | |
| +      this.emit(FrameTree.Events.SameDocumentNavigation, frame);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  _channelId(channel) {
 | |
| +    if (channel instanceof Ci.nsIHttpChannel) {
 | |
| +      const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +      return String(httpChannel.channelId);
 | |
| +    }
 | |
| +    return helper.generateId();
 | |
| +  }
 | |
| +
 | |
| +  _onDocShellCreated(docShell) {
 | |
| +    // Bug 1142752: sometimes, the docshell appears to be immediately
 | |
| +    // destroyed, bailout early to prevent random exceptions.
 | |
| +    if (docShell.isBeingDestroyed())
 | |
| +      return;
 | |
| +    // If this docShell doesn't belong to our frame tree - do nothing.
 | |
| +    let root = docShell;
 | |
| +    while (root.parent)
 | |
| +      root = root.parent;
 | |
| +    if (root === this._mainFrame._docShell)
 | |
| +      this._createFrame(docShell);
 | |
| +  }
 | |
| +
 | |
| +  _createFrame(docShell) {
 | |
| +    const parentFrame = this._docShellToFrame.get(docShell.parent) || null;
 | |
| +    const frame = new Frame(this, this._runtime, docShell, parentFrame);
 | |
| +    this._docShellToFrame.set(docShell, frame);
 | |
| +    this._frameIdToFrame.set(frame.id(), frame);
 | |
| +    this.emit(FrameTree.Events.FrameAttached, frame);
 | |
| +    // Create execution context **after** reporting frame.
 | |
| +    // This is our protocol contract.
 | |
| +    if (frame.domWindow())
 | |
| +      frame._onGlobalObjectCleared();
 | |
| +    return frame;
 | |
| +  }
 | |
| +
 | |
| +  _onDocShellDestroyed(docShell) {
 | |
| +    const frame = this._docShellToFrame.get(docShell);
 | |
| +    if (frame)
 | |
| +      this._detachFrame(frame);
 | |
| +  }
 | |
| +
 | |
| +  _detachFrame(frame) {
 | |
| +    // Detach all children first
 | |
| +    for (const subframe of frame._children)
 | |
| +      this._detachFrame(subframe);
 | |
| +    this._docShellToFrame.delete(frame._docShell);
 | |
| +    this._frameIdToFrame.delete(frame.id());
 | |
| +    if (frame._parentFrame)
 | |
| +      frame._parentFrame._children.delete(frame);
 | |
| +    frame._parentFrame = null;
 | |
| +    frame.dispose();
 | |
| +    this.emit(FrameTree.Events.FrameDetached, frame);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +FrameTree.Events = {
 | |
| +  BindingCalled: 'bindingcalled',
 | |
| +  FrameAttached: 'frameattached',
 | |
| +  FrameDetached: 'framedetached',
 | |
| +  GlobalObjectCreated: 'globalobjectcreated',
 | |
| +  WorkerCreated: 'workercreated',
 | |
| +  WorkerDestroyed: 'workerdestroyed',
 | |
| +  NavigationStarted: 'navigationstarted',
 | |
| +  NavigationCommitted: 'navigationcommitted',
 | |
| +  NavigationAborted: 'navigationaborted',
 | |
| +  SameDocumentNavigation: 'samedocumentnavigation',
 | |
| +  PageReady: 'pageready',
 | |
| +};
 | |
| +
 | |
| +class Frame {
 | |
| +  constructor(frameTree, runtime, docShell, parentFrame) {
 | |
| +    this._frameTree = frameTree;
 | |
| +    this._runtime = runtime;
 | |
| +    this._docShell = docShell;
 | |
| +    this._children = new Set();
 | |
| +    this._frameId = helper.generateId();
 | |
| +    this._parentFrame = null;
 | |
| +    this._url = '';
 | |
| +    if (docShell.domWindow && docShell.domWindow.location)
 | |
| +      this._url = docShell.domWindow.location.href;
 | |
| +    if (parentFrame) {
 | |
| +      this._parentFrame = parentFrame;
 | |
| +      parentFrame._children.add(this);
 | |
| +    }
 | |
| +
 | |
| +    this._lastCommittedNavigationId = null;
 | |
| +    this._pendingNavigationId = null;
 | |
| +    this._pendingNavigationURL = null;
 | |
| +
 | |
| +    this._textInputProcessor = null;
 | |
| +    this._executionContext = null;
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    if (this._executionContext)
 | |
| +      this._runtime.destroyExecutionContext(this._executionContext);
 | |
| +    this._executionContext = null;
 | |
| +  }
 | |
| +
 | |
| +  _addBinding(name, script) {
 | |
| +    Cu.exportFunction((...args) => {
 | |
| +      this._frameTree.emit(FrameTree.Events.BindingCalled, {
 | |
| +        frame: this,
 | |
| +        name,
 | |
| +        payload: args[0]
 | |
| +      });
 | |
| +    }, this.domWindow(), {
 | |
| +      defineAs: name,
 | |
| +    });
 | |
| +    this.domWindow().eval(script);
 | |
| +  }
 | |
| +
 | |
| +  _onGlobalObjectCleared() {
 | |
| +    if (this._executionContext)
 | |
| +      this._runtime.destroyExecutionContext(this._executionContext);
 | |
| +    this._executionContext = this._runtime.createExecutionContext(this.domWindow(), this.domWindow(), {
 | |
| +      frameId: this._frameId,
 | |
| +      name: '',
 | |
| +    });
 | |
| +    for (const [name, script] of this._frameTree._bindings)
 | |
| +      this._addBinding(name, script);
 | |
| +    for (const script of this._frameTree._scriptsToEvaluateOnNewDocument.values()) {
 | |
| +      try {
 | |
| +        const result = this._executionContext.evaluateScript(script);
 | |
| +        if (result && result.objectId)
 | |
| +          this._executionContext.disposeObject(result.objectId);
 | |
| +      } catch (e) {
 | |
| +        dump(`ERROR: ${e.message}\n${e.stack}\n`);
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  executionContext() {
 | |
| +    return this._executionContext;
 | |
| +  }
 | |
| +
 | |
| +  textInputProcessor() {
 | |
| +    if (!this._textInputProcessor) {
 | |
| +      this._textInputProcessor = Cc["@mozilla.org/text-input-processor;1"].createInstance(Ci.nsITextInputProcessor);
 | |
| +      this._textInputProcessor.beginInputTransactionForTests(this._docShell.DOMWindow);
 | |
| +    }
 | |
| +    return this._textInputProcessor;
 | |
| +  }
 | |
| +
 | |
| +  pendingNavigationId() {
 | |
| +    return this._pendingNavigationId;
 | |
| +  }
 | |
| +
 | |
| +  pendingNavigationURL() {
 | |
| +    return this._pendingNavigationURL;
 | |
| +  }
 | |
| +
 | |
| +  lastCommittedNavigationId() {
 | |
| +    return this._lastCommittedNavigationId;
 | |
| +  }
 | |
| +
 | |
| +  docShell() {
 | |
| +    return this._docShell;
 | |
| +  }
 | |
| +
 | |
| +  domWindow() {
 | |
| +    return this._docShell.domWindow;
 | |
| +  }
 | |
| +
 | |
| +  name() {
 | |
| +    const frameElement = this._docShell.domWindow.frameElement;
 | |
| +    let name = '';
 | |
| +    if (frameElement)
 | |
| +      name = frameElement.getAttribute('name') || frameElement.getAttribute('id') || '';
 | |
| +    return name;
 | |
| +  }
 | |
| +
 | |
| +  parentFrame() {
 | |
| +    return this._parentFrame;
 | |
| +  }
 | |
| +
 | |
| +  id() {
 | |
| +    return this._frameId;
 | |
| +  }
 | |
| +
 | |
| +  url() {
 | |
| +    return this._url;
 | |
| +  }
 | |
| +
 | |
| +}
 | |
| +
 | |
| +class Worker {
 | |
| +  constructor(frame, workerDebugger) {
 | |
| +    this._frame = frame;
 | |
| +    this._workerId = helper.generateId();
 | |
| +    this._workerDebugger = workerDebugger;
 | |
| +
 | |
| +    workerDebugger.initialize('chrome://juggler/content/content/WorkerMain.js');
 | |
| +
 | |
| +    this._channel = new SimpleChannel(`content::worker[${this._workerId}]`);
 | |
| +    this._channel.transport = {
 | |
| +      sendMessage: obj => workerDebugger.postMessage(JSON.stringify(obj)),
 | |
| +      dispose: () => {},
 | |
| +    };
 | |
| +    this._workerDebuggerListener = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsIWorkerDebuggerListener]),
 | |
| +      onMessage: msg => void this._channel._onMessage(JSON.parse(msg)),
 | |
| +      onClose: () => void this._channel.dispose(),
 | |
| +      onError: (filename, lineno, message) => {
 | |
| +        dump(`Error in worker: ${message} @${filename}:${lineno}\n`);
 | |
| +      },
 | |
| +    };
 | |
| +    workerDebugger.addListener(this._workerDebuggerListener);
 | |
| +  }
 | |
| +
 | |
| +  channel() {
 | |
| +    return this._channel;
 | |
| +  }
 | |
| +
 | |
| +  frame() {
 | |
| +    return this._frame;
 | |
| +  }
 | |
| +
 | |
| +  id() {
 | |
| +    return this._workerId;
 | |
| +  }
 | |
| +
 | |
| +  url() {
 | |
| +    return this._workerDebugger.url;
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._channel.dispose();
 | |
| +    this._workerDebugger.removeListener(this._workerDebuggerListener);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['FrameTree'];
 | |
| +this.FrameTree = FrameTree;
 | |
| +
 | |
| diff --git a/juggler/content/NetworkMonitor.js b/juggler/content/NetworkMonitor.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..155d0770ddf704728829272a41a31ce8c9509a25
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/NetworkMonitor.js
 | |
| @@ -0,0 +1,48 @@
 | |
| +"use strict";
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cr = Components.results;
 | |
| +const Cu = Components.utils;
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class NetworkMonitor {
 | |
| +  constructor(rootDocShell, frameTree) {
 | |
| +    this._frameTree = frameTree;
 | |
| +    this._requestDetails = new Map();
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      helper.addObserver(this._onRequest.bind(this), 'http-on-opening-request'),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  _onRequest(channel) {
 | |
| +    if (!(channel instanceof Ci.nsIHttpChannel))
 | |
| +      return;
 | |
| +    const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
 | |
| +    const loadContext = helper.getLoadContext(httpChannel);
 | |
| +    if (!loadContext)
 | |
| +      return;
 | |
| +    const window = loadContext.associatedWindow;
 | |
| +    const frame = this._frameTree.frameForDocShell(window.docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._requestDetails.set(httpChannel.channelId, {
 | |
| +      frameId: frame.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  requestDetails(channelId) {
 | |
| +    return this._requestDetails.get(channelId) || null;
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._requestDetails.clear();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['NetworkMonitor'];
 | |
| +this.NetworkMonitor = NetworkMonitor;
 | |
| +
 | |
| diff --git a/juggler/content/PageAgent.js b/juggler/content/PageAgent.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..7828bbea9a32fc7bf161c1bc814b900bdc19a2a9
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/PageAgent.js
 | |
| @@ -0,0 +1,977 @@
 | |
| +"use strict";
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cr = Components.results;
 | |
| +const Cu = Components.utils;
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {NetUtil} = ChromeUtils.import('resource://gre/modules/NetUtil.jsm');
 | |
| +const dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
 | |
| +  Ci.nsIDragService
 | |
| +);
 | |
| +const obs = Cc["@mozilla.org/observer-service;1"].getService(
 | |
| +  Ci.nsIObserverService
 | |
| +);
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class WorkerData {
 | |
| +  constructor(pageAgent, browserChannel, sessionId, worker) {
 | |
| +    this._workerRuntime = worker.channel().connect(sessionId + 'runtime');
 | |
| +    this._browserWorker = browserChannel.connect(sessionId + worker.id());
 | |
| +    this._worker = worker;
 | |
| +    this._sessionId = sessionId;
 | |
| +    const emit = name => {
 | |
| +      return (...args) => this._browserWorker.emit(name, ...args);
 | |
| +    };
 | |
| +    this._eventListeners = [
 | |
| +      worker.channel().register(sessionId + 'runtime', {
 | |
| +        runtimeConsole: emit('runtimeConsole'),
 | |
| +        runtimeExecutionContextCreated: emit('runtimeExecutionContextCreated'),
 | |
| +        runtimeExecutionContextDestroyed: emit('runtimeExecutionContextDestroyed'),
 | |
| +      }),
 | |
| +      browserChannel.register(sessionId + worker.id(), {
 | |
| +        evaluate: (options) => this._workerRuntime.send('evaluate', options),
 | |
| +        callFunction: (options) => this._workerRuntime.send('callFunction', options),
 | |
| +        getObjectProperties: (options) => this._workerRuntime.send('getObjectProperties', options),
 | |
| +        disposeObject: (options) =>this._workerRuntime.send('disposeObject', options),
 | |
| +      }),
 | |
| +    ];
 | |
| +    worker.channel().connect('').emit('attach', {sessionId});
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._worker.channel().connect('').emit('detach', {sessionId: this._sessionId});
 | |
| +    this._workerRuntime.dispose();
 | |
| +    this._browserWorker.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class FrameData {
 | |
| +  constructor(agent, runtime, frame) {
 | |
| +    this._agent = agent;
 | |
| +    this._runtime = runtime;
 | |
| +    this._frame = frame;
 | |
| +    this._isolatedWorlds = new Map();
 | |
| +    this.reset();
 | |
| +  }
 | |
| +
 | |
| +  reset() {
 | |
| +    for (const world of this._isolatedWorlds.values())
 | |
| +      this._runtime.destroyExecutionContext(world);
 | |
| +    this._isolatedWorlds.clear();
 | |
| +
 | |
| +    for (const {script, worldName} of this._agent._isolatedWorlds.values()) {
 | |
| +      const context = worldName ? this.createIsolatedWorld(worldName) : this._frame.executionContext();
 | |
| +      try {
 | |
| +        let result = context.evaluateScript(script);
 | |
| +        if (result && result.objectId)
 | |
| +          context.disposeObject(result.objectId);
 | |
| +      } catch (e) {
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  createIsolatedWorld(name) {
 | |
| +    const principal = [this._frame.domWindow()]; // extended principal
 | |
| +    const sandbox = Cu.Sandbox(principal, {
 | |
| +      sandboxPrototype: this._frame.domWindow(),
 | |
| +      wantComponents: false,
 | |
| +      wantExportHelpers: false,
 | |
| +      wantXrays: true,
 | |
| +    });
 | |
| +    const world = this._runtime.createExecutionContext(this._frame.domWindow(), sandbox, {
 | |
| +      frameId: this._frame.id(),
 | |
| +      name,
 | |
| +    });
 | |
| +    this._isolatedWorlds.set(world.id(), world);
 | |
| +    return world;
 | |
| +  }
 | |
| +
 | |
| +  unsafeObject(objectId) {
 | |
| +    const contexts = [this._frame.executionContext(), ...this._isolatedWorlds.values()];
 | |
| +    for (const context of contexts) {
 | |
| +      const result = context.unsafeObject(objectId);
 | |
| +      if (result)
 | |
| +        return result.object;
 | |
| +    }
 | |
| +    throw new Error('Cannot find object with id = ' + objectId);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    for (const world of this._isolatedWorlds.values())
 | |
| +      this._runtime.destroyExecutionContext(world);
 | |
| +    this._isolatedWorlds.clear();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class PageAgent {
 | |
| +  constructor(messageManager, browserChannel, sessionId, frameTree, networkMonitor) {
 | |
| +    this._messageManager = messageManager;
 | |
| +    this._browserChannel = browserChannel;
 | |
| +    this._sessionId = sessionId;
 | |
| +    this._browserPage = browserChannel.connect(sessionId + 'page');
 | |
| +    this._browserRuntime = browserChannel.connect(sessionId + 'runtime');
 | |
| +    this._frameTree = frameTree;
 | |
| +    this._runtime = frameTree.runtime();
 | |
| +    this._networkMonitor = networkMonitor;
 | |
| +
 | |
| +    this._frameData = new Map();
 | |
| +    this._workerData = new Map();
 | |
| +    this._scriptsToEvaluateOnNewDocument = new Map();
 | |
| +    this._isolatedWorlds = new Map();
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      browserChannel.register(sessionId + 'page', {
 | |
| +        addBinding: ({ name, script }) => this._frameTree.addBinding(name, script),
 | |
| +        addScriptToEvaluateOnNewDocument: this._addScriptToEvaluateOnNewDocument.bind(this),
 | |
| +        adoptNode: this._adoptNode.bind(this),
 | |
| +        crash: this._crash.bind(this),
 | |
| +        describeNode: this._describeNode.bind(this),
 | |
| +        dispatchKeyEvent: this._dispatchKeyEvent.bind(this),
 | |
| +        dispatchMouseEvent: this._dispatchMouseEvent.bind(this),
 | |
| +        dispatchTouchEvent: this._dispatchTouchEvent.bind(this),
 | |
| +        getBoundingBox: this._getBoundingBox.bind(this),
 | |
| +        getContentQuads: this._getContentQuads.bind(this),
 | |
| +        getFullAXTree: this._getFullAXTree.bind(this),
 | |
| +        goBack: this._goBack.bind(this),
 | |
| +        goForward: this._goForward.bind(this),
 | |
| +        insertText: this._insertText.bind(this),
 | |
| +        navigate: this._navigate.bind(this),
 | |
| +        reload: this._reload.bind(this),
 | |
| +        removeScriptToEvaluateOnNewDocument: this._removeScriptToEvaluateOnNewDocument.bind(this),
 | |
| +        requestDetails: this._requestDetails.bind(this),
 | |
| +        screenshot: this._screenshot.bind(this),
 | |
| +        scrollIntoViewIfNeeded: this._scrollIntoViewIfNeeded.bind(this),
 | |
| +        setCacheDisabled: this._setCacheDisabled.bind(this),
 | |
| +        setEmulatedMedia: this._setEmulatedMedia.bind(this),
 | |
| +        setFileInputFiles: this._setFileInputFiles.bind(this),
 | |
| +        setInterceptFileChooserDialog: this._setInterceptFileChooserDialog.bind(this),
 | |
| +      }),
 | |
| +      browserChannel.register(sessionId + 'runtime', {
 | |
| +        evaluate: this._runtime.evaluate.bind(this._runtime),
 | |
| +        callFunction: this._runtime.callFunction.bind(this._runtime),
 | |
| +        getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
 | |
| +        disposeObject: this._runtime.disposeObject.bind(this._runtime),
 | |
| +      }),
 | |
| +    ];
 | |
| +    this._enabled = false;
 | |
| +
 | |
| +    const docShell = frameTree.mainFrame().docShell();
 | |
| +    this._docShell = docShell;
 | |
| +    this._initialDPPX = docShell.contentViewer.overrideDPPX;
 | |
| +    this._customScrollbars = null;
 | |
| +    this._dataTransfer = null;
 | |
| +  }
 | |
| +
 | |
| +  _requestDetails({channelId}) {
 | |
| +    return this._networkMonitor.requestDetails(channelId);
 | |
| +  }
 | |
| +
 | |
| +  async _setEmulatedMedia({type, colorScheme}) {
 | |
| +    const docShell = this._frameTree.mainFrame().docShell();
 | |
| +    const cv = docShell.contentViewer;
 | |
| +    if (type === '')
 | |
| +      cv.stopEmulatingMedium();
 | |
| +    else if (type)
 | |
| +      cv.emulateMedium(type);
 | |
| +    this._frameTree.setColorScheme(colorScheme);
 | |
| +  }
 | |
| +
 | |
| +  _addScriptToEvaluateOnNewDocument({script, worldName}) {
 | |
| +    if (worldName)
 | |
| +      return this._createIsolatedWorld({script, worldName});
 | |
| +    return {scriptId: this._frameTree.addScriptToEvaluateOnNewDocument(script)};
 | |
| +  }
 | |
| +
 | |
| +  _createIsolatedWorld({script, worldName}) {
 | |
| +    const scriptId = helper.generateId();
 | |
| +    this._isolatedWorlds.set(scriptId, {script, worldName});
 | |
| +    for (const frameData of this._frameData.values())
 | |
| +      frameData.createIsolatedWorld(worldName);
 | |
| +    return {scriptId};
 | |
| +  }
 | |
| +
 | |
| +  _removeScriptToEvaluateOnNewDocument({scriptId}) {
 | |
| +    if (this._isolatedWorlds.has(scriptId))
 | |
| +      this._isolatedWorlds.delete(scriptId);
 | |
| +    else
 | |
| +      this._frameTree.removeScriptToEvaluateOnNewDocument(scriptId);
 | |
| +  }
 | |
| +
 | |
| +  _setCacheDisabled({cacheDisabled}) {
 | |
| +    const enable = Ci.nsIRequest.LOAD_NORMAL;
 | |
| +    const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
 | |
| +                  Ci.nsIRequest.INHIBIT_CACHING;
 | |
| +
 | |
| +    const docShell = this._frameTree.mainFrame().docShell();
 | |
| +    docShell.defaultLoadFlags = cacheDisabled ? disable : enable;
 | |
| +  }
 | |
| +
 | |
| +  enable() {
 | |
| +    if (this._enabled)
 | |
| +      return;
 | |
| +
 | |
| +    this._enabled = true;
 | |
| +    // Dispatch frameAttached events for all initial frames
 | |
| +    for (const frame of this._frameTree.frames()) {
 | |
| +      this._onFrameAttached(frame);
 | |
| +      if (frame.url())
 | |
| +        this._onNavigationCommitted(frame);
 | |
| +      if (frame.pendingNavigationId())
 | |
| +        this._onNavigationStarted(frame);
 | |
| +    }
 | |
| +
 | |
| +    for (const worker of this._frameTree.workers())
 | |
| +      this._onWorkerCreated(worker);
 | |
| +
 | |
| +    this._eventListeners.push(...[
 | |
| +      helper.addObserver(this._linkClicked.bind(this, false), 'juggler-link-click'),
 | |
| +      helper.addObserver(this._linkClicked.bind(this, true), 'juggler-link-click-sync'),
 | |
| +      helper.addObserver(this._onWindowOpenInNewContext.bind(this), 'juggler-window-open-in-new-context'),
 | |
| +      helper.addObserver(this._filePickerShown.bind(this), 'juggler-file-picker-shown'),
 | |
| +      helper.addEventListener(this._messageManager, 'DOMContentLoaded', this._onDOMContentLoaded.bind(this)),
 | |
| +      helper.addEventListener(this._messageManager, 'pageshow', this._onLoad.bind(this)),
 | |
| +      helper.addObserver(this._onDocumentOpenLoad.bind(this), 'juggler-document-open-loaded'),
 | |
| +      helper.addEventListener(this._messageManager, 'error', this._onError.bind(this)),
 | |
| +      helper.on(this._frameTree, 'bindingcalled', this._onBindingCalled.bind(this)),
 | |
| +      helper.on(this._frameTree, 'frameattached', this._onFrameAttached.bind(this)),
 | |
| +      helper.on(this._frameTree, 'framedetached', this._onFrameDetached.bind(this)),
 | |
| +      helper.on(this._frameTree, 'globalobjectcreated', this._onGlobalObjectCreated.bind(this)),
 | |
| +      helper.on(this._frameTree, 'navigationstarted', this._onNavigationStarted.bind(this)),
 | |
| +      helper.on(this._frameTree, 'navigationcommitted', this._onNavigationCommitted.bind(this)),
 | |
| +      helper.on(this._frameTree, 'navigationaborted', this._onNavigationAborted.bind(this)),
 | |
| +      helper.on(this._frameTree, 'samedocumentnavigation', this._onSameDocumentNavigation.bind(this)),
 | |
| +      helper.on(this._frameTree, 'pageready', () => this._browserPage.emit('pageReady', {})),
 | |
| +      helper.on(this._frameTree, 'workercreated', this._onWorkerCreated.bind(this)),
 | |
| +      helper.on(this._frameTree, 'workerdestroyed', this._onWorkerDestroyed.bind(this)),
 | |
| +      helper.addObserver(this._onWindowOpen.bind(this), 'webNavigation-createdNavigationTarget-from-js'),
 | |
| +      this._runtime.events.onErrorFromWorker((domWindow, message, stack) => {
 | |
| +        const frame = this._frameTree.frameForDocShell(domWindow.docShell);
 | |
| +        if (!frame)
 | |
| +          return;
 | |
| +        this._browserPage.emit('pageUncaughtError', {
 | |
| +          frameId: frame.id(),
 | |
| +          message,
 | |
| +          stack,
 | |
| +        });
 | |
| +      }),
 | |
| +      this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
 | |
| +      this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
 | |
| +      this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
 | |
| +    ]);
 | |
| +    for (const context of this._runtime.executionContexts())
 | |
| +      this._onExecutionContextCreated(context);
 | |
| +
 | |
| +    if (this._frameTree.isPageReady())
 | |
| +      this._browserPage.emit('pageReady', {});
 | |
| +  }
 | |
| +
 | |
| +  _onExecutionContextCreated(executionContext) {
 | |
| +    this._browserRuntime.emit('runtimeExecutionContextCreated', {
 | |
| +      executionContextId: executionContext.id(),
 | |
| +      auxData: executionContext.auxData(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onExecutionContextDestroyed(executionContext) {
 | |
| +    this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
 | |
| +      executionContextId: executionContext.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerCreated(worker) {
 | |
| +    const workerData = new WorkerData(this, this._browserChannel, this._sessionId, worker);
 | |
| +    this._workerData.set(worker.id(), workerData);
 | |
| +    this._browserPage.emit('pageWorkerCreated', {
 | |
| +      workerId: worker.id(),
 | |
| +      frameId: worker.frame().id(),
 | |
| +      url: worker.url(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerDestroyed(worker) {
 | |
| +    const workerData = this._workerData.get(worker.id());
 | |
| +    if (!workerData)
 | |
| +      return;
 | |
| +    this._workerData.delete(worker.id());
 | |
| +    workerData.dispose();
 | |
| +    this._browserPage.emit('pageWorkerDestroyed', {
 | |
| +      workerId: worker.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onWindowOpen(subject) {
 | |
| +    if (!(subject instanceof Ci.nsIPropertyBag2))
 | |
| +      return;
 | |
| +    const props = subject.QueryInterface(Ci.nsIPropertyBag2);
 | |
| +    const hasUrl = props.hasKey('url');
 | |
| +    const createdDocShell = props.getPropertyAsInterface('createdTabDocShell', Ci.nsIDocShell);
 | |
| +    if (!hasUrl && createdDocShell === this._docShell && this._frameTree.forcePageReady()) {
 | |
| +      this._browserPage.emit('pageEventFired', {
 | |
| +        frameId: this._frameTree.mainFrame().id(),
 | |
| +        name: 'DOMContentLoaded',
 | |
| +      });
 | |
| +      this._browserPage.emit('pageEventFired', {
 | |
| +        frameId: this._frameTree.mainFrame().id(),
 | |
| +        name: 'load',
 | |
| +      });
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  _setInterceptFileChooserDialog({enabled}) {
 | |
| +    this._docShell.fileInputInterceptionEnabled = !!enabled;
 | |
| +  }
 | |
| +
 | |
| +  _linkClicked(sync, anchorElement) {
 | |
| +    if (anchorElement.ownerGlobal.docShell !== this._docShell)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageLinkClicked', { phase: sync ? 'after' : 'before' });
 | |
| +  }
 | |
| +
 | |
| +  _onWindowOpenInNewContext(docShell) {
 | |
| +    // TODO: unify this with _onWindowOpen if possible.
 | |
| +    const frame = this._frameTree.frameForDocShell(docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageWillOpenNewWindowAsynchronously');
 | |
| +  }
 | |
| +
 | |
| +  _filePickerShown(inputElement) {
 | |
| +    if (inputElement.ownerGlobal.docShell !== this._docShell)
 | |
| +      return;
 | |
| +    const frameData = this._findFrameForNode(inputElement);
 | |
| +    this._browserPage.emit('pageFileChooserOpened', {
 | |
| +      executionContextId: frameData._frame.executionContext().id(),
 | |
| +      element: frameData._frame.executionContext().rawValueToRemoteObject(inputElement)
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _findFrameForNode(node) {
 | |
| +    return Array.from(this._frameData.values()).find(data => {
 | |
| +      const doc = data._frame.domWindow().document;
 | |
| +      return node === doc || node.ownerDocument === doc;
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onDOMContentLoaded(event) {
 | |
| +    const docShell = event.target.ownerGlobal.docShell;
 | |
| +    const frame = this._frameTree.frameForDocShell(docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageEventFired', {
 | |
| +      frameId: frame.id(),
 | |
| +      name: 'DOMContentLoaded',
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onError(errorEvent) {
 | |
| +    const docShell = errorEvent.target.ownerGlobal.docShell;
 | |
| +    const frame = this._frameTree.frameForDocShell(docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageUncaughtError', {
 | |
| +      frameId: frame.id(),
 | |
| +      message: errorEvent.message,
 | |
| +      stack: errorEvent.error ? errorEvent.error.stack : '',
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onDocumentOpenLoad(document) {
 | |
| +    const docShell = document.ownerGlobal.docShell;
 | |
| +    const frame = this._frameTree.frameForDocShell(docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageEventFired', {
 | |
| +      frameId: frame.id(),
 | |
| +      name: 'load'
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onLoad(event) {
 | |
| +    const docShell = event.target.ownerGlobal.docShell;
 | |
| +    const frame = this._frameTree.frameForDocShell(docShell);
 | |
| +    if (!frame)
 | |
| +      return;
 | |
| +    this._browserPage.emit('pageEventFired', {
 | |
| +      frameId: frame.id(),
 | |
| +      name: 'load'
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onNavigationStarted(frame) {
 | |
| +    this._browserPage.emit('pageNavigationStarted', {
 | |
| +      frameId: frame.id(),
 | |
| +      navigationId: frame.pendingNavigationId(),
 | |
| +      url: frame.pendingNavigationURL(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onNavigationAborted(frame, navigationId, errorText) {
 | |
| +    this._browserPage.emit('pageNavigationAborted', {
 | |
| +      frameId: frame.id(),
 | |
| +      navigationId,
 | |
| +      errorText,
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onSameDocumentNavigation(frame) {
 | |
| +    this._browserPage.emit('pageSameDocumentNavigation', {
 | |
| +      frameId: frame.id(),
 | |
| +      url: frame.url(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onNavigationCommitted(frame) {
 | |
| +    this._browserPage.emit('pageNavigationCommitted', {
 | |
| +      frameId: frame.id(),
 | |
| +      navigationId: frame.lastCommittedNavigationId() || undefined,
 | |
| +      url: frame.url(),
 | |
| +      name: frame.name(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onGlobalObjectCreated({ frame }) {
 | |
| +    this._frameData.get(frame).reset();
 | |
| +  }
 | |
| +
 | |
| +  _onFrameAttached(frame) {
 | |
| +    this._browserPage.emit('pageFrameAttached', {
 | |
| +      frameId: frame.id(),
 | |
| +      parentFrameId: frame.parentFrame() ? frame.parentFrame().id() : undefined,
 | |
| +    });
 | |
| +    this._frameData.set(frame, new FrameData(this, this._runtime, frame));
 | |
| +  }
 | |
| +
 | |
| +  _onFrameDetached(frame) {
 | |
| +    this._frameData.delete(frame);
 | |
| +    this._browserPage.emit('pageFrameDetached', {
 | |
| +      frameId: frame.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onBindingCalled({frame, name, payload}) {
 | |
| +    this._browserPage.emit('pageBindingCalled', {
 | |
| +      executionContextId: frame.executionContext().id(),
 | |
| +      name,
 | |
| +      payload
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    for (const workerData of this._workerData.values())
 | |
| +      workerData.dispose();
 | |
| +    this._workerData.clear();
 | |
| +    for (const frameData of this._frameData.values())
 | |
| +      frameData.dispose();
 | |
| +    this._frameData.clear();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +
 | |
| +  async _navigate({frameId, url, referer}) {
 | |
| +    try {
 | |
| +      const uri = NetUtil.newURI(url);
 | |
| +    } catch (e) {
 | |
| +      throw new Error(`Invalid url: "${url}"`);
 | |
| +    }
 | |
| +    let referrerURI = null;
 | |
| +    let referrerInfo = null;
 | |
| +    if (referer) {
 | |
| +      try {
 | |
| +        referrerURI = NetUtil.newURI(referer);
 | |
| +        const ReferrerInfo = Components.Constructor(
 | |
| +          '@mozilla.org/referrer-info;1',
 | |
| +          'nsIReferrerInfo',
 | |
| +          'init'
 | |
| +        );
 | |
| +        referrerInfo = new ReferrerInfo(Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, true, referrerURI);
 | |
| +      } catch (e) {
 | |
| +        throw new Error(`Invalid referer: "${referer}"`);
 | |
| +      }
 | |
| +    }
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
 | |
| +    docShell.loadURI(url, {
 | |
| +      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
 | |
| +      flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
 | |
| +      referrerInfo,
 | |
| +      postData: null,
 | |
| +      headers: null,
 | |
| +    });
 | |
| +    return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
 | |
| +  }
 | |
| +
 | |
| +  async _reload({frameId, url}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    const docShell = frame.docShell().QueryInterface(Ci.nsIWebNavigation);
 | |
| +    docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
 | |
| +    return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
 | |
| +  }
 | |
| +
 | |
| +  async _goBack({frameId, url}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    const docShell = frame.docShell();
 | |
| +    if (!docShell.canGoBack)
 | |
| +      return {navigationId: null, navigationURL: null};
 | |
| +    docShell.goBack();
 | |
| +    return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
 | |
| +  }
 | |
| +
 | |
| +  async _goForward({frameId, url}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    const docShell = frame.docShell();
 | |
| +    if (!docShell.canGoForward)
 | |
| +      return {navigationId: null, navigationURL: null};
 | |
| +    docShell.goForward();
 | |
| +    return {navigationId: frame.pendingNavigationId(), navigationURL: frame.pendingNavigationURL()};
 | |
| +  }
 | |
| +
 | |
| +  async _adoptNode({frameId, objectId, executionContextId}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    const context = this._runtime.findExecutionContext(executionContextId);
 | |
| +    const fromPrincipal = unsafeObject.nodePrincipal;
 | |
| +    const toFrame = this._frameTree.frame(context.auxData().frameId);
 | |
| +    const toPrincipal = toFrame.domWindow().document.nodePrincipal;
 | |
| +    if (!toPrincipal.subsumes(fromPrincipal))
 | |
| +      return { remoteObject: null };
 | |
| +    return { remoteObject: context.rawValueToRemoteObject(unsafeObject) };
 | |
| +  }
 | |
| +
 | |
| +  async _setFileInputFiles({objectId, frameId, files}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    if (!unsafeObject)
 | |
| +      throw new Error('Object is not input!');
 | |
| +    const nsFiles = await Promise.all(files.map(filePath => File.createFromFileName(filePath)));
 | |
| +    unsafeObject.mozSetFileArray(nsFiles);
 | |
| +  }
 | |
| +
 | |
| +  _getContentQuads({objectId, frameId}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    if (!unsafeObject.getBoxQuads)
 | |
| +      throw new Error('RemoteObject is not a node');
 | |
| +    const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document}).map(quad => {
 | |
| +      return {
 | |
| +        p1: {x: quad.p1.x, y: quad.p1.y},
 | |
| +        p2: {x: quad.p2.x, y: quad.p2.y},
 | |
| +        p3: {x: quad.p3.x, y: quad.p3.y},
 | |
| +        p4: {x: quad.p4.x, y: quad.p4.y},
 | |
| +      };
 | |
| +    });
 | |
| +    return {quads};
 | |
| +  }
 | |
| +
 | |
| +  _describeNode({objectId, frameId}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    const browsingContextGroup = frame.docShell().browsingContext.group;
 | |
| +    const frames = this._frameTree.allFramesInBrowsingContextGroup(browsingContextGroup);
 | |
| +    let contentFrame;
 | |
| +    let ownerFrame;
 | |
| +    for (const frame of frames) {
 | |
| +      if (unsafeObject.contentWindow && frame.docShell() === unsafeObject.contentWindow.docShell)
 | |
| +        contentFrame = frame;
 | |
| +      const document = frame.domWindow().document;
 | |
| +      if (unsafeObject === document || unsafeObject.ownerDocument === document)
 | |
| +        ownerFrame = frame;
 | |
| +    }
 | |
| +    return {
 | |
| +      contentFrameId: contentFrame ? contentFrame.id() : undefined,
 | |
| +      ownerFrameId: ownerFrame ? ownerFrame.id() : undefined,
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  async _scrollIntoViewIfNeeded({objectId, frameId, rect}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    if (!unsafeObject.isConnected)
 | |
| +      throw new Error('Node is detached from document');
 | |
| +    if (!rect)
 | |
| +      rect = { x: -1, y: -1, width: -1, height: -1};
 | |
| +    if (unsafeObject.scrollRectIntoViewIfNeeded)
 | |
| +      unsafeObject.scrollRectIntoViewIfNeeded(rect.x, rect.y, rect.width, rect.height);
 | |
| +    else
 | |
| +      throw new Error('Node type does not support scrollRectIntoViewIfNeeded');
 | |
| +  }
 | |
| +
 | |
| +  _getNodeBoundingBox(unsafeObject) {
 | |
| +    if (!unsafeObject.getBoxQuads)
 | |
| +      throw new Error('RemoteObject is not a node');
 | |
| +    const quads = unsafeObject.getBoxQuads({relativeTo: this._frameTree.mainFrame().domWindow().document});
 | |
| +    if (!quads.length)
 | |
| +      return;
 | |
| +    let x1 = Infinity;
 | |
| +    let y1 = Infinity;
 | |
| +    let x2 = -Infinity;
 | |
| +    let y2 = -Infinity;
 | |
| +    for (const quad of quads) {
 | |
| +      const boundingBox = quad.getBounds();
 | |
| +      x1 = Math.min(boundingBox.x, x1);
 | |
| +      y1 = Math.min(boundingBox.y, y1);
 | |
| +      x2 = Math.max(boundingBox.x + boundingBox.width, x2);
 | |
| +      y2 = Math.max(boundingBox.y + boundingBox.height, y2);
 | |
| +    }
 | |
| +    return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
 | |
| +  }
 | |
| +
 | |
| +  async _getBoundingBox({frameId, objectId}) {
 | |
| +    const frame = this._frameTree.frame(frameId);
 | |
| +    if (!frame)
 | |
| +      throw new Error('Failed to find frame with id = ' + frameId);
 | |
| +    const unsafeObject = this._frameData.get(frame).unsafeObject(objectId);
 | |
| +    const box = this._getNodeBoundingBox(unsafeObject);
 | |
| +    if (!box)
 | |
| +      return {boundingBox: null};
 | |
| +    return {boundingBox: {x: box.x + frame.domWindow().scrollX, y: box.y + frame.domWindow().scrollY, width: box.width, height: box.height}};
 | |
| +  }
 | |
| +
 | |
| +  async _screenshot({mimeType, fullPage, clip}) {
 | |
| +    const content = this._messageManager.content;
 | |
| +    if (clip) {
 | |
| +      const data = takeScreenshot(content, clip.x, clip.y, clip.width, clip.height, mimeType);
 | |
| +      return {data};
 | |
| +    }
 | |
| +    if (fullPage) {
 | |
| +      const rect = content.document.documentElement.getBoundingClientRect();
 | |
| +      const width = content.innerWidth + content.scrollMaxX - content.scrollMinX;
 | |
| +      const height = content.innerHeight + content.scrollMaxY - content.scrollMinY;
 | |
| +      const data = takeScreenshot(content, 0, 0, width, height, mimeType);
 | |
| +      return {data};
 | |
| +    }
 | |
| +    const data = takeScreenshot(content, content.scrollX, content.scrollY, content.innerWidth, content.innerHeight, mimeType);
 | |
| +    return {data};
 | |
| +  }
 | |
| +
 | |
| +  async _dispatchKeyEvent({type, keyCode, code, key, repeat, location, text}) {
 | |
| +    // key events don't fire if we are dragging.
 | |
| +    if (this._dataTransfer) {
 | |
| +      if (type === 'keydown' && key === 'Escape')
 | |
| +        this._cancelDragIfNeeded();
 | |
| +      return;
 | |
| +    }
 | |
| +    const frame = this._frameTree.mainFrame();
 | |
| +    const tip = frame.textInputProcessor();
 | |
| +    if (key === 'Meta' && Services.appinfo.OS !== 'Darwin')
 | |
| +      key = 'OS';
 | |
| +    else if (key === 'OS' && Services.appinfo.OS === 'Darwin')
 | |
| +      key = 'Meta';
 | |
| +    let keyEvent = new (frame.domWindow().KeyboardEvent)("", {
 | |
| +      key,
 | |
| +      code,
 | |
| +      location,
 | |
| +      repeat,
 | |
| +      keyCode
 | |
| +    });
 | |
| +    if (type === 'keydown') {
 | |
| +      if (text && text !== key) {
 | |
| +        tip.commitCompositionWith(text, keyEvent);
 | |
| +      } else {
 | |
| +        const flags = 0;
 | |
| +        tip.keydown(keyEvent, flags);
 | |
| +      }
 | |
| +    } else if (type === 'keyup') {
 | |
| +      if (text)
 | |
| +        throw new Error(`keyup does not support text option`);
 | |
| +      const flags = 0;
 | |
| +      tip.keyup(keyEvent, flags);
 | |
| +    } else {
 | |
| +      throw new Error(`Unknown type ${type}`);
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  async _dispatchTouchEvent({type, touchPoints, modifiers}) {
 | |
| +    const frame = this._frameTree.mainFrame();
 | |
| +    const defaultPrevented = frame.domWindow().windowUtils.sendTouchEvent(
 | |
| +      type.toLowerCase(),
 | |
| +      touchPoints.map((point, id) => id),
 | |
| +      touchPoints.map(point => point.x),
 | |
| +      touchPoints.map(point => point.y),
 | |
| +      touchPoints.map(point => point.radiusX === undefined ? 1.0 : point.radiusX),
 | |
| +      touchPoints.map(point => point.radiusY === undefined ? 1.0 : point.radiusY),
 | |
| +      touchPoints.map(point => point.rotationAngle === undefined ? 0.0 : point.rotationAngle),
 | |
| +      touchPoints.map(point => point.force === undefined ? 1.0 : point.force),
 | |
| +      touchPoints.length,
 | |
| +      modifiers);
 | |
| +    return {defaultPrevented};
 | |
| +  }
 | |
| +
 | |
| +  _startDragSessionIfNeeded() {
 | |
| +    const sess = dragService.getCurrentSession();
 | |
| +    if (sess) return;
 | |
| +    dragService.startDragSessionForTests(
 | |
| +      Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
 | |
| +        Ci.nsIDragService.DRAGDROP_ACTION_COPY |
 | |
| +        Ci.nsIDragService.DRAGDROP_ACTION_LINK
 | |
| +    );
 | |
| +  }
 | |
| +
 | |
| +  _simulateDragEvent(type, x, y, modifiers) {
 | |
| +    const window = this._frameTree.mainFrame().domWindow();
 | |
| +    const element = window.windowUtils.elementFromPoint(x, y, false, false);
 | |
| +    const event = window.document.createEvent('DragEvent');
 | |
| +
 | |
| +    event.initDragEvent(
 | |
| +      type,
 | |
| +      true /* bubble */,
 | |
| +      true /* cancelable */,
 | |
| +      window,
 | |
| +      0 /* clickCount */,
 | |
| +      window.mozInnerScreenX + x,
 | |
| +      window.mozInnerScreenY + y,
 | |
| +      x,
 | |
| +      y,
 | |
| +      modifiers & 2 /* ctrlkey */,
 | |
| +      modifiers & 1 /* altKey */,
 | |
| +      modifiers & 4 /* shiftKey */,
 | |
| +      modifiers & 8 /* metaKey */,
 | |
| +      0 /* button */, // firefox always has the button as 0 on drops, regardless of which was pressed
 | |
| +      null /* relatedTarget */,
 | |
| +      this._dataTransfer
 | |
| +    );
 | |
| +
 | |
| +    window.windowUtils.dispatchDOMEventViaPresShellForTesting(element, event);
 | |
| +    if (type === 'drop')
 | |
| +      dragService.endDragSession(true);
 | |
| +  }
 | |
| +
 | |
| +  _cancelDragIfNeeded() {
 | |
| +    this._dataTransfer = null;
 | |
| +    const sess = dragService.getCurrentSession();
 | |
| +    if (sess)
 | |
| +      dragService.endDragSession(false);
 | |
| +  }
 | |
| +
 | |
| +  async _dispatchMouseEvent({type, x, y, button, clickCount, modifiers, buttons}) {
 | |
| +    this._startDragSessionIfNeeded();
 | |
| +    const trapDrag = subject => {
 | |
| +      this._dataTransfer = subject.mozCloneForEvent('drop');
 | |
| +    }
 | |
| +
 | |
| +    const frame = this._frameTree.mainFrame();
 | |
| +
 | |
| +    obs.addObserver(trapDrag, 'on-datatransfer-available');
 | |
| +    frame.domWindow().windowUtils.sendMouseEvent(
 | |
| +      type,
 | |
| +      x,
 | |
| +      y,
 | |
| +      button,
 | |
| +      clickCount,
 | |
| +      modifiers,
 | |
| +      false /*aIgnoreRootScrollFrame*/,
 | |
| +      undefined /*pressure*/,
 | |
| +      undefined /*inputSource*/,
 | |
| +      undefined /*isDOMEventSynthesized*/,
 | |
| +      undefined /*isWidgetEventSynthesized*/,
 | |
| +      buttons);
 | |
| +    obs.removeObserver(trapDrag, 'on-datatransfer-available');
 | |
| +
 | |
| +    if (type === 'mousedown' && button === 2) {
 | |
| +      frame.domWindow().windowUtils.sendMouseEvent(
 | |
| +        'contextmenu',
 | |
| +        x,
 | |
| +        y,
 | |
| +        button,
 | |
| +        clickCount,
 | |
| +        modifiers,
 | |
| +        false /*aIgnoreRootScrollFrame*/,
 | |
| +        undefined /*pressure*/,
 | |
| +        undefined /*inputSource*/,
 | |
| +        undefined /*isDOMEventSynthesized*/,
 | |
| +        undefined /*isWidgetEventSynthesized*/,
 | |
| +        buttons);
 | |
| +    }
 | |
| +
 | |
| +    // update drag state
 | |
| +    if (this._dataTransfer) {
 | |
| +      if (type === 'mousemove')
 | |
| +        this._simulateDragEvent('dragover', x, y, modifiers);
 | |
| +      else if (type === 'mouseup') // firefox will do drops when any mouse button is released
 | |
| +        this._simulateDragEvent('drop', x, y, modifiers);
 | |
| +    } else {
 | |
| +      this._cancelDragIfNeeded();
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  async _insertText({text}) {
 | |
| +    const frame = this._frameTree.mainFrame();
 | |
| +    frame.textInputProcessor().commitCompositionWith(text);
 | |
| +  }
 | |
| +
 | |
| +  async _crash() {
 | |
| +    dump(`Crashing intentionally\n`);
 | |
| +    // This is to intentionally crash the frame.
 | |
| +    // We crash by using js-ctypes and dereferencing
 | |
| +    // a bad pointer. The crash should happen immediately
 | |
| +    // upon loading this frame script.
 | |
| +    const { ctypes } = ChromeUtils.import('resource://gre/modules/ctypes.jsm');
 | |
| +    ChromeUtils.privateNoteIntentionalCrash();
 | |
| +    const zero = new ctypes.intptr_t(8);
 | |
| +    const badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
 | |
| +    badptr.contents;
 | |
| +  }
 | |
| +
 | |
| +  async _getFullAXTree({objectId}) {
 | |
| +    let unsafeObject = null;
 | |
| +    if (objectId) {
 | |
| +      unsafeObject = this._frameData.get(this._frameTree.mainFrame()).unsafeObject(objectId);
 | |
| +      if (!unsafeObject)
 | |
| +        throw new Error(`No object found for id "${objectId}"`);
 | |
| +    }
 | |
| +
 | |
| +    const service = Cc["@mozilla.org/accessibilityService;1"]
 | |
| +      .getService(Ci.nsIAccessibilityService);
 | |
| +    const document = this._frameTree.mainFrame().domWindow().document;
 | |
| +    const docAcc = service.getAccessibleFor(document);
 | |
| +
 | |
| +    while (docAcc.document.isUpdatePendingForJugglerAccessibility)
 | |
| +      await new Promise(x => this._frameTree.mainFrame().domWindow().requestAnimationFrame(x));
 | |
| +
 | |
| +    async function waitForQuiet() {
 | |
| +      let state = {};
 | |
| +      docAcc.getState(state, {});
 | |
| +      if ((state.value & Ci.nsIAccessibleStates.STATE_BUSY) == 0)
 | |
| +        return;
 | |
| +      let resolve, reject;
 | |
| +      const promise = new Promise((x, y) => {resolve = x, reject = y});
 | |
| +      let eventObserver = {
 | |
| +        observe(subject, topic) {
 | |
| +          if (topic !== "accessible-event") {
 | |
| +            return;
 | |
| +          }
 | |
| +
 | |
| +          // If event type does not match expected type, skip the event.
 | |
| +          let event = subject.QueryInterface(Ci.nsIAccessibleEvent);
 | |
| +          if (event.eventType !== Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) {
 | |
| +            return;
 | |
| +          }
 | |
| +
 | |
| +          // If event's accessible does not match expected accessible,
 | |
| +          // skip the event.
 | |
| +          if (event.accessible !== docAcc) {
 | |
| +            return;
 | |
| +          }
 | |
| +
 | |
| +          Services.obs.removeObserver(this, "accessible-event");
 | |
| +          resolve();
 | |
| +        },
 | |
| +      };
 | |
| +      Services.obs.addObserver(eventObserver, "accessible-event");
 | |
| +      return promise;
 | |
| +    }
 | |
| +    function buildNode(accElement) {
 | |
| +      let a = {}, b = {};
 | |
| +      accElement.getState(a, b);
 | |
| +      const tree = {
 | |
| +        role: service.getStringRole(accElement.role),
 | |
| +        name: accElement.name || '',
 | |
| +      };
 | |
| +      if (unsafeObject && unsafeObject === accElement.DOMNode)
 | |
| +        tree.foundObject = true;
 | |
| +      for (const userStringProperty of [
 | |
| +        'value',
 | |
| +        'description'
 | |
| +      ]) {
 | |
| +        tree[userStringProperty] = accElement[userStringProperty] || undefined;
 | |
| +      }
 | |
| +
 | |
| +      const states = {};
 | |
| +      for (const name of service.getStringStates(a.value, b.value))
 | |
| +        states[name] = true;
 | |
| +      for (const name of ['selected',
 | |
| +        'focused',
 | |
| +        'pressed',
 | |
| +        'focusable',
 | |
| +        'haspopup',
 | |
| +        'required',
 | |
| +        'invalid',
 | |
| +        'modal',
 | |
| +        'editable',
 | |
| +        'busy',
 | |
| +        'checked',
 | |
| +        'multiselectable']) {
 | |
| +        if (states[name])
 | |
| +          tree[name] = true;
 | |
| +      }
 | |
| +
 | |
| +      if (states['multi line'])
 | |
| +        tree['multiline'] = true;
 | |
| +      if (states['editable'] && states['readonly'])
 | |
| +        tree['readonly'] = true;
 | |
| +      if (states['checked'])
 | |
| +        tree['checked'] = true;
 | |
| +      if (states['mixed'])
 | |
| +        tree['checked'] = 'mixed';
 | |
| +      if (states['expanded'])
 | |
| +        tree['expanded'] = true;
 | |
| +      else if (states['collapsed'])
 | |
| +        tree['expanded'] = false;
 | |
| +      if (!states['enabled'])
 | |
| +        tree['disabled'] = true;
 | |
| +
 | |
| +      const attributes = {};
 | |
| +      if (accElement.attributes) {
 | |
| +        for (const { key, value } of accElement.attributes.enumerate()) {
 | |
| +          attributes[key] = value;
 | |
| +        }
 | |
| +      }
 | |
| +      for (const numericalProperty of ['level']) {
 | |
| +        if (numericalProperty in attributes)
 | |
| +          tree[numericalProperty] = parseFloat(attributes[numericalProperty]);
 | |
| +      }
 | |
| +      for (const stringProperty of ['tag', 'roledescription', 'valuetext', 'orientation', 'autocomplete', 'keyshortcuts']) {
 | |
| +        if (stringProperty in attributes)
 | |
| +          tree[stringProperty] = attributes[stringProperty];
 | |
| +      }
 | |
| +      const children = [];
 | |
| +
 | |
| +      for (let child = accElement.firstChild; child; child = child.nextSibling) {
 | |
| +        children.push(buildNode(child));
 | |
| +      }
 | |
| +      if (children.length)
 | |
| +        tree.children = children;
 | |
| +      return tree;
 | |
| +    }
 | |
| +    await waitForQuiet();
 | |
| +    return {
 | |
| +      tree: buildNode(docAcc)
 | |
| +    };
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +function takeScreenshot(win, left, top, width, height, mimeType) {
 | |
| +  const MAX_SKIA_DIMENSIONS = 32767;
 | |
| +
 | |
| +  const scale = win.devicePixelRatio;
 | |
| +  const canvasWidth = width * scale;
 | |
| +  const canvasHeight = height * scale;
 | |
| +
 | |
| +  if (canvasWidth > MAX_SKIA_DIMENSIONS || canvasHeight > MAX_SKIA_DIMENSIONS)
 | |
| +    throw new Error('Cannot take screenshot larger than ' + MAX_SKIA_DIMENSIONS);
 | |
| +
 | |
| +  const canvas = win.document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
 | |
| +  canvas.width = canvasWidth;
 | |
| +  canvas.height = canvasHeight;
 | |
| +
 | |
| +  let ctx = canvas.getContext('2d');
 | |
| +  ctx.scale(scale, scale);
 | |
| +  ctx.drawWindow(win, left, top, width, height, 'rgb(255,255,255)', ctx.DRAWWINDOW_DRAW_CARET);
 | |
| +  const dataURL = canvas.toDataURL(mimeType);
 | |
| +  return dataURL.substring(dataURL.indexOf(',') + 1);
 | |
| +};
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['PageAgent'];
 | |
| +this.PageAgent = PageAgent;
 | |
| +
 | |
| diff --git a/juggler/content/Runtime.js b/juggler/content/Runtime.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..9ed30684e25c7943a72b37d4289ce1f4629428e0
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/Runtime.js
 | |
| @@ -0,0 +1,537 @@
 | |
| +"use strict";
 | |
| +// Note: this file should be loadabale with eval() into worker environment.
 | |
| +// Avoid Components.*, ChromeUtils and global const variables.
 | |
| +
 | |
| +if (!this.Debugger) {
 | |
| +  // Worker has a Debugger defined already.
 | |
| +  const {addDebuggerToGlobal} = ChromeUtils.import("resource://gre/modules/jsdebugger.jsm", {});
 | |
| +  addDebuggerToGlobal(Components.utils.getGlobalForObject(this));
 | |
| +}
 | |
| +
 | |
| +let lastId = 0;
 | |
| +function generateId() {
 | |
| +  return 'id-' + (++lastId);
 | |
| +}
 | |
| +
 | |
| +const consoleLevelToProtocolType = {
 | |
| +  'dir': 'dir',
 | |
| +  'log': 'log',
 | |
| +  'debug': 'debug',
 | |
| +  'info': 'info',
 | |
| +  'error': 'error',
 | |
| +  'warn': 'warning',
 | |
| +  'dirxml': 'dirxml',
 | |
| +  'table': 'table',
 | |
| +  'trace': 'trace',
 | |
| +  'clear': 'clear',
 | |
| +  'group': 'startGroup',
 | |
| +  'groupCollapsed': 'startGroupCollapsed',
 | |
| +  'groupEnd': 'endGroup',
 | |
| +  'assert': 'assert',
 | |
| +  'profile': 'profile',
 | |
| +  'profileEnd': 'profileEnd',
 | |
| +  'count': 'count',
 | |
| +  'countReset': 'countReset',
 | |
| +  'time': null,
 | |
| +  'timeLog': 'timeLog',
 | |
| +  'timeEnd': 'timeEnd',
 | |
| +  'timeStamp': 'timeStamp',
 | |
| +};
 | |
| +
 | |
| +const disallowedMessageCategories = new Set([
 | |
| +  'XPConnect JavaScript',
 | |
| +  'component javascript',
 | |
| +  'chrome javascript',
 | |
| +  'chrome registration',
 | |
| +  'XBL',
 | |
| +  'XBL Prototype Handler',
 | |
| +  'XBL Content Sink',
 | |
| +  'xbl javascript',
 | |
| +]);
 | |
| +
 | |
| +class Runtime {
 | |
| +  constructor(isWorker = false) {
 | |
| +    this._debugger = new Debugger();
 | |
| +    this._pendingPromises = new Map();
 | |
| +    this._executionContexts = new Map();
 | |
| +    this._windowToExecutionContext = new Map();
 | |
| +    this._eventListeners = [];
 | |
| +    if (isWorker) {
 | |
| +      this._registerWorkerConsoleHandler();
 | |
| +    } else {
 | |
| +      const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +      this._registerConsoleServiceListener(Services);
 | |
| +      this._registerConsoleObserver(Services);
 | |
| +    }
 | |
| +    // We can't use event listener here to be compatible with Worker Global Context.
 | |
| +    // Use plain callbacks instead.
 | |
| +    this.events = {
 | |
| +      onConsoleMessage: createEvent(),
 | |
| +      onErrorFromWorker: createEvent(),
 | |
| +      onExecutionContextCreated: createEvent(),
 | |
| +      onExecutionContextDestroyed: createEvent(),
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  executionContexts() {
 | |
| +    return [...this._executionContexts.values()];
 | |
| +  }
 | |
| +
 | |
| +  async evaluate({executionContextId, expression, returnByValue}) {
 | |
| +    const executionContext = this.findExecutionContext(executionContextId);
 | |
| +    if (!executionContext)
 | |
| +      throw new Error('Failed to find execution context with id = ' + executionContextId);
 | |
| +    const exceptionDetails = {};
 | |
| +    let result = await executionContext.evaluateScript(expression, exceptionDetails);
 | |
| +    if (!result)
 | |
| +      return {exceptionDetails};
 | |
| +    if (returnByValue)
 | |
| +      result = executionContext.ensureSerializedToValue(result);
 | |
| +    return {result};
 | |
| +  }
 | |
| +
 | |
| +  async callFunction({executionContextId, functionDeclaration, args, returnByValue}) {
 | |
| +    const executionContext = this.findExecutionContext(executionContextId);
 | |
| +    if (!executionContext)
 | |
| +      throw new Error('Failed to find execution context with id = ' + executionContextId);
 | |
| +    const exceptionDetails = {};
 | |
| +    let result = await executionContext.evaluateFunction(functionDeclaration, args, exceptionDetails);
 | |
| +    if (!result)
 | |
| +      return {exceptionDetails};
 | |
| +    if (returnByValue)
 | |
| +      result = executionContext.ensureSerializedToValue(result);
 | |
| +    return {result};
 | |
| +  }
 | |
| +
 | |
| +  async getObjectProperties({executionContextId, objectId}) {
 | |
| +    const executionContext = this.findExecutionContext(executionContextId);
 | |
| +    if (!executionContext)
 | |
| +      throw new Error('Failed to find execution context with id = ' + executionContextId);
 | |
| +    return {properties: executionContext.getObjectProperties(objectId)};
 | |
| +  }
 | |
| +
 | |
| +  async disposeObject({executionContextId, objectId}) {
 | |
| +    const executionContext = this.findExecutionContext(executionContextId);
 | |
| +    if (!executionContext)
 | |
| +      throw new Error('Failed to find execution context with id = ' + executionContextId);
 | |
| +    return executionContext.disposeObject(objectId);
 | |
| +  }
 | |
| +
 | |
| +  _registerConsoleServiceListener(Services) {
 | |
| +    const Ci = Components.interfaces;
 | |
| +    const consoleServiceListener = {
 | |
| +      QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener]),
 | |
| +
 | |
| +      observe: message => {
 | |
| +        if (!(message instanceof Ci.nsIScriptError) || !message.outerWindowID ||
 | |
| +            !message.category || disallowedMessageCategories.has(message.category)) {
 | |
| +          return;
 | |
| +        }
 | |
| +        const errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
 | |
| +        if (message.category === 'Web Worker' && (message.flags & Ci.nsIScriptError.exceptionFlag)) {
 | |
| +          emitEvent(this.events.onErrorFromWorker, errorWindow, message.message, '' + message.stack);
 | |
| +          return;
 | |
| +        }
 | |
| +        const executionContext = this._windowToExecutionContext.get(errorWindow);
 | |
| +        if (!executionContext)
 | |
| +          return;
 | |
| +        const typeNames = {
 | |
| +          [Ci.nsIConsoleMessage.debug]: 'debug',
 | |
| +          [Ci.nsIConsoleMessage.info]: 'info',
 | |
| +          [Ci.nsIConsoleMessage.warn]: 'warn',
 | |
| +          [Ci.nsIConsoleMessage.error]: 'error',
 | |
| +        };
 | |
| +        emitEvent(this.events.onConsoleMessage, {
 | |
| +          args: [{
 | |
| +            value: message.message,
 | |
| +          }],
 | |
| +          type: typeNames[message.logLevel],
 | |
| +          executionContextId: executionContext.id(),
 | |
| +          location: {
 | |
| +            lineNumber: message.lineNumber,
 | |
| +            columnNumber: message.columnNumber,
 | |
| +            url: message.sourceName,
 | |
| +          },
 | |
| +        });
 | |
| +      },
 | |
| +    };
 | |
| +    Services.console.registerListener(consoleServiceListener);
 | |
| +    this._eventListeners.push(() => Services.console.unregisterListener(consoleServiceListener));
 | |
| +  }
 | |
| +
 | |
| +  _registerConsoleObserver(Services) {
 | |
| +    const consoleObserver = ({wrappedJSObject}, topic, data) => {
 | |
| +      const executionContext = Array.from(this._executionContexts.values()).find(context => {
 | |
| +        const domWindow = context._domWindow;
 | |
| +        return domWindow && domWindow.windowUtils.currentInnerWindowID === wrappedJSObject.innerID;
 | |
| +      });
 | |
| +      if (!executionContext)
 | |
| +        return;
 | |
| +      this._onConsoleMessage(executionContext, wrappedJSObject);
 | |
| +    };
 | |
| +    Services.obs.addObserver(consoleObserver, "console-api-log-event");
 | |
| +    this._eventListeners.push(() => Services.obs.removeObserver(consoleObserver, "console-api-log-event"));
 | |
| +  }
 | |
| +
 | |
| +  _registerWorkerConsoleHandler() {
 | |
| +    setConsoleEventHandler(message => {
 | |
| +      const executionContext = Array.from(this._executionContexts.values())[0];
 | |
| +      this._onConsoleMessage(executionContext, message);
 | |
| +    });
 | |
| +    this._eventListeners.push(() => setConsoleEventHandler(null));
 | |
| +  }
 | |
| +
 | |
| +  _onConsoleMessage(executionContext, message) {
 | |
| +    const type = consoleLevelToProtocolType[message.level];
 | |
| +    if (!type)
 | |
| +      return;
 | |
| +    const args = message.arguments.map(arg => executionContext.rawValueToRemoteObject(arg));
 | |
| +    emitEvent(this.events.onConsoleMessage, {
 | |
| +      args,
 | |
| +      type,
 | |
| +      executionContextId: executionContext.id(),
 | |
| +      location: {
 | |
| +        lineNumber: message.lineNumber - 1,
 | |
| +        columnNumber: message.columnNumber - 1,
 | |
| +        url: message.filename,
 | |
| +      },
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    for (const tearDown of this._eventListeners)
 | |
| +      tearDown.call(null);
 | |
| +    this._eventListeners = [];
 | |
| +  }
 | |
| +
 | |
| +  async _awaitPromise(executionContext, obj, exceptionDetails = {}) {
 | |
| +    if (obj.promiseState === 'fulfilled')
 | |
| +      return {success: true, obj: obj.promiseValue};
 | |
| +    if (obj.promiseState === 'rejected') {
 | |
| +      const global = executionContext._global;
 | |
| +      exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
 | |
| +      exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
 | |
| +      return {success: false, obj: null};
 | |
| +    }
 | |
| +    let resolve, reject;
 | |
| +    const promise = new Promise((a, b) => {
 | |
| +      resolve = a;
 | |
| +      reject = b;
 | |
| +    });
 | |
| +    this._pendingPromises.set(obj.promiseID, {resolve, reject, executionContext, exceptionDetails});
 | |
| +    if (this._pendingPromises.size === 1)
 | |
| +      this._debugger.onPromiseSettled = this._onPromiseSettled.bind(this);
 | |
| +    return await promise;
 | |
| +  }
 | |
| +
 | |
| +  _onPromiseSettled(obj) {
 | |
| +    const pendingPromise = this._pendingPromises.get(obj.promiseID);
 | |
| +    if (!pendingPromise)
 | |
| +      return;
 | |
| +    this._pendingPromises.delete(obj.promiseID);
 | |
| +    if (!this._pendingPromises.size)
 | |
| +      this._debugger.onPromiseSettled = undefined;
 | |
| +
 | |
| +    if (obj.promiseState === 'fulfilled') {
 | |
| +      pendingPromise.resolve({success: true, obj: obj.promiseValue});
 | |
| +      return;
 | |
| +    };
 | |
| +    const global = pendingPromise.executionContext._global;
 | |
| +    pendingPromise.exceptionDetails.text = global.executeInGlobalWithBindings('e.message', {e: obj.promiseReason}).return;
 | |
| +    pendingPromise.exceptionDetails.stack = global.executeInGlobalWithBindings('e.stack', {e: obj.promiseReason}).return;
 | |
| +    pendingPromise.resolve({success: false, obj: null});
 | |
| +  }
 | |
| +
 | |
| +  createExecutionContext(domWindow, contextGlobal, auxData) {
 | |
| +    // Note: domWindow is null for workers.
 | |
| +    const context = new ExecutionContext(this, domWindow, contextGlobal, this._debugger.addDebuggee(contextGlobal), auxData);
 | |
| +    this._executionContexts.set(context._id, context);
 | |
| +    if (domWindow)
 | |
| +      this._windowToExecutionContext.set(domWindow, context);
 | |
| +    emitEvent(this.events.onExecutionContextCreated, context);
 | |
| +    return context;
 | |
| +  }
 | |
| +
 | |
| +  findExecutionContext(executionContextId) {
 | |
| +    const executionContext = this._executionContexts.get(executionContextId);
 | |
| +    if (!executionContext)
 | |
| +      throw new Error('Failed to find execution context with id = ' + executionContextId);
 | |
| +    return executionContext;
 | |
| +  }
 | |
| +
 | |
| +  destroyExecutionContext(destroyedContext) {
 | |
| +    for (const [promiseID, {reject, executionContext}] of this._pendingPromises) {
 | |
| +      if (executionContext === destroyedContext) {
 | |
| +        reject(new Error('Execution context was destroyed!'));
 | |
| +        this._pendingPromises.delete(promiseID);
 | |
| +      }
 | |
| +    }
 | |
| +    if (!this._pendingPromises.size)
 | |
| +      this._debugger.onPromiseSettled = undefined;
 | |
| +    this._debugger.removeDebuggee(destroyedContext._contextGlobal);
 | |
| +    this._executionContexts.delete(destroyedContext._id);
 | |
| +    if (destroyedContext._domWindow)
 | |
| +      this._windowToExecutionContext.delete(destroyedContext._domWindow);
 | |
| +    emitEvent(this.events.onExecutionContextDestroyed, destroyedContext);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class ExecutionContext {
 | |
| +  constructor(runtime, domWindow, contextGlobal, global, auxData) {
 | |
| +    this._runtime = runtime;
 | |
| +    this._domWindow = domWindow;
 | |
| +    this._contextGlobal = contextGlobal;
 | |
| +    this._global = global;
 | |
| +    this._remoteObjects = new Map();
 | |
| +    this._id = generateId();
 | |
| +    this._auxData = auxData;
 | |
| +    this._jsonStringifyObject = this._global.executeInGlobal(`((stringify, dateProto, object) => {
 | |
| +      const oldToJson = dateProto.toJSON;
 | |
| +      dateProto.toJSON = undefined;
 | |
| +      let hasSymbol = false;
 | |
| +      const result = stringify(object, (key, value) => {
 | |
| +        if (typeof value === 'symbol')
 | |
| +          hasSymbol = true;
 | |
| +        return value;
 | |
| +      });
 | |
| +      dateProto.toJSON = oldToJson;
 | |
| +      return hasSymbol ? undefined : result;
 | |
| +    }).bind(null, JSON.stringify.bind(JSON), Date.prototype)`).return;
 | |
| +  }
 | |
| +
 | |
| +  id() {
 | |
| +    return this._id;
 | |
| +  }
 | |
| +
 | |
| +  auxData() {
 | |
| +    return this._auxData;
 | |
| +  }
 | |
| +
 | |
| +  async evaluateScript(script, exceptionDetails = {}) {
 | |
| +    const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
 | |
| +    if (this._domWindow && this._domWindow.document)
 | |
| +      this._domWindow.document.notifyUserGestureActivation();
 | |
| +
 | |
| +    let {success, obj} = this._getResult(this._global.executeInGlobal(script), exceptionDetails);
 | |
| +    userInputHelper && userInputHelper.destruct();
 | |
| +    if (!success)
 | |
| +      return null;
 | |
| +    if (obj && obj.isPromise) {
 | |
| +      const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
 | |
| +      if (!awaitResult.success)
 | |
| +        return null;
 | |
| +      obj = awaitResult.obj;
 | |
| +    }
 | |
| +    return this._createRemoteObject(obj);
 | |
| +  }
 | |
| +
 | |
| +  async evaluateFunction(functionText, args, exceptionDetails = {}) {
 | |
| +    const funEvaluation = this._getResult(this._global.executeInGlobal('(' + functionText + ')'), exceptionDetails);
 | |
| +    if (!funEvaluation.success)
 | |
| +      return null;
 | |
| +    if (!funEvaluation.obj.callable)
 | |
| +      throw new Error('functionText does not evaluate to a function!');
 | |
| +    args = args.map(arg => {
 | |
| +      if (arg.objectId) {
 | |
| +        if (!this._remoteObjects.has(arg.objectId))
 | |
| +          throw new Error('Cannot find object with id = ' + arg.objectId);
 | |
| +        return this._remoteObjects.get(arg.objectId);
 | |
| +      }
 | |
| +      switch (arg.unserializableValue) {
 | |
| +        case 'Infinity': return Infinity;
 | |
| +        case '-Infinity': return -Infinity;
 | |
| +        case '-0': return -0;
 | |
| +        case 'NaN': return NaN;
 | |
| +        default: return this._toDebugger(arg.value);
 | |
| +      }
 | |
| +    });
 | |
| +    const userInputHelper = this._domWindow ? this._domWindow.windowUtils.setHandlingUserInput(true) : null;
 | |
| +    if (this._domWindow && this._domWindow.document)
 | |
| +      this._domWindow.document.notifyUserGestureActivation();
 | |
| +    let {success, obj} = this._getResult(funEvaluation.obj.apply(null, args), exceptionDetails);
 | |
| +    userInputHelper && userInputHelper.destruct();
 | |
| +    if (!success)
 | |
| +      return null;
 | |
| +    if (obj && obj.isPromise) {
 | |
| +      const awaitResult = await this._runtime._awaitPromise(this, obj, exceptionDetails);
 | |
| +      if (!awaitResult.success)
 | |
| +        return null;
 | |
| +      obj = awaitResult.obj;
 | |
| +    }
 | |
| +    return this._createRemoteObject(obj);
 | |
| +  }
 | |
| +
 | |
| +  unsafeObject(objectId) {
 | |
| +    if (!this._remoteObjects.has(objectId))
 | |
| +      return;
 | |
| +    return { object: this._remoteObjects.get(objectId).unsafeDereference() };
 | |
| +  }
 | |
| +
 | |
| +  rawValueToRemoteObject(rawValue) {
 | |
| +    const debuggerObj = this._global.makeDebuggeeValue(rawValue);
 | |
| +    return this._createRemoteObject(debuggerObj);
 | |
| +  }
 | |
| +
 | |
| +  _instanceOf(debuggerObj, rawObj, className) {
 | |
| +    if (this._domWindow)
 | |
| +      return rawObj instanceof this._domWindow[className];
 | |
| +    return this._global.executeInGlobalWithBindings('o instanceof this[className]', {o: debuggerObj, className: this._global.makeDebuggeeValue(className)}).return;
 | |
| +  }
 | |
| +
 | |
| +  _createRemoteObject(debuggerObj) {
 | |
| +    if (debuggerObj instanceof Debugger.Object) {
 | |
| +      const objectId = generateId();
 | |
| +      this._remoteObjects.set(objectId, debuggerObj);
 | |
| +      const rawObj = debuggerObj.unsafeDereference();
 | |
| +      const type = typeof rawObj;
 | |
| +      let subtype = undefined;
 | |
| +      if (debuggerObj.isProxy)
 | |
| +        subtype = 'proxy';
 | |
| +      else if (Array.isArray(rawObj))
 | |
| +        subtype = 'array';
 | |
| +      else if (Object.is(rawObj, null))
 | |
| +        subtype = 'null';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Node'))
 | |
| +        subtype = 'node';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'RegExp'))
 | |
| +        subtype = 'regexp';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Date'))
 | |
| +        subtype = 'date';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Map'))
 | |
| +        subtype = 'map';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Set'))
 | |
| +        subtype = 'set';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'WeakMap'))
 | |
| +        subtype = 'weakmap';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'WeakSet'))
 | |
| +        subtype = 'weakset';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Error'))
 | |
| +        subtype = 'error';
 | |
| +      else if (this._instanceOf(debuggerObj, rawObj, 'Promise'))
 | |
| +        subtype = 'promise';
 | |
| +      else if ((this._instanceOf(debuggerObj, rawObj, 'Int8Array')) || (this._instanceOf(debuggerObj, rawObj, 'Uint8Array')) ||
 | |
| +               (this._instanceOf(debuggerObj, rawObj, 'Uint8ClampedArray')) || (this._instanceOf(debuggerObj, rawObj, 'Int16Array')) ||
 | |
| +               (this._instanceOf(debuggerObj, rawObj, 'Uint16Array')) || (this._instanceOf(debuggerObj, rawObj, 'Int32Array')) ||
 | |
| +               (this._instanceOf(debuggerObj, rawObj, 'Uint32Array')) || (this._instanceOf(debuggerObj, rawObj, 'Float32Array')) ||
 | |
| +               (this._instanceOf(debuggerObj, rawObj, 'Float64Array'))) {
 | |
| +        subtype = 'typedarray';
 | |
| +      }
 | |
| +      return {objectId, type, subtype};
 | |
| +    }
 | |
| +    if (typeof debuggerObj === 'symbol') {
 | |
| +      const objectId = generateId();
 | |
| +      this._remoteObjects.set(objectId, debuggerObj);
 | |
| +      return {objectId, type: 'symbol'};
 | |
| +    }
 | |
| +
 | |
| +    let unserializableValue = undefined;
 | |
| +    if (Object.is(debuggerObj, NaN))
 | |
| +      unserializableValue = 'NaN';
 | |
| +    else if (Object.is(debuggerObj, -0))
 | |
| +      unserializableValue = '-0';
 | |
| +    else if (Object.is(debuggerObj, Infinity))
 | |
| +      unserializableValue = 'Infinity';
 | |
| +    else if (Object.is(debuggerObj, -Infinity))
 | |
| +      unserializableValue = '-Infinity';
 | |
| +    return unserializableValue ? {unserializableValue} : {value: debuggerObj};
 | |
| +  }
 | |
| +
 | |
| +  ensureSerializedToValue(protocolObject) {
 | |
| +    if (!protocolObject.objectId)
 | |
| +      return protocolObject;
 | |
| +    const obj = this._remoteObjects.get(protocolObject.objectId);
 | |
| +    this._remoteObjects.delete(protocolObject.objectId);
 | |
| +    return {value: this._serialize(obj)};
 | |
| +  }
 | |
| +
 | |
| +  _toDebugger(obj) {
 | |
| +    if (typeof obj !== 'object')
 | |
| +      return obj;
 | |
| +    if (obj === null)
 | |
| +      return obj;
 | |
| +    const properties = {};
 | |
| +    for (let [key, value] of Object.entries(obj)) {
 | |
| +      properties[key] = {
 | |
| +        configurable: true,
 | |
| +        writable: true,
 | |
| +        enumerable: true,
 | |
| +        value: this._toDebugger(value),
 | |
| +      };
 | |
| +    }
 | |
| +    const baseObject = Array.isArray(obj) ? '([])' : '({})';
 | |
| +    const debuggerObj = this._global.executeInGlobal(baseObject).return;
 | |
| +    debuggerObj.defineProperties(properties);
 | |
| +    return debuggerObj;
 | |
| +  }
 | |
| +
 | |
| +  _serialize(obj) {
 | |
| +    const result = this._global.executeInGlobalWithBindings('stringify(e)', {e: obj, stringify: this._jsonStringifyObject});
 | |
| +    if (result.throw)
 | |
| +      throw new Error('Object is not serializable');
 | |
| +    return result.return === undefined ? undefined : JSON.parse(result.return);
 | |
| +  }
 | |
| +
 | |
| +  disposeObject(objectId) {
 | |
| +    this._remoteObjects.delete(objectId);
 | |
| +  }
 | |
| +
 | |
| +  getObjectProperties(objectId) {
 | |
| +    if (!this._remoteObjects.has(objectId))
 | |
| +      throw new Error('Cannot find object with id = ' + arg.objectId);
 | |
| +    const result = [];
 | |
| +    for (let obj = this._remoteObjects.get(objectId); obj; obj = obj.proto) {
 | |
| +      for (const propertyName of obj.getOwnPropertyNames()) {
 | |
| +        const descriptor = obj.getOwnPropertyDescriptor(propertyName);
 | |
| +        if (!descriptor.enumerable)
 | |
| +          continue;
 | |
| +        result.push({
 | |
| +          name: propertyName,
 | |
| +          value: this._createRemoteObject(descriptor.value),
 | |
| +        });
 | |
| +      }
 | |
| +    }
 | |
| +    return result;
 | |
| +  }
 | |
| +
 | |
| +  _getResult(completionValue, exceptionDetails = {}) {
 | |
| +    if (!completionValue) {
 | |
| +      exceptionDetails.text = 'Evaluation terminated!';
 | |
| +      exceptionDetails.stack = '';
 | |
| +      return {success: false, obj: null};
 | |
| +    }
 | |
| +    if (completionValue.throw) {
 | |
| +      if (this._global.executeInGlobalWithBindings('e instanceof Error', {e: completionValue.throw}).return) {
 | |
| +        exceptionDetails.text = this._global.executeInGlobalWithBindings('e.message', {e: completionValue.throw}).return;
 | |
| +        exceptionDetails.stack = this._global.executeInGlobalWithBindings('e.stack', {e: completionValue.throw}).return;
 | |
| +      } else {
 | |
| +        exceptionDetails.value = this._serialize(completionValue.throw);
 | |
| +      }
 | |
| +      return {success: false, obj: null};
 | |
| +    }
 | |
| +    return {success: true, obj: completionValue.return};
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +const listenersSymbol = Symbol('listeners');
 | |
| +
 | |
| +function createEvent() {
 | |
| +  const listeners = new Set();
 | |
| +  const subscribeFunction = listener => {
 | |
| +    listeners.add(listener);
 | |
| +    return () => listeners.delete(listener);
 | |
| +  }
 | |
| +  subscribeFunction[listenersSymbol] = listeners;
 | |
| +  return subscribeFunction;
 | |
| +}
 | |
| +
 | |
| +function emitEvent(event, ...args) {
 | |
| +  let listeners = event[listenersSymbol];
 | |
| +  if (!listeners || !listeners.size)
 | |
| +    return;
 | |
| +  listeners = new Set(listeners);
 | |
| +  for (const listener of listeners)
 | |
| +    listener.call(null, ...args);
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['Runtime'];
 | |
| +this.Runtime = Runtime;
 | |
| diff --git a/juggler/content/ScrollbarManager.js b/juggler/content/ScrollbarManager.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..caee4df323d0a526ed7e38947c41c6430983568d
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/ScrollbarManager.js
 | |
| @@ -0,0 +1,85 @@
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cr = Components.results;
 | |
| +const Cu = Components.utils;
 | |
| +const Cc = Components.classes;
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +
 | |
| +const HIDDEN_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/hidden-scrollbars.css');
 | |
| +const FLOATING_SCROLLBARS = Services.io.newURI('chrome://juggler/content/content/floating-scrollbars.css');
 | |
| +
 | |
| +const isHeadless = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo).isHeadless;
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class ScrollbarManager {
 | |
| +  constructor(docShell) {
 | |
| +    this._docShell = docShell;
 | |
| +    this._customScrollbars = null;
 | |
| +    this._contentViewerScrollBars = new Map();
 | |
| +
 | |
| +    if (isHeadless)
 | |
| +      this._setCustomScrollbars(HIDDEN_SCROLLBARS);
 | |
| +
 | |
| +    const webProgress = this._docShell.QueryInterface(Ci.nsIInterfaceRequestor)
 | |
| +                                .getInterface(Ci.nsIWebProgress);
 | |
| +
 | |
| +    this.QueryInterface = ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']);
 | |
| +    this._eventListeners = [
 | |
| +      helper.addProgressListener(webProgress, this, Ci.nsIWebProgress.NOTIFY_ALL),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  onLocationChange(webProgress, request, URI, flags) {
 | |
| +    if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
 | |
| +      return;
 | |
| +    this._updateAllDocShells();
 | |
| +  }
 | |
| +
 | |
| +  setFloatingScrollbars(enabled) {
 | |
| +    if (this._customScrollbars === HIDDEN_SCROLLBARS)
 | |
| +      return;
 | |
| +    this._setCustomScrollbars(enabled ? FLOATING_SCROLLBARS : null);
 | |
| +  }
 | |
| +
 | |
| +  _setCustomScrollbars(customScrollbars) {
 | |
| +    if (this._customScrollbars === customScrollbars)
 | |
| +      return;
 | |
| +    this._customScrollbars = customScrollbars;
 | |
| +    this._updateAllDocShells();
 | |
| +  }
 | |
| +
 | |
| +  _updateAllDocShells() {
 | |
| +    const allDocShells = [this._docShell];
 | |
| +    for (let i = 0; i < this._docShell.childCount; i++)
 | |
| +      allDocShells.push(this._docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
 | |
| +    // At this point, a content viewer might not be loaded for certain docShells.
 | |
| +    // Scrollbars will be updated in onLocationChange.
 | |
| +    const contentViewers = allDocShells.map(docShell => docShell.contentViewer).filter(contentViewer => !!contentViewer);
 | |
| +
 | |
| +    // Update scrollbar stylesheets.
 | |
| +    for (const contentViewer of contentViewers) {
 | |
| +      const oldScrollbars = this._contentViewerScrollBars.get(contentViewer);
 | |
| +      if (oldScrollbars === this._customScrollbars)
 | |
| +        continue;
 | |
| +      const winUtils = contentViewer.DOMDocument.defaultView.windowUtils;
 | |
| +      if (oldScrollbars)
 | |
| +        winUtils.removeSheet(oldScrollbars, winUtils.AGENT_SHEET);
 | |
| +      if (this._customScrollbars)
 | |
| +        winUtils.loadSheet(this._customScrollbars, winUtils.AGENT_SHEET);
 | |
| +    }
 | |
| +    // Update state for all *existing* docShells.
 | |
| +    this._contentViewerScrollBars.clear();
 | |
| +    for (const contentViewer of contentViewers)
 | |
| +      this._contentViewerScrollBars.set(contentViewer, this._customScrollbars);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._setCustomScrollbars(null);
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['ScrollbarManager'];
 | |
| +this.ScrollbarManager = ScrollbarManager;
 | |
| +
 | |
| diff --git a/juggler/content/WorkerMain.js b/juggler/content/WorkerMain.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..fb35b515e58829d86bb74f36eecd8dc885102d69
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/WorkerMain.js
 | |
| @@ -0,0 +1,83 @@
 | |
| +"use strict";
 | |
| +loadSubScript('chrome://juggler/content/content/Runtime.js');
 | |
| +loadSubScript('chrome://juggler/content/SimpleChannel.js');
 | |
| +
 | |
| +const runtimeAgents = new Map();
 | |
| +
 | |
| +const channel = new SimpleChannel('worker::worker');
 | |
| +const eventListener = event => channel._onMessage(JSON.parse(event.data));
 | |
| +this.addEventListener('message', eventListener);
 | |
| +channel.transport = {
 | |
| +  sendMessage: msg => postMessage(JSON.stringify(msg)),
 | |
| +  dispose: () => this.removeEventListener('message', eventListener),
 | |
| +};
 | |
| +
 | |
| +const runtime = new Runtime(true /* isWorker */);
 | |
| +
 | |
| +(() => {
 | |
| +  // Create execution context in the runtime only when the script
 | |
| +  // source was actually evaluated in it.
 | |
| +  const dbg = new Debugger(global);
 | |
| +  if (dbg.findScripts({global}).length) {
 | |
| +    runtime.createExecutionContext(null /* domWindow */, global, {});
 | |
| +  } else {
 | |
| +    dbg.onNewScript = function(s) {
 | |
| +      dbg.onNewScript = undefined;
 | |
| +      dbg.removeAllDebuggees();
 | |
| +      runtime.createExecutionContext(null /* domWindow */, global, {});
 | |
| +    };
 | |
| +  }
 | |
| +})();
 | |
| +
 | |
| +class RuntimeAgent {
 | |
| +  constructor(runtime, channel, sessionId) {
 | |
| +    this._runtime = runtime;
 | |
| +    this._browserRuntime = channel.connect(sessionId + 'runtime');
 | |
| +    this._eventListeners = [
 | |
| +      channel.register(sessionId + 'runtime', {
 | |
| +        evaluate: this._runtime.evaluate.bind(this._runtime),
 | |
| +        callFunction: this._runtime.callFunction.bind(this._runtime),
 | |
| +        getObjectProperties: this._runtime.getObjectProperties.bind(this._runtime),
 | |
| +        disposeObject: this._runtime.disposeObject.bind(this._runtime),
 | |
| +      }),
 | |
| +      this._runtime.events.onConsoleMessage(msg => this._browserRuntime.emit('runtimeConsole', msg)),
 | |
| +      this._runtime.events.onExecutionContextCreated(this._onExecutionContextCreated.bind(this)),
 | |
| +      this._runtime.events.onExecutionContextDestroyed(this._onExecutionContextDestroyed.bind(this)),
 | |
| +    ];
 | |
| +    for (const context of this._runtime.executionContexts())
 | |
| +      this._onExecutionContextCreated(context);
 | |
| +  }
 | |
| +
 | |
| +  _onExecutionContextCreated(executionContext) {
 | |
| +    this._browserRuntime.emit('runtimeExecutionContextCreated', {
 | |
| +      executionContextId: executionContext.id(),
 | |
| +      auxData: executionContext.auxData(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onExecutionContextDestroyed(executionContext) {
 | |
| +    this._browserRuntime.emit('runtimeExecutionContextDestroyed', {
 | |
| +      executionContextId: executionContext.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    for (const disposer of this._eventListeners)
 | |
| +      disposer();
 | |
| +    this._eventListeners = [];
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +channel.register('', {
 | |
| +  attach: ({sessionId}) => {
 | |
| +    const runtimeAgent = new RuntimeAgent(runtime, channel, sessionId);
 | |
| +    runtimeAgents.set(sessionId, runtimeAgent);
 | |
| +  },
 | |
| +
 | |
| +  detach: ({sessionId}) => {
 | |
| +    const runtimeAgent = runtimeAgents.get(sessionId);
 | |
| +    runtimeAgents.delete(sessionId);
 | |
| +    runtimeAgent.dispose();
 | |
| +  },
 | |
| +});
 | |
| +
 | |
| diff --git a/juggler/content/floating-scrollbars.css b/juggler/content/floating-scrollbars.css
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..7709bdd34c65062fc63684ef17fc792d3991d965
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/floating-scrollbars.css
 | |
| @@ -0,0 +1,47 @@
 | |
| +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 | |
| +@namespace html url("http://www.w3.org/1999/xhtml");
 | |
| +
 | |
| +/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
 | |
| +   inside a <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
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..3a386425d3796d0a6786dea193b3402dfd2ac4f6
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/hidden-scrollbars.css
 | |
| @@ -0,0 +1,13 @@
 | |
| +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 | |
| +@namespace html url("http://www.w3.org/1999/xhtml");
 | |
| +
 | |
| +/* Restrict all styles to `*|*:not(html|select) > scrollbar` so that scrollbars
 | |
| +   inside a <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
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..70c47bb426876f4a89709ba61460149dae370efe
 | |
| --- /dev/null
 | |
| +++ b/juggler/content/main.js
 | |
| @@ -0,0 +1,188 @@
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {FrameTree} = ChromeUtils.import('chrome://juggler/content/content/FrameTree.js');
 | |
| +const {NetworkMonitor} = ChromeUtils.import('chrome://juggler/content/content/NetworkMonitor.js');
 | |
| +const {ScrollbarManager} = ChromeUtils.import('chrome://juggler/content/content/ScrollbarManager.js');
 | |
| +const {SimpleChannel} = ChromeUtils.import('chrome://juggler/content/SimpleChannel.js');
 | |
| +const {PageAgent} = ChromeUtils.import('chrome://juggler/content/content/PageAgent.js');
 | |
| +
 | |
| +const scrollbarManager = new ScrollbarManager(docShell);
 | |
| +let frameTree;
 | |
| +let networkMonitor;
 | |
| +const helper = new Helper();
 | |
| +const messageManager = this;
 | |
| +
 | |
| +const sessions = new Map();
 | |
| +
 | |
| +function createContentSession(channel, sessionId) {
 | |
| +  const pageAgent = new PageAgent(messageManager, channel, sessionId, frameTree, networkMonitor);
 | |
| +  sessions.set(sessionId, [pageAgent]);
 | |
| +  pageAgent.enable();
 | |
| +}
 | |
| +
 | |
| +function disposeContentSession(sessionId) {
 | |
| +  const handlers = sessions.get(sessionId);
 | |
| +  sessions.delete(sessionId);
 | |
| +  for (const handler of handlers)
 | |
| +    handler.dispose();
 | |
| +}
 | |
| +
 | |
| +let failedToOverrideTimezone = false;
 | |
| +
 | |
| +const applySetting = {
 | |
| +  geolocation: (geolocation) => {
 | |
| +    if (geolocation) {
 | |
| +      docShell.setGeolocationOverride({
 | |
| +        coords: {
 | |
| +          latitude: geolocation.latitude,
 | |
| +          longitude: geolocation.longitude,
 | |
| +          accuracy: geolocation.accuracy,
 | |
| +          altitude: NaN,
 | |
| +          altitudeAccuracy: NaN,
 | |
| +          heading: NaN,
 | |
| +          speed: NaN,
 | |
| +        },
 | |
| +        address: null,
 | |
| +        timestamp: Date.now()
 | |
| +      });
 | |
| +    } else {
 | |
| +      docShell.setGeolocationOverride(null);
 | |
| +    }
 | |
| +  },
 | |
| +
 | |
| +  onlineOverride: (onlineOverride) => {
 | |
| +    if (!onlineOverride) {
 | |
| +      docShell.onlineOverride = Ci.nsIDocShell.ONLINE_OVERRIDE_NONE;
 | |
| +      return;
 | |
| +    }
 | |
| +    docShell.onlineOverride = onlineOverride === 'online' ?
 | |
| +        Ci.nsIDocShell.ONLINE_OVERRIDE_ONLINE : Ci.nsIDocShell.ONLINE_OVERRIDE_OFFLINE;
 | |
| +  },
 | |
| +
 | |
| +  userAgent: (userAgent) => {
 | |
| +    docShell.browsingContext.customUserAgent = userAgent;
 | |
| +  },
 | |
| +
 | |
| +  bypassCSP: (bypassCSP) => {
 | |
| +    docShell.bypassCSPEnabled = bypassCSP;
 | |
| +  },
 | |
| +
 | |
| +  timezoneId: (timezoneId) => {
 | |
| +    failedToOverrideTimezone = !docShell.overrideTimezone(timezoneId);
 | |
| +  },
 | |
| +
 | |
| +  locale: (locale) => {
 | |
| +    docShell.languageOverride = locale;
 | |
| +  },
 | |
| +
 | |
| +  javaScriptDisabled: (javaScriptDisabled) => {
 | |
| +    docShell.allowJavascript = !javaScriptDisabled;
 | |
| +  },
 | |
| +
 | |
| +  hasTouch: (hasTouch) => {
 | |
| +    docShell.touchEventsOverride = hasTouch ? Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED : Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_NONE;
 | |
| +  },
 | |
| +
 | |
| +  colorScheme: (colorScheme) => {
 | |
| +    frameTree.setColorScheme(colorScheme);
 | |
| +  },
 | |
| +
 | |
| +  deviceScaleFactor: (deviceScaleFactor) => {
 | |
| +    docShell.contentViewer.overrideDPPX = deviceScaleFactor || this._initialDPPX;
 | |
| +    docShell.deviceSizeIsPageSize = !!deviceScaleFactor;
 | |
| +  },
 | |
| +};
 | |
| +
 | |
| +function initialize() {
 | |
| +  const loadContext = docShell.QueryInterface(Ci.nsILoadContext);
 | |
| +  const userContextId = loadContext.originAttributes.userContextId;
 | |
| +
 | |
| +  const response = sendSyncMessage('juggler:content-ready', { userContextId })[0];
 | |
| +  const {
 | |
| +    sessionIds = [],
 | |
| +    scriptsToEvaluateOnNewDocument = [],
 | |
| +    bindings = [],
 | |
| +    settings = {}
 | |
| +  } = response || {};
 | |
| +
 | |
| +  // Enforce focused state for all top level documents.
 | |
| +  docShell.overrideHasFocus = true;
 | |
| +  frameTree = new FrameTree(docShell);
 | |
| +  for (const [name, value] of Object.entries(settings)) {
 | |
| +    if (value !== undefined)
 | |
| +      applySetting[name](value);
 | |
| +  }
 | |
| +  for (const script of scriptsToEvaluateOnNewDocument)
 | |
| +    frameTree.addScriptToEvaluateOnNewDocument(script);
 | |
| +  for (const { name, script } of bindings)
 | |
| +    frameTree.addBinding(name, script);
 | |
| +  networkMonitor = new NetworkMonitor(docShell, frameTree);
 | |
| +
 | |
| +  const channel = SimpleChannel.createForMessageManager('content::page', messageManager);
 | |
| +
 | |
| +  for (const sessionId of sessionIds)
 | |
| +    createContentSession(channel, sessionId);
 | |
| +
 | |
| +  channel.register('', {
 | |
| +    attach({sessionId}) {
 | |
| +      createContentSession(channel, sessionId);
 | |
| +    },
 | |
| +
 | |
| +    detach({sessionId}) {
 | |
| +      disposeContentSession(sessionId);
 | |
| +    },
 | |
| +
 | |
| +    addScriptToEvaluateOnNewDocument(script) {
 | |
| +      frameTree.addScriptToEvaluateOnNewDocument(script);
 | |
| +    },
 | |
| +
 | |
| +    addBinding({name, script}) {
 | |
| +      frameTree.addBinding(name, script);
 | |
| +    },
 | |
| +
 | |
| +    applyContextSetting({name, value}) {
 | |
| +      applySetting[name](value);
 | |
| +    },
 | |
| +
 | |
| +    ensurePermissions() {
 | |
| +      // noop, just a rountrip.
 | |
| +    },
 | |
| +
 | |
| +    hasFailedToOverrideTimezone() {
 | |
| +      return failedToOverrideTimezone;
 | |
| +    },
 | |
| +
 | |
| +    async awaitViewportDimensions({width, height}) {
 | |
| +      const win = docShell.domWindow;
 | |
| +      if (win.innerWidth === width && win.innerHeight === height)
 | |
| +        return;
 | |
| +      await new Promise(resolve => {
 | |
| +        const listener = helper.addEventListener(win, 'resize', () => {
 | |
| +          if (win.innerWidth === width && win.innerHeight === height) {
 | |
| +            helper.removeListeners([listener]);
 | |
| +            resolve();
 | |
| +          }
 | |
| +        });
 | |
| +      });
 | |
| +    },
 | |
| +
 | |
| +    dispose() {
 | |
| +    },
 | |
| +  });
 | |
| +
 | |
| +  const gListeners = [
 | |
| +    helper.addEventListener(messageManager, 'unload', msg => {
 | |
| +      helper.removeListeners(gListeners);
 | |
| +      channel.dispose();
 | |
| +
 | |
| +      for (const sessionId of sessions.keys())
 | |
| +        disposeContentSession(sessionId);
 | |
| +
 | |
| +      scrollbarManager.dispose();
 | |
| +      networkMonitor.dispose();
 | |
| +      frameTree.dispose();
 | |
| +    }),
 | |
| +  ];
 | |
| +}
 | |
| +
 | |
| +initialize();
 | |
| diff --git a/juggler/jar.mn b/juggler/jar.mn
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..ec78981943bcaa615806b6da18b8c373ba7f23b2
 | |
| --- /dev/null
 | |
| +++ b/juggler/jar.mn
 | |
| @@ -0,0 +1,28 @@
 | |
| +# This Source Code Form is subject to the terms of the Mozilla Public
 | |
| +# License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| +
 | |
| +juggler.jar:
 | |
| +% content juggler %content/
 | |
| +  content/Helper.js (Helper.js)
 | |
| +  content/NetworkObserver.js (NetworkObserver.js)
 | |
| +  content/TargetRegistry.js (TargetRegistry.js)
 | |
| +  content/SimpleChannel.js (SimpleChannel.js)
 | |
| +  content/protocol/PrimitiveTypes.js (protocol/PrimitiveTypes.js)
 | |
| +  content/protocol/Protocol.js (protocol/Protocol.js)
 | |
| +  content/protocol/Dispatcher.js (protocol/Dispatcher.js)
 | |
| +  content/protocol/PageHandler.js (protocol/PageHandler.js)
 | |
| +  content/protocol/RuntimeHandler.js (protocol/RuntimeHandler.js)
 | |
| +  content/protocol/NetworkHandler.js (protocol/NetworkHandler.js)
 | |
| +  content/protocol/BrowserHandler.js (protocol/BrowserHandler.js)
 | |
| +  content/protocol/AccessibilityHandler.js (protocol/AccessibilityHandler.js)
 | |
| +  content/content/main.js (content/main.js)
 | |
| +  content/content/FrameTree.js (content/FrameTree.js)
 | |
| +  content/content/NetworkMonitor.js (content/NetworkMonitor.js)
 | |
| +  content/content/PageAgent.js (content/PageAgent.js)
 | |
| +  content/content/Runtime.js (content/Runtime.js)
 | |
| +  content/content/WorkerMain.js (content/WorkerMain.js)
 | |
| +  content/content/ScrollbarManager.js (content/ScrollbarManager.js)
 | |
| +  content/content/floating-scrollbars.css (content/floating-scrollbars.css)
 | |
| +  content/content/hidden-scrollbars.css (content/hidden-scrollbars.css)
 | |
| +
 | |
| diff --git a/juggler/moz.build b/juggler/moz.build
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..1a0a3130bf9509829744fadc692a79754fddd351
 | |
| --- /dev/null
 | |
| +++ b/juggler/moz.build
 | |
| @@ -0,0 +1,15 @@
 | |
| +# This Source Code Form is subject to the terms of the Mozilla Public
 | |
| +# License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
| +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | |
| +
 | |
| +DIRS += ["components"]
 | |
| +
 | |
| +JAR_MANIFESTS += ["jar.mn"]
 | |
| +#JS_PREFERENCE_FILES += ["prefs/marionette.js"]
 | |
| +
 | |
| +#MARIONETTE_UNIT_MANIFESTS += ["harness/marionette_harness/tests/unit/unit-tests.ini"]
 | |
| +#XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
 | |
| +
 | |
| +with Files("**"):
 | |
| +    BUG_COMPONENT = ("Testing", "Juggler")
 | |
| +
 | |
| diff --git a/juggler/protocol/AccessibilityHandler.js b/juggler/protocol/AccessibilityHandler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..bf37558bccc48f4d90eadc971c1eb3e44d8e92f0
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/AccessibilityHandler.js
 | |
| @@ -0,0 +1,16 @@
 | |
| +class AccessibilityHandler {
 | |
| +  constructor(session, contentChannel) {
 | |
| +    this._contentPage = contentChannel.connect(session.sessionId() + 'page');
 | |
| +  }
 | |
| +
 | |
| +  async getFullAXTree(params) {
 | |
| +    return await this._contentPage.send('getFullAXTree', params);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._contentPage.dispose();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['AccessibilityHandler'];
 | |
| +this.AccessibilityHandler = AccessibilityHandler;
 | |
| diff --git a/juggler/protocol/BrowserHandler.js b/juggler/protocol/BrowserHandler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..ad4bfcf776c2ae97957c8b0e675f920371955728
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/BrowserHandler.js
 | |
| @@ -0,0 +1,239 @@
 | |
| +"use strict";
 | |
| +
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +const {TargetRegistry} = ChromeUtils.import("chrome://juggler/content/TargetRegistry.js");
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class BrowserHandler {
 | |
| +  constructor(session, dispatcher, targetRegistry, onclose) {
 | |
| +    this._session = session;
 | |
| +    this._dispatcher = dispatcher;
 | |
| +    this._targetRegistry = targetRegistry;
 | |
| +    this._enabled = false;
 | |
| +    this._attachToDefaultContext = false;
 | |
| +    this._eventListeners = [];
 | |
| +    this._createdBrowserContextIds = new Set();
 | |
| +    this._attachedSessions = new Map();
 | |
| +    this._onclose = onclose;
 | |
| +  }
 | |
| +
 | |
| +  async enable({attachToDefaultContext}) {
 | |
| +    if (this._enabled)
 | |
| +      return;
 | |
| +    this._enabled = true;
 | |
| +    this._attachToDefaultContext = attachToDefaultContext;
 | |
| +
 | |
| +    for (const target of this._targetRegistry.targets()) {
 | |
| +      if (!this._shouldAttachToTarget(target))
 | |
| +        continue;
 | |
| +      const session = this._dispatcher.createSession();
 | |
| +      target.connectSession(session);
 | |
| +      this._attachedSessions.set(target, session);
 | |
| +      this._session.emitEvent('Browser.attachedToTarget', {
 | |
| +        sessionId: session.sessionId(),
 | |
| +        targetInfo: target.info()
 | |
| +      });
 | |
| +    }
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      helper.on(this._targetRegistry, TargetRegistry.Events.TargetCreated, this._onTargetCreated.bind(this)),
 | |
| +      helper.on(this._targetRegistry, TargetRegistry.Events.TargetDestroyed, this._onTargetDestroyed.bind(this)),
 | |
| +      helper.on(this._targetRegistry, TargetRegistry.Events.DownloadCreated, this._onDownloadCreated.bind(this)),
 | |
| +      helper.on(this._targetRegistry, TargetRegistry.Events.DownloadFinished, this._onDownloadFinished.bind(this)),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  async createBrowserContext({removeOnDetach}) {
 | |
| +    if (!this._enabled)
 | |
| +      throw new Error('Browser domain is not enabled');
 | |
| +    const browserContext = this._targetRegistry.createBrowserContext(removeOnDetach);
 | |
| +    this._createdBrowserContextIds.add(browserContext.browserContextId);
 | |
| +    return {browserContextId: browserContext.browserContextId};
 | |
| +  }
 | |
| +
 | |
| +  async removeBrowserContext({browserContextId}) {
 | |
| +    if (!this._enabled)
 | |
| +      throw new Error('Browser domain is not enabled');
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).destroy();
 | |
| +    this._createdBrowserContextIds.delete(browserContextId);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +    for (const [target, session] of this._attachedSessions) {
 | |
| +      target.disconnectSession(session);
 | |
| +      this._dispatcher.destroySession(session);
 | |
| +    }
 | |
| +    this._attachedSessions.clear();
 | |
| +    for (const browserContextId of this._createdBrowserContextIds) {
 | |
| +      const browserContext = this._targetRegistry.browserContextForId(browserContextId);
 | |
| +      if (browserContext.removeOnDetach)
 | |
| +        browserContext.destroy();
 | |
| +    }
 | |
| +    this._createdBrowserContextIds.clear();
 | |
| +  }
 | |
| +
 | |
| +  _shouldAttachToTarget(target) {
 | |
| +    if (!target._browserContext)
 | |
| +      return false;
 | |
| +    if (this._createdBrowserContextIds.has(target._browserContext.browserContextId))
 | |
| +      return true;
 | |
| +    return this._attachToDefaultContext && target._browserContext === this._targetRegistry.defaultContext();
 | |
| +  }
 | |
| +
 | |
| +  _onTargetCreated({sessions, target}) {
 | |
| +    if (!this._shouldAttachToTarget(target))
 | |
| +      return;
 | |
| +    const session = this._dispatcher.createSession();
 | |
| +    this._attachedSessions.set(target, session);
 | |
| +    this._session.emitEvent('Browser.attachedToTarget', {
 | |
| +      sessionId: session.sessionId(),
 | |
| +      targetInfo: target.info()
 | |
| +    });
 | |
| +    sessions.push(session);
 | |
| +  }
 | |
| +
 | |
| +  _onTargetDestroyed(target) {
 | |
| +    const session = this._attachedSessions.get(target);
 | |
| +    if (!session)
 | |
| +      return;
 | |
| +    this._attachedSessions.delete(target);
 | |
| +    this._dispatcher.destroySession(session);
 | |
| +    this._session.emitEvent('Browser.detachedFromTarget', {
 | |
| +      sessionId: session.sessionId(),
 | |
| +      targetId: target.id(),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  _onDownloadCreated(downloadInfo) {
 | |
| +    this._session.emitEvent('Browser.downloadCreated', downloadInfo);
 | |
| +  }
 | |
| +
 | |
| +  _onDownloadFinished(downloadInfo) {
 | |
| +    this._session.emitEvent('Browser.downloadFinished', downloadInfo);
 | |
| +  }
 | |
| +
 | |
| +  async newPage({browserContextId}) {
 | |
| +    const targetId = await this._targetRegistry.newPage({browserContextId});
 | |
| +    return {targetId};
 | |
| +  }
 | |
| +
 | |
| +  async close() {
 | |
| +    this._onclose();
 | |
| +    let browserWindow = Services.wm.getMostRecentWindow(
 | |
| +      "navigator:browser"
 | |
| +    );
 | |
| +    if (browserWindow && browserWindow.gBrowserInit) {
 | |
| +      await browserWindow.gBrowserInit.idleTasksFinishedPromise;
 | |
| +    }
 | |
| +    Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
 | |
| +  }
 | |
| +
 | |
| +  async grantPermissions({browserContextId, origin, permissions}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).grantPermissions(origin, permissions);
 | |
| +  }
 | |
| +
 | |
| +  resetPermissions({browserContextId}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).resetPermissions();
 | |
| +  }
 | |
| +
 | |
| +  setExtraHTTPHeaders({browserContextId, headers}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).extraHTTPHeaders = headers;
 | |
| +  }
 | |
| +
 | |
| +  setHTTPCredentials({browserContextId, credentials}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).httpCredentials = nullToUndefined(credentials);
 | |
| +  }
 | |
| +
 | |
| +  setRequestInterception({browserContextId, enabled}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).requestInterceptionEnabled = enabled;
 | |
| +  }
 | |
| +
 | |
| +  setIgnoreHTTPSErrors({browserContextId, ignoreHTTPSErrors}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).setIgnoreHTTPSErrors(nullToUndefined(ignoreHTTPSErrors));
 | |
| +  }
 | |
| +
 | |
| +  setDownloadOptions({browserContextId, downloadOptions}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).downloadOptions = nullToUndefined(downloadOptions);
 | |
| +  }
 | |
| +
 | |
| +  async setGeolocationOverride({browserContextId, geolocation}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('geolocation', nullToUndefined(geolocation));
 | |
| +  }
 | |
| +
 | |
| +  async setOnlineOverride({browserContextId, override}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('onlineOverride', nullToUndefined(override));
 | |
| +  }
 | |
| +
 | |
| +  async setColorScheme({browserContextId, colorScheme}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('colorScheme', nullToUndefined(colorScheme));
 | |
| +  }
 | |
| +
 | |
| +  async setUserAgentOverride({browserContextId, userAgent}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('userAgent', nullToUndefined(userAgent));
 | |
| +  }
 | |
| +
 | |
| +  async setBypassCSP({browserContextId, bypassCSP}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('bypassCSP', nullToUndefined(bypassCSP));
 | |
| +  }
 | |
| +
 | |
| +  async setJavaScriptDisabled({browserContextId, javaScriptDisabled}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('javaScriptDisabled', nullToUndefined(javaScriptDisabled));
 | |
| +  }
 | |
| +
 | |
| +  async setLocaleOverride({browserContextId, locale}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('locale', nullToUndefined(locale));
 | |
| +  }
 | |
| +
 | |
| +  async setTimezoneOverride({browserContextId, timezoneId}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('timezoneId', nullToUndefined(timezoneId));
 | |
| +  }
 | |
| +
 | |
| +  async setTouchOverride({browserContextId, hasTouch}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).applySetting('hasTouch', nullToUndefined(hasTouch));
 | |
| +  }
 | |
| +
 | |
| +  async setDefaultViewport({browserContextId, viewport}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).setDefaultViewport(nullToUndefined(viewport));
 | |
| +  }
 | |
| +
 | |
| +  async addScriptToEvaluateOnNewDocument({browserContextId, script}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).addScriptToEvaluateOnNewDocument(script);
 | |
| +  }
 | |
| +
 | |
| +  async addBinding({browserContextId, name, script}) {
 | |
| +    await this._targetRegistry.browserContextForId(browserContextId).addBinding(name, script);
 | |
| +  }
 | |
| +
 | |
| +  setCookies({browserContextId, cookies}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).setCookies(cookies);
 | |
| +  }
 | |
| +
 | |
| +  clearCookies({browserContextId}) {
 | |
| +    this._targetRegistry.browserContextForId(browserContextId).clearCookies();
 | |
| +  }
 | |
| +
 | |
| +  getCookies({browserContextId}) {
 | |
| +    const cookies = this._targetRegistry.browserContextForId(browserContextId).getCookies();
 | |
| +    return {cookies};
 | |
| +  }
 | |
| +
 | |
| +  async getInfo() {
 | |
| +    const version = Components.classes["@mozilla.org/xre/app-info;1"]
 | |
| +                              .getService(Components.interfaces.nsIXULAppInfo)
 | |
| +                              .version;
 | |
| +    const userAgent = Components.classes["@mozilla.org/network/protocol;1?name=http"]
 | |
| +                                .getService(Components.interfaces.nsIHttpProtocolHandler)
 | |
| +                                .userAgent;
 | |
| +    return {version: 'Firefox/' + version, userAgent};
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +function nullToUndefined(value) {
 | |
| +  return value === null ? undefined : value;
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['BrowserHandler'];
 | |
| +this.BrowserHandler = BrowserHandler;
 | |
| diff --git a/juggler/protocol/Dispatcher.js b/juggler/protocol/Dispatcher.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..0b28a9568877d99967b2ad845df3eb5904a7a508
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/Dispatcher.js
 | |
| @@ -0,0 +1,135 @@
 | |
| +const {protocol, checkScheme} = ChromeUtils.import("chrome://juggler/content/protocol/Protocol.js");
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class Dispatcher {
 | |
| +  /**
 | |
| +   * @param {Connection} connection
 | |
| +   */
 | |
| +  constructor(connection) {
 | |
| +    this._connection = connection;
 | |
| +    this._connection.onmessage = this._dispatch.bind(this);
 | |
| +    this._connection.onclose = this._dispose.bind(this);
 | |
| +    this._sessions = new Map();
 | |
| +    this._rootSession = new ProtocolSession(this, undefined);
 | |
| +  }
 | |
| +
 | |
| +  rootSession() {
 | |
| +    return this._rootSession;
 | |
| +  }
 | |
| +
 | |
| +  createSession() {
 | |
| +    const session = new ProtocolSession(this, helper.generateId());
 | |
| +    this._sessions.set(session.sessionId(), session);
 | |
| +    return session;
 | |
| +  }
 | |
| +
 | |
| +  destroySession(session) {
 | |
| +    session.dispose();
 | |
| +    this._sessions.delete(session.sessionId());
 | |
| +  }
 | |
| +
 | |
| +  _dispose() {
 | |
| +    this._connection.onmessage = null;
 | |
| +    this._connection.onclose = null;
 | |
| +    this._rootSession.dispose();
 | |
| +    this._rootSession = null;
 | |
| +    this._sessions.clear();
 | |
| +  }
 | |
| +
 | |
| +  async _dispatch(event) {
 | |
| +    const data = JSON.parse(event.data);
 | |
| +    const id = data.id;
 | |
| +    const sessionId = data.sessionId;
 | |
| +    delete data.sessionId;
 | |
| +    try {
 | |
| +      const session = sessionId ? this._sessions.get(sessionId) : this._rootSession;
 | |
| +      if (!session)
 | |
| +        throw new Error(`ERROR: cannot find session with id "${sessionId}"`);
 | |
| +      const method = data.method;
 | |
| +      const params = data.params || {};
 | |
| +      if (!id)
 | |
| +        throw new Error(`ERROR: every message must have an 'id' parameter`);
 | |
| +      if (!method)
 | |
| +        throw new Error(`ERROR: every message must have a 'method' parameter`);
 | |
| +
 | |
| +      const [domain, methodName] = method.split('.');
 | |
| +      const descriptor = protocol.domains[domain] ? protocol.domains[domain].methods[methodName] : null;
 | |
| +      if (!descriptor)
 | |
| +        throw new Error(`ERROR: method '${method}' is not supported`);
 | |
| +      let details = {};
 | |
| +      if (!checkScheme(descriptor.params || {}, params, details))
 | |
| +        throw new Error(`ERROR: failed to call method '${method}' with parameters ${JSON.stringify(params, null, 2)}\n${details.error}`);
 | |
| +
 | |
| +      const result = await session.dispatch(domain, methodName, params);
 | |
| +
 | |
| +      details = {};
 | |
| +      if ((descriptor.returns || result) && !checkScheme(descriptor.returns, result, details))
 | |
| +        throw new Error(`ERROR: failed to dispatch method '${method}' result ${JSON.stringify(result, null, 2)}\n${details.error}`);
 | |
| +
 | |
| +      this._connection.send(JSON.stringify({id, sessionId, result}));
 | |
| +    } catch (e) {
 | |
| +      this._connection.send(JSON.stringify({id, sessionId, error: {
 | |
| +        message: e.message,
 | |
| +        data: e.stack
 | |
| +      }}));
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  _emitEvent(sessionId, eventName, params) {
 | |
| +    const [domain, eName] = eventName.split('.');
 | |
| +    const scheme = protocol.domains[domain] ? protocol.domains[domain].events[eName] : null;
 | |
| +    if (!scheme)
 | |
| +      throw new Error(`ERROR: event '${eventName}' is not supported`);
 | |
| +    const details = {};
 | |
| +    if (!checkScheme(scheme, params || {}, details))
 | |
| +      throw new Error(`ERROR: failed to emit event '${eventName}' ${JSON.stringify(params, null, 2)}\n${details.error}`);
 | |
| +    this._connection.send(JSON.stringify({method: eventName, params, sessionId}));
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class ProtocolSession {
 | |
| +  constructor(dispatcher, sessionId) {
 | |
| +    this._sessionId = sessionId;
 | |
| +    this._dispatcher = dispatcher;
 | |
| +    this._handlers = new Map();
 | |
| +  }
 | |
| +
 | |
| +  sessionId() {
 | |
| +    return this._sessionId;
 | |
| +  }
 | |
| +
 | |
| +  registerHandler(domainName, handler) {
 | |
| +    this._handlers.set(domainName, handler);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    for (const [domainName, handler] of this._handlers) {
 | |
| +      if (typeof handler.dispose !== 'function')
 | |
| +        throw new Error(`Handler for "${domainName}" domain does not define |dispose| method!`);
 | |
| +      handler.dispose();
 | |
| +    }
 | |
| +    this._handlers.clear();
 | |
| +    this._dispatcher = null;
 | |
| +  }
 | |
| +
 | |
| +  emitEvent(eventName, params) {
 | |
| +    if (!this._dispatcher)
 | |
| +      throw new Error(`Session has been disposed.`);
 | |
| +    this._dispatcher._emitEvent(this._sessionId, eventName, params);
 | |
| +  }
 | |
| +
 | |
| +  async dispatch(domainName, methodName, params) {
 | |
| +    const handler = this._handlers.get(domainName);
 | |
| +    if (!handler)
 | |
| +      throw new Error(`Domain "${domainName}" does not exist`);
 | |
| +    if (!handler[methodName])
 | |
| +      throw new Error(`Handler for domain "${domainName}" does not implement method "${methodName}"`);
 | |
| +    return await handler[methodName](params);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +this.EXPORTED_SYMBOLS = ['Dispatcher'];
 | |
| +this.Dispatcher = Dispatcher;
 | |
| +
 | |
| diff --git a/juggler/protocol/NetworkHandler.js b/juggler/protocol/NetworkHandler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..10ce1e9eb24879426ca11a21ffeb89f3567ea078
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/NetworkHandler.js
 | |
| @@ -0,0 +1,158 @@
 | |
| +"use strict";
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
 | |
| +
 | |
| +const Cc = Components.classes;
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cu = Components.utils;
 | |
| +const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class NetworkHandler {
 | |
| +  constructor(target, session, contentChannel) {
 | |
| +    this._session = session;
 | |
| +    this._contentPage = contentChannel.connect(session.sessionId() + 'page');
 | |
| +    this._httpActivity = new Map();
 | |
| +    this._enabled = false;
 | |
| +    this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
 | |
| +    this._requestInterception = false;
 | |
| +    this._eventListeners = [];
 | |
| +    this._pendingRequstWillBeSentEvents = new Set();
 | |
| +    this._requestIdToFrameId = new Map();
 | |
| +  }
 | |
| +
 | |
| +  async enable() {
 | |
| +    if (this._enabled)
 | |
| +      return;
 | |
| +    this._enabled = true;
 | |
| +    this._eventListeners = [
 | |
| +      helper.on(this._pageNetwork, PageNetwork.Events.Request, this._onRequest.bind(this)),
 | |
| +      helper.on(this._pageNetwork, PageNetwork.Events.Response, this._onResponse.bind(this)),
 | |
| +      helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._onRequestFinished.bind(this)),
 | |
| +      helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._onRequestFailed.bind(this)),
 | |
| +      this._pageNetwork.addSession(),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  async getResponseBody({requestId}) {
 | |
| +    return this._pageNetwork.getResponseBody(requestId);
 | |
| +  }
 | |
| +
 | |
| +  async setExtraHTTPHeaders({headers}) {
 | |
| +    this._pageNetwork.setExtraHTTPHeaders(headers);
 | |
| +  }
 | |
| +
 | |
| +  async setRequestInterception({enabled}) {
 | |
| +    if (enabled)
 | |
| +      this._pageNetwork.enableRequestInterception();
 | |
| +    else
 | |
| +    this._pageNetwork.disableRequestInterception();
 | |
| +    // Right after we enable/disable request interception we need to await all pending
 | |
| +    // requestWillBeSent events before successfully returning from the method.
 | |
| +    await Promise.all(Array.from(this._pendingRequstWillBeSentEvents));
 | |
| +  }
 | |
| +
 | |
| +  async resumeInterceptedRequest({requestId, method, headers, postData}) {
 | |
| +    this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData);
 | |
| +  }
 | |
| +
 | |
| +  async abortInterceptedRequest({requestId, errorCode}) {
 | |
| +    this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
 | |
| +  }
 | |
| +
 | |
| +  async fulfillInterceptedRequest({requestId, status, statusText, headers, base64body}) {
 | |
| +    this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._contentPage.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +
 | |
| +  _ensureHTTPActivity(requestId) {
 | |
| +    let activity = this._httpActivity.get(requestId);
 | |
| +    if (!activity) {
 | |
| +      activity = {
 | |
| +        _id: requestId,
 | |
| +        _lastSentEvent: null,
 | |
| +        request: null,
 | |
| +        response: null,
 | |
| +        complete: null,
 | |
| +        failed: null,
 | |
| +      };
 | |
| +      this._httpActivity.set(requestId, activity);
 | |
| +    }
 | |
| +    return activity;
 | |
| +  }
 | |
| +
 | |
| +  _reportHTTPAcitivityEvents(activity) {
 | |
| +    // State machine - sending network events.
 | |
| +    if (!activity._lastSentEvent && activity.request) {
 | |
| +      this._session.emitEvent('Network.requestWillBeSent', activity.request);
 | |
| +      activity._lastSentEvent = 'requestWillBeSent';
 | |
| +    }
 | |
| +    if (activity._lastSentEvent === 'requestWillBeSent' && activity.response) {
 | |
| +      this._session.emitEvent('Network.responseReceived', activity.response);
 | |
| +      activity._lastSentEvent = 'responseReceived';
 | |
| +    }
 | |
| +    if (activity._lastSentEvent === 'responseReceived' && activity.complete) {
 | |
| +      this._session.emitEvent('Network.requestFinished', activity.complete);
 | |
| +      activity._lastSentEvent = 'requestFinished';
 | |
| +    }
 | |
| +    if (activity._lastSentEvent && activity.failed) {
 | |
| +      this._session.emitEvent('Network.requestFailed', activity.failed);
 | |
| +      activity._lastSentEvent = 'requestFailed';
 | |
| +    }
 | |
| +
 | |
| +    // Clean up if request lifecycle is over.
 | |
| +    if (activity._lastSentEvent === 'requestFinished' || activity._lastSentEvent === 'requestFailed')
 | |
| +      this._httpActivity.delete(activity._id);
 | |
| +  }
 | |
| +
 | |
| +  async _onRequest(httpChannel, eventDetails) {
 | |
| +    let pendingRequestCallback;
 | |
| +    let pendingRequestPromise = new Promise(x => pendingRequestCallback = x);
 | |
| +    this._pendingRequstWillBeSentEvents.add(pendingRequestPromise);
 | |
| +    let details = null;
 | |
| +    try {
 | |
| +      details = await this._contentPage.send('requestDetails', {channelId: httpChannel.channelId});
 | |
| +    } catch (e) {
 | |
| +      pendingRequestCallback();
 | |
| +      this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
 | |
| +      return;
 | |
| +    }
 | |
| +    // Inherit frameId for redirects when details are not available.
 | |
| +    const frameId = details ? details.frameId : (eventDetails.redirectedFrom ? this._requestIdToFrameId.get(eventDetails.redirectedFrom) : undefined);
 | |
| +    this._requestIdToFrameId.set(eventDetails.requestId, frameId);
 | |
| +    const activity = this._ensureHTTPActivity(eventDetails.requestId);
 | |
| +    activity.request = {
 | |
| +      frameId,
 | |
| +      ...eventDetails,
 | |
| +    };
 | |
| +    this._reportHTTPAcitivityEvents(activity);
 | |
| +    pendingRequestCallback();
 | |
| +    this._pendingRequstWillBeSentEvents.delete(pendingRequestPromise);
 | |
| +  }
 | |
| +
 | |
| +  async _onResponse(httpChannel, eventDetails) {
 | |
| +    const activity = this._ensureHTTPActivity(eventDetails.requestId);
 | |
| +    activity.response = eventDetails;
 | |
| +    this._reportHTTPAcitivityEvents(activity);
 | |
| +  }
 | |
| +
 | |
| +  async _onRequestFinished(httpChannel, eventDetails) {
 | |
| +    const activity = this._ensureHTTPActivity(eventDetails.requestId);
 | |
| +    activity.complete = eventDetails;
 | |
| +    this._reportHTTPAcitivityEvents(activity);
 | |
| +  }
 | |
| +
 | |
| +  async _onRequestFailed(httpChannel, eventDetails) {
 | |
| +    const activity = this._ensureHTTPActivity(eventDetails.requestId);
 | |
| +    activity.failed = eventDetails;
 | |
| +    this._reportHTTPAcitivityEvents(activity);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['NetworkHandler'];
 | |
| +this.NetworkHandler = NetworkHandler;
 | |
| diff --git a/juggler/protocol/PageHandler.js b/juggler/protocol/PageHandler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..2ad037e57ac4b0b97d85c55bcd08489d840205c1
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/PageHandler.js
 | |
| @@ -0,0 +1,341 @@
 | |
| +"use strict";
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +
 | |
| +const Cc = Components.classes;
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cu = Components.utils;
 | |
| +const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class WorkerHandler {
 | |
| +  constructor(session, contentChannel, workerId) {
 | |
| +    this._session = session;
 | |
| +    this._contentWorker = contentChannel.connect(session.sessionId() + workerId);
 | |
| +    this._workerId = workerId;
 | |
| +
 | |
| +    const emitWrappedProtocolEvent = eventName => {
 | |
| +      return params => {
 | |
| +        this._session.emitEvent('Page.dispatchMessageFromWorker', {
 | |
| +          workerId,
 | |
| +          message: JSON.stringify({method: eventName, params}),
 | |
| +        });
 | |
| +      }
 | |
| +    }
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      contentChannel.register(session.sessionId() + workerId, {
 | |
| +        runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
 | |
| +        runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
 | |
| +        runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
 | |
| +      }),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  async sendMessage(message) {
 | |
| +    const [domain, method] = message.method.split('.');
 | |
| +    if (domain !== 'Runtime')
 | |
| +      throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
 | |
| +    const result = await this._contentWorker.send(method, message.params);
 | |
| +    this._session.emitEvent('Page.dispatchMessageFromWorker', {
 | |
| +      workerId: this._workerId,
 | |
| +      message: JSON.stringify({result, id: message.id}),
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._contentWorker.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class PageHandler {
 | |
| +  constructor(target, session, contentChannel) {
 | |
| +    this._session = session;
 | |
| +    this._contentChannel = contentChannel;
 | |
| +    this._contentPage = contentChannel.connect(session.sessionId() + 'page');
 | |
| +    this._workers = new Map();
 | |
| +
 | |
| +    const emitProtocolEvent = eventName => {
 | |
| +      return (...args) => this._session.emitEvent(eventName, ...args);
 | |
| +    }
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      contentChannel.register(session.sessionId() + 'page', {
 | |
| +        pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
 | |
| +        pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
 | |
| +        pageEventFired: emitProtocolEvent('Page.eventFired'),
 | |
| +        pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
 | |
| +        pageFrameAttached: emitProtocolEvent('Page.frameAttached'),
 | |
| +        pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
 | |
| +        pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
 | |
| +        pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
 | |
| +        pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
 | |
| +        pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
 | |
| +        pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
 | |
| +        pageReady: emitProtocolEvent('Page.ready'),
 | |
| +        pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
 | |
| +        pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
 | |
| +        pageWorkerCreated: this._onWorkerCreated.bind(this),
 | |
| +        pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
 | |
| +      }),
 | |
| +    ];
 | |
| +    this._pageTarget = target;
 | |
| +    this._browser = target.linkedBrowser();
 | |
| +    this._dialogs = new Map();
 | |
| +
 | |
| +    this._enabled = false;
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerCreated({workerId, frameId, url}) {
 | |
| +    const worker = new WorkerHandler(this._session, this._contentChannel, workerId);
 | |
| +    this._workers.set(workerId, worker);
 | |
| +    this._session.emitEvent('Page.workerCreated', {workerId, frameId, url});
 | |
| +  }
 | |
| +
 | |
| +  _onWorkerDestroyed({workerId}) {
 | |
| +    const worker = this._workers.get(workerId);
 | |
| +    if (!worker)
 | |
| +      return;
 | |
| +    this._workers.delete(workerId);
 | |
| +    worker.dispose();
 | |
| +    this._session.emitEvent('Page.workerDestroyed', {workerId});
 | |
| +  }
 | |
| +
 | |
| +  async close({runBeforeUnload}) {
 | |
| +    // Postpone target close to deliver response in session.
 | |
| +    Services.tm.dispatchToMainThread(() => {
 | |
| +      this._pageTarget.close(runBeforeUnload);
 | |
| +    });
 | |
| +  }
 | |
| +
 | |
| +  async enable() {
 | |
| +    if (this._enabled)
 | |
| +      return;
 | |
| +    this._enabled = true;
 | |
| +    this._updateModalDialogs();
 | |
| +
 | |
| +    this._eventListeners.push(...[
 | |
| +      helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => {
 | |
| +        // wait for the dialog to be actually added to DOM.
 | |
| +        await Promise.resolve();
 | |
| +        this._updateModalDialogs();
 | |
| +      }),
 | |
| +      helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
 | |
| +      helper.on(this._pageTarget, 'crashed', () => {
 | |
| +        this._session.emitEvent('Page.crashed', {});
 | |
| +      }),
 | |
| +    ]);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._contentPage.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +
 | |
| +  async setViewportSize({viewportSize}) {
 | |
| +    await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
 | |
| +  }
 | |
| +
 | |
| +  _updateModalDialogs() {
 | |
| +    const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []);
 | |
| +    for (const dialog of this._dialogs.values()) {
 | |
| +      if (!prompts.has(dialog.prompt())) {
 | |
| +        this._dialogs.delete(dialog.id());
 | |
| +        this._session.emitEvent('Page.dialogClosed', {
 | |
| +          dialogId: dialog.id(),
 | |
| +        });
 | |
| +      } else {
 | |
| +        prompts.delete(dialog.prompt());
 | |
| +      }
 | |
| +    }
 | |
| +    for (const prompt of prompts) {
 | |
| +      const dialog = Dialog.createIfSupported(prompt);
 | |
| +      if (!dialog)
 | |
| +        continue;
 | |
| +      this._dialogs.set(dialog.id(), dialog);
 | |
| +      this._session.emitEvent('Page.dialogOpened', {
 | |
| +        dialogId: dialog.id(),
 | |
| +        type: dialog.type(),
 | |
| +        message: dialog.message(),
 | |
| +        defaultValue: dialog.defaultValue(),
 | |
| +      });
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  async setFileInputFiles(options) {
 | |
| +    return await this._contentPage.send('setFileInputFiles', options);
 | |
| +  }
 | |
| +
 | |
| +  async setEmulatedMedia(options) {
 | |
| +    return await this._contentPage.send('setEmulatedMedia', options);
 | |
| +  }
 | |
| +
 | |
| +  async setCacheDisabled(options) {
 | |
| +    return await this._contentPage.send('setCacheDisabled', options);
 | |
| +  }
 | |
| +
 | |
| +  async addBinding(options) {
 | |
| +    return await this._contentPage.send('addBinding', options);
 | |
| +  }
 | |
| +
 | |
| +  async adoptNode(options) {
 | |
| +    return await this._contentPage.send('adoptNode', options);
 | |
| +  }
 | |
| +
 | |
| +  async screenshot(options) {
 | |
| +    return await this._contentPage.send('screenshot', options);
 | |
| +  }
 | |
| +
 | |
| +  async getBoundingBox(options) {
 | |
| +    return await this._contentPage.send('getBoundingBox', options);
 | |
| +  }
 | |
| +
 | |
| +  async getContentQuads(options) {
 | |
| +    return await this._contentPage.send('getContentQuads', options);
 | |
| +  }
 | |
| +
 | |
| +  /**
 | |
| +   * @param {{frameId: string, url: string}} options
 | |
| +   */
 | |
| +  async navigate(options) {
 | |
| +    return await this._contentPage.send('navigate', options);
 | |
| +  }
 | |
| +
 | |
| +  /**
 | |
| +   * @param {{frameId: string, url: string}} options
 | |
| +   */
 | |
| +  async goBack(options) {
 | |
| +    return await this._contentPage.send('goBack', options);
 | |
| +  }
 | |
| +
 | |
| +  /**
 | |
| +   * @param {{frameId: string, url: string}} options
 | |
| +   */
 | |
| +  async goForward(options) {
 | |
| +    return await this._contentPage.send('goForward', options);
 | |
| +  }
 | |
| +
 | |
| +  /**
 | |
| +   * @param {{frameId: string, url: string}} options
 | |
| +   */
 | |
| +  async reload(options) {
 | |
| +    return await this._contentPage.send('reload', options);
 | |
| +  }
 | |
| +
 | |
| +  async describeNode(options) {
 | |
| +    return await this._contentPage.send('describeNode', options);
 | |
| +  }
 | |
| +
 | |
| +  async scrollIntoViewIfNeeded(options) {
 | |
| +    return await this._contentPage.send('scrollIntoViewIfNeeded', options);
 | |
| +  }
 | |
| +
 | |
| +  async addScriptToEvaluateOnNewDocument(options) {
 | |
| +    return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
 | |
| +  }
 | |
| +
 | |
| +  async removeScriptToEvaluateOnNewDocument(options) {
 | |
| +    return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
 | |
| +  }
 | |
| +
 | |
| +  async dispatchKeyEvent(options) {
 | |
| +    return await this._contentPage.send('dispatchKeyEvent', options);
 | |
| +  }
 | |
| +
 | |
| +  async dispatchTouchEvent(options) {
 | |
| +    return await this._contentPage.send('dispatchTouchEvent', options);
 | |
| +  }
 | |
| +
 | |
| +  async dispatchMouseEvent(options) {
 | |
| +    return await this._contentPage.send('dispatchMouseEvent', options);
 | |
| +  }
 | |
| +
 | |
| +  async insertText(options) {
 | |
| +    return await this._contentPage.send('insertText', options);
 | |
| +  }
 | |
| +
 | |
| +  async crash(options) {
 | |
| +    return await this._contentPage.send('crash', options);
 | |
| +  }
 | |
| +
 | |
| +  async handleDialog({dialogId, accept, promptText}) {
 | |
| +    const dialog = this._dialogs.get(dialogId);
 | |
| +    if (!dialog)
 | |
| +      throw new Error('Failed to find dialog with id = ' + dialogId);
 | |
| +    if (accept)
 | |
| +      dialog.accept(promptText);
 | |
| +    else
 | |
| +      dialog.dismiss();
 | |
| +  }
 | |
| +
 | |
| +  async setInterceptFileChooserDialog(options) {
 | |
| +    return await this._contentPage.send('setInterceptFileChooserDialog', options);
 | |
| +  }
 | |
| +
 | |
| +  async sendMessageToWorker({workerId, message}) {
 | |
| +    const worker = this._workers.get(workerId);
 | |
| +    if (!worker)
 | |
| +      throw new Error('ERROR: cannot find worker with id ' + workerId);
 | |
| +    return await worker.sendMessage(JSON.parse(message));
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +class Dialog {
 | |
| +  static createIfSupported(prompt) {
 | |
| +    const type = prompt.args.promptType;
 | |
| +    switch (type) {
 | |
| +      case 'alert':
 | |
| +      case 'prompt':
 | |
| +      case 'confirm':
 | |
| +        return new Dialog(prompt, type);
 | |
| +      case 'confirmEx':
 | |
| +        return new Dialog(prompt, 'beforeunload');
 | |
| +      default:
 | |
| +        return null;
 | |
| +    };
 | |
| +  }
 | |
| +
 | |
| +  constructor(prompt, type) {
 | |
| +    this._id = helper.generateId();
 | |
| +    this._type = type;
 | |
| +    this._prompt = prompt;
 | |
| +  }
 | |
| +
 | |
| +  id() {
 | |
| +    return this._id;
 | |
| +  }
 | |
| +
 | |
| +  message() {
 | |
| +    return this._prompt.ui.infoBody.textContent;
 | |
| +  }
 | |
| +
 | |
| +  type() {
 | |
| +    return this._type;
 | |
| +  }
 | |
| +
 | |
| +  prompt() {
 | |
| +    return this._prompt;
 | |
| +  }
 | |
| +
 | |
| +  dismiss() {
 | |
| +    if (this._prompt.ui.button1)
 | |
| +      this._prompt.ui.button1.click();
 | |
| +    else
 | |
| +      this._prompt.ui.button0.click();
 | |
| +  }
 | |
| +
 | |
| +  defaultValue() {
 | |
| +    return this._prompt.ui.loginTextbox.value;
 | |
| +  }
 | |
| +
 | |
| +  accept(promptValue) {
 | |
| +    if (typeof promptValue === 'string' && this._type === 'prompt')
 | |
| +      this._prompt.ui.loginTextbox.value = promptValue;
 | |
| +    this._prompt.ui.button0.click();
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['PageHandler'];
 | |
| +this.PageHandler = PageHandler;
 | |
| diff --git a/juggler/protocol/PrimitiveTypes.js b/juggler/protocol/PrimitiveTypes.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..78b6601b91d0b7fcda61114e6846aa07f95a06fa
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/PrimitiveTypes.js
 | |
| @@ -0,0 +1,143 @@
 | |
| +const t = {};
 | |
| +
 | |
| +t.String = function(x, details = {}, path = ['<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
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..e6bb9a8d5b553531a2ee22e27e693dd0569bda22
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/Protocol.js
 | |
| @@ -0,0 +1,845 @@
 | |
| +const {t, checkScheme} = ChromeUtils.import('chrome://juggler/content/protocol/PrimitiveTypes.js');
 | |
| +
 | |
| +// Protocol-specific types.
 | |
| +const browserTypes = {};
 | |
| +
 | |
| +browserTypes.TargetInfo = {
 | |
| +  type: t.Enum(['page']),
 | |
| +  targetId: t.String,
 | |
| +  browserContextId: t.Optional(t.String),
 | |
| +  // PageId of parent tab, if any.
 | |
| +  openerId: t.Optional(t.String),
 | |
| +};
 | |
| +
 | |
| +browserTypes.CookieOptions = {
 | |
| +  name: t.String,
 | |
| +  value: t.String,
 | |
| +  url: t.Optional(t.String),
 | |
| +  domain: t.Optional(t.String),
 | |
| +  path: t.Optional(t.String),
 | |
| +  secure: t.Optional(t.Boolean),
 | |
| +  httpOnly: t.Optional(t.Boolean),
 | |
| +  sameSite: t.Optional(t.Enum(['Strict', 'Lax', 'None'])),
 | |
| +  expires: t.Optional(t.Number),
 | |
| +};
 | |
| +
 | |
| +browserTypes.Cookie = {
 | |
| +  name: t.String,
 | |
| +  domain: t.String,
 | |
| +  path: t.String,
 | |
| +  value: t.String,
 | |
| +  expires: t.Number,
 | |
| +  size: t.Number,
 | |
| +  httpOnly: t.Boolean,
 | |
| +  secure: t.Boolean,
 | |
| +  session: t.Boolean,
 | |
| +  sameSite: t.Enum(['Strict', 'Lax', 'None']),
 | |
| +};
 | |
| +
 | |
| +browserTypes.Geolocation = {
 | |
| +  latitude: t.Number,
 | |
| +  longitude: t.Number,
 | |
| +  accuracy: t.Optional(t.Number),
 | |
| +};
 | |
| +
 | |
| +browserTypes.DownloadOptions = {
 | |
| +  behavior: t.Optional(t.Enum(['saveToDisk', 'cancel'])),
 | |
| +  downloadsDir: t.Optional(t.String),
 | |
| +};
 | |
| +
 | |
| +const pageTypes = {};
 | |
| +pageTypes.DOMPoint = {
 | |
| +  x: t.Number,
 | |
| +  y: t.Number,
 | |
| +};
 | |
| +
 | |
| +pageTypes.Rect = {
 | |
| +  x: t.Number,
 | |
| +  y: t.Number,
 | |
| +  width: t.Number,
 | |
| +  height: t.Number,
 | |
| +};
 | |
| +
 | |
| +pageTypes.Size = {
 | |
| +  width: t.Number,
 | |
| +  height: t.Number,
 | |
| +};
 | |
| +
 | |
| +pageTypes.Viewport = {
 | |
| +  viewportSize: pageTypes.Size,
 | |
| +  deviceScaleFactor: t.Number,
 | |
| +};
 | |
| +
 | |
| +pageTypes.DOMQuad = {
 | |
| +  p1: pageTypes.DOMPoint,
 | |
| +  p2: pageTypes.DOMPoint,
 | |
| +  p3: pageTypes.DOMPoint,
 | |
| +  p4: pageTypes.DOMPoint,
 | |
| +};
 | |
| +
 | |
| +pageTypes.TouchPoint = {
 | |
| +  x: t.Number,
 | |
| +  y: t.Number,
 | |
| +  radiusX: t.Optional(t.Number),
 | |
| +  radiusY: t.Optional(t.Number),
 | |
| +  rotationAngle: t.Optional(t.Number),
 | |
| +  force: t.Optional(t.Number),
 | |
| +};
 | |
| +
 | |
| +pageTypes.Clip = {
 | |
| +  x: t.Number,
 | |
| +  y: t.Number,
 | |
| +  width: t.Number,
 | |
| +  height: t.Number,
 | |
| +};
 | |
| +
 | |
| +
 | |
| +const runtimeTypes = {};
 | |
| +runtimeTypes.RemoteObject = {
 | |
| +  type: t.Optional(t.Enum(['object', 'function', 'undefined', 'string', 'number', 'boolean', 'symbol', 'bigint'])),
 | |
| +  subtype: t.Optional(t.Enum(['array', 'null', 'node', 'regexp', 'date', 'map', 'set', 'weakmap', 'weakset', 'error', 'proxy', 'promise', 'typedarray'])),
 | |
| +  objectId: t.Optional(t.String),
 | |
| +  unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
 | |
| +  value: t.Any
 | |
| +};
 | |
| +
 | |
| +runtimeTypes.ObjectProperty = {
 | |
| +  name: t.String,
 | |
| +  value: runtimeTypes.RemoteObject,
 | |
| +};
 | |
| +
 | |
| +runtimeTypes.ScriptLocation = {
 | |
| +  columnNumber: t.Number,
 | |
| +  lineNumber: t.Number,
 | |
| +  url: t.String,
 | |
| +};
 | |
| +
 | |
| +runtimeTypes.ExceptionDetails = {
 | |
| +  text: t.Optional(t.String),
 | |
| +  stack: t.Optional(t.String),
 | |
| +  value: t.Optional(t.Any),
 | |
| +};
 | |
| +
 | |
| +runtimeTypes.CallFunctionArgument = {
 | |
| +  objectId: t.Optional(t.String),
 | |
| +  unserializableValue: t.Optional(t.Enum(['Infinity', '-Infinity', '-0', 'NaN'])),
 | |
| +  value: t.Any,
 | |
| +};
 | |
| +
 | |
| +const axTypes = {};
 | |
| +axTypes.AXTree = {
 | |
| +  role: t.String,
 | |
| +  name: t.String,
 | |
| +  children: t.Optional(t.Array(t.Recursive(axTypes, 'AXTree'))),
 | |
| +
 | |
| +  selected: t.Optional(t.Boolean),
 | |
| +  focused: t.Optional(t.Boolean),
 | |
| +  pressed: t.Optional(t.Boolean),
 | |
| +  focusable: t.Optional(t.Boolean),
 | |
| +  haspopup: t.Optional(t.Boolean),
 | |
| +  required: t.Optional(t.Boolean),
 | |
| +  invalid: t.Optional(t.Boolean),
 | |
| +  modal: t.Optional(t.Boolean),
 | |
| +  editable: t.Optional(t.Boolean),
 | |
| +  busy: t.Optional(t.Boolean),
 | |
| +  multiline: t.Optional(t.Boolean),
 | |
| +  readonly: t.Optional(t.Boolean),
 | |
| +  checked: t.Optional(t.Enum(['mixed', true])),
 | |
| +  expanded: t.Optional(t.Boolean),
 | |
| +  disabled: t.Optional(t.Boolean),
 | |
| +  multiselectable: t.Optional(t.Boolean),
 | |
| +
 | |
| +  value: t.Optional(t.String),
 | |
| +  description: t.Optional(t.String),
 | |
| +
 | |
| +  value: t.Optional(t.String),
 | |
| +  roledescription: t.Optional(t.String),
 | |
| +  valuetext: t.Optional(t.String),
 | |
| +  orientation: t.Optional(t.String),
 | |
| +  autocomplete: t.Optional(t.String),
 | |
| +  keyshortcuts: t.Optional(t.String),
 | |
| +
 | |
| +  level: t.Optional(t.Number),
 | |
| +
 | |
| +  tag: t.Optional(t.String),
 | |
| +
 | |
| +  foundObject: t.Optional(t.Boolean),
 | |
| +}
 | |
| +
 | |
| +const networkTypes = {};
 | |
| +
 | |
| +networkTypes.HTTPHeader = {
 | |
| +  name: t.String,
 | |
| +  value: t.String,
 | |
| +};
 | |
| +
 | |
| +networkTypes.HTTPCredentials = {
 | |
| +  username: t.String,
 | |
| +  password: t.String,
 | |
| +};
 | |
| +
 | |
| +networkTypes.SecurityDetails = {
 | |
| +  protocol: t.String,
 | |
| +  subjectName: t.String,
 | |
| +  issuer: t.String,
 | |
| +  validFrom: t.Number,
 | |
| +  validTo: t.Number,
 | |
| +};
 | |
| +
 | |
| +
 | |
| +const Browser = {
 | |
| +  targets: ['browser'],
 | |
| +
 | |
| +  types: browserTypes,
 | |
| +
 | |
| +  events: {
 | |
| +    'attachedToTarget': {
 | |
| +      sessionId: t.String,
 | |
| +      targetInfo: browserTypes.TargetInfo,
 | |
| +    },
 | |
| +    'detachedFromTarget': {
 | |
| +      sessionId: t.String,
 | |
| +      targetId: t.String,
 | |
| +    },
 | |
| +    'downloadCreated': {
 | |
| +      uuid: t.String,
 | |
| +      browserContextId: t.String,
 | |
| +      pageTargetId: t.String,
 | |
| +      url: t.String,
 | |
| +      suggestedFileName: t.String,
 | |
| +    },
 | |
| +    'downloadFinished': {
 | |
| +      uuid: t.String,
 | |
| +      canceled: t.Optional(t.Boolean),
 | |
| +      error: t.Optional(t.String),
 | |
| +    },
 | |
| +  },
 | |
| +
 | |
| +  methods: {
 | |
| +    'enable': {
 | |
| +      params: {
 | |
| +        attachToDefaultContext: t.Boolean,
 | |
| +      },
 | |
| +    },
 | |
| +    'createBrowserContext': {
 | |
| +      params: {
 | |
| +        removeOnDetach: t.Optional(t.Boolean),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        browserContextId: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'removeBrowserContext': {
 | |
| +      params: {
 | |
| +        browserContextId: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'newPage': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        targetId: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'close': {},
 | |
| +    'getInfo': {
 | |
| +      returns: {
 | |
| +        userAgent: t.String,
 | |
| +        version: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'setExtraHTTPHeaders': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        headers: t.Array(networkTypes.HTTPHeader),
 | |
| +      },
 | |
| +    },
 | |
| +    'setHTTPCredentials': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        credentials: t.Nullable(networkTypes.HTTPCredentials),
 | |
| +      },
 | |
| +    },
 | |
| +    'setRequestInterception': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        enabled: t.Boolean,
 | |
| +      },
 | |
| +    },
 | |
| +    'setGeolocationOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        geolocation: t.Nullable(browserTypes.Geolocation),
 | |
| +      }
 | |
| +    },
 | |
| +    'setUserAgentOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        userAgent: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'setBypassCSP': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        bypassCSP: t.Nullable(t.Boolean),
 | |
| +      }
 | |
| +    },
 | |
| +    'setIgnoreHTTPSErrors': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        ignoreHTTPSErrors: t.Nullable(t.Boolean),
 | |
| +      }
 | |
| +    },
 | |
| +    'setJavaScriptDisabled': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        javaScriptDisabled: t.Nullable(t.Boolean),
 | |
| +      }
 | |
| +    },
 | |
| +    'setLocaleOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        locale: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'setTimezoneOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        timezoneId: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'setDownloadOptions': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        downloadOptions: t.Nullable(browserTypes.DownloadOptions),
 | |
| +      }
 | |
| +    },
 | |
| +    'setTouchOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        hasTouch: t.Nullable(t.Boolean),
 | |
| +      }
 | |
| +    },
 | |
| +    'setDefaultViewport': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        viewport: t.Nullable(pageTypes.Viewport),
 | |
| +      }
 | |
| +    },
 | |
| +    'addScriptToEvaluateOnNewDocument': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        script: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'addBinding': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        name: t.String,
 | |
| +        script: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'grantPermissions': {
 | |
| +      params: {
 | |
| +        origin: t.String,
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        permissions: t.Array(t.String),
 | |
| +      },
 | |
| +    },
 | |
| +    'resetPermissions': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'setCookies': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        cookies: t.Array(browserTypes.CookieOptions),
 | |
| +      }
 | |
| +    },
 | |
| +    'clearCookies': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'getCookies': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String)
 | |
| +      },
 | |
| +      returns: {
 | |
| +        cookies: t.Array(browserTypes.Cookie),
 | |
| +      },
 | |
| +    },
 | |
| +    'setOnlineOverride': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        override: t.Nullable(t.Enum(['online', 'offline'])),
 | |
| +      }
 | |
| +    },
 | |
| +    'setColorScheme': {
 | |
| +      params: {
 | |
| +        browserContextId: t.Optional(t.String),
 | |
| +        colorScheme: t.Nullable(t.Enum(['dark', 'light', 'no-preference'])),
 | |
| +      },
 | |
| +    },
 | |
| +  },
 | |
| +};
 | |
| +
 | |
| +const Network = {
 | |
| +  targets: ['page'],
 | |
| +  types: networkTypes,
 | |
| +  events: {
 | |
| +    'requestWillBeSent': {
 | |
| +      // frameId may be absent for redirected requests.
 | |
| +      frameId: t.Optional(t.String),
 | |
| +      requestId: t.String,
 | |
| +      // RequestID of redirected request.
 | |
| +      redirectedFrom: t.Optional(t.String),
 | |
| +      postData: t.Optional(t.String),
 | |
| +      headers: t.Array(networkTypes.HTTPHeader),
 | |
| +      isIntercepted: t.Boolean,
 | |
| +      url: t.String,
 | |
| +      method: t.String,
 | |
| +      navigationId: t.Optional(t.String),
 | |
| +      cause: t.String,
 | |
| +    },
 | |
| +    'responseReceived': {
 | |
| +      securityDetails: t.Nullable(networkTypes.SecurityDetails),
 | |
| +      requestId: t.String,
 | |
| +      fromCache: t.Boolean,
 | |
| +      remoteIPAddress: t.Optional(t.String),
 | |
| +      remotePort: t.Optional(t.Number),
 | |
| +      status: t.Number,
 | |
| +      statusText: t.String,
 | |
| +      headers: t.Array(networkTypes.HTTPHeader),
 | |
| +    },
 | |
| +    'requestFinished': {
 | |
| +      requestId: t.String,
 | |
| +    },
 | |
| +    'requestFailed': {
 | |
| +      requestId: t.String,
 | |
| +      errorCode: t.String,
 | |
| +    },
 | |
| +  },
 | |
| +  methods: {
 | |
| +    'setRequestInterception': {
 | |
| +      params: {
 | |
| +        enabled: t.Boolean,
 | |
| +      },
 | |
| +    },
 | |
| +    'setExtraHTTPHeaders': {
 | |
| +      params: {
 | |
| +        headers: t.Array(networkTypes.HTTPHeader),
 | |
| +      },
 | |
| +    },
 | |
| +    'abortInterceptedRequest': {
 | |
| +      params: {
 | |
| +        requestId: t.String,
 | |
| +        errorCode: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'resumeInterceptedRequest': {
 | |
| +      params: {
 | |
| +        requestId: t.String,
 | |
| +        method: t.Optional(t.String),
 | |
| +        headers: t.Optional(t.Array(networkTypes.HTTPHeader)),
 | |
| +        postData: t.Optional(t.String),
 | |
| +      },
 | |
| +    },
 | |
| +    'fulfillInterceptedRequest': {
 | |
| +      params: {
 | |
| +        requestId: t.String,
 | |
| +        status: t.Number,
 | |
| +        statusText: t.String,
 | |
| +        headers: t.Array(networkTypes.HTTPHeader),
 | |
| +        base64body: t.Optional(t.String),  // base64-encoded
 | |
| +      },
 | |
| +    },
 | |
| +    'getResponseBody': {
 | |
| +      params: {
 | |
| +        requestId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        base64body: t.String,
 | |
| +        evicted: t.Optional(t.Boolean),
 | |
| +      },
 | |
| +    },
 | |
| +  },
 | |
| +};
 | |
| +
 | |
| +const Runtime = {
 | |
| +  targets: ['page'],
 | |
| +  types: runtimeTypes,
 | |
| +  events: {
 | |
| +    'executionContextCreated': {
 | |
| +      executionContextId: t.String,
 | |
| +      auxData: t.Any,
 | |
| +    },
 | |
| +    'executionContextDestroyed': {
 | |
| +      executionContextId: t.String,
 | |
| +    },
 | |
| +    'console': {
 | |
| +      executionContextId: t.String,
 | |
| +      args: t.Array(runtimeTypes.RemoteObject),
 | |
| +      type: t.String,
 | |
| +      location: runtimeTypes.ScriptLocation,
 | |
| +    },
 | |
| +  },
 | |
| +  methods: {
 | |
| +    'evaluate': {
 | |
| +      params: {
 | |
| +        // Pass frameId here.
 | |
| +        executionContextId: t.String,
 | |
| +        expression: t.String,
 | |
| +        returnByValue: t.Optional(t.Boolean),
 | |
| +      },
 | |
| +
 | |
| +      returns: {
 | |
| +        result: t.Optional(runtimeTypes.RemoteObject),
 | |
| +        exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
 | |
| +      }
 | |
| +    },
 | |
| +    'callFunction': {
 | |
| +      params: {
 | |
| +        // Pass frameId here.
 | |
| +        executionContextId: t.String,
 | |
| +        functionDeclaration: t.String,
 | |
| +        returnByValue: t.Optional(t.Boolean),
 | |
| +        args: t.Array(runtimeTypes.CallFunctionArgument),
 | |
| +      },
 | |
| +
 | |
| +      returns: {
 | |
| +        result: t.Optional(runtimeTypes.RemoteObject),
 | |
| +        exceptionDetails: t.Optional(runtimeTypes.ExceptionDetails),
 | |
| +      }
 | |
| +    },
 | |
| +    'disposeObject': {
 | |
| +      params: {
 | |
| +        executionContextId: t.String,
 | |
| +        objectId: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +
 | |
| +    'getObjectProperties': {
 | |
| +      params: {
 | |
| +        executionContextId: t.String,
 | |
| +        objectId: t.String,
 | |
| +      },
 | |
| +
 | |
| +      returns: {
 | |
| +        properties: t.Array(runtimeTypes.ObjectProperty),
 | |
| +      }
 | |
| +    },
 | |
| +  },
 | |
| +};
 | |
| +
 | |
| +const Page = {
 | |
| +  targets: ['page'],
 | |
| +
 | |
| +  types: pageTypes,
 | |
| +  events: {
 | |
| +    'ready': {
 | |
| +    },
 | |
| +    'crashed': {
 | |
| +    },
 | |
| +    'eventFired': {
 | |
| +      frameId: t.String,
 | |
| +      name: t.Enum(['load', 'DOMContentLoaded']),
 | |
| +    },
 | |
| +    'uncaughtError': {
 | |
| +      frameId: t.String,
 | |
| +      message: t.String,
 | |
| +      stack: t.String,
 | |
| +    },
 | |
| +    'frameAttached': {
 | |
| +      frameId: t.String,
 | |
| +      parentFrameId: t.Optional(t.String),
 | |
| +    },
 | |
| +    'frameDetached': {
 | |
| +      frameId: t.String,
 | |
| +    },
 | |
| +    'navigationStarted': {
 | |
| +      frameId: t.String,
 | |
| +      navigationId: t.String,
 | |
| +      url: t.String,
 | |
| +    },
 | |
| +    'navigationCommitted': {
 | |
| +      frameId: t.String,
 | |
| +      // |navigationId| can only be null in response to enable.
 | |
| +      navigationId: t.Optional(t.String),
 | |
| +      url: t.String,
 | |
| +      // frame.id or frame.name
 | |
| +      name: t.String,
 | |
| +    },
 | |
| +    'navigationAborted': {
 | |
| +      frameId: t.String,
 | |
| +      navigationId: t.String,
 | |
| +      errorText: t.String,
 | |
| +    },
 | |
| +    'sameDocumentNavigation': {
 | |
| +      frameId: t.String,
 | |
| +      url: t.String,
 | |
| +    },
 | |
| +    'dialogOpened': {
 | |
| +      dialogId: t.String,
 | |
| +      type: t.Enum(['prompt', 'alert', 'confirm', 'beforeunload']),
 | |
| +      message: t.String,
 | |
| +      defaultValue: t.Optional(t.String),
 | |
| +    },
 | |
| +    'dialogClosed': {
 | |
| +      dialogId: t.String,
 | |
| +    },
 | |
| +    'bindingCalled': {
 | |
| +      executionContextId: t.String,
 | |
| +      name: t.String,
 | |
| +      payload: t.Any,
 | |
| +    },
 | |
| +    'linkClicked': {
 | |
| +      phase: t.Enum(['before', 'after']),
 | |
| +    },
 | |
| +    'willOpenNewWindowAsynchronously': {},
 | |
| +    'fileChooserOpened': {
 | |
| +      executionContextId: t.String,
 | |
| +      element: runtimeTypes.RemoteObject
 | |
| +    },
 | |
| +    'workerCreated': {
 | |
| +      workerId: t.String,
 | |
| +      frameId: t.String,
 | |
| +      url: t.String,
 | |
| +    },
 | |
| +    'workerDestroyed': {
 | |
| +      workerId: t.String,
 | |
| +    },
 | |
| +    'dispatchMessageFromWorker': {
 | |
| +      workerId: t.String,
 | |
| +      message: t.String,
 | |
| +    },
 | |
| +  },
 | |
| +
 | |
| +  methods: {
 | |
| +    'close': {
 | |
| +      params: {
 | |
| +        runBeforeUnload: t.Optional(t.Boolean),
 | |
| +      },
 | |
| +    },
 | |
| +    'setFileInputFiles': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +        files: t.Array(t.String),
 | |
| +      },
 | |
| +    },
 | |
| +    'addBinding': {
 | |
| +      params: {
 | |
| +        name: t.String,
 | |
| +        script: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'setViewportSize': {
 | |
| +      params: {
 | |
| +        viewportSize: t.Nullable(pageTypes.Size),
 | |
| +      },
 | |
| +    },
 | |
| +    'setEmulatedMedia': {
 | |
| +      params: {
 | |
| +        type: t.Optional(t.Enum(['screen', 'print', ''])),
 | |
| +        colorScheme: t.Optional(t.Enum(['dark', 'light', 'no-preference'])),
 | |
| +      },
 | |
| +    },
 | |
| +    'setCacheDisabled': {
 | |
| +      params: {
 | |
| +        cacheDisabled: t.Boolean,
 | |
| +      },
 | |
| +    },
 | |
| +    'describeNode': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        contentFrameId: t.Optional(t.String),
 | |
| +        ownerFrameId: t.Optional(t.String),
 | |
| +      },
 | |
| +    },
 | |
| +    'scrollIntoViewIfNeeded': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +        rect: t.Optional(pageTypes.Rect),
 | |
| +      },
 | |
| +    },
 | |
| +    'addScriptToEvaluateOnNewDocument': {
 | |
| +      params: {
 | |
| +        script: t.String,
 | |
| +        worldName: t.Optional(t.String),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        scriptId: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'removeScriptToEvaluateOnNewDocument': {
 | |
| +      params: {
 | |
| +        scriptId: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +    'navigate': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        url: t.String,
 | |
| +        referer: t.Optional(t.String),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        navigationId: t.Nullable(t.String),
 | |
| +        navigationURL: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'goBack': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        navigationId: t.Nullable(t.String),
 | |
| +        navigationURL: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'goForward': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        navigationId: t.Nullable(t.String),
 | |
| +        navigationURL: t.Nullable(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'reload': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        navigationId: t.String,
 | |
| +        navigationURL: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'getBoundingBox': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        boundingBox: t.Nullable(pageTypes.Rect),
 | |
| +      },
 | |
| +    },
 | |
| +    'adoptNode': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +        executionContextId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        remoteObject: t.Nullable(runtimeTypes.RemoteObject),
 | |
| +      },
 | |
| +    },
 | |
| +    'screenshot': {
 | |
| +      params: {
 | |
| +        mimeType: t.Enum(['image/png', 'image/jpeg']),
 | |
| +        fullPage: t.Optional(t.Boolean),
 | |
| +        clip: t.Optional(pageTypes.Clip),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        data: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'getContentQuads': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        objectId: t.String,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        quads: t.Array(pageTypes.DOMQuad),
 | |
| +      },
 | |
| +    },
 | |
| +    'dispatchKeyEvent': {
 | |
| +      params: {
 | |
| +        type: t.String,
 | |
| +        key: t.String,
 | |
| +        keyCode: t.Number,
 | |
| +        location: t.Number,
 | |
| +        code: t.String,
 | |
| +        repeat: t.Boolean,
 | |
| +        text: t.Optional(t.String),
 | |
| +      }
 | |
| +    },
 | |
| +    'dispatchTouchEvent': {
 | |
| +      params: {
 | |
| +        type: t.Enum(['touchStart', 'touchEnd', 'touchMove', 'touchCancel']),
 | |
| +        touchPoints: t.Array(pageTypes.TouchPoint),
 | |
| +        modifiers: t.Number,
 | |
| +      },
 | |
| +      returns: {
 | |
| +        defaultPrevented: t.Boolean,
 | |
| +      }
 | |
| +    },
 | |
| +    'dispatchMouseEvent': {
 | |
| +      params: {
 | |
| +        type: t.String,
 | |
| +        button: t.Number,
 | |
| +        x: t.Number,
 | |
| +        y: t.Number,
 | |
| +        modifiers: t.Number,
 | |
| +        clickCount: t.Optional(t.Number),
 | |
| +        buttons: t.Number,
 | |
| +      }
 | |
| +    },
 | |
| +    'insertText': {
 | |
| +      params: {
 | |
| +        text: t.String,
 | |
| +      }
 | |
| +    },
 | |
| +    'crash': {
 | |
| +      params: {}
 | |
| +    },
 | |
| +    'handleDialog': {
 | |
| +      params: {
 | |
| +        dialogId: t.String,
 | |
| +        accept: t.Boolean,
 | |
| +        promptText: t.Optional(t.String),
 | |
| +      },
 | |
| +    },
 | |
| +    'setInterceptFileChooserDialog': {
 | |
| +      params: {
 | |
| +        enabled: t.Boolean,
 | |
| +      },
 | |
| +    },
 | |
| +    'sendMessageToWorker': {
 | |
| +      params: {
 | |
| +        frameId: t.String,
 | |
| +        workerId: t.String,
 | |
| +        message: t.String,
 | |
| +      },
 | |
| +    },
 | |
| +  },
 | |
| +};
 | |
| +
 | |
| +
 | |
| +const Accessibility = {
 | |
| +  targets: ['page'],
 | |
| +  types: axTypes,
 | |
| +  events: {},
 | |
| +  methods: {
 | |
| +    'getFullAXTree': {
 | |
| +      params: {
 | |
| +        objectId: t.Optional(t.String),
 | |
| +      },
 | |
| +      returns: {
 | |
| +        tree: axTypes.AXTree
 | |
| +      },
 | |
| +    }
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +this.protocol = {
 | |
| +  domains: {Browser, Page, Runtime, Network, Accessibility},
 | |
| +};
 | |
| +this.checkScheme = checkScheme;
 | |
| +this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
 | |
| diff --git a/juggler/protocol/RuntimeHandler.js b/juggler/protocol/RuntimeHandler.js
 | |
| new file mode 100644
 | |
| index 0000000000000000000000000000000000000000..df2bfabfd2f569ac8ccdf5f65497c5c365cb3842
 | |
| --- /dev/null
 | |
| +++ b/juggler/protocol/RuntimeHandler.js
 | |
| @@ -0,0 +1,52 @@
 | |
| +"use strict";
 | |
| +
 | |
| +const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| +const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| +
 | |
| +const Cc = Components.classes;
 | |
| +const Ci = Components.interfaces;
 | |
| +const Cu = Components.utils;
 | |
| +const helper = new Helper();
 | |
| +
 | |
| +class RuntimeHandler {
 | |
| +  constructor(session, contentChannel) {
 | |
| +    const sessionId = session.sessionId();
 | |
| +    this._contentRuntime = contentChannel.connect(sessionId + 'runtime');
 | |
| +
 | |
| +    const emitProtocolEvent = eventName => {
 | |
| +      return (...args) => session.emitEvent(eventName, ...args);
 | |
| +    }
 | |
| +
 | |
| +    this._eventListeners = [
 | |
| +      contentChannel.register(sessionId + 'runtime', {
 | |
| +        runtimeConsole: emitProtocolEvent('Runtime.console'),
 | |
| +        runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
 | |
| +        runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
 | |
| +      }),
 | |
| +    ];
 | |
| +  }
 | |
| +
 | |
| +  async evaluate(options) {
 | |
| +    return await this._contentRuntime.send('evaluate', options);
 | |
| +  }
 | |
| +
 | |
| +  async callFunction(options) {
 | |
| +    return await this._contentRuntime.send('callFunction', options);
 | |
| +  }
 | |
| +
 | |
| +  async getObjectProperties(options) {
 | |
| +    return await this._contentRuntime.send('getObjectProperties', options);
 | |
| +  }
 | |
| +
 | |
| +  async disposeObject(options) {
 | |
| +    return await this._contentRuntime.send('disposeObject', options);
 | |
| +  }
 | |
| +
 | |
| +  dispose() {
 | |
| +    this._contentRuntime.dispose();
 | |
| +    helper.removeListeners(this._eventListeners);
 | |
| +  }
 | |
| +}
 | |
| +
 | |
| +var EXPORTED_SYMBOLS = ['RuntimeHandler'];
 | |
| +this.RuntimeHandler = RuntimeHandler;
 | |
| diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp
 | |
| index 249349388e925bc1832593d6c0c9df642040eb08..33a7c3b7f4c66a08f0fb9a48fd9a16c1ee3eedb1 100644
 | |
| --- a/parser/html/nsHtml5TreeOpExecutor.cpp
 | |
| +++ 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/SSLServerCertVerification.cpp b/security/manager/ssl/SSLServerCertVerification.cpp
 | |
| index 33c88a1f12aae814e17be708f38514b606839b1a..bc5085343a64f7f09e2639d6fad8413ceedb44a4 100644
 | |
| --- a/security/manager/ssl/SSLServerCertVerification.cpp
 | |
| +++ b/security/manager/ssl/SSLServerCertVerification.cpp
 | |
| @@ -1183,8 +1183,8 @@ PRErrorCode AuthCertificateParseResults(
 | |
|          return SEC_ERROR_NO_MEMORY;
 | |
|        }
 | |
|        nsresult rv = overrideService->HasMatchingOverride(
 | |
| -          aHostName, aPort, nssCert, &overrideBits, &isTemporaryOverride,
 | |
| -          &haveOverride);
 | |
| +          aHostName, aPort, aOriginAttributes.mUserContextId, nssCert,
 | |
| +          &overrideBits, &isTemporaryOverride, &haveOverride);
 | |
|        if (NS_SUCCEEDED(rv) && haveOverride) {
 | |
|          // remove the errors that are already overriden
 | |
|          remainingDisplayErrors &= ~overrideBits;
 | |
| diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp
 | |
| index d6b9e8f606b4d4871793d4c8ca85d7bfe0e86f37..fed251dec07057bd9a9398023e47bef5bd2ef6c4 100644
 | |
| --- a/security/manager/ssl/nsCertOverrideService.cpp
 | |
| +++ b/security/manager/ssl/nsCertOverrideService.cpp
 | |
| @@ -413,13 +413,20 @@ nsCertOverrideService::RememberTemporaryValidityOverrideUsingFingerprint(
 | |
|  
 | |
|  NS_IMETHODIMP
 | |
|  nsCertOverrideService::HasMatchingOverride(const nsACString& aHostName,
 | |
| -                                           int32_t aPort, nsIX509Cert* aCert,
 | |
| +                                           int32_t aPort,
 | |
| +                                           uint32_t aUserContextId,
 | |
| +                                           nsIX509Cert* aCert,
 | |
|                                             uint32_t* aOverrideBits,
 | |
|                                             bool* aIsTemporary, bool* _retval) {
 | |
|    bool disableAllSecurityCheck = false;
 | |
|    {
 | |
|      MutexAutoLock lock(mMutex);
 | |
| -    disableAllSecurityCheck = mDisableAllSecurityCheck;
 | |
| +    if (aUserContextId) {
 | |
| +      disableAllSecurityCheck = mUserContextIdsWithDisabledSecurityChecks.has(
 | |
| +          aUserContextId);
 | |
| +    } else {
 | |
| +      disableAllSecurityCheck = mDisableAllSecurityCheck;
 | |
| +    }
 | |
|    }
 | |
|    if (disableAllSecurityCheck) {
 | |
|      nsCertOverride::OverrideBits all = nsCertOverride::OverrideBits::Untrusted |
 | |
| @@ -632,12 +639,21 @@ static bool IsDebugger() {
 | |
|  
 | |
|  NS_IMETHODIMP
 | |
|  nsCertOverrideService::
 | |
| -    SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(bool aDisable) {
 | |
| -  if (!(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
 | |
| +    SetDisableAllSecurityChecksAndLetAttackersInterceptMyData(
 | |
| +      bool aDisable, uint32_t aUserContextId) {
 | |
| +  if (false /* juggler hacks */ && !(PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR") || IsDebugger())) {
 | |
|      return NS_ERROR_NOT_AVAILABLE;
 | |
|    }
 | |
|  
 | |
|    MutexAutoLock lock(mMutex);
 | |
| +  if (aUserContextId) {
 | |
| +    if (aDisable) {
 | |
| +      mozilla::Unused << mUserContextIdsWithDisabledSecurityChecks.put(aUserContextId);
 | |
| +    } else {
 | |
| +      mUserContextIdsWithDisabledSecurityChecks.remove(aUserContextId);
 | |
| +    }
 | |
| +    return NS_OK;
 | |
| +  }
 | |
|    mDisableAllSecurityCheck = aDisable;
 | |
|    return NS_OK;
 | |
|  }
 | |
| diff --git a/security/manager/ssl/nsCertOverrideService.h b/security/manager/ssl/nsCertOverrideService.h
 | |
| index a9fd392c18e330c107a5b2cf9720a9b6f6abd3db..ac2e2739ab2fffddea417dd73fba76d3dc639490 100644
 | |
| --- a/security/manager/ssl/nsCertOverrideService.h
 | |
| +++ b/security/manager/ssl/nsCertOverrideService.h
 | |
| @@ -120,6 +120,7 @@ class nsCertOverrideService final : public nsICertOverrideService,
 | |
|    ~nsCertOverrideService();
 | |
|  
 | |
|    bool mDisableAllSecurityCheck;
 | |
| +  mozilla::HashSet<uint32_t> mUserContextIdsWithDisabledSecurityChecks;
 | |
|    mozilla::Mutex mMutex;
 | |
|    nsCOMPtr<nsIFile> mSettingsFile;
 | |
|    nsTHashtable<nsCertOverrideEntry> mSettingsTable;
 | |
| diff --git a/security/manager/ssl/nsICertOverrideService.idl b/security/manager/ssl/nsICertOverrideService.idl
 | |
| index 6f0f8259b309c0a299c9c80b2943a498b0f1b0e6..03d17899be96bc87dc78f06277e1bd9eb93d08f8 100644
 | |
| --- a/security/manager/ssl/nsICertOverrideService.idl
 | |
| +++ b/security/manager/ssl/nsICertOverrideService.idl
 | |
| @@ -98,6 +98,7 @@ interface nsICertOverrideService : nsISupports {
 | |
|    [must_use]
 | |
|    boolean hasMatchingOverride(in AUTF8String aHostName,
 | |
|                                in int32_t aPort,
 | |
| +                              in uint32_t aUserContextId,
 | |
|                                in nsIX509Cert aCert,
 | |
|                                out uint32_t aOverrideBits,
 | |
|                                out boolean aIsTemporary);
 | |
| @@ -137,5 +138,7 @@ interface nsICertOverrideService : nsISupports {
 | |
|     *  @param aDisable If true, disable all security check and make
 | |
|     *                  hasMatchingOverride always return true.
 | |
|     */
 | |
| -  void setDisableAllSecurityChecksAndLetAttackersInterceptMyData(in boolean aDisable);
 | |
| +  void setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
 | |
| +      in boolean aDisable,
 | |
| +      [optional] in uint32_t aUserContextId);
 | |
|  };
 | |
| diff --git a/services/settings/Utils.jsm b/services/settings/Utils.jsm
 | |
| index f44f839dd4a52cb7a2073dae57d86df54cd72706..4b4a2fbf3b303adfec7010250b3ed86259421f46 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 73d33f54032c0ec785145214fe572d6655ca12b2..87daad40c1d811b75e3b8acab6837469e8443383 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;
 | |
| diff --git a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
 | |
| index ba7205c529c402512532c840225a535ce14fed5d..4597d7e950313dff1888aaf35cab49f04ae506bd 100644
 | |
| --- a/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
 | |
| +++ b/toolkit/components/statusfilter/nsBrowserStatusFilter.cpp
 | |
| @@ -176,8 +176,16 @@ nsBrowserStatusFilter::OnStateChange(nsIWebProgress* aWebProgress,
 | |
|  }
 | |
|  
 | |
|  NS_IMETHODIMP
 | |
| -nsBrowserStatusFilter::OnProgressChange(nsIWebProgress* aWebProgress,
 | |
| -                                        nsIRequest* aRequest,
 | |
| +nsBrowserStatusFilter::OnFrameLocationChange(nsIWebProgress *aWebProgress,
 | |
| +                                             nsIRequest *aRequest,
 | |
| +                                             nsIURI *aLocation,
 | |
| +                                             uint32_t aFlags) {
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
| +NS_IMETHODIMP
 | |
| +nsBrowserStatusFilter::OnProgressChange(nsIWebProgress *aWebProgress,
 | |
| +                                        nsIRequest *aRequest,
 | |
|                                          int32_t aCurSelfProgress,
 | |
|                                          int32_t aMaxSelfProgress,
 | |
|                                          int32_t aCurTotalProgress,
 | |
| diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm
 | |
| index 1f6de902e738a00eb12003bf5cb2f4fcb0bd16ae..47d02fb5dd0283b5de1f95f9e7a780a169be14e4 100644
 | |
| --- a/toolkit/mozapps/update/UpdateService.jsm
 | |
| +++ b/toolkit/mozapps/update/UpdateService.jsm
 | |
| @@ -3076,7 +3076,7 @@ UpdateService.prototype = {
 | |
|        ).running;
 | |
|      }
 | |
|  
 | |
| -    return (
 | |
| +    return true || (
 | |
|        (Cu.isInAutomation || marionetteRunning) &&
 | |
|        Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false)
 | |
|      );
 | |
| diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
 | |
| index 299230cb3bde5ecd111454ed6f59d1f0504b67a1..09f4ef69776217e5e9f5cc4ad4de939887d8c871 100644
 | |
| --- a/toolkit/toolkit.mozbuild
 | |
| +++ b/toolkit/toolkit.mozbuild
 | |
| @@ -168,6 +168,7 @@ if CONFIG['ENABLE_MARIONETTE']:
 | |
|      DIRS += [
 | |
|          '/testing/firefox-ui',
 | |
|          '/testing/marionette',
 | |
| +        '/juggler',
 | |
|          '/toolkit/components/telemetry/tests/marionette',
 | |
|      ]
 | |
|  
 | |
| diff --git a/uriloader/base/nsDocLoader.cpp b/uriloader/base/nsDocLoader.cpp
 | |
| index ef08f301d18466f1b08d18b88343fb4b7f11c476..04e3628094fe1c0afde7050e5b28c97a9e90e087 100644
 | |
| --- a/uriloader/base/nsDocLoader.cpp
 | |
| +++ b/uriloader/base/nsDocLoader.cpp
 | |
| @@ -757,6 +757,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
 | |
| @@ -1363,6 +1370,24 @@ void nsDocLoader::FireOnLocationChange(nsIWebProgress* aWebProgress,
 | |
|    }
 | |
|  }
 | |
|  
 | |
| +void nsDocLoader::FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
 | |
| +                                       nsIRequest* aRequest,
 | |
| +                                       nsIURI *aUri,
 | |
| +                                       uint32_t aFlags) {
 | |
| +  NOTIFY_LISTENERS(nsIWebProgress::NOTIFY_FRAME_LOCATION,
 | |
| +    nsCOMPtr<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 9fa3ebf90357db3996c8768b82822102405d1e56..96bbfa5069648ba94fa469e8d9791a29b00bd783 100644
 | |
| --- a/uriloader/base/nsDocLoader.h
 | |
| +++ b/uriloader/base/nsDocLoader.h
 | |
| @@ -206,6 +206,11 @@ class nsDocLoader : public nsIDocumentLoader,
 | |
|                                        nsIURI* aURI, int32_t aDelay,
 | |
|                                        bool aSameURI);
 | |
|  
 | |
| +  void FireOnFrameLocationChange(nsIWebProgress* aWebProgress,
 | |
| +                               nsIRequest* aRequest,
 | |
| +                               nsIURI *aUri,
 | |
| +                               uint32_t aFlags);
 | |
| +
 | |
|    // this function is overridden by the docshell, it is provided so that we
 | |
|    // can pass more information about redirect state (the normal OnStateChange
 | |
|    // doesn't get the new channel).
 | |
| diff --git a/uriloader/base/nsIWebProgress.idl b/uriloader/base/nsIWebProgress.idl
 | |
| index b0cde5026dc7c414e8f20300ac2b7d735dbd846e..09ebb0ef6799cf6a74fe529d4d000c6bed2c9497 100644
 | |
| --- a/uriloader/base/nsIWebProgress.idl
 | |
| +++ b/uriloader/base/nsIWebProgress.idl
 | |
| @@ -87,6 +87,10 @@ interface nsIWebProgress : nsISupports
 | |
|     * NOTIFY_REFRESH
 | |
|     *   Receive onRefreshAttempted events.
 | |
|     *   This is defined on nsIWebProgressListener2.
 | |
| +   *
 | |
| +   * NOTIFY_FRAME_LOCATION
 | |
| +   *   Receive onFrameLocationChange events.
 | |
| +   *   This is defined on nsIWebProgressListener2.
 | |
|     */
 | |
|    const unsigned long NOTIFY_PROGRESS         = 0x00000010;
 | |
|    const unsigned long NOTIFY_STATUS           = 0x00000020;
 | |
| @@ -94,11 +98,12 @@ interface nsIWebProgress : nsISupports
 | |
|    const unsigned long NOTIFY_LOCATION         = 0x00000080;
 | |
|    const unsigned long NOTIFY_REFRESH          = 0x00000100;
 | |
|    const unsigned long NOTIFY_CONTENT_BLOCKING = 0x00000200;
 | |
| +  const unsigned long NOTIFY_FRAME_LOCATION   = 0x00000400;
 | |
|  
 | |
|    /**
 | |
|     * This flag enables all notifications.
 | |
|     */
 | |
| -  const unsigned long NOTIFY_ALL              = 0x000003ff;
 | |
| +  const unsigned long NOTIFY_ALL              = 0x000007ff;
 | |
|  
 | |
|    /**
 | |
|     * Registers a listener to receive web progress events.
 | |
| diff --git a/uriloader/base/nsIWebProgressListener2.idl b/uriloader/base/nsIWebProgressListener2.idl
 | |
| index 87701f8d2cfee8bd84acd28c62b3be4989c9474c..ae1aa85c019cb21d4f7e79c35e8afe72709468a1 100644
 | |
| --- a/uriloader/base/nsIWebProgressListener2.idl
 | |
| +++ b/uriloader/base/nsIWebProgressListener2.idl
 | |
| @@ -66,4 +66,27 @@ interface nsIWebProgressListener2 : nsIWebProgressListener {
 | |
|                               in nsIURI aRefreshURI,
 | |
|                               in long aMillis,
 | |
|                               in boolean aSameURI);
 | |
| +
 | |
| +  /**
 | |
| +   * Called when the location of the window or its subframes changes.  This is not
 | |
| +   * when a load is requested, but rather once it is verified that the load is
 | |
| +   * going to occur in the given window.  For instance, a load that starts in a
 | |
| +   * window might send progress and status messages for the new site, but it
 | |
| +   * will not send the onLocationChange until we are sure that we are loading
 | |
| +   * this new page here.
 | |
| +   *
 | |
| +   * @param aWebProgress
 | |
| +   *        The nsIWebProgress instance that fired the notification.
 | |
| +   * @param aRequest
 | |
| +   *        The associated nsIRequest.  This may be null in some cases.
 | |
| +   * @param aLocation
 | |
| +   *        The URI of the location that is being loaded.
 | |
| +   * @param aFlags
 | |
| +   *        This is a value which explains the situation or the reason why
 | |
| +   *        the location has changed.
 | |
| +   */
 | |
| +  void onFrameLocationChange(in nsIWebProgress aWebProgress,
 | |
| +                             in nsIRequest aRequest,
 | |
| +                             in nsIURI aLocation,
 | |
| +                             [optional] in unsigned long aFlags);
 | |
|  };
 | |
| diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
 | |
| index b06fe25ea23b97f17b8b53677f4167a7cc994bec..196e5d6b1e9965dd9c05cc50c7bcb0841b8dafb9 100644
 | |
| --- a/uriloader/exthandler/nsExternalHelperAppService.cpp
 | |
| +++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
 | |
| @@ -99,6 +99,7 @@
 | |
|  
 | |
|  #include "mozilla/Components.h"
 | |
|  #include "mozilla/ClearOnShutdown.h"
 | |
| +#include "mozilla/ErrorNames.h"
 | |
|  #include "mozilla/Preferences.h"
 | |
|  #include "mozilla/ipc/URIUtils.h"
 | |
|  
 | |
| @@ -836,6 +837,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
 | |
|    return NS_OK;
 | |
|  }
 | |
|  
 | |
| +NS_IMETHODIMP nsExternalHelperAppService::SetDownloadInterceptor(
 | |
| +    nsIDownloadInterceptor* interceptor) {
 | |
| +  mInterceptor = interceptor;
 | |
| +  return NS_OK;
 | |
| +}
 | |
| +
 | |
|  nsresult nsExternalHelperAppService::GetFileTokenForPath(
 | |
|      const char16_t* aPlatformAppPath, nsIFile** aFile) {
 | |
|    nsDependentString platformAppPath(aPlatformAppPath);
 | |
| @@ -1418,7 +1425,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
 | |
|    // Strip off the ".part" from mTempLeafName
 | |
|    mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
 | |
|  
 | |
| +  return CreateSaverForTempFile();
 | |
| +}
 | |
| +
 | |
| +nsresult nsExternalAppHandler::CreateSaverForTempFile() {
 | |
|    MOZ_ASSERT(!mSaver, "Output file initialization called more than once!");
 | |
| +  nsresult rv;
 | |
|    mSaver =
 | |
|        do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
 | |
|    NS_ENSURE_SUCCESS(rv, rv);
 | |
| @@ -1578,7 +1590,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
 | |
|      return NS_OK;
 | |
|    }
 | |
|  
 | |
| -  rv = SetUpTempFile(aChannel);
 | |
| +  bool isIntercepted = false;
 | |
| +  nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
 | |
| +  if (interceptor) {
 | |
| +    nsCOMPtr<nsIFile> fileToUse;
 | |
| +    rv = interceptor->InterceptDownloadRequest(this, request, mBrowsingContext, getter_AddRefs(fileToUse), &isIntercepted);
 | |
| +    if (!NS_SUCCEEDED(rv)) {
 | |
| +      LOG(("    failed to call nsIDowloadInterceptor.interceptDownloadRequest"));
 | |
| +      return rv;
 | |
| +    }
 | |
| +    if (isIntercepted) {
 | |
| +      LOG(("    request interceped by nsIDowloadInterceptor"));
 | |
| +      if (fileToUse) {
 | |
| +        mTempFile = fileToUse;
 | |
| +        rv = mTempFile->GetLeafName(mTempLeafName);
 | |
| +        NS_ENSURE_SUCCESS(rv, rv);
 | |
| +      } else {
 | |
| +        Cancel(NS_BINDING_ABORTED);
 | |
| +        return NS_OK;
 | |
| +      }
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
| +  // Temp file is the final destination when download is intercepted. In that
 | |
| +  // case we only need to create saver (and not create transfer later). Not creating
 | |
| +  // mTransfer also cuts off all downloads handling logic in the js compoenents and
 | |
| +  // browser UI.
 | |
| +  if (isIntercepted)
 | |
| +    rv = CreateSaverForTempFile();
 | |
| +  else
 | |
| +    rv = SetUpTempFile(aChannel);
 | |
|    if (NS_FAILED(rv)) {
 | |
|      nsresult transferError = rv;
 | |
|  
 | |
| @@ -1626,6 +1667,11 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
 | |
|    mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
 | |
|    nsAutoCString MIMEType;
 | |
|    mMimeInfo->GetMIMEType(MIMEType);
 | |
| +
 | |
| +  if (isIntercepted) {
 | |
| +    return NS_OK;
 | |
| +  }
 | |
| +
 | |
|    if (alwaysAsk) {
 | |
|      // But we *don't* ask if this mimeInfo didn't come from
 | |
|      // our user configuration datastore and the user has said
 | |
| @@ -2026,6 +2072,15 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
 | |
|      NotifyTransfer(aStatus);
 | |
|    }
 | |
|  
 | |
| +  if (!mCanceled) {
 | |
| +    nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
 | |
| +    if (interceptor) {
 | |
| +      nsCString noError;
 | |
| +      nsresult rv = interceptor->OnDownloadComplete(this, noError);
 | |
| +      MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to call nsIDowloadInterceptor.OnDownloadComplete");
 | |
| +    }
 | |
| +  }
 | |
| +
 | |
|    return NS_OK;
 | |
|  }
 | |
|  
 | |
| @@ -2396,6 +2451,14 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
 | |
|      }
 | |
|    }
 | |
|  
 | |
| +  nsCOMPtr<nsIDownloadInterceptor> interceptor = mExtProtSvc->mInterceptor;
 | |
| +  if (interceptor) {
 | |
| +    nsCString errorName;
 | |
| +    GetErrorName(aReason, errorName);
 | |
| +    nsresult rv = interceptor->OnDownloadComplete(this, errorName);
 | |
| +    MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed notify nsIDowloadInterceptor about cancel");
 | |
| +  }
 | |
| +
 | |
|    // Break our reference cycle with the helper app dialog (set up in
 | |
|    // OnStartRequest)
 | |
|    mDialog = nullptr;
 | |
| diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h
 | |
| index c7a41c3b714b6d788ac4a6c6931939ce07672868..6fbe9a61b821fbb2cb417cc640d5ca9efdbff733 100644
 | |
| --- a/uriloader/exthandler/nsExternalHelperAppService.h
 | |
| +++ b/uriloader/exthandler/nsExternalHelperAppService.h
 | |
| @@ -188,6 +188,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
 | |
|        mozilla::dom::BrowsingContext* aContentContext, bool aForceSave,
 | |
|        nsIInterfaceRequestor* aWindowContext,
 | |
|        nsIStreamListener** aStreamListener);
 | |
| +
 | |
| +  nsCOMPtr<nsIDownloadInterceptor> mInterceptor;
 | |
|  };
 | |
|  
 | |
|  /**
 | |
| @@ -365,6 +367,9 @@ class nsExternalAppHandler final : public nsIStreamListener,
 | |
|     * Upon successful return, both mTempFile and mSaver will be valid.
 | |
|     */
 | |
|    nsresult SetUpTempFile(nsIChannel* aChannel);
 | |
| +
 | |
| +  nsresult CreateSaverForTempFile();
 | |
| +
 | |
|    /**
 | |
|     * When we download a helper app, we are going to retarget all load
 | |
|     * notifications into our own docloader and load group instead of
 | |
| diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl
 | |
| index 8a55c1bd666c4f7a032863f1527a2315830643c5..f7891682bd1903e45f96bd081f5af5a20a098edd 100644
 | |
| --- a/uriloader/exthandler/nsIExternalHelperAppService.idl
 | |
| +++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
 | |
| @@ -6,6 +6,8 @@
 | |
|  
 | |
|  #include "nsICancelable.idl"
 | |
|  
 | |
| +webidl BrowsingContext;
 | |
| +interface nsIHelperAppLauncher;
 | |
|  interface nsIURI;
 | |
|  interface nsIRequest;
 | |
|  interface nsIStreamListener;
 | |
| @@ -20,6 +22,17 @@ webidl BrowsingContext;
 | |
|  class nsExternalAppHandler;
 | |
|  %}
 | |
|  
 | |
| +/**
 | |
| + * Interceptor interface used by Juggler.
 | |
| + */
 | |
| +[scriptable, uuid(9a20e9b0-75d0-11ea-bc55-0242ac130003)]
 | |
| +interface nsIDownloadInterceptor : nsISupports
 | |
| +{
 | |
| +  bool interceptDownloadRequest(in nsIHelperAppLauncher aHandler, in nsIRequest aRequest, in BrowsingContext aBrowsingContext, out nsIFile file);
 | |
| +
 | |
| +  void onDownloadComplete(in nsIHelperAppLauncher aHandler, in ACString aErrorName);
 | |
| +};
 | |
| +
 | |
|  /**
 | |
|   * The external helper app service is used for finding and launching
 | |
|   * platform specific external applications for a given mime content type.
 | |
| @@ -49,7 +62,7 @@ interface nsIExternalHelperAppService : nsISupports
 | |
|                                 in nsIInterfaceRequestor aContentContext,
 | |
|                                 in boolean aForceSave,
 | |
|                                 [optional] in nsIInterfaceRequestor aWindowContext);
 | |
| -  
 | |
| +
 | |
|    /**
 | |
|     * Binds an external helper application to a stream listener. The caller
 | |
|     * should pump data into the returned stream listener. When the OnStopRequest
 | |
| @@ -82,6 +95,7 @@ interface nsIExternalHelperAppService : nsISupports
 | |
|    boolean applyDecodingForExtension(in AUTF8String aExtension,
 | |
|                                      in ACString aEncodingType);
 | |
|  
 | |
| +  void setDownloadInterceptor(in nsIDownloadInterceptor interceptor);
 | |
|  };
 | |
|  
 | |
|  /**
 |