summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/webidl-api
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/webidl-api')
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPI.cpp.in56
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPI.h.in71
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPI.webidl.in33
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIAddRemoveListener.h36
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIBase.cpp364
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIBase.h198
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPICallAsyncFunction.h29
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPICallFunctionNoReturn.h29
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPICallSyncFunction.h29
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIGetProperty.h29
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIRequest.cpp242
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIRequest.h121
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp710
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h258
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAlarms.cpp49
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionAlarms.h70
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowser.cpp345
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowser.h154
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.cpp106
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.h107
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.cpp58
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.h77
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionDns.cpp40
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionDns.h63
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp690
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventListener.h234
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventManager.cpp168
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionEventManager.h99
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionMockAPI.cpp59
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionMockAPI.h78
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionPort.cpp109
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionPort.h96
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionProxy.cpp51
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionProxy.h72
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionRuntime.cpp67
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionRuntime.h85
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionScripting.cpp43
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionScripting.h67
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionSetting.cpp48
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionSetting.h69
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionTest.cpp527
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionTest.h102
-rw-r--r--toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf101
-rw-r--r--toolkit/components/extensions/webidl-api/GenerateWebIDLBindings.py1612
-rw-r--r--toolkit/components/extensions/webidl-api/InspectJSONSchema.py152
-rw-r--r--toolkit/components/extensions/webidl-api/moz.build78
-rw-r--r--toolkit/components/extensions/webidl-api/test/README.md60
-rw-r--r--toolkit/components/extensions/webidl-api/test/conftest.py39
-rw-r--r--toolkit/components/extensions/webidl-api/test/helpers.py22
-rw-r--r--toolkit/components/extensions/webidl-api/test/python.ini7
-rw-r--r--toolkit/components/extensions/webidl-api/test/test_all_schemas_smoketest.py22
-rw-r--r--toolkit/components/extensions/webidl-api/test/test_json_schema_parsing.py215
-rw-r--r--toolkit/components/extensions/webidl-api/test/test_json_schema_platform_diffs.py153
-rw-r--r--toolkit/components/extensions/webidl-api/test/test_webidl_from_json_schema.py110
54 files changed, 8509 insertions, 0 deletions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPI.cpp.in b/toolkit/components/extensions/webidl-api/ExtensionAPI.cpp.in
new file mode 100644
index 0000000000..8c64472a78
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPI.cpp.in
@@ -0,0 +1,56 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "{{ webidl_name }}.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/{{ webidl_name }}Binding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF({{ webidl_name }});
+NS_IMPL_CYCLE_COLLECTING_RELEASE({{ webidl_name }})
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE({{ webidl_name }}, mGlobal, mExtensionBrowser
+ /* TODO: add events properties if any */);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION({{ webidl_name }})
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/* TODO add implementation for the event manager getter if any.
+
+// Defines the OnEVENTNAME method implementation and expects a data member computed
+// based on the getter method name (e.g. `mOnEVENTNAMEEventManager`) where the
+// ExtensionEventManager instance is going to be stored.
+NS_IMPL_WEBEXT_EVENTMGR({{webidl_name}}, u"onEVENTNAME"_ns, OnEVENTNAME)
+
+// or to use a specific data member name:
+//
+// NS_IMPL_WEBEXT_EVENTMGR_WITH_DATAMEMBER({{webidl_name}}, u"onEVENTNAME"_ns, OnEventName, mDataMemberName)
+
+*/
+
+{{ webidl_name }}::{{ webidl_name }}(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool {{ webidl_name }}::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* {{ webidl_name }}::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::{{ webidl_name }}_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* {{ webidl_name }}::GetParentObject() const { return mGlobal; }
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPI.h.in b/toolkit/components/extensions/webidl-api/ExtensionAPI.h.in
new file mode 100644
index 0000000000..f902d98d2e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPI.h.in
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_{{ webidl_name }}_h
+#define mozilla_extensions_{{ webidl_name }}_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionEventManager;
+
+class {{ webidl_name }} final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ {{ webidl_name }}(nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"{{ api_namespace }}"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ // TODO: add method for the event manager objects if any.
+ // ExtensionEventManager* OnEVENTNAME();
+
+ // TODO: add methods for the property getters if any.
+ // void GetPROP_NAME(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS({{ webidl_name }})
+
+ private:
+ ~{{ webidl_name }}() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ // TODO: add RefPtr for the ExtensionEventManager instances if any.
+ // RefPtr<ExtensionEventManager> mOnEVENTNAMEEventMgr;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_{{ webidl_name }}_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPI.webidl.in b/toolkit/components/extensions/webidl-api/ExtensionAPI.webidl.in
new file mode 100644
index 0000000000..43a25d5498
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPI.webidl.in
@@ -0,0 +1,33 @@
+/*
+ * THIS IS AN AUTOGENERATED FILE. DO NOT EDIT
+ *
+ * The content of this file has been generated based on the WebExtensions API
+ * JSONSchema using the following command:
+ *
+ * export SCRIPT_DIR="toolkit/components/extensions/webidl-api"
+ * mach python $SCRIPT_DIR/GenerateWebIDLBindings.py -- {{ api_namespace }}
+ *
+ * More info about generating webidl API bindings for WebExtensions API at:
+ *
+ * https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/webidl_bindings.html
+ *
+ * A short summary of the special setup used by these WebIDL files (meant to aid
+ * webidl peers reviews and sign-offs) is available in the following section:
+ *
+ * https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/webidl_bindings.html#review-process-on-changes-to-webidl-definitions
+ */
+
+/* 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/.
+ *
+ * You are granted a license to use, reproduce and create derivative works of
+ * this document.
+ */
+{%+ if webidl_description_comment %}
+{{ webidl_description_comment }}
+{%- endif %}
+[Exposed=({{ webidl_exposed_attr }}), LegacyNoInterfaceObject]
+interface {{ webidl_name }} {
+{{- webidl_definition_body }}
+};
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIAddRemoveListener.h b/toolkit/components/extensions/webidl-api/ExtensionAPIAddRemoveListener.h
new file mode 100644
index 0000000000..5020641e23
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIAddRemoveListener.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPIAddRemoveListener_h
+#define mozilla_extensions_ExtensionAPIAddRemoveListener_h
+
+#include "ExtensionAPIRequestForwarder.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPIAddRemoveListener : public ExtensionAPIRequestForwarder {
+ public:
+ enum class EType {
+ eAddListener,
+ eRemoveListener,
+ };
+
+ ExtensionAPIAddRemoveListener(const EType type,
+ const nsAString& aApiNamespace,
+ const nsAString& aApiEvent,
+ const nsAString& aApiObjectType,
+ const nsAString& aApiObjectId)
+ : ExtensionAPIRequestForwarder(
+ type == EType::eAddListener ? APIRequestType::ADD_LISTENER
+ : APIRequestType::REMOVE_LISTENER,
+ aApiNamespace, aApiEvent, aApiObjectType, aApiObjectId) {}
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPIAddRemoveListener_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIBase.cpp b/toolkit/components/extensions/webidl-api/ExtensionAPIBase.cpp
new file mode 100644
index 0000000000..503124a39a
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIBase.cpp
@@ -0,0 +1,364 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionAPIBase.h"
+
+#include "ExtensionAPIRequestForwarder.h"
+#include "ExtensionAPIAddRemoveListener.h"
+#include "ExtensionAPICallAsyncFunction.h"
+#include "ExtensionAPICallFunctionNoReturn.h"
+#include "ExtensionAPICallSyncFunction.h"
+#include "ExtensionAPIGetProperty.h"
+#include "ExtensionBrowser.h"
+#include "ExtensionEventManager.h"
+#include "ExtensionPort.h"
+#include "ExtensionSetting.h"
+
+#include "mozilla/ConsoleReportCollector.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/SerializedStackHolder.h"
+#include "mozilla/dom/FunctionBinding.h"
+
+#include "js/CallAndConstruct.h" // JS::IsCallable
+
+namespace mozilla {
+namespace extensions {
+
+// ChromeCompatCallbackHandler
+
+NS_IMPL_ISUPPORTS0(ChromeCompatCallbackHandler)
+
+// static
+void ChromeCompatCallbackHandler::Create(
+ ExtensionBrowser* aExtensionBrowser, dom::Promise* aPromise,
+ const RefPtr<dom::Function>& aCallback) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(aExtensionBrowser);
+ MOZ_ASSERT(aCallback);
+
+ RefPtr<ChromeCompatCallbackHandler> handler =
+ new ChromeCompatCallbackHandler(aExtensionBrowser, aCallback);
+
+ aPromise->AppendNativeHandler(handler);
+}
+
+void ChromeCompatCallbackHandler::ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ JS::Rooted<JS::Value> retval(aCx);
+ IgnoredErrorResult rv;
+ MOZ_KnownLive(mCallback)->Call({aValue}, &retval, rv);
+}
+
+void ChromeCompatCallbackHandler::RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) {
+ JS::Rooted<JS::Value> retval(aCx);
+ IgnoredErrorResult rv;
+ // Call the chrome-compatible callback without any parameter, the errors
+ // isn't passed to the callback as a parameter but the extension will be
+ // able to retrieve it from chrome.runtime.lastError.
+ mExtensionBrowser->SetLastError(aValue);
+ MOZ_KnownLive(mCallback)->Call({}, &retval, rv);
+ if (mExtensionBrowser->ClearLastError()) {
+ ReportUncheckedLastError(aCx, aValue);
+ }
+}
+
+void ChromeCompatCallbackHandler::ReportUncheckedLastError(
+ JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ nsCString sourceSpec;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ nsString valueString;
+
+ nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
+ valueString);
+
+ nsTArray<nsString> params;
+ params.AppendElement(valueString);
+
+ RefPtr<ConsoleReportCollector> reporter = new ConsoleReportCollector();
+ reporter->AddConsoleReport(nsIScriptError::errorFlag, "content javascript"_ns,
+ nsContentUtils::eDOM_PROPERTIES, sourceSpec, line,
+ column, "WebExtensionUncheckedLastError"_ns,
+ params);
+
+ dom::WorkerPrivate* workerPrivate = dom::GetWorkerPrivateFromContext(aCx);
+ RefPtr<Runnable> r = NS_NewRunnableFunction(
+ "ChromeCompatCallbackHandler::ReportUncheckedLastError",
+ [reporter]() { reporter->FlushReportsToConsole(0); });
+ workerPrivate->DispatchToMainThread(r.forget());
+}
+
+// WebExtensionStub methods shared between multiple API namespaces.
+
+void ExtensionAPIBase::CallWebExtMethodNotImplementedNoReturn(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ aRv.ThrowNotSupportedError("Not implemented");
+}
+
+void ExtensionAPIBase::CallWebExtMethodNotImplementedAsync(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ CallWebExtMethodNotImplementedNoReturn(aCx, aApiMethod, aArgs, aRv);
+}
+
+void ExtensionAPIBase::CallWebExtMethodNotImplemented(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv) {
+ CallWebExtMethodNotImplementedNoReturn(aCx, aApiMethod, aArgs, aRv);
+}
+
+void ExtensionAPIBase::CallWebExtMethodNoReturn(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ auto request = CallFunctionNoReturn(aApiMethod);
+ request->Run(GetGlobalObject(), aCx, aArgs, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+}
+
+void ExtensionAPIBase::CallWebExtMethod(JSContext* aCx,
+ const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) {
+ auto request = CallSyncFunction(aApiMethod);
+ request->Run(GetGlobalObject(), aCx, aArgs, aRetVal, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+}
+
+void ExtensionAPIBase::CallWebExtMethodReturnsString(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, nsAString& aRetVal,
+ ErrorResult& aRv) {
+ JS::Rooted<JS::Value> retval(aCx);
+ auto request = CallSyncFunction(aApiMethod);
+ request->Run(GetGlobalObject(), aCx, aArgs, &retval, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ if (NS_WARN_IF(!retval.isString())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ nsAutoJSString str;
+ if (!str.init(aCx, retval.toString())) {
+ JS_ClearPendingException(aCx);
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ aRetVal = str;
+}
+
+already_AddRefed<ExtensionPort> ExtensionAPIBase::CallWebExtMethodReturnsPort(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ JS::Rooted<JS::Value> apiResult(aCx);
+ auto request = CallSyncFunction(aApiMethod);
+ request->Run(GetGlobalObject(), aCx, aArgs, &apiResult, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ IgnoredErrorResult rv;
+ auto* extensionBrowser = GetExtensionBrowser();
+ RefPtr<ExtensionPort> port = extensionBrowser->GetPort(apiResult, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ // ExtensionPort::Create doesn't throw the js exception with the generic
+ // error message as the "api request forwarding" helper classes.
+ ThrowUnexpectedError(aCx, aRv);
+ return nullptr;
+ }
+
+ return port.forget();
+}
+
+void ExtensionAPIBase::CallWebExtMethodAsyncInternal(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Function>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ auto* global = GetGlobalObject();
+
+ IgnoredErrorResult erv;
+ RefPtr<dom::Promise> domPromise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+ MOZ_ASSERT(domPromise);
+ auto request = CallAsyncFunction(aApiMethod);
+ request->Run(global, aCx, aArgs, domPromise, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ // The async method has been called with the chrome-compatible callback
+ // convention.
+ if (aCallback) {
+ ChromeCompatCallbackHandler::Create(GetExtensionBrowser(), domPromise,
+ aCallback);
+ return;
+ }
+
+ if (NS_WARN_IF(!ToJSValue(aCx, domPromise, aRetval))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+}
+
+void ExtensionAPIBase::CallWebExtMethodAsync(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ RefPtr<dom::Function> callback = nullptr;
+ if (aCallback.WasPassed()) {
+ callback = &aCallback.Value();
+ }
+ CallWebExtMethodAsyncInternal(aCx, aApiMethod, aArgs, callback, aRetval, aRv);
+}
+
+void ExtensionAPIBase::CallWebExtMethodAsyncAmbiguous(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv) {
+ RefPtr<dom::Function> chromeCompatCb;
+ auto lastElement =
+ aArgs.IsEmpty() ? JS::UndefinedValue() : aArgs.LastElement();
+ dom::Sequence<JS::Value> callArgs(aArgs);
+ if (lastElement.isObject() && JS::IsCallable(&lastElement.toObject())) {
+ JS::Rooted<JSObject*> tempRoot(aCx, &lastElement.toObject());
+ JS::Rooted<JSObject*> tempGlobalRoot(aCx, JS::CurrentGlobalOrNull(aCx));
+ chromeCompatCb = new dom::Function(aCx, tempRoot, tempGlobalRoot,
+ dom::GetIncumbentGlobal());
+
+ Unused << callArgs.PopLastElement();
+ }
+ CallWebExtMethodAsyncInternal(aCx, aApiMethod, callArgs, chromeCompatCb,
+ aRetval, aRv);
+}
+
+// ExtensionAPIBase - API Request helpers
+
+void ExtensionAPIBase::GetWebExtPropertyAsString(const nsString& aPropertyName,
+ dom::DOMString& aRetval) {
+ IgnoredErrorResult rv;
+
+ dom::AutoJSAPI jsapi;
+ auto* global = GetGlobalObject();
+
+ if (!jsapi.Init(global)) {
+ NS_WARNING("GetWebExtPropertyAsString fail to init jsapi");
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> retval(cx);
+
+ RefPtr<ExtensionAPIGetProperty> request = GetProperty(aPropertyName);
+ request->Run(global, cx, &retval, rv);
+ if (rv.Failed()) {
+ NS_WARNING("GetWebExtPropertyAsString failure");
+ return;
+ }
+ nsAutoJSString strRetval;
+ if (!retval.isString() || !strRetval.init(cx, retval)) {
+ NS_WARNING("GetWebExtPropertyAsString got a non string result");
+ return;
+ }
+ aRetval.SetKnownLiveString(strRetval);
+}
+
+void ExtensionAPIBase::GetWebExtPropertyAsJSValue(
+ JSContext* aCx, const nsAString& aPropertyName,
+ JS::MutableHandle<JS::Value> aRetval) {
+ IgnoredErrorResult rv;
+ RefPtr<ExtensionAPIGetProperty> request = GetProperty(aPropertyName);
+ request->Run(GetGlobalObject(), aCx, aRetval, rv);
+ if (rv.Failed()) {
+ NS_WARNING("GetWebExtPropertyAsJSValue failure");
+ return;
+ }
+}
+
+already_AddRefed<ExtensionEventManager> ExtensionAPIBase::CreateEventManager(
+ const nsAString& aEventName) {
+ RefPtr<ExtensionEventManager> eventMgr = new ExtensionEventManager(
+ GetGlobalObject(), GetExtensionBrowser(), GetAPINamespace(), aEventName,
+ GetAPIObjectType(), GetAPIObjectId());
+ return eventMgr.forget();
+}
+
+already_AddRefed<ExtensionSetting> ExtensionAPIBase::CreateSetting(
+ const nsAString& aSettingName) {
+ nsAutoString settingAPIPath;
+ settingAPIPath.Append(GetAPINamespace());
+ settingAPIPath.AppendLiteral(".");
+ settingAPIPath.Append(aSettingName);
+ RefPtr<ExtensionSetting> settingAPI = new ExtensionSetting(
+ GetGlobalObject(), GetExtensionBrowser(), settingAPIPath);
+ return settingAPI.forget();
+}
+
+RefPtr<ExtensionAPICallFunctionNoReturn> ExtensionAPIBase::CallFunctionNoReturn(
+ const nsAString& aApiMethod) {
+ return new ExtensionAPICallFunctionNoReturn(
+ GetAPINamespace(), aApiMethod, GetAPIObjectType(), GetAPIObjectId());
+}
+
+RefPtr<ExtensionAPICallSyncFunction> ExtensionAPIBase::CallSyncFunction(
+ const nsAString& aApiMethod) {
+ return new ExtensionAPICallSyncFunction(GetAPINamespace(), aApiMethod,
+ GetAPIObjectType(), GetAPIObjectId());
+}
+
+RefPtr<ExtensionAPICallAsyncFunction> ExtensionAPIBase::CallAsyncFunction(
+ const nsAString& aApiMethod) {
+ return new ExtensionAPICallAsyncFunction(
+ GetAPINamespace(), aApiMethod, GetAPIObjectType(), GetAPIObjectId());
+}
+
+RefPtr<ExtensionAPIGetProperty> ExtensionAPIBase::GetProperty(
+ const nsAString& aApiProperty) {
+ return new ExtensionAPIGetProperty(GetAPINamespace(), aApiProperty,
+ GetAPIObjectType(), GetAPIObjectId());
+}
+
+RefPtr<ExtensionAPIAddRemoveListener> ExtensionAPIBase::SendAddListener(
+ const nsAString& aEventName) {
+ using EType = ExtensionAPIAddRemoveListener::EType;
+ return new ExtensionAPIAddRemoveListener(
+ EType::eAddListener, GetAPINamespace(), aEventName, GetAPIObjectType(),
+ GetAPIObjectId());
+}
+
+RefPtr<ExtensionAPIAddRemoveListener> ExtensionAPIBase::SendRemoveListener(
+ const nsAString& aEventName) {
+ using EType = ExtensionAPIAddRemoveListener::EType;
+ return new ExtensionAPIAddRemoveListener(
+ EType::eRemoveListener, GetAPINamespace(), aEventName, GetAPIObjectType(),
+ GetAPIObjectId());
+}
+
+// static
+void ExtensionAPIBase::ThrowUnexpectedError(JSContext* aCx, ErrorResult& aRv) {
+ ExtensionAPIRequestForwarder::ThrowUnexpectedError(aCx, aRv);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIBase.h b/toolkit/components/extensions/webidl-api/ExtensionAPIBase.h
new file mode 100644
index 0000000000..cef4c77887
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIBase.h
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPIBase_h
+#define mozilla_extensions_ExtensionAPIBase_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/FunctionBinding.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/ErrorResult.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace dom {
+class Function;
+}
+
+namespace extensions {
+
+#define NS_IMPL_WEBEXT_EVENTMGR_WITH_DATAMEMBER( \
+ _class, _eventName, _eventGetterName, _eventDataMember) \
+ ExtensionEventManager* _class::_eventGetterName() { \
+ if (!(_eventDataMember)) { \
+ (_eventDataMember) = CreateEventManager(_eventName); \
+ } \
+ return (_eventDataMember); \
+ }
+#define NS_IMPL_WEBEXT_EVENTMGR(_class, _eventName, _eventGetterName) \
+ NS_IMPL_WEBEXT_EVENTMGR_WITH_DATAMEMBER( \
+ _class, _eventName, _eventGetterName, m##_eventGetterName##EventMgr)
+
+#define NS_IMPL_WEBEXT_SETTING_WITH_DATAMEMBER( \
+ _class, _settingName, _settingGetterName, _settingDataMember) \
+ ExtensionSetting* _class::_settingGetterName() { \
+ if (!(_settingDataMember)) { \
+ (_settingDataMember) = CreateSetting(_settingName); \
+ } \
+ return (_settingDataMember); \
+ }
+#define NS_IMPL_WEBEXT_SETTING(_class, _settingName, _settingGetterName) \
+ NS_IMPL_WEBEXT_SETTING_WITH_DATAMEMBER(_class, _settingName, \
+ _settingGetterName, \
+ m##_settingGetterName##Setting)
+
+class ExtensionAPIAddRemoveListener;
+class ExtensionAPICallFunctionNoReturn;
+class ExtensionAPICallSyncFunction;
+class ExtensionAPICallAsyncFunction;
+class ExtensionAPIGetProperty;
+class ExtensionBrowser;
+class ExtensionEventManager;
+class ExtensionPort;
+class ExtensionSetting;
+
+class ExtensionAPIBase {
+ public:
+ // WebExtensionStub methods shared between multiple API namespaces.
+
+ virtual void CallWebExtMethodNotImplementedNoReturn(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv);
+
+ virtual void CallWebExtMethodNotImplementedAsync(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv);
+
+ virtual void CallWebExtMethodNotImplemented(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv);
+
+ virtual void CallWebExtMethodNoReturn(JSContext* aCx,
+ const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ ErrorResult& aRv);
+ virtual void CallWebExtMethod(JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv);
+
+ virtual void CallWebExtMethodReturnsString(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, nsAString& aRetVal,
+ ErrorResult& aRv);
+
+ virtual already_AddRefed<ExtensionPort> CallWebExtMethodReturnsPort(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv);
+
+ virtual void CallWebExtMethodAsync(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);
+
+ virtual void CallWebExtMethodAsyncAmbiguous(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);
+
+ virtual void GetWebExtPropertyAsString(const nsString& aPropertyName,
+ dom::DOMString& aRetval);
+
+ virtual void GetWebExtPropertyAsJSValue(JSContext* aCx,
+ const nsAString& aPropertyName,
+ JS::MutableHandle<JS::Value> aRetval);
+
+ // API Requests helpers.
+ already_AddRefed<ExtensionEventManager> CreateEventManager(
+ const nsAString& aEventName);
+
+ already_AddRefed<ExtensionSetting> CreateSetting(
+ const nsAString& aSettingName);
+
+ RefPtr<ExtensionAPICallFunctionNoReturn> CallFunctionNoReturn(
+ const nsAString& aApiMethod);
+
+ RefPtr<ExtensionAPICallSyncFunction> CallSyncFunction(
+ const nsAString& aApiMethod);
+
+ RefPtr<ExtensionAPICallAsyncFunction> CallAsyncFunction(
+ const nsAString& aApiMethod);
+
+ RefPtr<ExtensionAPIGetProperty> GetProperty(const nsAString& aApiProperty);
+
+ RefPtr<ExtensionAPIAddRemoveListener> SendAddListener(
+ const nsAString& aEventName);
+
+ RefPtr<ExtensionAPIAddRemoveListener> SendRemoveListener(
+ const nsAString& aEventName);
+
+ static void ThrowUnexpectedError(JSContext* aCx, ErrorResult& aRv);
+
+ protected:
+ virtual nsIGlobalObject* GetGlobalObject() const = 0;
+ virtual ExtensionBrowser* GetExtensionBrowser() const = 0;
+ virtual nsString GetAPINamespace() const = 0;
+ virtual nsString GetAPIObjectType() const = 0;
+ virtual nsString GetAPIObjectId() const = 0;
+
+ private:
+ void CallWebExtMethodAsyncInternal(JSContext* aCx,
+ const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Function>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval,
+ ErrorResult& aRv);
+};
+
+class ExtensionAPINamespace : public ExtensionAPIBase {
+ protected:
+ nsString GetAPIObjectType() const override { return VoidString(); }
+
+ nsString GetAPIObjectId() const override { return VoidString(); };
+};
+
+class ChromeCompatCallbackHandler final : public dom::PromiseNativeHandler {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ static void Create(ExtensionBrowser* aExtensionBrowser,
+ dom::Promise* aPromise,
+ const RefPtr<dom::Function>& aCallback);
+
+ MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+ MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ private:
+ ChromeCompatCallbackHandler(ExtensionBrowser* aExtensionBrowser,
+ const RefPtr<dom::Function>& aCallback)
+ : mCallback(aCallback), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aExtensionBrowser);
+ }
+
+ ~ChromeCompatCallbackHandler() = default;
+
+ void ReportUncheckedLastError(JSContext* aCx, JS::Handle<JS::Value> aValue);
+
+ RefPtr<dom::Function> mCallback;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPIBase_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPICallAsyncFunction.h b/toolkit/components/extensions/webidl-api/ExtensionAPICallAsyncFunction.h
new file mode 100644
index 0000000000..bf2f57ceb3
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPICallAsyncFunction.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPICallAsyncFunction_h
+#define mozilla_extensions_ExtensionAPICallAsyncFunction_h
+
+#include "ExtensionAPIRequestForwarder.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPICallAsyncFunction : public ExtensionAPIRequestForwarder {
+ public:
+ ExtensionAPICallAsyncFunction(const nsAString& aApiNamespace,
+ const nsAString& aApiMethod,
+ const nsAString& aApiObjectType = u""_ns,
+ const nsAString& aApiObjectId = u""_ns)
+ : ExtensionAPIRequestForwarder(
+ mozIExtensionAPIRequest::RequestType::CALL_FUNCTION_ASYNC,
+ aApiNamespace, aApiMethod, aApiObjectType, aApiObjectId) {}
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPICallAsyncFunction_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPICallFunctionNoReturn.h b/toolkit/components/extensions/webidl-api/ExtensionAPICallFunctionNoReturn.h
new file mode 100644
index 0000000000..4ff179f652
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPICallFunctionNoReturn.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPICallFunctionNoReturn_h
+#define mozilla_extensions_ExtensionAPICallFunctionNoReturn_h
+
+#include "ExtensionAPIRequestForwarder.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPICallFunctionNoReturn : public ExtensionAPIRequestForwarder {
+ public:
+ ExtensionAPICallFunctionNoReturn(const nsAString& aApiNamespace,
+ const nsAString& aApiMethod,
+ const nsAString& aApiObjectType = u""_ns,
+ const nsAString& aApiObjectId = u""_ns)
+ : ExtensionAPIRequestForwarder(
+ mozIExtensionAPIRequest::RequestType::CALL_FUNCTION_NO_RETURN,
+ aApiNamespace, aApiMethod, aApiObjectType, aApiObjectId) {}
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPICallFunctionNoReturn_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPICallSyncFunction.h b/toolkit/components/extensions/webidl-api/ExtensionAPICallSyncFunction.h
new file mode 100644
index 0000000000..2a58082514
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPICallSyncFunction.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPICallSyncFunction_h
+#define mozilla_extensions_ExtensionAPICallSyncFunction_h
+
+#include "ExtensionAPIRequestForwarder.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPICallSyncFunction : public ExtensionAPIRequestForwarder {
+ public:
+ ExtensionAPICallSyncFunction(const nsAString& aApiNamespace,
+ const nsAString& aApiMethod,
+ const nsAString& aApiObjectType = u""_ns,
+ const nsAString& aApiObjectId = u""_ns)
+ : ExtensionAPIRequestForwarder(
+ mozIExtensionAPIRequest::RequestType::CALL_FUNCTION, aApiNamespace,
+ aApiMethod, aApiObjectType, aApiObjectId) {}
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPICallSyncFunction_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIGetProperty.h b/toolkit/components/extensions/webidl-api/ExtensionAPIGetProperty.h
new file mode 100644
index 0000000000..30b9423dad
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIGetProperty.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPIGetProperty_h
+#define mozilla_extensions_ExtensionAPIGetProperty_h
+
+#include "ExtensionAPIRequestForwarder.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPIGetProperty : public ExtensionAPIRequestForwarder {
+ public:
+ ExtensionAPIGetProperty(const nsAString& aApiNamespace,
+ const nsAString& aApiProperty,
+ const nsAString& aApiObjectType = u""_ns,
+ const nsAString& aApiObjectId = u""_ns)
+ : ExtensionAPIRequestForwarder(
+ mozIExtensionAPIRequest::RequestType::GET_PROPERTY, aApiNamespace,
+ aApiProperty, aApiObjectType, aApiObjectId) {}
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPICallSyncFunction_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.cpp b/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.cpp
new file mode 100644
index 0000000000..e5dd443695
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.cpp
@@ -0,0 +1,242 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionAPIRequest.h"
+
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "mozilla/ipc/BackgroundUtils.h" // PrincipalInfoToPrincipal
+
+namespace mozilla {
+namespace extensions {
+
+// mozIExtensionServiceWorkerInfo
+
+NS_IMPL_ISUPPORTS(ExtensionServiceWorkerInfo, mozIExtensionServiceWorkerInfo)
+
+NS_IMETHODIMP
+ExtensionServiceWorkerInfo::GetPrincipal(nsIPrincipal** aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ auto principalOrErr = PrincipalInfoToPrincipal(mClientInfo.PrincipalInfo());
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ principal.forget(aPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionServiceWorkerInfo::GetScriptURL(nsAString& aScriptURL) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aScriptURL = NS_ConvertUTF8toUTF16(mClientInfo.URL());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionServiceWorkerInfo::GetClientInfoId(nsAString& aClientInfoId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aClientInfoId = NS_ConvertUTF8toUTF16(mClientInfo.Id().ToString().get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionServiceWorkerInfo::GetDescriptorId(uint64_t* aDescriptorId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ *aDescriptorId = mDescriptorId;
+ return NS_OK;
+}
+
+// mozIExtensionAPIRequest
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionAPIRequest)
+ NS_INTERFACE_MAP_ENTRY(mozIExtensionAPIRequest)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ExtensionAPIRequest)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionAPIRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionAPIRequest)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ExtensionAPIRequest)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSWInfo)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ExtensionAPIRequest)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mArgs)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mNormalizedArgs)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mStack)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ExtensionAPIRequest)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventListener)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSWInfo)
+ tmp->mStack.setUndefined();
+ tmp->mArgs.setUndefined();
+ tmp->mNormalizedArgs.setUndefined();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+ExtensionAPIRequest::ExtensionAPIRequest(
+ const mozIExtensionAPIRequest::RequestType aRequestType,
+ const ExtensionAPIRequestTarget& aRequestTarget) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mRequestType = aRequestType;
+ mRequestTarget = aRequestTarget;
+ mozilla::HoldJSObjects(this);
+}
+
+void ExtensionAPIRequest::Init(Maybe<dom::ClientInfo>& aSWClientInfo,
+ const uint64_t aSWDescriptorId,
+ JS::Handle<JS::Value> aRequestArgs,
+ JS::Handle<JS::Value> aCallerStack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSWClientInfo = aSWClientInfo;
+ mSWDescriptorId = aSWDescriptorId;
+ mArgs.set(aRequestArgs);
+ mStack.set(aCallerStack);
+ mNormalizedArgs.setUndefined();
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::ToString(nsACString& aResult) {
+ aResult.Truncate();
+
+ nsAutoCString requestType;
+ nsAutoCString apiNamespace;
+ nsAutoCString apiName;
+ GetRequestType(requestType);
+ GetApiNamespace(apiNamespace);
+ GetApiName(apiName);
+
+ if (mRequestTarget.mObjectType.IsEmpty()) {
+ aResult.AppendPrintf("[ExtensionAPIRequest %s %s.%s]", requestType.get(),
+ apiNamespace.get(), apiName.get());
+ } else {
+ nsAutoCString objectType;
+ nsAutoCString objectId;
+ GetApiObjectType(objectType);
+ GetApiObjectId(objectId);
+
+ aResult.AppendPrintf("[ExtensionAPIRequest %s %s.%s.%s (%s)]",
+ requestType.get(), apiNamespace.get(),
+ objectType.get(), apiName.get(), objectId.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetRequestType(nsACString& aRequestTypeName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ switch (mRequestType) {
+ case mozIExtensionAPIRequest::RequestType::CALL_FUNCTION:
+ aRequestTypeName = "callFunction"_ns;
+ break;
+ case mozIExtensionAPIRequest::RequestType::CALL_FUNCTION_NO_RETURN:
+ aRequestTypeName = "callFunctionNoReturn"_ns;
+ break;
+ case mozIExtensionAPIRequest::RequestType::CALL_FUNCTION_ASYNC:
+ aRequestTypeName = "callAsyncFunction"_ns;
+ break;
+ case mozIExtensionAPIRequest::RequestType::ADD_LISTENER:
+ aRequestTypeName = "addListener"_ns;
+ break;
+ case mozIExtensionAPIRequest::RequestType::REMOVE_LISTENER:
+ aRequestTypeName = "removeListener"_ns;
+ break;
+ case mozIExtensionAPIRequest::RequestType::GET_PROPERTY:
+ aRequestTypeName = "getProperty"_ns;
+ break;
+ default:
+ return NS_ERROR_UNEXPECTED;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetApiNamespace(nsACString& aApiNamespace) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aApiNamespace.Assign(NS_ConvertUTF16toUTF8(mRequestTarget.mNamespace));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetApiName(nsACString& aApiName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aApiName.Assign(NS_ConvertUTF16toUTF8(mRequestTarget.mMethod));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetApiObjectType(nsACString& aApiObjectType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aApiObjectType.Assign(NS_ConvertUTF16toUTF8(mRequestTarget.mObjectType));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetApiObjectId(nsACString& aApiObjectId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aApiObjectId.Assign(NS_ConvertUTF16toUTF8(mRequestTarget.mObjectId));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetArgs(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aRetval.set(mArgs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetNormalizedArgs(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aRetval.set(mNormalizedArgs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::SetNormalizedArgs(JSContext* aCx,
+ JS::Handle<JS::Value> aNormalizedArgs) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mNormalizedArgs.set(aNormalizedArgs);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetCallerSavedFrame(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aSavedFrame) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aSavedFrame.set(mStack);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetServiceWorkerInfo(
+ mozIExtensionServiceWorkerInfo** aSWInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aSWInfo);
+ if (mSWClientInfo.isSome() && !mSWInfo) {
+ mSWInfo = new ExtensionServiceWorkerInfo(*mSWClientInfo, mSWDescriptorId);
+ }
+ NS_IF_ADDREF(*aSWInfo = mSWInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ExtensionAPIRequest::GetEventListener(mozIExtensionEventListener** aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aListener);
+ NS_IF_ADDREF(*aListener = mEventListener);
+ return NS_OK;
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.h b/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.h
new file mode 100644
index 0000000000..b34d137958
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequest.h
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPIRequest_h
+#define mozilla_extensions_ExtensionAPIRequest_h
+
+#include "ExtensionEventListener.h"
+
+#include "mozIExtensionAPIRequestHandling.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/ClientInfo.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla {
+namespace extensions {
+
+class ExtensionAPIRequestForwarder;
+class RequestWorkerRunnable;
+
+// Represent the target of the API request forwarded, mObjectType and mObjectId
+// are only expected to be polulated when the API request is originated from API
+// object (like an ExtensionPort returned by a call to browser.runtime.connect).
+struct ExtensionAPIRequestTarget {
+ nsString mNamespace;
+ nsString mMethod;
+ nsString mObjectType;
+ nsString mObjectId;
+};
+
+// A class that represents the service worker that has originated the API
+// request.
+class ExtensionServiceWorkerInfo : public mozIExtensionServiceWorkerInfo {
+ public:
+ NS_DECL_MOZIEXTENSIONSERVICEWORKERINFO
+ NS_DECL_ISUPPORTS
+
+ explicit ExtensionServiceWorkerInfo(const dom::ClientInfo& aClientInfo,
+ const uint64_t aDescriptorId)
+ : mClientInfo(aClientInfo), mDescriptorId(aDescriptorId) {}
+
+ private:
+ virtual ~ExtensionServiceWorkerInfo() = default;
+
+ dom::ClientInfo mClientInfo;
+ uint64_t mDescriptorId;
+};
+
+// A class that represents a WebExtensions API request (a method call,
+// add/remote listener or accessing a property getter) forwarded by the
+// WebIDL bindings to the mozIExtensionAPIRequestHandler.
+class ExtensionAPIRequest : public mozIExtensionAPIRequest {
+ public:
+ using APIRequestType = mozIExtensionAPIRequest::RequestType;
+
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ExtensionAPIRequest)
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_MOZIEXTENSIONAPIREQUEST
+
+ explicit ExtensionAPIRequest(
+ const mozIExtensionAPIRequest::RequestType aRequestType,
+ const ExtensionAPIRequestTarget& aRequestTarget);
+
+ void Init(Maybe<dom::ClientInfo>& aSWClientInfo,
+ const uint64_t aSWDescriptorId, JS::Handle<JS::Value> aRequestArgs,
+ JS::Handle<JS::Value> aCallerStack);
+
+ static bool ShouldHaveResult(const APIRequestType& aRequestType) {
+ switch (aRequestType) {
+ case APIRequestType::GET_PROPERTY:
+ case APIRequestType::CALL_FUNCTION:
+ case APIRequestType::CALL_FUNCTION_ASYNC:
+ return true;
+ case APIRequestType::CALL_FUNCTION_NO_RETURN:
+ case APIRequestType::ADD_LISTENER:
+ case APIRequestType::REMOVE_LISTENER:
+ break;
+ default:
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected APIRequestType");
+ }
+
+ return false;
+ }
+
+ bool ShouldHaveResult() const { return ShouldHaveResult(mRequestType); }
+
+ void SetEventListener(const RefPtr<ExtensionEventListener>& aListener) {
+ MOZ_ASSERT(!mEventListener);
+ mEventListener = aListener;
+ }
+
+ private:
+ virtual ~ExtensionAPIRequest() {
+ mSWClientInfo = Nothing();
+ mArgs.setUndefined();
+ mNormalizedArgs.setUndefined();
+ mStack.setUndefined();
+ mEventListener = nullptr;
+ mozilla::DropJSObjects(this);
+ };
+
+ APIRequestType mRequestType;
+ ExtensionAPIRequestTarget mRequestTarget;
+ JS::Heap<JS::Value> mStack;
+ JS::Heap<JS::Value> mArgs;
+ JS::Heap<JS::Value> mNormalizedArgs;
+ Maybe<dom::ClientInfo> mSWClientInfo;
+ uint64_t mSWDescriptorId;
+ RefPtr<ExtensionServiceWorkerInfo> mSWInfo;
+
+ // Only set for addListener/removeListener API requests.
+ RefPtr<ExtensionEventListener> mEventListener;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPIRequest_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp
new file mode 100644
index 0000000000..81e571bf8e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.cpp
@@ -0,0 +1,710 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionAPIRequestForwarder.h"
+#include "ExtensionEventListener.h"
+#include "ExtensionAPIBase.h"
+
+#include "js/Promise.h"
+#include "js/PropertyAndElement.h" // JS_GetElement
+#include "mozilla/dom/Client.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/ClonedErrorHolder.h"
+#include "mozilla/dom/ClonedErrorHolderBinding.h"
+#include "mozilla/dom/ExtensionBrowserBinding.h"
+#include "mozilla/dom/FunctionBinding.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/dom/SerializedStackHolder.h"
+#include "mozilla/dom/ServiceWorkerInfo.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/dom/ServiceWorkerRegistrationInfo.h"
+#include "mozilla/dom/StructuredCloneTags.h"
+#include "mozilla/ExtensionPolicyService.h"
+#include "nsIGlobalObject.h"
+#include "nsImportModule.h"
+#include "nsIXPConnect.h"
+
+namespace mozilla {
+namespace extensions {
+
+// ExtensionAPIRequestForwarder
+
+// static
+void ExtensionAPIRequestForwarder::ThrowUnexpectedError(JSContext* aCx,
+ ErrorResult& aRv) {
+ aRv.MightThrowJSException();
+ JS_ReportErrorASCII(aCx, "An unexpected error occurred");
+ aRv.StealExceptionFromJSContext(aCx);
+}
+
+ExtensionAPIRequestForwarder::ExtensionAPIRequestForwarder(
+ const mozIExtensionAPIRequest::RequestType aRequestType,
+ const nsAString& aApiNamespace, const nsAString& aApiMethod,
+ const nsAString& aApiObjectType, const nsAString& aApiObjectId) {
+ mRequestType = aRequestType;
+ mRequestTarget.mNamespace = aApiNamespace;
+ mRequestTarget.mMethod = aApiMethod;
+ mRequestTarget.mObjectType = aApiObjectType;
+ mRequestTarget.mObjectId = aApiObjectId;
+}
+
+// static
+nsresult ExtensionAPIRequestForwarder::JSArrayToSequence(
+ JSContext* aCx, JS::Handle<JS::Value> aJSValue,
+ dom::Sequence<JS::Value>& aResult) {
+ bool isArray;
+ JS::Rooted<JSObject*> obj(aCx, aJSValue.toObjectOrNull());
+
+ if (NS_WARN_IF(!obj || !JS::IsArrayObject(aCx, obj, &isArray))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (isArray) {
+ uint32_t len;
+ if (NS_WARN_IF(!JS::GetArrayLength(aCx, obj, &len))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (uint32_t i = 0; i < len; i++) {
+ JS::Rooted<JS::Value> v(aCx);
+ JS_GetElement(aCx, obj, i, &v);
+ if (NS_WARN_IF(!aResult.AppendElement(v, fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ } else if (NS_WARN_IF(!aResult.AppendElement(aJSValue, fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+/* static */
+mozIExtensionAPIRequestHandler&
+ExtensionAPIRequestForwarder::APIRequestHandler() {
+ static nsCOMPtr<mozIExtensionAPIRequestHandler> sAPIRequestHandler;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (MOZ_UNLIKELY(!sAPIRequestHandler)) {
+ sAPIRequestHandler =
+ do_ImportModule("resource://gre/modules/ExtensionProcessScript.jsm",
+ "ExtensionAPIRequestHandler");
+ MOZ_RELEASE_ASSERT(sAPIRequestHandler);
+ ClearOnShutdown(&sAPIRequestHandler);
+ }
+ return *sAPIRequestHandler;
+}
+
+void ExtensionAPIRequestForwarder::SetSerializedCallerStack(
+ UniquePtr<dom::SerializedStackHolder> aCallerStack) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mStackHolder.isNothing());
+ mStackHolder = Some(std::move(aCallerStack));
+}
+
+void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+
+ dom::WorkerPrivate* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ RefPtr<RequestWorkerRunnable> runnable =
+ new RequestWorkerRunnable(workerPrivate, this);
+
+ if (mStackHolder.isSome()) {
+ runnable->SetSerializedCallerStack(mStackHolder.extract());
+ }
+
+ RefPtr<dom::Promise> domPromise;
+
+ IgnoredErrorResult rv;
+
+ switch (mRequestType) {
+ case APIRequestType::CALL_FUNCTION_ASYNC:
+ domPromise = dom::Promise::Create(aGlobal, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ runnable->Init(aGlobal, aCx, aArgs, domPromise, rv);
+ break;
+
+ case APIRequestType::ADD_LISTENER:
+ [[fallthrough]];
+ case APIRequestType::REMOVE_LISTENER:
+ runnable->Init(aGlobal, aCx, aArgs, aListener, aRv);
+ break;
+
+ default:
+ runnable->Init(aGlobal, aCx, aArgs, rv);
+ }
+
+ if (NS_WARN_IF(rv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ runnable->Dispatch(dom::WorkerStatus::Canceling, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ auto resultType = runnable->GetResultType();
+ if (resultType.isNothing()) {
+ if (NS_WARN_IF(ExtensionAPIRequest::ShouldHaveResult(mRequestType))) {
+ ThrowUnexpectedError(aCx, aRv);
+ }
+ return;
+ }
+
+ // Read and throw the extension error if needed.
+ if (resultType.isSome() && *resultType == APIResultType::EXTENSION_ERROR) {
+ JS::Rooted<JS::Value> ignoredResultValue(aCx);
+ runnable->ReadResult(aCx, &ignoredResultValue, aRv);
+ // When the result type is an error aRv is expected to be
+ // failed, if it is not throw the generic
+ // "An unexpected error occurred".
+ if (NS_WARN_IF(!aRv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ }
+ return;
+ }
+
+ if (mRequestType == APIRequestType::CALL_FUNCTION_ASYNC) {
+ MOZ_ASSERT(domPromise);
+ if (NS_WARN_IF(!ToJSValue(aCx, domPromise, aRetVal))) {
+ ThrowUnexpectedError(aCx, aRv);
+ }
+ return;
+ }
+
+ JS::Rooted<JS::Value> resultValue(aCx);
+ runnable->ReadResult(aCx, &resultValue, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ aRetVal.set(resultValue);
+}
+
+void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) {
+ Run(aGlobal, aCx, aArgs, nullptr, aRetVal, aRv);
+}
+
+void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ErrorResult& aRv) {
+ JS::Rooted<JS::Value> ignoredRetval(aCx);
+ Run(aGlobal, aCx, aArgs, nullptr, &ignoredRetval, aRv);
+}
+
+void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aListener);
+ JS::Rooted<JS::Value> ignoredRetval(aCx);
+ Run(aGlobal, aCx, aArgs, aListener, &ignoredRetval, aRv);
+}
+
+void ExtensionAPIRequestForwarder::Run(
+ nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv) {
+ MOZ_ASSERT(aPromiseRetval);
+ JS::Rooted<JS::Value> promisedRetval(aCx);
+ Run(aGlobal, aCx, aArgs, &promisedRetval, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ aPromiseRetval->MaybeResolve(promisedRetval);
+}
+
+void ExtensionAPIRequestForwarder::Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetVal,
+ ErrorResult& aRv) {
+ Run(aGlobal, aCx, {}, aRetVal, aRv);
+}
+
+namespace {
+
+// Custom PromiseWorkerProxy callback to deserialize error objects
+// from ClonedErrorHolder structured clone data.
+JSObject* ExtensionAPIRequestStructuredCloneRead(
+ JSContext* aCx, JSStructuredCloneReader* aReader,
+ const dom::PromiseWorkerProxy* aProxy, uint32_t aTag, uint32_t aData) {
+ // Deserialize ClonedErrorHolder that may have been structured cloned
+ // as a result of a resolved/rejected promise.
+ if (aTag == dom::SCTAG_DOM_CLONED_ERROR_OBJECT) {
+ return dom::ClonedErrorHolder::ReadStructuredClone(aCx, aReader, nullptr);
+ }
+
+ return nullptr;
+}
+
+// Custom PromiseWorkerProxy callback to serialize error objects into
+// ClonedErrorHolder structured clone data.
+bool ExtensionAPIRequestStructuredCloneWrite(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter,
+ dom::PromiseWorkerProxy* aProxy,
+ JS::Handle<JSObject*> aObj) {
+ // Try to serialize the object as a CloneErrorHolder, if it fails then
+ // the object wasn't an error.
+ IgnoredErrorResult rv;
+ RefPtr<dom::ClonedErrorHolder> ceh =
+ dom::ClonedErrorHolder::Create(aCx, aObj, rv);
+ if (NS_WARN_IF(rv.Failed()) || !ceh) {
+ return false;
+ }
+
+ return ceh->WriteStructuredClone(aCx, aWriter, nullptr);
+}
+
+} // namespace
+
+RequestWorkerRunnable::RequestWorkerRunnable(
+ dom::WorkerPrivate* aWorkerPrivate,
+ ExtensionAPIRequestForwarder* aOuterAPIRequest)
+ : WorkerMainThreadRunnable(aWorkerPrivate,
+ "ExtensionAPIRequest :: WorkerRunnable"_ns) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+
+ MOZ_ASSERT(aOuterAPIRequest);
+ mOuterRequest = aOuterAPIRequest;
+}
+
+void RequestWorkerRunnable::Init(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+
+ mSWDescriptorId = mWorkerPrivate->ServiceWorkerID();
+
+ auto* workerScope = mWorkerPrivate->GlobalScope();
+ if (NS_WARN_IF(!workerScope)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ mClientInfo = workerScope->GetClientInfo();
+ if (mClientInfo.isNothing()) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ IgnoredErrorResult rv;
+ SerializeArgs(aCx, aArgs, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ if (!mStackHolder.isSome()) {
+ SerializeCallerStack(aCx);
+ }
+
+ mEventListener = aListener;
+}
+
+void RequestWorkerRunnable::Init(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Promise>& aPromiseRetval,
+ ErrorResult& aRv) {
+ // Custom callbacks needed to make the PromiseWorkerProxy instance to
+ // be able to write and read errors using CloneErrorHolder.
+ static const dom::PromiseWorkerProxy::
+ PromiseWorkerProxyStructuredCloneCallbacks
+ kExtensionAPIRequestStructuredCloneCallbacks = {
+ ExtensionAPIRequestStructuredCloneRead,
+ ExtensionAPIRequestStructuredCloneWrite,
+ };
+
+ Init(aGlobal, aCx, aArgs, /* aListener */ nullptr, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<dom::PromiseWorkerProxy> promiseProxy =
+ dom::PromiseWorkerProxy::Create(
+ mWorkerPrivate, aPromiseRetval,
+ &kExtensionAPIRequestStructuredCloneCallbacks);
+ if (!promiseProxy) {
+ aRv.Throw(NS_ERROR_DOM_ABORT_ERR);
+ return;
+ }
+ mPromiseProxy = promiseProxy.forget();
+}
+
+void RequestWorkerRunnable::SetSerializedCallerStack(
+ UniquePtr<dom::SerializedStackHolder> aCallerStack) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mStackHolder.isNothing());
+ mStackHolder = Some(std::move(aCallerStack));
+}
+
+void RequestWorkerRunnable::SerializeCallerStack(JSContext* aCx) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(mStackHolder.isNothing());
+ mStackHolder = Some(dom::GetCurrentStack(aCx));
+}
+
+void RequestWorkerRunnable::DeserializeCallerStack(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mStackHolder.isSome()) {
+ JS::Rooted<JSObject*> savedFrame(aCx, mStackHolder->get()->ReadStack(aCx));
+ MOZ_ASSERT(savedFrame);
+ aRetval.set(JS::ObjectValue(*savedFrame));
+ mStackHolder = Nothing();
+ }
+}
+
+void RequestWorkerRunnable::SerializeArgs(JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(!mArgsHolder);
+
+ JS::Rooted<JS::Value> jsval(aCx);
+ if (NS_WARN_IF(!ToJSValue(aCx, aArgs, &jsval))) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ mArgsHolder = Some(MakeUnique<dom::StructuredCloneHolder>(
+ dom::StructuredCloneHolder::CloningSupported,
+ dom::StructuredCloneHolder::TransferringNotSupported,
+ JS::StructuredCloneScope::SameProcess));
+ mArgsHolder->get()->Write(aCx, jsval, aRv);
+}
+
+nsresult RequestWorkerRunnable::DeserializeArgs(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aArgs) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mArgsHolder.isSome() && mArgsHolder->get()->HasData()) {
+ IgnoredErrorResult rv;
+
+ JS::Rooted<JS::Value> jsvalue(aCx);
+ mArgsHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, &jsvalue, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ aArgs.set(jsvalue);
+ }
+
+ return NS_OK;
+}
+
+bool RequestWorkerRunnable::MainThreadRun() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<mozIExtensionAPIRequestHandler> handler =
+ &ExtensionAPIRequestForwarder::APIRequestHandler();
+ nsCOMPtr<nsIXPConnectWrappedJS> wrapped = do_QueryInterface(handler);
+ dom::AutoJSAPI jsapi;
+ if (!jsapi.Init(wrapped->GetJSObjectGlobal())) {
+ return false;
+ }
+
+ auto* cx = jsapi.cx();
+ JS::Rooted<JS::Value> retval(cx);
+ return HandleAPIRequest(cx, &retval);
+}
+
+already_AddRefed<ExtensionAPIRequest> RequestWorkerRunnable::CreateAPIRequest(
+ JSContext* aCx) {
+ JS::Rooted<JS::Value> callArgs(aCx);
+ JS::Rooted<JS::Value> callerStackValue(aCx);
+
+ DeserializeArgs(aCx, &callArgs);
+ DeserializeCallerStack(aCx, &callerStackValue);
+
+ RefPtr<ExtensionAPIRequest> request = new ExtensionAPIRequest(
+ mOuterRequest->GetRequestType(), *mOuterRequest->GetRequestTarget());
+ request->Init(mClientInfo, mSWDescriptorId, callArgs, callerStackValue);
+
+ if (mEventListener) {
+ request->SetEventListener(mEventListener.forget());
+ }
+
+ return request.forget();
+}
+
+already_AddRefed<WebExtensionPolicy>
+RequestWorkerRunnable::GetWebExtensionPolicy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mWorkerPrivate);
+ auto* baseURI = mWorkerPrivate->GetBaseURI();
+ RefPtr<WebExtensionPolicy> policy =
+ ExtensionPolicyService::GetSingleton().GetByURL(baseURI);
+ return policy.forget();
+}
+
+bool RequestWorkerRunnable::HandleAPIRequest(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<WebExtensionPolicy> policy = GetWebExtensionPolicy();
+ if (NS_WARN_IF(!policy || !policy->Active())) {
+ // Fails if no extension policy object has been found, or if the
+ // extension is not active.
+ return false;
+ }
+
+ nsresult rv;
+
+ RefPtr<ExtensionAPIRequest> request = CreateAPIRequest(aCx);
+
+ nsCOMPtr<mozIExtensionAPIRequestHandler> handler =
+ &ExtensionAPIRequestForwarder::APIRequestHandler();
+ RefPtr<mozIExtensionAPIRequestResult> apiResult;
+ rv = handler->HandleAPIRequest(policy, request, getter_AddRefs(apiResult));
+
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // A missing apiResult is expected for some request types
+ // (e.g. CALL_FUNCTION_NO_RETURN/ADD_LISTENER/REMOVE_LISTENER).
+ // If the apiResult is missing for a request type that expects
+ // to have one, consider the request as failed with an unknown error.
+ if (!apiResult) {
+ return !request->ShouldHaveResult();
+ }
+
+ mozIExtensionAPIRequestResult::ResultType resultType;
+ apiResult->GetType(&resultType);
+ apiResult->GetValue(aRetval);
+
+ mResultType = Some(resultType);
+
+ bool isExtensionError =
+ resultType == mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR;
+ bool okSerializedError = false;
+
+ if (aRetval.isObject()) {
+ // Try to serialize the result as an ClonedErrorHolder
+ // (because all API requests could receive one for EXTENSION_ERROR
+ // result types, and some also as a RETURN_VALUE result, e.g.
+ // runtime.lastError).
+ JS::Rooted<JSObject*> errObj(aCx, &aRetval.toObject());
+ IgnoredErrorResult rv;
+ RefPtr<dom::ClonedErrorHolder> ceh =
+ dom::ClonedErrorHolder::Create(aCx, errObj, rv);
+ if (!rv.Failed() && ceh) {
+ JS::Rooted<JSObject*> obj(aCx);
+ // Note: `ToJSValue` cannot be used because ClonedErrorHolder isn't
+ // wrapper cached.
+ okSerializedError = ceh->WrapObject(aCx, nullptr, &obj);
+ aRetval.setObject(*obj);
+ } else {
+ okSerializedError = false;
+ }
+ }
+
+ if (isExtensionError && !okSerializedError) {
+ NS_WARNING("Failed to wrap ClonedErrorHolder");
+ MOZ_DIAGNOSTIC_ASSERT(false, "Failed to wrap ClonedErrorHolder");
+ return false;
+ }
+
+ if (isExtensionError && !aRetval.isObject()) {
+ NS_WARNING("Unexpected non-object error");
+ return false;
+ }
+
+ switch (resultType) {
+ case mozIExtensionAPIRequestResult::ResultType::RETURN_VALUE:
+ return ProcessHandlerResult(aCx, aRetval);
+ case mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR:
+ if (!aRetval.isObject()) {
+ return false;
+ }
+ return ProcessHandlerResult(aCx, aRetval);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType");
+ return false;
+}
+
+bool RequestWorkerRunnable::ProcessHandlerResult(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mOuterRequest->GetRequestType() == APIRequestType::CALL_FUNCTION_ASYNC) {
+ if (NS_WARN_IF(mResultType.isNothing())) {
+ return false;
+ }
+
+ if (*mResultType == APIResultType::RETURN_VALUE) {
+ // For an Async API method we expect a promise object to be set
+ // as the value to return, if it is not we return earlier here
+ // (and then throw a generic unexpected error to the caller).
+ if (NS_WARN_IF(!aRetval.isObject())) {
+ return false;
+ }
+ JS::Rooted<JSObject*> obj(aCx, &aRetval.toObject());
+ if (NS_WARN_IF(!JS::IsPromiseObject(obj))) {
+ return false;
+ }
+
+ ErrorResult rv;
+ nsIGlobalObject* glob = xpc::CurrentNativeGlobal(aCx);
+ RefPtr<dom::Promise> retPromise =
+ dom::Promise::Resolve(glob, aCx, aRetval, rv);
+ if (rv.Failed()) {
+ return false;
+ }
+ retPromise->AppendNativeHandler(mPromiseProxy);
+ return true;
+ }
+ }
+
+ switch (*mResultType) {
+ case APIResultType::RETURN_VALUE:
+ [[fallthrough]];
+ case APIResultType::EXTENSION_ERROR: {
+ // In all other case we expect the result to be:
+ // - a structured clonable result
+ // - an extension error (e.g. due to the API call params validation
+ // errors),
+ // previously converted into a CloneErrorHolder
+ IgnoredErrorResult rv;
+ mResultHolder = Some(MakeUnique<dom::StructuredCloneHolder>(
+ dom::StructuredCloneHolder::CloningSupported,
+ dom::StructuredCloneHolder::TransferringNotSupported,
+ JS::StructuredCloneScope::SameProcess));
+ mResultHolder->get()->Write(aCx, aRetval, rv);
+ return !rv.Failed();
+ }
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType");
+ return false;
+}
+
+void RequestWorkerRunnable::ReadResult(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(mWorkerPrivate->IsOnCurrentThread());
+ if (mResultHolder.isNothing() || !mResultHolder->get()->HasData()) {
+ return;
+ }
+
+ if (NS_WARN_IF(mResultType.isNothing())) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ switch (*mResultType) {
+ case mozIExtensionAPIRequestResult::ResultType::RETURN_VALUE:
+ mResultHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, aResult,
+ aRv);
+ return;
+ case mozIExtensionAPIRequestResult::ResultType::EXTENSION_ERROR:
+ JS::Rooted<JS::Value> exn(aCx);
+ IgnoredErrorResult rv;
+ mResultHolder->get()->Read(xpc::CurrentNativeGlobal(aCx), aCx, &exn, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to deserialize extension error");
+ ExtensionAPIBase::ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ aRv.MightThrowJSException();
+ aRv.ThrowJSException(aCx, exn);
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unexpected API request ResultType");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+}
+
+// RequestInitWorkerContextRunnable
+
+RequestInitWorkerRunnable::RequestInitWorkerRunnable(
+ dom::WorkerPrivate* aWorkerPrivate, Maybe<dom::ClientInfo>& aSWClientInfo)
+ : WorkerMainThreadRunnable(aWorkerPrivate,
+ "extensions::RequestInitWorkerRunnable"_ns) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ MOZ_ASSERT(aSWClientInfo.isSome());
+ mClientInfo = aSWClientInfo;
+}
+
+bool RequestInitWorkerRunnable::MainThreadRun() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto* baseURI = mWorkerPrivate->GetBaseURI();
+ RefPtr<WebExtensionPolicy> policy =
+ ExtensionPolicyService::GetSingleton().GetByURL(baseURI);
+
+ RefPtr<ExtensionServiceWorkerInfo> swInfo = new ExtensionServiceWorkerInfo(
+ *mClientInfo, mWorkerPrivate->ServiceWorkerID());
+
+ nsCOMPtr<mozIExtensionAPIRequestHandler> handler =
+ &ExtensionAPIRequestForwarder::APIRequestHandler();
+ MOZ_ASSERT(handler);
+
+ if (NS_FAILED(handler->InitExtensionWorker(policy, swInfo))) {
+ NS_WARNING("nsIExtensionAPIRequestHandler.initExtensionWorker call failed");
+ }
+
+ return true;
+}
+
+// NotifyWorkerLoadedRunnable
+
+nsresult NotifyWorkerLoadedRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<WebExtensionPolicy> policy =
+ ExtensionPolicyService::GetSingleton().GetByURL(mSWBaseURI.get());
+
+ nsCOMPtr<mozIExtensionAPIRequestHandler> handler =
+ &ExtensionAPIRequestForwarder::APIRequestHandler();
+ MOZ_ASSERT(handler);
+
+ if (NS_FAILED(handler->OnExtensionWorkerLoaded(policy, mSWDescriptorId))) {
+ NS_WARNING(
+ "nsIExtensionAPIRequestHandler.onExtensionWorkerLoaded call failed");
+ }
+
+ return NS_OK;
+}
+
+// NotifyWorkerDestroyedRunnable
+
+nsresult NotifyWorkerDestroyedRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<WebExtensionPolicy> policy =
+ ExtensionPolicyService::GetSingleton().GetByURL(mSWBaseURI.get());
+
+ nsCOMPtr<mozIExtensionAPIRequestHandler> handler =
+ &ExtensionAPIRequestForwarder::APIRequestHandler();
+ MOZ_ASSERT(handler);
+
+ if (NS_FAILED(handler->OnExtensionWorkerDestroyed(policy, mSWDescriptorId))) {
+ NS_WARNING(
+ "nsIExtensionAPIRequestHandler.onExtensionWorkerDestroyed call failed");
+ }
+
+ return NS_OK;
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h
new file mode 100644
index 0000000000..a74c3e7c45
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAPIRequestForwarder.h
@@ -0,0 +1,258 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAPIRequestForwarder_h
+#define mozilla_extensions_ExtensionAPIRequestForwarder_h
+
+#include "ExtensionAPIRequest.h"
+
+#include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/SerializedStackHolder.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/ToJSValue.h"
+
+namespace mozilla {
+namespace dom {
+class ClientInfoAndState;
+class Function;
+} // namespace dom
+namespace extensions {
+
+class ExtensionAPIRequestForwarder;
+
+// A class used to forward an API request (a method call, add/remote listener or
+// a property getter) originated from a WebExtensions global (a window, a
+// content script sandbox or a service worker) to the JS privileged API request
+// handler available on the main thread (mozIExtensionAPIRequestHandler).
+//
+// Instances of this class are meant to be short-living, and destroyed when the
+// caller function is exiting.
+class ExtensionAPIRequestForwarder {
+ friend class ExtensionAPIRequest;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExtensionAPIRequestForwarder)
+
+ public:
+ using APIRequestType = mozIExtensionAPIRequest::RequestType;
+ using APIResultType = mozIExtensionAPIRequestResult::ResultType;
+
+ static nsresult JSArrayToSequence(JSContext* aCx,
+ JS::Handle<JS::Value> aJSValue,
+ dom::Sequence<JS::Value>& aResult);
+
+ static void ThrowUnexpectedError(JSContext* aCx, ErrorResult& aRv);
+
+ static mozIExtensionAPIRequestHandler& APIRequestHandler();
+
+ ExtensionAPIRequestForwarder(const APIRequestType aRequestType,
+ const nsAString& aApiNamespace,
+ const nsAString& aApiMethod,
+ const nsAString& aApiObjectType = u""_ns,
+ const nsAString& aApiObjectId = u""_ns);
+
+ mozIExtensionAPIRequest::RequestType GetRequestType() const {
+ return mRequestType;
+ }
+
+ const ExtensionAPIRequestTarget* GetRequestTarget() {
+ return &mRequestTarget;
+ }
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv);
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener, ErrorResult& aRv);
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv);
+
+ void Run(nsIGlobalObject* aGlobal, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetVal, ErrorResult& aRv);
+
+ void SetSerializedCallerStack(
+ UniquePtr<dom::SerializedStackHolder> aCallerStack);
+
+ protected:
+ virtual ~ExtensionAPIRequestForwarder() = default;
+
+ private:
+ already_AddRefed<ExtensionAPIRequest> CreateAPIRequest(
+ nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs, ExtensionEventListener* aListener,
+ ErrorResult& aRv);
+
+ APIRequestType mRequestType;
+ ExtensionAPIRequestTarget mRequestTarget;
+ Maybe<UniquePtr<dom::SerializedStackHolder>> mStackHolder;
+};
+
+/*
+ * This runnable is used internally by ExtensionAPIRequestForwader class
+ * to call the JS privileged code that handle the API requests originated
+ * from the WebIDL bindings instantiated in a worker thread.
+ *
+ * The runnable is meant to block the worker thread until we get a result
+ * from the JS privileged code that handles the API request.
+ *
+ * For async API calls we still need to block the worker thread until
+ * we get a promise (which we link to the worker thread promise and
+ * at that point we unblock the worker thread), because the JS privileged
+ * code handling the API request may need to throw some errors synchonously
+ * (e.g. in case of additional validations based on the API schema definition
+ * for the parameter, like strings that has to pass additional validation
+ * or normalizations).
+ */
+class RequestWorkerRunnable : public dom::WorkerMainThreadRunnable {
+ public:
+ using APIRequestType = mozIExtensionAPIRequest::RequestType;
+ using APIResultType = mozIExtensionAPIRequestResult::ResultType;
+
+ RequestWorkerRunnable(dom::WorkerPrivate* aWorkerPrivate,
+ ExtensionAPIRequestForwarder* aOuterAPIRequest);
+
+ void SetSerializedCallerStack(
+ UniquePtr<dom::SerializedStackHolder> aCallerStack);
+
+ /**
+ * Init a request runnable for AddListener and RemoveListener API requests
+ * (which do have an event callback callback and do not expect any return
+ * value).
+ */
+ void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ ExtensionEventListener* aListener, ErrorResult& aRv);
+
+ /**
+ * Init a request runnable for CallFunctionNoReturn API requests (which do
+ * do not expect any return value).
+ */
+ void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ Init(aGlobal, aCx, aArgs, nullptr, aRv);
+ }
+
+ /**
+ * Init a request runnable for CallAsyncFunction API requests (which do
+ * expect a promise as return value).
+ */
+ void Init(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const dom::Sequence<JS::Value>& aArgs,
+ const RefPtr<dom::Promise>& aPromiseRetval, ErrorResult& aRv);
+
+ bool MainThreadRun() override;
+
+ void ReadResult(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ ErrorResult& aRv);
+
+ Maybe<mozIExtensionAPIRequestResult::ResultType> GetResultType() {
+ return mResultType;
+ }
+
+ protected:
+ virtual bool ProcessHandlerResult(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval);
+
+ already_AddRefed<WebExtensionPolicy> GetWebExtensionPolicy();
+ already_AddRefed<ExtensionAPIRequest> CreateAPIRequest(JSContext* aCx);
+
+ void SerializeCallerStack(JSContext* aCx);
+ void DeserializeCallerStack(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval);
+ void SerializeArgs(JSContext* aCx, const dom::Sequence<JS::Value>& aArgs,
+ ErrorResult& aRv);
+ nsresult DeserializeArgs(JSContext* aCx, JS::MutableHandle<JS::Value> aArgs);
+
+ bool HandleAPIRequest(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ Maybe<mozIExtensionAPIRequestResult::ResultType> mResultType;
+ Maybe<UniquePtr<dom::StructuredCloneHolder>> mResultHolder;
+ RefPtr<dom::PromiseWorkerProxy> mPromiseProxy;
+ Maybe<UniquePtr<dom::StructuredCloneHolder>> mArgsHolder;
+ Maybe<UniquePtr<dom::SerializedStackHolder>> mStackHolder;
+ Maybe<dom::ClientInfo> mClientInfo;
+ uint64_t mSWDescriptorId;
+
+ // Only set for addListener/removeListener API requests.
+ RefPtr<ExtensionEventListener> mEventListener;
+
+ // The outer request object is kept alive by the caller for the
+ // entire life of the inner worker runnable.
+ ExtensionAPIRequestForwarder* mOuterRequest;
+};
+
+class RequestInitWorkerRunnable : public dom::WorkerMainThreadRunnable {
+ Maybe<dom::ClientInfo> mClientInfo;
+
+ public:
+ RequestInitWorkerRunnable(dom::WorkerPrivate* aWorkerPrivate,
+ Maybe<dom::ClientInfo>& aSWClientInfo);
+ bool MainThreadRun() override;
+};
+
+class NotifyWorkerLoadedRunnable : public Runnable {
+ uint64_t mSWDescriptorId;
+ nsCOMPtr<nsIURI> mSWBaseURI;
+
+ public:
+ explicit NotifyWorkerLoadedRunnable(const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI)
+ : Runnable("extensions::NotifyWorkerLoadedRunnable"),
+ mSWDescriptorId(aServiceWorkerDescriptorId),
+ mSWBaseURI(aWorkerBaseURI) {
+ MOZ_ASSERT(mSWDescriptorId > 0);
+ MOZ_ASSERT(mSWBaseURI);
+ }
+
+ NS_IMETHOD Run() override;
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(NotifyWorkerLoadedRunnable, Runnable)
+
+ private:
+ ~NotifyWorkerLoadedRunnable() = default;
+};
+
+class NotifyWorkerDestroyedRunnable : public Runnable {
+ uint64_t mSWDescriptorId;
+ nsCOMPtr<nsIURI> mSWBaseURI;
+
+ public:
+ explicit NotifyWorkerDestroyedRunnable(
+ const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI)
+ : Runnable("extensions::NotifyWorkerDestroyedRunnable"),
+ mSWDescriptorId(aServiceWorkerDescriptorId),
+ mSWBaseURI(aWorkerBaseURI) {
+ MOZ_ASSERT(mSWDescriptorId > 0);
+ MOZ_ASSERT(mSWBaseURI);
+ }
+
+ NS_IMETHOD Run() override;
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(NotifyWorkerDestroyedRunnable, Runnable)
+
+ private:
+ ~NotifyWorkerDestroyedRunnable() = default;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAPIRequestForwarder_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAlarms.cpp b/toolkit/components/extensions/webidl-api/ExtensionAlarms.cpp
new file mode 100644
index 0000000000..71f04e3ded
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAlarms.cpp
@@ -0,0 +1,49 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionAlarms.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionAlarmsBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionAlarms);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionAlarms)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionAlarms, mGlobal,
+ mExtensionBrowser, mOnAlarmEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionAlarms)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionAlarms, u"onAlarm"_ns, OnAlarm)
+
+ExtensionAlarms::ExtensionAlarms(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionAlarms::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ // TODO(Bug 1725478): this API visibility should be gated by the "alarms"
+ // permission.
+ return true;
+}
+
+JSObject* ExtensionAlarms::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionAlarms_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionAlarms::GetParentObject() const { return mGlobal; }
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionAlarms.h b/toolkit/components/extensions/webidl-api/ExtensionAlarms.h
new file mode 100644
index 0000000000..b969a2eb69
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionAlarms.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionAlarms_h
+#define mozilla_extensions_ExtensionAlarms_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace extensions {
+
+class ExtensionEventManager;
+
+class ExtensionAlarms final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionAlarms(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"alarms"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionEventManager* OnAlarm();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionAlarms)
+
+ private:
+ ~ExtensionAlarms() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnAlarmEventMgr;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionAlarms_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowser.cpp b/toolkit/components/extensions/webidl-api/ExtensionBrowser.cpp
new file mode 100644
index 0000000000..069c914dc4
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowser.cpp
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionBrowser.h"
+#include "ExtensionAPIRequestForwarder.h"
+
+#include "mozilla/dom/ExtensionBrowserBinding.h"
+#include "mozilla/dom/ExtensionPortBinding.h" // ExtensionPortDescriptor
+#include "mozilla/dom/WorkerScope.h" // GetWorkerPrivateFromContext
+#include "mozilla/extensions/ExtensionAlarms.h"
+#include "mozilla/extensions/ExtensionBrowserSettings.h"
+#include "mozilla/extensions/ExtensionDns.h"
+#include "mozilla/extensions/ExtensionMockAPI.h"
+#include "mozilla/extensions/ExtensionPort.h"
+#include "mozilla/extensions/ExtensionProxy.h"
+#include "mozilla/extensions/ExtensionRuntime.h"
+#include "mozilla/extensions/ExtensionScripting.h"
+#include "mozilla/extensions/ExtensionTest.h"
+#include "mozilla/extensions/WebExtensionPolicy.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ExtensionBrowser)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionBrowser)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionBrowser)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionBrowser)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ExtensionBrowser)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionAlarms)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionBrowserSettings)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionDns)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionMockAPI)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionProxy)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionRuntime)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionScripting)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionTest)
+ tmp->mLastError.setUndefined();
+ tmp->mPortsLookup.Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ExtensionBrowser)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionAlarms)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionBrowserSettings)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionDns)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionMockAPI)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionRuntime)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionProxy)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionScripting)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionTest)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ExtensionBrowser)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mLastError)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+ExtensionBrowser::ExtensionBrowser(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+}
+
+JSObject* ExtensionBrowser::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionBrowser_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionBrowser::GetParentObject() const { return mGlobal; }
+
+bool ExtensionAPIAllowed(JSContext* aCx, JSObject* aGlobal) {
+#ifdef MOZ_WEBEXT_WEBIDL_ENABLED
+ // Only expose the Extension API bindings if:
+ // - the context is related to a worker where the Extension API are allowed
+ // (currently only the extension service worker declared in the extension
+ // manifest met this condition)
+ // - the global is an extension window or an extension content script sandbox
+ // TODO:
+ // - the support for the extension window is deferred to a followup.
+ // - support for the content script sandboxes is also deferred to follow-ups
+ // - lock native Extension API in an extension window or sandbox behind a
+ // separate pref.
+ MOZ_DIAGNOSTIC_ASSERT(
+ !NS_IsMainThread(),
+ "ExtensionAPI webidl bindings does not yet support main thread globals");
+
+ // Verify if the Extensions API should be allowed on a worker thread.
+ if (!StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) {
+ return false;
+ }
+
+ auto* workerPrivate = mozilla::dom::GetWorkerPrivateFromContext(aCx);
+ MOZ_ASSERT(workerPrivate);
+ MOZ_ASSERT(workerPrivate->IsServiceWorker());
+
+ return workerPrivate->ExtensionAPIAllowed();
+#else
+ // Always return false on build where MOZ_WEBEXT_WEBIDL_ENABLED is set to
+ // false (currently on all channels but nightly).
+ return false;
+#endif
+}
+
+void CreateAndDispatchInitWorkerContextRunnable() {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ // DO NOT pass this WorkerPrivate raw pointer to anything else but the
+ // RequestInitWorkerRunnable (which extends dom::WorkerMainThreadRunnable).
+ dom::WorkerPrivate* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ MOZ_ASSERT(workerPrivate->ExtensionAPIAllowed());
+ MOZ_ASSERT(workerPrivate->IsServiceWorker());
+ workerPrivate->AssertIsOnWorkerThread();
+
+ auto* workerScope = workerPrivate->GlobalScope();
+ if (NS_WARN_IF(!workerScope)) {
+ return;
+ }
+
+ Maybe<dom::ClientInfo> clientInfo = workerScope->GetClientInfo();
+ if (NS_WARN_IF(clientInfo.isNothing())) {
+ return;
+ }
+
+ RefPtr<RequestInitWorkerRunnable> runnable =
+ new RequestInitWorkerRunnable(std::move(workerPrivate), clientInfo);
+ IgnoredErrorResult rv;
+ runnable->Dispatch(dom::WorkerStatus::Canceling, rv);
+ if (rv.Failed()) {
+ NS_WARNING("Failed to dispatch extensions::RequestInitWorkerRunnable");
+ }
+}
+
+already_AddRefed<Runnable> CreateWorkerLoadedRunnable(
+ const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI) {
+ RefPtr<NotifyWorkerLoadedRunnable> runnable = new NotifyWorkerLoadedRunnable(
+ aServiceWorkerDescriptorId, aWorkerBaseURI);
+ return runnable.forget();
+}
+
+already_AddRefed<Runnable> CreateWorkerDestroyedRunnable(
+ const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI) {
+ RefPtr<NotifyWorkerDestroyedRunnable> runnable =
+ new NotifyWorkerDestroyedRunnable(aServiceWorkerDescriptorId,
+ aWorkerBaseURI);
+ return runnable.forget();
+}
+
+void ExtensionBrowser::SetLastError(JS::Handle<JS::Value> aLastError) {
+ mLastError.set(aLastError);
+ mCheckedLastError = false;
+}
+
+void ExtensionBrowser::GetLastError(JS::MutableHandle<JS::Value> aRetVal) {
+ aRetVal.set(mLastError);
+ mCheckedLastError = true;
+}
+
+bool ExtensionBrowser::ClearLastError() {
+ bool shouldReport = !mCheckedLastError;
+ mLastError.setUndefined();
+ return shouldReport;
+}
+
+already_AddRefed<ExtensionPort> ExtensionBrowser::GetPort(
+ JS::Handle<JS::Value> aDescriptorValue, ErrorResult& aRv) {
+ // Get a port descriptor from the js value got from the API request
+ // handler.
+ UniquePtr<dom::ExtensionPortDescriptor> portDescriptor =
+ ExtensionPort::ToPortDescriptor(aDescriptorValue, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ auto portId = portDescriptor->mPortId;
+ auto maybePort = mPortsLookup.MaybeGet(portId);
+ if (maybePort.isSome() && maybePort.value().get()) {
+ RefPtr<ExtensionPort> existingPort = maybePort.value().get();
+ return existingPort.forget();
+ }
+
+ RefPtr<ExtensionPort> newPort =
+ ExtensionPort::Create(mGlobal, this, std::move(portDescriptor));
+ mPortsLookup.InsertOrUpdate(portId, newPort);
+ return newPort.forget();
+}
+
+void ExtensionBrowser::ForgetReleasedPort(const nsAString& aPortId) {
+ mPortsLookup.Remove(aPortId);
+}
+
+ExtensionAlarms* ExtensionBrowser::GetExtensionAlarms() {
+ if (!mExtensionAlarms) {
+ mExtensionAlarms = new ExtensionAlarms(mGlobal, this);
+ }
+
+ return mExtensionAlarms;
+}
+
+ExtensionBrowserSettings* ExtensionBrowser::GetExtensionBrowserSettings() {
+ if (!mExtensionBrowserSettings) {
+ mExtensionBrowserSettings = new ExtensionBrowserSettings(mGlobal, this);
+ }
+
+ return mExtensionBrowserSettings;
+}
+
+ExtensionDns* ExtensionBrowser::GetExtensionDns() {
+ if (!mExtensionDns) {
+ mExtensionDns = new ExtensionDns(mGlobal, this);
+ }
+
+ return mExtensionDns;
+}
+
+ExtensionMockAPI* ExtensionBrowser::GetExtensionMockAPI() {
+ if (!mExtensionMockAPI) {
+ mExtensionMockAPI = new ExtensionMockAPI(mGlobal, this);
+ }
+
+ return mExtensionMockAPI;
+}
+
+ExtensionProxy* ExtensionBrowser::GetExtensionProxy() {
+ if (!mExtensionProxy) {
+ mExtensionProxy = new ExtensionProxy(mGlobal, this);
+ }
+
+ return mExtensionProxy;
+}
+
+ExtensionRuntime* ExtensionBrowser::GetExtensionRuntime() {
+ if (!mExtensionRuntime) {
+ mExtensionRuntime = new ExtensionRuntime(mGlobal, this);
+ }
+
+ return mExtensionRuntime;
+}
+
+ExtensionScripting* ExtensionBrowser::GetExtensionScripting() {
+ if (!mExtensionScripting) {
+ mExtensionScripting = new ExtensionScripting(mGlobal, this);
+ }
+
+ return mExtensionScripting;
+}
+
+ExtensionTest* ExtensionBrowser::GetExtensionTest() {
+ if (!mExtensionTest) {
+ mExtensionTest = new ExtensionTest(mGlobal, this);
+ }
+
+ return mExtensionTest;
+}
+
+// static
+void ExtensionEventWakeupMap::ToMapKey(const nsAString& aAPINamespace,
+ const nsAString& aAPIName,
+ nsAString& aResultMapKey) {
+ aResultMapKey.Truncate();
+ aResultMapKey.AppendPrintf("%s.%s",
+ NS_ConvertUTF16toUTF8(aAPINamespace).get(),
+ NS_ConvertUTF16toUTF8(aAPIName).get());
+}
+
+nsresult ExtensionEventWakeupMap::IncrementListeners(
+ const nsAString& aAPINamespace, const nsAString& aAPIName) {
+ nsString key;
+ ToMapKey(aAPINamespace, aAPIName, key);
+ auto maybeCount = MaybeGet(key);
+ if (maybeCount.isSome()) {
+ InsertOrUpdate(key, maybeCount.value() + 1);
+ } else {
+ InsertOrUpdate(key, 1);
+ }
+
+ return NS_OK;
+}
+
+nsresult ExtensionEventWakeupMap::DecrementListeners(
+ const nsAString& aAPINamespace, const nsAString& aAPIName) {
+ nsString key;
+ ToMapKey(aAPINamespace, aAPIName, key);
+ auto maybeCount = MaybeGet(key);
+ if (maybeCount.isSome()) {
+ MOZ_ASSERT(maybeCount.value() >= 1, "Unexpected counter value set to zero");
+ uint64_t val = maybeCount.value() - 1;
+ if (val == 0) {
+ Remove(key);
+ } else {
+ InsertOrUpdate(key, val);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool ExtensionEventWakeupMap::HasListener(const nsAString& aAPINamespace,
+ const nsAString& aAPIName) {
+ nsString key;
+ ToMapKey(aAPINamespace, aAPIName, key);
+ auto maybeCount = MaybeGet(key);
+ return (maybeCount.isSome() && maybeCount.value() > 0);
+}
+
+nsresult ExtensionBrowser::TrackWakeupEventListener(
+ JSContext* aCx, const nsString& aAPINamespace, const nsString& aAPIName) {
+ auto* workerPrivate = mozilla::dom::GetWorkerPrivateFromContext(aCx);
+ if (workerPrivate->WorkerScriptExecutedSuccessfully()) {
+ // Ignore if the worker script has already executed all its synchronous
+ // statements.
+ return NS_OK;
+ }
+ mExpectedEventWakeupMap.IncrementListeners(aAPINamespace, aAPIName);
+ return NS_OK;
+}
+
+nsresult ExtensionBrowser::UntrackWakeupEventListener(
+ JSContext* aCx, const nsString& aAPINamespace, const nsString& aAPIName) {
+ auto* workerPrivate = mozilla::dom::GetWorkerPrivateFromContext(aCx);
+ if (workerPrivate->WorkerScriptExecutedSuccessfully()) {
+ // Ignore if the worker script has already executed all its synchronous
+ return NS_OK;
+ }
+ mExpectedEventWakeupMap.DecrementListeners(aAPINamespace, aAPIName);
+ return NS_OK;
+}
+
+bool ExtensionBrowser::HasWakeupEventListener(const nsString& aAPINamespace,
+ const nsString& aAPIName) {
+ return mExpectedEventWakeupMap.HasListener(aAPINamespace, aAPIName);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowser.h b/toolkit/components/extensions/webidl-api/ExtensionBrowser.h
new file mode 100644
index 0000000000..3c4b831b4e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowser.h
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionBrowser_h
+#define mozilla_extensions_ExtensionBrowser_h
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsTHashMap.h"
+#include "nsWrapperCache.h"
+#include "mozilla/WeakPtr.h"
+#include "xpcpublic.h"
+
+class nsIGlobalObject;
+class nsIURI;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace extensions {
+
+class ExtensionAlarms;
+class ExtensionBrowserSettings;
+class ExtensionDns;
+class ExtensionMockAPI;
+class ExtensionPort;
+class ExtensionProxy;
+class ExtensionRuntime;
+class ExtensionScripting;
+class ExtensionTest;
+
+bool ExtensionAPIAllowed(JSContext* aCx, JSObject* aGlobal);
+
+void CreateAndDispatchInitWorkerContextRunnable();
+
+already_AddRefed<Runnable> CreateWorkerLoadedRunnable(
+ const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI);
+
+already_AddRefed<Runnable> CreateWorkerDestroyedRunnable(
+ const uint64_t aServiceWorkerDescriptorId,
+ const nsCOMPtr<nsIURI>& aWorkerBaseURI);
+
+// An HashMap used to keep track of listeners registered synchronously while
+// the worker script is executing, used internally by nsIServiceWorkerManager
+// wakeforExtensionAPIEvent method to resolve to true if the worker script
+// spawned did have a listener subscribed for the related API event name.
+class ExtensionEventWakeupMap final
+ : public nsTHashMap<nsStringHashKey, uint64_t> {
+ static void ToMapKey(const nsAString& aAPINamespace,
+ const nsAString& aAPIName, nsAString& aResultMapKey);
+
+ public:
+ nsresult IncrementListeners(const nsAString& aAPINamespace,
+ const nsAString& aAPIName);
+ nsresult DecrementListeners(const nsAString& aAPINamespace,
+ const nsAString& aAPIName);
+ bool HasListener(const nsAString& aAPINamespace, const nsAString& aAPIName);
+};
+
+class ExtensionBrowser final : public nsISupports, public nsWrapperCache {
+ public:
+ explicit ExtensionBrowser(nsIGlobalObject* aGlobal);
+
+ // Helpers used for the expected behavior of the browser.runtime.lastError
+ // and browser.extension.lastError.
+ void SetLastError(JS::Handle<JS::Value> aLastError);
+ void GetLastError(JS::MutableHandle<JS::Value> aRetVal);
+ // ClearLastError is used by ChromeCompatCallbackHandler::RejectedCallback
+ // to clear the lastError property. When this method returns true the
+ // caller will know that the error value wasn't checked by the callback and
+ // should be reported to the console
+ bool ClearLastError();
+
+ // Helpers used to keep track of the event listeners added during the
+ // initial sync worker script execution.
+ nsresult TrackWakeupEventListener(JSContext* aCx,
+ const nsString& aAPINamespace,
+ const nsString& aAPIName);
+ nsresult UntrackWakeupEventListener(JSContext* aCx,
+ const nsString& aAPINamespace,
+ const nsString& aAPIName);
+ bool HasWakeupEventListener(const nsString& aAPINamespace,
+ const nsString& aAPIName);
+
+ // Helpers used for the ExtensionPort.
+
+ // Get an ExtensionPort instance given its port descriptor (returns an
+ // existing port if an instance is still tracked in the ports lookup table,
+ // otherwise it creates a new one).
+ already_AddRefed<ExtensionPort> GetPort(
+ JS::Handle<JS::Value> aDescriptorValue, ErrorResult& aRv);
+
+ // Remove the entry for an ExtensionPort tracked in the ports lookup map
+ // given its portId (called from the ExtensionPort destructor when the
+ // instance is being released).
+ void ForgetReleasedPort(const nsAString& aPortId);
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionAlarms* GetExtensionAlarms();
+ ExtensionBrowserSettings* GetExtensionBrowserSettings();
+ ExtensionDns* GetExtensionDns();
+ ExtensionMockAPI* GetExtensionMockAPI();
+ ExtensionProxy* GetExtensionProxy();
+ ExtensionRuntime* GetExtensionRuntime();
+ ExtensionScripting* GetExtensionScripting();
+ ExtensionTest* GetExtensionTest();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ExtensionBrowser)
+
+ private:
+ ~ExtensionBrowser() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ JS::Heap<JS::Value> mLastError;
+ bool mCheckedLastError;
+ nsTHashMap<nsStringHashKey, WeakPtr<ExtensionPort>> mPortsLookup;
+ // `[APINamespace].[APIName]` => int64 (listeners count)
+ ExtensionEventWakeupMap mExpectedEventWakeupMap;
+ // NOTE: Make sure to don't forget to add for new API namespace instances
+ // added to the ones listed below the `NS_IMPL_CYCLE_COLLECTION_UNLINK` and
+ // `NS_IMPL_CYCLE_COLLECTION_TRAVERSE` macro calls in ExtensionBrowser.cpp,
+ // forgetting it would not result in a build error but it would leak the API
+ // namespace instance (and in debug builds the leak is going to hit an
+ // assertion failure when `WorkerThreadPrimaryRunnable::Run` calls the
+ // assertion `MOZ_ASSERT(!globalScopeAlive)`, after that
+ // `nsCycleCollector_shutdown()` has been called and we don't expect anything
+ // to be keeping the service worker global scope alive).
+ RefPtr<ExtensionAlarms> mExtensionAlarms;
+ RefPtr<ExtensionBrowserSettings> mExtensionBrowserSettings;
+ RefPtr<ExtensionDns> mExtensionDns;
+ RefPtr<ExtensionMockAPI> mExtensionMockAPI;
+ RefPtr<ExtensionProxy> mExtensionProxy;
+ RefPtr<ExtensionRuntime> mExtensionRuntime;
+ RefPtr<ExtensionScripting> mExtensionScripting;
+ RefPtr<ExtensionTest> mExtensionTest;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionBrowser_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.cpp b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.cpp
new file mode 100644
index 0000000000..57a5db3fe8
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.cpp
@@ -0,0 +1,106 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionBrowserSettings.h"
+#include "ExtensionEventManager.h"
+#include "ExtensionSetting.h"
+#include "ExtensionBrowserSettingsColorManagement.h"
+
+#include "mozilla/dom/ExtensionBrowserSettingsBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionBrowserSettings);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionBrowserSettings)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(
+ ExtensionBrowserSettings, mGlobal, mExtensionBrowser,
+ mAllowPopupsForUserEventsSetting, mCacheEnabledSetting,
+ mCloseTabsByDoubleClickSetting, mContextMenuShowEventSetting,
+ mFtpProtocolEnabledSetting, mHomepageOverrideSetting,
+ mImageAnimationBehaviorSetting, mNewTabPageOverrideSetting,
+ mNewTabPositionSetting, mOpenBookmarksInNewTabsSetting,
+ mOpenSearchResultsInNewTabsSetting, mOpenUrlbarResultsInNewTabsSetting,
+ mWebNotificationsDisabledSetting, mOverrideDocumentColorsSetting,
+ mOverrideContentColorSchemeSetting, mUseDocumentFontsSetting,
+ mZoomFullPageSetting, mZoomSiteSpecificSetting, mColorManagementNamespace);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionBrowserSettings)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"allowPopupsForUserEvents"_ns,
+ AllowPopupsForUserEvents)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"cacheEnabled"_ns,
+ CacheEnabled)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"closeTabsByDoubleClick"_ns,
+ CloseTabsByDoubleClick)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"contextMenuShowEvent"_ns,
+ ContextMenuShowEvent)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"ftpProtocolEnabled"_ns,
+ FtpProtocolEnabled)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"homepageOverride"_ns,
+ HomepageOverride)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"imageAnimationBehavior"_ns,
+ ImageAnimationBehavior)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"newTabPageOverride"_ns,
+ NewTabPageOverride)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"newTabPosition"_ns,
+ NewTabPosition)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"openBookmarksInNewTabs"_ns,
+ OpenBookmarksInNewTabs)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings,
+ u"openSearchResultsInNewTabs"_ns,
+ OpenSearchResultsInNewTabs)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings,
+ u"openUrlbarResultsInNewTabs"_ns,
+ OpenUrlbarResultsInNewTabs)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"webNotificationsDisabled"_ns,
+ WebNotificationsDisabled)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"overrideDocumentColors"_ns,
+ OverrideDocumentColors)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings,
+ u"overrideContentColorScheme"_ns,
+ OverrideContentColorScheme)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"useDocumentFonts"_ns,
+ UseDocumentFonts)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"zoomFullPage"_ns,
+ ZoomFullPage)
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettings, u"zoomSiteSpecific"_ns,
+ ZoomSiteSpecific)
+
+ExtensionBrowserSettings::ExtensionBrowserSettings(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionBrowserSettings::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionBrowserSettings::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionBrowserSettings_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionBrowserSettings::GetParentObject() const {
+ return mGlobal;
+}
+
+ExtensionBrowserSettingsColorManagement*
+ExtensionBrowserSettings::GetExtensionBrowserSettingsColorManagement() {
+ if (!mColorManagementNamespace) {
+ mColorManagementNamespace =
+ new ExtensionBrowserSettingsColorManagement(mGlobal, mExtensionBrowser);
+ }
+
+ return mColorManagementNamespace;
+}
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.h b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.h
new file mode 100644
index 0000000000..aed892d493
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettings.h
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionBrowserSettings_h
+#define mozilla_extensions_ExtensionBrowserSettings_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionBrowserSettingsColorManagement;
+class ExtensionEventManager;
+class ExtensionSetting;
+
+class ExtensionBrowserSettings final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionBrowserSettings(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"browserSettings"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionSetting* AllowPopupsForUserEvents();
+ ExtensionSetting* CacheEnabled();
+ ExtensionSetting* CloseTabsByDoubleClick();
+ ExtensionSetting* ContextMenuShowEvent();
+ ExtensionSetting* FtpProtocolEnabled();
+ ExtensionSetting* HomepageOverride();
+ ExtensionSetting* ImageAnimationBehavior();
+ ExtensionSetting* NewTabPageOverride();
+ ExtensionSetting* NewTabPosition();
+ ExtensionSetting* OpenBookmarksInNewTabs();
+ ExtensionSetting* OpenSearchResultsInNewTabs();
+ ExtensionSetting* OpenUrlbarResultsInNewTabs();
+ ExtensionSetting* WebNotificationsDisabled();
+ ExtensionSetting* OverrideDocumentColors();
+ ExtensionSetting* OverrideContentColorScheme();
+ ExtensionSetting* UseDocumentFonts();
+ ExtensionSetting* ZoomFullPage();
+ ExtensionSetting* ZoomSiteSpecific();
+
+ ExtensionBrowserSettingsColorManagement*
+ GetExtensionBrowserSettingsColorManagement();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionBrowserSettings)
+
+ private:
+ ~ExtensionBrowserSettings() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionSetting> mAllowPopupsForUserEventsSetting;
+ RefPtr<ExtensionSetting> mCacheEnabledSetting;
+ RefPtr<ExtensionSetting> mCloseTabsByDoubleClickSetting;
+ RefPtr<ExtensionSetting> mContextMenuShowEventSetting;
+ RefPtr<ExtensionSetting> mFtpProtocolEnabledSetting;
+ RefPtr<ExtensionSetting> mHomepageOverrideSetting;
+ RefPtr<ExtensionSetting> mImageAnimationBehaviorSetting;
+ RefPtr<ExtensionSetting> mNewTabPageOverrideSetting;
+ RefPtr<ExtensionSetting> mNewTabPositionSetting;
+ RefPtr<ExtensionSetting> mOpenBookmarksInNewTabsSetting;
+ RefPtr<ExtensionSetting> mOpenSearchResultsInNewTabsSetting;
+ RefPtr<ExtensionSetting> mOpenUrlbarResultsInNewTabsSetting;
+ RefPtr<ExtensionSetting> mWebNotificationsDisabledSetting;
+ RefPtr<ExtensionSetting> mOverrideDocumentColorsSetting;
+ RefPtr<ExtensionSetting> mOverrideContentColorSchemeSetting;
+ RefPtr<ExtensionSetting> mUseDocumentFontsSetting;
+ RefPtr<ExtensionSetting> mZoomFullPageSetting;
+ RefPtr<ExtensionSetting> mZoomSiteSpecificSetting;
+ RefPtr<ExtensionBrowserSettingsColorManagement> mColorManagementNamespace;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_ExtensionBrowserSettings_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.cpp b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.cpp
new file mode 100644
index 0000000000..40009b350e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.cpp
@@ -0,0 +1,58 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionBrowserSettingsColorManagement.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionBrowserSettingsColorManagementBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionBrowserSettingsColorManagement);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionBrowserSettingsColorManagement)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionBrowserSettingsColorManagement,
+ mGlobal, mExtensionBrowser, mModeSetting,
+ mUseNativeSRGBSetting,
+ mUseWebRenderCompositorSetting);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionBrowserSettingsColorManagement)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettingsColorManagement, u"mode"_ns,
+ Mode);
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettingsColorManagement,
+ u"useNativeSRGB"_ns, UseNativeSRGB);
+NS_IMPL_WEBEXT_SETTING(ExtensionBrowserSettingsColorManagement,
+ u"useWebRenderCompositor"_ns, UseWebRenderCompositor);
+
+ExtensionBrowserSettingsColorManagement::
+ ExtensionBrowserSettingsColorManagement(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionBrowserSettingsColorManagement::IsAllowed(JSContext* aCx,
+ JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionBrowserSettingsColorManagement::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionBrowserSettingsColorManagement_Binding::Wrap(
+ aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionBrowserSettingsColorManagement::GetParentObject()
+ const {
+ return mGlobal;
+}
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.h b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.h
new file mode 100644
index 0000000000..021038cbe7
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionBrowserSettingsColorManagement.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionBrowserSettingsColorManagement_h
+#define mozilla_extensions_ExtensionBrowserSettingsColorManagement_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+#include "ExtensionSetting.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionEventManager;
+class ExtensionSetting;
+
+class ExtensionBrowserSettingsColorManagement final
+ : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionBrowserSettingsColorManagement(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override {
+ return u"browserSettings.colorManagement"_ns;
+ }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionSetting* Mode();
+ ExtensionSetting* UseNativeSRGB();
+ ExtensionSetting* UseWebRenderCompositor();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(
+ ExtensionBrowserSettingsColorManagement)
+
+ private:
+ ~ExtensionBrowserSettingsColorManagement() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionSetting> mModeSetting;
+ RefPtr<ExtensionSetting> mUseNativeSRGBSetting;
+ RefPtr<ExtensionSetting> mUseWebRenderCompositorSetting;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_ExtensionBrowserSettingsColorManagement_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionDns.cpp b/toolkit/components/extensions/webidl-api/ExtensionDns.cpp
new file mode 100644
index 0000000000..b38df15ce2
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionDns.cpp
@@ -0,0 +1,40 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionDns.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionDnsBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionDns);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionDns)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionDns, mGlobal, mExtensionBrowser);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionDns)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+ExtensionDns::ExtensionDns(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionDns::IsAllowed(JSContext* aCx, JSObject* aGlobal) { return true; }
+
+JSObject* ExtensionDns::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionDns_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionDns::GetParentObject() const { return mGlobal; }
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionDns.h b/toolkit/components/extensions/webidl-api/ExtensionDns.h
new file mode 100644
index 0000000000..7359bbf10f
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionDns.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionDns_h
+#define mozilla_extensions_ExtensionDns_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionEventManager;
+
+class ExtensionDns final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionDns(nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"dns"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionDns)
+
+ private:
+ ~ExtensionDns() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_ExtensionDns_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp b/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp
new file mode 100644
index 0000000000..957c7ac8d8
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventListener.cpp
@@ -0,0 +1,690 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionEventListener.h"
+#include "ExtensionAPIRequestForwarder.h"
+#include "ExtensionPort.h"
+
+#include "mozilla/dom/ClonedErrorHolder.h"
+#include "mozilla/dom/FunctionBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsThreadManager.h" // NS_IsMainThread
+
+namespace mozilla {
+namespace extensions {
+
+namespace {
+
+class SendResponseCallback final : public nsISupports {
+ public:
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(SendResponseCallback)
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+
+ static RefPtr<SendResponseCallback> Create(
+ nsIGlobalObject* aGlobalObject, const RefPtr<dom::Promise>& aPromise,
+ JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+
+ RefPtr<SendResponseCallback> responseCallback =
+ new SendResponseCallback(aPromise, aValue);
+
+ auto cleanupCb = [responseCallback]() { responseCallback->Cleanup(); };
+
+ // Create a StrongWorkerRef to the worker thread, the cleanup callback
+ // associated to the StongerWorkerRef will release the reference and resolve
+ // the promise returned to the ExtensionEventListener caller with undefined
+ // if the worker global is being destroyed.
+ auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ workerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<dom::StrongWorkerRef> workerRef = dom::StrongWorkerRef::Create(
+ workerPrivate, "SendResponseCallback", cleanupCb);
+ if (NS_WARN_IF(!workerRef)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ responseCallback->mWorkerRef = workerRef;
+
+ return responseCallback;
+ }
+
+ SendResponseCallback(const RefPtr<dom::Promise>& aPromise,
+ JS::Handle<JS::Value> aValue)
+ : mPromise(aPromise), mValue(aValue) {
+ MOZ_ASSERT(mPromise);
+ mozilla::HoldJSObjects(this);
+
+ // Create a promise monitor that invalidates the sendResponse
+ // callback if the promise has been already resolved or rejected.
+ mPromiseListener = new dom::DomPromiseListener(
+ [self = RefPtr{this}](JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ self->Cleanup();
+ },
+ [self = RefPtr{this}](nsresult aError) { self->Cleanup(); });
+ mPromise->AppendNativeHandler(mPromiseListener);
+ }
+
+ void Cleanup(bool aIsDestroying = false) {
+ // Return earlier if the instance was already been cleaned up.
+ if (!mPromiseListener) {
+ return;
+ }
+
+ NS_WARNING("SendResponseCallback::Cleanup");
+ // Clear the promise listener's resolvers to release the
+ // RefPtr captured by the ones initially set.
+ mPromiseListener->Clear();
+ mPromiseListener = nullptr;
+
+ if (mPromise) {
+ mPromise->MaybeResolveWithUndefined();
+ }
+ mPromise = nullptr;
+
+ // Skipped if called from the destructor.
+ if (!aIsDestroying && mValue.isObject()) {
+ // Release the reference to the SendResponseCallback.
+ js::SetFunctionNativeReserved(&mValue.toObject(),
+ SLOT_SEND_RESPONSE_CALLBACK_INSTANCE,
+ JS::PrivateValue(nullptr));
+ }
+
+ if (mWorkerRef) {
+ mWorkerRef = nullptr;
+ }
+ }
+
+ static bool Call(JSContext* aCx, unsigned aArgc, JS::Value* aVp) {
+ JS::CallArgs args = CallArgsFromVp(aArgc, aVp);
+ JS::Rooted<JSObject*> callee(aCx, &args.callee());
+
+ JS::Value v = js::GetFunctionNativeReserved(
+ callee, SLOT_SEND_RESPONSE_CALLBACK_INSTANCE);
+
+ SendResponseCallback* sendResponse =
+ reinterpret_cast<SendResponseCallback*>(v.toPrivate());
+ if (!sendResponse || !sendResponse->mPromise ||
+ !sendResponse->mPromise->PromiseObj()) {
+ NS_WARNING("SendResponseCallback called after being invalidated");
+ return true;
+ }
+
+ sendResponse->mPromise->MaybeResolve(args.get(0));
+ sendResponse->Cleanup();
+
+ return true;
+ }
+
+ private:
+ ~SendResponseCallback() {
+ mozilla::DropJSObjects(this);
+ this->Cleanup(true);
+ };
+
+ RefPtr<dom::Promise> mPromise;
+ RefPtr<dom::DomPromiseListener> mPromiseListener;
+ JS::Heap<JS::Value> mValue;
+ RefPtr<dom::StrongWorkerRef> mWorkerRef;
+};
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SendResponseCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(SendResponseCallback)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SendResponseCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SendResponseCallback)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SendResponseCallback)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromise)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(SendResponseCallback)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mValue)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SendResponseCallback)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseListener);
+ tmp->mValue.setUndefined();
+ // Resolve the promise with undefined (as "unhandled") before unlinking it.
+ if (tmp->mPromise) {
+ tmp->mPromise->MaybeResolveWithUndefined();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromise);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+} // anonymous namespace
+
+// ExtensionEventListener
+
+NS_IMPL_ISUPPORTS(ExtensionEventListener, mozIExtensionEventListener)
+
+// static
+already_AddRefed<ExtensionEventListener> ExtensionEventListener::Create(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ dom::Function* aCallback, CleanupCallback&& aCleanupCallback,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(dom::IsCurrentThreadRunningWorker());
+ RefPtr<ExtensionEventListener> extCb =
+ new ExtensionEventListener(aGlobal, aExtensionBrowser, aCallback);
+
+ auto* workerPrivate = dom::GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+ workerPrivate->AssertIsOnWorkerThread();
+ RefPtr<dom::StrongWorkerRef> workerRef = dom::StrongWorkerRef::Create(
+ workerPrivate, "ExtensionEventListener", std::move(aCleanupCallback));
+ if (!workerRef) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ extCb->mWorkerRef = new dom::ThreadSafeWorkerRef(workerRef);
+
+ return extCb.forget();
+}
+
+// static
+UniquePtr<dom::StructuredCloneHolder>
+ExtensionEventListener::SerializeCallArguments(const nsTArray<JS::Value>& aArgs,
+ JSContext* aCx,
+ ErrorResult& aRv) {
+ JS::Rooted<JS::Value> jsval(aCx);
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, aArgs, &jsval))) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ UniquePtr<dom::StructuredCloneHolder> argsHolder =
+ MakeUnique<dom::StructuredCloneHolder>(
+ dom::StructuredCloneHolder::CloningSupported,
+ dom::StructuredCloneHolder::TransferringNotSupported,
+ JS::StructuredCloneScope::SameProcess);
+
+ argsHolder->Write(aCx, jsval, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return argsHolder;
+}
+
+NS_IMETHODIMP ExtensionEventListener::CallListener(
+ const nsTArray<JS::Value>& aArgs, ListenerCallOptions* aCallOptions,
+ JSContext* aCx, dom::Promise** aPromiseResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aPromiseResult);
+
+ // Process and validate call options.
+ APIObjectType apiObjectType = APIObjectType::NONE;
+ JS::Rooted<JS::Value> apiObjectDescriptor(aCx);
+ if (aCallOptions) {
+ aCallOptions->GetApiObjectType(&apiObjectType);
+ aCallOptions->GetApiObjectDescriptor(&apiObjectDescriptor);
+
+ // Explicitly check that the APIObjectType is one of expected ones,
+ // raise to the caller an explicit error if it is not.
+ //
+ // This is using a switch to also get a warning if a new value is added to
+ // the APIObjectType enum and it is not yet handled.
+ switch (apiObjectType) {
+ case APIObjectType::NONE:
+ if (NS_WARN_IF(!apiObjectDescriptor.isNullOrUndefined())) {
+ JS_ReportErrorASCII(
+ aCx,
+ "Unexpected non-null apiObjectDescriptor on apiObjectType=NONE");
+ return NS_ERROR_UNEXPECTED;
+ }
+ break;
+ case APIObjectType::RUNTIME_PORT:
+ if (NS_WARN_IF(apiObjectDescriptor.isNullOrUndefined())) {
+ JS_ReportErrorASCII(aCx,
+ "Unexpected null apiObjectDescriptor on "
+ "apiObjectType=RUNTIME_PORT");
+ return NS_ERROR_UNEXPECTED;
+ }
+ break;
+ default:
+ MOZ_CRASH("Unexpected APIObjectType");
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ // Create promise to be returned.
+ IgnoredErrorResult rv;
+ RefPtr<dom::Promise> retPromise;
+
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+ retPromise = dom::Promise::Create(global, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // Convert args into a non-const sequence.
+ dom::Sequence<JS::Value> args;
+ if (!args.AppendElements(aArgs, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Execute the listener call.
+
+ MutexAutoLock lock(mMutex);
+
+ if (NS_WARN_IF(!mWorkerRef)) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (apiObjectType != APIObjectType::NONE) {
+ bool prependArgument = false;
+ aCallOptions->GetApiObjectPrepended(&prependArgument);
+ // Prepend or append the apiObjectDescriptor data to the call arguments,
+ // the worker runnable will convert that into an API object
+ // instance on the worker thread.
+ if (prependArgument) {
+ if (!args.InsertElementAt(0, std::move(apiObjectDescriptor), fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ } else {
+ if (!args.AppendElement(std::move(apiObjectDescriptor), fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ UniquePtr<dom::StructuredCloneHolder> argsHolder =
+ SerializeCallArguments(args, aCx, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ RefPtr<ExtensionListenerCallWorkerRunnable> runnable =
+ new ExtensionListenerCallWorkerRunnable(this, std::move(argsHolder),
+ aCallOptions, retPromise);
+ runnable->Dispatch();
+ retPromise.forget(aPromiseResult);
+
+ return NS_OK;
+}
+
+dom::WorkerPrivate* ExtensionEventListener::GetWorkerPrivate() const {
+ MOZ_ASSERT(mWorkerRef);
+ return mWorkerRef->Private();
+}
+
+// ExtensionListenerCallWorkerRunnable
+
+void ExtensionListenerCallWorkerRunnable::DeserializeCallArguments(
+ JSContext* aCx, dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ JS::Rooted<JS::Value> jsvalue(aCx);
+
+ mArgsHolder->Read(xpc::CurrentNativeGlobal(aCx), aCx, &jsvalue, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ nsresult rv2 =
+ ExtensionAPIRequestForwarder::JSArrayToSequence(aCx, jsvalue, aArgs);
+ if (NS_FAILED(rv2)) {
+ aRv.Throw(rv2);
+ }
+}
+
+bool ExtensionListenerCallWorkerRunnable::WorkerRun(
+ JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate);
+ auto global = mListener->GetGlobalObject();
+ if (NS_WARN_IF(!global)) {
+ return true;
+ }
+
+ RefPtr<ExtensionBrowser> extensionBrowser = mListener->GetExtensionBrowser();
+ if (NS_WARN_IF(!extensionBrowser)) {
+ return true;
+ }
+
+ auto fn = mListener->GetCallback();
+ if (NS_WARN_IF(!fn)) {
+ return true;
+ }
+
+ IgnoredErrorResult rv;
+ dom::Sequence<JS::Value> argsSequence;
+ dom::SequenceRooter<JS::Value> arguments(aCx, &argsSequence);
+
+ DeserializeCallArguments(aCx, argsSequence, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return true;
+ }
+
+ RefPtr<dom::Promise> retPromise;
+ RefPtr<dom::StrongWorkerRef> workerRef;
+
+ retPromise = dom::Promise::Create(global, rv);
+ if (retPromise) {
+ workerRef = dom::StrongWorkerRef::Create(
+ aWorkerPrivate, "ExtensionListenerCallWorkerRunnable", []() {});
+ }
+
+ if (NS_WARN_IF(rv.Failed() || !workerRef)) {
+ auto rejectMainThreadPromise =
+ [error = rv.Failed() ? rv.StealNSResult() : NS_ERROR_UNEXPECTED,
+ promiseResult = std::move(mPromiseResult)]() {
+ // TODO(rpl): this seems to be currently rejecting an error object
+ // without a stack trace, its a corner case but we may look into
+ // improve this error.
+ promiseResult->MaybeReject(error);
+ };
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction(__func__, std::move(rejectMainThreadPromise));
+ NS_DispatchToMainThread(runnable);
+ JS_ClearPendingException(aCx);
+ return true;
+ }
+
+ ExtensionListenerCallPromiseResultHandler::Create(
+ retPromise, this, new dom::ThreadSafeWorkerRef(workerRef));
+
+ // Translate the first parameter into the API object type (e.g. an
+ // ExtensionPort), the content of the original argument value is expected to
+ // be a dictionary that is valid as an internal descriptor for that API object
+ // type.
+ if (mAPIObjectType != APIObjectType::NONE) {
+ IgnoredErrorResult rv;
+
+ // The api object descriptor is expected to have been prepended to the
+ // other arguments, assert here that the argsSequence does contain at least
+ // one element.
+ MOZ_ASSERT(!argsSequence.IsEmpty());
+
+ uint32_t apiObjectIdx = mAPIObjectPrepended ? 0 : argsSequence.Length() - 1;
+ JS::Rooted<JS::Value> apiObjectDescriptor(
+ aCx, argsSequence.ElementAt(apiObjectIdx));
+ JS::Rooted<JS::Value> apiObjectValue(aCx);
+
+ // We only expect the object type to be RUNTIME_PORT at the moment,
+ // until we will need to expect it to support other object types that
+ // some specific API may need.
+ MOZ_ASSERT(mAPIObjectType == APIObjectType::RUNTIME_PORT);
+ RefPtr<ExtensionPort> port =
+ extensionBrowser->GetPort(apiObjectDescriptor, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ retPromise->MaybeReject(rv.StealNSResult());
+ return true;
+ }
+
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, port, &apiObjectValue))) {
+ retPromise->MaybeReject(NS_ERROR_UNEXPECTED);
+ return true;
+ }
+
+ argsSequence.ReplaceElementAt(apiObjectIdx, apiObjectValue);
+ }
+
+ // Create callback argument and append it to the call arguments.
+ JS::Rooted<JSObject*> sendResponseObj(aCx);
+
+ switch (mCallbackArgType) {
+ case CallbackType::CALLBACK_NONE:
+ break;
+ case CallbackType::CALLBACK_SEND_RESPONSE: {
+ JS::Rooted<JSFunction*> sendResponseFn(
+ aCx, js::NewFunctionWithReserved(aCx, SendResponseCallback::Call,
+ /* nargs */ 1, 0, "sendResponse"));
+ sendResponseObj = JS_GetFunctionObject(sendResponseFn);
+ JS::Rooted<JS::Value> sendResponseValue(
+ aCx, JS::ObjectValue(*sendResponseObj));
+
+ // Create a SendResponseCallback instance that keeps a reference
+ // to the promise to resolve when the static SendReponseCallback::Call
+ // is being called.
+ // the SendReponseCallback instance from the resolved slot to resolve
+ // the promise and invalidated the sendResponse callback (any new call
+ // becomes a noop).
+ RefPtr<SendResponseCallback> sendResponsePtr =
+ SendResponseCallback::Create(global, retPromise, sendResponseValue,
+ rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ retPromise->MaybeReject(NS_ERROR_UNEXPECTED);
+ return true;
+ }
+
+ // Store the SendResponseCallback instance in a private value set on the
+ // function object reserved slot, where ehe SendResponseCallback::Call
+ // static function will get it back to resolve the related promise
+ // and then invalidate the sendResponse callback (any new call
+ // becomes a noop).
+ js::SetFunctionNativeReserved(sendResponseObj,
+ SLOT_SEND_RESPONSE_CALLBACK_INSTANCE,
+ JS::PrivateValue(sendResponsePtr));
+
+ if (NS_WARN_IF(
+ !argsSequence.AppendElement(sendResponseValue, fallible))) {
+ retPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return true;
+ }
+
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected callbackType");
+ break;
+ }
+
+ // TODO: should `nsAutoMicroTask mt;` be used here?
+ dom::AutoEntryScript aes(global, "WebExtensionAPIEvent");
+ JS::Rooted<JS::Value> retval(aCx);
+ ErrorResult erv;
+ erv.MightThrowJSException();
+ MOZ_KnownLive(fn)->Call(argsSequence, &retval, erv, "WebExtensionAPIEvent",
+ dom::Function::eRethrowExceptions);
+
+ // Calling the callback may have thrown an exception.
+ // TODO: add a ListenerCallOptions to optionally report the exception
+ // instead of forwarding it to the caller.
+ erv.WouldReportJSException();
+
+ if (erv.Failed()) {
+ if (erv.IsUncatchableException()) {
+ // TODO: include some more info? (e.g. api path).
+ retPromise->MaybeRejectWithTimeoutError(
+ "WebExtensions API Event listener threw uncatchable exception");
+ return true;
+ }
+
+ retPromise->MaybeReject(std::move(erv));
+ return true;
+ }
+
+ // Custom return value handling logic for events that do pass a
+ // sendResponse callback parameter (see expected behavior
+ // for the runtime.onMessage sendResponse parameter on MDN:
+ // https://mzl.la/3dokpMi):
+ //
+ // - listener returns Boolean true => the extension listener is
+ // expected to call sendResponse callback parameter asynchronosuly
+ // - listener return a Promise object => the promise is the listener
+ // response
+ // - listener return any other value => the listener didn't handle the
+ // event and the return value is ignored
+ //
+ if (mCallbackArgType == CallbackType::CALLBACK_SEND_RESPONSE) {
+ if (retval.isBoolean() && retval.isTrue()) {
+ // The listener returned `true` and so the promise relate to the
+ // listener call will be resolved once the extension will call
+ // the sendResponce function passed as a callback argument.
+ return true;
+ }
+
+ // If the retval isn't true and it is not a Promise object,
+ // the listener isn't handling the event, and we resolve the
+ // promise with undefined (if the listener didn't reply already
+ // by calling sendResponse synchronsouly).
+ // undefined (
+ if (!ExtensionEventListener::IsPromise(aCx, retval)) {
+ // Mark this listener call as cancelled,
+ // ExtensionListenerCallPromiseResult will check to know that it should
+ // release the main thread promise without resolving it.
+ //
+ // TODO: double-check if we should also cancel rejecting the promise
+ // returned by mozIExtensionEventListener.callListener when the listener
+ // call throws (by comparing it with the behavior on the current
+ // privileged-based API implementation).
+ mIsCallResultCancelled = true;
+ retPromise->MaybeResolveWithUndefined();
+
+ // Invalidate the sendResponse function by setting the private
+ // value where the SendResponseCallback instance was stored
+ // to a nullptr.
+ js::SetFunctionNativeReserved(sendResponseObj,
+ SLOT_SEND_RESPONSE_CALLBACK_INSTANCE,
+ JS::PrivateValue(nullptr));
+
+ return true;
+ }
+ }
+
+ retPromise->MaybeResolve(retval);
+
+ return true;
+}
+
+// ExtensionListenerCallPromiseResultHandler
+
+NS_IMPL_ISUPPORTS0(ExtensionListenerCallPromiseResultHandler)
+
+// static
+void ExtensionListenerCallPromiseResultHandler::Create(
+ const RefPtr<dom::Promise>& aPromise,
+ const RefPtr<ExtensionListenerCallWorkerRunnable>& aWorkerRunnable,
+ dom::ThreadSafeWorkerRef* aWorkerRef) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(aWorkerRef);
+ MOZ_ASSERT(aWorkerRef->Private()->IsOnCurrentThread());
+
+ RefPtr<ExtensionListenerCallPromiseResultHandler> handler =
+ new ExtensionListenerCallPromiseResultHandler(aWorkerRef,
+ aWorkerRunnable);
+ aPromise->AppendNativeHandler(handler);
+}
+
+void ExtensionListenerCallPromiseResultHandler::WorkerRunCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue,
+ PromiseCallbackType aCallbackType) {
+ MOZ_ASSERT(mWorkerRef);
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+
+ // The listener call was cancelled (e.g. when a runtime.onMessage listener
+ // returned false), release resources associated with this promise handler
+ // on the main thread without resolving the promise associated to the
+ // extension event listener call.
+ if (mWorkerRunnable->IsCallResultCancelled()) {
+ auto releaseMainThreadPromise = [runnable = std::move(mWorkerRunnable),
+ workerRef = std::move(mWorkerRef)]() {};
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction(__func__, std::move(releaseMainThreadPromise));
+ NS_DispatchToMainThread(runnable);
+ return;
+ }
+
+ JS::Rooted<JS::Value> retval(aCx, aValue);
+
+ if (retval.isObject()) {
+ // Try to serialize the result as an ClonedErrorHolder,
+ // in case the value is an Error object.
+ IgnoredErrorResult rv;
+ JS::Rooted<JSObject*> errObj(aCx, &retval.toObject());
+ RefPtr<dom::ClonedErrorHolder> ceh =
+ dom::ClonedErrorHolder::Create(aCx, errObj, rv);
+ if (!rv.Failed() && ceh) {
+ JS::Rooted<JSObject*> obj(aCx);
+ // Note: `ToJSValue` cannot be used because ClonedErrorHolder isn't
+ // wrapped cached.
+ Unused << NS_WARN_IF(!ceh->WrapObject(aCx, nullptr, &obj));
+ retval.setObject(*obj);
+ }
+ }
+
+ UniquePtr<dom::StructuredCloneHolder> resHolder =
+ MakeUnique<dom::StructuredCloneHolder>(
+ dom::StructuredCloneHolder::CloningSupported,
+ dom::StructuredCloneHolder::TransferringNotSupported,
+ JS::StructuredCloneScope::SameProcess);
+
+ IgnoredErrorResult erv;
+ resHolder->Write(aCx, retval, erv);
+
+ // Failed to serialize the result, dispatch a runnable to reject
+ // the promise returned to the caller of the mozIExtensionCallback
+ // callWithPromiseResult method.
+ if (NS_WARN_IF(erv.Failed())) {
+ auto rejectMainThreadPromise = [error = erv.StealNSResult(),
+ runnable = std::move(mWorkerRunnable),
+ resHolder = std::move(resHolder)]() {
+ RefPtr<dom::Promise> promiseResult = std::move(runnable->mPromiseResult);
+ promiseResult->MaybeReject(error);
+ };
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction(__func__, std::move(rejectMainThreadPromise));
+ NS_DispatchToMainThread(runnable);
+ JS_ClearPendingException(aCx);
+ return;
+ }
+
+ auto resolveMainThreadPromise = [callbackType = aCallbackType,
+ resHolder = std::move(resHolder),
+ runnable = std::move(mWorkerRunnable),
+ workerRef = std::move(mWorkerRef)]() {
+ RefPtr<dom::Promise> promiseResult = std::move(runnable->mPromiseResult);
+
+ auto* global = promiseResult->GetGlobalObject();
+ dom::AutoEntryScript aes(global,
+ "ExtensionListenerCallWorkerRunnable::WorkerRun");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JS::Value> jsvalue(cx);
+ IgnoredErrorResult rv;
+
+ resHolder->Read(global, cx, &jsvalue, rv);
+
+ if (NS_WARN_IF(rv.Failed())) {
+ promiseResult->MaybeReject(rv.StealNSResult());
+ JS_ClearPendingException(cx);
+ } else {
+ switch (callbackType) {
+ case PromiseCallbackType::Resolve:
+ promiseResult->MaybeResolve(jsvalue);
+ break;
+ case PromiseCallbackType::Reject:
+ promiseResult->MaybeReject(jsvalue);
+ break;
+ }
+ }
+ };
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction(__func__, std::move(resolveMainThreadPromise));
+ NS_DispatchToMainThread(runnable);
+}
+
+void ExtensionListenerCallPromiseResultHandler::ResolvedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ WorkerRunCallback(aCx, aValue, PromiseCallbackType::Resolve);
+}
+
+void ExtensionListenerCallPromiseResultHandler::RejectedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ WorkerRunCallback(aCx, aValue, PromiseCallbackType::Reject);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventListener.h b/toolkit/components/extensions/webidl-api/ExtensionEventListener.h
new file mode 100644
index 0000000000..1259f7466e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventListener.h
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionEventListener_h
+#define mozilla_extensions_ExtensionEventListener_h
+
+#include "js/Promise.h" // JS::IsPromiseObject
+#include "mozIExtensionAPIRequestHandling.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerPrivate.h"
+
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace dom {
+class Function;
+} // namespace dom
+
+namespace extensions {
+
+#define SLOT_SEND_RESPONSE_CALLBACK_INSTANCE 0
+
+// A class that represents a callback parameter passed to WebExtensions API
+// addListener / removeListener methods.
+//
+// Instances of this class are sent to the mozIExtensionAPIRequestHandler as
+// a property of the mozIExtensionAPIRequest.
+//
+// The mozIExtensionEventListener xpcom interface provides methods that allow
+// the mozIExtensionAPIRequestHandler running in the Main Thread to call the
+// underlying callback Function on its owning thread.
+class ExtensionEventListener final : public mozIExtensionEventListener {
+ public:
+ NS_DECL_MOZIEXTENSIONEVENTLISTENER
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ using CleanupCallback = std::function<void()>;
+ using ListenerCallOptions = mozIExtensionListenerCallOptions;
+ using APIObjectType = ListenerCallOptions::APIObjectType;
+ using CallbackType = ListenerCallOptions::CallbackType;
+
+ static already_AddRefed<ExtensionEventListener> Create(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ dom::Function* aCallback, CleanupCallback&& aCleanupCallback,
+ ErrorResult& aRv);
+
+ static bool IsPromise(JSContext* aCx, JS::Handle<JS::Value> aValue) {
+ if (!aValue.isObject()) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aValue.toObject());
+ return JS::IsPromiseObject(obj);
+ }
+
+ dom::WorkerPrivate* GetWorkerPrivate() const;
+
+ RefPtr<dom::Function> GetCallback() const { return mCallback; }
+
+ nsCOMPtr<nsIGlobalObject> GetGlobalObject() const {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryReferent(mGlobal);
+ return global;
+ }
+
+ ExtensionBrowser* GetExtensionBrowser() const { return mExtensionBrowser; }
+
+ void Cleanup() {
+ if (mWorkerRef) {
+ MutexAutoLock lock(mMutex);
+
+ mWorkerRef->Private()->AssertIsOnWorkerThread();
+ mWorkerRef = nullptr;
+ }
+
+ mGlobal = nullptr;
+ mCallback = nullptr;
+ mExtensionBrowser = nullptr;
+ }
+
+ private:
+ ExtensionEventListener(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser,
+ dom::Function* aCallback)
+ : mGlobal(do_GetWeakReference(aGlobal)),
+ mExtensionBrowser(aExtensionBrowser),
+ mCallback(aCallback),
+ mMutex("ExtensionEventListener::mMutex") {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aExtensionBrowser);
+ MOZ_ASSERT(aCallback);
+ };
+
+ static UniquePtr<dom::StructuredCloneHolder> SerializeCallArguments(
+ const nsTArray<JS::Value>& aArgs, JSContext* aCx, ErrorResult& aRv);
+
+ ~ExtensionEventListener() { Cleanup(); };
+
+ // Accessed on the main and on the owning threads.
+ RefPtr<dom::ThreadSafeWorkerRef> mWorkerRef;
+
+ // Accessed only on the owning thread.
+ nsWeakPtr mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<dom::Function> mCallback;
+
+ // Used to make sure we are not going to release the
+ // instance on the worker thread, while we are in the
+ // process of forwarding a call from the main thread.
+ Mutex mMutex MOZ_UNANNOTATED;
+};
+
+// A WorkerRunnable subclass used to call an ExtensionEventListener
+// in the thread that owns the dom::Function wrapped by the
+// ExtensionEventListener class.
+class ExtensionListenerCallWorkerRunnable : public dom::WorkerRunnable {
+ friend class ExtensionListenerCallPromiseResultHandler;
+
+ public:
+ using ListenerCallOptions = mozIExtensionListenerCallOptions;
+ using APIObjectType = ListenerCallOptions::APIObjectType;
+ using CallbackType = ListenerCallOptions::CallbackType;
+
+ ExtensionListenerCallWorkerRunnable(
+ const RefPtr<ExtensionEventListener>& aExtensionEventListener,
+ UniquePtr<dom::StructuredCloneHolder> aArgsHolder,
+ ListenerCallOptions* aCallOptions,
+ RefPtr<dom::Promise> aPromiseRetval = nullptr)
+ : WorkerRunnable(aExtensionEventListener->GetWorkerPrivate(),
+ WorkerThreadUnchangedBusyCount),
+ mListener(aExtensionEventListener),
+ mArgsHolder(std::move(aArgsHolder)),
+ mPromiseResult(std::move(aPromiseRetval)),
+ mAPIObjectType(APIObjectType::NONE),
+ mCallbackArgType(CallbackType::CALLBACK_NONE) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aExtensionEventListener);
+
+ if (aCallOptions) {
+ aCallOptions->GetApiObjectType(&mAPIObjectType);
+ aCallOptions->GetApiObjectPrepended(&mAPIObjectPrepended);
+ aCallOptions->GetCallbackType(&mCallbackArgType);
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ bool WorkerRun(JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate) override;
+
+ bool IsCallResultCancelled() { return mIsCallResultCancelled; }
+
+ private:
+ ~ExtensionListenerCallWorkerRunnable() {
+ NS_ReleaseOnMainThread("~ExtensionListenerCallWorkerRunnable",
+ mPromiseResult.forget());
+ ReleaseArgsHolder();
+ mListener = nullptr;
+ }
+
+ void ReleaseArgsHolder() {
+ if (NS_IsMainThread()) {
+ mArgsHolder = nullptr;
+ } else {
+ auto releaseArgsHolder = [argsHolder = std::move(mArgsHolder)]() {};
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction(__func__, std::move(releaseArgsHolder));
+ NS_DispatchToMainThread(runnable);
+ }
+ }
+
+ void DeserializeCallArguments(JSContext* aCx, dom::Sequence<JS::Value>& aArg,
+ ErrorResult& aRv);
+
+ RefPtr<ExtensionEventListener> mListener;
+ UniquePtr<dom::StructuredCloneHolder> mArgsHolder;
+ RefPtr<dom::Promise> mPromiseResult;
+ bool mIsCallResultCancelled = false;
+ // Call Options.
+ bool mAPIObjectPrepended;
+ APIObjectType mAPIObjectType;
+ CallbackType mCallbackArgType;
+};
+
+// A class attached to the promise that should be resolved once the extension
+// event listener call has been handled, responsible for serializing resolved
+// values or rejected errors on the listener's owning thread and sending them to
+// the extension event listener caller running on the main thread.
+class ExtensionListenerCallPromiseResultHandler
+ : public dom::PromiseNativeHandler {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ static void Create(
+ const RefPtr<dom::Promise>& aPromise,
+ const RefPtr<ExtensionListenerCallWorkerRunnable>& aWorkerRunnable,
+ dom::ThreadSafeWorkerRef* aWorkerRef);
+
+ // PromiseNativeHandler
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ enum class PromiseCallbackType { Resolve, Reject };
+
+ private:
+ ExtensionListenerCallPromiseResultHandler(
+ dom::ThreadSafeWorkerRef* aWorkerRef,
+ RefPtr<ExtensionListenerCallWorkerRunnable> aWorkerRunnable)
+ : mWorkerRef(aWorkerRef), mWorkerRunnable(std::move(aWorkerRunnable)) {}
+
+ ~ExtensionListenerCallPromiseResultHandler() = default;
+
+ void WorkerRunCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ PromiseCallbackType aCallbackType);
+
+ // Set and accessed only on the owning worker thread.
+ RefPtr<dom::ThreadSafeWorkerRef> mWorkerRef;
+
+ // Reference to the runnable created on and owned by the main thread,
+ // accessed on the worker thread and released on the owning thread.
+ RefPtr<ExtensionListenerCallWorkerRunnable> mWorkerRunnable;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionEventListener_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventManager.cpp b/toolkit/components/extensions/webidl-api/ExtensionEventManager.cpp
new file mode 100644
index 0000000000..2a5f9e0978
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventManager.cpp
@@ -0,0 +1,168 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionEventManager.h"
+
+#include "ExtensionAPIAddRemoveListener.h"
+
+#include "mozilla/dom/ExtensionEventManagerBinding.h"
+#include "nsIGlobalObject.h"
+#include "ExtensionEventListener.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(ExtensionEventManager);
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionEventManager);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionEventManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ExtensionEventManager)
+ tmp->mListeners.clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionBrowser)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ExtensionEventManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionBrowser)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ExtensionEventManager)
+ for (auto iter = tmp->mListeners.iter(); !iter.done(); iter.next()) {
+ aCallbacks.Trace(&iter.get().mutableKey(), "mListeners key", aClosure);
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionEventManager)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+ExtensionEventManager::ExtensionEventManager(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ const nsAString& aNamespace, const nsAString& aEventName,
+ const nsAString& aObjectType, const nsAString& aObjectId)
+ : mGlobal(aGlobal),
+ mExtensionBrowser(aExtensionBrowser),
+ mAPINamespace(aNamespace),
+ mEventName(aEventName),
+ mAPIObjectType(aObjectType),
+ mAPIObjectId(aObjectId) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+
+ RefPtr<ExtensionEventManager> self = this;
+ mozilla::HoldJSObjects(this);
+}
+
+ExtensionEventManager::~ExtensionEventManager() {
+ ReleaseListeners();
+ mozilla::DropJSObjects(this);
+};
+
+void ExtensionEventManager::ReleaseListeners() {
+ if (mListeners.empty()) {
+ return;
+ }
+
+ for (auto iter = mListeners.iter(); !iter.done(); iter.next()) {
+ iter.get().value()->Cleanup();
+ }
+
+ mListeners.clear();
+}
+
+JSObject* ExtensionEventManager::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionEventManager_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionEventManager::GetParentObject() const {
+ return mGlobal;
+}
+
+void ExtensionEventManager::AddListener(
+ JSContext* aCx, dom::Function& aCallback,
+ const dom::Optional<JS::Handle<JSObject*>>& aOptions, ErrorResult& aRv) {
+ JS::Rooted<JSObject*> cb(aCx, aCallback.CallbackOrNull());
+ if (cb == nullptr) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ RefPtr<ExtensionEventManager> self = this;
+
+ IgnoredErrorResult rv;
+ RefPtr<ExtensionEventListener> wrappedCb = ExtensionEventListener::Create(
+ mGlobal, mExtensionBrowser, &aCallback,
+ [self = std::move(self)]() { self->ReleaseListeners(); }, rv);
+
+ if (NS_WARN_IF(rv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ RefPtr<ExtensionEventListener> storedWrapper = wrappedCb;
+ if (!mListeners.put(cb, std::move(storedWrapper))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ auto request = SendAddListener(mEventName);
+ request->Run(mGlobal, aCx, {}, wrappedCb, aRv);
+
+ if (!aRv.Failed() && mAPIObjectType.IsEmpty()) {
+ mExtensionBrowser->TrackWakeupEventListener(aCx, mAPINamespace, mEventName);
+ }
+}
+
+void ExtensionEventManager::RemoveListener(dom::Function& aCallback,
+ ErrorResult& aRv) {
+ dom::AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(mGlobal))) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JSObject*> cb(cx, aCallback.CallbackOrNull());
+ const auto& ptr = mListeners.lookup(cb);
+
+ // Return earlier if the listener wasn't found
+ if (!ptr) {
+ return;
+ }
+
+ RefPtr<ExtensionEventListener> wrappedCb = ptr->value();
+ auto request = SendRemoveListener(mEventName);
+ request->Run(mGlobal, cx, {}, wrappedCb, aRv);
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ if (mAPIObjectType.IsEmpty()) {
+ mExtensionBrowser->UntrackWakeupEventListener(cx, mAPINamespace,
+ mEventName);
+ }
+
+ mListeners.remove(cb);
+
+ wrappedCb->Cleanup();
+}
+
+bool ExtensionEventManager::HasListener(dom::Function& aCallback,
+ ErrorResult& aRv) const {
+ return mListeners.has(aCallback.CallbackOrNull());
+}
+
+bool ExtensionEventManager::HasListeners(ErrorResult& aRv) const {
+ return !mListeners.empty();
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionEventManager.h b/toolkit/components/extensions/webidl-api/ExtensionEventManager.h
new file mode 100644
index 0000000000..eff3b3e045
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionEventManager.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionEventManager_h
+#define mozilla_extensions_ExtensionEventManager_h
+
+#include "js/GCHashTable.h" // for JS::GCHashMap
+#include "js/TypeDecls.h" // for JS::Handle, JSContext, JSObject, ...
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/ErrorResult.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsPointerHashKeys.h"
+#include "nsRefPtrHashtable.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace dom {
+class Function;
+} // namespace dom
+
+namespace extensions {
+
+class ExtensionBrowser;
+class ExtensionEventListener;
+
+class ExtensionEventManager final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPIBase {
+ public:
+ ExtensionEventManager(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser,
+ const nsAString& aNamespace,
+ const nsAString& aEventName,
+ const nsAString& aObjectType = VoidString(),
+ const nsAString& aObjectId = VoidString());
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsIGlobalObject* GetParentObject() const;
+
+ bool HasListener(dom::Function& aCallback, ErrorResult& aRv) const;
+ bool HasListeners(ErrorResult& aRv) const;
+
+ void AddListener(JSContext* aCx, dom::Function& aCallback,
+ const dom::Optional<JS::Handle<JSObject*>>& aOptions,
+ ErrorResult& aRv);
+ void RemoveListener(dom::Function& aCallback, ErrorResult& aRv);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ExtensionEventManager)
+
+ protected:
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return mAPINamespace; }
+
+ nsString GetAPIObjectType() const override { return mAPIObjectType; }
+
+ nsString GetAPIObjectId() const override { return mAPIObjectId; }
+
+ private:
+ using ListenerWrappersMap =
+ JS::GCHashMap<JS::Heap<JSObject*>, RefPtr<ExtensionEventListener>,
+ js::StableCellHasher<JS::Heap<JSObject*>>,
+ js::SystemAllocPolicy>;
+
+ ~ExtensionEventManager();
+
+ void ReleaseListeners();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ nsString mAPINamespace;
+ nsString mEventName;
+ nsString mAPIObjectType;
+ nsString mAPIObjectId;
+ ListenerWrappersMap mListeners;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionEventManager_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionMockAPI.cpp b/toolkit/components/extensions/webidl-api/ExtensionMockAPI.cpp
new file mode 100644
index 0000000000..c89ca3a11f
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionMockAPI.cpp
@@ -0,0 +1,59 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionMockAPI.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionMockAPIBinding.h"
+#include "mozilla/extensions/ExtensionPort.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionMockAPI);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionMockAPI)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionMockAPI, mGlobal,
+ mExtensionBrowser, mOnTestEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionMockAPI)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR_WITH_DATAMEMBER(ExtensionMockAPI, u"onTestEvent"_ns,
+ OnTestEvent, mOnTestEventMgr)
+
+ExtensionMockAPI::ExtensionMockAPI(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionMockAPI::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionMockAPI::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionMockAPI_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionMockAPI::GetParentObject() const { return mGlobal; }
+
+void ExtensionMockAPI::GetPropertyAsErrorObject(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ ExtensionAPIBase::GetWebExtPropertyAsJSValue(aCx, u"propertyAsErrorObject"_ns,
+ aRetval);
+}
+
+void ExtensionMockAPI::GetPropertyAsString(DOMString& aRetval) {
+ GetWebExtPropertyAsString(u"getPropertyAsString"_ns, aRetval);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionMockAPI.h b/toolkit/components/extensions/webidl-api/ExtensionMockAPI.h
new file mode 100644
index 0000000000..cc56bfa29e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionMockAPI.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionMockAPI_h
+#define mozilla_extensions_ExtensionMockAPI_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace extensions {
+
+using dom::DOMString;
+
+class ExtensionEventManager;
+class ExtensionPort;
+
+class ExtensionMockAPI final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionMockAPI(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ void GetPropertyAsErrorObject(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval);
+ void GetPropertyAsString(DOMString& aRetval);
+
+ ExtensionEventManager* OnTestEvent();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionMockAPI)
+
+ protected:
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"mockExtensionAPI"_ns; }
+
+ private:
+ ~ExtensionMockAPI() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnTestEventMgr;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionMockAPI_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionPort.cpp b/toolkit/components/extensions/webidl-api/ExtensionPort.cpp
new file mode 100644
index 0000000000..2329dfdfe2
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionPort.cpp
@@ -0,0 +1,109 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionPort.h"
+#include "ExtensionBrowser.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/BindingUtils.h" // SequenceRooter
+#include "mozilla/dom/ExtensionPortBinding.h"
+#include "mozilla/dom/ScriptSettings.h" // AutoEntryScript
+#include "nsIGlobalObject.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPort);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPort)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionPort)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ExtensionPort)
+ // Clean the entry for this instance from the ports lookup map
+ // stored in the related ExtensionBrowser instance.
+ tmp->ForgetReleasedPort();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionBrowser, mOnDisconnectEventMgr,
+ mOnMessageEventMgr)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ExtensionPort)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionBrowser, mOnDisconnectEventMgr,
+ mOnMessageEventMgr)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPort)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionPort, u"onMessage"_ns, OnMessage)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionPort, u"onDisconnect"_ns, OnDisconnect)
+
+// static
+already_AddRefed<ExtensionPort> ExtensionPort::Create(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ UniquePtr<dom::ExtensionPortDescriptor>&& aPortDescriptor) {
+ RefPtr<ExtensionPort> port =
+ new ExtensionPort(aGlobal, aExtensionBrowser, std::move(aPortDescriptor));
+ return port.forget();
+}
+
+// static
+UniquePtr<dom::ExtensionPortDescriptor> ExtensionPort::ToPortDescriptor(
+ JS::Handle<JS::Value> aDescriptorValue, ErrorResult& aRv) {
+ if (!aDescriptorValue.isObject()) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ dom::AutoEntryScript aes(&aDescriptorValue.toObject(), __func__);
+ JSContext* acx = aes.cx();
+ auto portDescriptor = MakeUnique<dom::ExtensionPortDescriptor>();
+ if (!portDescriptor->Init(acx, aDescriptorValue, __func__)) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ return portDescriptor;
+}
+
+ExtensionPort::ExtensionPort(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ UniquePtr<dom::ExtensionPortDescriptor>&& aPortDescriptor)
+ : mGlobal(aGlobal),
+ mExtensionBrowser(aExtensionBrowser),
+ mPortDescriptor(std::move(aPortDescriptor)) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+void ExtensionPort::ForgetReleasedPort() {
+ if (mExtensionBrowser) {
+ mExtensionBrowser->ForgetReleasedPort(mPortDescriptor->mPortId);
+ mExtensionBrowser = nullptr;
+ }
+ mPortDescriptor = nullptr;
+ mGlobal = nullptr;
+}
+
+nsString ExtensionPort::GetAPIObjectId() const {
+ return mPortDescriptor->mPortId;
+}
+
+JSObject* ExtensionPort::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionPort_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionPort::GetParentObject() const { return mGlobal; }
+
+void ExtensionPort::GetName(nsAString& aString) {
+ aString.Assign(mPortDescriptor->mName);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionPort.h b/toolkit/components/extensions/webidl-api/ExtensionPort.h
new file mode 100644
index 0000000000..35c072082c
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionPort.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionPort_h
+#define mozilla_extensions_ExtensionPort_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/WeakPtr.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+struct ExtensionPortDescriptor;
+}
+
+namespace extensions {
+
+class ExtensionEventManager;
+
+class ExtensionPort final : public nsISupports,
+ public nsWrapperCache,
+ public SupportsWeakPtr,
+ public ExtensionAPIBase {
+ public:
+ static already_AddRefed<ExtensionPort> Create(
+ nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ UniquePtr<dom::ExtensionPortDescriptor>&& aPortDescriptor);
+
+ static UniquePtr<dom::ExtensionPortDescriptor> ToPortDescriptor(
+ JS::Handle<JS::Value> aDescriptorValue, ErrorResult& aRv);
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionEventManager* OnDisconnect();
+ ExtensionEventManager* OnMessage();
+
+ void GetName(nsAString& aString);
+ void GetError(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ GetWebExtPropertyAsJSValue(aCx, u"error"_ns, aRetval);
+ }
+ void GetSender(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval) {
+ GetWebExtPropertyAsJSValue(aCx, u"sender"_ns, aRetval);
+ };
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionPort)
+
+ protected:
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"runtime"_ns; }
+
+ nsString GetAPIObjectType() const override { return u"Port"_ns; }
+
+ nsString GetAPIObjectId() const override;
+
+ private:
+ ExtensionPort(nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser,
+ UniquePtr<dom::ExtensionPortDescriptor>&& aPortDescriptor);
+
+ ~ExtensionPort() = default;
+
+ void ForgetReleasedPort();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnDisconnectEventMgr;
+ RefPtr<ExtensionEventManager> mOnMessageEventMgr;
+ UniquePtr<dom::ExtensionPortDescriptor> mPortDescriptor;
+ RefPtr<dom::Function> mCallback;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionPort_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionProxy.cpp b/toolkit/components/extensions/webidl-api/ExtensionProxy.cpp
new file mode 100644
index 0000000000..6abbdfd3c9
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionProxy.cpp
@@ -0,0 +1,51 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionProxy.h"
+#include "ExtensionEventManager.h"
+#include "ExtensionSetting.h"
+
+#include "mozilla/dom/ExtensionProxyBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionProxy);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionProxy)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionProxy, mGlobal,
+ mExtensionBrowser, mSettingsMgr,
+ mOnRequestEventMgr, mOnErrorEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionProxy)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionProxy, u"onRequest"_ns, OnRequest)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionProxy, u"onError"_ns, OnError)
+
+NS_IMPL_WEBEXT_SETTING_WITH_DATAMEMBER(ExtensionProxy, u"settings"_ns, Settings,
+ mSettingsMgr)
+
+ExtensionProxy::ExtensionProxy(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionProxy::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionProxy::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionProxy_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionProxy::GetParentObject() const { return mGlobal; }
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionProxy.h b/toolkit/components/extensions/webidl-api/ExtensionProxy.h
new file mode 100644
index 0000000000..3159c340f1
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionProxy.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionProxy_h
+#define mozilla_extensions_ExtensionProxy_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionEventManager;
+class ExtensionSetting;
+
+class ExtensionProxy final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionProxy(nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"proxy"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionEventManager* OnRequest();
+ ExtensionEventManager* OnError();
+
+ ExtensionSetting* Settings();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionProxy)
+
+ private:
+ ~ExtensionProxy() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnRequestEventMgr;
+ RefPtr<ExtensionEventManager> mOnErrorEventMgr;
+ RefPtr<ExtensionSetting> mSettingsMgr;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_ExtensionProxy_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionRuntime.cpp b/toolkit/components/extensions/webidl-api/ExtensionRuntime.cpp
new file mode 100644
index 0000000000..e93fec91da
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionRuntime.cpp
@@ -0,0 +1,67 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionRuntime.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionRuntimeBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla {
+namespace extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionRuntime);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionRuntime)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(
+ ExtensionRuntime, mGlobal, mExtensionBrowser, mOnStartupEventMgr,
+ mOnInstalledEventMgr, mOnUpdateAvailableEventMgr, mOnConnectEventMgr,
+ mOnConnectExternalEventMgr, mOnMessageEventMgr, mOnMessageExternalEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionRuntime)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onStartup"_ns, OnStartup)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onInstalled"_ns, OnInstalled)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onUpdateAvailable"_ns,
+ OnUpdateAvailable)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onConnect"_ns, OnConnect)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onConnectExternal"_ns,
+ OnConnectExternal)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onMessage"_ns, OnMessage)
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionRuntime, u"onMessageExternal"_ns,
+ OnMessageExternal)
+
+ExtensionRuntime::ExtensionRuntime(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionRuntime::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionRuntime::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionRuntime_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionRuntime::GetParentObject() const { return mGlobal; }
+
+void ExtensionRuntime::GetLastError(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval) {
+ mExtensionBrowser->GetLastError(aRetval);
+}
+
+void ExtensionRuntime::GetId(dom::DOMString& aRetval) {
+ GetWebExtPropertyAsString(u"id"_ns, aRetval);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionRuntime.h b/toolkit/components/extensions/webidl-api/ExtensionRuntime.h
new file mode 100644
index 0000000000..238f79cac0
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionRuntime.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionRuntime_h
+#define mozilla_extensions_ExtensionRuntime_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace extensions {
+
+class ExtensionEventManager;
+
+class ExtensionRuntime final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionRuntime(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"runtime"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionEventManager* OnStartup();
+ ExtensionEventManager* OnInstalled();
+ ExtensionEventManager* OnUpdateAvailable();
+ ExtensionEventManager* OnConnect();
+ ExtensionEventManager* OnConnectExternal();
+ ExtensionEventManager* OnMessage();
+ ExtensionEventManager* OnMessageExternal();
+
+ void GetLastError(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+ void GetId(dom::DOMString& aRetval);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionRuntime)
+
+ private:
+ ~ExtensionRuntime() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnStartupEventMgr;
+ RefPtr<ExtensionEventManager> mOnInstalledEventMgr;
+ RefPtr<ExtensionEventManager> mOnUpdateAvailableEventMgr;
+ RefPtr<ExtensionEventManager> mOnConnectEventMgr;
+ RefPtr<ExtensionEventManager> mOnConnectExternalEventMgr;
+ RefPtr<ExtensionEventManager> mOnMessageEventMgr;
+ RefPtr<ExtensionEventManager> mOnMessageExternalEventMgr;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionRuntime_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionScripting.cpp b/toolkit/components/extensions/webidl-api/ExtensionScripting.cpp
new file mode 100644
index 0000000000..2f07fbb8f1
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionScripting.cpp
@@ -0,0 +1,43 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionScripting.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionScriptingBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionScripting);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionScripting)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionScripting, mGlobal,
+ mExtensionBrowser);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionScripting)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+ExtensionScripting::ExtensionScripting(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionScripting::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionScripting::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionScripting_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionScripting::GetParentObject() const { return mGlobal; }
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionScripting.h b/toolkit/components/extensions/webidl-api/ExtensionScripting.h
new file mode 100644
index 0000000000..42d61015aa
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionScripting.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionScripting_h
+#define mozilla_extensions_ExtensionScripting_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace extensions {
+
+class ExtensionEventManager;
+
+class ExtensionScripting final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionScripting(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"scripting"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionScripting)
+
+ private:
+ ~ExtensionScripting() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionScripting_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionSetting.cpp b/toolkit/components/extensions/webidl-api/ExtensionSetting.cpp
new file mode 100644
index 0000000000..36a4fe8a01
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionSetting.cpp
@@ -0,0 +1,48 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionSetting.h"
+#include "ExtensionEventManager.h"
+
+#include "mozilla/dom/ExtensionSettingBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::extensions {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionSetting);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionSetting)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionSetting, mGlobal,
+ mExtensionBrowser, mOnChangeEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionSetting)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionSetting, u"onChange"_ns, OnChange)
+
+ExtensionSetting::ExtensionSetting(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser,
+ const nsAString& aNamespace)
+ : mGlobal(aGlobal),
+ mExtensionBrowser(aExtensionBrowser),
+ mAPINamespace(aNamespace) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionSetting::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ return true;
+}
+
+JSObject* ExtensionSetting::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionSetting_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionSetting::GetParentObject() const { return mGlobal; }
+
+} // namespace mozilla::extensions
diff --git a/toolkit/components/extensions/webidl-api/ExtensionSetting.h b/toolkit/components/extensions/webidl-api/ExtensionSetting.h
new file mode 100644
index 0000000000..c1692e8a14
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionSetting.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionSetting_h
+#define mozilla_extensions_ExtensionSetting_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::extensions {
+
+class ExtensionEventManager;
+
+class ExtensionSetting final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionSetting(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser,
+ const nsAString& aNamespace);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return mAPINamespace; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ ExtensionEventManager* OnChange();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionSetting)
+
+ private:
+ ~ExtensionSetting() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ nsString mAPINamespace;
+ RefPtr<ExtensionEventManager> mOnChangeEventMgr;
+};
+
+} // namespace mozilla::extensions
+
+#endif // mozilla_extensions_ExtensionDns_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.cpp b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp
new file mode 100644
index 0000000000..e9cde7ccad
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionTest.cpp
@@ -0,0 +1,527 @@
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "ExtensionTest.h"
+#include "ExtensionEventManager.h"
+#include "ExtensionAPICallFunctionNoReturn.h"
+
+#include "js/Equality.h" // JS::StrictlyEqual
+#include "js/PropertyAndElement.h" // JS_GetProperty
+#include "mozilla/dom/ExtensionTestBinding.h"
+#include "nsIGlobalObject.h"
+#include "js/RegExp.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "prenv.h"
+
+namespace mozilla {
+namespace extensions {
+
+bool IsInAutomation(JSContext* aCx, JSObject* aGlobal) {
+ return NS_IsMainThread()
+ ? xpc::IsInAutomation()
+ : dom::WorkerGlobalScope::IsInAutomation(aCx, aGlobal);
+}
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionTest);
+NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionTest)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ExtensionTest, mGlobal, mExtensionBrowser,
+ mOnMessageEventMgr);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionTest)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_WEBEXT_EVENTMGR(ExtensionTest, u"onMessage"_ns, OnMessage)
+
+ExtensionTest::ExtensionTest(nsIGlobalObject* aGlobal,
+ ExtensionBrowser* aExtensionBrowser)
+ : mGlobal(aGlobal), mExtensionBrowser(aExtensionBrowser) {
+ MOZ_DIAGNOSTIC_ASSERT(mGlobal);
+ MOZ_DIAGNOSTIC_ASSERT(mExtensionBrowser);
+}
+
+/* static */
+bool ExtensionTest::IsAllowed(JSContext* aCx, JSObject* aGlobal) {
+ // Allow browser.test API namespace while running in xpcshell tests.
+ if (PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) {
+ return true;
+ }
+
+ return IsInAutomation(aCx, aGlobal);
+}
+
+JSObject* ExtensionTest::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::ExtensionTest_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsIGlobalObject* ExtensionTest::GetParentObject() const { return mGlobal; }
+
+void ExtensionTest::CallWebExtMethodAssertEq(
+ JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs, ErrorResult& aRv) {
+ uint32_t argsCount = aArgs.Length();
+
+ JS::Rooted<JS::Value> expectedVal(
+ aCx, argsCount > 0 ? aArgs[0] : JS::UndefinedValue());
+ JS::Rooted<JS::Value> actualVal(
+ aCx, argsCount > 1 ? aArgs[1] : JS::UndefinedValue());
+ JS::Rooted<JS::Value> messageVal(
+ aCx, argsCount > 2 ? aArgs[2] : JS::UndefinedValue());
+
+ bool isEqual;
+ if (NS_WARN_IF(!JS::StrictlyEqual(aCx, actualVal, expectedVal, &isEqual))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ JS::Rooted<JSString*> expectedJSString(aCx, JS::ToString(aCx, expectedVal));
+ JS::Rooted<JSString*> actualJSString(aCx, JS::ToString(aCx, actualVal));
+ JS::Rooted<JSString*> messageJSString(aCx, JS::ToString(aCx, messageVal));
+
+ nsString expected;
+ nsString actual;
+ nsString message;
+
+ if (NS_WARN_IF(!AssignJSString(aCx, expected, expectedJSString) ||
+ !AssignJSString(aCx, actual, actualJSString) ||
+ !AssignJSString(aCx, message, messageJSString))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ if (!isEqual && actual.Equals(expected)) {
+ actual.AppendLiteral(" (different)");
+ }
+
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, expected, &expectedVal) ||
+ !dom::ToJSValue(aCx, actual, &actualVal) ||
+ !dom::ToJSValue(aCx, message, &messageVal))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ dom::Sequence<JS::Value> args;
+ if (NS_WARN_IF(!args.AppendElement(expectedVal, fallible) ||
+ !args.AppendElement(actualVal, fallible) ||
+ !args.AppendElement(messageVal, fallible))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ CallWebExtMethodNoReturn(aCx, aApiMethod, args, aRv);
+}
+
+MOZ_CAN_RUN_SCRIPT bool ExtensionTest::AssertMatchInternal(
+ JSContext* aCx, const JS::HandleValue aActualValue,
+ const JS::HandleValue aExpectedMatchValue, const nsAString& aMessagePre,
+ const nsAString& aMessage,
+ UniquePtr<dom::SerializedStackHolder> aSerializedCallerStack,
+ ErrorResult& aRv) {
+ // Stringify the actual value, if the expected value is a regexp or a string
+ // then it will be used as part of the matching assertion, otherwise it is
+ // still interpolated in the assertion message.
+ JS::Rooted<JSString*> actualToString(aCx, JS::ToString(aCx, aActualValue));
+ NS_ENSURE_TRUE(actualToString, false);
+ nsAutoJSString actualString;
+ NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false);
+
+ bool matched = false;
+
+ if (aExpectedMatchValue.isObject()) {
+ JS::Rooted<JSObject*> expectedMatchObj(aCx,
+ &aExpectedMatchValue.toObject());
+
+ bool isRegexp;
+ NS_ENSURE_TRUE(JS::ObjectIsRegExp(aCx, expectedMatchObj, &isRegexp), false);
+
+ if (isRegexp) {
+ // Expected value is a regexp, test if the stringified actual value does
+ // match.
+ nsString input(actualString);
+ size_t index = 0;
+ JS::Rooted<JS::Value> rxResult(aCx);
+ NS_ENSURE_TRUE(JS::ExecuteRegExpNoStatics(
+ aCx, expectedMatchObj, input.BeginWriting(),
+ actualString.Length(), &index, true, &rxResult),
+ false);
+ matched = !rxResult.isNull();
+ } else if (JS::IsCallable(expectedMatchObj) &&
+ !JS::IsConstructor(expectedMatchObj)) {
+ // Expected value is a matcher function, execute it with the value as a
+ // parameter:
+ //
+ // - if the matcher function throws, steal the exception to re-raise it
+ // to the extension code that called the assertion method, but
+ // continue to still report the assertion as failed to the WebExtensions
+ // internals.
+ //
+ // - if the function return a falsey value, the assertion should fail and
+ // no exception is raised to the extension code that called the
+ // assertion
+ JS::Rooted<JS::Value> retval(aCx);
+ aRv.MightThrowJSException();
+ if (!JS::Call(aCx, JS::UndefinedHandleValue, expectedMatchObj,
+ JS::HandleValueArray(aActualValue), &retval)) {
+ aRv.StealExceptionFromJSContext(aCx);
+ matched = false;
+ } else {
+ matched = JS::ToBoolean(retval);
+ }
+ } else if (JS::IsConstructor(expectedMatchObj)) {
+ // Expected value is a constructor, test if the actual value is an
+ // instanceof the expected constructor.
+ NS_ENSURE_TRUE(
+ JS_HasInstance(aCx, expectedMatchObj, aActualValue, &matched), false);
+ } else {
+ // Fallback to strict equal for any other js object type we don't expect.
+ NS_ENSURE_TRUE(
+ JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched),
+ false);
+ }
+ } else if (aExpectedMatchValue.isString()) {
+ // Expected value is a string, assertion should fail if the expected string
+ // isn't equal to the stringified actual value.
+ JS::Rooted<JSString*> expectedToString(
+ aCx, JS::ToString(aCx, aExpectedMatchValue));
+ NS_ENSURE_TRUE(expectedToString, false);
+
+ nsAutoJSString expectedString;
+ NS_ENSURE_TRUE(expectedString.init(aCx, expectedToString), false);
+
+ // If actual is an object and it has a message property that is a string,
+ // then we want to use that message string as the string to compare the
+ // expected one with.
+ //
+ // This is needed mainly to match the current JS implementation.
+ //
+ // TODO(Bug 1731094): as a low priority follow up, we may want to reconsider
+ // and compare the entire stringified error (which is also often a common
+ // behavior in many third party JS test frameworks).
+ JS::Rooted<JS::Value> messageVal(aCx);
+ if (aActualValue.isObject()) {
+ JS::Rooted<JSObject*> actualValueObj(aCx, &aActualValue.toObject());
+
+ if (!JS_GetProperty(aCx, actualValueObj, "message", &messageVal)) {
+ // GetProperty may raise an exception, in that case we steal the
+ // exception to re-raise it to the caller, but continue to still report
+ // the assertion as failed to the WebExtensions internals.
+ aRv.StealExceptionFromJSContext(aCx);
+ matched = false;
+ }
+
+ if (messageVal.isString()) {
+ actualToString.set(messageVal.toString());
+ NS_ENSURE_TRUE(actualString.init(aCx, actualToString), false);
+ }
+ }
+ matched = expectedString.Equals(actualString);
+ } else {
+ // Fallback to strict equal for any other js value type we don't expect.
+ NS_ENSURE_TRUE(
+ JS::StrictlyEqual(aCx, aActualValue, aExpectedMatchValue, &matched),
+ false);
+ }
+
+ // Convert the expected value to a source string, to be interpolated
+ // in the assertion message.
+ JS::Rooted<JSString*> expectedToSource(
+ aCx, JS_ValueToSource(aCx, aExpectedMatchValue));
+ NS_ENSURE_TRUE(expectedToSource, false);
+ nsAutoJSString expectedSource;
+ NS_ENSURE_TRUE(expectedSource.init(aCx, expectedToSource), false);
+
+ nsString message;
+ message.AppendPrintf("%s to match '%s', got '%s'",
+ NS_ConvertUTF16toUTF8(aMessagePre).get(),
+ NS_ConvertUTF16toUTF8(expectedSource).get(),
+ NS_ConvertUTF16toUTF8(actualString).get());
+ if (!aMessage.IsEmpty()) {
+ message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(aMessage).get());
+ }
+
+ // Complete the assertion by forwarding the boolean result and the
+ // interpolated assertion message to the test.assertTrue API method on the
+ // main thread.
+ dom::Sequence<JS::Value> assertTrueArgs;
+ JS::Rooted<JS::Value> arg0(aCx);
+ JS::Rooted<JS::Value> arg1(aCx);
+ NS_ENSURE_FALSE(!dom::ToJSValue(aCx, matched, &arg0) ||
+ !dom::ToJSValue(aCx, message, &arg1) ||
+ !assertTrueArgs.AppendElement(arg0, fallible) ||
+ !assertTrueArgs.AppendElement(arg1, fallible),
+ false);
+
+ auto request = CallFunctionNoReturn(u"assertTrue"_ns);
+ IgnoredErrorResult erv;
+ if (aSerializedCallerStack) {
+ request->SetSerializedCallerStack(std::move(aSerializedCallerStack));
+ }
+ request->Run(GetGlobalObject(), aCx, assertTrueArgs, erv);
+ NS_ENSURE_FALSE(erv.Failed(), false);
+ return true;
+}
+
+MOZ_CAN_RUN_SCRIPT void ExtensionTest::AssertThrows(
+ JSContext* aCx, dom::Function& aFunction,
+ const JS::HandleValue aExpectedError, const nsAString& aMessage,
+ ErrorResult& aRv) {
+ // Call the function that is expected to throw, then get the pending exception
+ // to pass it to the AssertMatchInternal.
+ ErrorResult erv;
+ erv.MightThrowJSException();
+ JS::Rooted<JS::Value> ignoredRetval(aCx);
+ aFunction.Call({}, &ignoredRetval, erv, "ExtensionTest::AssertThrows",
+ dom::Function::eRethrowExceptions);
+
+ bool didThrow = false;
+ JS::Rooted<JS::Value> exn(aCx);
+
+ if (erv.MaybeSetPendingException(aCx) && JS_GetPendingException(aCx, &exn)) {
+ JS_ClearPendingException(aCx);
+ didThrow = true;
+ }
+
+ // If the function did not throw, then the assertion is failed
+ // and the result should be forwarded to assertTrue on the main thread.
+ if (!didThrow) {
+ JS::Rooted<JSString*> expectedErrorToSource(
+ aCx, JS_ValueToSource(aCx, aExpectedError));
+ if (NS_WARN_IF(!expectedErrorToSource)) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+ nsAutoJSString expectedErrorSource;
+ if (NS_WARN_IF(!expectedErrorSource.init(aCx, expectedErrorToSource))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ nsString message;
+ message.AppendPrintf("Function did not throw, expected error '%s'",
+ NS_ConvertUTF16toUTF8(expectedErrorSource).get());
+ if (!aMessage.IsEmpty()) {
+ message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(aMessage).get());
+ }
+
+ dom::Sequence<JS::Value> assertTrueArgs;
+ JS::Rooted<JS::Value> arg0(aCx);
+ JS::Rooted<JS::Value> arg1(aCx);
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, false, &arg0) ||
+ !dom::ToJSValue(aCx, message, &arg1) ||
+ !assertTrueArgs.AppendElement(arg0, fallible) ||
+ !assertTrueArgs.AppendElement(arg1, fallible))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+
+ CallWebExtMethodNoReturn(aCx, u"assertTrue"_ns, assertTrueArgs, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ }
+ return;
+ }
+
+ if (NS_WARN_IF(!AssertMatchInternal(aCx, exn, aExpectedError,
+ u"Function threw, expecting error"_ns,
+ aMessage, nullptr, aRv))) {
+ ThrowUnexpectedError(aCx, aRv);
+ }
+}
+
+MOZ_CAN_RUN_SCRIPT void ExtensionTest::AssertThrows(
+ JSContext* aCx, dom::Function& aFunction,
+ const JS::HandleValue aExpectedError, ErrorResult& aRv) {
+ AssertThrows(aCx, aFunction, aExpectedError, EmptyString(), aRv);
+}
+
+#define ASSERT_REJECT_UNKNOWN_FAIL_STR "Failed to complete assertRejects call"
+
+class AssertRejectsHandler final : public dom::PromiseNativeHandler {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AssertRejectsHandler)
+
+ static void Create(ExtensionTest* aExtensionTest, dom::Promise* aPromise,
+ dom::Promise* outPromise,
+ JS::Handle<JS::Value> aExpectedMatchValue,
+ const nsAString& aMessage,
+ UniquePtr<dom::SerializedStackHolder>&& aCallerStack) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(outPromise);
+ MOZ_ASSERT(aExtensionTest);
+
+ RefPtr<AssertRejectsHandler> handler = new AssertRejectsHandler(
+ aExtensionTest, outPromise, aExpectedMatchValue, aMessage,
+ std::move(aCallerStack));
+
+ aPromise->AppendNativeHandler(handler);
+ }
+
+ MOZ_CAN_RUN_SCRIPT void ResolvedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ nsAutoJSString expectedErrorSource;
+ JS::Rooted<JS::Value> rootedExpectedMatchValue(aCx, mExpectedMatchValue);
+ JS::Rooted<JSString*> expectedErrorToSource(
+ aCx, JS_ValueToSource(aCx, rootedExpectedMatchValue));
+ if (NS_WARN_IF(!expectedErrorToSource ||
+ !expectedErrorSource.init(aCx, expectedErrorToSource))) {
+ mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR);
+ return;
+ }
+
+ nsString message;
+ message.AppendPrintf("Promise resolved, expect rejection '%s'",
+ NS_ConvertUTF16toUTF8(expectedErrorSource).get());
+
+ if (!mMessageStr.IsEmpty()) {
+ message.AppendPrintf(": %s", NS_ConvertUTF16toUTF8(mMessageStr).get());
+ }
+
+ dom::Sequence<JS::Value> assertTrueArgs;
+ JS::Rooted<JS::Value> arg0(aCx);
+ JS::Rooted<JS::Value> arg1(aCx);
+ if (NS_WARN_IF(!dom::ToJSValue(aCx, false, &arg0) ||
+ !dom::ToJSValue(aCx, message, &arg1) ||
+ !assertTrueArgs.AppendElement(arg0, fallible) ||
+ !assertTrueArgs.AppendElement(arg1, fallible))) {
+ mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR);
+ return;
+ }
+
+ IgnoredErrorResult erv;
+ auto request = mExtensionTest->CallFunctionNoReturn(u"assertTrue"_ns);
+ request->SetSerializedCallerStack(std::move(mCallerStack));
+ request->Run(mExtensionTest->GetGlobalObject(), aCx, assertTrueArgs, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR);
+ return;
+ }
+ mOutPromise->MaybeResolve(JS::UndefinedValue());
+ }
+
+ MOZ_CAN_RUN_SCRIPT void RejectedCallback(JSContext* aCx,
+ JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ JS::Rooted<JS::Value> expectedMatchRooted(aCx, mExpectedMatchValue);
+ ErrorResult erv;
+
+ if (NS_WARN_IF(!MOZ_KnownLive(mExtensionTest)
+ ->AssertMatchInternal(
+ aCx, aValue, expectedMatchRooted,
+ u"Promise rejected, expected rejection"_ns,
+ mMessageStr, std::move(mCallerStack), erv))) {
+ // Reject for other unknown errors.
+ mOutPromise->MaybeRejectWithUnknownError(ASSERT_REJECT_UNKNOWN_FAIL_STR);
+ return;
+ }
+
+ // Reject with the matcher function exception.
+ erv.WouldReportJSException();
+ if (erv.Failed()) {
+ mOutPromise->MaybeReject(std::move(erv));
+ return;
+ }
+ mExpectedMatchValue.setUndefined();
+ mOutPromise->MaybeResolveWithUndefined();
+ }
+
+ private:
+ AssertRejectsHandler(ExtensionTest* aExtensionTest, dom::Promise* mOutPromise,
+ JS::Handle<JS::Value> aExpectedMatchValue,
+ const nsAString& aMessage,
+ UniquePtr<dom::SerializedStackHolder>&& aCallerStack)
+ : mOutPromise(mOutPromise), mExtensionTest(aExtensionTest) {
+ MOZ_ASSERT(mOutPromise);
+ MOZ_ASSERT(mExtensionTest);
+ mozilla::HoldJSObjects(this);
+ mExpectedMatchValue.set(aExpectedMatchValue);
+ mCallerStack = std::move(aCallerStack);
+ mMessageStr = aMessage;
+ }
+
+ ~AssertRejectsHandler() {
+ mOutPromise = nullptr;
+ mExtensionTest = nullptr;
+ mExpectedMatchValue.setUndefined();
+ mozilla::DropJSObjects(this);
+ };
+
+ RefPtr<dom::Promise> mOutPromise;
+ RefPtr<ExtensionTest> mExtensionTest;
+ JS::Heap<JS::Value> mExpectedMatchValue;
+ UniquePtr<dom::SerializedStackHolder> mCallerStack;
+ nsString mMessageStr;
+};
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AssertRejectsHandler)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AssertRejectsHandler)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(AssertRejectsHandler)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(AssertRejectsHandler)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AssertRejectsHandler)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExtensionTest)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutPromise)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AssertRejectsHandler)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mExpectedMatchValue)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AssertRejectsHandler)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mExtensionTest)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutPromise)
+ tmp->mExpectedMatchValue.setUndefined();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+void ExtensionTest::AssertRejects(
+ JSContext* aCx, dom::Promise& aPromise,
+ const JS::HandleValue aExpectedError, const nsAString& aMessage,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ auto* global = GetGlobalObject();
+
+ IgnoredErrorResult erv;
+ RefPtr<dom::Promise> outPromise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+ MOZ_ASSERT(outPromise);
+
+ AssertRejectsHandler::Create(this, &aPromise, outPromise, aExpectedError,
+ aMessage, dom::GetCurrentStack(aCx));
+
+ if (aCallback.WasPassed()) {
+ // In theory we could also support the callback-based behavior, but we
+ // only use this in tests and so we don't really need to support it
+ // for Chrome-compatibility reasons.
+ aRv.ThrowNotSupportedError("assertRejects does not support a callback");
+ return;
+ }
+
+ if (NS_WARN_IF(!ToJSValue(aCx, outPromise, aRetval))) {
+ ThrowUnexpectedError(aCx, aRv);
+ return;
+ }
+}
+
+void ExtensionTest::AssertRejects(
+ JSContext* aCx, dom::Promise& aPromise,
+ const JS::HandleValue aExpectedError,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv) {
+ AssertRejects(aCx, aPromise, aExpectedError, EmptyString(), aCallback,
+ aRetval, aRv);
+}
+
+} // namespace extensions
+} // namespace mozilla
diff --git a/toolkit/components/extensions/webidl-api/ExtensionTest.h b/toolkit/components/extensions/webidl-api/ExtensionTest.h
new file mode 100644
index 0000000000..09cfa8bde0
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionTest.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_extensions_ExtensionTest_h
+#define mozilla_extensions_ExtensionTest_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/SerializedStackHolder.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+#include "ExtensionAPIBase.h"
+#include "ExtensionBrowser.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+namespace extensions {
+
+class ExtensionEventManager;
+
+bool IsInAutomation(JSContext* aCx, JSObject* aGlobal);
+
+class ExtensionTest final : public nsISupports,
+ public nsWrapperCache,
+ public ExtensionAPINamespace {
+ public:
+ ExtensionTest(nsIGlobalObject* aGlobal, ExtensionBrowser* aExtensionBrowser);
+
+ // ExtensionAPIBase methods
+ nsIGlobalObject* GetGlobalObject() const override { return mGlobal; }
+
+ ExtensionBrowser* GetExtensionBrowser() const override {
+ return mExtensionBrowser;
+ }
+
+ nsString GetAPINamespace() const override { return u"test"_ns; }
+
+ // nsWrapperCache interface methods
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // DOM bindings methods
+ static bool IsAllowed(JSContext* aCx, JSObject* aGlobal);
+
+ nsIGlobalObject* GetParentObject() const;
+
+ void CallWebExtMethodAssertEq(JSContext* aCx, const nsAString& aApiMethod,
+ const dom::Sequence<JS::Value>& aArgs,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT bool AssertMatchInternal(
+ JSContext* aCx, const JS::HandleValue aActualValue,
+ const JS::HandleValue aExpectedMatchValue, const nsAString& aMessagePre,
+ const nsAString& aMessage,
+ UniquePtr<dom::SerializedStackHolder> aSerializedCallerStack,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT void AssertThrows(JSContext* aCx, dom::Function& aFunction,
+ const JS::HandleValue aExpectedError,
+ const nsAString& aMessage,
+ ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT void AssertThrows(JSContext* aCx, dom::Function& aFunction,
+ const JS::HandleValue aExpectedError,
+ ErrorResult& aRv);
+
+ void AssertRejects(
+ JSContext* aCx, dom::Promise& aPromise,
+ const JS::HandleValue aExpectedError, const nsAString& aMessage,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv);
+ void AssertRejects(
+ JSContext* aCx, dom::Promise& aPromise,
+ const JS::HandleValue aExpectedError,
+ const dom::Optional<OwningNonNull<dom::Function>>& aCallback,
+ JS::MutableHandle<JS::Value> aRetval, ErrorResult& aRv);
+
+ ExtensionEventManager* OnMessage();
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ExtensionTest)
+
+ private:
+ ~ExtensionTest() = default;
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<ExtensionBrowser> mExtensionBrowser;
+ RefPtr<ExtensionEventManager> mOnMessageEventMgr;
+};
+
+} // namespace extensions
+} // namespace mozilla
+
+#endif // mozilla_extensions_ExtensionTest_h
diff --git a/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf b/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf
new file mode 100644
index 0000000000..be312c80d4
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/ExtensionWebIDL.conf
@@ -0,0 +1,101 @@
+# -*- Mode:Python; tab-width:8; indent-tabs-mode:nil -*- */
+# vim: set ts=8 sts=4 et sw=4 tw=80: */
+# 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/.
+
+# WebExtension WebIDL API Bindings Configuration, used by the script
+# `dom/extensions-webidl/GenerateWebIDLBindingsFromJSONSchema.py`
+# to customize the WebIDL generated based on the WebExtensions API JSON Schemas.
+#
+# Generating the WebIDL definitions for some of the WebExtensions API does require
+# some special handling, there are corresponding entries in the configuration tables
+# below.
+#
+
+# Mapping table between the JSON Schema types (represented as keys of the map)
+# and the related WebIDL type (represented by the value in the map).
+# Any mapping missing from this table will fallback to use the "any" webidl type
+# (See GenerateWebIDLBindings.py WebIDLHelpers.webidl_type_from_mapping method).
+#
+# NOTE: Please keep this table in alphabetic order (upper and lower case in two
+# separate alphabetic orders, group of the upcase ones first).
+WEBEXT_TYPES_MAPPING = {
+ "ExpectedError": "any", # Only used in test.assertThrows/assertRejects
+ "Port": "ExtensionPort",
+ "Promise": "Promise<any>",
+ "StreamFilter": "ExtensionStreamFilter",
+ "any": "any",
+ "boolean": "boolean",
+ "number": "float",
+ "function": "Function",
+ "integer": "long",
+ "object": "any", # TODO: as a follow up we may look into generating webidl dictionaries to achieve a more precise mapping
+ "runtime.Port": "ExtensionPort",
+ "string": "DOMString",
+ "types.Setting": "ExtensionSetting",
+}
+
+# Set of the types from the WEBEXT_TYPES_MAPPING that will be threated as primitive
+# types (e.g. used to omit optional attribute in the WebIDL methods type signatures).
+#
+# NOTE: Please keep this table in alphabetic order (upper and lower case in two
+# separate alphabetic orders, group of the update ones first).
+WEBIDL_PRIMITIVE_TYPES = set([
+ "DOMString",
+ "boolean",
+ "float"
+ "long",
+])
+
+# Mapping table for some APIs that do require special handling and a
+# specific stub method should be set in the generated webidl extended
+# attribute `WebExtensionStub`.
+#
+# The key in this map represent the API method name (including the
+# API namespace that is part of), the value is the value to set on the
+# `WebExtensionStub` webidl extended attribute:
+#
+# "namespace.methodName": "WebExtensionStubName",
+#
+# NOTE: Please keep this table in alphabetic order.
+WEBEXT_STUBS_MAPPING = {
+ "dns.resolve": "AsyncAmbiguous",
+ "runtime.connect": "ReturnsPort",
+ "runtime.connectNative": "ReturnsPort",
+ "runtime.getURL": "ReturnsString",
+ # TODO: Bug 1782690 - This method accepts functions/args so we'll need to
+ # serialize them.
+ "scripting.executeScript": "NotImplementedAsync",
+ "scripting.getRegisteredContentScripts": "AsyncAmbiguous",
+ "scripting.unregisterContentScripts": "AsyncAmbiguous",
+ "test.assertEq": "AssertEq",
+ "test.assertRejects": False, # No WebExtensionStub attribute.
+ "test.assertThrows": False, # No WebExtensionStub attribute.
+ "test.withHandlingUserInput": "NotImplementedNoReturn",
+}
+
+WEBEXT_WORKER_HIDDEN_SET = set([
+ "runtime.getFrameId",
+ "runtime.getBackgroundPage",
+])
+
+# Mapping table for the directories where the JSON API schema will be loaded
+# from.
+WEBEXT_SCHEMADIRS_MAPPING = {
+ "toolkit": ["toolkit", "components", "extensions", "schemas"],
+ "browser": ["browser", "components", "extensions", "schemas"],
+ "mobile": ["mobile", "android", "components", "extensions", "schemas"],
+}
+
+# List of toolkit-level WebExtensions API namespaces that are not included
+# in android builds.
+#
+# NOTE: keep this list in sync with the API namespaces excluded in
+# - toolkit/components/extensions/jar.mn
+# - toolkit/components/extensions/schemas/jar.mn
+WEBEXT_ANDROID_EXCLUDED = [
+ "captivePortal",
+ "geckoProfiler",
+ "identity"
+]
diff --git a/toolkit/components/extensions/webidl-api/GenerateWebIDLBindings.py b/toolkit/components/extensions/webidl-api/GenerateWebIDLBindings.py
new file mode 100644
index 0000000000..3b31bd924d
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/GenerateWebIDLBindings.py
@@ -0,0 +1,1612 @@
+# 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/.
+
+import argparse
+import difflib
+import json
+import logging
+import os
+import subprocess
+import sys
+import tempfile
+
+try:
+ import buildconfig
+ import jinja2
+ import jsonschema
+ import mozpack.path as mozpath
+except ModuleNotFoundError as e:
+ print(
+ "This script should be executed using `mach python %s`\n" % __file__,
+ file=sys.stderr,
+ )
+ raise e
+
+WEBIDL_DIR = mozpath.join("dom", "webidl")
+WEBIDL_DIR_FULLPATH = mozpath.join(buildconfig.topsrcdir, WEBIDL_DIR)
+
+CPP_DIR = mozpath.join("toolkit", "components", "extensions", "webidl-api")
+CPP_DIR_FULLPATH = mozpath.join(buildconfig.topsrcdir, CPP_DIR)
+
+# Absolute path to the base dir for this script.
+BASE_DIR = CPP_DIR_FULLPATH
+
+# TODO(Bug 1724785): a patch to introduce the doc page linked below is attached to
+# this bug and meant to ideally land along with this patch.
+DOCS_NEXT_STEPS = """
+The following documentation page provides more in depth details of the next steps:
+
+https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/wiring_up_new_webidl_bindings.html
+"""
+
+# Load the configuration file.
+glbl = {}
+with open(mozpath.join(BASE_DIR, "ExtensionWebIDL.conf")) as f:
+ exec(f.read(), glbl)
+
+# Special mapping between the JSON schema type and the related WebIDL type.
+WEBEXT_TYPES_MAPPING = glbl["WEBEXT_TYPES_MAPPING"]
+
+# Special mapping for the `WebExtensionStub` to be used for API methods that
+# require special handling.
+WEBEXT_STUBS_MAPPING = glbl["WEBEXT_STUBS_MAPPING"]
+
+# Schema entries that should be hidden in workers.
+WEBEXT_WORKER_HIDDEN_SET = glbl["WEBEXT_WORKER_HIDDEN_SET"]
+
+# Set of the webidl type names to be threated as primitive types.
+WEBIDL_PRIMITIVE_TYPES = glbl["WEBIDL_PRIMITIVE_TYPES"]
+
+# Mapping table for the directory where the JSON schema are going to be loaded from,
+# the 'toolkit' ones are potentially available on both desktop and mobile builds
+# (if not specified otherwise through the WEBEXT_ANDROID_EXCLUDED list), whereas the
+# 'browser' and 'mobile' ones are only available on desktop and mobile builds
+# respectively.
+#
+# `load_and_parse_JSONSchema` will iterate over this map and will call `Schemas`
+# load_schemas method passing the path to the directory with the schema data and the
+# related key from this map as the `schema_group` associated with all the schema data
+# being loaded.
+#
+# Schema data loaded from different groups may potentially overlap, and the resulting
+# generated webidl may contain preprocessing macro to conditionally include different
+# webidl signatures on different builds (in particular for the Desktop vs. Android
+# differences).
+WEBEXT_SCHEMADIRS_MAPPING = glbl["WEBEXT_SCHEMADIRS_MAPPING"]
+
+# List of toolkit-level WebExtensions API namespaces that are not included in
+# android builds.
+WEBEXT_ANDROID_EXCLUDED = glbl["WEBEXT_ANDROID_EXCLUDED"]
+
+# Define a custom jsonschema validation class
+WebExtAPIValidator = jsonschema.validators.extend(
+ jsonschema.validators.Draft4Validator,
+)
+# Hack: inject any as a valid simple types.
+WebExtAPIValidator.META_SCHEMA["definitions"]["simpleTypes"]["enum"].append("any")
+
+
+def run_diff(
+ diff_cmd,
+ left_name,
+ left_text,
+ right_name,
+ right_text,
+ always_return_diff_output=True,
+):
+ """
+ Creates two temporary files and run the given `diff_cmd` to generate a diff
+ between the two temporary files (used to generate diffs related to the JSON
+ Schema files for desktop and mobile builds)
+ """
+
+ diff_output = ""
+
+ # Generate the diff using difflib if diff_cmd isn't set.
+ if diff_cmd is None:
+ diff_generator = difflib.unified_diff(
+ left_text.splitlines(keepends=True),
+ right_text.splitlines(keepends=True),
+ fromfile=left_name,
+ tofile=right_name,
+ )
+ diff_output = "".join(diff_generator)
+ else:
+ # Optionally allow to generate the diff using an external diff tool
+ # (e.g. choosing `icdiff` through `--diff-command icdiff` would generate
+ # colored side-by-side diffs).
+ with tempfile.NamedTemporaryFile("w+t", prefix="%s-" % left_name) as left_file:
+ with tempfile.NamedTemporaryFile(
+ "w+t", prefix="%s-" % right_name
+ ) as right_file:
+ left_file.write(left_text)
+ left_file.flush()
+ right_file.write(right_text)
+ right_file.flush()
+ diff_output = subprocess.run(
+ [diff_cmd, "-u", left_file.name, right_file.name],
+ capture_output=True,
+ ).stdout.decode("utf-8")
+
+ if always_return_diff_output and len(diff_output) == 0:
+ return "Diff empty: both files have the exact same content."
+
+ return diff_output
+
+
+def read_json(json_file_path):
+ """
+ Helper function used to read the WebExtensions API schema JSON files
+ by ignoring the license comment on the top of some of those files.
+ Same helper as the one available in Schemas.jsm:
+ https://searchfox.org/mozilla-central/rev/3434a9df60373a997263107e6f124fb164ddebf2/toolkit/components/extensions/Schemas.jsm#70
+ """
+ with open(json_file_path) as json_file:
+ txt = json_file.read()
+ # Chrome JSON files include a license comment that we need to
+ # strip off for this to be valid JSON. As a hack, we just
+ # look for the first '[' character, which signals the start
+ # of the JSON content.
+ return json.loads(txt[txt.index("[") :])
+
+
+def write_with_overwrite_confirm(
+ relpath,
+ abspath,
+ newcontent,
+ diff_prefix,
+ diff_command=None,
+ overwrite_existing=False,
+):
+ is_overwriting = False
+ no_changes = False
+
+ # Make sure generated files do have a newline at the end of the file.
+ if newcontent[-1] != "\n":
+ newcontent = newcontent + "\n"
+
+ if os.path.exists(abspath):
+ with open(abspath, "r") as existingfile:
+ existingcontent = existingfile.read()
+ if existingcontent == newcontent:
+ no_changes = True
+ elif not overwrite_existing:
+ print("Found existing %s.\n" % relpath, file=sys.stderr)
+ print(
+ "(Run again with --overwrite-existing to allow overwriting it automatically)",
+ file=sys.stderr,
+ )
+ data = ""
+ while data not in ["Y", "N", "D"]:
+ data = input(
+ "\nOverwrite %s? (Y = Yes / N = No / D = Diff)\n" % relpath
+ ).upper()
+ if data == "N":
+ print(
+ "Aborted saving updated content to file %s" % relpath,
+ file=sys.stderr,
+ )
+ return False
+ elif data == "D":
+ print(
+ run_diff(
+ diff_command,
+ "%s--existing" % diff_prefix,
+ "".join(open(abspath, "r").readlines()),
+ "%s--updated" % diff_prefix,
+ newcontent,
+ )
+ )
+ data = "" # Ask confirmation again after printing diff.
+ elif data == "Y":
+ is_overwriting = True
+ break
+ else:
+ is_overwriting = True
+
+ if is_overwriting:
+ print("Overwriting %s.\n" % relpath, file=sys.stderr)
+
+ if no_changes:
+ print("No changes for the existing %s.\n" % relpath, file=sys.stderr)
+ else:
+ with open(abspath, "w") as dest_file:
+ dest_file.write(newcontent)
+ print("Wrote new content in file %s" % relpath)
+
+ # Return true if there were changes written on disk
+ return not no_changes
+
+
+class DefaultDict(dict):
+ def __init__(self, createDefault):
+ self._createDefault = createDefault
+
+ def getOrCreate(self, key):
+ if key not in self:
+ self[key] = self._createDefault(key)
+ return self[key]
+
+
+class WebIDLHelpers:
+ """
+ A collection of helpers used to generate the WebIDL definitions for the
+ API entries loaded from the collected JSON schema files.
+ """
+
+ @classmethod
+ def expect_instance(cls, obj, expected_class):
+ """
+ Raise a TypeError if `obj` is not an instance of `Class`.
+ """
+
+ if not isinstance(obj, expected_class):
+ raise TypeError(
+ "Unexpected object type, expected %s: %s" % (expected_class, obj)
+ )
+
+ @classmethod
+ def namespace_to_webidl_definition(cls, api_ns, schema_group):
+ """
+ Generate the WebIDL definition for the given APINamespace instance.
+ """
+
+ # TODO: schema_group is currently unused in this method.
+ template = api_ns.root.jinja_env.get_template("ExtensionAPI.webidl.in")
+ return template.render(cls.to_template_props(api_ns))
+
+ @classmethod
+ def to_webidl_definition_name(cls, text):
+ """
+ Convert a namespace name into its related webidl definition name.
+ """
+
+ # Join namespace parts, with capitalized first letters.
+ name = "Extension"
+ for part in text.split("."):
+ name += part[0].upper() + part[1:]
+ return name
+
+ @classmethod
+ def to_template_props(cls, api_ns):
+ """
+ Convert an APINamespace object its the set of properties that are
+ expected by the webidl template.
+ """
+
+ cls.expect_instance(api_ns, APINamespace)
+
+ webidl_description_comment = (
+ '// WebIDL definition for the "%s" WebExtensions API' % api_ns.name
+ )
+ webidl_name = cls.to_webidl_definition_name(api_ns.name)
+
+ # TODO: some API should not be exposed to service workers (e.g. runtime.getViews),
+ # refer to a config file to detect this kind of exceptions/special cases.
+ #
+ # TODO: once we want to expose the WebIDL bindings to extension windows
+ # and not just service workers we will need to add "Window" to the
+ # webidl_exposed_attr and only expose APIs with allowed_context "devtools_only"
+ # on Windows.
+ #
+ # e.g.
+ # if "devtools_only" in api_ns.allowed_contexts:
+ # webidl_exposed_attr = ", ".join(["Window"])
+ # else:
+ # webidl_exposed_attr = ", ".join(["ServiceWorker", "Window"])
+ if "devtools_only" in api_ns.allowed_contexts:
+ raise Exception("Not yet supported: devtools_only allowed_contexts")
+
+ if "content_only" in api_ns.allowed_contexts:
+ raise Exception("Not yet supported: content_only allowed_contexts")
+
+ webidl_exposed_attr = ", ".join(["ServiceWorker"])
+
+ webidl_definition_body = cls.to_webidl_definition_body(api_ns)
+ return {
+ "api_namespace": api_ns.name,
+ "webidl_description_comment": webidl_description_comment,
+ "webidl_name": webidl_name,
+ "webidl_exposed_attr": webidl_exposed_attr,
+ "webidl_definition_body": webidl_definition_body,
+ }
+
+ @classmethod
+ def to_webidl_definition_body(cls, api_ns):
+ """
+ Generate the body of an API namespace webidl definition.
+ """
+
+ cls.expect_instance(api_ns, APINamespace)
+
+ body = []
+
+ # TODO: once we are going to expose the webidl bindings to
+ # content scripts we should generate a separate definition
+ # for the content_only parts of the API namespaces and make
+ # them part of a separate `ExtensionContent<APINamespace>`
+ # webidl interface (e.g. `ExtensionContentUserScripts` would
+ # contain only the part of the userScripts API namespace that
+ # should be available to the content scripts globals.
+ def should_include(api_entry):
+ if isinstance(
+ api_entry, APIFunction
+ ) and WebIDLHelpers.webext_method_hidden_in_worker(api_entry):
+ return False
+ if api_entry.is_mv2_only:
+ return False
+ return "content_only" not in api_entry.get_allowed_contexts()
+
+ webidl_functions = [
+ cls.to_webidl_method(v)
+ for v in api_ns.functions.values()
+ if should_include(v)
+ ]
+ if len(webidl_functions) > 0:
+ body = body + ["\n // API methods.\n", "\n\n".join(webidl_functions)]
+
+ webidl_events = [
+ cls.to_webidl_event_property(v)
+ for v in api_ns.events.values()
+ if should_include(v)
+ ]
+ if len(webidl_events) > 0:
+ body = body + ["\n // API events.\n", "\n\n".join(webidl_events)]
+
+ webidl_props = [
+ cls.to_webidl_property(v)
+ for v in api_ns.properties.values()
+ if should_include(v)
+ ]
+ if len(webidl_props) > 0:
+ body = body + ["\n // API properties.\n", "\n\n".join(webidl_props)]
+
+ webidl_child_ns = [
+ cls.to_webidl_namespace_property(v)
+ for v in api_ns.get_child_namespaces()
+ if should_include(v)
+ ]
+ if len(webidl_child_ns) > 0:
+ body = body + [
+ "\n // API child namespaces.\n",
+ "\n\n".join(webidl_child_ns),
+ ]
+
+ return "\n".join(body)
+
+ @classmethod
+ def to_webidl_namespace_property(cls, api_ns):
+ """
+ Generate the webidl fragment for a child APINamespace property (an
+ API namespace included in a parent API namespace, e.g. `devtools.panels`
+ is a child namespace for `devtools` and `privacy.network` is a child
+ namespace for `privacy`).
+ """
+
+ cls.expect_instance(api_ns, APINamespace)
+
+ # TODO: at the moment this method is not yet checking if an entry should
+ # be wrapped into build time macros in the generated webidl definitions
+ # (as done for methods and event properties).
+ #
+ # We may look into it if there is any property that needs this
+ # (at the moment it seems that we may defer it)
+
+ prop_name = api_ns.name[api_ns.name.find(".") + 1 :]
+ prop_type = WebIDLHelpers.to_webidl_definition_name(api_ns.name)
+ attrs = [
+ "Replaceable",
+ "SameObject",
+ 'BinaryName="Get%s"' % prop_type,
+ 'Func="mozilla::extensions::%s::IsAllowed' % prop_type,
+ ]
+
+ lines = [
+ " [%s]" % ", ".join(attrs),
+ " readonly attribute %s %s;" % (prop_type, prop_name),
+ ]
+
+ return "\n".join(lines)
+
+ @classmethod
+ def to_webidl_definition(cls, api_entry, schema_group):
+ """
+ Convert a API namespace or entry class instance into its webidl
+ definition.
+ """
+
+ if isinstance(api_entry, APINamespace):
+ return cls.namespace_to_webidl_definition(api_entry, schema_group)
+ if isinstance(api_entry, APIFunction):
+ return cls.to_webidl_method(api_entry, schema_group)
+ if isinstance(api_entry, APIProperty):
+ return cls.to_webidl_property(api_entry, schema_group)
+ if isinstance(api_entry, APIEvent):
+ return cls.to_webidl_event_property(api_entry, schema_group)
+ if isinstance(api_entry, APIType):
+ # return None for APIType instances, which are currently not being
+ # turned into webidl definitions.
+ return None
+
+ raise Exception("Unknown api_entry type: %s" % api_entry)
+
+ @classmethod
+ def to_webidl_property(cls, api_property, schema_group=None):
+ """
+ Returns the WebIDL fragment for the given APIProperty entry to be included
+ in the body of a WebExtension API namespace webidl definition.
+ """
+
+ cls.expect_instance(api_property, APIProperty)
+
+ # TODO: at the moment this method is not yet checking if an entry should
+ # be wrapped into build time macros in the generated webidl definitions
+ # (as done for methods and event properties).
+ #
+ # We may look into it if there is any property that needs this
+ # (at the moment it seems that we may defer it)
+
+ attrs = ["Replaceable"]
+
+ schema_data = api_property.get_schema_data(schema_group)
+ proptype = cls.webidl_type_from_mapping(
+ schema_data, "%s property type" % api_property.api_path_string
+ )
+
+ lines = [
+ " [%s]" % ", ".join(attrs),
+ " readonly attribute %s %s;" % (proptype, api_property.name),
+ ]
+
+ return "\n".join(lines)
+
+ @classmethod
+ def to_webidl_event_property(cls, api_event, schema_group=None):
+ """
+ Returns the WebIDL fragment for the given APIEvent entry to be included
+ in the body of a WebExtension API namespace webidl definition.
+ """
+
+ cls.expect_instance(api_event, APIEvent)
+
+ def generate_webidl(group):
+ # Empty if the event doesn't exist in the given schema_group.
+ if group and group not in api_event.schema_groups:
+ return ""
+ attrs = ["Replaceable", "SameObject"]
+ return "\n".join(
+ [
+ " [%s]" % ", ".join(attrs),
+ " readonly attribute ExtensionEventManager %s;" % api_event.name,
+ ]
+ )
+
+ if schema_group is not None:
+ return generate_webidl(schema_group)
+
+ return cls.maybe_wrap_in_buildtime_macros(api_event, generate_webidl)
+
+ @classmethod
+ def to_webidl_method(cls, api_fun, schema_group=None):
+ """
+ Returns the WebIDL definition for the given APIFunction entry to be included
+ in the body of a WebExtension API namespace webidl definition.
+ """
+
+ cls.expect_instance(api_fun, APIFunction)
+
+ def generate_webidl(group):
+ attrs = ["Throws"]
+ stub_attr = cls.webext_method_stub(api_fun, group)
+ if stub_attr:
+ attrs = attrs + [stub_attr]
+ retval_type = cls.webidl_method_retval_type(api_fun, group)
+ lines = []
+ for fn_params in api_fun.iter_multiple_webidl_signatures_params(group):
+ params = ", ".join(cls.webidl_method_params(api_fun, group, fn_params))
+ lines.extend(
+ [
+ " [%s]" % ", ".join(attrs),
+ " %s %s(%s);" % (retval_type, api_fun.name, params),
+ ]
+ )
+ return "\n".join(lines)
+
+ if schema_group is not None:
+ return generate_webidl(schema_group)
+
+ return cls.maybe_wrap_in_buildtime_macros(api_fun, generate_webidl)
+
+ @classmethod
+ def maybe_wrap_in_buildtime_macros(cls, api_entry, generate_webidl_fn):
+ """
+ Wrap the generated webidl content into buildtime macros if there are
+ differences between Android and Desktop JSON schema that turns into
+ different webidl definitions.
+ """
+
+ browser_webidl = None
+ mobile_webidl = None
+
+ if api_entry.in_browser:
+ browser_webidl = generate_webidl_fn("browser")
+ elif api_entry.in_toolkit:
+ browser_webidl = generate_webidl_fn("toolkit")
+
+ if api_entry.in_mobile:
+ mobile_webidl = generate_webidl_fn("mobile")
+
+ # Generate a method signature surrounded by `#if defined(ANDROID)` macros
+ # to conditionally exclude APIs that are not meant to be available in
+ # Android builds.
+ if api_entry.in_browser and not api_entry.in_mobile:
+ return "#if !defined(ANDROID)\n%s\n#endif" % browser_webidl
+
+ # NOTE: at the moment none of the API seems to be exposed on mobile but
+ # not on desktop.
+ if api_entry.in_mobile and not api_entry.in_browser:
+ return "#if defined(ANDROID)\n%s\n#endif" % mobile_webidl
+
+ # NOTE: at the moment none of the API seems to be available in both
+ # mobile and desktop builds and have different webidl signature
+ # (at least until not all method param types are converted into non-any
+ # webidl type signatures)
+ if browser_webidl != mobile_webidl and mobile_webidl is not None:
+ return "#if defined(ANDROID)\n%s\n#else\n%s\n#endif" % (
+ mobile_webidl,
+ browser_webidl,
+ )
+
+ return browser_webidl
+
+ @classmethod
+ def webext_method_hidden_in_worker(cls, api_fun, schema_group=None):
+ """
+ Determine if a method should be hidden in the generated webidl
+ for a worker global.
+ """
+ cls.expect_instance(api_fun, APIFunction)
+ api_path = ".".join([*api_fun.path])
+ return api_path in WEBEXT_WORKER_HIDDEN_SET
+
+ @classmethod
+ def webext_method_stub(cls, api_fun, schema_group=None):
+ """
+ Returns the WebExtensionStub WebIDL extended attribute for the given APIFunction.
+ """
+
+ cls.expect_instance(api_fun, APIFunction)
+
+ stub = "WebExtensionStub"
+
+ api_path = ".".join([*api_fun.path])
+
+ if api_path in WEBEXT_STUBS_MAPPING:
+ logging.debug("Looking for %s in WEBEXT_STUBS_MAPPING", api_path)
+ # if the stub config for a given api_path is a boolean, then do not stub the
+ # method if it is set to False and use the default one if set to true.
+ if isinstance(WEBEXT_STUBS_MAPPING[api_path], bool):
+ if not WEBEXT_STUBS_MAPPING[api_path]:
+ return ""
+ else:
+ return "%s" % stub
+ return '%s="%s"' % (stub, WEBEXT_STUBS_MAPPING[api_path])
+
+ schema_data = api_fun.get_schema_data(schema_group)
+
+ is_ambiguous = False
+ if "allowAmbiguousOptionalArguments" in schema_data:
+ is_ambiguous = True
+
+ if api_fun.is_async():
+ if is_ambiguous:
+ # Specialized stub for async methods with ambiguous args.
+ return '%s="AsyncAmbiguous"' % stub
+ return '%s="Async"' % stub
+
+ if "returns" in schema_data:
+ # If the method requires special handling just add it to
+ # the WEBEXT_STUBS_MAPPING table.
+ return stub
+
+ return '%s="NoReturn"' % stub
+
+ @classmethod
+ def webidl_method_retval_type(cls, api_fun, schema_group=None):
+ """
+ Return the webidl return value type for the given `APIFunction` entry.
+
+ If the JSON schema for the method is not marked as asynchronous and
+ there is a `returns` schema property, the return type will be defined
+ from it (See WebIDLHelpers.webidl_type_from_mapping for more info about
+ the type mapping).
+ """
+
+ cls.expect_instance(api_fun, APIFunction)
+
+ if api_fun.is_async(schema_group):
+ # webidl signature for the Async methods will return any, then
+ # the implementation will return a Promise if no callback was passed
+ # to the method and undefined if the optional chrome compatible callback
+ # was passed as a parameter.
+ return "any"
+
+ schema_data = api_fun.get_schema_data(schema_group)
+ if "returns" in schema_data:
+ return cls.webidl_type_from_mapping(
+ schema_data["returns"], "%s return value" % api_fun.api_path_string
+ )
+
+ return "undefined"
+
+ @classmethod
+ def webidl_method_params(cls, api_fun, schema_group=None, params_schema_data=None):
+ """
+ Return the webidl method parameters for the given `APIFunction` entry.
+
+ If the schema for the function includes `allowAmbiguousOptionalArguments`
+ then the methods paramers are going to be the variadic arguments of type
+ `any` (e.g. `undefined myMethod(any... args);`).
+
+ If params_schema_data is None, then the parameters will be resolved internally
+ from the schema data.
+ """
+
+ cls.expect_instance(api_fun, APIFunction)
+
+ params = []
+
+ schema_data = api_fun.get_schema_data(schema_group)
+
+ # Use a variadic positional argument if the methods allows
+ # ambiguous optional arguments.
+ #
+ # The ambiguous mapping is currently used for:
+ #
+ # - API methods that have an allowAmbiguousOptionalArguments
+ # property in their JSONSchema definition
+ # (e.g. browser.runtime.sendMessage)
+ #
+ # - API methods for which the currently autogenerated
+ # methods are not all distinguishable from a WebIDL
+ # parser perspective
+ # (e.g. scripting.getRegisteredContentScripts and
+ # scripting.unregisterContentScripts, where
+ # `any filter, optional Function` and `optional Function`
+ # are not distinguishable when called with a single
+ # parameter set to an undefined value).
+ if api_fun.has_ambiguous_stub_mapping(schema_group):
+ return ["any... args"]
+
+ if params_schema_data is None:
+ if "parameters" in schema_data:
+ params_schema_data = schema_data["parameters"]
+ else:
+ params_schema_data = []
+
+ for param in params_schema_data:
+ is_optional = "optional" in param and param["optional"]
+
+ if (
+ api_fun.is_async(schema_group)
+ and schema_data["async"] == param["name"]
+ and schema_data["parameters"][-1] == param
+ ):
+ # the last async callback parameter is validated and added later
+ # in this method.
+ continue
+
+ api_path = api_fun.api_path_string
+ pname = param["name"]
+ ptype = cls.webidl_type_from_mapping(
+ param, f"{api_path} method parameter {pname}"
+ )
+
+ if (
+ ptype != "any"
+ and not cls.webidl_type_is_primitive(ptype)
+ and is_optional
+ ):
+ if ptype != "Function":
+ raise TypeError(
+ f"unexpected optional type: '{ptype}'. "
+ f"Only Function is expected to be marked as optional: '{api_path}' parameter '{pname}'"
+ )
+ ptype = f"optional {ptype}"
+
+ params.append(f"{ptype} {pname}")
+
+ if api_fun.is_async(schema_group):
+ # Add the chrome-compatible callback as an additional optional parameter
+ # when the method is async.
+ #
+ # The parameter name will be "callback" (default) or the custom one set in
+ # the schema data (`get_sync_callback_name` also validates the consistency
+ # of the schema data for the callback parameter and throws if the expected
+ # parameter is missing).
+ params.append(
+ f"optional Function {api_fun.get_async_callback_name(schema_group)}"
+ )
+
+ return params
+
+ @classmethod
+ def webidl_type_is_primitive(cls, webidl_type):
+ return webidl_type in WEBIDL_PRIMITIVE_TYPES
+
+ @classmethod
+ def webidl_type_from_mapping(cls, schema_data, where_info):
+ """
+ Return the WebIDL type related to the given `schema_data`.
+
+ The JSON schema type is going to be derived from:
+ - `type` and `isInstanceOf` properties
+ - or `$ref` property
+
+ and then converted into the related WebIDL type using the
+ `WEBEXT_TYPES_MAPPING` table.
+
+ The caller needs also specify where the type mapping
+ where meant to be used in form of an arbitrary string
+ passed through the `where_info` parameter, which is
+ only used to log a more detailed debug message for types
+ there couldn't be resolved from the schema data.
+
+ Returns `any` if no special mapping has been found.
+ """
+
+ if "type" in schema_data:
+ if (
+ "isInstanceOf" in schema_data
+ and schema_data["isInstanceOf"] in WEBEXT_TYPES_MAPPING
+ ):
+ schema_type = schema_data["isInstanceOf"]
+ else:
+ schema_type = schema_data["type"]
+ elif "$ref" in schema_data:
+ schema_type = schema_data["$ref"]
+ else:
+ logging.info(
+ "%s %s. %s: %s",
+ "Falling back to webidl type 'any' for",
+ where_info,
+ "Unable to get a schema_type from schema data",
+ json.dumps(schema_data, indent=True),
+ )
+ return "any"
+
+ if schema_type in WEBEXT_TYPES_MAPPING:
+ return WEBEXT_TYPES_MAPPING[schema_type]
+
+ logging.warning(
+ "%s %s. %s: %s",
+ "Falling back to webidl type 'any' for",
+ where_info,
+ "No type mapping found in WEBEXT_TYPES_MAPPING for schema_type",
+ schema_type,
+ )
+
+ return "any"
+
+
+class APIEntry:
+ """
+ Base class for the classes that represents the JSON schema data.
+ """
+
+ def __init__(self, parent, name, ns_path):
+ self.parent = parent
+ self.root = parent.root
+ self.name = name
+ self.path = [*ns_path, name]
+
+ self.schema_data_list = []
+ self.schema_data_by_group = DefaultDict(lambda _: [])
+
+ def add_schema(self, schema_data, schema_group):
+ """
+ Add schema data loaded from a specific group of schema files.
+
+ Each entry may have more than one schema_data coming from a different group
+ of schema files, but only one entry per schema group is currently expected
+ and a TypeError is going to raised if this assumption is violated.
+
+ NOTE: entries part of the 'manifest' are expected to have more than one schema_data
+ coming from the same group of schema files, but it doesn't represent any actual
+ API namespace and so we can ignore them for the purpose of generating the WebIDL
+ definitions.
+ """
+
+ self.schema_data_by_group.getOrCreate(schema_group).append(schema_data)
+
+ # If the new schema_data is deep equal to an existing one
+ # don't bother adding it even if it was in a different schema_group.
+ if schema_data not in self.schema_data_list:
+ self.schema_data_list.append(schema_data)
+
+ in_manifest_namespace = self.api_path_string.startswith("manifest.")
+
+ # Raise an error if we do have multiple schema entries for the same
+ # schema group, but skip it for the "manifest" namespace because it.
+ # is expected for it to have multiple schema data entries for the
+ # same type and at the moment we don't even use that namespace to
+ # generate and webidl definitions.
+ if (
+ not in_manifest_namespace
+ and len(self.schema_data_by_group[schema_group]) > 1
+ ):
+ raise TypeError(
+ 'Unxpected multiple schema data for API property "%s" in schema group %s'
+ % (self.api_path_string, schema_group)
+ )
+
+ def get_allowed_contexts(self, schema_group=None):
+ """
+ Return the allowed contexts for this API entry, or the default contexts from its
+ parent entry otherwise.
+ """
+
+ if schema_group is not None:
+ if schema_group not in self.schema_data_by_group:
+ return []
+ if "allowedContexts" in self.schema_data_by_group[schema_group]:
+ return self.schema_data_by_group[schema_group]["allowedContexts"]
+ else:
+ if "allowedContexts" in self.schema_data_list[0]:
+ return self.schema_data_list[0]["allowedContexts"]
+
+ if self.parent:
+ return self.parent.default_contexts
+
+ return []
+
+ @property
+ def schema_groups(self):
+ """List of the schema groups that have schema data for this entry."""
+ return [*self.schema_data_by_group.keys()]
+
+ @property
+ def in_toolkit(self):
+ """Whether the API entry is defined by toolkit schemas."""
+ return "toolkit" in self.schema_groups
+
+ @property
+ def in_browser(self):
+ """Whether the API entry is defined by browser schemas."""
+ return "browser" in self.schema_groups
+
+ @property
+ def in_mobile(self):
+ """Whether the API entry is defined by mobile schemas."""
+ return "mobile" in self.schema_groups
+
+ @property
+ def is_mv2_only(self):
+ # Each API entry should not have multiple max_manifest_version property
+ # conflicting with each other (even if there is schema data coming from multiple
+ # JSONSchema files, eg. when a base toolkit schema definition is extended by additional
+ # schema data on Desktop or Mobile), and so here we just iterate over all the schema
+ # data related to this entry and look for the first max_manifest_version property value
+ # we can find if any.
+ for entry in self.schema_data_list:
+ if "max_manifest_version" in entry and entry["max_manifest_version"] < 3:
+ return True
+ return False
+
+ def dump_platform_diff(self, diff_cmd, only_if_webidl_differ):
+ """
+ Dump a diff of the JSON schema data coming from browser and mobile,
+ if the API did have schema data loaded from both these group of schema files.
+ """
+ if len(self.schema_groups) <= 1:
+ return
+
+ # We don't expect any schema data from "toolkit" that we expect to also have
+ # duplicated (and potentially different) schema data in the other groups
+ # of schema data ("browser" and "mobile).
+ #
+ # For the API that are shared but slightly different in the Desktop and Android
+ # builds we expect the schema data to only be placed in the related group of schema
+ # ("browser" and "mobile").
+ #
+ # We throw a TypeError here to detect if that assumption is violated while we are
+ # collecting the platform diffs, while keeping the logic for the generated diff
+ # below simple with the guarantee that we wouldn't get to it if that assumption
+ # is violated.
+ if "toolkit" in self.schema_groups:
+ raise TypeError(
+ "Unexpected diff between toolkit and browser/mobile schema: %s"
+ % self.api_path_string
+ )
+
+ # Compare the webidl signature generated for mobile vs desktop,
+ # generate different signature surrounded by macro if they differ
+ # or only include one if the generated webidl signature would still
+ # be the same.
+ browser_schema_data = self.schema_data_by_group["browser"][0]
+ mobile_schema_data = self.schema_data_by_group["mobile"][0]
+
+ if only_if_webidl_differ:
+ browser_webidl = WebIDLHelpers.to_webidl_definition(self, "browser")
+ mobile_webidl = WebIDLHelpers.to_webidl_definition(self, "mobile")
+
+ if browser_webidl == mobile_webidl:
+ return
+
+ json_diff = run_diff(
+ diff_cmd,
+ "%s-browser" % self.api_path_string,
+ json.dumps(browser_schema_data, indent=True),
+ "%s-mobile" % self.api_path_string,
+ json.dumps(mobile_schema_data, indent=True),
+ always_return_diff_output=False,
+ )
+
+ if len(json_diff.strip()) == 0:
+ return
+
+ # Print a diff of the browser vs. mobile JSON schema.
+ print("\n\n## API schema desktop vs. mobile for %s\n\n" % self.api_path_string)
+ print("```diff\n%s\n```" % json_diff)
+
+ def get_schema_data(self, schema_group=None):
+ """
+ Get schema data loaded for this entry (optionally from a specific group
+ of schema files).
+ """
+ if schema_group is None:
+ return self.schema_data_list[0]
+ return self.schema_data_by_group[schema_group][0]
+
+ @property
+ def api_path_string(self):
+ """Convert the path list into the full namespace string."""
+ return ".".join(self.path)
+
+
+class APIType(APIEntry):
+ """Class to represent an API type"""
+
+
+class APIProperty(APIEntry):
+ """Class to represent an API property"""
+
+
+class APIEvent(APIEntry):
+ """Class to represent an API Event"""
+
+
+class APIFunction(APIEntry):
+ """Class to represent an API function"""
+
+ def is_async(self, schema_group=None):
+ """
+ Returns True is the APIFunction is marked as asynchronous in its schema data.
+ """
+ schema_data = self.get_schema_data(schema_group)
+ return "async" in schema_data
+
+ def is_optional_param(self, param):
+ return "optional" in param and param["optional"]
+
+ def is_callback_param(self, param, schema_group=None):
+ return self.is_async(schema_group) and (
+ param["name"] == self.get_async_callback_name(schema_group)
+ )
+
+ def iter_multiple_webidl_signatures_params(self, schema_group=None):
+ """
+ Lazily generate the parameters set to use in the multiple webidl definitions
+ that should be generated by this method, due to a set of optional parameters
+ followed by a mandatory one.
+
+ NOTE: the caller SHOULD NOT mutate (or save for later use) the list of parameters
+ yielded by this generator function (because the parameters list and parameters
+ are not deep cloned and reused internally between yielded values).
+ """
+ schema_data = self.get_schema_data(schema_group)
+ parameters = schema_data["parameters"].copy()
+ yield parameters
+
+ if not self.has_multiple_webidl_signatures(schema_group):
+ return
+
+ def get_next_idx(p):
+ return parameters.index(p) + 1
+
+ def get_next_rest(p):
+ return parameters[get_next_idx(p) : :]
+
+ def is_optional(p):
+ return self.is_optional_param(p)
+
+ def is_mandatory(p):
+ return not is_optional(p)
+
+ rest = parameters
+ while not all(is_mandatory(param) for param in rest):
+ param = next(filter(is_optional, rest))
+ rest = get_next_rest(param)
+ if self.is_callback_param(param, schema_group):
+ return
+
+ parameters.remove(param)
+ yield parameters
+
+ def has_ambiguous_stub_mapping(self, schema_group):
+ # Determine if the API should be using the AsyncAmbiguous
+ # stub method per its JSONSchema data.
+ schema_data = self.get_schema_data(schema_group)
+ is_ambiguous = False
+ if "allowAmbiguousOptionalArguments" in schema_data:
+ is_ambiguous = True
+
+ if not is_ambiguous:
+ # Determine if the API should be using the AsyncAmbiguous
+ # stub method per configuration set from ExtensionWebIDL.conf.
+ api_path = ".".join([*self.path])
+ if api_path in WEBEXT_STUBS_MAPPING:
+ return WEBEXT_STUBS_MAPPING[api_path] == "AsyncAmbiguous"
+
+ return is_ambiguous
+
+ def has_multiple_webidl_signatures(self, schema_group=None):
+ """
+ Determine if the API method in the JSONSchema needs to be turned in
+ multiple function signatures in the WebIDL definitions (e.g. `alarms.create`,
+ needs two separate WebIDL definitions accepting 1 and 2 parameters to match the
+ expected behaviors).
+ """
+
+ if self.has_ambiguous_stub_mapping(schema_group):
+ # The few methods that are marked as ambiguous (only runtime.sendMessage,
+ # besides the ones in the `test` API) are currently generated as
+ # a single webidl method with a variadic parameter.
+ return False
+
+ schema_data = self.get_schema_data(schema_group)
+ params = schema_data["parameters"] or []
+
+ return not all(not self.is_optional_param(param) for param in params)
+
+ def get_async_callback_name(self, schema_group):
+ """
+ Get the async callback name, or raise a TypeError if inconsistencies are detected
+ in the schema data related to the expected callback parameter.
+ """
+ # For an async method we expect the "async" keyword to be either
+ # set to `true` or to a callback name, in which case we expect
+ # to have a callback parameter with the same name as the last
+ # of the function schema parameters:
+ schema_data = self.get_schema_data(schema_group)
+ if "async" not in schema_data or schema_data["async"] is False:
+ raise TypeError("%s schema is not an async function" % self.api_path_string)
+
+ if isinstance(schema_data["async"], str):
+ cb_name = schema_data["async"]
+ if "parameters" not in schema_data or not schema_data["parameters"]:
+ raise TypeError(
+ "%s is missing a parameter definition for async callback %s"
+ % (self.api_path_string, cb_name)
+ )
+
+ last_param = schema_data["parameters"][-1]
+ if last_param["name"] != cb_name or last_param["type"] != "function":
+ raise TypeError(
+ "%s is missing a parameter definition for async callback %s"
+ % (self.api_path_string, cb_name)
+ )
+ return cb_name
+
+ # default callback name on `"async": true` in the schema data.
+ return "callback"
+
+
+class APINamespace:
+ """Class to represent an API namespace"""
+
+ def __init__(self, root, name, ns_path):
+ self.root = root
+ self.name = name
+ if name:
+ self.path = [*ns_path, name]
+ else:
+ self.path = [*ns_path]
+
+ # All the schema data collected for this namespace across all the
+ # json schema files loaded, grouped by the schem_group they are being
+ # loaded from ('toolkit', 'desktop', mobile').
+ self.schema_data_by_group = DefaultDict(lambda _: [])
+
+ # class properties populated by parse_schemas.
+
+ self.max_manifest_version = None
+ self.permissions = set()
+ self.allowed_contexts = set()
+ self.default_contexts = set()
+
+ self.types = DefaultDict(lambda type_id: APIType(self, type_id, self.path))
+ self.properties = DefaultDict(
+ lambda prop_id: APIProperty(self, prop_id, self.path)
+ )
+ self.functions = DefaultDict(
+ lambda fn_name: APIFunction(self, fn_name, self.path)
+ )
+ self.events = DefaultDict(
+ lambda event_name: APIEvent(self, event_name, self.path)
+ )
+
+ def get_allowed_contexts(self):
+ """
+ Return the allowed contexts for this API namespace
+ """
+ return self.allowed_contexts
+
+ @property
+ def schema_groups(self):
+ """List of the schema groups that have schema data for this entry."""
+ return [*self.schema_data_by_group.keys()]
+
+ @property
+ def in_toolkit(self):
+ """Whether the API entry is defined by toolkit schemas."""
+ return "toolkit" in self.schema_groups
+
+ @property
+ def in_browser(self):
+ """Whether the API entry is defined by browser schemas."""
+ return "browser" in self.schema_groups
+
+ @property
+ def in_mobile(self):
+ """Whether the API entry is defined by mobile schemas."""
+ if self.name in WEBEXT_ANDROID_EXCLUDED:
+ return False
+ return "mobile" in self.schema_groups
+
+ @property
+ def is_mv2_only(self):
+ return self.max_manifest_version == 2
+
+ @property
+ def api_path_string(self):
+ """Convert the path list into the full namespace string."""
+ return ".".join(self.path)
+
+ def add_schema(self, schema_data, schema_group):
+ """Add schema data loaded from a specific group of schema files."""
+ self.schema_data_by_group.getOrCreate(schema_group).append(schema_data)
+
+ def parse_schemas(self):
+ """Parse all the schema data collected (from all schema groups)."""
+ for schema_group, schema_data in self.schema_data_by_group.items():
+ self._parse_schema_data(schema_data, schema_group)
+
+ def _parse_schema_data(self, schema_data, schema_group):
+ for data in schema_data:
+ # TODO: we should actually don't merge together permissions and
+ # allowedContext/defaultContext, because in some cases the schema files
+ # are split in two when only part of the API is available to the
+ # content scripts.
+
+ # load permissions, allowed_contexts and default_contexts
+ if "permissions" in data:
+ self.permissions.update(data["permissions"])
+ if "allowedContexts" in data:
+ self.allowed_contexts.update(data["allowedContexts"])
+ if "defaultContexts" in data:
+ self.default_contexts.update(data["defaultContexts"])
+ if "max_manifest_version" in data:
+ if (
+ self.max_manifest_version is not None
+ and self.max_manifest_version != data["max_manifest_version"]
+ ):
+ raise TypeError(
+ "Error loading schema data - overwriting existing max_manifest_version"
+ " value\n\tPrevious max_manifest_version set: %s\n\tschema_group: %s"
+ "\n\tschema_data: %s"
+ % (self.max_manifest_version, schema_group, schema_data)
+ )
+ self.max_manifest_version = data["max_manifest_version"]
+
+ api_path = self.api_path_string
+
+ # load types
+ if "types" in data:
+ for type_data in data["types"]:
+ type_id = None
+ if "id" in type_data:
+ type_id = type_data["id"]
+ elif "$extend" in type_data:
+ type_id = type_data["$extend"]
+ elif "unsupported" in type_data:
+ # No need to raise an error for an unsupported type
+ # it will ignored below before adding it to the map
+ # of the namespace types.
+ pass
+ else:
+ # Supported entries without an "id" or "$extend"
+ # property are unexpected, log a warning and
+ # fail explicitly if that happens to be the case.
+ logging.critical(
+ "Error loading schema data type from '%s %s': %s",
+ schema_group,
+ api_path,
+ json.dumps(type_data, indent=True),
+ )
+ raise TypeError(
+ "Error loading schema type data defined in '%s %s'"
+ % (schema_group, api_path),
+ )
+
+ if "unsupported" in type_data:
+ # Skip unsupported type.
+ logging.debug(
+ "Skipping unsupported type '%s'",
+ "%s %s.%s" % (schema_group, api_path, type_id),
+ )
+ continue
+
+ assert type_id
+ type_entry = self.types.getOrCreate(type_id)
+ type_entry.add_schema(type_data, schema_group)
+
+ # load properties
+ if "properties" in data:
+ for prop_id, prop_data in data["properties"].items():
+ # Skip unsupported type.
+ if "unsupported" in prop_data:
+ logging.debug(
+ "Skipping unsupported property '%s'",
+ "%s %s.%s" % (schema_group, api_path, prop_id),
+ )
+ continue
+ prop_entry = self.properties.getOrCreate(prop_id)
+ prop_entry.add_schema(prop_data, schema_group)
+
+ # load functions
+ if "functions" in data:
+ for func_data in data["functions"]:
+ func_name = func_data["name"]
+ # Skip unsupported function.
+ if "unsupported" in func_data:
+ logging.debug(
+ "Skipping unsupported function '%s'",
+ "%s %s.%s" % (schema_group, api_path, func_name),
+ )
+ continue
+ func_entry = self.functions.getOrCreate(func_name)
+ func_entry.add_schema(func_data, schema_group)
+
+ # load events
+ if "events" in data:
+ for event_data in data["events"]:
+ event_name = event_data["name"]
+ # Skip unsupported function.
+ if "unsupported" in event_data:
+ logging.debug(
+ "Skipping unsupported event: '%s'",
+ "%s %s.%s" % (schema_group, api_path, event_name),
+ )
+ continue
+ event_entry = self.events.getOrCreate(event_name)
+ event_entry.add_schema(event_data, schema_group)
+
+ def get_child_namespace_names(self):
+ """Returns the list of child namespaces for the current namespace"""
+
+ # some API namespaces may contains other namespaces
+ # e.g. 'devtools' does contain 'devtools.inspectedWindow',
+ # 'devtools.panels' etc.
+ return [
+ ns
+ for ns in self.root.get_all_namespace_names()
+ if ns.startswith(self.name + ".")
+ ]
+
+ def get_child_namespaces(self):
+ """Returns all the APINamespace instances for the child namespaces"""
+ return [
+ self.root.get_namespace(name) for name in self.get_child_namespace_names()
+ ]
+
+ def get_boilerplate_cpp_header(self):
+ template = self.root.jinja_env.get_template("ExtensionAPI.h.in")
+ webidl_props = WebIDLHelpers.to_template_props(self)
+ return template.render(
+ {"webidl_name": webidl_props["webidl_name"], "api_namespace": self.name}
+ )
+
+ def get_boilerplate_cpp(self):
+ template = self.root.jinja_env.get_template("ExtensionAPI.cpp.in")
+ webidl_props = WebIDLHelpers.to_template_props(self)
+ return template.render(
+ {"webidl_name": webidl_props["webidl_name"], "api_namespace": self.name}
+ )
+
+ def dump(self, schema_group=None):
+ """
+ Used by the --dump-namespaces-info flag to dump some info
+ for a given namespace based on all the schema files loaded.
+ """
+
+ def get_entry_names_by_group(values):
+ res = {"both": [], "mobile": [], "browser": []}
+ for item in values:
+ if item.in_toolkit or (item.in_browser and item.in_mobile):
+ res["both"].append(item.name)
+ elif item.in_browser and not item.in_mobile:
+ res["browser"].append(item.name)
+ elif item.in_mobile and not item.in_desktop:
+ res["mobile"].append(item.name)
+ return res
+
+ def dump_names_by_group(values):
+ entries_map = get_entry_names_by_group(values)
+ print(" both: %s" % entries_map["both"])
+ print(" only on desktop: %s" % entries_map["browser"])
+ print(" only on mobile: %s" % entries_map["mobile"])
+
+ if schema_group is not None and [schema_group] != self.schema_groups:
+ return
+
+ print("\n## %s\n" % self.name)
+
+ print("schema groups: ", self.schema_groups)
+ print("max manifest version: ", self.max_manifest_version)
+ print("permissions: ", self.permissions)
+ print("allowed contexts: ", self.allowed_contexts)
+ print("default contexts: ", self.default_contexts)
+
+ print("functions:")
+ dump_names_by_group(self.functions.values())
+ fn_multi_signatures = list(
+ filter(
+ lambda fn: fn.has_multiple_webidl_signatures(), self.functions.values()
+ )
+ )
+ if len(fn_multi_signatures) > 0:
+ print("functions with multiple WebIDL type signatures:")
+ for fn in fn_multi_signatures:
+ print(" -", fn.name)
+ for params in fn.iter_multiple_webidl_signatures_params():
+ print(" -", params)
+
+ print("events:")
+ dump_names_by_group(self.events.values())
+ print("properties:")
+ dump_names_by_group(self.properties.values())
+ print("types:")
+ dump_names_by_group(self.types.values())
+
+ print("child namespaces:")
+ dump_names_by_group(self.get_child_namespaces())
+
+
+class Schemas:
+ """Helper class used to load and parse all the schema files"""
+
+ def __init__(self):
+ self.json_schemas = dict()
+ self.api_namespaces = DefaultDict(lambda name: APINamespace(self, name, []))
+ self.jinja_env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader(BASE_DIR),
+ )
+
+ def load_schemas(self, schema_dir_path, schema_group):
+ """
+ Helper function used to read all WebExtensions API schema JSON files
+ from a given directory.
+ """
+ for file_name in os.listdir(schema_dir_path):
+ if file_name.endswith(".json"):
+ full_path = os.path.join(schema_dir_path, file_name)
+ rel_path = os.path.relpath(full_path, buildconfig.topsrcdir)
+
+ logging.debug("Loading schema file %s", rel_path)
+
+ schema_data = read_json(full_path)
+ self.json_schemas[full_path] = schema_data
+
+ for schema_data_entry in schema_data:
+ name = schema_data_entry["namespace"]
+ # Validate the schema while loading them.
+ WebExtAPIValidator.check_schema(schema_data_entry)
+
+ api_ns = self.api_namespaces.getOrCreate(name)
+ api_ns.add_schema(schema_data_entry, schema_group)
+ self.api_namespaces[name] = api_ns
+
+ def get_all_namespace_names(self):
+ """
+ Return an array of all namespace names
+ """
+ return [*self.api_namespaces.keys()]
+
+ def parse_schemas(self):
+ """
+ Helper function used to parse all the collected API schemas.
+ """
+ for api_ns in self.api_namespaces.values():
+ api_ns.parse_schemas()
+
+ def get_namespace(self, name):
+ """
+ Return a APINamespace instance for the given api name.
+ """
+ return self.api_namespaces[name]
+
+ def dump_namespaces(self):
+ """
+ Dump all namespaces collected to stdout.
+ """
+ print(self.get_all_namespace_names())
+
+ def dump(self):
+ """
+ Dump all collected schema to stdout.
+ """
+ print(json.dumps(self.json_schemas, indent=True))
+
+
+def parse_command_and_args():
+ parser = argparse.ArgumentParser()
+
+ # global cli flags shared by all sub-commands.
+ parser.add_argument("--verbose", "-v", action="count", default=0)
+ parser.add_argument(
+ "--diff-command",
+ type=str,
+ metavar="DIFFCMD",
+ help="select the diff command used to generate diffs (defaults to 'diff')",
+ )
+
+ parser.add_argument(
+ "--generate-cpp-boilerplate",
+ action="store_true",
+ help="'generate' command flag to be used to generate cpp boilerplate"
+ + " for the given NAMESPACE",
+ )
+ parser.add_argument(
+ "--overwrite-existing",
+ action="store_true",
+ help="'generate' command flag to be used to allow the script to"
+ + " overwrite existing files (API webidl and cpp boilerplate files)",
+ )
+
+ parser.add_argument(
+ "api_namespaces",
+ type=str,
+ metavar="NAMESPACE",
+ nargs="+",
+ help="WebExtensions API namespaces to generate webidl and cpp boilerplates for",
+ )
+
+ return parser.parse_args()
+
+
+def load_and_parse_JSONSchema():
+ """Load and parse all JSONSchema data"""
+
+ # Initialize Schemas and load all the JSON schema from the directories
+ # listed in WEBEXT_SCHEMADIRS_MAPPING.
+ schemas = Schemas()
+ for schema_group, schema_dir_components in WEBEXT_SCHEMADIRS_MAPPING.items():
+ schema_dir = mozpath.join(buildconfig.topsrcdir, *schema_dir_components)
+ schemas.load_schemas(schema_dir, schema_group)
+
+ # Parse all the schema loaded (which also run some validation based on the
+ # expectations of the code that generates the webidl definitions).
+ schemas.parse_schemas()
+
+ return schemas
+
+
+# Run the 'generate' subcommand which does:
+#
+# - generates the webidl file for the new API
+# - generate boilerplate for the C++ files that implements the new webidl definition
+# - provides details about the rest of steps needed to fully wire up the WebExtensions API
+# in the `browser` and `chrome` globals defined through WebIDL.
+#
+# This command is the entry point for the main feature provided by this scripts.
+def run_generate_command(args, schemas):
+ show_next_steps = False
+
+ for api_ns_str in args.api_namespaces:
+ webidl_name = WebIDLHelpers.to_webidl_definition_name(api_ns_str)
+
+ # Generate webidl definition.
+ webidl_relpath = mozpath.join(WEBIDL_DIR, "%s.webidl" % webidl_name)
+ webidl_abspath = mozpath.join(WEBIDL_DIR_FULLPATH, "%s.webidl" % webidl_name)
+ print(
+ "\nGenerating webidl definition for '%s' => %s"
+ % (api_ns_str, webidl_relpath)
+ )
+ api_ns = schemas.get_namespace(api_ns_str)
+
+ did_wrote_webidl_changes = write_with_overwrite_confirm(
+ relpath=webidl_relpath,
+ abspath=webidl_abspath,
+ newcontent=WebIDLHelpers.to_webidl_definition(api_ns, None),
+ diff_prefix="%s.webidl" % webidl_name,
+ diff_command=args.diff_command,
+ overwrite_existing=args.overwrite_existing,
+ )
+
+ if did_wrote_webidl_changes:
+ show_next_steps = True
+
+ cpp_abspath = mozpath.join(CPP_DIR_FULLPATH, "%s.cpp" % webidl_name)
+ cpp_header_abspath = mozpath.join(CPP_DIR_FULLPATH, "%s.h" % webidl_name)
+
+ cpp_files_exist = os.path.exists(cpp_abspath) and os.path.exists(
+ cpp_header_abspath
+ )
+
+ # Generate c++ boilerplate files if forced by the cli flag or
+ # if the cpp files do not exist yet.
+ if args.generate_cpp_boilerplate or not cpp_files_exist:
+ print(
+ "\nGenerating C++ boilerplate for '%s' => %s.h/.cpp"
+ % (api_ns_str, webidl_name)
+ )
+
+ cpp_relpath = mozpath.join(CPP_DIR, "%s.cpp" % webidl_name)
+ cpp_header_relpath = mozpath.join(CPP_DIR, "%s.h" % webidl_name)
+
+ write_with_overwrite_confirm(
+ relpath=cpp_header_relpath,
+ abspath=cpp_header_abspath,
+ newcontent=api_ns.get_boilerplate_cpp_header(),
+ diff_prefix="%s.h" % webidl_name,
+ diff_command=args.diff_command,
+ overwrite_existing=False,
+ )
+ write_with_overwrite_confirm(
+ relpath=cpp_relpath,
+ abspath=cpp_abspath,
+ newcontent=api_ns.get_boilerplate_cpp(),
+ diff_prefix="%s.cpp" % webidl_name,
+ diff_command=args.diff_command,
+ overwrite_existing=False,
+ )
+
+ if show_next_steps:
+ separator = "-" * 20
+ print(
+ "\n%s\n\n"
+ "NEXT STEPS\n"
+ "==========\n\n"
+ "It is not done yet!!!\n"
+ "%s" % (separator, DOCS_NEXT_STEPS)
+ )
+
+
+def set_logging_level(verbose):
+ """Set the logging level (defaults to WARNING), and increased to
+ INFO or DEBUG based on the verbose counter flag value"""
+ # Increase logging level based on the args.verbose counter flag value.
+ # (Default logging level should include warnings).
+ if verbose == 0:
+ logging_level = "WARNING"
+ elif verbose >= 2:
+ logging_level = "DEBUG"
+ else:
+ logging_level = "INFO"
+ logging.getLogger().setLevel(logging_level)
+ logging.info("Logging level set to %s", logging_level)
+
+
+def main():
+ """Entry point function for this script"""
+
+ args = parse_command_and_args()
+ set_logging_level(args.verbose)
+ schemas = load_and_parse_JSONSchema()
+ run_generate_command(args, schemas)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/toolkit/components/extensions/webidl-api/InspectJSONSchema.py b/toolkit/components/extensions/webidl-api/InspectJSONSchema.py
new file mode 100644
index 0000000000..68c696ef63
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/InspectJSONSchema.py
@@ -0,0 +1,152 @@
+# 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/.
+
+import argparse
+import sys
+
+# Sanity check (ensure the script has been executed through `mach python`).
+try:
+ import buildconfig
+
+ # try to access an existing property to please flake8 linting and as an
+ # additional sanity check.
+ buildconfig.topsrcdir
+except ModuleNotFoundError or AttributeError:
+ print(
+ "This script should be executed using `mach python %s`" % __file__,
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+from GenerateWebIDLBindings import load_and_parse_JSONSchema, set_logging_level
+
+
+def get_args_and_argparser():
+ parser = argparse.ArgumentParser()
+
+ # global cli flags shared by all sub-commands.
+ parser.add_argument("--verbose", "-v", action="count", default=0)
+ parser.add_argument(
+ "--diff-command",
+ type=str,
+ metavar="DIFFCMD",
+ help="select the diff command used to generate diffs (defaults to 'diff')",
+ )
+
+ # --dump-namespaces-list flag (this is also the default for the 'inspect' command
+ # when no other flag is specified).
+ parser.add_argument(
+ "--dump-namespaces-list",
+ action="store_true",
+ help="'inspect' command flag - dump list of all API namespaces defined in all"
+ + " JSONSchema files loaded",
+ )
+
+ # --dump-platform-diffs flag and other sub-flags that can be used with it.
+ parser.add_argument(
+ "--dump-platform-diffs",
+ action="store_true",
+ help="'inspect' command flag - list all APIs with platform specific differences",
+ )
+ parser.add_argument(
+ "--only-if-webidl-diffs",
+ action="store_true",
+ help="'inspect' command flag - limits --dump-platform-diff to APIs with differences"
+ + " in the generated webidl",
+ )
+
+ # --dump-namespaces-info flag and other flags that can be used with it.
+ parser.add_argument(
+ "--dump-namespaces-info",
+ nargs="+",
+ type=str,
+ metavar="NAMESPACE",
+ help="'inspect' command flag - dump data loaded for the given NAMESPACE(s)",
+ )
+ parser.add_argument(
+ "--only-in-schema-group",
+ type=str,
+ metavar="SCHEMAGROUP",
+ help="'inspect' command flag - list api namespace in the given schema group"
+ + " (toolkit, browser or mobile)",
+ )
+
+ args = parser.parse_args()
+
+ return [args, parser]
+
+
+# Run the 'inspect' subcommand: these command (and its cli flags) is useful to
+# inspect the JSONSchema data loaded, which is explicitly useful when debugging
+# or evaluating changes to this scripts (e.g. changes that may be needed if the
+# API namespace definition isn't complete or its generated content has issues).
+def run_inspect_command(args, schemas, parser):
+ # --dump-namespaces-info: print a summary view of all the namespaces available
+ # after loading and parsing all the collected JSON schema files.
+ if args.dump_namespaces_info:
+ if "ALL" in args.dump_namespaces_info:
+ for namespace in schemas.get_all_namespace_names():
+ schemas.get_namespace(namespace).dump(args.only_in_schema_group)
+
+ return
+
+ for namespace in args.dump_namespaces_info:
+ schemas.get_namespace(namespace).dump(args.only_in_schema_group)
+ return
+
+ # --dump-platform-diffs: print diffs for the JSON schema where we detected
+ # differences between the desktop and mobile JSON schema files.
+ if args.dump_platform_diffs:
+ for namespace in schemas.get_all_namespace_names():
+ apiNamespace = schemas.get_namespace(namespace)
+ if len(apiNamespace.schema_groups) <= 1:
+ continue
+ for apiMethod in apiNamespace.functions.values():
+ if len(apiNamespace.schema_groups) <= 1:
+ continue
+ apiMethod.dump_platform_diff(
+ args.diff_command, args.only_if_webidl_diffs
+ )
+ for apiEvent in apiNamespace.events.values():
+ if len(apiEvent.schema_groups) <= 1:
+ continue
+ apiEvent.dump_platform_diff(
+ args.diff_command, args.only_if_webidl_diffs
+ )
+ for apiProperty in apiNamespace.properties.values():
+ if len(apiProperty.schema_groups) <= 1:
+ continue
+ apiProperty.dump_platform_diff(
+ args.diff_command, args.only_if_webidl_diffs
+ )
+ # TODO: ideally we may also want to report differences in the
+ # type definitions, but this requires also some tweaks to adjust
+ # dump_platform_diff expectations and logic.
+ return
+
+ # Dump the list of all known API namespaces based on all the loaded JSONSchema data.
+ if args.dump_namespaces_list:
+ schemas.dump_namespaces()
+ return
+
+ # Print the help message and exit 1 as a fallback.
+ print(
+ "ERROR: No option selected, choose one from the following usage message.\n",
+ file=sys.stderr,
+ )
+ parser.print_help()
+ sys.exit(1)
+
+
+def main():
+ """Entry point function for this script"""
+
+ [args, parser] = get_args_and_argparser()
+ set_logging_level(args.verbose)
+ schemas = load_and_parse_JSONSchema()
+ run_inspect_command(args, schemas, parser)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/toolkit/components/extensions/webidl-api/moz.build b/toolkit/components/extensions/webidl-api/moz.build
new file mode 100644
index 0000000000..027c3d147f
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/moz.build
@@ -0,0 +1,78 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("WebExtensions", "General")
+
+# WebExtensions API objects and request handling internals.
+UNIFIED_SOURCES += [
+ "ExtensionAPIBase.cpp",
+ "ExtensionAPIRequest.cpp",
+ "ExtensionAPIRequestForwarder.cpp",
+ "ExtensionBrowser.cpp",
+ "ExtensionEventListener.cpp",
+ "ExtensionEventManager.cpp",
+ "ExtensionPort.cpp",
+]
+
+EXPORTS.mozilla.extensions += [
+ "ExtensionAPIBase.h",
+ "ExtensionBrowser.h",
+ "ExtensionEventManager.h",
+ "ExtensionPort.h",
+]
+
+# WebExtensions API namespaces.
+UNIFIED_SOURCES += [
+ "ExtensionAlarms.cpp",
+ "ExtensionBrowserSettings.cpp",
+ "ExtensionBrowserSettingsColorManagement.cpp",
+ "ExtensionDns.cpp",
+ "ExtensionProxy.cpp",
+ "ExtensionRuntime.cpp",
+ "ExtensionScripting.cpp",
+ "ExtensionSetting.cpp",
+ "ExtensionTest.cpp",
+]
+
+EXPORTS.mozilla.extensions += [
+ "ExtensionAlarms.h",
+ "ExtensionBrowserSettings.h",
+ "ExtensionBrowserSettingsColorManagement.h",
+ "ExtensionDns.h",
+ "ExtensionProxy.h",
+ "ExtensionRuntime.h",
+ "ExtensionScripting.h",
+ "ExtensionSetting.h",
+ "ExtensionTest.h",
+]
+
+# The following is not a real WebExtensions API, it is a test WebIDL
+# interface that includes a collection of the cases useful to unit
+# test the API request forwarding mechanism without tying it to
+# a specific WebExtensions API.
+UNIFIED_SOURCES += ["ExtensionMockAPI.cpp"]
+EXPORTS.mozilla.extensions += ["ExtensionMockAPI.h"]
+
+# Propagate the build config to be able to use it in souce code preprocessing
+# (used in mozilla::extensions::ExtensionAPIAllowed to disable the webidl
+# bindings in non-nightly builds).
+if CONFIG["MOZ_WEBEXT_WEBIDL_ENABLED"]:
+ DEFINES["MOZ_WEBEXT_WEBIDL_ENABLED"] = True
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/js/xpconnect/src",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Must be defined unconditionally (TC tasks doesn't account for build
+# configs and these tests do not depend on the bindings to be enabled).
+PYTHON_UNITTEST_MANIFESTS += ["test/python.ini"]
+
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/toolkit/components/extensions/webidl-api/test/README.md b/toolkit/components/extensions/webidl-api/test/README.md
new file mode 100644
index 0000000000..14baae0d82
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/README.md
@@ -0,0 +1,60 @@
+pytest test coverage for the GenerateWebIDLBindings.py script
+=============================================================
+
+This directory contains tests for the GenerateWebIDLBindings.py script,
+which is used to parse the WebExtensions APIs schema files and generate
+the corresponding WebIDL definitions.
+
+See ["WebIDL WebExtensions API Bindings" section from the Firefox Developer documentation](https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/webidl_bindings.html)
+for more details about how the script is used, this README covers only how
+this test suite works.
+
+Run tests
+---------
+
+The tests part of this test suite can be executed locally using the following `mach` command:
+
+```
+mach python-test toolkit/components/extensions/webidl-api/test
+```
+
+Write a new test file
+---------------------
+
+To add a new test file to this test suite:
+- create a new python script file named as `test_....py`
+- add the test file to the `python.ini` manifest
+- In the new test file make sure to include:
+ - copyright notes as the other test file in this directory
+ - import the helper module and call its `setup()` method (`setup` makes sure to add
+ the directory where the target script is in the python library paths and the
+ `helpers` module does also enable the code coverage if the environment variable
+ is detected):
+ ```
+ import helpers # Import test helpers module.
+ ...
+
+ helpers.setup()
+ ```
+ - don't forget to call `mozunit.main` at the end of the test file:
+ ```
+ if __name__ == "__main__":
+ mozunit.main()
+ ```
+ - add new test cases by defining new functions named as `test_...`,
+ its parameter are the names of the pytest fixture functions to
+ be passed to the test case:
+ ```
+ def test_something(base_schema, write_jsonschema_fixtures):
+ ...
+ ```
+Create new test fixtures
+------------------------
+
+All the test fixture used by this set of tests are defined in `conftest.py`
+and decorated with `@pytest.fixture`.
+
+See the pytest documentation for more details about how the pytest fixture works:
+- https://docs.pytest.org/en/latest/explanation/fixtures.html
+- https://docs.pytest.org/en/latest/how-to/fixtures.html
+- https://docs.pytest.org/en/latest/reference/fixtures.html
diff --git a/toolkit/components/extensions/webidl-api/test/conftest.py b/toolkit/components/extensions/webidl-api/test/conftest.py
new file mode 100644
index 0000000000..1e41ed0690
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/conftest.py
@@ -0,0 +1,39 @@
+# 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/.
+
+import os
+
+import pytest
+
+
+@pytest.fixture
+def base_schema():
+ def inner():
+ return {
+ "namespace": "testAPIName",
+ "permissions": [],
+ "types": [],
+ "functions": [],
+ "events": [],
+ }
+
+ return inner
+
+
+@pytest.fixture
+def write_jsonschema_fixtures(tmpdir):
+ """Write test schema data into per-testcase (in tmpdir or the given directory)"""
+
+ def inner(jsonschema_fixtures, targetdir=None):
+ assert jsonschema_fixtures
+ if targetdir is None:
+ targetdir = tmpdir
+ for filename, filecontent in jsonschema_fixtures.items():
+ assert isinstance(filename, str) and filename
+ assert isinstance(filecontent, str) and filecontent
+ with open(os.path.join(targetdir, filename), "w") as jsonfile:
+ jsonfile.write(filecontent)
+ return targetdir
+
+ return inner
diff --git a/toolkit/components/extensions/webidl-api/test/helpers.py b/toolkit/components/extensions/webidl-api/test/helpers.py
new file mode 100644
index 0000000000..e2ebec7103
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/helpers.py
@@ -0,0 +1,22 @@
+# 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/.
+
+import sys
+
+import mozpack.path as mozpath
+
+setup_called = False
+
+
+def setup():
+ """Add the directory of the targeted python modules to the python sys.path"""
+
+ global setup_called
+ if setup_called:
+ return
+ setup_called = True
+
+ OUR_DIR = mozpath.abspath(mozpath.dirname(__file__))
+ TARGET_MOD_DIR = mozpath.normpath(mozpath.join(OUR_DIR, ".."))
+ sys.path.append(TARGET_MOD_DIR)
diff --git a/toolkit/components/extensions/webidl-api/test/python.ini b/toolkit/components/extensions/webidl-api/test/python.ini
new file mode 100644
index 0000000000..02315599c4
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/python.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = webext-python
+
+[test_all_schemas_smoketest.py]
+[test_json_schema_parsing.py]
+[test_json_schema_platform_diffs.py]
+[test_webidl_from_json_schema.py]
diff --git a/toolkit/components/extensions/webidl-api/test/test_all_schemas_smoketest.py b/toolkit/components/extensions/webidl-api/test/test_all_schemas_smoketest.py
new file mode 100644
index 0000000000..f0ab6d496e
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/test_all_schemas_smoketest.py
@@ -0,0 +1,22 @@
+# 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/.
+
+import helpers # Import test helpers module.
+import mozunit
+
+helpers.setup()
+
+from GenerateWebIDLBindings import load_and_parse_JSONSchema
+
+
+def test_all_jsonschema_load_and_parse_smoketest():
+ """Make sure it can load and parse all JSONSchema files successfully"""
+ schemas = load_and_parse_JSONSchema()
+ assert schemas
+ assert len(schemas.json_schemas) > 0
+ assert len(schemas.api_namespaces) > 0
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/extensions/webidl-api/test/test_json_schema_parsing.py b/toolkit/components/extensions/webidl-api/test/test_json_schema_parsing.py
new file mode 100644
index 0000000000..79b59bf928
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/test_json_schema_parsing.py
@@ -0,0 +1,215 @@
+# 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/.
+
+import json
+import os
+from textwrap import dedent
+
+import helpers # Import test helpers module.
+import mozunit
+import pytest
+
+helpers.setup()
+
+from GenerateWebIDLBindings import APIEvent, APIFunction, APINamespace, APIType, Schemas
+
+
+def test_parse_simple_single_api_namespace(write_jsonschema_fixtures):
+ """
+ Test Basic loading and parsing a single API JSONSchema:
+ - single line comments outside of the json structure are ignored
+ - parse a simple namespace that includes one permission, type,
+ function and event
+ """
+ schema_dir = write_jsonschema_fixtures(
+ {
+ "test_api.json": dedent(
+ """
+ // Single line comments added before the JSON data are tolerated
+ // and ignored.
+ [
+ {
+ "namespace": "fantasyApi",
+ "permissions": ["fantasyPermission"],
+ "types": [
+ {
+ "id": "MyType",
+ "type": "string",
+ "choices": ["value1", "value2"]
+ }
+ ],
+ "functions": [
+ {
+ "name": "myMethod",
+ "type": "function",
+ "parameters": [
+ { "name": "fnParam", "type": "string" },
+ { "name": "fnRefParam", "$ref": "MyType" }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onSomeEvent",
+ "type": "function",
+ "parameters": [
+ { "name": "evParam", "type": "string" },
+ { "name": "evRefParam", "$ref": "MyType" }
+ ]
+ }
+ ]
+ }
+ ]
+ """
+ ),
+ }
+ )
+
+ schemas = Schemas()
+ schemas.load_schemas(schema_dir, "toolkit")
+
+ assert schemas.get_all_namespace_names() == ["fantasyApi"]
+
+ apiNs = schemas.api_namespaces["fantasyApi"]
+ assert isinstance(apiNs, APINamespace)
+
+ # Properties related to where the JSON schema is coming from
+ # (toolkit, browser or mobile schema directories).
+ assert apiNs.in_toolkit
+ assert not apiNs.in_browser
+ assert not apiNs.in_mobile
+
+ # api_path_string is expected to be exactly the namespace name for
+ # non-nested API namespaces.
+ assert apiNs.api_path_string == "fantasyApi"
+
+ # parse the schema and verify it includes the expected types events and function.
+ schemas.parse_schemas()
+
+ assert set(["fantasyPermission"]) == apiNs.permissions
+ assert ["MyType"] == list(apiNs.types.keys())
+ assert ["myMethod"] == list(apiNs.functions.keys())
+ assert ["onSomeEvent"] == list(apiNs.events.keys())
+
+ type_entry = apiNs.types.get("MyType")
+ fn_entry = apiNs.functions.get("myMethod")
+ ev_entry = apiNs.events.get("onSomeEvent")
+
+ assert isinstance(type_entry, APIType)
+ assert isinstance(fn_entry, APIFunction)
+ assert isinstance(ev_entry, APIEvent)
+
+
+def test_parse_error_on_types_without_id_or_extend(
+ base_schema, write_jsonschema_fixtures
+):
+ """
+ Test parsing types without id or $extend raise an error while parsing.
+ """
+ schema_dir = write_jsonschema_fixtures(
+ {
+ "test_broken_types.json": json.dumps(
+ [
+ {
+ **base_schema(),
+ "namespace": "testBrokenTypeAPI",
+ "types": [
+ {
+ # type with no "id2 or "$ref" properties
+ }
+ ],
+ }
+ ]
+ )
+ }
+ )
+
+ schemas = Schemas()
+ schemas.load_schemas(schema_dir, "toolkit")
+
+ with pytest.raises(
+ Exception,
+ match=r"Error loading schema type data defined in 'toolkit testBrokenTypeAPI'",
+ ):
+ schemas.parse_schemas()
+
+
+def test_parse_ignores_unsupported_types(base_schema, write_jsonschema_fixtures):
+ """
+ Test parsing types without id or $extend raise an error while parsing.
+ """
+ schema_dir = write_jsonschema_fixtures(
+ {
+ "test_broken_types.json": json.dumps(
+ [
+ {
+ **base_schema(),
+ "namespace": "testUnsupportedTypesAPI",
+ "types": [
+ {
+ "id": "AnUnsupportedType",
+ "type": "string",
+ "unsupported": True,
+ },
+ {
+ # missing id or $ref shouldn't matter
+ # no parsing error expected.
+ "unsupported": True,
+ },
+ {"id": "ASupportedType", "type": "string"},
+ ],
+ }
+ ]
+ )
+ }
+ )
+
+ schemas = Schemas()
+ schemas.load_schemas(schema_dir, "toolkit")
+ schemas.parse_schemas()
+ apiNs = schemas.api_namespaces["testUnsupportedTypesAPI"]
+ assert set(apiNs.types.keys()) == set(["ASupportedType"])
+
+
+def test_parse_error_on_namespace_with_inconsistent_max_manifest_version(
+ base_schema, write_jsonschema_fixtures, tmpdir
+):
+ """
+ Test parsing types without id or $extend raise an error while parsing.
+ """
+ browser_schema_dir = os.path.join(tmpdir, "browser")
+ mobile_schema_dir = os.path.join(tmpdir, "mobile")
+ os.mkdir(browser_schema_dir)
+ os.mkdir(mobile_schema_dir)
+
+ base_namespace_schema = {
+ **base_schema(),
+ "namespace": "testInconsistentMaxManifestVersion",
+ }
+
+ browser_schema = {**base_namespace_schema, "max_manifest_version": 2}
+ mobile_schema = {**base_namespace_schema, "max_manifest_version": 3}
+
+ write_jsonschema_fixtures(
+ {"test_inconsistent_maxmv.json": json.dumps([browser_schema])},
+ browser_schema_dir,
+ )
+
+ write_jsonschema_fixtures(
+ {"test_inconsistent_maxmv.json": json.dumps([mobile_schema])}, mobile_schema_dir
+ )
+
+ schemas = Schemas()
+ schemas.load_schemas(browser_schema_dir, "browser")
+ schemas.load_schemas(mobile_schema_dir, "mobile")
+
+ with pytest.raises(
+ TypeError,
+ match=r"Error loading schema data - overwriting existing max_manifest_version value",
+ ):
+ schemas.parse_schemas()
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/extensions/webidl-api/test/test_json_schema_platform_diffs.py b/toolkit/components/extensions/webidl-api/test/test_json_schema_platform_diffs.py
new file mode 100644
index 0000000000..51ae918eb0
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/test_json_schema_platform_diffs.py
@@ -0,0 +1,153 @@
+# 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/.
+
+import os
+import types
+from textwrap import dedent
+
+import helpers # Import test helpers module.
+import mozunit
+
+helpers.setup()
+
+from GenerateWebIDLBindings import Schemas
+from InspectJSONSchema import run_inspect_command
+
+
+def test_inspect_schema_platform_diffs(capsys, write_jsonschema_fixtures, tmpdir):
+ """
+ Test InspectJSONSchema --dump-platform-diff command.
+ """
+ browser_schema_dir = os.path.join(tmpdir, "browser")
+ mobile_schema_dir = os.path.join(tmpdir, "mobile")
+ os.mkdir(browser_schema_dir)
+ os.mkdir(mobile_schema_dir)
+
+ write_jsonschema_fixtures(
+ {
+ "test_api_browser.json": dedent(
+ """
+ [
+ {
+ "namespace": "apiWithDiff",
+ "functions": [
+ {
+ "name": "sharedMethod",
+ "type": "function",
+ "parameters": [
+ { "name": "sharedParam", "type": "string" },
+ { "name": "desktopOnlyMethodParam", "type": "string" }
+ ]
+ },
+ {
+ "name": "desktopMethod",
+ "type": "function",
+ "parameters": []
+ }
+ ],
+ "events": [
+ {
+ "name": "onSharedEvent",
+ "type": "function",
+ "parameters": [
+ { "name": "sharedParam", "type": "string" },
+ { "name": "desktopOnlyEventParam", "type": "string" }
+ ]
+ }
+ ],
+ "properties": {
+ "sharedProperty": { "type": "string", "value": "desktop-value" },
+ "desktopOnlyProperty": { "type": "string", "value": "desktop-only-value" }
+ }
+ }
+ ]
+ """
+ )
+ },
+ browser_schema_dir,
+ )
+
+ write_jsonschema_fixtures(
+ {
+ "test_api_mobile.json": dedent(
+ """
+ [
+ {
+ "namespace": "apiWithDiff",
+ "functions": [
+ {
+ "name": "sharedMethod",
+ "type": "function",
+ "parameters": [
+ { "name": "sharedParam", "type": "string" },
+ { "name": "mobileOnlyMethodParam", "type": "string" }
+ ]
+ },
+ {
+ "name": "mobileMethod",
+ "type": "function",
+ "parameters": []
+ }
+ ],
+ "events": [
+ {
+ "name": "onSharedEvent",
+ "type": "function",
+ "parameters": [
+ { "name": "sharedParam", "type": "string" },
+ { "name": "mobileOnlyEventParam", "type": "string" }
+ ]
+ }
+ ],
+ "properties": {
+ "sharedProperty": { "type": "string", "value": "mobile-value" },
+ "mobileOnlyProperty": { "type": "string", "value": "mobile-only-value" }
+ }
+ }
+ ]
+ """
+ )
+ },
+ mobile_schema_dir,
+ )
+
+ schemas = Schemas()
+ schemas.load_schemas(browser_schema_dir, "browser")
+ schemas.load_schemas(mobile_schema_dir, "mobile")
+
+ assert schemas.get_all_namespace_names() == ["apiWithDiff"]
+ apiNs = schemas.api_namespaces["apiWithDiff"]
+ assert apiNs.in_browser
+ assert apiNs.in_mobile
+
+ apiNs.parse_schemas()
+
+ fakeArgs = types.SimpleNamespace()
+ fakeArgs.dump_namespaces_info = False
+ fakeArgs.dump_platform_diffs = True
+ fakeArgs.only_if_webidl_diffs = False
+ fakeArgs.diff_command = None
+
+ fakeParser = types.SimpleNamespace()
+ fakeParser.print_help = lambda: None
+
+ run_inspect_command(fakeArgs, schemas, fakeParser)
+
+ captured = capsys.readouterr()
+ assert "API schema desktop vs. mobile for apiWithDiff.sharedMethod" in captured.out
+ assert '- "name": "desktopOnlyMethodParam",' in captured.out
+ assert '+ "name": "mobileOnlyMethodParam",' in captured.out
+ assert "API schema desktop vs. mobile for apiWithDiff.onSharedEvent" in captured.out
+ assert '- "name": "desktopOnlyEventParam",' in captured.out
+ assert '+ "name": "mobileOnlyEventParam",' in captured.out
+ assert (
+ "API schema desktop vs. mobile for apiWithDiff.sharedProperty" in captured.out
+ )
+ assert '- "value": "desktop-value"' in captured.out
+ assert '+ "value": "mobile-value"' in captured.out
+ assert captured.err == ""
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/extensions/webidl-api/test/test_webidl_from_json_schema.py b/toolkit/components/extensions/webidl-api/test/test_webidl_from_json_schema.py
new file mode 100644
index 0000000000..64cd7a7361
--- /dev/null
+++ b/toolkit/components/extensions/webidl-api/test/test_webidl_from_json_schema.py
@@ -0,0 +1,110 @@
+# 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/.
+
+from textwrap import dedent
+
+import helpers # Import test helpers module.
+import mozunit
+
+helpers.setup()
+
+from GenerateWebIDLBindings import (
+ WEBEXT_STUBS_MAPPING,
+ APIFunction,
+ Schemas,
+ WebIDLHelpers,
+)
+
+original_stub_mapping_config = WEBEXT_STUBS_MAPPING.copy()
+
+
+def teardown_function():
+ WEBEXT_STUBS_MAPPING.clear()
+ for key in original_stub_mapping_config:
+ WEBEXT_STUBS_MAPPING[key] = original_stub_mapping_config[key]
+
+
+def test_ambiguous_stub_mappings(write_jsonschema_fixtures):
+ """
+ Test generated webidl for methods that are either
+ - being marked as ambiguous because of the "allowAmbiguousOptionalArguments" property
+ in their JSONSchema definition
+ - mapped to "AsyncAmbiguous" stub per WEBEXT_STUBS_MAPPING python script config
+ """
+
+ schema_dir = write_jsonschema_fixtures(
+ {
+ "test_api.json": dedent(
+ """
+ [
+ {
+ "namespace": "testAPINamespace",
+ "functions": [
+ {
+ "name": "jsonSchemaAmbiguousMethod",
+ "type": "function",
+ "allowAmbiguousOptionalArguments": true,
+ "async": true,
+ "parameters": [
+ {"type": "any", "name": "param1", "optional": true},
+ {"type": "any", "name": "param2", "optional": true},
+ {"type": "string", "name": "param3", "optional": true}
+ ]
+ },
+ {
+ "name": "configuredAsAmbiguousMethod",
+ "type": "function",
+ "async": "callback",
+ "parameters": [
+ {"name": "param1", "optional": true, "type": "object"},
+ {"name": "callback", "type": "function", "parameters": []}
+ ]
+ }
+ ]
+ }
+ ]
+ """
+ )
+ }
+ )
+
+ assert "testAPINamespace.configuredAsAmbiguousMethod" not in WEBEXT_STUBS_MAPPING
+ # NOTE: mocked config reverted in the teardown_method pytest hook.
+ WEBEXT_STUBS_MAPPING[
+ "testAPINamespace.configuredAsAmbiguousMethod"
+ ] = "AsyncAmbiguous"
+
+ schemas = Schemas()
+ schemas.load_schemas(schema_dir, "toolkit")
+
+ assert schemas.get_all_namespace_names() == ["testAPINamespace"]
+ schemas.parse_schemas()
+
+ apiNs = schemas.get_namespace("testAPINamespace")
+ fnAmbiguousBySchema = apiNs.functions.get("jsonSchemaAmbiguousMethod")
+
+ assert isinstance(fnAmbiguousBySchema, APIFunction)
+ generated_webidl = WebIDLHelpers.to_webidl_definition(fnAmbiguousBySchema, None)
+ expected_webidl = "\n".join(
+ [
+ ' [Throws, WebExtensionStub="AsyncAmbiguous"]',
+ " any jsonSchemaAmbiguousMethod(any... args);",
+ ]
+ )
+ assert generated_webidl == expected_webidl
+
+ fnAmbiguousByConfig = apiNs.functions.get("configuredAsAmbiguousMethod")
+ assert isinstance(fnAmbiguousByConfig, APIFunction)
+ generated_webidl = WebIDLHelpers.to_webidl_definition(fnAmbiguousByConfig, None)
+ expected_webidl = "\n".join(
+ [
+ ' [Throws, WebExtensionStub="AsyncAmbiguous"]',
+ " any configuredAsAmbiguousMethod(any... args);",
+ ]
+ )
+ assert generated_webidl == expected_webidl
+
+
+if __name__ == "__main__":
+ mozunit.main()