diff options
Diffstat (limited to 'dom/console')
36 files changed, 5761 insertions, 0 deletions
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<nsIGlobalObject> mGlobal; + nsTArray<RefPtr<BlobImpl>> mBlobs; +}; + +static void ComposeAndStoreGroupName(JSContext* aCx, + const Sequence<JS::Value>& aData, + nsAString& aName, + nsTArray<nsString>* aGroupStack); +static bool UnstoreGroupName(nsAString& aName, nsTArray<nsString>* aGroupStack); + +static bool ProcessArguments(JSContext* aCx, const Sequence<JS::Value>& aData, + Sequence<JS::Value>& aSequence, + Sequence<nsString>& 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<ConsoleStackEntry> mTopStackFrame; + Maybe<nsTArray<ConsoleStackEntry>> mReifiedStack; + nsCOMPtr<nsIStackFrame> 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<JS::Value>& 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<nsIConsoleAPIStorage> mStorage; + RefPtr<JSObjectHolder> mSandbox; + nsTArray<nsString> 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<JS::Value> val(aCx); + { + nsCOMPtr<nsIGlobalObject> global = mClonedData.mGlobal; + RefPtr<Blob> 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<JSObject*> aObj, + bool* aSameProcessScopeRequired) override { + RefPtr<Blob> 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<JS::Value> value(aCx, JS::ObjectOrNullValue(aObj)); + JS::Rooted<JSString*> 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<JS::Value> argumentsValue(aCx); + if (!Read(aCx, &argumentsValue)) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + + JS::Rooted<JSObject*> argumentsObj(aCx, &argumentsValue.toObject()); + + uint32_t length; + if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { + return; + } + + Sequence<JS::Value> values; + SequenceRooter<JS::Value> arguments(aCx, &values); + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted<JS::Value> 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<JS::Value>& aArguments) { + ConsoleCommon::ClearException ce(aCx); + + JS::Rooted<JSObject*> arguments( + aCx, JS::NewArrayObject(aCx, aArguments.Length())); + if (NS_WARN_IF(!arguments)) { + return false; + } + + JS::Rooted<JS::Value> 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<JS::Value> 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<JS::Value> argumentsValue(aCx); + bool ok = Read(aCx, &argumentsValue); + mClonedData.mGlobal = nullptr; + + if (!ok) { + return; + } + + MOZ_ASSERT(argumentsValue.isObject()); + JS::Rooted<JSObject*> argumentsObj(aCx, &argumentsValue.toObject()); + if (NS_WARN_IF(!argumentsObj)) { + return; + } + + uint32_t length; + if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { + return; + } + + Sequence<JS::Value> arguments; + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted<JS::Value> 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<JS::Value> 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<WorkletGlobalScope> global = do_QueryInterface(aConsole->mGlobal); + MOZ_ASSERT(global); + mWorkletImpl = global->Impl(); + MOZ_ASSERT(mWorkletImpl); + } + + ~ConsoleWorkletRunnable() override = default; + + protected: + RefPtr<MainThreadConsoleData> mConsoleData; + + RefPtr<WorkletImpl> 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<ConsoleCallDataWorkletRunnable> Create( + JSContext* aCx, Console* aConsole, ConsoleCallData* aConsoleData, + const Sequence<JS::Value>& aArguments) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr<ConsoleCallDataWorkletRunnable> 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<JSObject*> 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<ConsoleCallData> mCallData; +}; + +class ConsoleWorkerRunnable : public WorkerProxyToMainThreadRunnable, + public ConsoleRunnable { + public: + explicit ConsoleWorkerRunnable(Console* aConsole) + : mConsoleData(aConsole->GetOrCreateMainThreadData()) {} + + ~ConsoleWorkerRunnable() override = default; + + bool Dispatch(JSContext* aCx, const Sequence<JS::Value>& 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<nsPIDOMWindowInner> 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<nsGlobalWindowInner> win = nsGlobalWindowInner::Cast(aWindow); + if (NS_WARN_IF(!jsapi.Init(win))) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> 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<JSObject*> 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<nsIGlobalObject> 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<MainThreadConsoleData> 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<ConsoleCallData> mCallData; +}; + +// This runnable calls ProfileMethod() on the console on the main-thread. +class ConsoleProfileWorkletRunnable final : public ConsoleWorkletRunnable { + public: + static already_AddRefed<ConsoleProfileWorkletRunnable> Create( + JSContext* aCx, Console* aConsole, Console::MethodName aName, + const nsAString& aAction, const Sequence<JS::Value>& aArguments) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr<ConsoleProfileWorkletRunnable> 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<JSObject*> 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> 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> 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> Console::CreateForWorklet(JSContext* aCx, + nsIGlobalObject* aGlobal, + uint64_t aOuterWindowID, + uint64_t aInnerWindowID, + ErrorResult& aRv) { + WorkletThread::AssertIsOnWorkletThread(); + + RefPtr<Console> 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<nsIObserverService> 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<nsIObserverService> 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<nsISupportsPRUint64> 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<JS::Value>& 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<JS::Value> data; + Method(aGlobal, MethodClear, u"clear"_ns, data); +} + +/* static */ +void Console::GroupEnd(const GlobalObject& aGlobal) { + const Sequence<JS::Value> data; + Method(aGlobal, MethodGroupEnd, u"groupEnd"_ns, data); +} + +/* static */ +void Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTime, u"time"_ns); +} + +/* static */ +void Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodTimeEnd, + u"timeEnd"_ns); +} + +/* static */ +void Console::TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence<JS::Value>& aData) { + StringMethod(aGlobal, aLabel, aData, MethodTimeLog, u"timeLog"_ns); +} + +/* static */ +void Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence<JS::Value>& aData, + MethodName aMethodName, + const nsAString& aMethodString) { + RefPtr<Console> console = GetConsole(aGlobal); + if (!console) { + return; + } + + console->StringMethodInternal(aGlobal.Context(), aLabel, aData, aMethodName, + aMethodString); +} + +void Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel, + const Sequence<JS::Value>& aData, + MethodName aMethodName, + const nsAString& aMethodString) { + ConsoleCommon::ClearException ce(aCx); + + Sequence<JS::Value> data; + SequenceRooter<JS::Value> rooter(aCx, &data); + + JS::Rooted<JS::Value> 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<JS::Value> aData) { + JSContext* cx = aGlobal.Context(); + + ConsoleCommon::ClearException ce(cx); + + Sequence<JS::Value> data; + SequenceRooter<JS::Value> 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<JS::Value>& aData) { + ProfileMethod(aGlobal, MethodProfile, u"profile"_ns, aData); +} + +/* static */ +void Console::ProfileEnd(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData) { + ProfileMethod(aGlobal, MethodProfileEnd, u"profileEnd"_ns, aData); +} + +/* static */ +void Console::ProfileMethod(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aAction, + const Sequence<JS::Value>& aData) { + RefPtr<Console> 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<JS::Value>& aData) { + if (!ShouldProceed(aMethodName)) { + return; + } + + MaybeExecuteDumpFunction(aCx, aAction, aData, nullptr); + + if (WorkletThread::IsOnWorkletThread()) { + RefPtr<ConsoleProfileWorkletRunnable> 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<ConsoleProfileWorkerRunnable> 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<JS::Value>& aData) { + MOZ_ASSERT(NS_IsMainThread()); + ConsoleCommon::ClearException ce(aCx); + + RootedDictionary<ConsoleProfileEvent> event(aCx); + event.mAction = aAction; + event.mChromeContext = nsContentUtils::ThreadsafeIsSystemCaller(aCx); + + event.mArguments.Construct(); + Sequence<JS::Value>& sequence = event.mArguments.Value(); + + for (uint32_t i = 0; i < aData.Length(); ++i) { + if (!sequence.AppendElement(aData[i], fallible)) { + return; + } + } + + JS::Rooted<JS::Value> eventValue(aCx); + if (!ToJSValue(aCx, event, &eventValue)) { + return; + } + + JS::Rooted<JSObject*> eventObj(aCx, &eventValue.toObject()); + MOZ_ASSERT(eventObj); + + if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue, + JSPROP_ENUMERATE)) { + return; + } + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + nsCOMPtr<nsISupports> wrapper; + const nsIID& iid = NS_GET_IID(nsISupports); + + if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) { + return; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(wrapper, "console-api-profiler", nullptr); + } +} + +/* static */ +void Console::Assert(const GlobalObject& aGlobal, bool aCondition, + const Sequence<JS::Value>& aData) { + if (!aCondition) { + Method(aGlobal, MethodAssert, u"assert"_ns, aData); + } +} + +/* static */ +void Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), MethodCount, + u"count"_ns); +} + +/* static */ +void Console::CountReset(const GlobalObject& aGlobal, const nsAString& aLabel) { + StringMethod(aGlobal, aLabel, Sequence<JS::Value>(), 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<ConsoleStackEntry>& aRefiedStack) { + nsCOMPtr<nsIStackFrame> stack(aStack); + + while (stack) { + ConsoleStackEntry& data = *aRefiedStack.AppendElement(); + StackFrameToStackEntry(aCx, stack, data); + + nsCOMPtr<nsIStackFrame> 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<JS::Value>& aData) { + RefPtr<Console> 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<JS::Value>& aData) { + if (!ShouldProceed(aMethodName)) { + return; + } + + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + RefPtr<ConsoleCallData> 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<nsIScriptObjectPrincipal> sop = do_QueryInterface(mGlobal); + if (NS_WARN_IF(!sop)) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return; + } + + oa = principal->OriginAttributesRef(); + callData->SetAddonId(principal); + +#ifdef DEBUG + if (!principal->IsSystemPrincipal()) { + nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(mGlobal); + if (webNav) { + nsCOMPtr<nsILoadContext> 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<WorkletGlobalScope> 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<nsIStackFrame> 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<ConsoleCallDataWorkletRunnable> 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<ConsoleCallDataWorkerRunnable> 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<JSObject*> 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<nsIStackFrame*>(v.toPrivate()); + nsTArray<ConsoleStackEntry> reifiedStack; + ReifyStack(aCx, stack, reifiedStack); + + JS::Rooted<JS::Value> 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<JS::Value>& aArguments) { + AssertIsOnMainThread(); + MOZ_ASSERT(aData); + + JS::Rooted<JS::Value> 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<JSObject*> 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<nsresult> 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<JS::Value>& aArguments, + JS::Handle<JSObject*> aTargetScope, + JS::MutableHandle<JS::Value> aEventValue, ConsoleCallData* aData, + nsTArray<nsString>* 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<ConsoleEvent> 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<nsIURI> filenameURI; + nsAutoCString pass; + if (NS_IsMainThread() && + NS_SUCCEEDED(NS_NewURI(getter_AddRefs(filenameURI), frame.mFilename)) && + NS_SUCCEEDED(filenameURI->GetPassword(pass)) && !pass.IsEmpty()) { + nsCOMPtr<nsISensitiveInfoHiddenURI> 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<JSObject*> 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<JS::Value> 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<JSObject*> 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<JS::Value> 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<JS::Value>& aSequence, + nsString& aOutput) { + if (!aOutput.IsEmpty()) { + JS::Rooted<JSString*> 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<JS::Value>& aData, + Sequence<JS::Value>& aSequence, + Sequence<nsString>& 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<JS::Value> format(aCx, aData[0]); + JS::Rooted<JSString*> 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 %<number>.<number> 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<JS::Value> 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<JS::Value> v(aCx, aData[index++]); + JS::Rooted<JSString*> 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<JS::Value> value(aCx, aData[index++]); + JS::Rooted<JSString*> 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<JS::Value> value(aCx, aData[index++]); + + if (value.isBigInt()) { + JS::Rooted<JSString*> 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<JS::Value> 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<JS::Value>& aData, + nsAString& aName, + nsTArray<nsString>* aGroupStack) { + StringJoinAppend( + aName, u" "_ns, aData, [aCx](nsAString& dest, const JS::Value& valueRef) { + JS::Rooted<JS::Value> value(aCx, valueRef); + JS::Rooted<JSString*> 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<nsString>* 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<JS::Value> name(aCx, aName); + JS::Rooted<JSString*> 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<ConsoleTimerStart> timer(aCx); + + timer.mName = aTimerLabel; + + JS::Rooted<JS::Value> 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<JS::Value> name(aCx, aName); + JS::Rooted<JSString*> 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<ConsoleTimerLogOrEnd> timer(aCx); + timer.mName = aLabel; + timer.mDuration = aDuration; + + JS::Rooted<JS::Value> 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<ConsoleTimerError> 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<JS::Value> value(aCx); + if (!ToJSValue(aCx, error, &value)) { + return JS::UndefinedValue(); + } + + return value; +} + +uint32_t Console::IncreaseCounter(JSContext* aCx, + const Sequence<JS::Value>& aArguments, + nsAString& aCountLabel) { + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + MOZ_ASSERT(!aArguments.IsEmpty()); + + JS::Rooted<JS::Value> labelValue(aCx, aArguments[0]); + JS::Rooted<JSString*> 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<JS::Value>& aArguments, + nsAString& aCountLabel) { + AssertIsOnOwningThread(); + + ConsoleCommon::ClearException ce(aCx); + + MOZ_ASSERT(!aArguments.IsEmpty()); + + JS::Rooted<JS::Value> labelValue(aCx, aArguments[0]); + JS::Rooted<JSString*> 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<ConsoleCounterError> error(aCx); + error.mLabel = aCountLabel; + error.mError.AssignLiteral("counterDoesntExist"); + + JS::Rooted<JS::Value> value(aCx); + if (!ToJSValue(aCx, error, &value)) { + return JS::UndefinedValue(); + } + + return value; + } + + RootedDictionary<ConsoleCounter> data(aCx); + data.mLabel = aCountLabel; + data.mCount = aCountValue; + + JS::Rooted<JS::Value> 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<JSObject*> 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<JS::Value>& 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<JS::Value>& aArguments, + ConsoleCallData* aCallData) { + AssertIsOnOwningThread(); + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCallData); + + if (!mConsoleEventNotifier) { + return; + } + + JS::Rooted<JS::Value> value(aCx); + + JS::Rooted<JSObject*> 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<JS::Value> ignored(aCx); + RefPtr<AnyCallback> notifier(mConsoleEventNotifier); + notifier->Call(value, &ignored); +} + +void Console::RetrieveConsoleEvents(JSContext* aCx, + nsTArray<JS::Value>& aEvents, + ErrorResult& aRv) { + AssertIsOnOwningThread(); + + // We don't want to expose this functionality to main-thread yet. + MOZ_ASSERT(!NS_IsMainThread()); + + JS::Rooted<JSObject*> targetScope(aCx, JS::CurrentGlobalOrNull(aCx)); + + for (uint32_t i = 0; i < mArgumentStorage.length(); ++i) { + JS::Rooted<JS::Value> value(aCx); + + JS::Rooted<JSObject*> sequenceScope(aCx, mArgumentStorage[i].Global()); + JSAutoRealm ar(aCx, sequenceScope); + + Sequence<JS::Value> sequence; + SequenceRooter<JS::Value> 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> Console::GetConsole(const GlobalObject& aGlobal) { + ErrorResult rv; + RefPtr<Console> 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> Console::GetConsoleInternal( + const GlobalObject& aGlobal, ErrorResult& aRv) { + // Window + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> innerWindow = + do_QueryInterface(aGlobal.GetAsSupports()); + + // we are probably running a chrome script. + if (!innerWindow) { + RefPtr<Console> 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<WorkletGlobalScope> 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<nsIGlobalObject> 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<JS::Value>& aData, + DOMHighResTimeStamp* aTimeStamp) { + if (nsCOMPtr<nsPIDOMWindowInner> innerWindow = do_QueryInterface(mGlobal)) { + nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(innerWindow); + MOZ_ASSERT(win); + + RefPtr<Performance> performance = win->GetPerformance(); + if (!performance) { + return false; + } + + *aTimeStamp = performance->Now(); + + nsDocShell* docShell = static_cast<nsDocShell*>(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<JS::Value> value( + aCx, aData.Length() == 0 ? JS_GetEmptyStringValue(aCx) : aData[0]); + JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return false; + } + + nsAutoJSString key; + if (!key.init(aCx, jsString)) { + return false; + } + + TimelineConsumers::AddMarkerForDocShell( + docShell, MakeUnique<TimestampTimelineMarker>(key)); + } + // For `console.time(foo)` and `console.timeEnd(foo)`. + else if (isTimelineRecording && aData.Length() == 1) { + JS::Rooted<JS::Value> value(aCx, aData[0]); + JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value)); + if (!jsString) { + return false; + } + + nsAutoJSString key; + if (!key.init(aCx, jsString)) { + return false; + } + + TimelineConsumers::AddMarkerForDocShell( + docShell, + MakeUnique<ConsoleTimelineMarker>(key, aMethodName == MethodTime + ? MarkerTracingType::START + : MarkerTracingType::END)); + } + + return true; + } + + if (NS_IsMainThread()) { + *aTimeStamp = (TimeStamp::Now() - mCreationTimeStamp).ToMilliseconds(); + return true; + } + + if (nsCOMPtr<WorkletGlobalScope> 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<ConsoleInstance> Console::CreateInstance( + const GlobalObject& aGlobal, const ConsoleInstanceOptions& aOptions) { + RefPtr<ConsoleInstance> 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<JS::Value>& 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<JS::Value> 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*> 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<nsIStackFrame> 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<nsIStackFrame> 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<JS::Value> v(aCx, aData); + JS::Rooted<JSString*> 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<ConsoleInstanceDumpCallback> 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<ConsoleLogLevel>(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<JS::Value>& 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<JS::Value>& 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<Console> Create(JSContext* aCx, + nsPIDOMWindowInner* aWindow, + ErrorResult& aRv); + + static already_AddRefed<Console> 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<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Info(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Warn(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Error(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Exception(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Debug(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Table(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Trace(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Dir(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Dirxml(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Group(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void GroupCollapsed(const GlobalObject& aGlobal, + const Sequence<JS::Value>& 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<JS::Value>& 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<JS::Value> aData); + + MOZ_CAN_RUN_SCRIPT + static void Profile(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void ProfileEnd(const GlobalObject& aGlobal, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Assert(const GlobalObject& aGlobal, bool aCondition, + const Sequence<JS::Value>& 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<ConsoleInstance> CreateInstance( + const GlobalObject& aGlobal, const ConsoleInstanceOptions& aOptions); + + void ClearStorage(); + + void RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& 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<Console> GetConsole(const GlobalObject& aGlobal); + + static already_AddRefed<Console> GetConsoleInternal( + const GlobalObject& aGlobal, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT + static void ProfileMethod(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aAction, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void ProfileMethodInternal(JSContext* aCx, MethodName aName, + const nsAString& aAction, + const Sequence<JS::Value>& 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<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void Method(const GlobalObject& aGlobal, MethodName aName, + const nsAString& aString, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void MethodInternal(JSContext* aCx, MethodName aName, + const nsAString& aString, + const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + static void StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel, + const Sequence<JS::Value>& aData, + MethodName aMethodName, + const nsAString& aMethodString); + + MOZ_CAN_RUN_SCRIPT + void StringMethodInternal(JSContext* aCx, const nsAString& aLabel, + const Sequence<JS::Value>& aData, + MethodName aMethodName, + const nsAString& aMethodString); + + MainThreadConsoleData* GetOrCreateMainThreadData(); + + // Returns true on success; otherwise false. + bool StoreCallData(JSContext* aCx, ConsoleCallData* aCallData, + const Sequence<JS::Value>& 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<JS::Value>& 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<JS::Value>& aArguments, + JS::Handle<JSObject*> aTargetScope, + JS::MutableHandle<JS::Value> aEventValue, ConsoleCallData* aData, + nsTArray<nsString>* 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<JS::Value>& aData, + Sequence<JS::Value>& 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<JS::Value>& 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<JS::Value>& aData, + nsAString& aCountLabel); + + static bool ShouldIncludeStackTrace(MethodName aMethodName); + + void AssertIsOnOwningThread() const; + + bool IsShuttingDown() const; + + bool MonotonicTimer(JSContext* aCx, MethodName aMethodName, + const Sequence<JS::Value>& aData, + DOMHighResTimeStamp* aTimeStamp); + + void StringifyElement(Element* aElement, nsAString& aOut); + + MOZ_CAN_RUN_SCRIPT + void MaybeExecuteDumpFunction(JSContext* aCx, const nsAString& aMethodName, + const Sequence<JS::Value>& 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<JS::Value>& aArguments); + void Trace(const TraceCallbacks& aCallbacks, void* aClosure); + bool PopulateArgumentsSequence(Sequence<JS::Value>& aSequence) const; + JSObject* Global() const { return mGlobal; } + + private: + void AssertIsOnOwningThread() const { + NS_ASSERT_OWNINGTHREAD(ArgumentData); + } + + NS_DECL_OWNINGTHREAD; + JS::Heap<JSObject*> mGlobal; + nsTArray<JS::Heap<JS::Value>> mArguments; + }; + + // Owning/CC thread only + nsCOMPtr<nsIGlobalObject> mGlobal; + + // Touched on the owner thread. + nsTHashMap<nsStringHashKey, DOMHighResTimeStamp> mTimerRegistry; + nsTHashMap<nsStringHashKey, uint32_t> mCounterRegistry; + + nsTArray<RefPtr<ConsoleCallData>> mCallDataStorage; + // These are references to the arguments we received in each call + // from the DOM bindings. + // Vector<T> 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<ArgumentData> mArgumentStorage; + + RefPtr<AnyCallback> mConsoleEventNotifier; + + RefPtr<MainThreadConsoleData> mMainThreadData; + // This is the stack for grouping relating to Console-thread events, when + // the Console thread is not the main thread. + nsTArray<nsString> mGroupStack; + + uint64_t mOuterID; + uint64_t mInnerID; + + // Set only by ConsoleInstance: + nsString mConsoleID; + nsString mPassedInnerID; + RefPtr<ConsoleInstanceDumpCallback> 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<JSObject*> aGivenProto) { + return ConsoleInstance_Binding::Wrap(aCx, this, aGivenProto); +} + +#define METHOD(name, string) \ + void ConsoleInstance::name(JSContext* aCx, \ + const Sequence<JS::Value>& aData) { \ + RefPtr<Console> 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<JS::Value> data; + RefPtr<Console> console(mConsole); + console->MethodInternal(aCx, Console::MethodGroupEnd, u"groupEnd"_ns, data); +} + +void ConsoleInstance::Time(JSContext* aCx, const nsAString& aLabel) { + RefPtr<Console> console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(), + Console::MethodTime, u"time"_ns); +} + +void ConsoleInstance::TimeLog(JSContext* aCx, const nsAString& aLabel, + const Sequence<JS::Value>& aData) { + RefPtr<Console> console(mConsole); + console->StringMethodInternal(aCx, aLabel, aData, Console::MethodTimeLog, + u"timeLog"_ns); +} + +void ConsoleInstance::TimeEnd(JSContext* aCx, const nsAString& aLabel) { + RefPtr<Console> console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(), + Console::MethodTimeEnd, u"timeEnd"_ns); +} + +void ConsoleInstance::TimeStamp(JSContext* aCx, + const JS::Handle<JS::Value> aData) { + ConsoleCommon::ClearException ce(aCx); + + Sequence<JS::Value> data; + SequenceRooter<JS::Value> rooter(aCx, &data); + + if (aData.isString() && !data.AppendElement(aData, fallible)) { + return; + } + + RefPtr<Console> console(mConsole); + console->MethodInternal(aCx, Console::MethodTimeStamp, u"timeStamp"_ns, data); +} + +void ConsoleInstance::Profile(JSContext* aCx, + const Sequence<JS::Value>& aData) { + RefPtr<Console> console(mConsole); + console->ProfileMethodInternal(aCx, Console::MethodProfile, u"profile"_ns, + aData); +} + +void ConsoleInstance::ProfileEnd(JSContext* aCx, + const Sequence<JS::Value>& aData) { + RefPtr<Console> console(mConsole); + console->ProfileMethodInternal(aCx, Console::MethodProfileEnd, + u"profileEnd"_ns, aData); +} + +void ConsoleInstance::Assert(JSContext* aCx, bool aCondition, + const Sequence<JS::Value>& aData) { + if (!aCondition) { + RefPtr<Console> console(mConsole); + console->MethodInternal(aCx, Console::MethodAssert, u"assert"_ns, aData); + } +} + +void ConsoleInstance::Count(JSContext* aCx, const nsAString& aLabel) { + RefPtr<Console> console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(), + Console::MethodCount, u"count"_ns); +} + +void ConsoleInstance::CountReset(JSContext* aCx, const nsAString& aLabel) { + RefPtr<Console> console(mConsole); + console->StringMethodInternal(aCx, aLabel, Sequence<JS::Value>(), + Console::MethodCountReset, u"countReset"_ns); +} + +void ConsoleInstance::Clear(JSContext* aCx) { + const Sequence<JS::Value> data; + RefPtr<Console> 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<JSObject*> aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return nullptr; } + + MOZ_CAN_RUN_SCRIPT + void Log(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Info(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Warn(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Error(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Exception(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Debug(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Table(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Trace(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Dir(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Dirxml(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Group(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void GroupCollapsed(JSContext* aCx, const Sequence<JS::Value>& 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<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void TimeEnd(JSContext* aCx, const nsAString& aLabel); + + MOZ_CAN_RUN_SCRIPT + void TimeStamp(JSContext* aCx, const JS::Handle<JS::Value> aData); + + MOZ_CAN_RUN_SCRIPT + void Profile(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void ProfileEnd(JSContext* aCx, const Sequence<JS::Value>& aData); + + MOZ_CAN_RUN_SCRIPT + void Assert(JSContext* aCx, bool aCondition, + const Sequence<JS::Value>& 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<Console> 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<nsString>& 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<PendingReport> 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<nsIURI> 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<PendingReport> 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<PendingReport> 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<const nsTArray<nsString>&>(report.mStringParams)); + } +} + +void ConsoleReportCollector::StealConsoleReports( + nsTArray<net::ConsoleReportCollected>& aReports) { + aReports.Clear(); + + nsTArray<PendingReport> 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<nsString>& 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<net::ConsoleReportCollected>& 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<nsString>& 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<nsString> mStringParams; + }; + + Mutex mMutex; + + // protected by mMutex + nsTArray<PendingReport> 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<ConsoleUtils> 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<ConsoleUtils> 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<JSObject*> 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<ConsoleEvent> 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<JS::Value> messageValue(cx); + if (!dom::ToJSValue(cx, aMessage, &messageValue)) { + return; + } + + event.mArguments.Construct(); + if (!event.mArguments.Value().AppendElement(messageValue, fallible)) { + return; + } + + nsCOMPtr<nsIConsoleAPIStorage> storage = + do_GetService("@mozilla.org/consoleAPI-storage;1"); + + if (NS_WARN_IF(!storage)) { + return; + } + + JS::Rooted<JS::Value> eventValue(cx); + if (!ToJSValue(cx, event, &eventValue)) { + return; + } + + // This is a legacy property. + JS::Rooted<JSObject*> 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 = + NullPrincipal::CreateWithoutOriginAttributes(); + + JS::Rooted<JSObject*> 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<JSObjectHolder> 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<nsString>& 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 <typename... Params> + 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<nsString> params; + mozilla::dom::StringArrayAppender::Append(params, sizeof...(Params), + std::forward<Params>(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<mozilla::net::ConsoleReportCollected>& 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 @@ +<!DOCTYPE html><html><body></body></html> 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 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=659625 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 659625</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659625">Mozilla Bug 659625</a> +<script type="application/javascript"> + const { Cc, Ci } = SpecialPowers; + let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"]; + let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage); + + let clearAndCheckStorage = () => { + console.clear(); + is(storage.getEvents().length, 1, + "Only one event remains in consoleAPIStorage"); + is(storage.getEvents()[0].level, "clear", + "Remaining event has level 'clear'"); + }; + + storage.clearEvents(); + is(storage.getEvents().length, 0, + "Console is empty when test is starting"); + clearAndCheckStorage(); + + console.log("log"); + console.debug("debug"); + console.warn("warn"); + console.error("error"); + console.exception("exception"); + is(storage.getEvents().length, 6, + "5 new console events have been registered for logging variants"); + clearAndCheckStorage(); + + console.trace(); + is(storage.getEvents().length, 2, + "1 new console event registered for trace"); + clearAndCheckStorage(); + + console.dir({}); + is(storage.getEvents().length, 2, + "1 new console event registered for dir"); + clearAndCheckStorage(); + + console.count("count-label"); + console.count("count-label"); + is(storage.getEvents().length, 3, + "2 new console events registered for 2 count calls"); + clearAndCheckStorage(); + + // For bug 1346326. + console.count("default"); + console.count(); + console.count(undefined); + let events = storage.getEvents(); + // Drop the event from the previous "clear". + events.splice(0, 1); + is(events.length, 3, + "3 new console events registered for 3 'default' count calls"); + for (let i = 0; i < events.length; ++i) { + is(events[i].counter.count, i + 1, "check counter for event " + i); + is(events[i].counter.label, "default", "check label for event " + i); + } + clearAndCheckStorage(); + + console.group("group-label"); + console.log("group-log"); + is(storage.getEvents().length, 3, + "2 new console events registered for group + log"); + clearAndCheckStorage(); + + console.groupCollapsed("group-collapsed"); + console.log("group-collapsed-log"); + is(storage.getEvents().length, 3, + "2 new console events registered for groupCollapsed + log"); + clearAndCheckStorage(); + + console.group("closed-group-label"); + console.log("group-log"); + console.groupEnd(); + is(storage.getEvents().length, 4, + "3 new console events registered for group/groupEnd"); + clearAndCheckStorage(); + + console.time("time-label"); + console.timeEnd(); + is(storage.getEvents().length, 3, + "2 new console events registered for time/timeEnd"); + clearAndCheckStorage(); + + console.timeStamp("timestamp-label"); + is(storage.getEvents().length, 2, + "1 new console event registered for timeStamp"); + clearAndCheckStorage(); + + // Check that console.clear() clears previous clear messages + clearAndCheckStorage(); + +</script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=978522 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 978522 - basic support</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978522">Mozilla Bug 978522</a> +<script type="application/javascript"> + + console.log("%s", { + toString() { + console.log("%s", { + toString() { + ok(true, "Still alive \\o/"); + SimpleTest.finish(); + return "hello world"; + }, + }); + }, + }); + + SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=979109 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 979109</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=979109">Mozilla Bug 979109</a> +<script type="application/javascript"> + + console.warn("%", "a"); + console.warn("%%", "a"); + console.warn("%123", "a"); + console.warn("%123.", "a"); + console.warn("%123.123", "a"); + console.warn("%123.123o", "a"); + console.warn("%123.123s", "a"); + console.warn("%123.123d", "a"); + console.warn("%123.123f", "a"); + console.warn("%123.123z", "a"); + console.warn("%.", "a"); + console.warn("%.123", "a"); + ok(true, "Still alive \\o/"); + +</script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=989665 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 989665</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=989665">Mozilla Bug 989665</a> +<script type="application/javascript"> + +let w = new Worker("data:text/javascript;charset=UTF-8, console.log('%s', {toString: function() { throw 3 }}); "); +ok(true, "This test should not crash."); + +</script> +</body> +</html> 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 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<window title="Test for URL API" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <iframe id="iframe" /> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + ok("console" in window, "Console exists"); + window.console.log(42); + ok("table" in console, "Console has the 'table' method."); + window.console = 42; + is(window.console, 42, "Console is replacable"); + + var frame = document.getElementById("iframe"); + ok(frame, "Frame must exist"); + frame.src="http://mochi.test:8888/tests/dom/console/test/file_empty.html"; + frame.onload = function() { + ok("console" in frame.contentWindow, "Console exists in the iframe"); + frame.contentWindow.console.log(42); + frame.contentWindow.console = 42; + is(frame.contentWindow.console, 42, "Console is replacable in the iframe"); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + ]]></script> +</window> 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 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Test for empty stack in console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +window.setTimeout(console.log.bind(console), 0, "xyz"); + +window.addEventListener("fake", console.log.bind(console, "xyz")); + +window.addEventListener("fake", function() { + ok(true, "Still alive"); + SimpleTest.finish(); +}); + +window.dispatchEvent(new Event("fake")); +</script> +</pre> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test Console binding</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +function consoleListener() { + addConsoleStorageListener(this); +} + +var order = 0; +consoleListener.prototype = { + observe(obj) { + ok(!obj.chromeContext, "Thils is not a chrome context"); + if (order + 1 == parseInt(obj.arguments[0])) { + ok(true, "Message received: " + obj.arguments[0]); + order++; + } + + if (order == 3) { + removeConsoleStorageListener(this); + SimpleTest.finish(); + } + }, +}; + +var cl = new consoleListener(); +SimpleTest.waitForExplicitFinish(); + +[1, 2, 3].forEach(console.log); + + </script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for console.__proto__</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + + isnot(Object.getPrototypeOf(console), Object.prototype, "Foo"); + is(Object.getPrototypeOf(Object.getPrototypeOf(console)), Object.prototype, "Boo"); + + </script> +</body> +</html> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for count/countReset in console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function ConsoleListener() { + addConsoleStorageListener(this); +} + +ConsoleListener.prototype = { + observe(aSubject) { + let obj = aSubject.wrappedJSObject; + if (obj.arguments[0] != "test") { + return; + } + + if (!this._cb) { + ok(false, "Callback not set!"); + return; + } + + if (!this._cb(obj)) { + return; + } + + this._cb = null; + this._resolve(); + }, + + shutdown() { + removeConsoleStorageListener(this); + }, + + waitFor(cb) { + return new Promise(resolve => { + this._cb = SpecialPowers.wrapCallback(cb); + this._resolve = resolve; + }); + }, +}; + +let listener = new ConsoleListener(); + +async function runTest() { + // First count. + let cl = listener.waitFor(obj => { + return ("counter" in obj) && + ("label" in obj.counter) && + obj.counter.label == "test" && + obj.counter.count == 1; + }); + console.count("test"); + await cl; + ok(true, "Console.count == 1 received!"); + + // Second count. + cl = listener.waitFor(obj => { + return ("counter" in obj) && + ("label" in obj.counter) && + obj.counter.label == "test" && + obj.counter.count == 2; + }); + console.count("test"); + await cl; + ok(true, "Console.count == 2 received!"); + + // Counter reset. + cl = listener.waitFor(obj => { + return ("counter" in obj) && + ("label" in obj.counter) && + obj.counter.label == "test" && + obj.counter.count == 0; + }); + console.countReset("test"); + await cl; + ok(true, "Console.countReset == 0 received!"); + + // Counter reset with error. + cl = listener.waitFor(obj => { + return ("counter" in obj) && + ("label" in obj.counter) && + obj.counter.label == "test" && + obj.counter.error == "counterDoesntExist"; + }); + console.countReset("test"); + await cl; + ok(true, "Console.countReset with error received!"); + + // First again! + cl = listener.waitFor(obj => { + return ("counter" in obj) && + ("label" in obj.counter) && + obj.counter.label == "test" && + obj.counter.count == 1; + }); + console.count("test"); + await cl; + ok(true, "Console.count == 1 received!"); +} + +runTest().then(() => { + listener.shutdown(); + SimpleTest.finish(); +}); + + </script> +</body> +</html> 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 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Console + MJS" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="head.js"/> + + <script type="application/javascript"> + <![CDATA[ + +const MJS = "chrome://mochitests/content/chrome/dom/console/tests/console.sys.mjs"; + +let dumpCalled = 0; +function dumpFunction(msg) { + ok(msg.includes("_PREFIX_"), "we have a prefix"); + dump("Message received: " + msg); // Just for debugging + dumpCalled++; +} + +function promiseConsoleListenerCalled() { + return new Promise(resolve => { + let consoleListener = { + count: 0, + + observe: function(aSubject) { + var obj = aSubject.wrappedJSObject; + ok(obj.chromeContext, "MJS is always a chrome context"); + + if (obj.innerID == MJS) { + is(obj.ID, "jsm", "ID and InnerID are correctly set."); + is(obj.arguments[0], "Hello world!", "Message matches"); + is(obj.consoleID, "", "No consoleID for console API"); + is(obj.prefix, "", "prefix is empty by default"); + + // We want to see 2 messages from this innerID, the first is generated + // by console.log, the second one from createInstance().log(); + ++this.count; + } else if (obj.innerID == "CUSTOM INNER") { + is(obj.ID, "jsm", "ID and InnerID are correctly set."); + is(obj.arguments[0], "Hello world!", "Message matches"); + is(obj.consoleID, "wow", "consoleID is set by consoleInstance"); + is(obj.prefix, "_PREFIX_", "prefix is set by consoleInstance"); + // We expect to see 2 messages from this innerID. + ++this.count; + } else if (obj.innerID == "LEVEL") { + // Nothing special... just we don't want to see 'invisible' messages. + is(obj.ID, "jsm", "ID and InnerID are correctly set."); + is(obj.arguments[0], "Hello world!", "Message matches"); + is(obj.prefix, "", "prefix is empty by default"); + // We expect to see 2 messages from this innerID. + ++this.count; + } else if (obj.innerID == "NO PREF") { + // Nothing special... just we don't want to see 'invisible' messages. + is(obj.ID, "jsm", "ID and InnerID are correctly set."); + is(obj.arguments[0], "Hello world!", "Message matches"); + is(obj.prefix, "", "prefix is empty by default"); + // We expect to see 2 messages from this innerID. + ++this.count; + } + + if (this.count == 8) { + is(dumpCalled, 2, "Dump has been called!"); + removeConsoleStorageListener(consoleListener); + resolve(); + } + } + } + addConsoleStorageListener(consoleListener); + }); +} + +async function test() { + SimpleTest.waitForExplicitFinish(); + + let consolePromise = promiseConsoleListenerCalled(); + let { ConsoleTest } = ChromeUtils.importESModule(MJS); + await SpecialPowers.pushPrefEnv({set: [["pref.test.console", "log"]]}) + ConsoleTest.go(dumpFunction); + + await SpecialPowers.pushPrefEnv({ + set: [["pref.test.console.notset", "Log"]], + }); + ConsoleTest.go2(); + + await consolePromise; + SimpleTest.finish(); +} + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + </body> +</window> 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for timeStart/timeLog/timeEnd in console</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision"); +SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false); + +function ConsoleListener() { + addConsoleStorageListener(this); +} + +ConsoleListener.prototype = { + observe(aSubject) { + let obj = aSubject.wrappedJSObject; + if (obj.arguments[0] != "test_bug1463614") { + return; + } + + if (!this._cb || !this._cb(obj)) { + return; + } + + this._cb = null; + this._resolve(); + }, + + shutdown() { + removeConsoleStorageListener(this); + }, + + waitFor(cb) { + return new Promise(resolve => { + this._cb = SpecialPowers.wrapCallback(cb); + this._resolve = resolve; + }); + }, +}; + +let listener = new ConsoleListener(); + +// Timer creation: +async function runTest() { + let cl = listener.waitFor(obj => { + return ("timer" in obj) && + ("name" in obj.timer) && + obj.timer.name == "test_bug1463614"; + }); + + console.time("test_bug1463614"); + await cl; + ok(true, "Console.time received!"); + + // Timer check: + cl = listener.waitFor(obj => { + return ("timer" in obj) && + ("name" in obj.timer) && + obj.timer.name == "test_bug1463614" && + ("duration" in obj.timer) && + obj.timer.duration >= 0 && + obj.arguments[1] == 1 && + obj.arguments[2] == 2 && + obj.arguments[3] == 3 && + obj.arguments[4] == 4; + }); + console.timeLog("test_bug1463614", 1, 2, 3, 4); + await cl; + ok(true, "Console.timeLog received!"); + + // Time deleted: + cl = listener.waitFor(obj => { + return ("timer" in obj) && + ("name" in obj.timer) && + obj.timer.name == "test_bug1463614" && + ("duration" in obj.timer) && + obj.timer.duration >= 0; + }); + console.timeEnd("test_bug1463614"); + await cl; + ok(true, "Console.timeEnd received!"); + + // Here an error: + cl = listener.waitFor(obj => { + return ("timer" in obj) && + ("name" in obj.timer) && + obj.timer.name == "test_bug1463614" && + ("error" in obj.timer); + }); + console.timeLog("test_bug1463614"); + await cl; + ok(true, "Console.time with error received!"); +} + +runTest().then(() => { + listener.shutdown(); + + SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue); + SimpleTest.finish(); +}); + + </script> +</body> +</html> 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] |