diff options
Diffstat (limited to 'toolkit/components/extensions/webidl-api')
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() |