summaryrefslogtreecommitdiffstats
path: root/dom/console
diff options
context:
space:
mode:
Diffstat (limited to 'dom/console')
-rw-r--r--dom/console/Console.cpp3045
-rw-r--r--dom/console/Console.h450
-rw-r--r--dom/console/ConsoleAPIStorage.sys.mjs195
-rw-r--r--dom/console/ConsoleCommon.h28
-rw-r--r--dom/console/ConsoleInstance.cpp206
-rw-r--r--dom/console/ConsoleInstance.h114
-rw-r--r--dom/console/ConsoleReportCollector.cpp198
-rw-r--r--dom/console/ConsoleReportCollector.h92
-rw-r--r--dom/console/ConsoleUtils.cpp167
-rw-r--r--dom/console/ConsoleUtils.h53
-rw-r--r--dom/console/components.conf14
-rw-r--r--dom/console/moz.build57
-rw-r--r--dom/console/nsIConsoleAPIStorage.idl65
-rw-r--r--dom/console/nsIConsoleReportCollector.h132
-rw-r--r--dom/console/tests/chrome.ini9
-rw-r--r--dom/console/tests/console.sys.mjs45
-rw-r--r--dom/console/tests/file_empty.html1
-rw-r--r--dom/console/tests/head.js24
-rw-r--r--dom/console/tests/mochitest.ini14
-rw-r--r--dom/console/tests/test_bug659625.html107
-rw-r--r--dom/console/tests/test_bug978522.html32
-rw-r--r--dom/console/tests/test_bug979109.html32
-rw-r--r--dom/console/tests/test_bug989665.html21
-rw-r--r--dom/console/tests/test_console.xhtml35
-rw-r--r--dom/console/tests/test_consoleEmptyStack.html26
-rw-r--r--dom/console/tests/test_console_binding.html40
-rw-r--r--dom/console/tests/test_console_proto.html17
-rw-r--r--dom/console/tests/test_count.html117
-rw-r--r--dom/console/tests/test_jsm.xhtml100
-rw-r--r--dom/console/tests/test_timer.html112
-rw-r--r--dom/console/tests/xpcshell/head.js22
-rw-r--r--dom/console/tests/xpcshell/test_basic.js29
-rw-r--r--dom/console/tests/xpcshell/test_failing_console_listener.js35
-rw-r--r--dom/console/tests/xpcshell/test_formatting.js77
-rw-r--r--dom/console/tests/xpcshell/test_reportForServiceWorkerScope.js42
-rw-r--r--dom/console/tests/xpcshell/xpcshell.ini8
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]