/* -*- 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/Mutex.h" #include "mozilla/Preferences.h" #include "mozilla/StaticPrefs_devtools.h" #include "mozilla/StaticPrefs_dom.h" #include "nsCycleCollectionParticipant.h" #include "nsDOMNavigationTiming.h" #include "nsGlobalWindowInner.h" #include "nsJSUtils.h" #include "nsNetUtil.h" #include "xpcpublic.h" #include "nsContentUtils.h" #include "nsDocShell.h" #include "nsProxyRelease.h" #include "nsReadableUtils.h" #include "nsIConsoleAPIStorage.h" #include "nsIException.h" // for nsIStackFrame #include "nsIInterfaceRequestorUtils.h" #include "nsILoadContext.h" #include "nsISensitiveInfoHiddenURI.h" #include "nsISupportsPrimitives.h" #include "nsIWebNavigation.h" #include "nsIXPConnect.h" // The maximum allowed number of concurrent timers per page. #define MAX_PAGE_TIMERS 10000 // The maximum allowed number of concurrent counters per page. #define MAX_PAGE_COUNTERS 10000 // The maximum stacktrace depth when populating the stacktrace array used for // console.trace(). #define DEFAULT_MAX_STACKTRACE_DEPTH 200 // This tags are used in the Structured Clone Algorithm to move js values from // worker thread to main thread #define CONSOLE_TAG_BLOB JS_SCTAG_USER_MIN // This value is taken from ConsoleAPIStorage.js #define STORAGE_MAX_EVENTS 1000 using namespace mozilla::dom::exceptions; namespace mozilla::dom { struct ConsoleStructuredCloneData { nsCOMPtr mGlobal; nsTArray> mBlobs; }; static void ComposeAndStoreGroupName(JSContext* aCx, const Sequence& aData, nsAString& aName, nsTArray* aGroupStack); static bool UnstoreGroupName(nsAString& aName, nsTArray* aGroupStack); static bool ProcessArguments(JSContext* aCx, const Sequence& aData, Sequence& aSequence, Sequence& aStyles); static JS::Value CreateCounterOrResetCounterValue(JSContext* aCx, const nsAString& aCountLabel, uint32_t aCountValue); /** * Console API in workers uses the Structured Clone Algorithm to move any value * from the worker thread to the main-thread. Some object cannot be moved and, * in these cases, we convert them to strings. * It's not the best, but at least we are able to show something. */ class ConsoleCallData final { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ConsoleCallData) ConsoleCallData(Console::MethodName aName, const nsAString& aString, Console* aConsole) : mMutex("ConsoleCallData"), 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_REQUIRES(mMutex) { MOZ_ASSERT(mIDType == eUnknown); mOuterIDNumber = aOuterID; mInnerIDNumber = aInnerID; mIDType = eNumber; } void SetIDs(const nsAString& aOuterID, const nsAString& aInnerID) MOZ_REQUIRES(mMutex) { MOZ_ASSERT(mIDType == eUnknown); mOuterIDString = aOuterID; mInnerIDString = aInnerID; mIDType = eString; } void SetOriginAttributes(const OriginAttributes& aOriginAttributes) MOZ_REQUIRES(mMutex) { mOriginAttributes = aOriginAttributes; } void SetAddonId(nsIPrincipal* aPrincipal) MOZ_REQUIRES(mMutex) { nsAutoString addonId; aPrincipal->GetAddonId(addonId); mAddonId = addonId; } void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(ConsoleCallData); } Mutex mMutex; const nsString mConsoleID MOZ_GUARDED_BY(mMutex); const nsString mPrefix MOZ_GUARDED_BY(mMutex); const Console::MethodName mMethodName MOZ_GUARDED_BY(mMutex); int64_t mMicroSecondTimeStamp MOZ_GUARDED_BY(mMutex); // 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 MOZ_GUARDED_BY(mMutex); nsString mStartTimerLabel MOZ_GUARDED_BY(mMutex); Console::TimerStatus mStartTimerStatus MOZ_GUARDED_BY(mMutex); // 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 MOZ_GUARDED_BY(mMutex); nsString mLogTimerLabel MOZ_GUARDED_BY(mMutex); Console::TimerStatus mLogTimerStatus MOZ_GUARDED_BY(mMutex); // 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 MOZ_GUARDED_BY(mMutex); uint32_t mCountValue MOZ_GUARDED_BY(mMutex); // 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 MOZ_GUARDED_BY(mMutex); uint64_t mOuterIDNumber MOZ_GUARDED_BY(mMutex); nsString mOuterIDString MOZ_GUARDED_BY(mMutex); uint64_t mInnerIDNumber MOZ_GUARDED_BY(mMutex); nsString mInnerIDString MOZ_GUARDED_BY(mMutex); OriginAttributes mOriginAttributes MOZ_GUARDED_BY(mMutex); nsString mAddonId MOZ_GUARDED_BY(mMutex); const nsString mMethodString MOZ_GUARDED_BY(mMutex); // Stack management is complicated, because we want to do it as // lazily as possible. Therefore, we have the following behavior: // 1) mTopStackFrame is initialized whenever we have any JS on the stack // 2) mReifiedStack is initialized if we're created in a worker. // 3) mStack is set (possibly to null if there is no JS on the stack) if // we're created on main thread. Maybe mTopStackFrame MOZ_GUARDED_BY(mMutex); Maybe> mReifiedStack MOZ_GUARDED_BY(mMutex); nsCOMPtr mStack MOZ_GUARDED_BY(mMutex); private: ~ConsoleCallData() = default; NS_DECL_OWNINGTHREAD; }; // MainThreadConsoleData instances are created on the Console thread and // referenced from both main and Console threads in order to provide the same // object for any ConsoleRunnables relating to the same Console. A Console // owns a MainThreadConsoleData; MainThreadConsoleData does not keep its // Console alive. class MainThreadConsoleData final { NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MainThreadConsoleData); JSObject* GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal); // This method must receive aCx and aArguments in the same JS::Compartment. void ProcessCallData(JSContext* aCx, ConsoleCallData* aData, const Sequence& aArguments); private: ~MainThreadConsoleData() { NS_ReleaseOnMainThread("MainThreadConsoleData::mStorage", mStorage.forget()); NS_ReleaseOnMainThread("MainThreadConsoleData::mSandbox", mSandbox.forget()); } // All members, except for mRefCnt, are accessed only on the main thread, // except in MainThreadConsoleData destruction, at which point there are no // other references. nsCOMPtr mStorage; RefPtr mSandbox; nsTArray mGroupStack; }; // This base class must be extended for Worker and for Worklet. class ConsoleRunnable : public StructuredCloneHolderBase { public: ~ConsoleRunnable() override { MOZ_ASSERT(!mClonedData.mGlobal, "mClonedData.mGlobal is set and cleared in a main thread scope"); // Clear the StructuredCloneHolderBase class. Clear(); } protected: JSObject* CustomReadHandler(JSContext* aCx, JSStructuredCloneReader* aReader, const JS::CloneDataPolicy& aCloneDataPolicy, uint32_t aTag, uint32_t aIndex) override { AssertIsOnMainThread(); if (aTag == CONSOLE_TAG_BLOB) { MOZ_ASSERT(mClonedData.mBlobs.Length() > aIndex); JS::Rooted val(aCx); { nsCOMPtr global = mClonedData.mGlobal; RefPtr blob = Blob::Create(global, mClonedData.mBlobs.ElementAt(aIndex)); if (!ToJSValue(aCx, blob, &val)) { return nullptr; } } return &val.toObject(); } MOZ_CRASH("No other tags are supported."); return nullptr; } bool CustomWriteHandler(JSContext* aCx, JSStructuredCloneWriter* aWriter, JS::Handle aObj, bool* aSameProcessScopeRequired) override { RefPtr blob; if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, aObj, blob))) { if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, CONSOLE_TAG_BLOB, mClonedData.mBlobs.Length()))) { return false; } mClonedData.mBlobs.AppendElement(blob->Impl()); return true; } if (!JS_ObjectNotWritten(aWriter, aObj)) { return false; } JS::Rooted value(aCx, JS::ObjectOrNullValue(aObj)); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (NS_WARN_IF(!jsString)) { return false; } if (NS_WARN_IF(!JS_WriteString(aWriter, jsString))) { return false; } return true; } // Helper method for CallData void ProcessCallData(JSContext* aCx, MainThreadConsoleData* aConsoleData, ConsoleCallData* aCallData) { AssertIsOnMainThread(); ConsoleCommon::ClearException ce(aCx); // This is the same policy as when writing from the other side, in // WriteData. JS::CloneDataPolicy cloneDataPolicy; cloneDataPolicy.allowIntraClusterClonableSharedObjects(); cloneDataPolicy.allowSharedMemoryObjects(); JS::Rooted argumentsValue(aCx); if (!Read(aCx, &argumentsValue, cloneDataPolicy)) { return; } MOZ_ASSERT(argumentsValue.isObject()); JS::Rooted argumentsObj(aCx, &argumentsValue.toObject()); uint32_t length; if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { return; } Sequence values; SequenceRooter arguments(aCx, &values); for (uint32_t i = 0; i < length; ++i) { JS::Rooted value(aCx); if (!JS_GetElement(aCx, argumentsObj, i, &value)) { return; } if (!values.AppendElement(value, fallible)) { return; } } MOZ_ASSERT(values.Length() == length); aConsoleData->ProcessCallData(aCx, aCallData, values); } // Generic bool WriteArguments(JSContext* aCx, const Sequence& aArguments) { ConsoleCommon::ClearException ce(aCx); JS::Rooted arguments( aCx, JS::NewArrayObject(aCx, aArguments.Length())); if (NS_WARN_IF(!arguments)) { return false; } JS::Rooted arg(aCx); for (uint32_t i = 0; i < aArguments.Length(); ++i) { arg = aArguments[i]; if (NS_WARN_IF( !JS_DefineElement(aCx, arguments, i, arg, JSPROP_ENUMERATE))) { return false; } } JS::Rooted value(aCx, JS::ObjectValue(*arguments)); return WriteData(aCx, value); } // Helper method for Profile calls void ProcessProfileData(JSContext* aCx, Console::MethodName aMethodName, const nsAString& aAction) { AssertIsOnMainThread(); ConsoleCommon::ClearException ce(aCx); JS::Rooted argumentsValue(aCx); bool ok = Read(aCx, &argumentsValue); mClonedData.mGlobal = nullptr; if (!ok) { return; } MOZ_ASSERT(argumentsValue.isObject()); JS::Rooted argumentsObj(aCx, &argumentsValue.toObject()); if (NS_WARN_IF(!argumentsObj)) { return; } uint32_t length; if (!JS::GetArrayLength(aCx, argumentsObj, &length)) { return; } Sequence arguments; for (uint32_t i = 0; i < length; ++i) { JS::Rooted value(aCx); if (!JS_GetElement(aCx, argumentsObj, i, &value)) { return; } if (!arguments.AppendElement(value, fallible)) { return; } } Console::ProfileMethodMainthread(aCx, aAction, arguments); } bool WriteData(JSContext* aCx, JS::Handle aValue) { // We use structuredClone to send the JSValue to the main-thread, in order // to store it into the Console API Service. The consumer will be the // console panel in the devtools and, because of this, we want to allow the // cloning of sharedArrayBuffers and WASM modules. JS::CloneDataPolicy cloneDataPolicy; cloneDataPolicy.allowIntraClusterClonableSharedObjects(); cloneDataPolicy.allowSharedMemoryObjects(); if (NS_WARN_IF( !Write(aCx, aValue, JS::UndefinedHandleValue, cloneDataPolicy))) { // Ignore the message. return false; } return true; } ConsoleStructuredCloneData mClonedData; }; class ConsoleWorkletRunnable : public Runnable, public ConsoleRunnable { protected: explicit ConsoleWorkletRunnable(Console* aConsole) : Runnable("dom::console::ConsoleWorkletRunnable"), mConsoleData(aConsole->GetOrCreateMainThreadData()) { WorkletThread::AssertIsOnWorkletThread(); nsCOMPtr global = do_QueryInterface(aConsole->mGlobal); MOZ_ASSERT(global); mWorkletImpl = global->Impl(); MOZ_ASSERT(mWorkletImpl); } ~ConsoleWorkletRunnable() override = default; protected: RefPtr mConsoleData; RefPtr mWorkletImpl; }; // This runnable appends a CallData object into the Console queue running on // the main-thread. class ConsoleCallDataWorkletRunnable final : public ConsoleWorkletRunnable { public: static already_AddRefed Create( JSContext* aCx, Console* aConsole, ConsoleCallData* aConsoleData, const Sequence& aArguments) { WorkletThread::AssertIsOnWorkletThread(); RefPtr runnable = new ConsoleCallDataWorkletRunnable(aConsole, aConsoleData); if (!runnable->WriteArguments(aCx, aArguments)) { return nullptr; } return runnable.forget(); } private: ConsoleCallDataWorkletRunnable(Console* aConsole, ConsoleCallData* aCallData) : ConsoleWorkletRunnable(aConsole), mCallData(aCallData) { WorkletThread::AssertIsOnWorkletThread(); MOZ_ASSERT(aCallData); aCallData->AssertIsOnOwningThread(); const WorkletLoadInfo& loadInfo = mWorkletImpl->LoadInfo(); mCallData->SetIDs(loadInfo.OuterWindowID(), loadInfo.InnerWindowID()); } ~ConsoleCallDataWorkletRunnable() override = default; NS_IMETHOD Run() override { AssertIsOnMainThread(); AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); { MutexAutoLock lock(mCallData->mMutex); JSObject* sandbox = mConsoleData->GetOrCreateSandbox(cx, mWorkletImpl->Principal()); JS::Rooted global(cx, sandbox); if (NS_WARN_IF(!global)) { return NS_ERROR_FAILURE; } // The CreateSandbox call returns a proxy to the actual sandbox object. We // don't need a proxy here. global = js::UncheckedUnwrap(global); JSAutoRealm ar(cx, global); // We don't need to set a parent object in mCallData bacause there are not // DOM objects exposed to worklet. ProcessCallData(cx, mConsoleData, mCallData); } return NS_OK; } RefPtr mCallData; }; class ConsoleWorkerRunnable : public WorkerProxyToMainThreadRunnable, public ConsoleRunnable { public: explicit ConsoleWorkerRunnable(Console* aConsole) : mConsoleData(aConsole->GetOrCreateMainThreadData()) {} ~ConsoleWorkerRunnable() override = default; bool Dispatch(JSContext* aCx, const Sequence& aArguments) { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); if (NS_WARN_IF(!WriteArguments(aCx, aArguments))) { RunBackOnWorkerThreadForCleanup(workerPrivate); return false; } if (NS_WARN_IF(!WorkerProxyToMainThreadRunnable::Dispatch(workerPrivate))) { // RunBackOnWorkerThreadForCleanup() will be called by // WorkerProxyToMainThreadRunnable::Dispatch(). return false; } return true; } protected: void RunOnMainThread(WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); AssertIsOnMainThread(); // Walk up to our containing page WorkerPrivate* wp = aWorkerPrivate->GetTopLevelWorker(); nsCOMPtr window = wp->GetWindow(); if (!window) { RunWindowless(aWorkerPrivate); } else { RunWithWindow(aWorkerPrivate, window); } } void RunWithWindow(WorkerPrivate* aWorkerPrivate, nsPIDOMWindowInner* aWindow) { MOZ_ASSERT(aWorkerPrivate); AssertIsOnMainThread(); AutoJSAPI jsapi; MOZ_ASSERT(aWindow); RefPtr win = nsGlobalWindowInner::Cast(aWindow); if (NS_WARN_IF(!jsapi.Init(win))) { return; } nsCOMPtr outerWindow = aWindow->GetOuterWindow(); if (NS_WARN_IF(!outerWindow)) { return; } RunConsole(jsapi.cx(), aWindow->AsGlobal(), aWorkerPrivate, outerWindow, aWindow); } void RunWindowless(WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); AssertIsOnMainThread(); WorkerPrivate* wp = aWorkerPrivate->GetTopLevelWorker(); MOZ_ASSERT(!wp->GetWindow()); AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); JS::Rooted global( cx, mConsoleData->GetOrCreateSandbox(cx, wp->GetPrincipal())); if (NS_WARN_IF(!global)) { return; } // The GetOrCreateSandbox call returns a proxy to the actual sandbox object. // We don't need a proxy here. global = js::UncheckedUnwrap(global); JSAutoRealm ar(cx, global); nsCOMPtr globalObject = xpc::NativeGlobal(global); if (NS_WARN_IF(!globalObject)) { return; } RunConsole(cx, globalObject, aWorkerPrivate, nullptr, nullptr); } void RunBackOnWorkerThreadForCleanup(WorkerPrivate* aWorkerPrivate) override { MOZ_ASSERT(aWorkerPrivate); aWorkerPrivate->AssertIsOnWorkerThread(); } // This method is called in the main-thread. virtual void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, WorkerPrivate* aWorkerPrivate, nsPIDOMWindowOuter* aOuterWindow, nsPIDOMWindowInner* aInnerWindow) = 0; bool ForMessaging() const override { return true; } RefPtr mConsoleData; }; // This runnable appends a CallData object into the Console queue running on // the main-thread. class ConsoleCallDataWorkerRunnable final : public ConsoleWorkerRunnable { public: ConsoleCallDataWorkerRunnable(Console* aConsole, ConsoleCallData* aCallData) : ConsoleWorkerRunnable(aConsole), mCallData(aCallData) { MOZ_ASSERT(aCallData); mCallData->AssertIsOnOwningThread(); } private: ~ConsoleCallDataWorkerRunnable() override = default; void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, WorkerPrivate* aWorkerPrivate, nsPIDOMWindowOuter* aOuterWindow, nsPIDOMWindowInner* aInnerWindow) override { MOZ_ASSERT(aGlobal); MOZ_ASSERT(aWorkerPrivate); AssertIsOnMainThread(); // The windows have to run in parallel. MOZ_ASSERT(!!aOuterWindow == !!aInnerWindow); { MutexAutoLock lock(mCallData->mMutex); if (aOuterWindow) { mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID()); } else { ConsoleStackEntry frame; if (mCallData->mTopStackFrame) { frame = *mCallData->mTopStackFrame; } nsCString 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 id = aWorkerPrivate->ServiceWorkerScope(); } else { innerID = u"Worker"_ns; } mCallData->SetIDs(NS_ConvertUTF8toUTF16(id), innerID); } mClonedData.mGlobal = aGlobal; ProcessCallData(aCx, mConsoleData, mCallData); mClonedData.mGlobal = nullptr; } } RefPtr mCallData; }; // This runnable calls ProfileMethod() on the console on the main-thread. class ConsoleProfileWorkletRunnable final : public ConsoleWorkletRunnable { public: static already_AddRefed Create( JSContext* aCx, Console* aConsole, Console::MethodName aName, const nsAString& aAction, const Sequence& aArguments) { WorkletThread::AssertIsOnWorkletThread(); RefPtr runnable = new ConsoleProfileWorkletRunnable(aConsole, aName, aAction); if (!runnable->WriteArguments(aCx, aArguments)) { return nullptr; } return runnable.forget(); } private: ConsoleProfileWorkletRunnable(Console* aConsole, Console::MethodName aName, const nsAString& aAction) : ConsoleWorkletRunnable(aConsole), mName(aName), mAction(aAction) { MOZ_ASSERT(aConsole); } NS_IMETHOD Run() override { AssertIsOnMainThread(); AutoJSAPI jsapi; jsapi.Init(); JSContext* cx = jsapi.cx(); JSObject* sandbox = mConsoleData->GetOrCreateSandbox(cx, mWorkletImpl->Principal()); JS::Rooted global(cx, sandbox); if (NS_WARN_IF(!global)) { return NS_ERROR_FAILURE; } // The CreateSandbox call returns a proxy to the actual sandbox object. We // don't need a proxy here. global = js::UncheckedUnwrap(global); JSAutoRealm ar(cx, global); // We don't need to set a parent object in mCallData bacause there are not // DOM objects exposed to worklet. ProcessProfileData(cx, mName, mAction); return NS_OK; } Console::MethodName mName; nsString mAction; }; // This runnable calls ProfileMethod() on the console on the main-thread. class ConsoleProfileWorkerRunnable final : public ConsoleWorkerRunnable { public: ConsoleProfileWorkerRunnable(Console* aConsole, Console::MethodName aName, const nsAString& aAction) : ConsoleWorkerRunnable(aConsole), mName(aName), mAction(aAction) { MOZ_ASSERT(aConsole); } private: void RunConsole(JSContext* aCx, nsIGlobalObject* aGlobal, WorkerPrivate* aWorkerPrivate, nsPIDOMWindowOuter* aOuterWindow, nsPIDOMWindowInner* aInnerWindow) override { AssertIsOnMainThread(); MOZ_ASSERT(aGlobal); mClonedData.mGlobal = aGlobal; ProcessProfileData(aCx, mName, mAction); mClonedData.mGlobal = nullptr; } Console::MethodName mName; nsString mAction; }; NS_IMPL_CYCLE_COLLECTION_CLASS(Console) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console) NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsoleEventNotifier) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDumpFunction) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE tmp->Shutdown(); tmp->mArgumentStorage.clearAndFree(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsoleEventNotifier) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDumpFunction) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console) for (uint32_t i = 0; i < tmp->mArgumentStorage.length(); ++i) { tmp->mArgumentStorage[i].Trace(aCallbacks, aClosure); } NS_IMPL_CYCLE_COLLECTION_TRACE_END NS_IMPL_CYCLE_COLLECTING_ADDREF(Console) NS_IMPL_CYCLE_COLLECTING_RELEASE(Console) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Console) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END /* static */ already_AddRefed Console::Create(JSContext* aCx, nsPIDOMWindowInner* aWindow, ErrorResult& aRv) { MOZ_ASSERT_IF(NS_IsMainThread(), aWindow); uint64_t outerWindowID = 0; uint64_t innerWindowID = 0; if (aWindow) { innerWindowID = aWindow->WindowID(); // Without outerwindow any console message coming from this object will not // shown in the devtools webconsole. But this should be fine because // probably we are shutting down, or the window is CCed/GCed. nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow(); if (outerWindow) { outerWindowID = outerWindow->WindowID(); } } RefPtr console = new Console(aCx, nsGlobalWindowInner::Cast(aWindow), outerWindowID, innerWindowID); console->Initialize(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return console.forget(); } /* static */ already_AddRefed Console::CreateForWorklet(JSContext* aCx, nsIGlobalObject* aGlobal, uint64_t aOuterWindowID, uint64_t aInnerWindowID, ErrorResult& aRv) { WorkletThread::AssertIsOnWorkletThread(); RefPtr console = new Console(aCx, aGlobal, aOuterWindowID, aInnerWindowID); console->Initialize(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return console.forget(); } Console::Console(JSContext* aCx, nsIGlobalObject* aGlobal, uint64_t aOuterWindowID, uint64_t aInnerWindowID, const nsAString& aPrefix) : mGlobal(aGlobal), mOuterID(aOuterWindowID), mInnerID(aInnerWindowID), mDumpToStdout(false), mLogModule(nullptr), mPrefix(aPrefix), mChromeInstance(false), mCurrentLogLevel(WebIDLLogLevelToInteger(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(); } // By default, the console uses "console" MOZ_LOG module name, // but ConsoleInstance may pass a custom prefix which we will use a module // name. mLogModule = mPrefix.IsEmpty() ? LogModule::Get("console") : LogModule::Get(NS_ConvertUTF16toUTF8(mPrefix).get()); mozilla::HoldJSObjects(this); } Console::~Console() { AssertIsOnOwningThread(); Shutdown(); mozilla::DropJSObjects(this); } void Console::Initialize(ErrorResult& aRv) { AssertIsOnOwningThread(); MOZ_ASSERT(mStatus == eUnknown); if (NS_IsMainThread()) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (NS_WARN_IF(!obs)) { aRv.Throw(NS_ERROR_FAILURE); return; } if (mInnerID) { aRv = obs->AddObserver(this, "inner-window-destroyed", true); if (NS_WARN_IF(aRv.Failed())) { return; } } aRv = obs->AddObserver(this, "memory-pressure", true); if (NS_WARN_IF(aRv.Failed())) { return; } } mStatus = eInitialized; } void Console::Shutdown() { AssertIsOnOwningThread(); if (mStatus == eUnknown || mStatus == eShuttingDown) { return; } if (NS_IsMainThread()) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->RemoveObserver(this, "inner-window-destroyed"); obs->RemoveObserver(this, "memory-pressure"); } } mTimerRegistry.Clear(); mCounterRegistry.Clear(); ClearStorage(); mCallDataStorage.Clear(); mStatus = eShuttingDown; } NS_IMETHODIMP Console::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { AssertIsOnMainThread(); if (!strcmp(aTopic, "inner-window-destroyed")) { nsCOMPtr wrapper = do_QueryInterface(aSubject); NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); uint64_t innerID; nsresult rv = wrapper->GetData(&innerID); NS_ENSURE_SUCCESS(rv, rv); if (innerID == mInnerID) { Shutdown(); } return NS_OK; } if (!strcmp(aTopic, "memory-pressure")) { ClearStorage(); return NS_OK; } return NS_OK; } void Console::ClearStorage() { mCallDataStorage.Clear(); mArgumentStorage.clearAndFree(); } #define METHOD(name, string) \ /* static */ void Console::name(const GlobalObject& aGlobal, \ const Sequence& aData) { \ Method(aGlobal, Method##name, nsLiteralString(string), aData); \ } METHOD(Log, u"log") METHOD(Info, u"info") METHOD(Warn, u"warn") METHOD(Error, u"error") METHOD(Exception, u"exception") METHOD(Debug, u"debug") METHOD(Table, u"table") METHOD(Trace, u"trace") // Displays an interactive listing of all the properties of an object. METHOD(Dir, u"dir"); METHOD(Dirxml, u"dirxml"); METHOD(Group, u"group") METHOD(GroupCollapsed, u"groupCollapsed") #undef METHOD /* static */ void Console::Clear(const GlobalObject& aGlobal) { const Sequence data; Method(aGlobal, MethodClear, u"clear"_ns, data); } /* static */ void Console::GroupEnd(const GlobalObject& aGlobal) { const Sequence data; Method(aGlobal, MethodGroupEnd, u"groupEnd"_ns, data); } /* static */ void Console::Time(const GlobalObject& aGlobal, const nsAString& aLabel) { StringMethod(aGlobal, aLabel, Sequence(), MethodTime, u"time"_ns); } /* static */ void Console::TimeEnd(const GlobalObject& aGlobal, const nsAString& aLabel) { StringMethod(aGlobal, aLabel, Sequence(), MethodTimeEnd, u"timeEnd"_ns); } /* static */ void Console::TimeLog(const GlobalObject& aGlobal, const nsAString& aLabel, const Sequence& aData) { StringMethod(aGlobal, aLabel, aData, MethodTimeLog, u"timeLog"_ns); } /* static */ void Console::StringMethod(const GlobalObject& aGlobal, const nsAString& aLabel, const Sequence& aData, MethodName aMethodName, const nsAString& aMethodString) { RefPtr console = GetConsole(aGlobal); if (!console) { return; } console->StringMethodInternal(aGlobal.Context(), aLabel, aData, aMethodName, aMethodString); } void Console::StringMethodInternal(JSContext* aCx, const nsAString& aLabel, const Sequence& aData, MethodName aMethodName, const nsAString& aMethodString) { ConsoleCommon::ClearException ce(aCx); Sequence data; SequenceRooter rooter(aCx, &data); JS::Rooted value(aCx); if (!dom::ToJSValue(aCx, aLabel, &value)) { return; } if (!data.AppendElement(value, fallible)) { return; } for (uint32_t i = 0; i < aData.Length(); ++i) { if (!data.AppendElement(aData[i], fallible)) { return; } } MethodInternal(aCx, aMethodName, aMethodString, data); } /* static */ void Console::TimeStamp(const GlobalObject& aGlobal, const JS::Handle aData) { JSContext* cx = aGlobal.Context(); ConsoleCommon::ClearException ce(cx); Sequence data; SequenceRooter rooter(cx, &data); if (aData.isString() && !data.AppendElement(aData, fallible)) { return; } Method(aGlobal, MethodTimeStamp, u"timeStamp"_ns, data); } /* static */ void Console::Profile(const GlobalObject& aGlobal, const Sequence& aData) { ProfileMethod(aGlobal, MethodProfile, u"profile"_ns, aData); } /* static */ void Console::ProfileEnd(const GlobalObject& aGlobal, const Sequence& aData) { ProfileMethod(aGlobal, MethodProfileEnd, u"profileEnd"_ns, aData); } /* static */ void Console::ProfileMethod(const GlobalObject& aGlobal, MethodName aName, const nsAString& aAction, const Sequence& aData) { RefPtr console = GetConsole(aGlobal); if (!console) { return; } JSContext* cx = aGlobal.Context(); console->ProfileMethodInternal(cx, aName, aAction, aData); } void Console::ProfileMethodInternal(JSContext* aCx, MethodName aMethodName, const nsAString& aAction, const Sequence& aData) { if (!ShouldProceed(aMethodName)) { return; } MaybeExecuteDumpFunction(aCx, aMethodName, aAction, aData, nullptr, DOMHighResTimeStamp(0.0)); if (WorkletThread::IsOnWorkletThread()) { RefPtr runnable = ConsoleProfileWorkletRunnable::Create(aCx, this, aMethodName, aAction, aData); if (!runnable) { return; } NS_DispatchToMainThread(runnable.forget()); return; } if (!NS_IsMainThread()) { // Here we are in a worker thread. RefPtr runnable = new ConsoleProfileWorkerRunnable(this, aMethodName, aAction); runnable->Dispatch(aCx, aData); return; } ProfileMethodMainthread(aCx, aAction, aData); } // static void Console::ProfileMethodMainthread(JSContext* aCx, const nsAString& aAction, const Sequence& aData) { MOZ_ASSERT(NS_IsMainThread()); ConsoleCommon::ClearException ce(aCx); RootedDictionary event(aCx); event.mAction = aAction; event.mChromeContext = nsContentUtils::ThreadsafeIsSystemCaller(aCx); event.mArguments.Construct(); Sequence& sequence = event.mArguments.Value(); for (uint32_t i = 0; i < aData.Length(); ++i) { if (!sequence.AppendElement(aData[i], fallible)) { return; } } JS::Rooted eventValue(aCx); if (!ToJSValue(aCx, event, &eventValue)) { return; } JS::Rooted eventObj(aCx, &eventValue.toObject()); MOZ_ASSERT(eventObj); if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue, JSPROP_ENUMERATE)) { return; } nsIXPConnect* xpc = nsContentUtils::XPConnect(); nsCOMPtr wrapper; const nsIID& iid = NS_GET_IID(nsISupports); if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) { return; } nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(wrapper, "console-api-profiler", nullptr); } } /* static */ void Console::Assert(const GlobalObject& aGlobal, bool aCondition, const Sequence& aData) { if (!aCondition) { Method(aGlobal, MethodAssert, u"assert"_ns, aData); } } /* static */ void Console::Count(const GlobalObject& aGlobal, const nsAString& aLabel) { StringMethod(aGlobal, aLabel, Sequence(), MethodCount, u"count"_ns); } /* static */ void Console::CountReset(const GlobalObject& aGlobal, const nsAString& aLabel) { StringMethod(aGlobal, aLabel, Sequence(), MethodCountReset, u"countReset"_ns); } namespace { void StackFrameToStackEntry(JSContext* aCx, nsIStackFrame* aStackFrame, ConsoleStackEntry& aStackEntry) { MOZ_ASSERT(aStackFrame); aStackFrame->GetFilename(aCx, aStackEntry.mFilename); aStackEntry.mSourceId = aStackFrame->GetSourceId(aCx); aStackEntry.mLineNumber = aStackFrame->GetLineNumber(aCx); aStackEntry.mColumnNumber = aStackFrame->GetColumnNumber(aCx); aStackFrame->GetName(aCx, aStackEntry.mFunctionName); nsString cause; aStackFrame->GetAsyncCause(aCx, cause); if (!cause.IsEmpty()) { aStackEntry.mAsyncCause.Construct(cause); } } void ReifyStack(JSContext* aCx, nsIStackFrame* aStack, nsTArray& aRefiedStack) { nsCOMPtr stack(aStack); while (stack) { ConsoleStackEntry& data = *aRefiedStack.AppendElement(); StackFrameToStackEntry(aCx, stack, data); nsCOMPtr caller = stack->GetCaller(aCx); if (!caller) { caller = stack->GetAsyncCaller(aCx); } stack.swap(caller); } } } // anonymous namespace // Queue a call to a console method. See the CALL_DELAY constant. /* static */ void Console::Method(const GlobalObject& aGlobal, MethodName aMethodName, const nsAString& aMethodString, const Sequence& aData) { RefPtr console = GetConsole(aGlobal); if (!console) { return; } console->MethodInternal(aGlobal.Context(), aMethodName, aMethodString, aData); } void Console::MethodInternal(JSContext* aCx, MethodName aMethodName, const nsAString& aMethodString, const Sequence& aData) { if (!ShouldProceed(aMethodName)) { return; } AssertIsOnOwningThread(); ConsoleCommon::ClearException ce(aCx); RefPtr callData = new ConsoleCallData(aMethodName, aMethodString, this); MutexAutoLock lock(callData->mMutex); if (!StoreCallData(aCx, callData, aData)) { return; } OriginAttributes oa; if (NS_IsMainThread()) { if (mGlobal) { // Save the principal's OriginAttributes in the console event data // so that we will be able to filter messages by origin attributes. nsCOMPtr sop = do_QueryInterface(mGlobal); if (NS_WARN_IF(!sop)) { return; } nsCOMPtr principal = sop->GetPrincipal(); if (NS_WARN_IF(!principal)) { return; } oa = principal->OriginAttributesRef(); callData->SetAddonId(principal); #ifdef DEBUG if (!principal->IsSystemPrincipal()) { nsCOMPtr webNav = do_GetInterface(mGlobal); if (webNav) { nsCOMPtr loadContext = do_QueryInterface(webNav); MOZ_ASSERT(loadContext); bool pb; if (NS_SUCCEEDED(loadContext->GetUsePrivateBrowsing(&pb))) { MOZ_ASSERT(pb == oa.IsPrivateBrowsing()); } } } #endif } } else if (WorkletThread::IsOnWorkletThread()) { nsCOMPtr global = do_QueryInterface(mGlobal); MOZ_ASSERT(global); oa = global->Impl()->OriginAttributesRef(); } else { WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); oa = workerPrivate->GetOriginAttributes(); } callData->SetOriginAttributes(oa); JS::StackCapture captureMode = ShouldIncludeStackTrace(aMethodName) ? JS::StackCapture(JS::MaxFrames(DEFAULT_MAX_STACKTRACE_DEPTH)) : JS::StackCapture(JS::FirstSubsumedFrame(aCx)); nsCOMPtr stack = CreateStack(aCx, std::move(captureMode)); if (stack) { callData->mTopStackFrame.emplace(); StackFrameToStackEntry(aCx, stack, *callData->mTopStackFrame); } if (NS_IsMainThread()) { callData->mStack = stack; } else { // nsIStackFrame is not threadsafe, so we need to snapshot it now, // before we post our runnable to the main thread. callData->mReifiedStack.emplace(); ReifyStack(aCx, stack, *callData->mReifiedStack); } DOMHighResTimeStamp monotonicTimer = 0.0; // 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. // // Only log the stack trace for console.trace() and console.assert() if (aMethodName == MethodTrace || aMethodName == MethodAssert) { MaybeExecuteDumpFunction(aCx, aMethodName, aMethodString, aData, stack, monotonicTimer); } else { MaybeExecuteDumpFunction(aCx, aMethodName, aMethodString, aData, nullptr, monotonicTimer); } if (NS_IsMainThread()) { if (mInnerID) { callData->SetIDs(mOuterID, mInnerID); } else if (!mPassedInnerID.IsEmpty()) { callData->SetIDs(u"jsm"_ns, mPassedInnerID); } else { nsAutoCString filename; if (callData->mTopStackFrame.isSome()) { filename = callData->mTopStackFrame->mFilename; } callData->SetIDs(u"jsm"_ns, NS_ConvertUTF8toUTF16(filename)); } GetOrCreateMainThreadData()->ProcessCallData(aCx, callData, aData); // Just because we don't want to expose // retrieveConsoleEvents/setConsoleEventHandler to main-thread, we can // cleanup the mCallDataStorage: UnstoreCallData(callData); return; } if (WorkletThread::IsOnWorkletThread()) { RefPtr runnable = ConsoleCallDataWorkletRunnable::Create(aCx, this, callData, aData); if (!runnable) { return; } NS_DispatchToMainThread(runnable); return; } // We do this only in workers for now. NotifyHandler(aCx, aData, callData); if (StaticPrefs::dom_worker_console_dispatch_events_to_main_thread()) { RefPtr runnable = new ConsoleCallDataWorkerRunnable(this, callData); Unused << NS_WARN_IF(!runnable->Dispatch(aCx, aData)); } } MainThreadConsoleData* Console::GetOrCreateMainThreadData() { AssertIsOnOwningThread(); if (!mMainThreadData) { mMainThreadData = new MainThreadConsoleData(); } return mMainThreadData; } // We store information to lazily compute the stack in the reserved slots of // LazyStackGetter. The first slot always stores a JS object: it's either the // JS wrapper of the nsIStackFrame or the actual reified stack representation. // The second slot is a PrivateValue() holding an nsIStackFrame* when we haven't // reified the stack yet, or an UndefinedValue() otherwise. enum { SLOT_STACKOBJ, SLOT_RAW_STACK }; bool LazyStackGetter(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { JS::CallArgs args = CallArgsFromVp(aArgc, aVp); JS::Rooted callee(aCx, &args.callee()); JS::Value v = js::GetFunctionNativeReserved(&args.callee(), SLOT_RAW_STACK); if (v.isUndefined()) { // Already reified. args.rval().set(js::GetFunctionNativeReserved(callee, SLOT_STACKOBJ)); return true; } nsIStackFrame* stack = reinterpret_cast(v.toPrivate()); nsTArray reifiedStack; ReifyStack(aCx, stack, reifiedStack); JS::Rooted stackVal(aCx); if (NS_WARN_IF(!ToJSValue(aCx, reifiedStack, &stackVal))) { return false; } MOZ_ASSERT(stackVal.isObject()); js::SetFunctionNativeReserved(callee, SLOT_STACKOBJ, stackVal); js::SetFunctionNativeReserved(callee, SLOT_RAW_STACK, JS::UndefinedValue()); args.rval().set(stackVal); return true; } void MainThreadConsoleData::ProcessCallData( JSContext* aCx, ConsoleCallData* aData, const Sequence& aArguments) { AssertIsOnMainThread(); MOZ_ASSERT(aData); aData->mMutex.AssertCurrentThreadOwns(); JS::Rooted eventValue(aCx); // We want to create a console event object and pass it to our // nsIConsoleAPIStorage implementation. We want to define some accessor // properties on this object, and those will need to keep an nsIStackFrame // alive. But nsIStackFrame cannot be wrapped in an untrusted scope. And // further, passing untrusted objects to system code is likely to run afoul of // Object Xrays. So we want to wrap in a system-principal scope here. But // which one? We could cheat and try to get the underlying JSObject* of // mStorage, but that's a bit fragile. Instead, we just use the junk scope, // with explicit permission from the XPConnect module owner. If you're // tempted to do that anywhere else, talk to said module owner first. // aCx and aArguments are in the same compartment. JS::Rooted targetScope(aCx, xpc::PrivilegedJunkScope()); if (NS_WARN_IF(!Console::PopulateConsoleNotificationInTheTargetScope( aCx, aArguments, targetScope, &eventValue, aData, &mGroupStack))) { return; } if (!mStorage) { mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1"); } if (!mStorage) { NS_WARNING("Failed to get the ConsoleAPIStorage service."); return; } nsAutoString innerID; MOZ_ASSERT(aData->mIDType != ConsoleCallData::eUnknown); if (aData->mIDType == ConsoleCallData::eString) { innerID = aData->mInnerIDString; } else { MOZ_ASSERT(aData->mIDType == ConsoleCallData::eNumber); innerID.AppendInt(aData->mInnerIDNumber); } if (aData->mMethodName == Console::MethodClear) { DebugOnly rv = mStorage->ClearEvents(innerID); NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "ClearEvents failed"); } if (NS_FAILED(mStorage->RecordEvent(innerID, eventValue))) { NS_WARNING("Failed to record a console event."); } } /* static */ bool Console::PopulateConsoleNotificationInTheTargetScope( JSContext* aCx, const Sequence& aArguments, JS::Handle aTargetScope, JS::MutableHandle aEventValue, ConsoleCallData* aData, nsTArray* aGroupStack) { MOZ_ASSERT(aCx); MOZ_ASSERT(aData); MOZ_ASSERT(aTargetScope); MOZ_ASSERT(JS_IsGlobalObject(aTargetScope)); aData->mMutex.AssertCurrentThreadOwns(); ConsoleStackEntry frame; if (aData->mTopStackFrame) { frame = *aData->mTopStackFrame; } ConsoleCommon::ClearException ce(aCx); RootedDictionary event(aCx); event.mAddonId = aData->mAddonId; event.mID.Construct(); event.mInnerID.Construct(); event.mChromeContext = nsContentUtils::ThreadsafeIsSystemCaller(aCx); if (aData->mIDType == ConsoleCallData::eString) { event.mID.Value().SetAsString() = aData->mOuterIDString; event.mInnerID.Value().SetAsString() = aData->mInnerIDString; } else if (aData->mIDType == ConsoleCallData::eNumber) { event.mID.Value().SetAsUnsignedLongLong() = aData->mOuterIDNumber; event.mInnerID.Value().SetAsUnsignedLongLong() = aData->mInnerIDNumber; } else { // aData->mIDType can be eUnknown when we dispatch notifications via // mConsoleEventNotifier. event.mID.Value().SetAsUnsignedLongLong() = 0; event.mInnerID.Value().SetAsUnsignedLongLong() = 0; } event.mConsoleID = aData->mConsoleID; event.mLevel = aData->mMethodString; event.mFilename = frame.mFilename; event.mPrefix = aData->mPrefix; nsCOMPtr filenameURI; nsAutoCString pass; if (NS_IsMainThread() && NS_SUCCEEDED(NS_NewURI(getter_AddRefs(filenameURI), frame.mFilename)) && NS_SUCCEEDED(filenameURI->GetPassword(pass)) && !pass.IsEmpty()) { nsCOMPtr safeURI = do_QueryInterface(filenameURI); nsAutoCString spec; if (safeURI && NS_SUCCEEDED(safeURI->GetSensitiveInfoHiddenSpec(spec))) { event.mFilename = spec; } } 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.IsPrivateBrowsing(); switch (aData->mMethodName) { case MethodLog: case MethodInfo: case MethodWarn: case MethodError: case MethodException: case MethodDebug: case MethodAssert: case MethodGroup: case MethodGroupCollapsed: case MethodTrace: event.mArguments.Construct(); event.mStyles.Construct(); if (NS_WARN_IF(!ProcessArguments(aCx, aArguments, event.mArguments.Value(), event.mStyles.Value()))) { return false; } break; default: event.mArguments.Construct(); if (NS_WARN_IF( !event.mArguments.Value().AppendElements(aArguments, fallible))) { return false; } } if (aData->mMethodName == MethodGroup || aData->mMethodName == MethodGroupCollapsed) { ComposeAndStoreGroupName(aCx, event.mArguments.Value(), event.mGroupName, aGroupStack); } else if (aData->mMethodName == MethodGroupEnd) { if (!UnstoreGroupName(event.mGroupName, aGroupStack)) { return false; } } else if (aData->mMethodName == MethodTime && !aArguments.IsEmpty()) { event.mTimer = CreateStartTimerValue(aCx, aData->mStartTimerLabel, aData->mStartTimerStatus); } else if ((aData->mMethodName == MethodTimeEnd || aData->mMethodName == MethodTimeLog) && !aArguments.IsEmpty()) { event.mTimer = CreateLogOrEndTimerValue(aCx, aData->mLogTimerLabel, aData->mLogTimerDuration, aData->mLogTimerStatus); } else if (aData->mMethodName == MethodCount || aData->mMethodName == MethodCountReset) { event.mCounter = CreateCounterOrResetCounterValue(aCx, aData->mCountLabel, aData->mCountValue); } JSAutoRealm ar2(aCx, aTargetScope); if (NS_WARN_IF(!ToJSValue(aCx, event, aEventValue))) { return false; } JS::Rooted eventObj(aCx, &aEventValue.toObject()); if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventObj, JSPROP_ENUMERATE))) { return false; } if (ShouldIncludeStackTrace(aData->mMethodName)) { // Now define the "stacktrace" property on eventObj. There are two cases // here. Either we came from a worker and have a reified stack, or we want // to define a getter that will lazily reify the stack. if (aData->mReifiedStack) { JS::Rooted stacktrace(aCx); if (NS_WARN_IF(!ToJSValue(aCx, *aData->mReifiedStack, &stacktrace)) || NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", stacktrace, JSPROP_ENUMERATE))) { return false; } } else { JSFunction* fun = js::NewFunctionWithReserved(aCx, LazyStackGetter, 0, 0, "stacktrace"); if (NS_WARN_IF(!fun)) { return false; } JS::Rooted funObj(aCx, JS_GetFunctionObject(fun)); // We want to store our stack in the function and have it stay alive. But // we also need sane access to the C++ nsIStackFrame. So store both a JS // wrapper and the raw pointer: the former will keep the latter alive. JS::Rooted stackVal(aCx); nsresult rv = nsContentUtils::WrapNative(aCx, aData->mStack, &stackVal); if (NS_WARN_IF(NS_FAILED(rv))) { return false; } js::SetFunctionNativeReserved(funObj, SLOT_STACKOBJ, stackVal); js::SetFunctionNativeReserved(funObj, SLOT_RAW_STACK, JS::PrivateValue(aData->mStack.get())); if (NS_WARN_IF(!JS_DefineProperty(aCx, eventObj, "stacktrace", funObj, nullptr, JSPROP_ENUMERATE))) { return false; } } } return true; } namespace { // Helper method for ProcessArguments. Flushes output, if non-empty, to // aSequence. bool FlushOutput(JSContext* aCx, Sequence& aSequence, nsString& aOutput) { if (!aOutput.IsEmpty()) { JS::Rooted str( aCx, JS_NewUCStringCopyN(aCx, aOutput.get(), aOutput.Length())); if (NS_WARN_IF(!str)) { return false; } if (NS_WARN_IF(!aSequence.AppendElement(JS::StringValue(str), fallible))) { return false; } aOutput.Truncate(); } return true; } } // namespace static void MakeFormatString(nsCString& aFormat, int32_t aInteger, int32_t aMantissa, char aCh) { aFormat.Append('%'); if (aInteger >= 0) { aFormat.AppendInt(aInteger); } if (aMantissa >= 0) { aFormat.Append('.'); aFormat.AppendInt(aMantissa); } aFormat.Append(aCh); } // If the first JS::Value of the array is a string, this method uses it to // format a string. The supported sequences are: // %s - string // %d,%i - integer // %f - double // %o,%O - a JS object. // %c - style string. // The output is an array where any object is a separated item, the rest is // unified in a format string. // Example if the input is: // "string: %s, integer: %d, object: %o, double: %f", 's', 1, window, 0.9 // The output will be: // [ "string: s, integer: 1, object: ", window, ", double: 0.9" ] // // The aStyles array is populated with the style strings that the function // finds based the format string. The index of the styles matches the indexes // of elements that need the custom styling from aSequence. For elements with // no custom styling the array is padded with null elements. static bool ProcessArguments(JSContext* aCx, const Sequence& aData, Sequence& aSequence, Sequence& aStyles) { // This method processes the arguments as format strings (%d, %i, %s...) // only if the first element of them is a valid and not-empty string. if (aData.IsEmpty()) { return true; } if (aData.Length() == 1 || !aData[0].isString()) { return aSequence.AppendElements(aData, fallible); } JS::Rooted format(aCx, aData[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, format)); if (NS_WARN_IF(!jsString)) { return false; } nsAutoJSString string; if (NS_WARN_IF(!string.init(aCx, jsString))) { return false; } if (string.IsEmpty()) { return aSequence.AppendElements(aData, fallible); } nsString::const_iterator start, end; string.BeginReading(start); string.EndReading(end); nsString output; uint32_t index = 1; while (start != end) { if (*start != '%') { output.Append(*start); ++start; continue; } ++start; if (start == end) { output.Append('%'); break; } if (*start == '%') { output.Append(*start); ++start; continue; } nsAutoString tmp; tmp.Append('%'); int32_t integer = -1; int32_t mantissa = -1; // Let's parse %. for %d and %f if (*start >= '0' && *start <= '9') { integer = 0; do { integer = integer * 10 + *start - '0'; tmp.Append(*start); ++start; } while (*start >= '0' && *start <= '9' && start != end); } if (start == end) { output.Append(tmp); break; } if (*start == '.') { tmp.Append(*start); ++start; if (start == end) { output.Append(tmp); break; } // '.' must be followed by a number. if (*start < '0' || *start > '9') { output.Append(tmp); continue; } mantissa = 0; do { mantissa = mantissa * 10 + *start - '0'; tmp.Append(*start); ++start; } while (*start >= '0' && *start <= '9' && start != end); if (start == end) { output.Append(tmp); break; } } char ch = *start; tmp.Append(ch); ++start; switch (ch) { case 'o': case 'O': { if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { return false; } JS::Rooted v(aCx); if (index < aData.Length()) { v = aData[index++]; } if (NS_WARN_IF(!aSequence.AppendElement(v, fallible))) { return false; } break; } case 'c': { // If there isn't any output but there's already a style, then // discard the previous style and use the next one instead. if (output.IsEmpty() && !aStyles.IsEmpty()) { aStyles.RemoveLastElement(); } if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { return false; } if (index < aData.Length()) { JS::Rooted v(aCx, aData[index++]); JS::Rooted jsString(aCx, JS::ToString(aCx, v)); if (NS_WARN_IF(!jsString)) { return false; } int32_t diff = aSequence.Length() - aStyles.Length(); if (diff > 0) { for (int32_t i = 0; i < diff; i++) { if (NS_WARN_IF(!aStyles.AppendElement(VoidString(), fallible))) { return false; } } } nsAutoJSString string; if (NS_WARN_IF(!string.init(aCx, jsString))) { return false; } if (NS_WARN_IF(!aStyles.AppendElement(string, fallible))) { return false; } } break; } case 's': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (NS_WARN_IF(!jsString)) { return false; } nsAutoJSString v; if (NS_WARN_IF(!v.init(aCx, jsString))) { return false; } output.Append(v); } break; case 'd': case 'i': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); if (value.isBigInt()) { JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (NS_WARN_IF(!jsString)) { return false; } nsAutoJSString v; if (NS_WARN_IF(!v.init(aCx, jsString))) { return false; } output.Append(v); break; } int32_t v; if (NS_WARN_IF(!JS::ToInt32(aCx, value, &v))) { return false; } nsCString format; MakeFormatString(format, integer, mantissa, 'd'); output.AppendPrintf(format.get(), v); } break; case 'f': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); double v; if (NS_WARN_IF(!JS::ToNumber(aCx, value, &v))) { return false; } // nspr returns "nan", but we want to expose it as "NaN" if (std::isnan(v)) { output.AppendFloat(v); } else { nsCString format; MakeFormatString(format, integer, mantissa, 'f'); output.AppendPrintf(format.get(), v); } } break; default: output.Append(tmp); break; } } if (NS_WARN_IF(!FlushOutput(aCx, aSequence, output))) { return false; } // Discard trailing style element if there is no output to apply it to. if (aStyles.Length() > aSequence.Length()) { aStyles.TruncateLength(aSequence.Length()); } // The rest of the array, if unused by the format string. for (; index < aData.Length(); ++index) { if (NS_WARN_IF(!aSequence.AppendElement(aData[index], fallible))) { return false; } } return true; } // Stringify and Concat all the JS::Value in a single string using ' ' as // separator. The new group name will be stored in aGroupStack array. static void ComposeAndStoreGroupName(JSContext* aCx, const Sequence& aData, nsAString& aName, nsTArray* aGroupStack) { StringJoinAppend( aName, u" "_ns, aData, [aCx](nsAString& dest, const JS::Value& valueRef) { JS::Rooted value(aCx, valueRef); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (!jsString) { return; } nsAutoJSString string; if (!string.init(aCx, jsString)) { return; } dest.Append(string); }); aGroupStack->AppendElement(aName); } // Remove the last group name and return that name. It returns false if // aGroupStack is empty. static bool UnstoreGroupName(nsAString& aName, nsTArray* aGroupStack) { if (aGroupStack->IsEmpty()) { return false; } aName = aGroupStack->PopLastElement(); return true; } Console::TimerStatus Console::StartTimer(JSContext* aCx, const JS::Value& aName, DOMHighResTimeStamp aTimestamp, nsAString& aTimerLabel, DOMHighResTimeStamp* aTimerValue) { AssertIsOnOwningThread(); MOZ_ASSERT(aTimerValue); *aTimerValue = 0; if (NS_WARN_IF(mTimerRegistry.Count() >= MAX_PAGE_TIMERS)) { return eTimerMaxReached; } JS::Rooted name(aCx, aName); JS::Rooted jsString(aCx, JS::ToString(aCx, name)); if (NS_WARN_IF(!jsString)) { return eTimerJSException; } nsAutoJSString label; if (NS_WARN_IF(!label.init(aCx, jsString))) { return eTimerJSException; } aTimerLabel = label; if (mTimerRegistry.WithEntryHandle(label, [&](auto&& entry) { if (entry) { return true; } entry.Insert(aTimestamp); return false; })) { return eTimerAlreadyExists; } *aTimerValue = aTimestamp; return eTimerDone; } /* static */ JS::Value Console::CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel, TimerStatus aTimerStatus) { MOZ_ASSERT(aTimerStatus != eTimerUnknown); if (aTimerStatus != eTimerDone) { return CreateTimerError(aCx, aTimerLabel, aTimerStatus); } RootedDictionary timer(aCx); timer.mName = aTimerLabel; JS::Rooted value(aCx); if (!ToJSValue(aCx, timer, &value)) { return JS::UndefinedValue(); } return value; } Console::TimerStatus Console::LogTimer(JSContext* aCx, const JS::Value& aName, DOMHighResTimeStamp aTimestamp, nsAString& aTimerLabel, double* aTimerDuration, bool aCancelTimer) { AssertIsOnOwningThread(); MOZ_ASSERT(aTimerDuration); *aTimerDuration = 0; JS::Rooted name(aCx, aName); JS::Rooted jsString(aCx, JS::ToString(aCx, name)); if (NS_WARN_IF(!jsString)) { return eTimerJSException; } nsAutoJSString key; if (NS_WARN_IF(!key.init(aCx, jsString))) { return eTimerJSException; } aTimerLabel = key; DOMHighResTimeStamp value = 0; if (aCancelTimer) { if (!mTimerRegistry.Remove(key, &value)) { NS_WARNING("mTimerRegistry entry not found"); return eTimerDoesntExist; } } else { if (!mTimerRegistry.Get(key, &value)) { NS_WARNING("mTimerRegistry entry not found"); return eTimerDoesntExist; } } *aTimerDuration = aTimestamp - value; return eTimerDone; } /* static */ JS::Value Console::CreateLogOrEndTimerValue(JSContext* aCx, const nsAString& aLabel, double aDuration, TimerStatus aStatus) { if (aStatus != eTimerDone) { return CreateTimerError(aCx, aLabel, aStatus); } RootedDictionary timer(aCx); timer.mName = aLabel; timer.mDuration = aDuration; JS::Rooted value(aCx); if (!ToJSValue(aCx, timer, &value)) { return JS::UndefinedValue(); } return value; } /* static */ JS::Value Console::CreateTimerError(JSContext* aCx, const nsAString& aLabel, TimerStatus aStatus) { MOZ_ASSERT(aStatus != eTimerUnknown && aStatus != eTimerDone); RootedDictionary error(aCx); error.mName = aLabel; switch (aStatus) { case eTimerAlreadyExists: error.mError.AssignLiteral("timerAlreadyExists"); break; case eTimerDoesntExist: error.mError.AssignLiteral("timerDoesntExist"); break; case eTimerJSException: error.mError.AssignLiteral("timerJSError"); break; case eTimerMaxReached: error.mError.AssignLiteral("maxTimersExceeded"); break; default: MOZ_CRASH("Unsupported status"); break; } JS::Rooted value(aCx); if (!ToJSValue(aCx, error, &value)) { return JS::UndefinedValue(); } return value; } uint32_t Console::IncreaseCounter(JSContext* aCx, const Sequence& aArguments, nsAString& aCountLabel) { AssertIsOnOwningThread(); ConsoleCommon::ClearException ce(aCx); MOZ_ASSERT(!aArguments.IsEmpty()); JS::Rooted labelValue(aCx, aArguments[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); if (!jsString) { return 0; // We cannot continue. } nsAutoJSString string; if (!string.init(aCx, jsString)) { return 0; // We cannot continue. } aCountLabel = string; const bool maxCountersReached = mCounterRegistry.Count() >= MAX_PAGE_COUNTERS; return mCounterRegistry.WithEntryHandle( aCountLabel, [maxCountersReached](auto&& entry) -> uint32_t { if (entry) { ++entry.Data(); } else { if (maxCountersReached) { return MAX_PAGE_COUNTERS; } entry.Insert(1); } return entry.Data(); }); } uint32_t Console::ResetCounter(JSContext* aCx, const Sequence& aArguments, nsAString& aCountLabel) { AssertIsOnOwningThread(); ConsoleCommon::ClearException ce(aCx); MOZ_ASSERT(!aArguments.IsEmpty()); JS::Rooted labelValue(aCx, aArguments[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); if (!jsString) { return 0; // We cannot continue. } nsAutoJSString string; if (!string.init(aCx, jsString)) { return 0; // We cannot continue. } aCountLabel = string; if (mCounterRegistry.Remove(aCountLabel)) { return 0; } // Let's return something different than 0 if the key doesn't exist. return MAX_PAGE_COUNTERS; } // This method generates a ConsoleCounter dictionary as JS::Value. If // aCountValue is == MAX_PAGE_COUNTERS it generates a ConsoleCounterError // instead. See IncreaseCounter. // * aCx - this is the context that will root the returned value. // * aCountLabel - this label must be what IncreaseCounter received as // aTimerLabel. // * aCountValue - the return value of IncreaseCounter. static JS::Value CreateCounterOrResetCounterValue(JSContext* aCx, const nsAString& aCountLabel, uint32_t aCountValue) { ConsoleCommon::ClearException ce(aCx); if (aCountValue == MAX_PAGE_COUNTERS) { RootedDictionary error(aCx); error.mLabel = aCountLabel; error.mError.AssignLiteral("counterDoesntExist"); JS::Rooted value(aCx); if (!ToJSValue(aCx, error, &value)) { return JS::UndefinedValue(); } return value; } RootedDictionary data(aCx); data.mLabel = aCountLabel; data.mCount = aCountValue; JS::Rooted value(aCx); if (!ToJSValue(aCx, data, &value)) { return JS::UndefinedValue(); } return value; } /* static */ bool Console::ShouldIncludeStackTrace(MethodName aMethodName) { switch (aMethodName) { case MethodError: case MethodException: case MethodAssert: case MethodTrace: return true; default: return false; } } JSObject* MainThreadConsoleData::GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal) { AssertIsOnMainThread(); if (!mSandbox) { nsIXPConnect* xpc = nsContentUtils::XPConnect(); MOZ_ASSERT(xpc, "This should never be null!"); JS::Rooted sandbox(aCx); nsresult rv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address()); if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; } mSandbox = new JSObjectHolder(aCx, sandbox); } return mSandbox->GetJSObject(); } bool Console::StoreCallData(JSContext* aCx, ConsoleCallData* aCallData, const Sequence& aArguments) { AssertIsOnOwningThread(); if (NS_WARN_IF(!mArgumentStorage.growBy(1))) { return false; } if (!mArgumentStorage.end()[-1].Initialize(aCx, aArguments)) { mArgumentStorage.shrinkBy(1); return false; } MOZ_ASSERT(aCallData); MOZ_ASSERT(!mCallDataStorage.Contains(aCallData)); mCallDataStorage.AppendElement(aCallData); MOZ_ASSERT(mCallDataStorage.Length() == mArgumentStorage.length()); if (mCallDataStorage.Length() > STORAGE_MAX_EVENTS) { mCallDataStorage.RemoveElementAt(0); mArgumentStorage.erase(&mArgumentStorage[0]); } return true; } void Console::UnstoreCallData(ConsoleCallData* aCallData) { AssertIsOnOwningThread(); MOZ_ASSERT(aCallData); MOZ_ASSERT(mCallDataStorage.Length() == mArgumentStorage.length()); size_t index = mCallDataStorage.IndexOf(aCallData); // It can be that mCallDataStorage has been already cleaned in case the // processing of the argument of some Console methods triggers the // window.close(). if (index == mCallDataStorage.NoIndex) { return; } mCallDataStorage.RemoveElementAt(index); mArgumentStorage.erase(&mArgumentStorage[index]); } void Console::NotifyHandler(JSContext* aCx, const Sequence& aArguments, ConsoleCallData* aCallData) { AssertIsOnOwningThread(); MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aCallData); if (!mConsoleEventNotifier) { return; } JS::Rooted value(aCx); JS::Rooted callableGlobal( aCx, mConsoleEventNotifier->CallbackGlobalOrNull()); if (NS_WARN_IF(!callableGlobal)) { return; } // aCx and aArguments are in the same compartment because this method is // called directly when a Console.something() runs. // mConsoleEventNotifier->CallbackGlobal() is the scope where value will be // sent to. if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope( aCx, aArguments, callableGlobal, &value, aCallData, &mGroupStack))) { return; } JS::Rooted ignored(aCx); RefPtr notifier(mConsoleEventNotifier); notifier->Call(value, &ignored); } void Console::RetrieveConsoleEvents(JSContext* aCx, nsTArray& aEvents, ErrorResult& aRv) { AssertIsOnOwningThread(); // We don't want to expose this functionality to main-thread yet. MOZ_ASSERT(!NS_IsMainThread()); JS::Rooted targetScope(aCx, JS::CurrentGlobalOrNull(aCx)); for (uint32_t i = 0; i < mArgumentStorage.length(); ++i) { JS::Rooted value(aCx); JS::Rooted sequenceScope(aCx, mArgumentStorage[i].Global()); JSAutoRealm ar(aCx, sequenceScope); Sequence sequence; SequenceRooter arguments(aCx, &sequence); if (!mArgumentStorage[i].PopulateArgumentsSequence(sequence)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } // Here we have aCx and sequence in the same compartment. // targetScope is the destination scope and value will be populated in its // compartment. { MutexAutoLock lock(mCallDataStorage[i]->mMutex); if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope( aCx, sequence, targetScope, &value, mCallDataStorage[i], &mGroupStack))) { aRv.Throw(NS_ERROR_FAILURE); return; } } aEvents.AppendElement(value); } } void Console::SetConsoleEventHandler(AnyCallback* aHandler) { AssertIsOnOwningThread(); // We don't want to expose this functionality to main-thread yet. MOZ_ASSERT(!NS_IsMainThread()); mConsoleEventNotifier = aHandler; } void Console::AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(Console); } bool Console::IsShuttingDown() const { MOZ_ASSERT(mStatus != eUnknown); return mStatus == eShuttingDown; } /* static */ already_AddRefed Console::GetConsole(const GlobalObject& aGlobal) { ErrorResult rv; RefPtr console = GetConsoleInternal(aGlobal, rv); if (NS_WARN_IF(rv.Failed()) || !console) { rv.SuppressException(); return nullptr; } console->AssertIsOnOwningThread(); if (console->IsShuttingDown()) { return nullptr; } return console.forget(); } /* static */ already_AddRefed Console::GetConsoleInternal( const GlobalObject& aGlobal, ErrorResult& aRv) { // Window if (NS_IsMainThread()) { nsCOMPtr innerWindow = do_QueryInterface(aGlobal.GetAsSupports()); // we are probably running a chrome script. if (!innerWindow) { RefPtr console = new Console(aGlobal.Context(), nullptr, 0, 0); console->Initialize(aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } return console.forget(); } nsGlobalWindowInner* window = nsGlobalWindowInner::Cast(innerWindow); return window->GetConsole(aGlobal.Context(), aRv); } // Worklet nsCOMPtr workletScope = do_QueryInterface(aGlobal.GetAsSupports()); if (workletScope) { WorkletThread::AssertIsOnWorkletThread(); return workletScope->GetConsole(aGlobal.Context(), aRv); } // Workers MOZ_ASSERT(!NS_IsMainThread()); JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); MOZ_ASSERT(workerPrivate); nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); if (NS_WARN_IF(!global)) { return nullptr; } WorkerGlobalScope* scope = workerPrivate->GlobalScope(); MOZ_ASSERT(scope); // Normal worker scope. if (scope == global) { return scope->GetConsole(aRv); } // Debugger worker scope WorkerDebuggerGlobalScope* debuggerScope = workerPrivate->DebuggerGlobalScope(); MOZ_ASSERT(debuggerScope); MOZ_ASSERT(debuggerScope == global, "Which kind of global do we have?"); return debuggerScope->GetConsole(aRv); } bool Console::MonotonicTimer(JSContext* aCx, MethodName aMethodName, const Sequence& aData, DOMHighResTimeStamp* aTimeStamp) { if (nsCOMPtr innerWindow = do_QueryInterface(mGlobal)) { nsGlobalWindowInner* win = nsGlobalWindowInner::Cast(innerWindow); MOZ_ASSERT(win); RefPtr performance = win->GetPerformance(); if (!performance) { return false; } *aTimeStamp = performance->Now(); return true; } if (NS_IsMainThread()) { *aTimeStamp = (TimeStamp::Now() - mCreationTimeStamp).ToMilliseconds(); return true; } if (nsCOMPtr workletGlobal = do_QueryInterface(mGlobal)) { *aTimeStamp = workletGlobal->TimeStampToDOMHighRes(TimeStamp::Now()); return true; } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); *aTimeStamp = workerPrivate->TimeStampToDOMHighRes(TimeStamp::Now()); return true; } /* static */ already_AddRefed Console::CreateInstance( const GlobalObject& aGlobal, const ConsoleInstanceOptions& aOptions) { RefPtr console = new ConsoleInstance(aGlobal.Context(), aOptions); return console.forget(); } void Console::StringifyElement(Element* aElement, nsAString& aOut) { aOut.AppendLiteral("<"); aOut.Append(aElement->LocalName()); uint32_t attrCount = aElement->GetAttrCount(); nsAutoString idAttr; nsAutoString classAttr; nsAutoString nameAttr; nsAutoString otherAttrs; for (uint32_t i = 0; i < attrCount; i++) { BorrowedAttrInfo attrInfo = aElement->GetAttrInfoAt(i); nsAutoString attrValue; attrInfo.mValue->ToString(attrValue); const nsAttrName* attrName = attrInfo.mName; if (attrName->Equals(nsGkAtoms::id)) { idAttr.AppendLiteral(" id=\""); idAttr.Append(attrValue); idAttr.AppendLiteral("\""); } else if (attrName->Equals(nsGkAtoms::_class)) { classAttr.AppendLiteral(" class=\""); classAttr.Append(attrValue); classAttr.AppendLiteral("\""); } else if (attrName->Equals(nsGkAtoms::name)) { nameAttr.AppendLiteral(" name=\""); nameAttr.Append(attrValue); nameAttr.AppendLiteral("\""); } else { nsAutoString attrNameStr; attrName->GetQualifiedName(attrNameStr); otherAttrs.AppendLiteral(" "); otherAttrs.Append(attrNameStr); otherAttrs.AppendLiteral("=\""); otherAttrs.Append(attrValue); otherAttrs.AppendLiteral("\""); } } if (!idAttr.IsEmpty()) { aOut.Append(idAttr); } if (!classAttr.IsEmpty()) { aOut.Append(classAttr); } if (!nameAttr.IsEmpty()) { aOut.Append(nameAttr); } if (!otherAttrs.IsEmpty()) { aOut.Append(otherAttrs); } aOut.AppendLiteral(">"); } void Console::MaybeExecuteDumpFunction(JSContext* aCx, MethodName aMethodName, const nsAString& aMethodString, const Sequence& aData, nsIStackFrame* aStack, DOMHighResTimeStamp aMonotonicTimer) { if (mLogModule->ShouldLog(InternalLogLevelToMozLog(aMethodName))) { nsString message = GetDumpMessage(aCx, aMethodName, aMethodString, aData, aStack, aMonotonicTimer, true); MOZ_LOG(mLogModule, InternalLogLevelToMozLog(aMethodName), ("%s", NS_ConvertUTF16toUTF8(message).get())); } if (!mDumpFunction && !mDumpToStdout) { return; } nsString message = GetDumpMessage(aCx, aMethodName, aMethodString, aData, aStack, aMonotonicTimer, false); ExecuteDumpFunction(message); } nsString Console::GetDumpMessage(JSContext* aCx, MethodName aMethodName, const nsAString& aMethodString, const Sequence& aData, nsIStackFrame* aStack, DOMHighResTimeStamp aMonotonicTimer, bool aIsForMozLog) { nsString message; // MOZ_LOG already logs either console or the prefix if (!aIsForMozLog) { message.AssignLiteral("console."); } else { message.AssignLiteral(""); } message.Append(aMethodString); message.AppendLiteral(": "); if (!aIsForMozLog && !mPrefix.IsEmpty()) { message.Append(mPrefix); message.AppendLiteral(": "); } for (uint32_t i = 0; i < aData.Length(); ++i) { JS::Rooted v(aCx, aData[i]); if (v.isObject()) { Element* element = nullptr; if (NS_SUCCEEDED(UNWRAP_OBJECT(Element, &v, element))) { if (i != 0) { message.AppendLiteral(" "); } StringifyElement(element, message); continue; } } JS::Rooted jsString(aCx, JS_ValueToSource(aCx, v)); if (!jsString) { continue; } nsAutoJSString string; if (NS_WARN_IF(!string.init(aCx, jsString))) { return message; } if (i != 0) { message.AppendLiteral(" "); } message.Append(string); } if (aMethodName == MethodTime || aMethodName == MethodTimeEnd) { message.AppendLiteral(" @ "); message.AppendFloat(aMonotonicTimer); } message.AppendLiteral("\n"); // aStack can be null. nsCOMPtr stack(aStack); while (stack) { nsAutoCString filename; stack->GetFilename(aCx, filename); AppendUTF8toUTF16(filename, message); message.AppendLiteral(" "); message.AppendInt(stack->GetLineNumber(aCx)); message.AppendLiteral(" "); nsAutoString functionName; stack->GetName(aCx, functionName); message.Append(functionName); message.AppendLiteral("\n"); nsCOMPtr caller = stack->GetCaller(aCx); if (!caller) { caller = stack->GetAsyncCaller(aCx); } stack.swap(caller); } return message; } void Console::ExecuteDumpFunction(const nsAString& aMessage) { if (mDumpFunction) { RefPtr dumpFunction(mDumpFunction); dumpFunction->Call(aMessage); return; } NS_ConvertUTF16toUTF8 str(aMessage); MOZ_LOG(nsContentUtils::DOMDumpLog(), LogLevel::Debug, ("%s", str.get())); #ifdef ANDROID __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); #endif fputs(str.get(), stdout); fflush(stdout); } bool Console::ShouldProceed(MethodName aName) const { return mCurrentLogLevel <= 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; } } LogLevel Console::InternalLogLevelToMozLog(MethodName aName) const { switch (aName) { case MethodLog: return LogLevel::Info; case MethodInfo: return LogLevel::Info; case MethodWarn: return LogLevel::Warning; case MethodError: return LogLevel::Error; case MethodException: return LogLevel::Error; case MethodDebug: return LogLevel::Debug; case MethodTable: return LogLevel::Info; case MethodTrace: return LogLevel::Info; case MethodDir: return LogLevel::Info; case MethodDirxml: return LogLevel::Info; case MethodGroup: return LogLevel::Info; case MethodGroupCollapsed: return LogLevel::Info; case MethodGroupEnd: return LogLevel::Info; case MethodTime: return LogLevel::Info; case MethodTimeLog: return LogLevel::Info; case MethodTimeEnd: return LogLevel::Info; case MethodTimeStamp: return LogLevel::Info; case MethodAssert: return LogLevel::Error; case MethodCount: return LogLevel::Info; case MethodCountReset: return LogLevel::Info; case MethodClear: return LogLevel::Info; case MethodProfile: return LogLevel::Info; case MethodProfileEnd: return LogLevel::Info; default: MOZ_CRASH("MethodName is out of sync with the Console implementation!"); return LogLevel::Disabled; } } bool Console::ArgumentData::Initialize(JSContext* aCx, const Sequence& aArguments) { mGlobal = JS::CurrentGlobalOrNull(aCx); if (NS_WARN_IF(!mArguments.AppendElements(aArguments, fallible))) { return false; } return true; } void Console::ArgumentData::Trace(const TraceCallbacks& aCallbacks, void* aClosure) { ArgumentData* tmp = this; for (uint32_t i = 0; i < mArguments.Length(); ++i) { NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mArguments[i]) } NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal) } bool Console::ArgumentData::PopulateArgumentsSequence( Sequence& aSequence) const { AssertIsOnOwningThread(); for (uint32_t i = 0; i < mArguments.Length(); ++i) { if (NS_WARN_IF(!aSequence.AppendElement(mArguments[i], fallible))) { return false; } } return true; } } // namespace mozilla::dom