/* -*- Mode: c++; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*- * vim: set sw=2 ts=4 expandtab: * 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 "EventDispatcher.h" #include "CFTypeRefPtr.h" #include "mozilla/MacStringHelpers.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/TypedArray.h" #include "mozilla/widget/GeckoViewSupport.h" using namespace mozilla; using namespace mozilla::widget; namespace mozilla::widget::detail { nsresult BoxValue(JSContext* aCx, JS::Handle aData, CFTypeRefPtr& aOut); nsresult BoxObject(JSContext* aCx, JS::Handle aObj, CFTypeRefPtr& aOut) { JS::Rooted ids(aCx, JS::IdVector(aCx)); if (!JS_Enumerate(aCx, aObj, &ids)) { return NS_ERROR_FAILURE; } auto dict = CFTypeRefPtr::WrapUnderCreateRule( CFDictionaryCreateMutable(kCFAllocatorDefault, (CFIndex)ids.length(), &kCFCopyStringDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); if (!dict) { return NS_ERROR_OUT_OF_MEMORY; } CFTypeRefPtr valCF; JS::Rooted valJS(aCx); for (size_t i = 0; i < ids.length(); ++i) { nsAutoJSString id; if (!id.init(aCx, ids[i])) { return NS_ERROR_FAILURE; } auto key = CFTypeRefPtr::WrapUnderCreateRule( CFStringCreateWithCharacters(kCFAllocatorDefault, (const UniChar*)id.BeginReading(), (CFIndex)id.Length())); if (!key) { return NS_ERROR_OUT_OF_MEMORY; } if (!JS_GetPropertyById(aCx, aObj, ids[i], &valJS)) { return NS_ERROR_FAILURE; } nsresult rv = BoxValue(aCx, valJS, valCF); NS_ENSURE_SUCCESS(rv, rv); CFDictionaryAddValue(dict.get(), key.get(), valCF ? valCF.get() : kCFNull); } // NOTE: A CFRetain() and CFRelease() could be avoided here if we could steal // the pointer from `dict`. aOut.AssignUnderGetRule(dict.get()); return NS_OK; } nsresult BoxArray(JSContext* aCx, JS::Handle aArray, CFTypeRefPtr& aOut) { uint32_t length = 0; if (!JS::GetArrayLength(aCx, aArray, &length)) { return NS_ERROR_FAILURE; } auto array = CFTypeRefPtr::WrapUnderCreateRule(CFArrayCreateMutable( kCFAllocatorDefault, (CFIndex)length, &kCFTypeArrayCallBacks)); if (!array) { return NS_ERROR_OUT_OF_MEMORY; } CFTypeRefPtr valCF; JS::Rooted valJS(aCx); for (size_t i = 0; i < length; ++i) { if (!JS_GetElement(aCx, aArray, i, &valJS)) { return NS_ERROR_FAILURE; } nsresult rv = BoxValue(aCx, valJS, valCF); NS_ENSURE_SUCCESS(rv, rv); CFArrayAppendValue(array.get(), valCF.get()); } // NOTE: A CFRetain() and CFRelease() could be avoided here if we could steal // the pointer from `array`. aOut.AssignUnderGetRule(array.get()); return NS_OK; } nsresult BoxTypedArray(JSContext* aCx, JS::Handle aTypedArray, CFTypeRefPtr& aOut) { dom::RootedSpiderMonkeyInterface typedArray(aCx); if (!typedArray.Init(aTypedArray)) { return NS_ERROR_INVALID_ARG; } if (JS::IsArrayBufferViewShared(typedArray.Obj())) { return NS_ERROR_INVALID_ARG; } if (JS::IsLargeArrayBufferView(typedArray.Obj())) { return NS_ERROR_INVALID_ARG; } if (JS::IsResizableArrayBufferView(typedArray.Obj())) { return NS_ERROR_INVALID_ARG; } typedArray.ProcessData( [&](const Span& aData, JS::AutoCheckCannotGC&&) { aOut.AssignUnderCreateRule(CFDataCreate( kCFAllocatorDefault, aData.data(), (CFIndex)aData.size())); }); if (!aOut) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } nsresult BoxValue(JSContext* aCx, JS::Handle aData, CFTypeRefPtr& aOut) { if (aData.isNullOrUndefined()) { aOut = nullptr; } else if (aData.isBoolean()) { aOut.AssignUnderGetRule(aData.toBoolean() ? kCFBooleanTrue : kCFBooleanFalse); } else if (aData.isInt32()) { int32_t value = aData.toInt32(); aOut.AssignUnderCreateRule( CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &value)); return aOut ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } else if (aData.isNumber()) { double value = aData.toDouble(); aOut.AssignUnderCreateRule( CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &value)); return aOut ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } else if (aData.isString()) { nsAutoJSString str; if (!str.init(aCx, aData)) { return NS_ERROR_OUT_OF_MEMORY; } aOut.AssignUnderCreateRule(CFStringCreateWithCharacters( kCFAllocatorDefault, (const UniChar*)str.BeginReading(), (CFIndex)str.Length())); return aOut ? NS_OK : NS_ERROR_OUT_OF_MEMORY; } else if (aData.isObject()) { JS::Rooted obj(aCx, &aData.toObject()); // Array objects bool isArray = false; if (!JS::IsArrayObject(aCx, obj, &isArray)) { return NS_ERROR_FAILURE; } if (isArray) { return BoxArray(aCx, obj, aOut); } // Typed array objects if (JS_IsTypedArrayObject(obj)) { return BoxTypedArray(aCx, obj, aOut); } // Plain JS objects return BoxObject(aCx, obj, aOut); } else { return NS_ERROR_INVALID_ARG; } return NS_OK; } nsresult BoxData(const nsAString& aEvent, JSContext* aCx, JS::Handle aData, CFTypeRefPtr& aOut, bool aObjectOnly) { nsresult rv = NS_ERROR_INVALID_ARG; if (!aObjectOnly) { rv = BoxValue(aCx, aData, aOut); } else if (aData.isNullOrUndefined()) { aOut = nil; rv = NS_OK; } else if (aData.isObject()) { JS::Rooted obj(aCx, &aData.toObject()); rv = BoxObject(aCx, obj, aOut); } if (rv != NS_ERROR_INVALID_ARG) { return rv; } NS_ConvertUTF16toUTF8 event(aEvent); if (JS_IsExceptionPending(aCx)) { JS::WarnUTF8(aCx, "Error dispatching %s", event.get()); } else { JS_ReportErrorUTF8(aCx, "Invalid event data for %s", event.get()); } return NS_ERROR_INVALID_ARG; } nsresult UnboxValue(JSContext* aCx, CFTypeRef aData, JS::MutableHandle aOut); nsresult UnboxString(JSContext* aCx, CFStringRef aValue, JS::MutableHandle aOut) { CFIndex length = CFStringGetLength(aValue); JS::UniqueTwoByteChars chars( (char16_t*)JS_string_malloc(aCx, length * sizeof(char16_t))); if (!chars) { return NS_ERROR_OUT_OF_MEMORY; } CFStringGetCharacters(aValue, CFRangeMake(0, length), (UniChar*)chars.get()); JSString* str = JS_NewUCString(aCx, std::move(chars), length); if (!str) { return NS_ERROR_OUT_OF_MEMORY; } aOut.setString(str); return NS_OK; } nsresult UnboxBundle(JSContext* aCx, CFDictionaryRef aData, JS::MutableHandle aOut) { size_t count = CFDictionaryGetCount(aData); AutoTArray keys; AutoTArray values; keys.SetLength(count); values.SetLength(count); CFDictionaryGetKeysAndValues(aData, keys.Elements(), values.Elements()); JS::Rooted obj(aCx, JS_NewPlainObject(aCx)); if (!obj) { return NS_ERROR_OUT_OF_MEMORY; } JS::Rooted key(aCx); JS::Rooted value(aCx); JS::Rooted id(aCx); for (size_t i = 0; i < count; ++i) { nsresult rv = UnboxValue(aCx, keys[i], &key); NS_ENSURE_SUCCESS(rv, rv); rv = UnboxValue(aCx, values[i], &value); NS_ENSURE_SUCCESS(rv, rv); if (!JS_ValueToId(aCx, key, &id)) { return NS_ERROR_FAILURE; } if (!JS_DefinePropertyById(aCx, obj, id, value, JSPROP_ENUMERATE)) { return NS_ERROR_FAILURE; } } aOut.setObject(*obj); return NS_OK; } nsresult UnboxArray(JSContext* aCx, CFArrayRef aData, JS::MutableHandle aOut) { size_t count = CFArrayGetCount(aData); JS::Rooted arr(aCx, JS::NewArrayObject(aCx, count)); if (!arr) { return NS_ERROR_OUT_OF_MEMORY; } JS::Rooted elt(aCx); for (size_t i = 0; i < count; ++i) { nsresult rv = UnboxValue(aCx, CFArrayGetValueAtIndex(aData, (CFIndex)i), &elt); NS_ENSURE_SUCCESS(rv, rv); if (!JS_SetElement(aCx, arr, i, elt)) { return NS_ERROR_FAILURE; } } aOut.setObject(*arr); return NS_OK; } nsresult UnboxValue(JSContext* aCx, CFTypeRef aData, JS::MutableHandle aOut) { CFTypeID typeID = aData ? CFGetTypeID(aData) : CFNullGetTypeID(); if (typeID == CFNullGetTypeID()) { aOut.setNull(); } else if (typeID == CFBooleanGetTypeID()) { aOut.setBoolean(CFBooleanGetValue((CFBooleanRef)aData)); } else if (typeID == CFNumberGetTypeID()) { double numberValue = 0; CFNumberGetValue((CFNumberRef)aData, kCFNumberDoubleType, &numberValue); aOut.setDouble(numberValue); } else if (typeID == CFStringGetTypeID()) { return UnboxString(aCx, (CFStringRef)aData, aOut); } else if (typeID == CFDataGetTypeID()) { size_t length = CFDataGetLength((CFDataRef)aData); const uint8_t* data = CFDataGetBytePtr((CFDataRef)aData); IgnoredErrorResult error; dom::Uint8Array::Create(aCx, Span{data, length}, error); return error.StealNSResult(); } else if (typeID == CFDictionaryGetTypeID()) { return UnboxBundle(aCx, (CFDictionaryRef)aData, aOut); } else if (typeID == CFArrayGetTypeID()) { return UnboxArray(aCx, (CFArrayRef)aData, aOut); } else { NS_WARNING("Invalid type"); return NS_ERROR_INVALID_ARG; } return NS_OK; } nsresult UnboxData(const nsAString& aEvent, JSContext* aCx, CFTypeRef aData, JS::MutableHandle aOut, bool aBundleOnly) { MOZ_ASSERT(NS_IsMainThread()); // NOTE: aBundleOnly is used to maintain behaviour parity with Android. nsresult rv = NS_ERROR_INVALID_ARG; if (!aBundleOnly) { rv = UnboxValue(aCx, aData, aOut); } else if (!aData || CFGetTypeID(aData) == CFNullGetTypeID()) { aOut.setNull(); rv = NS_OK; } else if (CFGetTypeID(aData) == CFDictionaryGetTypeID()) { rv = UnboxBundle(aCx, (CFDictionaryRef)aData, aOut); } if (rv != NS_ERROR_INVALID_ARG) { return rv; } if (JS_IsExceptionPending(aCx)) { JS::WarnUTF8(aCx, "Error dispatching %s", NS_ConvertUTF16toUTF8(aEvent).get()); } else { JS_ReportErrorUTF8(aCx, "Invalid event data for %s", NS_ConvertUTF16toUTF8(aEvent).get()); } return NS_ERROR_INVALID_ARG; } } // namespace mozilla::widget::detail namespace { class SwiftCallbackDelegate final : public nsIGeckoViewEventCallback { public: NS_DECL_ISUPPORTS explicit SwiftCallbackDelegate(id aCallback) : mCallback(aCallback) { [aCallback retain]; } NS_IMETHOD OnSuccess(JS::Handle aData, JSContext* aCx) override { return Call(aCx, aData, [&](const CFTypeRefPtr& data) { [mCallback sendSuccess:(id)data.get()]; }); } NS_IMETHOD OnError(JS::Handle aData, JSContext* aCx) override { return Call(aCx, aData, [&](const CFTypeRefPtr& data) { [mCallback sendError:(id)data.get()]; }); } private: virtual ~SwiftCallbackDelegate() { [mCallback release]; } template nsresult Call(JSContext* aCx, JS::Handle aData, F aCall) { MOZ_ASSERT(NS_IsMainThread()); CFTypeRefPtr data; nsresult rv = mozilla::widget::detail::BoxData(u"callback"_ns, aCx, aData, data, /* ObjectOnly */ false); NS_ENSURE_SUCCESS(rv, rv); dom::AutoNoJSAPI nojsapi; aCall(data); return NS_OK; } id mCallback; }; NS_IMPL_ISUPPORTS(SwiftCallbackDelegate, nsIGeckoViewEventCallback) } // namespace static void CallbackFromSwift(nsIGeckoViewEventCallback* aCallback, CFTypeRef aData, nsresult (nsIGeckoViewEventCallback::*aCall)( JS::Handle, JSContext*)) { MOZ_ASSERT(NS_IsMainThread()); // Use either the attached window's realm or a default realm. dom::AutoJSAPI jsapi; NS_ENSURE_TRUE_VOID(jsapi.Init(xpc::PrivilegedJunkScope())); JS::Rooted data(jsapi.cx()); nsresult rv = mozilla::widget::detail::UnboxData(u"callback"_ns, jsapi.cx(), aData, &data, /* BundleOnly */ false); NS_ENSURE_SUCCESS_VOID(rv); rv = (aCallback->*aCall)(data, jsapi.cx()); NS_ENSURE_SUCCESS_VOID(rv); } // Objective-C wrapper for a nsIGeckoViewEventCallback. @interface NativeCallbackDelegateSupport : NSObject { nsCOMPtr mCallback; } - (id)initWithCallback:(nsIGeckoViewEventCallback*)callback; - (void)sendSuccess:(id)response; - (void)sendError:(id)response; @end @implementation NativeCallbackDelegateSupport - (id)initWithCallback:(nsIGeckoViewEventCallback*)callback { self = [super init]; mCallback = callback; return self; } - (void)sendSuccess:(id)response { AssertIsOnMainThread(); CallbackFromSwift(mCallback, (CFTypeRef)response, &nsIGeckoViewEventCallback::OnSuccess); } - (void)sendError:(id)response { AssertIsOnMainThread(); CallbackFromSwift(mCallback, (CFTypeRef)response, &nsIGeckoViewEventCallback::OnError); } @end // Objective-C wrapper for an EventDispatcher. @interface EventDispatcherImpl : NSObject { RefPtr mDispatcher; } - (id)initWithDispatcher:(EventDispatcher*)dispatcher; @end @implementation EventDispatcherImpl - (id)initWithDispatcher:(EventDispatcher*)dispatcher { self = [super init]; self->mDispatcher = dispatcher; return self; } - (void)dispatchToGecko:(NSString*)type message:(id)message callback:(id)callback { AssertIsOnMainThread(); nsString event; CopyNSStringToXPCOMString(type, event); nsCOMPtr geckoCb; if (callback) { geckoCb = new SwiftCallbackDelegate(callback); } dom::AutoJSAPI jsapi; NS_ENSURE_TRUE_VOID(jsapi.Init(xpc::PrivilegedJunkScope())); JS::Rooted data(jsapi.cx()); nsresult rv = mozilla::widget::detail::UnboxData( event, jsapi.cx(), (CFTypeRef)message, &data, /* BundleOnly */ true); NS_ENSURE_SUCCESS_VOID(rv); mDispatcher->DispatchToGecko(jsapi.cx(), event, data, geckoCb); } - (BOOL)hasListener:(NSString*)type { nsString event; CopyNSStringToXPCOMString(type, event); return mDispatcher->HasGeckoListener(event); } @end namespace mozilla::widget { bool EventDispatcher::HasEmbedderListener(const nsAString& aEvent) { id dispatcher = (id)mDispatcher; return [dispatcher hasListener:XPCOMStringToNSString(aEvent)]; } nsresult EventDispatcher::DispatchToEmbedder( JSContext* aCx, const nsAString& aEvent, JS::Handle aData, nsIGeckoViewEventCallback* aCallback) { // Convert the data payload to CoreFoundation types CFTypeRefPtr data; nsresult rv = detail::BoxData(aEvent, aCx, aData, data, /* ObjectOnly */ true); NS_ENSURE_SUCCESS(rv, rv); // Wrap the callback if provided into a Swift callback. NativeCallbackDelegateSupport* callback = nil; if (aCallback) { callback = [[[NativeCallbackDelegateSupport alloc] initWithCallback:aCallback] autorelease]; } // Call the swift dispatcher. dom::AutoNoJSAPI nojsapi; id dispatcher = (id)mDispatcher; [dispatcher dispatchToSwift:XPCOMStringToNSString(aEvent) message:(id)data.get() callback:callback]; return NS_OK; } void EventDispatcher::Attach(id aDispatcher) { AssertIsOnMainThread(); MOZ_ASSERT(aDispatcher); id prevDispatcher = (id)mDispatcher; id newDispatcher = (id)aDispatcher; if (prevDispatcher && prevDispatcher == newDispatcher) { // Nothing to do if the new dispatcher is the same. return; } [prevDispatcher attach:nil]; [prevDispatcher release]; mDispatcher = [newDispatcher retain]; EventDispatcherImpl* proxy = [[EventDispatcherImpl alloc] initWithDispatcher:this]; [newDispatcher attach:[proxy autorelease]]; } void EventDispatcher::Shutdown() { AssertIsOnMainThread(); if (mDispatcher) { [mDispatcher release]; } mDispatcher = nullptr; } void EventDispatcher::Detach() { AssertIsOnMainThread(); MOZ_ASSERT(mDispatcher); // SetAttachedToGecko will call disposeNative for us later on the Gecko // thread to make sure all pending dispatchToGecko calls have completed. if (mDispatcher) { [(id)mDispatcher attach:nil]; } Shutdown(); } EventDispatcher::~EventDispatcher() { if (mDispatcher) { [mDispatcher release]; } mDispatcher = nullptr; } /* static */ nsresult EventDispatcher::UnboxBundle(JSContext* aCx, CFDictionaryRef aData, JS::MutableHandle aOut) { return detail::UnboxBundle(aCx, aData, aOut); } } // namespace mozilla::widget