From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/console/Console.cpp | 3045 ++++++++++++++++++++ dom/console/Console.h | 450 +++ dom/console/ConsoleAPIStorage.sys.mjs | 195 ++ dom/console/ConsoleCommon.h | 28 + dom/console/ConsoleInstance.cpp | 206 ++ dom/console/ConsoleInstance.h | 114 + dom/console/ConsoleReportCollector.cpp | 198 ++ dom/console/ConsoleReportCollector.h | 92 + dom/console/ConsoleUtils.cpp | 167 ++ dom/console/ConsoleUtils.h | 53 + dom/console/components.conf | 14 + dom/console/moz.build | 57 + dom/console/nsIConsoleAPIStorage.idl | 65 + dom/console/nsIConsoleReportCollector.h | 132 + dom/console/tests/chrome.ini | 9 + dom/console/tests/console.sys.mjs | 45 + dom/console/tests/file_empty.html | 1 + dom/console/tests/head.js | 24 + dom/console/tests/mochitest.ini | 14 + dom/console/tests/test_bug659625.html | 107 + dom/console/tests/test_bug978522.html | 32 + dom/console/tests/test_bug979109.html | 32 + dom/console/tests/test_bug989665.html | 21 + dom/console/tests/test_console.xhtml | 35 + dom/console/tests/test_consoleEmptyStack.html | 26 + dom/console/tests/test_console_binding.html | 40 + dom/console/tests/test_console_proto.html | 17 + dom/console/tests/test_count.html | 117 + dom/console/tests/test_jsm.xhtml | 100 + dom/console/tests/test_timer.html | 112 + dom/console/tests/xpcshell/head.js | 22 + dom/console/tests/xpcshell/test_basic.js | 29 + .../xpcshell/test_failing_console_listener.js | 35 + dom/console/tests/xpcshell/test_formatting.js | 77 + .../xpcshell/test_reportForServiceWorkerScope.js | 42 + dom/console/tests/xpcshell/xpcshell.ini | 8 + 36 files changed, 5761 insertions(+) create mode 100644 dom/console/Console.cpp create mode 100644 dom/console/Console.h create mode 100644 dom/console/ConsoleAPIStorage.sys.mjs create mode 100644 dom/console/ConsoleCommon.h create mode 100644 dom/console/ConsoleInstance.cpp create mode 100644 dom/console/ConsoleInstance.h create mode 100644 dom/console/ConsoleReportCollector.cpp create mode 100644 dom/console/ConsoleReportCollector.h create mode 100644 dom/console/ConsoleUtils.cpp create mode 100644 dom/console/ConsoleUtils.h create mode 100644 dom/console/components.conf create mode 100644 dom/console/moz.build create mode 100644 dom/console/nsIConsoleAPIStorage.idl create mode 100644 dom/console/nsIConsoleReportCollector.h create mode 100644 dom/console/tests/chrome.ini create mode 100644 dom/console/tests/console.sys.mjs create mode 100644 dom/console/tests/file_empty.html create mode 100644 dom/console/tests/head.js create mode 100644 dom/console/tests/mochitest.ini create mode 100644 dom/console/tests/test_bug659625.html create mode 100644 dom/console/tests/test_bug978522.html create mode 100644 dom/console/tests/test_bug979109.html create mode 100644 dom/console/tests/test_bug989665.html create mode 100644 dom/console/tests/test_console.xhtml create mode 100644 dom/console/tests/test_consoleEmptyStack.html create mode 100644 dom/console/tests/test_console_binding.html create mode 100644 dom/console/tests/test_console_proto.html create mode 100644 dom/console/tests/test_count.html create mode 100644 dom/console/tests/test_jsm.xhtml create mode 100644 dom/console/tests/test_timer.html create mode 100644 dom/console/tests/xpcshell/head.js create mode 100644 dom/console/tests/xpcshell/test_basic.js create mode 100644 dom/console/tests/xpcshell/test_failing_console_listener.js create mode 100644 dom/console/tests/xpcshell/test_formatting.js create mode 100644 dom/console/tests/xpcshell/test_reportForServiceWorkerScope.js create mode 100644 dom/console/tests/xpcshell/xpcshell.ini (limited to 'dom/console') diff --git a/dom/console/Console.cpp b/dom/console/Console.cpp new file mode 100644 index 0000000000..54ca865d95 --- /dev/null +++ b/dom/console/Console.cpp @@ -0,0 +1,3045 @@ +/* -*- 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 "mozilla/dom/Console.h" +#include "mozilla/dom/ConsoleInstance.h" +#include "mozilla/dom/ConsoleBinding.h" +#include "ConsoleCommon.h" + +#include "js/Array.h" // JS::GetArrayLength, JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_GetElement +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/WorkletThread.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/JSObjectHolder.h" +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_devtools.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDOMNavigationTiming.h" +#include "nsGlobalWindow.h" +#include "nsJSUtils.h" +#include "nsNetUtil.h" +#include "xpcpublic.h" +#include "nsContentUtils.h" +#include "nsDocShell.h" +#include "nsProxyRelease.h" +#include "nsReadableUtils.h" +#include "mozilla/ConsoleTimelineMarker.h" +#include "mozilla/TimestampTimelineMarker.h" + +#include "nsIConsoleAPIStorage.h" +#include "nsIException.h" // for nsIStackFrame +#include "nsIInterfaceRequestorUtils.h" +#include "nsILoadContext.h" +#include "nsISensitiveInfoHiddenURI.h" +#include "nsISupportsPrimitives.h" +#include "nsIWebNavigation.h" +#include "nsIXPConnect.h" + +// The maximum allowed number of concurrent timers per page. +#define MAX_PAGE_TIMERS 10000 + +// The maximum allowed number of concurrent counters per page. +#define MAX_PAGE_COUNTERS 10000 + +// The maximum stacktrace depth when populating the stacktrace array used for +// console.trace(). +#define DEFAULT_MAX_STACKTRACE_DEPTH 200 + +// This tags are used in the Structured Clone Algorithm to move js values from +// worker thread to main thread +#define CONSOLE_TAG_BLOB JS_SCTAG_USER_MIN + +// This value is taken from ConsoleAPIStorage.js +#define STORAGE_MAX_EVENTS 1000 + +using namespace mozilla::dom::exceptions; + +namespace mozilla::dom { + +struct ConsoleStructuredCloneData { + nsCOMPtr mGlobal; + nsTArray> mBlobs; +}; + +static void ComposeAndStoreGroupName(JSContext* aCx, + const Sequence& aData, + nsAString& aName, + nsTArray* aGroupStack); +static bool UnstoreGroupName(nsAString& aName, nsTArray* aGroupStack); + +static bool ProcessArguments(JSContext* aCx, const Sequence& aData, + Sequence& aSequence, + Sequence& aStyles); + +static JS::Value CreateCounterOrResetCounterValue(JSContext* aCx, + const nsAString& aCountLabel, + uint32_t aCountValue); + +/** + * Console API in workers uses the Structured Clone Algorithm to move any value + * from the worker thread to the main-thread. Some object cannot be moved and, + * in these cases, we convert them to strings. + * It's not the best, but at least we are able to show something. + */ + +class ConsoleCallData final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ConsoleCallData) + + ConsoleCallData(Console::MethodName aName, const nsAString& aString, + Console* aConsole) + : mConsoleID(aConsole->mConsoleID), + mPrefix(aConsole->mPrefix), + mMethodName(aName), + mMicroSecondTimeStamp(JS_Now()), + mStartTimerValue(0), + mStartTimerStatus(Console::eTimerUnknown), + mLogTimerDuration(0), + mLogTimerStatus(Console::eTimerUnknown), + mCountValue(MAX_PAGE_COUNTERS), + mIDType(eUnknown), + mOuterIDNumber(0), + mInnerIDNumber(0), + mMethodString(aString) {} + + void SetIDs(uint64_t aOuterID, uint64_t aInnerID) { + MOZ_ASSERT(mIDType == eUnknown); + + mOuterIDNumber = aOuterID; + mInnerIDNumber = aInnerID; + mIDType = eNumber; + } + + void SetIDs(const nsAString& aOuterID, const nsAString& aInnerID) { + MOZ_ASSERT(mIDType == eUnknown); + + mOuterIDString = aOuterID; + mInnerIDString = aInnerID; + mIDType = eString; + } + + void SetOriginAttributes(const OriginAttributes& aOriginAttributes) { + mOriginAttributes = aOriginAttributes; + } + + void SetAddonId(nsIPrincipal* aPrincipal) { + nsAutoString addonId; + aPrincipal->GetAddonId(addonId); + + mAddonId = addonId; + } + + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(ConsoleCallData); + } + + const nsString mConsoleID; + const nsString mPrefix; + + const Console::MethodName mMethodName; + int64_t mMicroSecondTimeStamp; + + // These values are set in the owning thread and they contain the timestamp of + // when the new timer has started, the name of it and the status of the + // creation of it. If status is false, something went wrong. User + // DOMHighResTimeStamp instead mozilla::TimeStamp because we use + // monotonicTimer from Performance.now(); + // They will be set on the owning thread and never touched again on that + // thread. They will be used in order to create a ConsoleTimerStart dictionary + // when console.time() is used. + DOMHighResTimeStamp mStartTimerValue; + nsString mStartTimerLabel; + Console::TimerStatus mStartTimerStatus; + + // These values are set in the owning thread and they contain the duration, + // the name and the status of the LogTimer method. If status is false, + // something went wrong. They will be set on the owning thread and never + // touched again on that thread. They will be used in order to create a + // ConsoleTimerLogOrEnd dictionary. This members are set when + // console.timeEnd() or console.timeLog() are called. + double mLogTimerDuration; + nsString mLogTimerLabel; + Console::TimerStatus mLogTimerStatus; + + // These 2 values are set by IncreaseCounter or ResetCounter on the owning + // thread and they are used by CreateCounterOrResetCounterValue. + // These members are set when console.count() or console.countReset() are + // called. + nsString mCountLabel; + uint32_t mCountValue; + + // The concept of outerID and innerID is misleading because when a + // ConsoleCallData is created from a window, these are the window IDs, but + // when the object is created from a SharedWorker, a ServiceWorker or a + // subworker of a ChromeWorker these IDs are the type of worker and the + // filename of the callee. + // In Console.sys.mjs the ID is 'jsm'. + enum { eString, eNumber, eUnknown } mIDType; + + uint64_t mOuterIDNumber; + nsString mOuterIDString; + + uint64_t mInnerIDNumber; + nsString mInnerIDString; + + OriginAttributes mOriginAttributes; + + nsString mAddonId; + + const nsString mMethodString; + + // Stack management is complicated, because we want to do it as + // lazily as possible. Therefore, we have the following behavior: + // 1) mTopStackFrame is initialized whenever we have any JS on the stack + // 2) mReifiedStack is initialized if we're created in a worker. + // 3) mStack is set (possibly to null if there is no JS on the stack) if + // we're created on main thread. + Maybe mTopStackFrame; + Maybe> mReifiedStack; + nsCOMPtr mStack; + + private: + ~ConsoleCallData() = default; + + NS_DECL_OWNINGTHREAD; +}; + +// MainThreadConsoleData instances are created on the Console thread and +// referenced from both main and Console threads in order to provide the same +// object for any ConsoleRunnables relating to the same Console. A Console +// owns a MainThreadConsoleData; MainThreadConsoleData does not keep its +// Console alive. +class MainThreadConsoleData final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MainThreadConsoleData); + + JSObject* GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal); + // This method must receive aCx and aArguments in the same JS::Compartment. + void ProcessCallData(JSContext* aCx, ConsoleCallData* aData, + const Sequence& aArguments); + + private: + ~MainThreadConsoleData() { + NS_ReleaseOnMainThread("MainThreadConsoleData::mStorage", + mStorage.forget()); + NS_ReleaseOnMainThread("MainThreadConsoleData::mSandbox", + mSandbox.forget()); + } + + // All members, except for mRefCnt, are accessed only on the main thread, + // except in MainThreadConsoleData destruction, at which point there are no + // other references. + nsCOMPtr mStorage; + RefPtr mSandbox; + nsTArray mGroupStack; +}; + +// This base class must be extended for Worker and for Worklet. +class ConsoleRunnable : public StructuredCloneHolderBase { + public: + ~ConsoleRunnable() override { + MOZ_ASSERT(!mClonedData.mGlobal, + "mClonedData.mGlobal is set and cleared in a main thread scope"); + // Clear the StructuredCloneHolderBase class. + Clear(); + } + + protected: + JSObject* CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader, + const JS::CloneDataPolicy& aCloneDataPolicy, + uint32_t aTag, uint32_t aIndex) override { + AssertIsOnMainThread(); + + if (aTag == CONSOLE_TAG_BLOB) { + MOZ_ASSERT(mClonedData.mBlobs.Length() > aIndex); + + JS::Rooted val(aCx); + { + nsCOMPtr global = mClonedData.mGlobal; + RefPtr blob = + Blob::Create(global, mClonedData.mBlobs.ElementAt(aIndex)); + if (!ToJSValue(aCx, blob, &val)) { + return nullptr; + } + } + + return &val.toObject(); + } + + MOZ_CRASH("No other tags are supported."); + return nullptr; + } + + bool CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter, + JS::Handle aObj, + bool* aSameProcessScopeRequired) override { + RefPtr blob; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, aObj, blob))) { + if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, CONSOLE_TAG_BLOB, + mClonedData.mBlobs.Length()))) { + return false; + } + + mClonedData.mBlobs.AppendElement(blob->Impl()); + return true; + } + + if (!JS_ObjectNotWritten(aWriter, aObj)) { + return false; + } + + JS::Rooted value(aCx, JS::ObjectOrNullValue(aObj)); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (NS_WARN_IF(!jsString)) { + return false; + } + + if (NS_WARN_IF(!JS_WriteString(aWriter, jsString))) { + return false; + } + + return true; + } + + // Helper method for CallData + void ProcessCallData(JSContext* aCx, MainThreadConsoleData* aConsoleData, + ConsoleCallData* aCallData) { + AssertIsOnMainThread(); + + ConsoleCommon::ClearException ce(aCx); + + JS::Rooted argumentsValue(aCx); + if (!Read(aCx, &argumentsValue)) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + + JS::Rooted argumentsObj(aCx, &argumentsValue.toObject()); + + uint32_t length; + if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { + return; + } + + Sequence values; + SequenceRooter arguments(aCx, &values); + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(aCx); + + if (!JS_GetElement(aCx, argumentsObj, i, &value)) { + return; + } + + if (!values.AppendElement(value, fallible)) { + return; + } + } + + MOZ_ASSERT(values.Length() == length); + + aConsoleData->ProcessCallData(aCx, aCallData, values); + } + + // Generic + bool WriteArguments(JSContext* aCx, const Sequence& aArguments) { + ConsoleCommon::ClearException ce(aCx); + + JS::Rooted arguments( + aCx, JS::NewArrayObject(aCx, aArguments.Length())); + if (NS_WARN_IF(!arguments)) { + return false; + } + + JS::Rooted arg(aCx); + for (uint32_t i = 0; i < aArguments.Length(); ++i) { + arg = aArguments[i]; + if (NS_WARN_IF( + !JS_DefineElement(aCx, arguments, i, arg, JSPROP_ENUMERATE))) { + return false; + } + } + + JS::Rooted value(aCx, JS::ObjectValue(*arguments)); + return WriteData(aCx, value); + } + + // Helper method for Profile calls + void ProcessProfileData(JSContext* aCx, Console::MethodName aMethodName, + const nsAString& aAction) { + AssertIsOnMainThread(); + + ConsoleCommon::ClearException ce(aCx); + + JS::Rooted argumentsValue(aCx); + bool ok = Read(aCx, &argumentsValue); + mClonedData.mGlobal = nullptr; + + if (!ok) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + JS::Rooted argumentsObj(aCx, &argumentsValue.toObject()); + if (NS_WARN_IF(!argumentsObj)) { + return; + } + + uint32_t length; + if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { + return; + } + + Sequence arguments; + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(aCx); + + if (!JS_GetElement(aCx, argumentsObj, i, &value)) { + return; + } + + if (!arguments.AppendElement(value, fallible)) { + return; + } + } + + Console::ProfileMethodMainthread(aCx, aAction, arguments); + } + + bool WriteData(JSContext* aCx, JS::Handle aValue) { + // We use structuredClone to send the JSValue to the main-thread, in order + // to store it into the Console API Service. The consumer will be the + // console panel in the devtools and, because of this, we want to allow the + // cloning of sharedArrayBuffers and WASM modules. + JS::CloneDataPolicy cloneDataPolicy; + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + cloneDataPolicy.allowSharedMemoryObjects(); + + if (NS_WARN_IF( + !Write(aCx, aValue, JS::UndefinedHandleValue, cloneDataPolicy))) { + // Ignore the message. + return false; + } + + return true; + } + + ConsoleStructuredCloneData mClonedData; +}; + +class ConsoleWorkletRunnable : public Runnable, public ConsoleRunnable { + protected: + explicit ConsoleWorkletRunnable(Console* aConsole) + : Runnable("dom::console::ConsoleWorkletRunnable"), + mConsoleData(aConsole->GetOrCreateMainThreadData()) { + WorkletThread::AssertIsOnWorkletThread(); + nsCOMPtr global = do_QueryInterface(aConsole->mGlobal); + MOZ_ASSERT(global); + mWorkletImpl = global->Impl(); + MOZ_ASSERT(mWorkletImpl); + } + + ~ConsoleWorkletRunnable() override = default; + + protected: + RefPtr mConsoleData; + + RefPtr mWorkletImpl; +}; + +// This runnable appends a CallData object into the Console queue running on +// the main-thread. +class ConsoleCallDataWorkletRunnable final : public ConsoleWorkletRunnable { + public: + static already_AddRefed Create( + JSContext* aCx, Console* aConsole, ConsoleCallData* aConsoleData, + const Sequence& aArguments) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr runnable = + new ConsoleCallDataWorkletRunnable(aConsole, aConsoleData); + + if (!runnable->WriteArguments(aCx, aArguments)) { + return nullptr; + } + + return runnable.forget(); + } + + private: + ConsoleCallDataWorkletRunnable(Console* aConsole, ConsoleCallData* aCallData) + : ConsoleWorkletRunnable(aConsole), mCallData(aCallData) { + WorkletThread::AssertIsOnWorkletThread(); + MOZ_ASSERT(aCallData); + aCallData->AssertIsOnOwningThread(); + + const WorkletLoadInfo& loadInfo = mWorkletImpl->LoadInfo(); + mCallData->SetIDs(loadInfo.OuterWindowID(), loadInfo.InnerWindowID()); + } + + ~ConsoleCallDataWorkletRunnable() override = default; + + NS_IMETHOD Run() override { + AssertIsOnMainThread(); + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + JSObject* sandbox = + mConsoleData->GetOrCreateSandbox(cx, mWorkletImpl->Principal()); + JS::Rooted global(cx, sandbox); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + // The CreateSandbox call returns a proxy to the actual sandbox object. We + // don't need a proxy here. + global = js::UncheckedUnwrap(global); + + JSAutoRealm ar(cx, global); + + // We don't need to set a parent object in mCallData bacause there are not + // DOM objects exposed to worklet. + + ProcessCallData(cx, mConsoleData, mCallData); + + return NS_OK; + } + + RefPtr mCallData; +}; + +class ConsoleWorkerRunnable : public WorkerProxyToMainThreadRunnable, + public ConsoleRunnable { + public: + explicit ConsoleWorkerRunnable(Console* aConsole) + : mConsoleData(aConsole->GetOrCreateMainThreadData()) {} + + ~ConsoleWorkerRunnable() override = default; + + bool Dispatch(JSContext* aCx, const Sequence& aArguments) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (NS_WARN_IF(!WriteArguments(aCx, aArguments))) { + RunBackOnWorkerThreadForCleanup(workerPrivate); + return false; + } + + if (NS_WARN_IF(!WorkerProxyToMainThreadRunnable::Dispatch(workerPrivate))) { + // RunBackOnWorkerThreadForCleanup() will be called by + // WorkerProxyToMainThreadRunnable::Dispatch(). + return false; + } + + return true; + } + + protected: + void RunOnMainThread(WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + AssertIsOnMainThread(); + + // Walk up to our containing page + WorkerPrivate* wp = aWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + nsCOMPtr window = wp->GetWindow(); + if (!window) { + RunWindowless(aWorkerPrivate); + } else { + RunWithWindow(aWorkerPrivate, window); + } + } + + void RunWithWindow(WorkerPrivate* aWorkerPrivate, + nsPIDOMWindowInner* aWindow) { + MOZ_ASSERT(aWorkerPrivate); + AssertIsOnMainThread(); + + AutoJSAPI jsapi; + MOZ_ASSERT(aWindow); + + RefPtr win = nsGlobalWindowInner::Cast(aWindow); + if (NS_WARN_IF(!jsapi.Init(win))) { + return; + } + + nsCOMPtr outerWindow = aWindow->GetOuterWindow(); + if (NS_WARN_IF(!outerWindow)) { + return; + } + + RunConsole(jsapi.cx(), aWindow->AsGlobal(), aWorkerPrivate, outerWindow, + aWindow); + } + + void RunWindowless(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + AssertIsOnMainThread(); + + WorkerPrivate* wp = aWorkerPrivate; + while (wp->GetParent()) { + wp = wp->GetParent(); + } + + MOZ_ASSERT(!wp->GetWindow()); + + AutoJSAPI jsapi; + jsapi.Init(); + + JSContext* cx = jsapi.cx(); + + JS::Rooted global( + cx, mConsoleData->GetOrCreateSandbox(cx, wp->GetPrincipal())); + if (NS_WARN_IF(!global)) { + return; + } + + // The GetOrCreateSandbox call returns a proxy to the actual sandbox object. + // We don't need a proxy here. + global = js::UncheckedUnwrap(global); + + JSAutoRealm ar(cx, global); + + nsCOMPtr globalObject = xpc::NativeGlobal(global); + if (NS_WARN_IF(!globalObject)) { + return; + } + + RunConsole(cx, globalObject, aWorkerPrivate, nullptr, nullptr); + } + + void RunBackOnWorkerThreadForCleanup(WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + // This method is called in the main-thread. + virtual void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, + WorkerPrivate* aWorkerPrivate, + nsPIDOMWindowOuter* aOuterWindow, + nsPIDOMWindowInner* aInnerWindow) = 0; + + bool ForMessaging() const override { return true; } + + RefPtr mConsoleData; +}; + +// This runnable appends a CallData object into the Console queue running on +// the main-thread. +class ConsoleCallDataWorkerRunnable final : public ConsoleWorkerRunnable { + public: + ConsoleCallDataWorkerRunnable(Console* aConsole, ConsoleCallData* aCallData) + : ConsoleWorkerRunnable(aConsole), mCallData(aCallData) { + MOZ_ASSERT(aCallData); + mCallData->AssertIsOnOwningThread(); + } + + private: + ~ConsoleCallDataWorkerRunnable() override = default; + + void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, + WorkerPrivate* aWorkerPrivate, + nsPIDOMWindowOuter* aOuterWindow, + nsPIDOMWindowInner* aInnerWindow) override { + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aWorkerPrivate); + AssertIsOnMainThread(); + + // The windows have to run in parallel. + MOZ_ASSERT(!!aOuterWindow == !!aInnerWindow); + + if (aOuterWindow) { + mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID()); + } else { + ConsoleStackEntry frame; + if (mCallData->mTopStackFrame) { + frame = *mCallData->mTopStackFrame; + } + + nsString id = frame.mFilename; + nsString innerID; + if (aWorkerPrivate->IsSharedWorker()) { + innerID = u"SharedWorker"_ns; + } else if (aWorkerPrivate->IsServiceWorker()) { + innerID = u"ServiceWorker"_ns; + // Use scope as ID so the webconsole can decide if the message should + // show up per tab + CopyASCIItoUTF16(aWorkerPrivate->ServiceWorkerScope(), id); + } else { + innerID = u"Worker"_ns; + } + + mCallData->SetIDs(id, innerID); + } + + mClonedData.mGlobal = aGlobal; + + ProcessCallData(aCx, mConsoleData, mCallData); + + mClonedData.mGlobal = nullptr; + } + + RefPtr mCallData; +}; + +// This runnable calls ProfileMethod() on the console on the main-thread. +class ConsoleProfileWorkletRunnable final : public ConsoleWorkletRunnable { + public: + static already_AddRefed Create( + JSContext* aCx, Console* aConsole, Console::MethodName aName, + const nsAString& aAction, const Sequence& aArguments) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr runnable = + new ConsoleProfileWorkletRunnable(aConsole, aName, aAction); + + if (!runnable->WriteArguments(aCx, aArguments)) { + return nullptr; + } + + return runnable.forget(); + } + + private: + ConsoleProfileWorkletRunnable(Console* aConsole, Console::MethodName aName, + const nsAString& aAction) + : ConsoleWorkletRunnable(aConsole), mName(aName), mAction(aAction) { + MOZ_ASSERT(aConsole); + } + + NS_IMETHOD Run() override { + AssertIsOnMainThread(); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + JSObject* sandbox = + mConsoleData->GetOrCreateSandbox(cx, mWorkletImpl->Principal()); + JS::Rooted global(cx, sandbox); + if (NS_WARN_IF(!global)) { + return NS_ERROR_FAILURE; + } + + // The CreateSandbox call returns a proxy to the actual sandbox object. We + // don't need a proxy here. + global = js::UncheckedUnwrap(global); + + JSAutoRealm ar(cx, global); + + // We don't need to set a parent object in mCallData bacause there are not + // DOM objects exposed to worklet. + ProcessProfileData(cx, mName, mAction); + + return NS_OK; + } + + Console::MethodName mName; + nsString mAction; +}; + +// This runnable calls ProfileMethod() on the console on the main-thread. +class ConsoleProfileWorkerRunnable final : public ConsoleWorkerRunnable { + public: + ConsoleProfileWorkerRunnable(Console* aConsole, Console::MethodName aName, + const nsAString& aAction) + : ConsoleWorkerRunnable(aConsole), mName(aName), mAction(aAction) { + MOZ_ASSERT(aConsole); + } + + private: + void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, + WorkerPrivate* aWorkerPrivate, + nsPIDOMWindowOuter* aOuterWindow, + nsPIDOMWindowInner* aInnerWindow) override { + AssertIsOnMainThread(); + MOZ_ASSERT(aGlobal); + + mClonedData.mGlobal = aGlobal; + + ProcessProfileData(aCx, mName, mAction); + + mClonedData.mGlobal = nullptr; + } + + Console::MethodName mName; + nsString mAction; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(Console) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsoleEventNotifier) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDumpFunction) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsoleEventNotifier) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDumpFunction) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console) + for (uint32_t i = 0; i < tmp->mArgumentStorage.length(); ++i) { + tmp->mArgumentStorage[i].Trace(aCallbacks, aClosure); + } +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Console) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Console) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Console) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed Console::Create(JSContext* aCx, + nsPIDOMWindowInner* aWindow, + ErrorResult& aRv) { + MOZ_ASSERT_IF(NS_IsMainThread(), aWindow); + + uint64_t outerWindowID = 0; + uint64_t innerWindowID = 0; + + if (aWindow) { + innerWindowID = aWindow->WindowID(); + + // Without outerwindow any console message coming from this object will not + // shown in the devtools webconsole. But this should be fine because + // probably we are shutting down, or the window is CCed/GCed. + nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow(); + if (outerWindow) { + outerWindowID = outerWindow->WindowID(); + } + } + + RefPtr console = new Console(aCx, nsGlobalWindowInner::Cast(aWindow), + outerWindowID, innerWindowID); + console->Initialize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return console.forget(); +} + +/* static */ +already_AddRefed Console::CreateForWorklet(JSContext* aCx, + nsIGlobalObject* aGlobal, + uint64_t aOuterWindowID, + uint64_t aInnerWindowID, + ErrorResult& aRv) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr console = + new Console(aCx, aGlobal, aOuterWindowID, aInnerWindowID); + console->Initialize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return console.forget(); +} + +Console::Console(JSContext* aCx, nsIGlobalObject* aGlobal, + uint64_t aOuterWindowID, uint64_t aInnerWindowID) + : mGlobal(aGlobal), + mOuterID(aOuterWindowID), + mInnerID(aInnerWindowID), + mDumpToStdout(false), + mChromeInstance(false), + mMaxLogLevel(ConsoleLogLevel::All), + mStatus(eUnknown), + mCreationTimeStamp(TimeStamp::Now()) { + // Let's enable the dumping to stdout by default for chrome. + if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) { + mDumpToStdout = StaticPrefs::devtools_console_stdout_chrome(); + } else { + mDumpToStdout = StaticPrefs::devtools_console_stdout_content(); + } + + mozilla::HoldJSObjects(this); +} + +Console::~Console() { + AssertIsOnOwningThread(); + Shutdown(); + mozilla::DropJSObjects(this); +} + +void Console::Initialize(ErrorResult& aRv) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mStatus == eUnknown); + + if (NS_IsMainThread()) { + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mInnerID) { + aRv = obs->AddObserver(this, "inner-window-destroyed", true); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + aRv = obs->AddObserver(this, "memory-pressure", true); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + mStatus = eInitialized; +} + +void Console::Shutdown() { + AssertIsOnOwningThread(); + + if (mStatus == eUnknown || mStatus == eShuttingDown) { + return; + } + + if (NS_IsMainThread()) { + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + obs->RemoveObserver(this, "memory-pressure"); + } + } + + mTimerRegistry.Clear(); + mCounterRegistry.Clear(); + + ClearStorage(); + mCallDataStorage.Clear(); + + mStatus = eShuttingDown; +} + +NS_IMETHODIMP +Console::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + + if (!strcmp(aTopic, "inner-window-destroyed")) { + nsCOMPtr wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + + if (innerID == mInnerID) { + Shutdown(); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "memory-pressure")) { + ClearStorage(); + return NS_OK; + } + + return NS_OK; +} + +void Console::ClearStorage() { + mCallDataStorage.Clear(); + mArgumentStorage.clearAndFree(); +} + +#define METHOD(name, string) \ + /* static */ void Console::name(const GlobalObject& aGlobal, \ + const Sequence& aData) { \ + Method(aGlobal, Method##name, nsLiteralString(string), aData); \ + } + +METHOD(Log, u"log") +METHOD(Info, u"info") +METHOD(Warn, u"warn") +METHOD(Error, u"error") +METHOD(Exception, u"exception") +METHOD(Debug, u"debug") +METHOD(Table, u"table") +METHOD(Trace, u"trace") + +// Displays an interactive listing of all the properties of an object. +METHOD(Dir, u"dir"); +METHOD(Dirxml, u"dirxml"); + +METHOD(Group, u"group") +METHOD(GroupCollapsed, u"groupCollapsed") + +#undef METHOD + +/* static */ +void Console::Clear(const GlobalObject& aGlobal) { + const Sequence data; + Method(aGlobal, MethodClear, u"clear"_ns, data); +} + +/* static */ +void Console::GroupEnd(const GlobalObject& aGlobal) { + const Sequence data; + Method(aGlobal, MethodGroupEnd, u"groupEnd"_ns, data); +} + +/* static */ +void Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence(), MethodTime, u"time"_ns); +} + +/* static */ +void Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence(), MethodTimeEnd, + u"timeEnd"_ns); +} + +/* static */ +void Console::TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence& aData) { + StringMethod(aGlobal, aLabel, aData, MethodTimeLog, u"timeLog"_ns); +} + +/* static */ +void Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence& aData, + MethodName aMethodName, + const nsAString& aMethodString) { + RefPtr console = GetConsole(aGlobal); + if (!console) { + return; + } + + console->StringMethodInternal(aGlobal.Context(), aLabel, aData, aMethodName, + aMethodString); +} + +void Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel, + const Sequence& aData, + MethodName aMethodName, + const nsAString& aMethodString) { + ConsoleCommon::ClearException ce(aCx); + + Sequence data; + SequenceRooter rooter(aCx, &data); + + JS::Rooted value(aCx); + if (!dom::ToJSValue(aCx, aLabel, &value)) { + return; + } + + if (!data.AppendElement(value, fallible)) { + return; + } + + for (uint32_t i = 0; i < aData.Length(); ++i) { + if (!data.AppendElement(aData[i], fallible)) { + return; + } + } + + MethodInternal(aCx, aMethodName, aMethodString, data); +} + +/* static */ +void Console::TimeStamp(const GlobalObject& aGlobal, + const JS::Handle aData) { + JSContext* cx = aGlobal.Context(); + + ConsoleCommon::ClearException ce(cx); + + Sequence data; + SequenceRooter rooter(cx, &data); + + if (aData.isString() && !data.AppendElement(aData, fallible)) { + return; + } + + Method(aGlobal, MethodTimeStamp, u"timeStamp"_ns, data); +} + +/* static */ +void Console::Profile(const GlobalObject& aGlobal, + const Sequence& aData) { + ProfileMethod(aGlobal, MethodProfile, u"profile"_ns, aData); +} + +/* static */ +void Console::ProfileEnd(const GlobalObject& aGlobal, + const Sequence& aData) { + ProfileMethod(aGlobal, MethodProfileEnd, u"profileEnd"_ns, aData); +} + +/* static */ +void Console::ProfileMethod(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aAction, + const Sequence& aData) { + RefPtr console = GetConsole(aGlobal); + if (!console) { + return; + } + + JSContext* cx = aGlobal.Context(); + console->ProfileMethodInternal(cx, aName, aAction, aData); +} + +void Console::ProfileMethodInternal(JSContext* aCx, MethodName aMethodName, + const nsAString& aAction, + const Sequence& aData) { + if (!ShouldProceed(aMethodName)) { + return; + } + + MaybeExecuteDumpFunction(aCx, aAction, aData, nullptr); + + if (WorkletThread::IsOnWorkletThread()) { + RefPtr runnable = + ConsoleProfileWorkletRunnable::Create(aCx, this, aMethodName, aAction, + aData); + if (!runnable) { + return; + } + + NS_DispatchToMainThread(runnable.forget()); + return; + } + + if (!NS_IsMainThread()) { + // Here we are in a worker thread. + RefPtr runnable = + new ConsoleProfileWorkerRunnable(this, aMethodName, aAction); + + runnable->Dispatch(aCx, aData); + return; + } + + ProfileMethodMainthread(aCx, aAction, aData); +} + +// static +void Console::ProfileMethodMainthread(JSContext* aCx, const nsAString& aAction, + const Sequence& aData) { + MOZ_ASSERT(NS_IsMainThread()); + ConsoleCommon::ClearException ce(aCx); + + RootedDictionary event(aCx); + event.mAction = aAction; + event.mChromeContext = nsContentUtils::ThreadsafeIsSystemCaller(aCx); + + event.mArguments.Construct(); + Sequence& sequence = event.mArguments.Value(); + + for (uint32_t i = 0; i < aData.Length(); ++i) { + if (!sequence.AppendElement(aData[i], fallible)) { + return; + } + } + + JS::Rooted eventValue(aCx); + if (!ToJSValue(aCx, event, &eventValue)) { + return; + } + + JS::Rooted eventObj(aCx, &eventValue.toObject()); + MOZ_ASSERT(eventObj); + + if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue, + JSPROP_ENUMERATE)) { + return; + } + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + nsCOMPtr wrapper; + const nsIID& iid = NS_GET_IID(nsISupports); + + if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) { + return; + } + + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(wrapper, "console-api-profiler", nullptr); + } +} + +/* static */ +void Console::Assert(const GlobalObject& aGlobal, bool aCondition, + const Sequence& aData) { + if (!aCondition) { + Method(aGlobal, MethodAssert, u"assert"_ns, aData); + } +} + +/* static */ +void Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence(), MethodCount, + u"count"_ns); +} + +/* static */ +void Console::CountReset(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence(), MethodCountReset, + u"countReset"_ns); +} + +namespace { + +void StackFrameToStackEntry(JSContext* aCx, nsIStackFrame* aStackFrame, + ConsoleStackEntry& aStackEntry) { + MOZ_ASSERT(aStackFrame); + + aStackFrame->GetFilename(aCx, aStackEntry.mFilename); + + aStackEntry.mSourceId = aStackFrame->GetSourceId(aCx); + aStackEntry.mLineNumber = aStackFrame->GetLineNumber(aCx); + aStackEntry.mColumnNumber = aStackFrame->GetColumnNumber(aCx); + + aStackFrame->GetName(aCx, aStackEntry.mFunctionName); + + nsString cause; + aStackFrame->GetAsyncCause(aCx, cause); + if (!cause.IsEmpty()) { + aStackEntry.mAsyncCause.Construct(cause); + } +} + +void ReifyStack(JSContext* aCx, nsIStackFrame* aStack, + nsTArray& aRefiedStack) { + nsCOMPtr stack(aStack); + + while (stack) { + ConsoleStackEntry& data = *aRefiedStack.AppendElement(); + StackFrameToStackEntry(aCx, stack, data); + + nsCOMPtr caller = stack->GetCaller(aCx); + + if (!caller) { + caller = stack->GetAsyncCaller(aCx); + } + stack.swap(caller); + } +} + +} // anonymous namespace + +// Queue a call to a console method. See the CALL_DELAY constant. +/* static */ +void Console::Method(const GlobalObject& aGlobal, MethodName aMethodName, + const nsAString& aMethodString, + const Sequence& aData) { + RefPtr console = GetConsole(aGlobal); + if (!console) { + return; + } + + console->MethodInternal(aGlobal.Context(), aMethodName, aMethodString, aData); +} + +void Console::MethodInternal(JSContext* aCx, MethodName aMethodName, + const nsAString& aMethodString, + const Sequence& aData) { + if (!ShouldProceed(aMethodName)) { + return; + } + + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + RefPtr callData = + new ConsoleCallData(aMethodName, aMethodString, this); + if (!StoreCallData(aCx, callData, aData)) { + return; + } + + OriginAttributes oa; + + if (NS_IsMainThread()) { + if (mGlobal) { + // Save the principal's OriginAttributes in the console event data + // so that we will be able to filter messages by origin attributes. + nsCOMPtr sop = do_QueryInterface(mGlobal); + if (NS_WARN_IF(!sop)) { + return; + } + + nsCOMPtr principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return; + } + + oa = principal->OriginAttributesRef(); + callData->SetAddonId(principal); + +#ifdef DEBUG + if (!principal->IsSystemPrincipal()) { + nsCOMPtr webNav = do_GetInterface(mGlobal); + if (webNav) { + nsCOMPtr loadContext = do_QueryInterface(webNav); + MOZ_ASSERT(loadContext); + + bool pb; + if (NS_SUCCEEDED(loadContext->GetUsePrivateBrowsing(&pb))) { + MOZ_ASSERT(pb == !!oa.mPrivateBrowsingId); + } + } + } +#endif + } + } else if (WorkletThread::IsOnWorkletThread()) { + nsCOMPtr global = do_QueryInterface(mGlobal); + MOZ_ASSERT(global); + oa = global->Impl()->OriginAttributesRef(); + } else { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + oa = workerPrivate->GetOriginAttributes(); + } + + callData->SetOriginAttributes(oa); + + JS::StackCapture captureMode = + ShouldIncludeStackTrace(aMethodName) + ? JS::StackCapture(JS::MaxFrames(DEFAULT_MAX_STACKTRACE_DEPTH)) + : JS::StackCapture(JS::FirstSubsumedFrame(aCx)); + nsCOMPtr stack = CreateStack(aCx, std::move(captureMode)); + + if (stack) { + callData->mTopStackFrame.emplace(); + StackFrameToStackEntry(aCx, stack, *callData->mTopStackFrame); + } + + if (NS_IsMainThread()) { + callData->mStack = stack; + } else { + // nsIStackFrame is not threadsafe, so we need to snapshot it now, + // before we post our runnable to the main thread. + callData->mReifiedStack.emplace(); + ReifyStack(aCx, stack, *callData->mReifiedStack); + } + + DOMHighResTimeStamp monotonicTimer; + + // Monotonic timer for 'time', 'timeLog' and 'timeEnd' + if ((aMethodName == MethodTime || aMethodName == MethodTimeLog || + aMethodName == MethodTimeEnd || aMethodName == MethodTimeStamp) && + !MonotonicTimer(aCx, aMethodName, aData, &monotonicTimer)) { + return; + } + + if (aMethodName == MethodTime && !aData.IsEmpty()) { + callData->mStartTimerStatus = + StartTimer(aCx, aData[0], monotonicTimer, callData->mStartTimerLabel, + &callData->mStartTimerValue); + } + + else if (aMethodName == MethodTimeEnd && !aData.IsEmpty()) { + callData->mLogTimerStatus = + LogTimer(aCx, aData[0], monotonicTimer, callData->mLogTimerLabel, + &callData->mLogTimerDuration, true /* Cancel timer */); + } + + else if (aMethodName == MethodTimeLog && !aData.IsEmpty()) { + callData->mLogTimerStatus = + LogTimer(aCx, aData[0], monotonicTimer, callData->mLogTimerLabel, + &callData->mLogTimerDuration, false /* Cancel timer */); + } + + else if (aMethodName == MethodCount) { + callData->mCountValue = IncreaseCounter(aCx, aData, callData->mCountLabel); + if (!callData->mCountValue) { + return; + } + } + + else if (aMethodName == MethodCountReset) { + callData->mCountValue = ResetCounter(aCx, aData, callData->mCountLabel); + if (callData->mCountLabel.IsEmpty()) { + return; + } + } + + // Before processing this CallData differently, it's time to call the dump + // function. + if (aMethodName == MethodTrace || aMethodName == MethodAssert) { + MaybeExecuteDumpFunction(aCx, aMethodString, aData, stack); + } else if ((aMethodName == MethodTime || aMethodName == MethodTimeEnd) && + !aData.IsEmpty()) { + MaybeExecuteDumpFunctionForTime(aCx, aMethodName, aMethodString, + monotonicTimer, aData[0]); + } else { + MaybeExecuteDumpFunction(aCx, aMethodString, aData, nullptr); + } + + if (NS_IsMainThread()) { + if (mInnerID) { + callData->SetIDs(mOuterID, mInnerID); + } else if (!mPassedInnerID.IsEmpty()) { + callData->SetIDs(u"jsm"_ns, mPassedInnerID); + } else { + nsAutoString filename; + if (callData->mTopStackFrame.isSome()) { + filename = callData->mTopStackFrame->mFilename; + } + + callData->SetIDs(u"jsm"_ns, filename); + } + + GetOrCreateMainThreadData()->ProcessCallData(aCx, callData, aData); + + // Just because we don't want to expose + // retrieveConsoleEvents/setConsoleEventHandler to main-thread, we can + // cleanup the mCallDataStorage: + UnstoreCallData(callData); + return; + } + + if (WorkletThread::IsOnWorkletThread()) { + RefPtr runnable = + ConsoleCallDataWorkletRunnable::Create(aCx, this, callData, aData); + if (!runnable) { + return; + } + + NS_DispatchToMainThread(runnable); + return; + } + + // We do this only in workers for now. + NotifyHandler(aCx, aData, callData); + + if (StaticPrefs::dom_worker_console_dispatch_events_to_main_thread()) { + RefPtr runnable = + new ConsoleCallDataWorkerRunnable(this, callData); + Unused << NS_WARN_IF(!runnable->Dispatch(aCx, aData)); + } +} + +MainThreadConsoleData* Console::GetOrCreateMainThreadData() { + AssertIsOnOwningThread(); + + if (!mMainThreadData) { + mMainThreadData = new MainThreadConsoleData(); + } + + return mMainThreadData; +} + +// We store information to lazily compute the stack in the reserved slots of +// LazyStackGetter. The first slot always stores a JS object: it's either the +// JS wrapper of the nsIStackFrame or the actual reified stack representation. +// The second slot is a PrivateValue() holding an nsIStackFrame* when we haven't +// reified the stack yet, or an UndefinedValue() otherwise. +enum { SLOT_STACKOBJ, SLOT_RAW_STACK }; + +bool LazyStackGetter(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { + JS::CallArgs args = CallArgsFromVp(aArgc, aVp); + JS::Rooted callee(aCx, &args.callee()); + + JS::Value v = js::GetFunctionNativeReserved(&args.callee(), SLOT_RAW_STACK); + if (v.isUndefined()) { + // Already reified. + args.rval().set(js::GetFunctionNativeReserved(callee, SLOT_STACKOBJ)); + return true; + } + + nsIStackFrame* stack = reinterpret_cast(v.toPrivate()); + nsTArray reifiedStack; + ReifyStack(aCx, stack, reifiedStack); + + JS::Rooted stackVal(aCx); + if (NS_WARN_IF(!ToJSValue(aCx, reifiedStack, &stackVal))) { + return false; + } + + MOZ_ASSERT(stackVal.isObject()); + + js::SetFunctionNativeReserved(callee, SLOT_STACKOBJ, stackVal); + js::SetFunctionNativeReserved(callee, SLOT_RAW_STACK, JS::UndefinedValue()); + + args.rval().set(stackVal); + return true; +} + +void MainThreadConsoleData::ProcessCallData( + JSContext* aCx, ConsoleCallData* aData, + const Sequence& aArguments) { + AssertIsOnMainThread(); + MOZ_ASSERT(aData); + + JS::Rooted eventValue(aCx); + + // We want to create a console event object and pass it to our + // nsIConsoleAPIStorage implementation. We want to define some accessor + // properties on this object, and those will need to keep an nsIStackFrame + // alive. But nsIStackFrame cannot be wrapped in an untrusted scope. And + // further, passing untrusted objects to system code is likely to run afoul of + // Object Xrays. So we want to wrap in a system-principal scope here. But + // which one? We could cheat and try to get the underlying JSObject* of + // mStorage, but that's a bit fragile. Instead, we just use the junk scope, + // with explicit permission from the XPConnect module owner. If you're + // tempted to do that anywhere else, talk to said module owner first. + + // aCx and aArguments are in the same compartment. + JS::Rooted targetScope(aCx, xpc::PrivilegedJunkScope()); + if (NS_WARN_IF(!Console::PopulateConsoleNotificationInTheTargetScope( + aCx, aArguments, targetScope, &eventValue, aData, &mGroupStack))) { + return; + } + + if (!mStorage) { + mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1"); + } + + if (!mStorage) { + NS_WARNING("Failed to get the ConsoleAPIStorage service."); + return; + } + + nsAutoString innerID; + + MOZ_ASSERT(aData->mIDType != ConsoleCallData::eUnknown); + if (aData->mIDType == ConsoleCallData::eString) { + innerID = aData->mInnerIDString; + } else { + MOZ_ASSERT(aData->mIDType == ConsoleCallData::eNumber); + innerID.AppendInt(aData->mInnerIDNumber); + } + + if (aData->mMethodName == Console::MethodClear) { + DebugOnly rv = mStorage->ClearEvents(innerID); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "ClearEvents failed"); + } + + if (NS_FAILED(mStorage->RecordEvent(innerID, eventValue))) { + NS_WARNING("Failed to record a console event."); + } +} + +/* static */ +bool Console::PopulateConsoleNotificationInTheTargetScope( + JSContext* aCx, const Sequence& aArguments, + JS::Handle aTargetScope, + JS::MutableHandle aEventValue, ConsoleCallData* aData, + nsTArray* aGroupStack) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aData); + MOZ_ASSERT(aTargetScope); + MOZ_ASSERT(JS_IsGlobalObject(aTargetScope)); + + ConsoleStackEntry frame; + if (aData->mTopStackFrame) { + frame = *aData->mTopStackFrame; + } + + ConsoleCommon::ClearException ce(aCx); + RootedDictionary event(aCx); + + event.mAddonId = aData->mAddonId; + + event.mID.Construct(); + event.mInnerID.Construct(); + + event.mChromeContext = nsContentUtils::ThreadsafeIsSystemCaller(aCx); + + if (aData->mIDType == ConsoleCallData::eString) { + event.mID.Value().SetAsString() = aData->mOuterIDString; + event.mInnerID.Value().SetAsString() = aData->mInnerIDString; + } else if (aData->mIDType == ConsoleCallData::eNumber) { + event.mID.Value().SetAsUnsignedLongLong() = aData->mOuterIDNumber; + event.mInnerID.Value().SetAsUnsignedLongLong() = aData->mInnerIDNumber; + } else { + // aData->mIDType can be eUnknown when we dispatch notifications via + // mConsoleEventNotifier. + event.mID.Value().SetAsUnsignedLongLong() = 0; + event.mInnerID.Value().SetAsUnsignedLongLong() = 0; + } + + event.mConsoleID = aData->mConsoleID; + event.mLevel = aData->mMethodString; + event.mFilename = frame.mFilename; + event.mPrefix = aData->mPrefix; + + nsCOMPtr filenameURI; + nsAutoCString pass; + if (NS_IsMainThread() && + NS_SUCCEEDED(NS_NewURI(getter_AddRefs(filenameURI), frame.mFilename)) && + NS_SUCCEEDED(filenameURI->GetPassword(pass)) && !pass.IsEmpty()) { + nsCOMPtr safeURI = + do_QueryInterface(filenameURI); + nsAutoCString spec; + if (safeURI && NS_SUCCEEDED(safeURI->GetSensitiveInfoHiddenSpec(spec))) { + CopyUTF8toUTF16(spec, event.mFilename); + } + } + + event.mSourceId = frame.mSourceId; + event.mLineNumber = frame.mLineNumber; + event.mColumnNumber = frame.mColumnNumber; + event.mFunctionName = frame.mFunctionName; + event.mTimeStamp = aData->mMicroSecondTimeStamp / PR_USEC_PER_MSEC; + event.mMicroSecondTimeStamp = aData->mMicroSecondTimeStamp; + event.mPrivate = !!aData->mOriginAttributes.mPrivateBrowsingId; + + switch (aData->mMethodName) { + case MethodLog: + case MethodInfo: + case MethodWarn: + case MethodError: + case MethodException: + case MethodDebug: + case MethodAssert: + case MethodGroup: + case MethodGroupCollapsed: + case MethodTrace: + event.mArguments.Construct(); + event.mStyles.Construct(); + if (NS_WARN_IF(!ProcessArguments(aCx, aArguments, + event.mArguments.Value(), + event.mStyles.Value()))) { + return false; + } + + break; + + default: + event.mArguments.Construct(); + if (NS_WARN_IF( + !event.mArguments.Value().AppendElements(aArguments, fallible))) { + return false; + } + } + + if (aData->mMethodName == MethodGroup || + aData->mMethodName == MethodGroupCollapsed) { + ComposeAndStoreGroupName(aCx, event.mArguments.Value(), event.mGroupName, + aGroupStack); + } + + else if (aData->mMethodName == MethodGroupEnd) { + if (!UnstoreGroupName(event.mGroupName, aGroupStack)) { + return false; + } + } + + else if (aData->mMethodName == MethodTime && !aArguments.IsEmpty()) { + event.mTimer = CreateStartTimerValue(aCx, aData->mStartTimerLabel, + aData->mStartTimerStatus); + } + + else if ((aData->mMethodName == MethodTimeEnd || + aData->mMethodName == MethodTimeLog) && + !aArguments.IsEmpty()) { + event.mTimer = CreateLogOrEndTimerValue(aCx, aData->mLogTimerLabel, + aData->mLogTimerDuration, + aData->mLogTimerStatus); + } + + else if (aData->mMethodName == MethodCount || + aData->mMethodName == MethodCountReset) { + event.mCounter = CreateCounterOrResetCounterValue(aCx, aData->mCountLabel, + aData->mCountValue); + } + + JSAutoRealm ar2(aCx, aTargetScope); + + if (NS_WARN_IF(!ToJSValue(aCx, event, aEventValue))) { + return false; + } + + JS::Rooted eventObj(aCx, &aEventValue.toObject()); + if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventObj, + JSPROP_ENUMERATE))) { + return false; + } + + if (ShouldIncludeStackTrace(aData->mMethodName)) { + // Now define the "stacktrace" property on eventObj. There are two cases + // here. Either we came from a worker and have a reified stack, or we want + // to define a getter that will lazily reify the stack. + if (aData->mReifiedStack) { + JS::Rooted stacktrace(aCx); + if (NS_WARN_IF(!ToJSValue(aCx, *aData->mReifiedStack, &stacktrace)) || + NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", stacktrace, + JSPROP_ENUMERATE))) { + return false; + } + } else { + JSFunction* fun = + js::NewFunctionWithReserved(aCx, LazyStackGetter, 0, 0, "stacktrace"); + if (NS_WARN_IF(!fun)) { + return false; + } + + JS::Rooted funObj(aCx, JS_GetFunctionObject(fun)); + + // We want to store our stack in the function and have it stay alive. But + // we also need sane access to the C++ nsIStackFrame. So store both a JS + // wrapper and the raw pointer: the former will keep the latter alive. + JS::Rooted stackVal(aCx); + nsresult rv = nsContentUtils::WrapNative(aCx, aData->mStack, &stackVal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + js::SetFunctionNativeReserved(funObj, SLOT_STACKOBJ, stackVal); + js::SetFunctionNativeReserved(funObj, SLOT_RAW_STACK, + JS::PrivateValue(aData->mStack.get())); + + if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", funObj, + nullptr, JSPROP_ENUMERATE))) { + return false; + } + } + } + + return true; +} + +namespace { + +// Helper method for ProcessArguments. Flushes output, if non-empty, to +// aSequence. +bool FlushOutput(JSContext* aCx, Sequence& aSequence, + nsString& aOutput) { + if (!aOutput.IsEmpty()) { + JS::Rooted str( + aCx, JS_NewUCStringCopyN(aCx, aOutput.get(), aOutput.Length())); + if (NS_WARN_IF(!str)) { + return false; + } + + if (NS_WARN_IF(!aSequence.AppendElement(JS::StringValue(str), fallible))) { + return false; + } + + aOutput.Truncate(); + } + + return true; +} + +} // namespace + +static void MakeFormatString(nsCString& aFormat, int32_t aInteger, + int32_t aMantissa, char aCh) { + aFormat.Append('%'); + if (aInteger >= 0) { + aFormat.AppendInt(aInteger); + } + + if (aMantissa >= 0) { + aFormat.Append('.'); + aFormat.AppendInt(aMantissa); + } + + aFormat.Append(aCh); +} + +// If the first JS::Value of the array is a string, this method uses it to +// format a string. The supported sequences are: +// %s - string +// %d,%i - integer +// %f - double +// %o,%O - a JS object. +// %c - style string. +// The output is an array where any object is a separated item, the rest is +// unified in a format string. +// Example if the input is: +// "string: %s, integer: %d, object: %o, double: %d", 's', 1, window, 0.9 +// The output will be: +// [ "string: s, integer: 1, object: ", window, ", double: 0.9" ] +// +// The aStyles array is populated with the style strings that the function +// finds based the format string. The index of the styles matches the indexes +// of elements that need the custom styling from aSequence. For elements with +// no custom styling the array is padded with null elements. +static bool ProcessArguments(JSContext* aCx, const Sequence& aData, + Sequence& aSequence, + Sequence& aStyles) { + // This method processes the arguments as format strings (%d, %i, %s...) + // only if the first element of them is a valid and not-empty string. + + if (aData.IsEmpty()) { + return true; + } + + if (aData.Length() == 1 || !aData[0].isString()) { + return aSequence.AppendElements(aData, fallible); + } + + JS::Rooted format(aCx, aData[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, format)); + if (NS_WARN_IF(!jsString)) { + return false; + } + + nsAutoJSString string; + if (NS_WARN_IF(!string.init(aCx, jsString))) { + return false; + } + + if (string.IsEmpty()) { + return aSequence.AppendElements(aData, fallible); + } + + nsString::const_iterator start, end; + string.BeginReading(start); + string.EndReading(end); + + nsString output; + uint32_t index = 1; + + while (start != end) { + if (*start != '%') { + output.Append(*start); + ++start; + continue; + } + + ++start; + if (start == end) { + output.Append('%'); + break; + } + + if (*start == '%') { + output.Append(*start); + ++start; + continue; + } + + nsAutoString tmp; + tmp.Append('%'); + + int32_t integer = -1; + int32_t mantissa = -1; + + // Let's parse %. for %d and %f + if (*start >= '0' && *start <= '9') { + integer = 0; + + do { + integer = integer * 10 + *start - '0'; + tmp.Append(*start); + ++start; + } while (*start >= '0' && *start <= '9' && start != end); + } + + if (start == end) { + output.Append(tmp); + break; + } + + if (*start == '.') { + tmp.Append(*start); + ++start; + + if (start == end) { + output.Append(tmp); + break; + } + + // '.' must be followed by a number. + if (*start < '0' || *start > '9') { + output.Append(tmp); + continue; + } + + mantissa = 0; + + do { + mantissa = mantissa * 10 + *start - '0'; + tmp.Append(*start); + ++start; + } while (*start >= '0' && *start <= '9' && start != end); + + if (start == end) { + output.Append(tmp); + break; + } + } + + char ch = *start; + tmp.Append(ch); + ++start; + + switch (ch) { + case 'o': + case 'O': { + if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { + return false; + } + + JS::Rooted v(aCx); + if (index < aData.Length()) { + v = aData[index++]; + } + + if (NS_WARN_IF(!aSequence.AppendElement(v, fallible))) { + return false; + } + + break; + } + + case 'c': { + // If there isn't any output but there's already a style, then + // discard the previous style and use the next one instead. + if (output.IsEmpty() && !aStyles.IsEmpty()) { + aStyles.RemoveLastElement(); + } + + if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { + return false; + } + + if (index < aData.Length()) { + JS::Rooted v(aCx, aData[index++]); + JS::Rooted jsString(aCx, JS::ToString(aCx, v)); + if (NS_WARN_IF(!jsString)) { + return false; + } + + int32_t diff = aSequence.Length() - aStyles.Length(); + if (diff > 0) { + for (int32_t i = 0; i < diff; i++) { + if (NS_WARN_IF(!aStyles.AppendElement(VoidString(), fallible))) { + return false; + } + } + } + + nsAutoJSString string; + if (NS_WARN_IF(!string.init(aCx, jsString))) { + return false; + } + + if (NS_WARN_IF(!aStyles.AppendElement(string, fallible))) { + return false; + } + } + break; + } + + case 's': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (NS_WARN_IF(!jsString)) { + return false; + } + + nsAutoJSString v; + if (NS_WARN_IF(!v.init(aCx, jsString))) { + return false; + } + + output.Append(v); + } + break; + + case 'd': + case 'i': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + + if (value.isBigInt()) { + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (NS_WARN_IF(!jsString)) { + return false; + } + + nsAutoJSString v; + if (NS_WARN_IF(!v.init(aCx, jsString))) { + return false; + } + output.Append(v); + break; + } + + int32_t v; + if (NS_WARN_IF(!JS::ToInt32(aCx, value, &v))) { + return false; + } + + nsCString format; + MakeFormatString(format, integer, mantissa, 'd'); + output.AppendPrintf(format.get(), v); + } + break; + + case 'f': + if (index < aData.Length()) { + JS::Rooted value(aCx, aData[index++]); + + double v; + if (NS_WARN_IF(!JS::ToNumber(aCx, value, &v))) { + return false; + } + + // nspr returns "nan", but we want to expose it as "NaN" + if (std::isnan(v)) { + output.AppendFloat(v); + } else { + nsCString format; + MakeFormatString(format, integer, mantissa, 'f'); + output.AppendPrintf(format.get(), v); + } + } + break; + + default: + output.Append(tmp); + break; + } + } + + if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { + return false; + } + + // Discard trailing style element if there is no output to apply it to. + if (aStyles.Length() > aSequence.Length()) { + aStyles.TruncateLength(aSequence.Length()); + } + + // The rest of the array, if unused by the format string. + for (; index < aData.Length(); ++index) { + if (NS_WARN_IF(!aSequence.AppendElement(aData[index], fallible))) { + return false; + } + } + + return true; +} + +// Stringify and Concat all the JS::Value in a single string using ' ' as +// separator. The new group name will be stored in aGroupStack array. +static void ComposeAndStoreGroupName(JSContext* aCx, + const Sequence& aData, + nsAString& aName, + nsTArray* aGroupStack) { + StringJoinAppend( + aName, u" "_ns, aData, [aCx](nsAString& dest, const JS::Value& valueRef) { + JS::Rooted value(aCx, valueRef); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return; + } + + nsAutoJSString string; + if (!string.init(aCx, jsString)) { + return; + } + + dest.Append(string); + }); + + aGroupStack->AppendElement(aName); +} + +// Remove the last group name and return that name. It returns false if +// aGroupStack is empty. +static bool UnstoreGroupName(nsAString& aName, + nsTArray* aGroupStack) { + if (aGroupStack->IsEmpty()) { + return false; + } + + aName = aGroupStack->PopLastElement(); + return true; +} + +Console::TimerStatus Console::StartTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp, + nsAString& aTimerLabel, + DOMHighResTimeStamp* aTimerValue) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aTimerValue); + + *aTimerValue = 0; + + if (NS_WARN_IF(mTimerRegistry.Count() >= MAX_PAGE_TIMERS)) { + return eTimerMaxReached; + } + + JS::Rooted name(aCx, aName); + JS::Rooted jsString(aCx, JS::ToString(aCx, name)); + if (NS_WARN_IF(!jsString)) { + return eTimerJSException; + } + + nsAutoJSString label; + if (NS_WARN_IF(!label.init(aCx, jsString))) { + return eTimerJSException; + } + + aTimerLabel = label; + + if (mTimerRegistry.WithEntryHandle(label, [&](auto&& entry) { + if (entry) { + return true; + } + entry.Insert(aTimestamp); + return false; + })) { + return eTimerAlreadyExists; + } + + *aTimerValue = aTimestamp; + return eTimerDone; +} + +/* static */ +JS::Value Console::CreateStartTimerValue(JSContext* aCx, + const nsAString& aTimerLabel, + TimerStatus aTimerStatus) { + MOZ_ASSERT(aTimerStatus != eTimerUnknown); + + if (aTimerStatus != eTimerDone) { + return CreateTimerError(aCx, aTimerLabel, aTimerStatus); + } + + RootedDictionary timer(aCx); + + timer.mName = aTimerLabel; + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, timer, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +Console::TimerStatus Console::LogTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp, + nsAString& aTimerLabel, + double* aTimerDuration, + bool aCancelTimer) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aTimerDuration); + + *aTimerDuration = 0; + + JS::Rooted name(aCx, aName); + JS::Rooted jsString(aCx, JS::ToString(aCx, name)); + if (NS_WARN_IF(!jsString)) { + return eTimerJSException; + } + + nsAutoJSString key; + if (NS_WARN_IF(!key.init(aCx, jsString))) { + return eTimerJSException; + } + + aTimerLabel = key; + + DOMHighResTimeStamp value = 0; + + if (aCancelTimer) { + if (!mTimerRegistry.Remove(key, &value)) { + NS_WARNING("mTimerRegistry entry not found"); + return eTimerDoesntExist; + } + } else { + if (!mTimerRegistry.Get(key, &value)) { + NS_WARNING("mTimerRegistry entry not found"); + return eTimerDoesntExist; + } + } + + *aTimerDuration = aTimestamp - value; + return eTimerDone; +} + +/* static */ +JS::Value Console::CreateLogOrEndTimerValue(JSContext* aCx, + const nsAString& aLabel, + double aDuration, + TimerStatus aStatus) { + if (aStatus != eTimerDone) { + return CreateTimerError(aCx, aLabel, aStatus); + } + + RootedDictionary timer(aCx); + timer.mName = aLabel; + timer.mDuration = aDuration; + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, timer, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +/* static */ +JS::Value Console::CreateTimerError(JSContext* aCx, const nsAString& aLabel, + TimerStatus aStatus) { + MOZ_ASSERT(aStatus != eTimerUnknown && aStatus != eTimerDone); + + RootedDictionary error(aCx); + + error.mName = aLabel; + + switch (aStatus) { + case eTimerAlreadyExists: + error.mError.AssignLiteral("timerAlreadyExists"); + break; + + case eTimerDoesntExist: + error.mError.AssignLiteral("timerDoesntExist"); + break; + + case eTimerJSException: + error.mError.AssignLiteral("timerJSError"); + break; + + case eTimerMaxReached: + error.mError.AssignLiteral("maxTimersExceeded"); + break; + + default: + MOZ_CRASH("Unsupported status"); + break; + } + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, error, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +uint32_t Console::IncreaseCounter(JSContext* aCx, + const Sequence& aArguments, + nsAString& aCountLabel) { + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + MOZ_ASSERT(!aArguments.IsEmpty()); + + JS::Rooted labelValue(aCx, aArguments[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); + if (!jsString) { + return 0; // We cannot continue. + } + + nsAutoJSString string; + if (!string.init(aCx, jsString)) { + return 0; // We cannot continue. + } + + aCountLabel = string; + + const bool maxCountersReached = mCounterRegistry.Count() >= MAX_PAGE_COUNTERS; + return mCounterRegistry.WithEntryHandle( + aCountLabel, [maxCountersReached](auto&& entry) -> uint32_t { + if (entry) { + ++entry.Data(); + } else { + if (maxCountersReached) { + return MAX_PAGE_COUNTERS; + } + entry.Insert(1); + } + return entry.Data(); + }); +} + +uint32_t Console::ResetCounter(JSContext* aCx, + const Sequence& aArguments, + nsAString& aCountLabel) { + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + MOZ_ASSERT(!aArguments.IsEmpty()); + + JS::Rooted labelValue(aCx, aArguments[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); + if (!jsString) { + return 0; // We cannot continue. + } + + nsAutoJSString string; + if (!string.init(aCx, jsString)) { + return 0; // We cannot continue. + } + + aCountLabel = string; + + if (mCounterRegistry.Remove(aCountLabel)) { + return 0; + } + + // Let's return something different than 0 if the key doesn't exist. + return MAX_PAGE_COUNTERS; +} + +// This method generates a ConsoleCounter dictionary as JS::Value. If +// aCountValue is == MAX_PAGE_COUNTERS it generates a ConsoleCounterError +// instead. See IncreaseCounter. +// * aCx - this is the context that will root the returned value. +// * aCountLabel - this label must be what IncreaseCounter received as +// aTimerLabel. +// * aCountValue - the return value of IncreaseCounter. +static JS::Value CreateCounterOrResetCounterValue(JSContext* aCx, + const nsAString& aCountLabel, + uint32_t aCountValue) { + ConsoleCommon::ClearException ce(aCx); + + if (aCountValue == MAX_PAGE_COUNTERS) { + RootedDictionary error(aCx); + error.mLabel = aCountLabel; + error.mError.AssignLiteral("counterDoesntExist"); + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, error, &value)) { + return JS::UndefinedValue(); + } + + return value; + } + + RootedDictionary data(aCx); + data.mLabel = aCountLabel; + data.mCount = aCountValue; + + JS::Rooted value(aCx); + if (!ToJSValue(aCx, data, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +/* static */ +bool Console::ShouldIncludeStackTrace(MethodName aMethodName) { + switch (aMethodName) { + case MethodError: + case MethodException: + case MethodAssert: + case MethodTrace: + return true; + default: + return false; + } +} + +JSObject* MainThreadConsoleData::GetOrCreateSandbox(JSContext* aCx, + nsIPrincipal* aPrincipal) { + AssertIsOnMainThread(); + + if (!mSandbox) { + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + JS::Rooted sandbox(aCx); + nsresult rv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + mSandbox = new JSObjectHolder(aCx, sandbox); + } + + return mSandbox->GetJSObject(); +} + +bool Console::StoreCallData(JSContext* aCx, ConsoleCallData* aCallData, + const Sequence& aArguments) { + AssertIsOnOwningThread(); + + if (NS_WARN_IF(!mArgumentStorage.growBy(1))) { + return false; + } + if (!mArgumentStorage.end()[-1].Initialize(aCx, aArguments)) { + mArgumentStorage.shrinkBy(1); + return false; + } + + MOZ_ASSERT(aCallData); + MOZ_ASSERT(!mCallDataStorage.Contains(aCallData)); + + mCallDataStorage.AppendElement(aCallData); + + MOZ_ASSERT(mCallDataStorage.Length() == mArgumentStorage.length()); + + if (mCallDataStorage.Length() > STORAGE_MAX_EVENTS) { + mCallDataStorage.RemoveElementAt(0); + mArgumentStorage.erase(&mArgumentStorage[0]); + } + return true; +} + +void Console::UnstoreCallData(ConsoleCallData* aCallData) { + AssertIsOnOwningThread(); + + MOZ_ASSERT(aCallData); + MOZ_ASSERT(mCallDataStorage.Length() == mArgumentStorage.length()); + + size_t index = mCallDataStorage.IndexOf(aCallData); + // It can be that mCallDataStorage has been already cleaned in case the + // processing of the argument of some Console methods triggers the + // window.close(). + if (index == mCallDataStorage.NoIndex) { + return; + } + + mCallDataStorage.RemoveElementAt(index); + mArgumentStorage.erase(&mArgumentStorage[index]); +} + +void Console::NotifyHandler(JSContext* aCx, + const Sequence& aArguments, + ConsoleCallData* aCallData) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCallData); + + if (!mConsoleEventNotifier) { + return; + } + + JS::Rooted value(aCx); + + JS::Rooted callableGlobal( + aCx, mConsoleEventNotifier->CallbackGlobalOrNull()); + if (NS_WARN_IF(!callableGlobal)) { + return; + } + + // aCx and aArguments are in the same compartment because this method is + // called directly when a Console.something() runs. + // mConsoleEventNotifier->CallbackGlobal() is the scope where value will be + // sent to. + if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope( + aCx, aArguments, callableGlobal, &value, aCallData, &mGroupStack))) { + return; + } + + JS::Rooted ignored(aCx); + RefPtr notifier(mConsoleEventNotifier); + notifier->Call(value, &ignored); +} + +void Console::RetrieveConsoleEvents(JSContext* aCx, + nsTArray& aEvents, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + // We don't want to expose this functionality to main-thread yet. + MOZ_ASSERT(!NS_IsMainThread()); + + JS::Rooted targetScope(aCx, JS::CurrentGlobalOrNull(aCx)); + + for (uint32_t i = 0; i < mArgumentStorage.length(); ++i) { + JS::Rooted value(aCx); + + JS::Rooted sequenceScope(aCx, mArgumentStorage[i].Global()); + JSAutoRealm ar(aCx, sequenceScope); + + Sequence sequence; + SequenceRooter arguments(aCx, &sequence); + + if (!mArgumentStorage[i].PopulateArgumentsSequence(sequence)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + // Here we have aCx and sequence in the same compartment. + // targetScope is the destination scope and value will be populated in its + // compartment. + if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope( + aCx, sequence, targetScope, &value, mCallDataStorage[i], + &mGroupStack))) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + aEvents.AppendElement(value); + } +} + +void Console::SetConsoleEventHandler(AnyCallback* aHandler) { + AssertIsOnOwningThread(); + + // We don't want to expose this functionality to main-thread yet. + MOZ_ASSERT(!NS_IsMainThread()); + + mConsoleEventNotifier = aHandler; +} + +void Console::AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(Console); +} + +bool Console::IsShuttingDown() const { + MOZ_ASSERT(mStatus != eUnknown); + return mStatus == eShuttingDown; +} + +/* static */ +already_AddRefed Console::GetConsole(const GlobalObject& aGlobal) { + ErrorResult rv; + RefPtr console = GetConsoleInternal(aGlobal, rv); + if (NS_WARN_IF(rv.Failed()) || !console) { + rv.SuppressException(); + return nullptr; + } + + console->AssertIsOnOwningThread(); + + if (console->IsShuttingDown()) { + return nullptr; + } + + return console.forget(); +} + +/* static */ +already_AddRefed Console::GetConsoleInternal( + const GlobalObject& aGlobal, ErrorResult& aRv) { + // Window + if (NS_IsMainThread()) { + nsCOMPtr innerWindow = + do_QueryInterface(aGlobal.GetAsSupports()); + + // we are probably running a chrome script. + if (!innerWindow) { + RefPtr console = new Console(aGlobal.Context(), nullptr, 0, 0); + console->Initialize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return console.forget(); + } + + nsGlobalWindowInner* window = nsGlobalWindowInner::Cast(innerWindow); + return window->GetConsole(aGlobal.Context(), aRv); + } + + // Worklet + nsCOMPtr workletScope = + do_QueryInterface(aGlobal.GetAsSupports()); + if (workletScope) { + WorkletThread::AssertIsOnWorkletThread(); + return workletScope->GetConsole(aGlobal.Context(), aRv); + } + + // Workers + MOZ_ASSERT(!NS_IsMainThread()); + + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + if (NS_WARN_IF(!global)) { + return nullptr; + } + + WorkerGlobalScope* scope = workerPrivate->GlobalScope(); + MOZ_ASSERT(scope); + + // Normal worker scope. + if (scope == global) { + return scope->GetConsole(aRv); + } + + // Debugger worker scope + + WorkerDebuggerGlobalScope* debuggerScope = + workerPrivate->DebuggerGlobalScope(); + MOZ_ASSERT(debuggerScope); + MOZ_ASSERT(debuggerScope == global, "Which kind of global do we have?"); + + return debuggerScope->GetConsole(aRv); +} + +bool Console::MonotonicTimer(JSContext* aCx, MethodName aMethodName, + const Sequence& aData, + DOMHighResTimeStamp* aTimeStamp) { + if (nsCOMPtr innerWindow = do_QueryInterface(mGlobal)) { + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(innerWindow); + MOZ_ASSERT(win); + + RefPtr performance = win->GetPerformance(); + if (!performance) { + return false; + } + + *aTimeStamp = performance->Now(); + + nsDocShell* docShell = static_cast(win->GetDocShell()); + bool isTimelineRecording = TimelineConsumers::HasConsumer(docShell); + + // The 'timeStamp' recordings do not need an argument; use empty string + // if no arguments passed in. + if (isTimelineRecording && aMethodName == MethodTimeStamp) { + JS::Rooted value( + aCx, aData.Length() == 0 ? JS_GetEmptyStringValue(aCx) : aData[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return false; + } + + nsAutoJSString key; + if (!key.init(aCx, jsString)) { + return false; + } + + TimelineConsumers::AddMarkerForDocShell( + docShell, MakeUnique(key)); + } + // For `console.time(foo)` and `console.timeEnd(foo)`. + else if (isTimelineRecording && aData.Length() == 1) { + JS::Rooted value(aCx, aData[0]); + JS::Rooted jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return false; + } + + nsAutoJSString key; + if (!key.init(aCx, jsString)) { + return false; + } + + TimelineConsumers::AddMarkerForDocShell( + docShell, + MakeUnique(key, aMethodName == MethodTime + ? MarkerTracingType::START + : MarkerTracingType::END)); + } + + return true; + } + + if (NS_IsMainThread()) { + *aTimeStamp = (TimeStamp::Now() - mCreationTimeStamp).ToMilliseconds(); + return true; + } + + if (nsCOMPtr workletGlobal = do_QueryInterface(mGlobal)) { + *aTimeStamp = workletGlobal->TimeStampToDOMHighRes(TimeStamp::Now()); + return true; + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + *aTimeStamp = workerPrivate->TimeStampToDOMHighRes(TimeStamp::Now()); + return true; +} + +/* static */ +already_AddRefed Console::CreateInstance( + const GlobalObject& aGlobal, const ConsoleInstanceOptions& aOptions) { + RefPtr console = + new ConsoleInstance(aGlobal.Context(), aOptions); + return console.forget(); +} + +void Console::StringifyElement(Element* aElement, nsAString& aOut) { + aOut.AppendLiteral("<"); + aOut.Append(aElement->LocalName()); + uint32_t attrCount = aElement->GetAttrCount(); + nsAutoString idAttr; + nsAutoString classAttr; + nsAutoString nameAttr; + nsAutoString otherAttrs; + for (uint32_t i = 0; i < attrCount; i++) { + BorrowedAttrInfo attrInfo = aElement->GetAttrInfoAt(i); + nsAutoString attrValue; + attrInfo.mValue->ToString(attrValue); + + const nsAttrName* attrName = attrInfo.mName; + if (attrName->Equals(nsGkAtoms::id)) { + idAttr.AppendLiteral(" id=\""); + idAttr.Append(attrValue); + idAttr.AppendLiteral("\""); + } else if (attrName->Equals(nsGkAtoms::_class)) { + classAttr.AppendLiteral(" class=\""); + classAttr.Append(attrValue); + classAttr.AppendLiteral("\""); + } else if (attrName->Equals(nsGkAtoms::name)) { + nameAttr.AppendLiteral(" name=\""); + nameAttr.Append(attrValue); + nameAttr.AppendLiteral("\""); + } else { + nsAutoString attrNameStr; + attrName->GetQualifiedName(attrNameStr); + otherAttrs.AppendLiteral(" "); + otherAttrs.Append(attrNameStr); + otherAttrs.AppendLiteral("=\""); + otherAttrs.Append(attrValue); + otherAttrs.AppendLiteral("\""); + } + } + if (!idAttr.IsEmpty()) { + aOut.Append(idAttr); + } + if (!classAttr.IsEmpty()) { + aOut.Append(classAttr); + } + if (!nameAttr.IsEmpty()) { + aOut.Append(nameAttr); + } + if (!otherAttrs.IsEmpty()) { + aOut.Append(otherAttrs); + } + aOut.AppendLiteral(">"); +} + +void Console::MaybeExecuteDumpFunction(JSContext* aCx, + const nsAString& aMethodName, + const Sequence& aData, + nsIStackFrame* aStack) { + if (!mDumpFunction && !mDumpToStdout) { + return; + } + + nsAutoString message; + message.AssignLiteral("console."); + message.Append(aMethodName); + message.AppendLiteral(": "); + + if (!mPrefix.IsEmpty()) { + message.Append(mPrefix); + message.AppendLiteral(": "); + } + + for (uint32_t i = 0; i < aData.Length(); ++i) { + JS::Rooted v(aCx, aData[i]); + if (v.isObject()) { + Element* element = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Element, &v, element))) { + if (i != 0) { + message.AppendLiteral(" "); + } + StringifyElement(element, message); + continue; + } + } + + JS::Rooted jsString(aCx, JS_ValueToSource(aCx, v)); + if (!jsString) { + continue; + } + + nsAutoJSString string; + if (NS_WARN_IF(!string.init(aCx, jsString))) { + return; + } + + if (i != 0) { + message.AppendLiteral(" "); + } + + message.Append(string); + } + + message.AppendLiteral("\n"); + + // aStack can be null. + + nsCOMPtr stack(aStack); + + while (stack) { + nsAutoString filename; + stack->GetFilename(aCx, filename); + + message.Append(filename); + message.AppendLiteral(" "); + + message.AppendInt(stack->GetLineNumber(aCx)); + message.AppendLiteral(" "); + + nsAutoString functionName; + stack->GetName(aCx, functionName); + + message.Append(functionName); + message.AppendLiteral("\n"); + + nsCOMPtr caller = stack->GetCaller(aCx); + + if (!caller) { + caller = stack->GetAsyncCaller(aCx); + } + + stack.swap(caller); + } + + ExecuteDumpFunction(message); +} + +void Console::MaybeExecuteDumpFunctionForTime(JSContext* aCx, + MethodName aMethodName, + const nsAString& aMethodString, + uint64_t aMonotonicTimer, + const JS::Value& aData) { + if (!mDumpFunction && !mDumpToStdout) { + return; + } + + nsAutoString message; + message.AssignLiteral("console."); + message.Append(aMethodString); + message.AppendLiteral(": "); + + if (!mPrefix.IsEmpty()) { + message.Append(mPrefix); + message.AppendLiteral(": "); + } + + JS::Rooted v(aCx, aData); + JS::Rooted jsString(aCx, JS_ValueToSource(aCx, v)); + if (!jsString) { + return; + } + + nsAutoJSString string; + if (NS_WARN_IF(!string.init(aCx, jsString))) { + return; + } + + message.Append(string); + message.AppendLiteral(" @ "); + message.AppendInt(aMonotonicTimer); + + message.AppendLiteral("\n"); + ExecuteDumpFunction(message); +} + +void Console::ExecuteDumpFunction(const nsAString& aMessage) { + if (mDumpFunction) { + RefPtr dumpFunction(mDumpFunction); + dumpFunction->Call(aMessage); + return; + } + + NS_ConvertUTF16toUTF8 str(aMessage); + MOZ_LOG(nsContentUtils::DOMDumpLog(), LogLevel::Debug, ("%s", str.get())); +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); +#endif + fputs(str.get(), stdout); + fflush(stdout); +} + +ConsoleLogLevel PrefToValue(const nsAString& aPref, + const ConsoleLogLevel aLevel) { + if (!NS_IsMainThread()) { + NS_WARNING("Console.maxLogLevelPref is not supported on workers!"); + return ConsoleLogLevel::All; + } + if (aPref.IsEmpty()) { + return aLevel; + } + + NS_ConvertUTF16toUTF8 pref(aPref); + nsAutoCString value; + nsresult rv = Preferences::GetCString(pref.get(), value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return aLevel; + } + + int index = FindEnumStringIndexImpl(value.get(), value.Length(), + ConsoleLogLevelValues::strings); + if (NS_WARN_IF(index < 0)) { + nsString message; + message.AssignLiteral("Invalid Console.maxLogLevelPref value: "); + message.Append(NS_ConvertUTF8toUTF16(value)); + + nsContentUtils::LogSimpleConsoleError(message, "chrome"_ns, false, + true /* from chrome context*/); + return aLevel; + } + + MOZ_ASSERT(index < (int)ConsoleLogLevelValues::Count); + return static_cast(index); +} + +bool Console::ShouldProceed(MethodName aName) const { + ConsoleLogLevel maxLogLevel = PrefToValue(mMaxLogLevelPref, mMaxLogLevel); + return WebIDLLogLevelToInteger(maxLogLevel) <= + InternalLogLevelToInteger(aName); +} + +uint32_t Console::WebIDLLogLevelToInteger(ConsoleLogLevel aLevel) const { + switch (aLevel) { + case ConsoleLogLevel::All: + return 0; + case ConsoleLogLevel::Debug: + return 2; + case ConsoleLogLevel::Log: + return 3; + case ConsoleLogLevel::Info: + return 3; + case ConsoleLogLevel::Clear: + return 3; + case ConsoleLogLevel::Trace: + return 3; + case ConsoleLogLevel::TimeLog: + return 3; + case ConsoleLogLevel::TimeEnd: + return 3; + case ConsoleLogLevel::Time: + return 3; + case ConsoleLogLevel::Group: + return 3; + case ConsoleLogLevel::GroupEnd: + return 3; + case ConsoleLogLevel::Profile: + return 3; + case ConsoleLogLevel::ProfileEnd: + return 3; + case ConsoleLogLevel::Dir: + return 3; + case ConsoleLogLevel::Dirxml: + return 3; + case ConsoleLogLevel::Warn: + return 4; + case ConsoleLogLevel::Error: + return 5; + case ConsoleLogLevel::Off: + return UINT32_MAX; + default: + MOZ_CRASH( + "ConsoleLogLevel is out of sync with the Console implementation!"); + return 0; + } +} + +uint32_t Console::InternalLogLevelToInteger(MethodName aName) const { + switch (aName) { + case MethodLog: + return 3; + case MethodInfo: + return 3; + case MethodWarn: + return 4; + case MethodError: + return 5; + case MethodException: + return 5; + case MethodDebug: + return 2; + case MethodTable: + return 3; + case MethodTrace: + return 3; + case MethodDir: + return 3; + case MethodDirxml: + return 3; + case MethodGroup: + return 3; + case MethodGroupCollapsed: + return 3; + case MethodGroupEnd: + return 3; + case MethodTime: + return 3; + case MethodTimeLog: + return 3; + case MethodTimeEnd: + return 3; + case MethodTimeStamp: + return 3; + case MethodAssert: + return 3; + case MethodCount: + return 3; + case MethodCountReset: + return 3; + case MethodClear: + return 3; + case MethodProfile: + return 3; + case MethodProfileEnd: + return 3; + default: + MOZ_CRASH("MethodName is out of sync with the Console implementation!"); + return 0; + } +} + +bool Console::ArgumentData::Initialize(JSContext* aCx, + const Sequence& aArguments) { + mGlobal = JS::CurrentGlobalOrNull(aCx); + + if (NS_WARN_IF(!mArguments.AppendElements(aArguments, fallible))) { + return false; + } + + return true; +} + +void Console::ArgumentData::Trace(const TraceCallbacks& aCallbacks, + void* aClosure) { + ArgumentData* tmp = this; + for (uint32_t i = 0; i < mArguments.Length(); ++i) { + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mArguments[i]) + } + + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) +} + +bool Console::ArgumentData::PopulateArgumentsSequence( + Sequence& aSequence) const { + AssertIsOnOwningThread(); + + for (uint32_t i = 0; i < mArguments.Length(); ++i) { + if (NS_WARN_IF(!aSequence.AppendElement(mArguments[i], fallible))) { + return false; + } + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/console/Console.h b/dom/console/Console.h new file mode 100644 index 0000000000..c7a5590467 --- /dev/null +++ b/dom/console/Console.h @@ -0,0 +1,450 @@ +/* -*- 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_dom_Console_h +#define mozilla_dom_Console_h + +#include "domstubs.h" +#include "mozilla/dom/ConsoleBinding.h" +#include "mozilla/TimeStamp.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" + +class nsIConsoleAPIStorage; +class nsIGlobalObject; +class nsPIDOMWindowInner; +class nsIStackFrame; + +namespace mozilla::dom { + +class AnyCallback; +class ConsoleCallData; +class ConsoleInstance; +class ConsoleRunnable; +class ConsoleCallDataRunnable; +class ConsoleProfileRunnable; +class MainThreadConsoleData; + +class Console final : public nsIObserver, public nsSupportsWeakReference { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(Console, nsIObserver) + NS_DECL_NSIOBSERVER + + static already_AddRefed Create(JSContext* aCx, + nsPIDOMWindowInner* aWindow, + ErrorResult& aRv); + + static already_AddRefed CreateForWorklet(JSContext* aCx, + nsIGlobalObject* aGlobal, + uint64_t aOuterWindowID, + uint64_t aInnerWindowID, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + static void Log(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Info(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Warn(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Error(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Exception(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Debug(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Table(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Trace(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Dir(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Dirxml(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Group(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void GroupCollapsed(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void GroupEnd(const GlobalObject& aGlobal); + + MOZ_CAN_RUN_SCRIPT + static void Time(const GlobalObject& aGlobal, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + static void TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + static void TimeStamp(const GlobalObject& aGlobal, + const JS::Handle aData); + + MOZ_CAN_RUN_SCRIPT + static void Profile(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void ProfileEnd(const GlobalObject& aGlobal, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Assert(const GlobalObject& aGlobal, bool aCondition, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Count(const GlobalObject& aGlobal, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + static void CountReset(const GlobalObject& aGlobal, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + static void Clear(const GlobalObject& aGlobal); + + static already_AddRefed CreateInstance( + const GlobalObject& aGlobal, const ConsoleInstanceOptions& aOptions); + + void ClearStorage(); + + void RetrieveConsoleEvents(JSContext* aCx, nsTArray& aEvents, + ErrorResult& aRv); + + void SetConsoleEventHandler(AnyCallback* aHandler); + + private: + Console(JSContext* aCx, nsIGlobalObject* aGlobal, uint64_t aOuterWindowID, + uint64_t aInnerWIndowID); + ~Console(); + + void Initialize(ErrorResult& aRv); + + void Shutdown(); + + enum MethodName { + MethodLog, + MethodInfo, + MethodWarn, + MethodError, + MethodException, + MethodDebug, + MethodTable, + MethodTrace, + MethodDir, + MethodDirxml, + MethodGroup, + MethodGroupCollapsed, + MethodGroupEnd, + MethodTime, + MethodTimeLog, + MethodTimeEnd, + MethodTimeStamp, + MethodAssert, + MethodCount, + MethodCountReset, + MethodClear, + MethodProfile, + MethodProfileEnd, + }; + + static already_AddRefed GetConsole(const GlobalObject& aGlobal); + + static already_AddRefed GetConsoleInternal( + const GlobalObject& aGlobal, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + static void ProfileMethod(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aAction, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void ProfileMethodInternal(JSContext* aCx, MethodName aName, + const nsAString& aAction, + const Sequence& aData); + + // Implementation of the mainthread-only parts of ProfileMethod. + // This is indepedent of console instance state. + static void ProfileMethodMainthread(JSContext* aCx, const nsAString& aAction, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void Method(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aString, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void MethodInternal(JSContext* aCx, MethodName aName, + const nsAString& aString, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + static void StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence& aData, + MethodName aMethodName, + const nsAString& aMethodString); + + MOZ_CAN_RUN_SCRIPT + void StringMethodInternal(JSContext* aCx, const nsAString& aLabel, + const Sequence& aData, + MethodName aMethodName, + const nsAString& aMethodString); + + MainThreadConsoleData* GetOrCreateMainThreadData(); + + // Returns true on success; otherwise false. + bool StoreCallData(JSContext* aCx, ConsoleCallData* aCallData, + const Sequence& aArguments); + + void UnstoreCallData(ConsoleCallData* aData); + + // aCx and aArguments must be in the same JS compartment. + MOZ_CAN_RUN_SCRIPT + void NotifyHandler(JSContext* aCx, const Sequence& aArguments, + ConsoleCallData* aData); + + // PopulateConsoleNotificationInTheTargetScope receives aCx and aArguments in + // the same JS compartment and populates the ConsoleEvent object + // (aEventValue) in the aTargetScope. + // aTargetScope can be: + // - the system-principal scope when we want to dispatch the ConsoleEvent to + // nsIConsoleAPIStorage (See the comment in Console.cpp about the use of + // xpc::PrivilegedJunkScope() + // - the mConsoleEventNotifier->CallableGlobal() when we want to notify this + // handler about a new ConsoleEvent. + // - It can be the global from the JSContext when RetrieveConsoleEvents is + // called. + static bool PopulateConsoleNotificationInTheTargetScope( + JSContext* aCx, const Sequence& aArguments, + JS::Handle aTargetScope, + JS::MutableHandle aEventValue, ConsoleCallData* aData, + nsTArray* aGroupStack); + + enum TimerStatus { + eTimerUnknown, + eTimerDone, + eTimerAlreadyExists, + eTimerDoesntExist, + eTimerJSException, + eTimerMaxReached, + }; + + static JS::Value CreateTimerError(JSContext* aCx, const nsAString& aLabel, + TimerStatus aStatus); + + // StartTimer is called on the owning thread and populates aTimerLabel and + // aTimerValue. + // * aCx - the JSContext rooting aName. + // * aName - this is (should be) the name of the timer as JS::Value. + // * aTimestamp - the monotonicTimer for this context taken from + // performance.now(). + // * aTimerLabel - This label will be populated with the aName converted to a + // string. + // * aTimerValue - the StartTimer value stored into (or taken from) + // mTimerRegistry. + TimerStatus StartTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp, nsAString& aTimerLabel, + DOMHighResTimeStamp* aTimerValue); + + // CreateStartTimerValue generates a ConsoleTimerStart dictionary exposed as + // JS::Value. If aTimerStatus is false, it generates a ConsoleTimerError + // instead. It's called only after the execution StartTimer on the owning + // thread. + // * aCx - this is the context that will root the returned value. + // * aTimerLabel - this label must be what StartTimer received as aTimerLabel. + // * aTimerStatus - the return value of StartTimer. + static JS::Value CreateStartTimerValue(JSContext* aCx, + const nsAString& aTimerLabel, + TimerStatus aTimerStatus); + + // LogTimer follows the same pattern as StartTimer: it runs on the + // owning thread and populates aTimerLabel and aTimerDuration, used by + // CreateLogOrEndTimerValue. + // * aCx - the JSContext rooting aName. + // * aName - this is (should be) the name of the timer as JS::Value. + // * aTimestamp - the monotonicTimer for this context taken from + // performance.now(). + // * aTimerLabel - This label will be populated with the aName converted to a + // string. + // * aTimerDuration - the difference between aTimestamp and when the timer + // started (see StartTimer). + // * aCancelTimer - if true, the timer is removed from the table. + TimerStatus LogTimer(JSContext* aCx, const JS::Value& aName, + DOMHighResTimeStamp aTimestamp, nsAString& aTimerLabel, + double* aTimerDuration, bool aCancelTimer); + + // This method generates a ConsoleTimerEnd dictionary exposed as JS::Value, or + // a ConsoleTimerError dictionary if aTimerStatus is false. See LogTimer. + // * aCx - this is the context that will root the returned value. + // * aTimerLabel - this label must be what LogTimer received as aTimerLabel. + // * aTimerDuration - this is what LogTimer received as aTimerDuration + // * aTimerStatus - the return value of LogTimer. + static JS::Value CreateLogOrEndTimerValue(JSContext* aCx, + const nsAString& aLabel, + double aDuration, + TimerStatus aStatus); + + // The method populates a Sequence from an array of JS::Value. + bool ArgumentsToValueList(const Sequence& aData, + Sequence& aSequence) const; + + // This method follows the same pattern as StartTimer: its runs on the owning + // thread and populate aCountLabel, used by CreateCounterOrResetCounterValue. + // Returns 3 possible values: + // * MAX_PAGE_COUNTERS in case of error that has to be reported; + // * 0 in case of a CX exception. The operation cannot continue; + // * the incremented counter value. + // Params: + // * aCx - the JSContext rooting aData. + // * aData - the arguments received by the console.count() method. + // * aCountLabel - the label that will be populated by this method. + uint32_t IncreaseCounter(JSContext* aCx, const Sequence& aData, + nsAString& aCountLabel); + + // This method follows the same pattern as StartTimer: its runs on the owning + // thread and populate aCountLabel, used by CreateCounterResetValue. Returns + // 3 possible values: + // * MAX_PAGE_COUNTERS in case of error that has to be reported; + // * 0 elsewhere. In case of a CX exception, aCountLabel will be an empty + // string. + // Params: + // * aCx - the JSContext rooting aData. + // * aData - the arguments received by the console.count() method. + // * aCountLabel - the label that will be populated by this method. + uint32_t ResetCounter(JSContext* aCx, const Sequence& aData, + nsAString& aCountLabel); + + static bool ShouldIncludeStackTrace(MethodName aMethodName); + + void AssertIsOnOwningThread() const; + + bool IsShuttingDown() const; + + bool MonotonicTimer(JSContext* aCx, MethodName aMethodName, + const Sequence& aData, + DOMHighResTimeStamp* aTimeStamp); + + void StringifyElement(Element* aElement, nsAString& aOut); + + MOZ_CAN_RUN_SCRIPT + void MaybeExecuteDumpFunction(JSContext* aCx, const nsAString& aMethodName, + const Sequence& aData, + nsIStackFrame* aStack); + + MOZ_CAN_RUN_SCRIPT + void MaybeExecuteDumpFunctionForTime(JSContext* aCx, MethodName aMethodName, + const nsAString& aMethodString, + uint64_t aMonotonicTimer, + const JS::Value& aData); + + MOZ_CAN_RUN_SCRIPT + void ExecuteDumpFunction(const nsAString& aMessage); + + bool ShouldProceed(MethodName aName) const; + + uint32_t WebIDLLogLevelToInteger(ConsoleLogLevel aLevel) const; + + uint32_t InternalLogLevelToInteger(MethodName aName) const; + + class ArgumentData { + public: + bool Initialize(JSContext* aCx, const Sequence& aArguments); + void Trace(const TraceCallbacks& aCallbacks, void* aClosure); + bool PopulateArgumentsSequence(Sequence& aSequence) const; + JSObject* Global() const { return mGlobal; } + + private: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(ArgumentData); + } + + NS_DECL_OWNINGTHREAD; + JS::Heap mGlobal; + nsTArray> mArguments; + }; + + // Owning/CC thread only + nsCOMPtr mGlobal; + + // Touched on the owner thread. + nsTHashMap mTimerRegistry; + nsTHashMap mCounterRegistry; + + nsTArray> mCallDataStorage; + // These are references to the arguments we received in each call + // from the DOM bindings. + // Vector supports non-memmovable types such as ArgumentData + // (without any need to jump through hoops like + // MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR_FOR_TEMPLATE for nsTArray). + Vector mArgumentStorage; + + RefPtr mConsoleEventNotifier; + + RefPtr mMainThreadData; + // This is the stack for grouping relating to Console-thread events, when + // the Console thread is not the main thread. + nsTArray mGroupStack; + + uint64_t mOuterID; + uint64_t mInnerID; + + // Set only by ConsoleInstance: + nsString mConsoleID; + nsString mPassedInnerID; + RefPtr mDumpFunction; + bool mDumpToStdout; + nsString mPrefix; + bool mChromeInstance; + ConsoleLogLevel mMaxLogLevel; + nsString mMaxLogLevelPref; + + enum { eUnknown, eInitialized, eShuttingDown } mStatus; + + // This is used when Console is created and it's used only for JSM custom + // console instance. + mozilla::TimeStamp mCreationTimeStamp; + + friend class ConsoleCallData; + friend class ConsoleCallDataWorkletRunnable; + friend class ConsoleInstance; + friend class ConsoleProfileWorkerRunnable; + friend class ConsoleProfileWorkletRunnable; + friend class ConsoleRunnable; + friend class ConsoleWorkerRunnable; + friend class ConsoleWorkletRunnable; + friend class MainThreadConsoleData; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_Console_h */ diff --git a/dom/console/ConsoleAPIStorage.sys.mjs b/dom/console/ConsoleAPIStorage.sys.mjs new file mode 100644 index 0000000000..f920d0d5e0 --- /dev/null +++ b/dom/console/ConsoleAPIStorage.sys.mjs @@ -0,0 +1,195 @@ +/* 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/. */ + +const STORAGE_MAX_EVENTS = 1000; + +var _consoleStorage = new Map(); + +// NOTE: these listeners used to just be added as observers and notified via +// Services.obs.notifyObservers. However, that has enough overhead to be a +// problem for this. Using an explicit global array is much cheaper, and +// should be equivalent. +var _logEventListeners = []; + +const CONSOLEAPISTORAGE_CID = Components.ID( + "{96cf7855-dfa9-4c6d-8276-f9705b4890f2}" +); + +/** + * The ConsoleAPIStorage is meant to cache window.console API calls for later + * reuse by other components when needed. For example, the Web Console code can + * display the cached messages when it opens for the active tab. + * + * ConsoleAPI messages are stored as they come from the ConsoleAPI code, with + * all their properties. They are kept around until the inner window object that + * created the messages is destroyed. Messages are indexed by the inner window + * ID. + * + * Usage: + * let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + * .getService(Ci.nsIConsoleAPIStorage); + * + * // Get the cached events array for the window you want (use the inner + * // window ID). + * let events = ConsoleAPIStorage.getEvents(innerWindowID); + * events.forEach(function(event) { ... }); + * + * // Clear the events for the given inner window ID. + * ConsoleAPIStorage.clearEvents(innerWindowID); + */ +export function ConsoleAPIStorageService() { + this.init(); +} + +ConsoleAPIStorageService.prototype = { + classID: CONSOLEAPISTORAGE_CID, + QueryInterface: ChromeUtils.generateQI([ + "nsIConsoleAPIStorage", + "nsIObserver", + ]), + + observe: function CS_observe(aSubject, aTopic, aData) { + if (aTopic == "xpcom-shutdown") { + Services.obs.removeObserver(this, "xpcom-shutdown"); + Services.obs.removeObserver(this, "inner-window-destroyed"); + Services.obs.removeObserver(this, "memory-pressure"); + } else if (aTopic == "inner-window-destroyed") { + let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + this.clearEvents(innerWindowID + ""); + } else if (aTopic == "memory-pressure") { + this.clearEvents(); + } + }, + + /** @private */ + init: function CS_init() { + Services.obs.addObserver(this, "xpcom-shutdown"); + Services.obs.addObserver(this, "inner-window-destroyed"); + Services.obs.addObserver(this, "memory-pressure"); + }, + + /** + * Get the events array by inner window ID or all events from all windows. + * + * @param string [aId] + * Optional, the inner window ID for which you want to get the array of + * cached events. + * @returns array + * The array of cached events for the given window. If no |aId| is + * given this function returns all of the cached events, from any + * window. + */ + getEvents: function CS_getEvents(aId) { + if (aId != null) { + return (_consoleStorage.get(aId) || []).slice(0); + } + + let result = []; + + for (let [, events] of _consoleStorage) { + result.push.apply(result, events); + } + + return result.sort(function (a, b) { + return a.timeStamp - b.timeStamp; + }); + }, + + /** + * Adds a listener to be notified of log events. + * + * @param jsval [aListener] + * A JS listener which will be notified with the message object when + * a log event occurs. + * @param nsIPrincipal [aPrincipal] + * The principal of the listener - used to determine if we need to + * clone the message before forwarding it. + */ + addLogEventListener: function CS_addLogEventListener(aListener, aPrincipal) { + // If our listener has a less-privileged principal than us, then they won't + // be able to access the log event object which was populated for our + // scope. Accordingly we need to clone it for these listeners. + // + // XXX: AFAICT these listeners which we need to clone messages for are all + // tests. Alternative solutions are welcome. + const clone = !aPrincipal.subsumes( + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); + _logEventListeners.push({ + callback: aListener, + clone, + }); + }, + + /** + * Removes a listener added with `addLogEventListener`. + * + * @param jsval [aListener] + * A JS listener which was added with `addLogEventListener`. + */ + removeLogEventListener: function CS_removeLogEventListener(aListener) { + const index = _logEventListeners.findIndex(l => l.callback === aListener); + if (index != -1) { + _logEventListeners.splice(index, 1); + } else { + console.error( + "Attempted to remove a log event listener that does not exist." + ); + } + }, + + /** + * Record an event associated with the given window ID. + * + * @param string aId + * The ID of the inner window for which the event occurred or "jsm" for + * messages logged from JavaScript modules.. + * @param object aEvent + * A JavaScript object you want to store. + */ + recordEvent: function CS_recordEvent(aId, aEvent) { + if (!_consoleStorage.has(aId)) { + _consoleStorage.set(aId, []); + } + + let storage = _consoleStorage.get(aId); + + storage.push(aEvent); + + // truncate + if (storage.length > STORAGE_MAX_EVENTS) { + storage.shift(); + } + + for (let { callback, clone } of _logEventListeners) { + try { + if (clone) { + callback(Cu.cloneInto(aEvent, callback)); + } else { + callback(aEvent); + } + } catch (e) { + // A failing listener should not prevent from calling other listeners. + console.error(e); + } + } + }, + + /** + * Clear storage data for the given window. + * + * @param string [aId] + * Optional, the inner window ID for which you want to clear the + * messages. If this is not specified all of the cached messages are + * cleared, from all window objects. + */ + clearEvents: function CS_clearEvents(aId) { + if (aId != null) { + _consoleStorage.delete(aId); + } else { + _consoleStorage.clear(); + Services.obs.notifyObservers(null, "console-storage-reset"); + } + }, +}; diff --git a/dom/console/ConsoleCommon.h b/dom/console/ConsoleCommon.h new file mode 100644 index 0000000000..7076c957a4 --- /dev/null +++ b/dom/console/ConsoleCommon.h @@ -0,0 +1,28 @@ +/* -*- 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_dom_ConsoleCommon_h +#define mozilla_dom_ConsoleCommon_h + +#include "jsapi.h" +#include "nsString.h" + +namespace mozilla::dom::ConsoleCommon { + +// This class is used to clear any exception at the end of this method. +class MOZ_RAII ClearException { + public: + explicit ClearException(JSContext* aCx) : mCx(aCx) {} + + ~ClearException() { JS_ClearPendingException(mCx); } + + private: + JSContext* mCx; +}; + +} // namespace mozilla::dom::ConsoleCommon + +#endif /* mozilla_dom_ConsoleCommon_h */ diff --git a/dom/console/ConsoleInstance.cpp b/dom/console/ConsoleInstance.cpp new file mode 100644 index 0000000000..5eb8325faf --- /dev/null +++ b/dom/console/ConsoleInstance.cpp @@ -0,0 +1,206 @@ +/* -*- 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 "mozilla/dom/ConsoleInstance.h" +#include "mozilla/dom/ConsoleBinding.h" +#include "mozilla/Preferences.h" +#include "ConsoleCommon.h" +#include "ConsoleUtils.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ConsoleInstance, mConsole) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ConsoleInstance) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ConsoleInstance) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConsoleInstance) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END + +namespace { + +ConsoleUtils::Level WebIDLevelToConsoleUtilsLevel(ConsoleLevel aLevel) { + switch (aLevel) { + case ConsoleLevel::Log: + return ConsoleUtils::eLog; + case ConsoleLevel::Warning: + return ConsoleUtils::eWarning; + case ConsoleLevel::Error: + return ConsoleUtils::eError; + default: + break; + } + + return ConsoleUtils::eLog; +} + +} // namespace + +ConsoleInstance::ConsoleInstance(JSContext* aCx, + const ConsoleInstanceOptions& aOptions) + : mConsole(new Console(aCx, nullptr, 0, 0)) { + mConsole->mConsoleID = aOptions.mConsoleID; + mConsole->mPassedInnerID = aOptions.mInnerID; + + if (aOptions.mDump.WasPassed()) { + mConsole->mDumpFunction = &aOptions.mDump.Value(); + } + + mConsole->mPrefix = aOptions.mPrefix; + + // Let's inform that this is a custom instance. + mConsole->mChromeInstance = true; + + if (aOptions.mMaxLogLevel.WasPassed()) { + mConsole->mMaxLogLevel = aOptions.mMaxLogLevel.Value(); + } + + if (!aOptions.mMaxLogLevelPref.IsEmpty()) { + mConsole->mMaxLogLevelPref = aOptions.mMaxLogLevelPref; + NS_ConvertUTF16toUTF8 pref(aOptions.mMaxLogLevelPref); + nsAutoCString value; + nsresult rv = Preferences::GetCString(pref.get(), value); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsString message; + message.AssignLiteral( + "Console.maxLogLevelPref used with a non-existing pref: "); + message.Append(aOptions.mMaxLogLevelPref); + + nsContentUtils::LogSimpleConsoleError(message, "chrome"_ns, false, + true /* from chrome context*/); + } + } +} + +ConsoleInstance::~ConsoleInstance() = default; + +JSObject* ConsoleInstance::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return ConsoleInstance_Binding::Wrap(aCx, this, aGivenProto); +} + +#define METHOD(name, string) \ + void ConsoleInstance::name(JSContext* aCx, \ + const Sequence& aData) { \ + RefPtr console(mConsole); \ + console->MethodInternal(aCx, Console::Method##name, \ + nsLiteralString(string), aData); \ + } + +METHOD(Log, u"log") +METHOD(Info, u"info") +METHOD(Warn, u"warn") +METHOD(Error, u"error") +METHOD(Exception, u"exception") +METHOD(Debug, u"debug") +METHOD(Table, u"table") +METHOD(Trace, u"trace") +METHOD(Dir, u"dir"); +METHOD(Dirxml, u"dirxml"); +METHOD(Group, u"group") +METHOD(GroupCollapsed, u"groupCollapsed") + +#undef METHOD + +void ConsoleInstance::GroupEnd(JSContext* aCx) { + const Sequence data; + RefPtr console(mConsole); + console->MethodInternal(aCx, Console::MethodGroupEnd, u"groupEnd"_ns, data); +} + +void ConsoleInstance::Time(JSContext* aCx, const nsAString& aLabel) { + RefPtr console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence(), + Console::MethodTime, u"time"_ns); +} + +void ConsoleInstance::TimeLog(JSContext* aCx, const nsAString& aLabel, + const Sequence& aData) { + RefPtr console(mConsole); + console->StringMethodInternal(aCx, aLabel, aData, Console::MethodTimeLog, + u"timeLog"_ns); +} + +void ConsoleInstance::TimeEnd(JSContext* aCx, const nsAString& aLabel) { + RefPtr console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence(), + Console::MethodTimeEnd, u"timeEnd"_ns); +} + +void ConsoleInstance::TimeStamp(JSContext* aCx, + const JS::Handle aData) { + ConsoleCommon::ClearException ce(aCx); + + Sequence data; + SequenceRooter rooter(aCx, &data); + + if (aData.isString() && !data.AppendElement(aData, fallible)) { + return; + } + + RefPtr console(mConsole); + console->MethodInternal(aCx, Console::MethodTimeStamp, u"timeStamp"_ns, data); +} + +void ConsoleInstance::Profile(JSContext* aCx, + const Sequence& aData) { + RefPtr console(mConsole); + console->ProfileMethodInternal(aCx, Console::MethodProfile, u"profile"_ns, + aData); +} + +void ConsoleInstance::ProfileEnd(JSContext* aCx, + const Sequence& aData) { + RefPtr console(mConsole); + console->ProfileMethodInternal(aCx, Console::MethodProfileEnd, + u"profileEnd"_ns, aData); +} + +void ConsoleInstance::Assert(JSContext* aCx, bool aCondition, + const Sequence& aData) { + if (!aCondition) { + RefPtr console(mConsole); + console->MethodInternal(aCx, Console::MethodAssert, u"assert"_ns, aData); + } +} + +void ConsoleInstance::Count(JSContext* aCx, const nsAString& aLabel) { + RefPtr console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence(), + Console::MethodCount, u"count"_ns); +} + +void ConsoleInstance::CountReset(JSContext* aCx, const nsAString& aLabel) { + RefPtr console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence(), + Console::MethodCountReset, u"countReset"_ns); +} + +void ConsoleInstance::Clear(JSContext* aCx) { + const Sequence data; + RefPtr console(mConsole); + console->MethodInternal(aCx, Console::MethodClear, u"clear"_ns, data); +} + +void ConsoleInstance::ReportForServiceWorkerScope(const nsAString& aScope, + const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + ConsoleLevel aLevel) { + if (!NS_IsMainThread()) { + return; + } + + ConsoleUtils::ReportForServiceWorkerScope( + aScope, aMessage, aFilename, aLineNumber, aColumnNumber, + WebIDLevelToConsoleUtilsLevel(aLevel)); +} + +} // namespace mozilla::dom diff --git a/dom/console/ConsoleInstance.h b/dom/console/ConsoleInstance.h new file mode 100644 index 0000000000..6afed56b57 --- /dev/null +++ b/dom/console/ConsoleInstance.h @@ -0,0 +1,114 @@ +/* -*- 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_dom_ConsoleInstance_h +#define mozilla_dom_ConsoleInstance_h + +#include "mozilla/dom/Console.h" + +namespace mozilla::dom { + +class ConsoleInstance final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ConsoleInstance) + + explicit ConsoleInstance(JSContext* aCx, + const ConsoleInstanceOptions& aOptions); + + // WebIDL methods + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return nullptr; } + + MOZ_CAN_RUN_SCRIPT + void Log(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Info(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Warn(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Error(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Exception(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Debug(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Table(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Trace(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Dir(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Dirxml(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Group(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void GroupCollapsed(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void GroupEnd(JSContext* aCx); + + MOZ_CAN_RUN_SCRIPT + void Time(JSContext* aCx, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + void TimeLog(JSContext* aCx, const nsAString& aLabel, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void TimeEnd(JSContext* aCx, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + void TimeStamp(JSContext* aCx, const JS::Handle aData); + + MOZ_CAN_RUN_SCRIPT + void Profile(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void ProfileEnd(JSContext* aCx, const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Assert(JSContext* aCx, bool aCondition, + const Sequence& aData); + + MOZ_CAN_RUN_SCRIPT + void Count(JSContext* aCx, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + void CountReset(JSContext* aCx, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + void Clear(JSContext* aCx); + + // For testing only. + void ReportForServiceWorkerScope(const nsAString& aScope, + const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, uint32_t aColumnNumber, + ConsoleLevel aLevel); + + private: + ~ConsoleInstance(); + + RefPtr mConsole; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ConsoleInstance_h diff --git a/dom/console/ConsoleReportCollector.cpp b/dom/console/ConsoleReportCollector.cpp new file mode 100644 index 0000000000..f64d79aa64 --- /dev/null +++ b/dom/console/ConsoleReportCollector.cpp @@ -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/. */ + +#include "mozilla/ConsoleReportCollector.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/net/NeckoChannelParams.h" + +#include "ConsoleUtils.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" + +namespace mozilla { + +using mozilla::dom::ConsoleUtils; + +NS_IMPL_ISUPPORTS(ConsoleReportCollector, nsIConsoleReportCollector) + +ConsoleReportCollector::ConsoleReportCollector() + : mMutex("mozilla::ConsoleReportCollector") {} + +void ConsoleReportCollector::AddConsoleReport( + uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + const nsTArray& aStringParams) { + // any thread + MutexAutoLock lock(mMutex); + + mPendingReports.EmplaceBack(aErrorFlags, aCategory, aPropertiesFile, + aSourceFileURI, aLineNumber, aColumnNumber, + aMessageName, aStringParams); +} + +void ConsoleReportCollector::FlushReportsToConsole(uint64_t aInnerWindowID, + ReportAction aAction) { + nsTArray reports; + + { + MutexAutoLock lock(mMutex); + if (aAction == ReportAction::Forget) { + reports = std::move(mPendingReports); + } else { + reports = mPendingReports.Clone(); + } + } + + for (uint32_t i = 0; i < reports.Length(); ++i) { + PendingReport& report = reports[i]; + + nsAutoString errorText; + nsresult rv; + if (!report.mStringParams.IsEmpty()) { + rv = nsContentUtils::FormatLocalizedString( + report.mPropertiesFile, report.mMessageName.get(), + report.mStringParams, errorText); + } else { + rv = nsContentUtils::GetLocalizedString( + report.mPropertiesFile, report.mMessageName.get(), errorText); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + // It would be nice if we did not have to do this since ReportToConsole() + // just turns around and converts it back to a spec. + nsCOMPtr uri; + if (!report.mSourceFileURI.IsEmpty()) { + nsresult rv = NS_NewURI(getter_AddRefs(uri), report.mSourceFileURI); + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString("Failed to transform %s to uri", + report.mSourceFileURI.get()) + .get()); + continue; + } + } + + nsContentUtils::ReportToConsoleByWindowID( + errorText, report.mErrorFlags, report.mCategory, aInnerWindowID, uri, + u""_ns, report.mLineNumber, report.mColumnNumber); + } +} + +void ConsoleReportCollector::FlushReportsToConsoleForServiceWorkerScope( + const nsACString& aScope, ReportAction aAction) { + nsTArray reports; + + { + MutexAutoLock lock(mMutex); + if (aAction == ReportAction::Forget) { + reports = std::move(mPendingReports); + } else { + reports = mPendingReports.Clone(); + } + } + + for (uint32_t i = 0; i < reports.Length(); ++i) { + PendingReport& report = reports[i]; + + nsAutoString errorText; + nsresult rv; + if (!report.mStringParams.IsEmpty()) { + rv = nsContentUtils::FormatLocalizedString( + report.mPropertiesFile, report.mMessageName.get(), + report.mStringParams, errorText); + } else { + rv = nsContentUtils::GetLocalizedString( + report.mPropertiesFile, report.mMessageName.get(), errorText); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + ConsoleUtils::Level level = ConsoleUtils::eLog; + switch (report.mErrorFlags) { + case nsIScriptError::errorFlag: + level = ConsoleUtils::eError; + break; + case nsIScriptError::warningFlag: + level = ConsoleUtils::eWarning; + break; + default: + // default to log otherwise + break; + } + + ConsoleUtils::ReportForServiceWorkerScope( + NS_ConvertUTF8toUTF16(aScope), errorText, + NS_ConvertUTF8toUTF16(report.mSourceFileURI), report.mLineNumber, + report.mColumnNumber, level); + } +} + +void ConsoleReportCollector::FlushConsoleReports(dom::Document* aDocument, + ReportAction aAction) { + MOZ_ASSERT(NS_IsMainThread()); + + FlushReportsToConsole(aDocument ? aDocument->InnerWindowID() : 0, aAction); +} + +void ConsoleReportCollector::FlushConsoleReports(nsILoadGroup* aLoadGroup, + ReportAction aAction) { + FlushReportsToConsole(nsContentUtils::GetInnerWindowID(aLoadGroup), aAction); +} + +void ConsoleReportCollector::FlushConsoleReports( + nsIConsoleReportCollector* aCollector) { + MOZ_ASSERT(aCollector); + + nsTArray reports; + + { + MutexAutoLock lock(mMutex); + reports = std::move(mPendingReports); + } + + for (uint32_t i = 0; i < reports.Length(); ++i) { + PendingReport& report = reports[i]; + aCollector->AddConsoleReport( + report.mErrorFlags, report.mCategory, report.mPropertiesFile, + report.mSourceFileURI, report.mLineNumber, report.mColumnNumber, + report.mMessageName, + static_cast&>(report.mStringParams)); + } +} + +void ConsoleReportCollector::StealConsoleReports( + nsTArray& aReports) { + aReports.Clear(); + + nsTArray reports; + + { + MutexAutoLock lock(mMutex); + reports = std::move(mPendingReports); + } + + for (const PendingReport& report : reports) { + aReports.AppendElement(net::ConsoleReportCollected( + report.mErrorFlags, report.mCategory, report.mPropertiesFile, + report.mSourceFileURI, report.mLineNumber, report.mColumnNumber, + report.mMessageName, report.mStringParams)); + } +} + +void ConsoleReportCollector::ClearConsoleReports() { + MutexAutoLock lock(mMutex); + + mPendingReports.Clear(); +} + +ConsoleReportCollector::~ConsoleReportCollector() = default; + +} // namespace mozilla diff --git a/dom/console/ConsoleReportCollector.h b/dom/console/ConsoleReportCollector.h new file mode 100644 index 0000000000..9d4e0b1aa0 --- /dev/null +++ b/dom/console/ConsoleReportCollector.h @@ -0,0 +1,92 @@ +/* -*- 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_ConsoleReportCollector_h +#define mozilla_ConsoleReportCollector_h + +#include "mozilla/Mutex.h" +#include "nsIConsoleReportCollector.h" +#include "nsTArray.h" + +namespace mozilla { + +namespace net { +class ConsoleReportCollected; +} + +class ConsoleReportCollector final : public nsIConsoleReportCollector { + public: + ConsoleReportCollector(); + + void AddConsoleReport(uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + const nsTArray& aStringParams) override; + + void FlushReportsToConsole( + uint64_t aInnerWindowID, + ReportAction aAction = ReportAction::Forget) override; + + void FlushReportsToConsoleForServiceWorkerScope( + const nsACString& aScope, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports( + dom::Document* aDocument, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports( + nsILoadGroup* aLoadGroup, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports(nsIConsoleReportCollector* aCollector) override; + + void StealConsoleReports( + nsTArray& aReports) override; + + void ClearConsoleReports() override; + + private: + ~ConsoleReportCollector(); + + struct PendingReport { + PendingReport(uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + const nsTArray& aStringParams) + : mErrorFlags(aErrorFlags), + mCategory(aCategory), + mPropertiesFile(aPropertiesFile), + mSourceFileURI(aSourceFileURI), + mLineNumber(aLineNumber), + mColumnNumber(aColumnNumber), + mMessageName(aMessageName), + mStringParams(aStringParams.Clone()) {} + + const uint32_t mErrorFlags; + const nsCString mCategory; + const nsContentUtils::PropertiesFile mPropertiesFile; + const nsCString mSourceFileURI; + const uint32_t mLineNumber; + const uint32_t mColumnNumber; + const nsCString mMessageName; + const CopyableTArray mStringParams; + }; + + Mutex mMutex; + + // protected by mMutex + nsTArray mPendingReports MOZ_GUARDED_BY(mMutex); + + public: + NS_DECL_THREADSAFE_ISUPPORTS +}; + +} // namespace mozilla + +#endif // mozilla_ConsoleReportCollector_h diff --git a/dom/console/ConsoleUtils.cpp b/dom/console/ConsoleUtils.cpp new file mode 100644 index 0000000000..9767dd30dd --- /dev/null +++ b/dom/console/ConsoleUtils.cpp @@ -0,0 +1,167 @@ +/* -*- 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 "ConsoleUtils.h" +#include "ConsoleCommon.h" +#include "nsContentUtils.h" +#include "nsIConsoleAPIStorage.h" +#include "nsIXPConnect.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/ConsoleBinding.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScriptSettings.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty + +namespace mozilla::dom { + +namespace { + +StaticRefPtr gConsoleUtilsService; + +} + +/* static */ +ConsoleUtils* ConsoleUtils::GetOrCreate() { + if (!gConsoleUtilsService) { + MOZ_ASSERT(NS_IsMainThread()); + + gConsoleUtilsService = new ConsoleUtils(); + ClearOnShutdown(&gConsoleUtilsService); + } + + return gConsoleUtilsService; +} + +ConsoleUtils::ConsoleUtils() = default; +ConsoleUtils::~ConsoleUtils() = default; + +/* static */ +void ConsoleUtils::ReportForServiceWorkerScope(const nsAString& aScope, + const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + Level aLevel) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr service = ConsoleUtils::GetOrCreate(); + if (NS_WARN_IF(!service)) { + return; + } + + service->ReportForServiceWorkerScopeInternal( + aScope, aMessage, aFilename, aLineNumber, aColumnNumber, aLevel); +} + +void ConsoleUtils::ReportForServiceWorkerScopeInternal( + const nsAString& aScope, const nsAString& aMessage, + const nsAString& aFilename, uint32_t aLineNumber, uint32_t aColumnNumber, + Level aLevel) { + MOZ_ASSERT(NS_IsMainThread()); + + AutoJSAPI jsapi; + jsapi.Init(); + + JSContext* cx = jsapi.cx(); + + ConsoleCommon::ClearException ce(cx); + JS::Rooted global(cx, GetOrCreateSandbox(cx)); + if (NS_WARN_IF(!global)) { + return; + } + + // The GetOrCreateSandbox call returns a proxy to the actual sandbox object. + // We don't need a proxy here. + global = js::UncheckedUnwrap(global); + + JSAutoRealm ar(cx, global); + + RootedDictionary event(cx); + + event.mID.Construct(); + event.mID.Value().SetAsString() = aScope; + + event.mInnerID.Construct(); + event.mInnerID.Value().SetAsString() = u"ServiceWorker"_ns; + + switch (aLevel) { + case eLog: + event.mLevel = u"log"_ns; + break; + + case eWarning: + event.mLevel = u"warn"_ns; + break; + + case eError: + event.mLevel = u"error"_ns; + break; + } + + event.mFilename = aFilename; + event.mLineNumber = aLineNumber; + event.mColumnNumber = aColumnNumber; + event.mTimeStamp = JS_Now() / PR_USEC_PER_MSEC; + event.mMicroSecondTimeStamp = JS_Now(); + + JS::Rooted messageValue(cx); + if (!dom::ToJSValue(cx, aMessage, &messageValue)) { + return; + } + + event.mArguments.Construct(); + if (!event.mArguments.Value().AppendElement(messageValue, fallible)) { + return; + } + + nsCOMPtr storage = + do_GetService("@mozilla.org/consoleAPI-storage;1"); + + if (NS_WARN_IF(!storage)) { + return; + } + + JS::Rooted eventValue(cx); + if (!ToJSValue(cx, event, &eventValue)) { + return; + } + + // This is a legacy property. + JS::Rooted eventObj(cx, &eventValue.toObject()); + if (NS_WARN_IF(!JS_DefineProperty(cx, eventObj, "wrappedJSObject", eventObj, + JSPROP_ENUMERATE))) { + return; + } + + storage->RecordEvent(u"ServiceWorker"_ns, eventValue); +} + +JSObject* ConsoleUtils::GetOrCreateSandbox(JSContext* aCx) { + AssertIsOnMainThread(); + + if (!mSandbox) { + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + RefPtr nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + JS::Rooted sandbox(aCx); + nsresult rv = xpc->CreateSandbox(aCx, nullPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + mSandbox = new JSObjectHolder(aCx, sandbox); + } + + return mSandbox->GetJSObject(); +} + +} // namespace mozilla::dom diff --git a/dom/console/ConsoleUtils.h b/dom/console/ConsoleUtils.h new file mode 100644 index 0000000000..6e3d71ea04 --- /dev/null +++ b/dom/console/ConsoleUtils.h @@ -0,0 +1,53 @@ +/* -*- 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_dom_ConsoleUtils_h +#define mozilla_dom_ConsoleUtils_h + +#include "mozilla/JSObjectHolder.h" +#include "nsISupportsImpl.h" +#include "nsString.h" + +namespace mozilla::dom { + +class ConsoleUtils final { + public: + NS_INLINE_DECL_REFCOUNTING(ConsoleUtils) + + enum Level { + eLog, + eWarning, + eError, + }; + + // Main-thread only, reports a console message from a ServiceWorker. + static void ReportForServiceWorkerScope(const nsAString& aScope, + const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, Level aLevel); + + private: + ConsoleUtils(); + ~ConsoleUtils(); + + static ConsoleUtils* GetOrCreate(); + + JSObject* GetOrCreateSandbox(JSContext* aCx); + + void ReportForServiceWorkerScopeInternal(const nsAString& aScope, + const nsAString& aMessage, + const nsAString& aFilename, + uint32_t aLineNumber, + uint32_t aColumnNumber, + Level aLevel); + + RefPtr mSandbox; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ConsoleUtils_h */ diff --git a/dom/console/components.conf b/dom/console/components.conf new file mode 100644 index 0000000000..fec7e41972 --- /dev/null +++ b/dom/console/components.conf @@ -0,0 +1,14 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{96cf7855-dfa9-4c6d-8276-f9705b4890f2}', + 'contract_ids': ['@mozilla.org/consoleAPI-storage;1'], + 'esModule': 'resource://gre/modules/ConsoleAPIStorage.sys.mjs', + 'constructor': 'ConsoleAPIStorageService', + }, +] diff --git a/dom/console/moz.build b/dom/console/moz.build new file mode 100644 index 0000000000..27335d6c5f --- /dev/null +++ b/dom/console/moz.build @@ -0,0 +1,57 @@ +# -*- 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 = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIConsoleAPIStorage.idl", +] + +XPIDL_MODULE = "dom" + +EXPORTS += [ + "nsIConsoleReportCollector.h", +] + +EXPORTS.mozilla += [ + "ConsoleReportCollector.h", +] + +EXPORTS.mozilla.dom += [ + "Console.h", + "ConsoleInstance.h", + "ConsoleUtils.h", +] + +UNIFIED_SOURCES += [ + "Console.cpp", + "ConsoleInstance.cpp", + "ConsoleReportCollector.cpp", + "ConsoleUtils.cpp", +] + +EXTRA_JS_MODULES += [ + "ConsoleAPIStorage.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/js/xpconnect/src", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["tests/chrome.ini"] +XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/console/nsIConsoleAPIStorage.idl b/dom/console/nsIConsoleAPIStorage.idl new file mode 100644 index 0000000000..ec3638c295 --- /dev/null +++ b/dom/console/nsIConsoleAPIStorage.idl @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" +#include "nsIPrincipal.idl" + +[scriptable, uuid(9e32a7b6-c4d1-4d9a-87b9-1ef6b75c27a9)] +interface nsIConsoleAPIStorage : nsISupports +{ + /** + * Get the events array by inner window ID or all events from all windows. + * + * @param string [aId] + * Optional, the inner window ID for which you want to get the array of + * cached events. + * @returns array + * The array of cached events for the given window. If no |aId| is + * given this function returns all of the cached events, from any + * window. + */ + jsval getEvents([optional] in AString aId); + + /** + * Adds a listener to be notified of log events. + * + * @param jsval [aListener] + * A JS listener which will be notified with the message object when + * a log event occurs. + * @param nsIPrincipal [aPrincipal] + * The principal of the listener - used to determine if we need to + * clone the message before forwarding it. + */ + void addLogEventListener(in jsval aListener, in nsIPrincipal aPrincipal); + + /** + * Removes a listener added with `addLogEventListener`. + * + * @param jsval [aListener] + * A JS listener which was added with `addLogEventListener`. + */ + void removeLogEventListener(in jsval aListener); + + /** + * Record an event associated with the given window ID. + * + * @param string aId + * The ID of the inner window for which the event occurred or "jsm" for + * messages logged from JavaScript modules.. + * @param object aEvent + * A JavaScript object you want to store. + */ + void recordEvent(in AString aId, in jsval aEvent); + + /** + * Clear storage data for the given window. + * + * @param string [aId] + * Optional, the inner window ID for which you want to clear the + * messages. If this is not specified all of the cached messages are + * cleared, from all window objects. + */ + void clearEvents([optional] in AString aId); +}; diff --git a/dom/console/nsIConsoleReportCollector.h b/dom/console/nsIConsoleReportCollector.h new file mode 100644 index 0000000000..3e972184e3 --- /dev/null +++ b/dom/console/nsIConsoleReportCollector.h @@ -0,0 +1,132 @@ +/* -*- 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 nsIConsoleReportCollector_h +#define nsIConsoleReportCollector_h + +#include "nsContentUtils.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" + +// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs +#define NS_NSICONSOLEREPORTCOLLECTOR_IID \ + { \ + 0xdd98a481, 0xd2c4, 0x4203, { \ + 0x8d, 0xfa, 0x85, 0xbf, 0xd7, 0xdc, 0xd7, 0x05 \ + } \ + } + +namespace mozilla::net { +class ConsoleReportCollected; +} // namespace mozilla::net + +// An interface for saving reports until we can flush them to the correct +// window at a later time. +class NS_NO_VTABLE nsIConsoleReportCollector : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_NSICONSOLEREPORTCOLLECTOR_IID) + + // Add a pending report to be later displayed on the console. This may be + // called from any thread. + // + // aErrorFlags A nsIScriptError flags value. + // aCategory Name of module reporting error. + // aPropertiesFile Properties file containing localized message. + // aSourceFileURI The URI of the script generating the error. Must be a URI + // spec. + // aLineNumber The line number where the error was generated. May be 0 if + // the line number is not known. + // aColumnNumber The column number where the error was generated. May be 0 + // if the line number is not known. + // aMessageName The name of the localized message contained in the + // properties file. + // aStringParams An array of nsString parameters to use when localizing the + // message. + virtual void AddConsoleReport(uint32_t aErrorFlags, + const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, + uint32_t aLineNumber, uint32_t aColumnNumber, + const nsACString& aMessageName, + const nsTArray& aStringParams) = 0; + + // A version of AddConsoleReport() that accepts the message parameters + // as variable nsString arguments (or really, any sort of const nsAString). + // All other args the same as AddConsoleReport(). + template + void AddConsoleReport(uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + Params&&... aParams) { + nsTArray params; + mozilla::dom::StringArrayAppender::Append(params, sizeof...(Params), + std::forward(aParams)...); + AddConsoleReport(aErrorFlags, aCategory, aPropertiesFile, aSourceFileURI, + aLineNumber, aColumnNumber, aMessageName, params); + } + + // An enum calss to indicate whether should free the pending reports or not. + // Forget Free the pending reports. + // Save Keep the pending reports. + enum class ReportAction { Forget, Save }; + + // Flush all pending reports to the console. May be called from any thread. + // + // aInnerWindowID A inner window ID representing where to flush the reports. + // aAction An action to determine whether to reserve the pending + // reports. Defalut action is to forget the report. + virtual void FlushReportsToConsole( + uint64_t aInnerWindowID, ReportAction aAction = ReportAction::Forget) = 0; + + virtual void FlushReportsToConsoleForServiceWorkerScope( + const nsACString& aScope, + ReportAction aAction = ReportAction::Forget) = 0; + + // Flush all pending reports to the console. Main thread only. + // + // aDocument An optional document representing where to flush the + // reports. If provided, then the corresponding window's + // web console will get the reports. Otherwise the reports + // go to the browser console. + // aAction An action to determine whether to reserve the pending + // reports. Defalut action is to forget the report. + virtual void FlushConsoleReports( + mozilla::dom::Document* aDocument, + ReportAction aAction = ReportAction::Forget) = 0; + + // Flush all pending reports to the console. May be called from any thread. + // + // aLoadGroup An optional loadGroup representing where to flush the + // reports. If provided, then the corresponding window's + // web console will get the reports. Otherwise the reports + // go to the browser console. + // aAction An action to determine whether to reserve the pending + // reports. Defalut action is to forget the report. + virtual void FlushConsoleReports( + nsILoadGroup* aLoadGroup, + ReportAction aAction = ReportAction::Forget) = 0; + + // Flush all pending reports to another collector. May be called from any + // thread. + // + // aCollector A required collector object that will effectively take + // ownership of our currently console reports. + virtual void FlushConsoleReports(nsIConsoleReportCollector* aCollector) = 0; + + // Steal all pending reports to IPC structs. May be called from any thread. + virtual void StealConsoleReports( + nsTArray& aReports) = 0; + + // Clear all pending reports. + virtual void ClearConsoleReports() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIConsoleReportCollector, + NS_NSICONSOLEREPORTCOLLECTOR_IID) + +#endif // nsIConsoleReportCollector_h diff --git a/dom/console/tests/chrome.ini b/dom/console/tests/chrome.ini new file mode 100644 index 0000000000..b1f4e082c2 --- /dev/null +++ b/dom/console/tests/chrome.ini @@ -0,0 +1,9 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + file_empty.html + console.sys.mjs + head.js + +[test_console.xhtml] +[test_jsm.xhtml] diff --git a/dom/console/tests/console.sys.mjs b/dom/console/tests/console.sys.mjs new file mode 100644 index 0000000000..d239d58a5d --- /dev/null +++ b/dom/console/tests/console.sys.mjs @@ -0,0 +1,45 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +export var ConsoleTest = { + go(dumpFunction) { + console.log("Hello world!"); + console.createInstance().log("Hello world!"); + + let c = console.createInstance({ + consoleID: "wow", + innerID: "CUSTOM INNER", + dump: dumpFunction, + prefix: "_PREFIX_", + }); + + c.log("Hello world!"); + c.trace("Hello world!"); + + console + .createInstance({ innerID: "LEVEL", maxLogLevel: "Off" }) + .log("Invisible!"); + console + .createInstance({ innerID: "LEVEL", maxLogLevel: "All" }) + .log("Hello world!"); + console + .createInstance({ + innerID: "LEVEL", + maxLogLevelPref: "pref.test.console", + }) + .log("Hello world!"); + + this.c2 = console.createInstance({ + innerID: "NO PREF", + maxLogLevel: "Warn", + maxLogLevelPref: "pref.test.console.notset", + }); + this.c2.log("Invisible!"); + this.c2.warn("Hello world!"); + }, + + go2() { + this.c2.log("Hello world!"); + }, +}; diff --git a/dom/console/tests/file_empty.html b/dom/console/tests/file_empty.html new file mode 100644 index 0000000000..495c23ec8a --- /dev/null +++ b/dom/console/tests/file_empty.html @@ -0,0 +1 @@ + diff --git a/dom/console/tests/head.js b/dom/console/tests/head.js new file mode 100644 index 0000000000..3e9c72cfb9 --- /dev/null +++ b/dom/console/tests/head.js @@ -0,0 +1,24 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" +].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + +// This is intended to just be a drop-in replacement for an old observer +// notification. +function addConsoleStorageListener(listener) { + listener.__handler = (message, id) => { + listener.observe(message, id); + }; + ConsoleAPIStorage.addLogEventListener( + listener.__handler, + SpecialPowers.wrap(document).nodePrincipal + ); +} + +function removeConsoleStorageListener(listener) { + ConsoleAPIStorage.removeLogEventListener(listener.__handler); +} diff --git a/dom/console/tests/mochitest.ini b/dom/console/tests/mochitest.ini new file mode 100644 index 0000000000..d66a7245b3 --- /dev/null +++ b/dom/console/tests/mochitest.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + file_empty.html + head.js + +[test_bug659625.html] +[test_bug978522.html] +[test_bug979109.html] +[test_bug989665.html] +[test_consoleEmptyStack.html] +[test_console_binding.html] +[test_console_proto.html] +[test_timer.html] +[test_count.html] diff --git a/dom/console/tests/test_bug659625.html b/dom/console/tests/test_bug659625.html new file mode 100644 index 0000000000..1cf93d9bfd --- /dev/null +++ b/dom/console/tests/test_bug659625.html @@ -0,0 +1,107 @@ + + + + + + Test for Bug 659625 + + + + +Mozilla Bug 659625 + + + diff --git a/dom/console/tests/test_bug978522.html b/dom/console/tests/test_bug978522.html new file mode 100644 index 0000000000..b0cc4e76b9 --- /dev/null +++ b/dom/console/tests/test_bug978522.html @@ -0,0 +1,32 @@ + + + + + + Test for Bug 978522 - basic support + + + + +Mozilla Bug 978522 + + + diff --git a/dom/console/tests/test_bug979109.html b/dom/console/tests/test_bug979109.html new file mode 100644 index 0000000000..231808e260 --- /dev/null +++ b/dom/console/tests/test_bug979109.html @@ -0,0 +1,32 @@ + + + + + + Test for Bug 979109 + + + + +Mozilla Bug 979109 + + + diff --git a/dom/console/tests/test_bug989665.html b/dom/console/tests/test_bug989665.html new file mode 100644 index 0000000000..484656a06b --- /dev/null +++ b/dom/console/tests/test_bug989665.html @@ -0,0 +1,21 @@ + + + + + + Test for Bug 989665 + + + + +Mozilla Bug 989665 + + + diff --git a/dom/console/tests/test_console.xhtml b/dom/console/tests/test_console.xhtml new file mode 100644 index 0000000000..cbeb9469e7 --- /dev/null +++ b/dom/console/tests/test_console.xhtml @@ -0,0 +1,35 @@ + + + + + + diff --git a/dom/console/tests/test_consoleEmptyStack.html b/dom/console/tests/test_consoleEmptyStack.html new file mode 100644 index 0000000000..ec77d0ac6f --- /dev/null +++ b/dom/console/tests/test_consoleEmptyStack.html @@ -0,0 +1,26 @@ + + + + + Test for empty stack in console + + + + + + + + diff --git a/dom/console/tests/test_console_binding.html b/dom/console/tests/test_console_binding.html new file mode 100644 index 0000000000..0ec1926400 --- /dev/null +++ b/dom/console/tests/test_console_binding.html @@ -0,0 +1,40 @@ + + + + + Test Console binding + + + + + + + + diff --git a/dom/console/tests/test_console_proto.html b/dom/console/tests/test_console_proto.html new file mode 100644 index 0000000000..3e9461bade --- /dev/null +++ b/dom/console/tests/test_console_proto.html @@ -0,0 +1,17 @@ + + + + + Test for console.__proto__ + + + + + + + diff --git a/dom/console/tests/test_count.html b/dom/console/tests/test_count.html new file mode 100644 index 0000000000..5591768150 --- /dev/null +++ b/dom/console/tests/test_count.html @@ -0,0 +1,117 @@ + + + + + Test for count/countReset in console + + + + + + + + diff --git a/dom/console/tests/test_jsm.xhtml b/dom/console/tests/test_jsm.xhtml new file mode 100644 index 0000000000..9296d40b22 --- /dev/null +++ b/dom/console/tests/test_jsm.xhtml @@ -0,0 +1,100 @@ + + + + + + + + + diff --git a/dom/console/tests/test_timer.html b/dom/console/tests/test_timer.html new file mode 100644 index 0000000000..fa3ca7baec --- /dev/null +++ b/dom/console/tests/test_timer.html @@ -0,0 +1,112 @@ + + + + + Test for timeStart/timeLog/timeEnd in console + + + + + + + + diff --git a/dom/console/tests/xpcshell/head.js b/dom/console/tests/xpcshell/head.js new file mode 100644 index 0000000000..446eb9ba9e --- /dev/null +++ b/dom/console/tests/xpcshell/head.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService( + Ci.nsIConsoleAPIStorage +); + +// This is intended to just be a drop-in replacement for an old observer +// notification. +function addConsoleStorageListener(listener) { + listener.__handler = (message, id) => { + listener.observe(message, id); + }; + ConsoleAPIStorage.addLogEventListener( + listener.__handler, + Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) + ); +} + +function removeConsoleStorageListener(listener) { + ConsoleAPIStorage.removeLogEventListener(listener.__handler); +} diff --git a/dom/console/tests/xpcshell/test_basic.js b/dom/console/tests/xpcshell/test_basic.js new file mode 100644 index 0000000000..5736912979 --- /dev/null +++ b/dom/console/tests/xpcshell/test_basic.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + Assert.ok("console" in this); + + let p = new Promise(resolve => { + function consoleListener() { + addConsoleStorageListener(this); + } + + consoleListener.prototype = { + observe(aSubject) { + let obj = aSubject.wrappedJSObject; + Assert.ok(obj.arguments[0] === 42, "Message received!"); + Assert.ok(obj.ID === "jsm", "The ID is JSM"); + Assert.ok(obj.innerID.endsWith("test_basic.js"), "The innerID matches"); + + removeConsoleStorageListener(this); + resolve(); + }, + }; + + new consoleListener(); + }); + + console.log(42); + await p; +}); diff --git a/dom/console/tests/xpcshell/test_failing_console_listener.js b/dom/console/tests/xpcshell/test_failing_console_listener.js new file mode 100644 index 0000000000..bb1b7cd46c --- /dev/null +++ b/dom/console/tests/xpcshell/test_failing_console_listener.js @@ -0,0 +1,35 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + Assert.ok("console" in this); + + // Add a first listener that synchronously throws. + const removeListener1 = addConsoleListener(() => { + throw new Error("Fail"); + }); + + // Add a second listener updating a flag we can observe from the test. + let secondListenerCalled = false; + const removeListener2 = addConsoleListener( + () => (secondListenerCalled = true) + ); + + console.log(42); + Assert.ok(secondListenerCalled, "Second listener was called"); + + // Cleanup listeners. + removeListener1(); + removeListener2(); +}); + +function addConsoleListener(callback) { + const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( + Ci.nsIPrincipal + ); + ConsoleAPIStorage.addLogEventListener(callback, principal); + + return () => { + ConsoleAPIStorage.removeLogEventListener(callback, principal); + }; +} diff --git a/dom/console/tests/xpcshell/test_formatting.js b/dom/console/tests/xpcshell/test_formatting.js new file mode 100644 index 0000000000..89d5a1947e --- /dev/null +++ b/dom/console/tests/xpcshell/test_formatting.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + Assert.ok("console" in this); + + const tests = [ + // Plain value. + [[42], ["42"]], + + // Integers. + [["%d", 42], ["42"]], + [["%i", 42], ["42"]], + [["c%iao", 42], ["c42ao"]], + + // Floats. + [["%2.4f", 42], ["42.0000"]], + [["%2.2f", 42], ["42.00"]], + [["%1.2f", 42], ["42.00"]], + [["a%3.2fb", 42], ["a42.00b"]], + [["%f", NaN], ["NaN"]], + + // Strings + [["%s", 42], ["42"]], + + // Empty values. + [ + ["", 42], + ["", "42"], + ], + [ + ["", 42], + ["", "42"], + ], + ]; + + let p = new Promise(resolve => { + let t = 0; + + function consoleListener() { + addConsoleStorageListener(this); + } + + consoleListener.prototype = { + observe(aSubject) { + let test = tests[t++]; + + let obj = aSubject.wrappedJSObject; + Assert.equal( + obj.arguments.length, + test[1].length, + "Same number of arguments" + ); + for (let i = 0; i < test[1].length; ++i) { + Assert.equal( + "" + obj.arguments[i], + test[1][i], + "Message received: " + test[1][i] + ); + } + + if (t === tests.length) { + removeConsoleStorageListener(this); + resolve(); + } + }, + }; + + new consoleListener(); + }); + + tests.forEach(test => { + console.log(...test[0]); + }); + + await p; +}); diff --git a/dom/console/tests/xpcshell/test_reportForServiceWorkerScope.js b/dom/console/tests/xpcshell/test_reportForServiceWorkerScope.js new file mode 100644 index 0000000000..f96519183c --- /dev/null +++ b/dom/console/tests/xpcshell/test_reportForServiceWorkerScope.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function () { + let p = new Promise(resolve => { + function consoleListener() { + addConsoleStorageListener(this); + } + + consoleListener.prototype = { + observe(aSubject) { + let obj = aSubject.wrappedJSObject; + Assert.ok(obj.arguments[0] === "Hello world!", "Message received!"); + Assert.ok(obj.ID === "scope", "The ID is the scope"); + Assert.ok( + obj.innerID === "ServiceWorker", + "The innerID is ServiceWorker" + ); + Assert.ok(obj.filename === "filename", "The filename matches"); + Assert.ok(obj.lineNumber === 42, "The lineNumber matches"); + Assert.ok(obj.columnNumber === 24, "The columnNumber matches"); + Assert.ok(obj.level === "error", "The level is correct"); + + removeConsoleStorageListener(this); + resolve(); + }, + }; + + new consoleListener(); + }); + + let ci = console.createInstance(); + ci.reportForServiceWorkerScope( + "scope", + "Hello world!", + "filename", + 42, + 24, + "error" + ); + await p; +}); diff --git a/dom/console/tests/xpcshell/xpcshell.ini b/dom/console/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..51cc86f54c --- /dev/null +++ b/dom/console/tests/xpcshell/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = head.js +support-files = + +[test_basic.js] +[test_failing_console_listener.js] +[test_reportForServiceWorkerScope.js] +[test_formatting.js] -- cgit v1.2.3