summaryrefslogtreecommitdiffstats
path: root/dom/console
diff options
context:
space:
mode:
Diffstat (limited to 'dom/console')
-rw-r--r--dom/console/Console.cpp2440
-rw-r--r--dom/console/Console.h404
-rw-r--r--dom/console/ConsoleAPI.manifest2
-rw-r--r--dom/console/ConsoleAPIStorage.js161
-rw-r--r--dom/console/ConsoleReportCollector.cpp190
-rw-r--r--dom/console/ConsoleReportCollector.h84
-rw-r--r--dom/console/moz.build45
-rw-r--r--dom/console/nsIConsoleAPIStorage.idl47
-rw-r--r--dom/console/nsIConsoleReportCollector.h115
-rw-r--r--dom/console/tests/chrome.ini6
-rw-r--r--dom/console/tests/file_empty.html1
-rw-r--r--dom/console/tests/mochitest.ini11
-rw-r--r--dom/console/tests/test_bug659625.html92
-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.xul35
-rw-r--r--dom/console/tests/test_consoleEmptyStack.html27
-rw-r--r--dom/console/tests/test_console_binding.html42
-rw-r--r--dom/console/tests/test_console_proto.html17
20 files changed, 3804 insertions, 0 deletions
diff --git a/dom/console/Console.cpp b/dom/console/Console.cpp
new file mode 100644
index 000000000..9ede26501
--- /dev/null
+++ b/dom/console/Console.cpp
@@ -0,0 +1,2440 @@
+/* -*- 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/ConsoleBinding.h"
+
+#include "mozilla/dom/BlobBinding.h"
+#include "mozilla/dom/Exceptions.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/FunctionBinding.h"
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "mozilla/Maybe.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDocument.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsGlobalWindow.h"
+#include "nsJSUtils.h"
+#include "nsNetUtil.h"
+#include "ScriptSettings.h"
+#include "WorkerPrivate.h"
+#include "WorkerRunnable.h"
+#include "WorkerScope.h"
+#include "xpcpublic.h"
+#include "nsContentUtils.h"
+#include "nsDocShell.h"
+#include "nsProxyRelease.h"
+#include "mozilla/ConsoleTimelineMarker.h"
+#include "mozilla/TimestampTimelineMarker.h"
+
+#include "nsIConsoleAPIStorage.h"
+#include "nsIDOMWindowUtils.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsILoadContext.h"
+#include "nsIProgrammingLanguage.h"
+#include "nsISensitiveInfoHiddenURI.h"
+#include "nsIServiceManager.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;
+using namespace mozilla::dom::workers;
+
+namespace mozilla {
+namespace dom {
+
+struct
+ConsoleStructuredCloneData
+{
+ nsCOMPtr<nsISupports> mParent;
+ nsTArray<RefPtr<BlobImpl>> mBlobs;
+};
+
+/**
+ * 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_REFCOUNTING(ConsoleCallData)
+
+ ConsoleCallData()
+ : mMethodName(Console::MethodLog)
+ , mPrivate(false)
+ , mTimeStamp(JS_Now() / PR_USEC_PER_MSEC)
+ , mStartTimerValue(0)
+ , mStartTimerStatus(false)
+ , mStopTimerDuration(0)
+ , mStopTimerStatus(false)
+ , mCountValue(MAX_PAGE_COUNTERS)
+ , mIDType(eUnknown)
+ , mOuterIDNumber(0)
+ , mInnerIDNumber(0)
+ , mStatus(eUnused)
+#ifdef DEBUG
+ , mOwningThread(PR_GetCurrentThread())
+#endif
+ {}
+
+ bool
+ Initialize(JSContext* aCx, Console::MethodName aName,
+ const nsAString& aString,
+ const Sequence<JS::Value>& aArguments,
+ Console* aConsole)
+ {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aConsole);
+
+ // We must be registered before doing any JS operation otherwise it can
+ // happen that mCopiedArguments are not correctly traced.
+ aConsole->StoreCallData(this);
+
+ mMethodName = aName;
+ mMethodString = aString;
+
+ mGlobal = JS::CurrentGlobalOrNull(aCx);
+
+ for (uint32_t i = 0; i < aArguments.Length(); ++i) {
+ if (NS_WARN_IF(!mCopiedArguments.AppendElement(aArguments[i]))) {
+ aConsole->UnstoreCallData(this);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ 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 PrincipalOriginAttributes& aOriginAttributes)
+ {
+ mOriginAttributes = aOriginAttributes;
+ }
+
+ bool
+ PopulateArgumentsSequence(Sequence<JS::Value>& aSequence) const
+ {
+ AssertIsOnOwningThread();
+
+ for (uint32_t i = 0; i < mCopiedArguments.Length(); ++i) {
+ if (NS_WARN_IF(!aSequence.AppendElement(mCopiedArguments[i],
+ fallible))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ void
+ Trace(const TraceCallbacks& aCallbacks, void* aClosure)
+ {
+ AssertIsOnOwningThread();
+
+ ConsoleCallData* tmp = this;
+ for (uint32_t i = 0; i < mCopiedArguments.Length(); ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCopiedArguments[i])
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mGlobal);
+ }
+
+ void
+ AssertIsOnOwningThread() const
+ {
+ MOZ_ASSERT(mOwningThread);
+ MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread);
+ }
+
+ JS::Heap<JSObject*> mGlobal;
+
+ // This is a copy of the arguments we received from the DOM bindings. Console
+ // object traces them because this ConsoleCallData calls
+ // RegisterConsoleCallData() in the Initialize().
+ nsTArray<JS::Heap<JS::Value>> mCopiedArguments;
+
+ Console::MethodName mMethodName;
+ bool mPrivate;
+ int64_t mTimeStamp;
+
+ // 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;
+ bool mStartTimerStatus;
+
+ // These values are set in the owning thread and they contain the duration,
+ // the name and the status of the StopTimer 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
+ // ConsoleTimerEnd dictionary. This members are set when
+ // console.timeEnd() is called.
+ double mStopTimerDuration;
+ nsString mStopTimerLabel;
+ bool mStopTimerStatus;
+
+ // These 2 values are set by IncreaseCounter on the owning thread and they are
+ // used CreateCounterValue. These members are set when console.count() is
+ // 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.jsm the ID is 'jsm'.
+ enum {
+ eString,
+ eNumber,
+ eUnknown
+ } mIDType;
+
+ uint64_t mOuterIDNumber;
+ nsString mOuterIDString;
+
+ uint64_t mInnerIDNumber;
+ nsString mInnerIDString;
+
+ PrincipalOriginAttributes mOriginAttributes;
+
+ 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;
+
+ // mStatus is about the lifetime of this object. Console must take care of
+ // keep it alive or not following this enumeration.
+ enum {
+ // If the object is created but it is owned by some runnable, this is its
+ // status. It can be deleted at any time.
+ eUnused,
+
+ // When a runnable takes ownership of a ConsoleCallData and send it to
+ // different thread, this is its status. Console cannot delete it at this
+ // time.
+ eInUse,
+
+ // When a runnable owns this ConsoleCallData, we can't delete it directly.
+ // instead, we mark it with this new status and we move it in
+ // mCallDataStoragePending list in order to keep it alive an trace it
+ // correctly. Once the runnable finishs its task, it will delete this object
+ // calling ReleaseCallData().
+ eToBeDeleted
+ } mStatus;
+
+#ifdef DEBUG
+ PRThread* mOwningThread;
+#endif
+
+private:
+ ~ConsoleCallData()
+ {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mStatus != eInUse);
+ }
+};
+
+// This class is used to clear any exception at the end of this method.
+class ClearException
+{
+public:
+ explicit ClearException(JSContext* aCx)
+ : mCx(aCx)
+ {
+ }
+
+ ~ClearException()
+ {
+ JS_ClearPendingException(mCx);
+ }
+
+private:
+ JSContext* mCx;
+};
+
+class ConsoleRunnable : public WorkerProxyToMainThreadRunnable
+ , public StructuredCloneHolderBase
+{
+public:
+ explicit ConsoleRunnable(Console* aConsole)
+ : WorkerProxyToMainThreadRunnable(GetCurrentThreadWorkerPrivate())
+ , mConsole(aConsole)
+ {}
+
+ virtual
+ ~ConsoleRunnable()
+ {
+ // Clear the StructuredCloneHolderBase class.
+ Clear();
+ }
+
+ bool
+ Dispatch(JSContext* aCx)
+ {
+ mWorkerPrivate->AssertIsOnWorkerThread();
+
+ if (NS_WARN_IF(!PreDispatch(aCx))) {
+ RunBackOnWorkerThread();
+ return false;
+ }
+
+ if (NS_WARN_IF(!WorkerProxyToMainThreadRunnable::Dispatch())) {
+ // RunBackOnWorkerThread() will be called by
+ // WorkerProxyToMainThreadRunnable::Dispatch().
+ return false;
+ }
+
+ return true;
+ }
+
+protected:
+ void
+ RunOnMainThread() override
+ {
+ AssertIsOnMainThread();
+
+ // Walk up to our containing page
+ WorkerPrivate* wp = mWorkerPrivate;
+ while (wp->GetParent()) {
+ wp = wp->GetParent();
+ }
+
+ nsPIDOMWindowInner* window = wp->GetWindow();
+ if (!window) {
+ RunWindowless();
+ } else {
+ RunWithWindow(window);
+ }
+ }
+
+ void
+ RunWithWindow(nsPIDOMWindowInner* aWindow)
+ {
+ AssertIsOnMainThread();
+
+ AutoJSAPI jsapi;
+ MOZ_ASSERT(aWindow);
+
+ RefPtr<nsGlobalWindow> win = nsGlobalWindow::Cast(aWindow);
+ if (NS_WARN_IF(!jsapi.Init(win))) {
+ return;
+ }
+
+ MOZ_ASSERT(aWindow->IsInnerWindow());
+ nsPIDOMWindowOuter* outerWindow = aWindow->GetOuterWindow();
+ if (NS_WARN_IF(!outerWindow)) {
+ return;
+ }
+
+ RunConsole(jsapi.cx(), outerWindow, aWindow);
+ }
+
+ void
+ RunWindowless()
+ {
+ AssertIsOnMainThread();
+
+ WorkerPrivate* wp = mWorkerPrivate;
+ while (wp->GetParent()) {
+ wp = wp->GetParent();
+ }
+
+ MOZ_ASSERT(!wp->GetWindow());
+
+ AutoSafeJSContext cx;
+
+ JS::Rooted<JSObject*> global(cx, mConsole->GetOrCreateSandbox(cx, wp->GetPrincipal()));
+ if (NS_WARN_IF(!global)) {
+ return;
+ }
+
+ // The CreateSandbox call returns a proxy to the actual sandbox object. We
+ // don't need a proxy here.
+ global = js::UncheckedUnwrap(global);
+
+ JSAutoCompartment ac(cx, global);
+
+ RunConsole(cx, nullptr, nullptr);
+ }
+
+ void
+ RunBackOnWorkerThread() override
+ {
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ ReleaseData();
+ mConsole = nullptr;
+ }
+
+ // This method is called in the owning thread of the Console object.
+ virtual bool
+ PreDispatch(JSContext* aCx) = 0;
+
+ // This method is called in the main-thread.
+ virtual void
+ RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
+ nsPIDOMWindowInner* aInnerWindow) = 0;
+
+ // This method is called in the owning thread of the Console object.
+ virtual void
+ ReleaseData() = 0;
+
+ virtual JSObject* CustomReadHandler(JSContext* aCx,
+ JSStructuredCloneReader* aReader,
+ 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);
+ {
+ RefPtr<Blob> blob =
+ Blob::Create(mClonedData.mParent, mClonedData.mBlobs.ElementAt(aIndex));
+ if (!ToJSValue(aCx, blob, &val)) {
+ return nullptr;
+ }
+ }
+
+ return &val.toObject();
+ }
+
+ MOZ_CRASH("No other tags are supported.");
+ return nullptr;
+ }
+
+ virtual bool CustomWriteHandler(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter,
+ JS::Handle<JSObject*> aObj) override
+ {
+ RefPtr<Blob> blob;
+ if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, aObj, blob)) &&
+ blob->Impl()->MayBeClonedToOtherThreads()) {
+ 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;
+ }
+
+ // This must be released on the worker thread.
+ RefPtr<Console> mConsole;
+
+ ConsoleStructuredCloneData mClonedData;
+};
+
+// This runnable appends a CallData object into the Console queue running on
+// the main-thread.
+class ConsoleCallDataRunnable final : public ConsoleRunnable
+{
+public:
+ ConsoleCallDataRunnable(Console* aConsole,
+ ConsoleCallData* aCallData)
+ : ConsoleRunnable(aConsole)
+ , mCallData(aCallData)
+ {
+ MOZ_ASSERT(aCallData);
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ mCallData->AssertIsOnOwningThread();
+ }
+
+private:
+ ~ConsoleCallDataRunnable()
+ {
+ MOZ_ASSERT(!mCallData);
+ }
+
+ bool
+ PreDispatch(JSContext* aCx) override
+ {
+ mWorkerPrivate->AssertIsOnWorkerThread();
+ mCallData->AssertIsOnOwningThread();
+
+ ClearException ce(aCx);
+
+ JS::Rooted<JSObject*> arguments(aCx,
+ JS_NewArrayObject(aCx, mCallData->mCopiedArguments.Length()));
+ if (NS_WARN_IF(!arguments)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> arg(aCx);
+ for (uint32_t i = 0; i < mCallData->mCopiedArguments.Length(); ++i) {
+ arg = mCallData->mCopiedArguments[i];
+ if (NS_WARN_IF(!JS_DefineElement(aCx, arguments, i, arg,
+ JSPROP_ENUMERATE))) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*arguments));
+
+ if (NS_WARN_IF(!Write(aCx, value))) {
+ return false;
+ }
+
+ mCallData->mStatus = ConsoleCallData::eInUse;
+ return true;
+ }
+
+ void
+ RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
+ nsPIDOMWindowInner* aInnerWindow) override
+ {
+ AssertIsOnMainThread();
+
+ // The windows have to run in parallel.
+ MOZ_ASSERT(!!aOuterWindow == !!aInnerWindow);
+
+ if (aOuterWindow) {
+ mCallData->SetIDs(aOuterWindow->WindowID(), aInnerWindow->WindowID());
+
+ // 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(aInnerWindow);
+ if (NS_WARN_IF(!sop)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ mCallData->SetOriginAttributes(BasePrincipal::Cast(principal)->OriginAttributesRef());
+ } else {
+ ConsoleStackEntry frame;
+ if (mCallData->mTopStackFrame) {
+ frame = *mCallData->mTopStackFrame;
+ }
+
+ nsString id = frame.mFilename;
+ nsString innerID;
+ if (mWorkerPrivate->IsSharedWorker()) {
+ innerID = NS_LITERAL_STRING("SharedWorker");
+ } else if (mWorkerPrivate->IsServiceWorker()) {
+ innerID = NS_LITERAL_STRING("ServiceWorker");
+ // Use scope as ID so the webconsole can decide if the message should
+ // show up per tab
+ id.AssignWithConversion(mWorkerPrivate->WorkerName());
+ } else {
+ innerID = NS_LITERAL_STRING("Worker");
+ }
+
+ mCallData->SetIDs(id, innerID);
+
+ // Save the principal's OriginAttributes in the console event data
+ // so that we will be able to filter messages by origin attributes.
+ nsCOMPtr<nsIPrincipal> principal = mWorkerPrivate->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ mCallData->SetOriginAttributes(BasePrincipal::Cast(principal)->OriginAttributesRef());
+ }
+
+ // Now we could have the correct window (if we are not window-less).
+ mClonedData.mParent = aInnerWindow;
+
+ ProcessCallData(aCx);
+
+ mClonedData.mParent = nullptr;
+ }
+
+ virtual void
+ ReleaseData() override
+ {
+ mConsole->AssertIsOnOwningThread();
+
+ if (mCallData->mStatus == ConsoleCallData::eToBeDeleted) {
+ mConsole->ReleaseCallData(mCallData);
+ } else {
+ MOZ_ASSERT(mCallData->mStatus == ConsoleCallData::eInUse);
+ mCallData->mStatus = ConsoleCallData::eUnused;
+ }
+
+ mCallData = nullptr;
+ }
+
+ void
+ ProcessCallData(JSContext* aCx)
+ {
+ AssertIsOnMainThread();
+
+ 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);
+
+ mConsole->ProcessCallData(aCx, mCallData, values);
+ }
+
+ RefPtr<ConsoleCallData> mCallData;
+};
+
+// This runnable calls ProfileMethod() on the console on the main-thread.
+class ConsoleProfileRunnable final : public ConsoleRunnable
+{
+public:
+ ConsoleProfileRunnable(Console* aConsole, const nsAString& aAction,
+ const Sequence<JS::Value>& aArguments)
+ : ConsoleRunnable(aConsole)
+ , mAction(aAction)
+ , mArguments(aArguments)
+ {
+ MOZ_ASSERT(aConsole);
+ }
+
+private:
+ bool
+ PreDispatch(JSContext* aCx) override
+ {
+ ClearException ce(aCx);
+
+ JS::Rooted<JSObject*> arguments(aCx,
+ JS_NewArrayObject(aCx, mArguments.Length()));
+ if (NS_WARN_IF(!arguments)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> arg(aCx);
+ for (uint32_t i = 0; i < mArguments.Length(); ++i) {
+ arg = mArguments[i];
+ if (NS_WARN_IF(!JS_DefineElement(aCx, arguments, i, arg,
+ JSPROP_ENUMERATE))) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*arguments));
+
+ if (NS_WARN_IF(!Write(aCx, value))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ void
+ RunConsole(JSContext* aCx, nsPIDOMWindowOuter* aOuterWindow,
+ nsPIDOMWindowInner* aInnerWindow) override
+ {
+ AssertIsOnMainThread();
+
+ ClearException ce(aCx);
+
+ // Now we could have the correct window (if we are not window-less).
+ mClonedData.mParent = aInnerWindow;
+
+ JS::Rooted<JS::Value> argumentsValue(aCx);
+ bool ok = Read(aCx, &argumentsValue);
+ mClonedData.mParent = nullptr;
+
+ if (!ok) {
+ 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> 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;
+ }
+ }
+
+ mConsole->ProfileMethodInternal(aCx, mAction, arguments);
+ }
+
+ virtual void
+ ReleaseData() override
+ {}
+
+ nsString mAction;
+
+ // This is a reference of the sequence of arguments we receive from the DOM
+ // bindings and it's rooted by them. It's only used on the owning thread in
+ // PreDispatch().
+ const Sequence<JS::Value>& mArguments;
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(Console)
+
+// We don't need to traverse/unlink mStorage and mSandbox because they are not
+// CCed objects and they are only used on the main thread, even when this
+// Console object is used on workers.
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsoleEventNotifier)
+ tmp->Shutdown();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsoleEventNotifier)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console)
+ for (uint32_t i = 0; i < tmp->mCallDataStorage.Length(); ++i) {
+ tmp->mCallDataStorage[i]->Trace(aCallbacks, aClosure);
+ }
+
+ for (uint32_t i = 0; i < tmp->mCallDataStoragePending.Length(); ++i) {
+ tmp->mCallDataStoragePending[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(nsPIDOMWindowInner* aWindow, ErrorResult& aRv)
+{
+ MOZ_ASSERT_IF(NS_IsMainThread(), aWindow);
+
+ RefPtr<Console> console = new Console(aWindow);
+ console->Initialize(aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return console.forget();
+}
+
+Console::Console(nsPIDOMWindowInner* aWindow)
+ : mWindow(aWindow)
+#ifdef DEBUG
+ , mOwningThread(PR_GetCurrentThread())
+#endif
+ , mOuterID(0)
+ , mInnerID(0)
+ , mStatus(eUnknown)
+{
+ MOZ_ASSERT_IF(NS_IsMainThread(), aWindow);
+
+ if (mWindow) {
+ MOZ_ASSERT(mWindow->IsInnerWindow());
+ mInnerID = mWindow->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 = mWindow->GetOuterWindow();
+ if (outerWindow) {
+ mOuterID = outerWindow->WindowID();
+ }
+ }
+
+ 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;
+ }
+
+ 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");
+ }
+ }
+
+ NS_ReleaseOnMainThread(mStorage.forget());
+ NS_ReleaseOnMainThread(mSandbox.forget());
+
+ mTimerRegistry.Clear();
+ mCounterRegistry.Clear();
+
+ mCallDataStorage.Clear();
+ mCallDataStoragePending.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();
+}
+
+#define METHOD(name, string) \
+ /* static */ void \
+ Console::name(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData) \
+ { \
+ Method(aGlobal, Method##name, NS_LITERAL_STRING(string), aData); \
+ }
+
+METHOD(Log, "log")
+METHOD(Info, "info")
+METHOD(Warn, "warn")
+METHOD(Error, "error")
+METHOD(Exception, "exception")
+METHOD(Debug, "debug")
+METHOD(Table, "table")
+METHOD(Clear, "clear")
+
+/* static */ void
+Console::Trace(const GlobalObject& aGlobal)
+{
+ const Sequence<JS::Value> data;
+ Method(aGlobal, MethodTrace, NS_LITERAL_STRING("trace"), data);
+}
+
+// Displays an interactive listing of all the properties of an object.
+METHOD(Dir, "dir");
+METHOD(Dirxml, "dirxml");
+
+METHOD(Group, "group")
+METHOD(GroupCollapsed, "groupCollapsed")
+METHOD(GroupEnd, "groupEnd")
+
+/* static */ void
+Console::Time(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aTime)
+{
+ JSContext* cx = aGlobal.Context();
+
+ Sequence<JS::Value> data;
+ SequenceRooter<JS::Value> rooter(cx, &data);
+
+ if (!aTime.isUndefined() && !data.AppendElement(aTime, fallible)) {
+ return;
+ }
+
+ Method(aGlobal, MethodTime, NS_LITERAL_STRING("time"), data);
+}
+
+/* static */ void
+Console::TimeEnd(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aTime)
+{
+ JSContext* cx = aGlobal.Context();
+
+ Sequence<JS::Value> data;
+ SequenceRooter<JS::Value> rooter(cx, &data);
+
+ if (!aTime.isUndefined() && !data.AppendElement(aTime, fallible)) {
+ return;
+ }
+
+ Method(aGlobal, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"), data);
+}
+
+/* static */ void
+Console::TimeStamp(const GlobalObject& aGlobal,
+ const JS::Handle<JS::Value> aData)
+{
+ JSContext* cx = aGlobal.Context();
+
+ Sequence<JS::Value> data;
+ SequenceRooter<JS::Value> rooter(cx, &data);
+
+ if (aData.isString() && !data.AppendElement(aData, fallible)) {
+ return;
+ }
+
+ Method(aGlobal, MethodTimeStamp, NS_LITERAL_STRING("timeStamp"), data);
+}
+
+/* static */ void
+Console::Profile(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData)
+{
+ ProfileMethod(aGlobal, NS_LITERAL_STRING("profile"), aData);
+}
+
+/* static */ void
+Console::ProfileEnd(const GlobalObject& aGlobal,
+ const Sequence<JS::Value>& aData)
+{
+ ProfileMethod(aGlobal, NS_LITERAL_STRING("profileEnd"), aData);
+}
+
+/* static */ void
+Console::ProfileMethod(const GlobalObject& aGlobal, const nsAString& aAction,
+ const Sequence<JS::Value>& aData)
+{
+ RefPtr<Console> console = GetConsole(aGlobal);
+ if (!console) {
+ return;
+ }
+
+ JSContext* cx = aGlobal.Context();
+ console->ProfileMethodInternal(cx, aAction, aData);
+}
+
+void
+Console::ProfileMethodInternal(JSContext* aCx, const nsAString& aAction,
+ const Sequence<JS::Value>& aData)
+{
+ if (!NS_IsMainThread()) {
+ // Here we are in a worker thread.
+ RefPtr<ConsoleProfileRunnable> runnable =
+ new ConsoleProfileRunnable(this, aAction, aData);
+
+ runnable->Dispatch(aCx);
+ return;
+ }
+
+ ClearException ce(aCx);
+
+ RootedDictionary<ConsoleProfileEvent> event(aCx);
+ event.mAction = aAction;
+
+ 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, NS_LITERAL_STRING("assert"), aData);
+ }
+}
+
+METHOD(Count, "count")
+
+/* static */ void
+Console::NoopMethod(const GlobalObject& aGlobal)
+{
+ // Nothing to do.
+}
+
+namespace {
+
+nsresult
+StackFrameToStackEntry(JSContext* aCx, nsIStackFrame* aStackFrame,
+ ConsoleStackEntry& aStackEntry)
+{
+ MOZ_ASSERT(aStackFrame);
+
+ nsresult rv = aStackFrame->GetFilename(aCx, aStackEntry.mFilename);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t lineNumber;
+ rv = aStackFrame->GetLineNumber(aCx, &lineNumber);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aStackEntry.mLineNumber = lineNumber;
+
+ int32_t columnNumber;
+ rv = aStackFrame->GetColumnNumber(aCx, &columnNumber);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aStackEntry.mColumnNumber = columnNumber;
+
+ rv = aStackFrame->GetName(aCx, aStackEntry.mFunctionName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString cause;
+ rv = aStackFrame->GetAsyncCause(aCx, cause);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cause.IsEmpty()) {
+ aStackEntry.mAsyncCause.Construct(cause);
+ }
+
+ aStackEntry.mLanguage = nsIProgrammingLanguage::JAVASCRIPT;
+ return NS_OK;
+}
+
+nsresult
+ReifyStack(JSContext* aCx, nsIStackFrame* aStack,
+ nsTArray<ConsoleStackEntry>& aRefiedStack)
+{
+ nsCOMPtr<nsIStackFrame> stack(aStack);
+
+ while (stack) {
+ ConsoleStackEntry& data = *aRefiedStack.AppendElement();
+ nsresult rv = StackFrameToStackEntry(aCx, stack, data);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStackFrame> caller;
+ rv = stack->GetCaller(aCx, getter_AddRefs(caller));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!caller) {
+ rv = stack->GetAsyncCaller(aCx, getter_AddRefs(caller));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ stack.swap(caller);
+ }
+
+ return NS_OK;
+}
+
+} // 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)
+{
+ AssertIsOnOwningThread();
+
+ RefPtr<ConsoleCallData> callData(new ConsoleCallData());
+
+ ClearException ce(aCx);
+
+ if (NS_WARN_IF(!callData->Initialize(aCx, aMethodName, aMethodString,
+ aData, this))) {
+ return;
+ }
+
+ if (mWindow) {
+ nsCOMPtr<nsIWebNavigation> webNav = do_GetInterface(mWindow);
+ if (!webNav) {
+ return;
+ }
+
+ nsCOMPtr<nsILoadContext> loadContext = do_QueryInterface(webNav);
+ MOZ_ASSERT(loadContext);
+
+ loadContext->GetUsePrivateBrowsing(&callData->mPrivate);
+
+ // 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(mWindow);
+ if (NS_WARN_IF(!sop)) {
+ return;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return;
+ }
+
+ callData->SetOriginAttributes(BasePrincipal::Cast(principal)->OriginAttributesRef());
+ }
+
+ JS::StackCapture captureMode = ShouldIncludeStackTrace(aMethodName) ?
+ JS::StackCapture(JS::MaxFrames(DEFAULT_MAX_STACKTRACE_DEPTH)) :
+ JS::StackCapture(JS::FirstSubsumedFrame(aCx));
+ nsCOMPtr<nsIStackFrame> stack = CreateStack(aCx, mozilla::Move(captureMode));
+
+ if (stack) {
+ callData->mTopStackFrame.emplace();
+ nsresult rv = StackFrameToStackEntry(aCx, stack,
+ *callData->mTopStackFrame);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ }
+
+ 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();
+ nsresult rv = ReifyStack(aCx, stack, *callData->mReifiedStack);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ DOMHighResTimeStamp monotonicTimer;
+
+ // Monotonic timer for 'time' and 'timeEnd'
+ if (aMethodName == MethodTime ||
+ aMethodName == MethodTimeEnd ||
+ aMethodName == MethodTimeStamp) {
+ if (mWindow) {
+ nsGlobalWindow *win = nsGlobalWindow::Cast(mWindow);
+ MOZ_ASSERT(win);
+
+ RefPtr<Performance> performance = win->GetPerformance();
+ if (!performance) {
+ return;
+ }
+
+ monotonicTimer = performance->Now();
+
+ nsDocShell* docShell = static_cast<nsDocShell*>(mWindow->GetDocShell());
+ RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get();
+ bool isTimelineRecording = timelines && timelines->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));
+
+ nsAutoJSString key;
+ if (jsString) {
+ key.init(aCx, jsString);
+ }
+
+ timelines->AddMarkerForDocShell(docShell, Move(
+ 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) {
+ nsAutoJSString key;
+ if (key.init(aCx, jsString)) {
+ timelines->AddMarkerForDocShell(docShell, Move(
+ MakeUnique<ConsoleTimelineMarker>(
+ key, aMethodName == MethodTime ? MarkerTracingType::START
+ : MarkerTracingType::END)));
+ }
+ }
+ }
+ } else {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ TimeDuration duration =
+ mozilla::TimeStamp::Now() - workerPrivate->NowBaseTimeStamp();
+
+ monotonicTimer = duration.ToMilliseconds();
+ }
+ }
+
+ if (aMethodName == MethodTime && !aData.IsEmpty()) {
+ callData->mStartTimerStatus = StartTimer(aCx, aData[0],
+ monotonicTimer,
+ callData->mStartTimerLabel,
+ &callData->mStartTimerValue);
+ }
+
+ else if (aMethodName == MethodTimeEnd && !aData.IsEmpty()) {
+ callData->mStopTimerStatus = StopTimer(aCx, aData[0],
+ monotonicTimer,
+ callData->mStopTimerLabel,
+ &callData->mStopTimerDuration);
+ }
+
+ else if (aMethodName == MethodCount) {
+ ConsoleStackEntry frame;
+ if (callData->mTopStackFrame) {
+ frame = *callData->mTopStackFrame;
+ }
+
+ callData->mCountValue = IncreaseCounter(aCx, frame, aData,
+ callData->mCountLabel);
+ }
+
+ if (NS_IsMainThread()) {
+ callData->SetIDs(mOuterID, mInnerID);
+ 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;
+ }
+
+ // We do this only in workers for now.
+ NotifyHandler(aCx, aData, callData);
+
+ RefPtr<ConsoleCallDataRunnable> runnable =
+ new ConsoleCallDataRunnable(this, callData);
+ Unused << NS_WARN_IF(!runnable->Dispatch(aCx));
+}
+
+// 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;
+ nsresult rv = ReifyStack(aCx, stack, reifiedStack);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Throw(aCx, rv);
+ return false;
+ }
+
+ 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
+Console::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.
+ if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope(aCx, aArguments,
+ xpc::PrivilegedJunkScope(),
+ &eventValue, aData))) {
+ return;
+ }
+
+ if (!mStorage) {
+ mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1");
+ }
+
+ if (!mStorage) {
+ NS_WARNING("Failed to get the ConsoleAPIStorage service.");
+ return;
+ }
+
+ nsAutoString innerID, outerID;
+
+ MOZ_ASSERT(aData->mIDType != ConsoleCallData::eUnknown);
+ if (aData->mIDType == ConsoleCallData::eString) {
+ outerID = aData->mOuterIDString;
+ innerID = aData->mInnerIDString;
+ } else {
+ MOZ_ASSERT(aData->mIDType == ConsoleCallData::eNumber);
+ outerID.AppendInt(aData->mOuterIDNumber);
+ innerID.AppendInt(aData->mInnerIDNumber);
+ }
+
+ if (aData->mMethodName == MethodClear) {
+ DebugOnly<nsresult> rv = mStorage->ClearEvents(innerID);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "ClearEvents failed");
+ }
+
+ if (NS_FAILED(mStorage->RecordEvent(innerID, outerID, eventValue))) {
+ NS_WARNING("Failed to record a console event.");
+ }
+}
+
+bool
+Console::PopulateConsoleNotificationInTheTargetScope(JSContext* aCx,
+ const Sequence<JS::Value>& aArguments,
+ JSObject* aTargetScope,
+ JS::MutableHandle<JS::Value> aEventValue,
+ ConsoleCallData* aData) const
+{
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aData);
+ MOZ_ASSERT(aTargetScope);
+
+ JS::Rooted<JSObject*> targetScope(aCx, aTargetScope);
+
+ ConsoleStackEntry frame;
+ if (aData->mTopStackFrame) {
+ frame = *aData->mTopStackFrame;
+ }
+
+ ClearException ce(aCx);
+ RootedDictionary<ConsoleEvent> event(aCx);
+
+ // Save the principal's OriginAttributes in the console event data
+ // so that we will be able to filter messages by origin attributes.
+ JS::Rooted<JS::Value> originAttributesValue(aCx);
+ if (ToJSValue(aCx, aData->mOriginAttributes, &originAttributesValue)) {
+ event.mOriginAttributes = originAttributesValue;
+ }
+
+ event.mID.Construct();
+ event.mInnerID.Construct();
+
+ 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.mLevel = aData->mMethodString;
+ event.mFilename = frame.mFilename;
+
+ 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.mLineNumber = frame.mLineNumber;
+ event.mColumnNumber = frame.mColumnNumber;
+ event.mFunctionName = frame.mFunctionName;
+ event.mTimeStamp = aData->mTimeStamp;
+ event.mPrivate = aData->mPrivate;
+
+ switch (aData->mMethodName) {
+ case MethodLog:
+ case MethodInfo:
+ case MethodWarn:
+ case MethodError:
+ case MethodException:
+ case MethodDebug:
+ case MethodAssert:
+ 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(!ArgumentsToValueList(aArguments,
+ event.mArguments.Value()))) {
+ return false;
+ }
+ }
+
+ if (aData->mMethodName == MethodGroup ||
+ aData->mMethodName == MethodGroupCollapsed ||
+ aData->mMethodName == MethodGroupEnd) {
+ ComposeGroupName(aCx, aArguments, event.mGroupName);
+ }
+
+ else if (aData->mMethodName == MethodTime && !aArguments.IsEmpty()) {
+ event.mTimer = CreateStartTimerValue(aCx, aData->mStartTimerLabel,
+ aData->mStartTimerValue,
+ aData->mStartTimerStatus);
+ }
+
+ else if (aData->mMethodName == MethodTimeEnd && !aArguments.IsEmpty()) {
+ event.mTimer = CreateStopTimerValue(aCx, aData->mStopTimerLabel,
+ aData->mStopTimerDuration,
+ aData->mStopTimerStatus);
+ }
+
+ else if (aData->mMethodName == MethodCount) {
+ event.mCounter = CreateCounterValue(aCx, aData->mCountLabel,
+ aData->mCountValue);
+ }
+
+ JSAutoCompartment ac2(aCx, targetScope);
+
+ 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",
+ JS::UndefinedHandleValue,
+ JSPROP_ENUMERATE | JSPROP_SHARED |
+ JSPROP_GETTER | JSPROP_SETTER,
+ JS_DATA_TO_FUNC_PTR(JSNative, funObj.get()),
+ nullptr))) {
+ 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
+
+bool
+Console::ProcessArguments(JSContext* aCx,
+ const Sequence<JS::Value>& aData,
+ Sequence<JS::Value>& aSequence,
+ Sequence<nsString>& aStyles) const
+{
+ if (aData.IsEmpty()) {
+ return true;
+ }
+
+ if (aData.Length() == 1 || !aData[0].isString()) {
+ return ArgumentsToValueList(aData, aSequence);
+ }
+
+ 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;
+ }
+
+ 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.TruncateLength(aStyles.Length() - 1);
+ }
+
+ 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(NullString(), 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++]);
+
+ 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;
+}
+
+void
+Console::MakeFormatString(nsCString& aFormat, int32_t aInteger,
+ int32_t aMantissa, char aCh) const
+{
+ aFormat.Append('%');
+ if (aInteger >= 0) {
+ aFormat.AppendInt(aInteger);
+ }
+
+ if (aMantissa >= 0) {
+ aFormat.Append('.');
+ aFormat.AppendInt(aMantissa);
+ }
+
+ aFormat.Append(aCh);
+}
+
+void
+Console::ComposeGroupName(JSContext* aCx,
+ const Sequence<JS::Value>& aData,
+ nsAString& aName) const
+{
+ for (uint32_t i = 0; i < aData.Length(); ++i) {
+ if (i != 0) {
+ aName.AppendASCII(" ");
+ }
+
+ JS::Rooted<JS::Value> value(aCx, aData[i]);
+ JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
+ if (!jsString) {
+ return;
+ }
+
+ nsAutoJSString string;
+ if (!string.init(aCx, jsString)) {
+ return;
+ }
+
+ aName.Append(string);
+ }
+}
+
+bool
+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 false;
+ }
+
+ JS::Rooted<JS::Value> name(aCx, aName);
+ JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, name));
+ if (NS_WARN_IF(!jsString)) {
+ return false;
+ }
+
+ nsAutoJSString label;
+ if (NS_WARN_IF(!label.init(aCx, jsString))) {
+ return false;
+ }
+
+ DOMHighResTimeStamp entry = 0;
+ if (!mTimerRegistry.Get(label, &entry)) {
+ mTimerRegistry.Put(label, aTimestamp);
+ } else {
+ aTimestamp = entry;
+ }
+
+ aTimerLabel = label;
+ *aTimerValue = aTimestamp;
+ return true;
+}
+
+JS::Value
+Console::CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
+ DOMHighResTimeStamp aTimerValue,
+ bool aTimerStatus) const
+{
+ if (!aTimerStatus) {
+ RootedDictionary<ConsoleTimerError> error(aCx);
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, error, &value)) {
+ return JS::UndefinedValue();
+ }
+
+ return value;
+ }
+
+ RootedDictionary<ConsoleTimerStart> timer(aCx);
+
+ timer.mName = aTimerLabel;
+ timer.mStarted = aTimerValue;
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, timer, &value)) {
+ return JS::UndefinedValue();
+ }
+
+ return value;
+}
+
+bool
+Console::StopTimer(JSContext* aCx, const JS::Value& aName,
+ DOMHighResTimeStamp aTimestamp,
+ nsAString& aTimerLabel,
+ double* aTimerDuration)
+{
+ 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 false;
+ }
+
+ nsAutoJSString key;
+ if (NS_WARN_IF(!key.init(aCx, jsString))) {
+ return false;
+ }
+
+ DOMHighResTimeStamp entry = 0;
+ if (NS_WARN_IF(!mTimerRegistry.Get(key, &entry))) {
+ return false;
+ }
+
+ mTimerRegistry.Remove(key);
+
+ aTimerLabel = key;
+ *aTimerDuration = aTimestamp - entry;
+ return true;
+}
+
+JS::Value
+Console::CreateStopTimerValue(JSContext* aCx, const nsAString& aLabel,
+ double aDuration, bool aStatus) const
+{
+ if (!aStatus) {
+ return JS::UndefinedValue();
+ }
+
+ RootedDictionary<ConsoleTimerEnd> timer(aCx);
+ timer.mName = aLabel;
+ timer.mDuration = aDuration;
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, timer, &value)) {
+ return JS::UndefinedValue();
+ }
+
+ return value;
+}
+
+bool
+Console::ArgumentsToValueList(const Sequence<JS::Value>& aData,
+ Sequence<JS::Value>& aSequence) const
+{
+ for (uint32_t i = 0; i < aData.Length(); ++i) {
+ if (NS_WARN_IF(!aSequence.AppendElement(aData[i], fallible))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+uint32_t
+Console::IncreaseCounter(JSContext* aCx, const ConsoleStackEntry& aFrame,
+ const Sequence<JS::Value>& aArguments,
+ nsAString& aCountLabel)
+{
+ AssertIsOnOwningThread();
+
+ ClearException ce(aCx);
+
+ nsAutoString key;
+ nsAutoString label;
+
+ if (!aArguments.IsEmpty()) {
+ JS::Rooted<JS::Value> labelValue(aCx, aArguments[0]);
+ JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, labelValue));
+
+ nsAutoJSString string;
+ if (jsString && string.init(aCx, jsString)) {
+ label = string;
+ key = string;
+ }
+ }
+
+ if (key.IsEmpty()) {
+ key.Append(aFrame.mFilename);
+ key.Append(':');
+ key.AppendInt(aFrame.mLineNumber);
+ }
+
+ uint32_t count = 0;
+ if (!mCounterRegistry.Get(key, &count) &&
+ mCounterRegistry.Count() >= MAX_PAGE_COUNTERS) {
+ return MAX_PAGE_COUNTERS;
+ }
+
+ ++count;
+ mCounterRegistry.Put(key, count);
+
+ aCountLabel = label;
+ return count;
+}
+
+JS::Value
+Console::CreateCounterValue(JSContext* aCx, const nsAString& aCountLabel,
+ uint32_t aCountValue) const
+{
+ ClearException ce(aCx);
+
+ if (aCountValue == MAX_PAGE_COUNTERS) {
+ RootedDictionary<ConsoleCounterError> error(aCx);
+
+ 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;
+}
+
+bool
+Console::ShouldIncludeStackTrace(MethodName aMethodName) const
+{
+ switch (aMethodName) {
+ case MethodError:
+ case MethodException:
+ case MethodAssert:
+ case MethodTrace:
+ return true;
+ default:
+ return false;
+ }
+}
+
+JSObject*
+Console::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();
+}
+
+void
+Console::StoreCallData(ConsoleCallData* aCallData)
+{
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(aCallData);
+ MOZ_ASSERT(!mCallDataStorage.Contains(aCallData));
+ MOZ_ASSERT(!mCallDataStoragePending.Contains(aCallData));
+
+ mCallDataStorage.AppendElement(aCallData);
+
+ if (mCallDataStorage.Length() > STORAGE_MAX_EVENTS) {
+ RefPtr<ConsoleCallData> callData = mCallDataStorage[0];
+ mCallDataStorage.RemoveElementAt(0);
+
+ MOZ_ASSERT(callData->mStatus != ConsoleCallData::eToBeDeleted);
+
+ // We cannot delete this object now because we have to trace its JSValues
+ // until the pending operation (ConsoleCallDataRunnable) is completed.
+ if (callData->mStatus == ConsoleCallData::eInUse) {
+ callData->mStatus = ConsoleCallData::eToBeDeleted;
+ mCallDataStoragePending.AppendElement(callData);
+ }
+ }
+}
+
+void
+Console::UnstoreCallData(ConsoleCallData* aCallData)
+{
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(aCallData);
+
+ MOZ_ASSERT(!mCallDataStoragePending.Contains(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().
+
+ mCallDataStorage.RemoveElement(aCallData);
+}
+
+void
+Console::ReleaseCallData(ConsoleCallData* aCallData)
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallData);
+ MOZ_ASSERT(aCallData->mStatus == ConsoleCallData::eToBeDeleted);
+ MOZ_ASSERT(mCallDataStoragePending.Contains(aCallData));
+
+ mCallDataStoragePending.RemoveElement(aCallData);
+}
+
+void
+Console::NotifyHandler(JSContext* aCx, const Sequence<JS::Value>& aArguments,
+ ConsoleCallData* aCallData) const
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aCallData);
+
+ if (!mConsoleEventNotifier) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> value(aCx);
+
+ // aCx and aArguments are in the same compartment because this method is
+ // called directly when a Console.something() runs.
+ // mConsoleEventNotifier->Callable() is the scope where value will be sent to.
+ if (NS_WARN_IF(!PopulateConsoleNotificationInTheTargetScope(aCx, aArguments,
+ mConsoleEventNotifier->Callable(),
+ &value,
+ aCallData))) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> ignored(aCx);
+ mConsoleEventNotifier->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 < mCallDataStorage.Length(); ++i) {
+ JS::Rooted<JS::Value> value(aCx);
+
+ JS::Rooted<JSObject*> sequenceScope(aCx, mCallDataStorage[i]->mGlobal);
+ JSAutoCompartment ac(aCx, sequenceScope);
+
+ Sequence<JS::Value> sequence;
+ SequenceRooter<JS::Value> arguments(aCx, &sequence);
+
+ if (!mCallDataStorage[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]))) {
+ 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
+{
+ MOZ_ASSERT(mOwningThread);
+ MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread);
+}
+
+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 */ Console*
+Console::GetConsoleInternal(const GlobalObject& aGlobal, ErrorResult& aRv)
+{
+ // Worklet
+ if (NS_IsMainThread()) {
+ nsCOMPtr<WorkletGlobalScope> workletScope =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (workletScope) {
+ return workletScope->GetConsole(aRv);
+ }
+ }
+
+ // Window
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> innerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (NS_WARN_IF(!innerWindow)) {
+ return nullptr;
+ }
+
+ nsGlobalWindow* window = nsGlobalWindow::Cast(innerWindow);
+ return window->GetConsole(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
+ else {
+ WorkerDebuggerGlobalScope* debuggerScope =
+ workerPrivate->DebuggerGlobalScope();
+ MOZ_ASSERT(debuggerScope);
+ MOZ_ASSERT(debuggerScope == global, "Which kind of global do we have?");
+
+ return debuggerScope->GetConsole(aRv);
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/console/Console.h b/dom/console/Console.h
new file mode 100644
index 000000000..b334d79f9
--- /dev/null
+++ b/dom/console/Console.h
@@ -0,0 +1,404 @@
+/* -*- 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 "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/JSObjectHolder.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsPIDOMWindow.h"
+
+class nsIConsoleAPIStorage;
+class nsIPrincipal;
+
+namespace mozilla {
+namespace dom {
+
+class AnyCallback;
+class ConsoleCallData;
+class ConsoleRunnable;
+class ConsoleCallDataRunnable;
+class ConsoleProfileRunnable;
+struct ConsoleStackEntry;
+
+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(nsPIDOMWindowInner* aWindow, ErrorResult& aRv);
+
+ // WebIDL methods
+ nsPIDOMWindowInner* GetParentObject() const
+ {
+ return mWindow;
+ }
+
+ static void
+ Log(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Info(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Warn(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Error(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Exception(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Debug(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Table(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Trace(const GlobalObject& aGlobal);
+
+ static void
+ Dir(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Dirxml(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Group(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ GroupCollapsed(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ GroupEnd(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Time(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aTime);
+
+ static void
+ TimeEnd(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aTime);
+
+ static void
+ TimeStamp(const GlobalObject& aGlobal, const JS::Handle<JS::Value> aData);
+
+ static void
+ Profile(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ ProfileEnd(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Assert(const GlobalObject& aGlobal, bool aCondition,
+ const Sequence<JS::Value>& aData);
+
+ static void
+ Count(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ Clear(const GlobalObject& aGlobal, const Sequence<JS::Value>& aData);
+
+ static void
+ NoopMethod(const GlobalObject& aGlobal);
+
+ void
+ ClearStorage();
+
+ void
+ RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& aEvents,
+ ErrorResult& aRv);
+
+ void
+ SetConsoleEventHandler(AnyCallback* aHandler);
+
+private:
+ explicit Console(nsPIDOMWindowInner* aWindow);
+ ~Console();
+
+ void
+ Initialize(ErrorResult& aRv);
+
+ void
+ Shutdown();
+
+ enum MethodName
+ {
+ MethodLog,
+ MethodInfo,
+ MethodWarn,
+ MethodError,
+ MethodException,
+ MethodDebug,
+ MethodTable,
+ MethodTrace,
+ MethodDir,
+ MethodDirxml,
+ MethodGroup,
+ MethodGroupCollapsed,
+ MethodGroupEnd,
+ MethodTime,
+ MethodTimeEnd,
+ MethodTimeStamp,
+ MethodAssert,
+ MethodCount,
+ MethodClear
+ };
+
+ static already_AddRefed<Console>
+ GetConsole(const GlobalObject& aGlobal);
+
+ static Console*
+ GetConsoleInternal(const GlobalObject& aGlobal, ErrorResult &aRv);
+
+ static void
+ ProfileMethod(const GlobalObject& aGlobal, const nsAString& aAction,
+ const Sequence<JS::Value>& aData);
+
+ void
+ ProfileMethodInternal(JSContext* aCx, const nsAString& aAction,
+ const Sequence<JS::Value>& aData);
+
+ static void
+ Method(const GlobalObject& aGlobal, MethodName aName,
+ const nsAString& aString, const Sequence<JS::Value>& aData);
+
+ void
+ MethodInternal(JSContext* aCx, MethodName aName,
+ const nsAString& aString, const Sequence<JS::Value>& aData);
+
+ // This method must receive aCx and aArguments in the same JSCompartment.
+ void
+ ProcessCallData(JSContext* aCx,
+ ConsoleCallData* aData,
+ const Sequence<JS::Value>& aArguments);
+
+ void
+ StoreCallData(ConsoleCallData* aData);
+
+ void
+ UnstoreCallData(ConsoleCallData* aData);
+
+ // Read in Console.cpp how this method is used.
+ void
+ ReleaseCallData(ConsoleCallData* aCallData);
+
+ // aCx and aArguments must be in the same JS compartment.
+ void
+ NotifyHandler(JSContext* aCx,
+ const Sequence<JS::Value>& aArguments,
+ ConsoleCallData* aData) const;
+
+ // PopulateConsoleNotificationInTheTargetScope receives aCx and aArguments in
+ // the same JS compartment and populates the ConsoleEvent object (aValue) 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->Callable() scope when we want to notify this
+ // handler about a new ConsoleEvent.
+ // - It can be the global from the JSContext when RetrieveConsoleEvents is
+ // called.
+ bool
+ PopulateConsoleNotificationInTheTargetScope(JSContext* aCx,
+ const Sequence<JS::Value>& aArguments,
+ JSObject* aTargetScope,
+ JS::MutableHandle<JS::Value> aValue,
+ ConsoleCallData* aData) const;
+
+ // 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.
+ bool
+ ProcessArguments(JSContext* aCx, const Sequence<JS::Value>& aData,
+ Sequence<JS::Value>& aSequence,
+ Sequence<nsString>& aStyles) const;
+
+ void
+ MakeFormatString(nsCString& aFormat, int32_t aInteger, int32_t aMantissa,
+ char aCh) const;
+
+ // Stringify and Concat all the JS::Value in a single string using ' ' as
+ // separator.
+ void
+ ComposeGroupName(JSContext* aCx, const Sequence<JS::Value>& aData,
+ nsAString& aName) const;
+
+ // StartTimer is called on the owning thread and populates aTimerLabel and
+ // aTimerValue. It returns false if a JS exception is thrown or if
+ // the max number of timers is reached.
+ // * 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
+ // window->performance.now() or from Now() -
+ // workerPrivate->NowBaseTimeStamp() in workers.
+ // * aTimerLabel - This label will be populated with the aName converted to a
+ // string.
+ // * aTimerValue - the StartTimer value stored into (or taken from)
+ // mTimerRegistry.
+ bool
+ 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.
+ // * aTimerValue - this is what StartTimer received as aTimerValue
+ // * aTimerStatus - the return value of StartTimer.
+ JS::Value
+ CreateStartTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
+ DOMHighResTimeStamp aTimerValue,
+ bool aTimerStatus) const;
+
+ // StopTimer follows the same pattern as StartTimer: it runs on the
+ // owning thread and populates aTimerLabel and aTimerDuration, used by
+ // CreateStopTimerValue. It returns false if a JS exception is thrown or if
+ // the aName timer doesn't exist in the mTimerRegistry.
+ // * 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
+ // window->performance.now() or from Now() -
+ // workerPrivate->NowBaseTimeStamp() in workers.
+ // * 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).
+ bool
+ StopTimer(JSContext* aCx, const JS::Value& aName,
+ DOMHighResTimeStamp aTimestamp,
+ nsAString& aTimerLabel,
+ double* aTimerDuration);
+
+ // This method generates a ConsoleTimerEnd dictionary exposed as JS::Value, or
+ // a ConsoleTimerError dictionary if aTimerStatus is false. See StopTimer.
+ // * aCx - this is the context that will root the returned value.
+ // * aTimerLabel - this label must be what StopTimer received as aTimerLabel.
+ // * aTimerDuration - this is what StopTimer received as aTimerDuration
+ // * aTimerStatus - the return value of StopTimer.
+ JS::Value
+ CreateStopTimerValue(JSContext* aCx, const nsAString& aTimerLabel,
+ double aTimerDuration,
+ bool aTimerStatus) const;
+
+ // 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 CreateCounterValue. Returns
+ // MAX_PAGE_COUNTERS in case of error, otherwise the incremented counter
+ // value.
+ // * aCx - the JSContext rooting aData.
+ // * aFrame - the first frame of ConsoleCallData.
+ // * 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 ConsoleStackEntry& aFrame,
+ const Sequence<JS::Value>& aData,
+ nsAString& aCountLabel);
+
+ // 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.
+ JS::Value
+ CreateCounterValue(JSContext* aCx, const nsAString& aCountLabel,
+ uint32_t aCountValue) const;
+
+ bool
+ ShouldIncludeStackTrace(MethodName aMethodName) const;
+
+ JSObject*
+ GetOrCreateSandbox(JSContext* aCx, nsIPrincipal* aPrincipal);
+
+ void
+ AssertIsOnOwningThread() const;
+
+ bool
+ IsShuttingDown() const;
+
+ // All these nsCOMPtr are touched on main thread only.
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ nsCOMPtr<nsIConsoleAPIStorage> mStorage;
+ RefPtr<JSObjectHolder> mSandbox;
+
+ // Touched on the owner thread.
+ nsDataHashtable<nsStringHashKey, DOMHighResTimeStamp> mTimerRegistry;
+ nsDataHashtable<nsStringHashKey, uint32_t> mCounterRegistry;
+
+ nsTArray<RefPtr<ConsoleCallData>> mCallDataStorage;
+
+ // This array is used in a particular corner-case where:
+ // 1. we are in a worker thread
+ // 2. we have more than STORAGE_MAX_EVENTS
+ // 3. but the main-thread ConsoleCallDataRunnable of the first one is still
+ // running (this means that something very bad is happening on the
+ // main-thread).
+ // When this happens we want to keep the ConsoleCallData alive for traceing
+ // its JSValues also if 'officially' this ConsoleCallData must be removed from
+ // the storage.
+ nsTArray<RefPtr<ConsoleCallData>> mCallDataStoragePending;
+
+ RefPtr<AnyCallback> mConsoleEventNotifier;
+
+#ifdef DEBUG
+ PRThread* mOwningThread;
+#endif
+
+ uint64_t mOuterID;
+ uint64_t mInnerID;
+
+ enum {
+ eUnknown,
+ eInitialized,
+ eShuttingDown
+ } mStatus;
+
+ friend class ConsoleCallData;
+ friend class ConsoleRunnable;
+ friend class ConsoleCallDataRunnable;
+ friend class ConsoleProfileRunnable;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_Console_h */
diff --git a/dom/console/ConsoleAPI.manifest b/dom/console/ConsoleAPI.manifest
new file mode 100644
index 000000000..56e0768a7
--- /dev/null
+++ b/dom/console/ConsoleAPI.manifest
@@ -0,0 +1,2 @@
+component {96cf7855-dfa9-4c6d-8276-f9705b4890f2} ConsoleAPIStorage.js
+contract @mozilla.org/consoleAPI-storage;1 {96cf7855-dfa9-4c6d-8276-f9705b4890f2}
diff --git a/dom/console/ConsoleAPIStorage.js b/dom/console/ConsoleAPIStorage.js
new file mode 100644
index 000000000..31be449e9
--- /dev/null
+++ b/dom/console/ConsoleAPIStorage.js
@@ -0,0 +1,161 @@
+/* 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/. */
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// This constant tells how many messages to process in a single timer execution.
+const MESSAGES_IN_INTERVAL = 1500
+
+const STORAGE_MAX_EVENTS = 1000;
+
+var _consoleStorage = new Map();
+
+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:
+ * Cu.import("resource://gre/modules/ConsoleAPIStorage.jsm");
+ *
+ * // 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);
+ */
+function ConsoleAPIStorageService() {
+ this.init();
+}
+
+ConsoleAPIStorageService.prototype = {
+ classID : CONSOLEAPISTORAGE_CID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleAPIStorage,
+ Ci.nsIObserver]),
+ classInfo: XPCOMUtils.generateCI({
+ classID: CONSOLEAPISTORAGE_CID,
+ contractID: '@mozilla.org/consoleAPI-storage;1',
+ interfaces: [Ci.nsIConsoleAPIStorage, Ci.nsIObserver],
+ flags: Ci.nsIClassInfo.SINGLETON
+ }),
+
+ 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", false);
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+ Services.obs.addObserver(this, "memory-pressure", false);
+ },
+
+ /**
+ * 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 [id, events] of _consoleStorage) {
+ result.push.apply(result, events);
+ }
+
+ return result.sort(function(a, b) {
+ return a.timeStamp - b.timeStamp;
+ });
+ },
+
+ /**
+ * 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 string aOuterId
+ * This ID is used as 3rd parameters for the console-api-log-event
+ * notification.
+ * @param object aEvent
+ * A JavaScript object you want to store.
+ */
+ recordEvent: function CS_recordEvent(aId, aOuterId, 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();
+ }
+
+ Services.obs.notifyObservers(aEvent, "console-api-log-event", aOuterId);
+ Services.obs.notifyObservers(aEvent, "console-storage-cache-event", aId);
+ },
+
+ /**
+ * 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", null);
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ConsoleAPIStorageService]);
diff --git a/dom/console/ConsoleReportCollector.cpp b/dom/console/ConsoleReportCollector.cpp
new file mode 100644
index 000000000..268f7f8de
--- /dev/null
+++ b/dom/console/ConsoleReportCollector.cpp
@@ -0,0 +1,190 @@
+/* -*- 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 "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+
+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.AppendElement(PendingReport(aErrorFlags, aCategory,
+ aPropertiesFile, aSourceFileURI,
+ aLineNumber, aColumnNumber,
+ aMessageName, aStringParams));
+}
+
+void
+ConsoleReportCollector::FlushConsoleReports(nsIDocument* aDocument,
+ ReportAction aAction)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsTArray<PendingReport> reports;
+
+ {
+ MutexAutoLock lock(mMutex);
+ if (aAction == ReportAction::Forget) {
+ mPendingReports.SwapElements(reports);
+ } else {
+ reports = mPendingReports;
+ }
+ }
+
+ for (uint32_t i = 0; i < reports.Length(); ++i) {
+ PendingReport& report = reports[i];
+
+ // 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);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ }
+
+ // Convert back from nsTArray<nsString> to the char16_t** format required
+ // by our l10n libraries and ReportToConsole. (bug 1219762)
+ UniquePtr<const char16_t*[]> params;
+ uint32_t paramsLength = report.mStringParams.Length();
+ if (paramsLength > 0) {
+ params = MakeUnique<const char16_t*[]>(paramsLength);
+ for (uint32_t j = 0; j < paramsLength; ++j) {
+ params[j] = report.mStringParams[j].get();
+ }
+ }
+
+ nsContentUtils::ReportToConsole(report.mErrorFlags, report.mCategory,
+ aDocument, report.mPropertiesFile,
+ report.mMessageName.get(),
+ params.get(),
+ paramsLength, uri, EmptyString(),
+ report.mLineNumber, report.mColumnNumber);
+ }
+}
+
+void
+ConsoleReportCollector::FlushConsoleReports(nsIConsoleReportCollector* aCollector)
+{
+ MOZ_ASSERT(aCollector);
+
+ nsTArray<PendingReport> reports;
+
+ {
+ MutexAutoLock lock(mMutex);
+ mPendingReports.SwapElements(reports);
+ }
+
+ 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, report.mStringParams);
+ }
+}
+
+void
+ConsoleReportCollector::FlushReportsByWindowId(uint64_t aWindowId,
+ ReportAction aAction)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsTArray<PendingReport> reports;
+
+ {
+ MutexAutoLock lock(mMutex);
+ if (aAction == ReportAction::Forget) {
+ mPendingReports.SwapElements(reports);
+ } else {
+ reports = mPendingReports;
+ }
+ }
+
+ nsCOMPtr<nsIConsoleService> consoleService =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (!consoleService) {
+ NS_WARNING("GetConsoleService failed");
+ return;
+ }
+
+ nsresult rv;
+ for (uint32_t i = 0; i < reports.Length(); ++i) {
+ PendingReport& report = reports[i];
+
+ nsXPIDLString errorText;
+ 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;
+ }
+
+ nsCOMPtr<nsIScriptError> errorObject =
+ do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ rv = errorObject->InitWithWindowID(errorText,
+ NS_ConvertUTF8toUTF16(report.mSourceFileURI),
+ EmptyString(),
+ report.mLineNumber,
+ report.mColumnNumber,
+ report.mErrorFlags,
+ report.mCategory,
+ aWindowId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ consoleService->LogMessage(errorObject);
+ }
+}
+
+void
+ConsoleReportCollector::ClearConsoleReports()
+{
+ MutexAutoLock lock(mMutex);
+
+ mPendingReports.Clear();
+}
+
+ConsoleReportCollector::~ConsoleReportCollector()
+{
+}
+
+} // namespace mozilla
diff --git a/dom/console/ConsoleReportCollector.h b/dom/console/ConsoleReportCollector.h
new file mode 100644
index 000000000..1d542eed6
--- /dev/null
+++ b/dom/console/ConsoleReportCollector.h
@@ -0,0 +1,84 @@
+/* -*- 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 {
+
+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
+ FlushConsoleReports(nsIDocument* aDocument,
+ ReportAction aAction = ReportAction::Forget) override;
+
+ void
+ FlushConsoleReports(nsIConsoleReportCollector* aCollector) override;
+
+ void
+ FlushReportsByWindowId(uint64_t aWindowId,
+ ReportAction aAction = ReportAction::Forget) 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)
+ { }
+
+ 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 nsTArray<nsString> mStringParams;
+ };
+
+ Mutex mMutex;
+
+ // protected by mMutex
+ nsTArray<PendingReport> mPendingReports;
+
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ConsoleReportCollector_h
diff --git a/dom/console/moz.build b/dom/console/moz.build
new file mode 100644
index 000000000..79bd1cf09
--- /dev/null
+++ b/dom/console/moz.build
@@ -0,0 +1,45 @@
+# -*- 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/.
+
+XPIDL_SOURCES += [
+ 'nsIConsoleAPIStorage.idl',
+]
+
+XPIDL_MODULE = 'dom'
+
+EXPORTS += [
+ 'nsIConsoleReportCollector.h',
+]
+
+EXPORTS.mozilla += [
+ 'ConsoleReportCollector.h',
+]
+
+EXPORTS.mozilla.dom += [
+ 'Console.h',
+]
+
+UNIFIED_SOURCES += [
+ 'Console.cpp',
+ 'ConsoleReportCollector.cpp',
+]
+
+EXTRA_COMPONENTS += [
+ 'ConsoleAPI.manifest',
+ 'ConsoleAPIStorage.js',
+]
+
+LOCAL_INCLUDES += [
+ '/docshell/base',
+ '/dom/base',
+ '/dom/workers',
+ '/js/xpconnect/src',
+]
+
+MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ]
+MOCHITEST_CHROME_MANIFESTS += [ 'tests/chrome.ini' ]
+
+FINAL_LIBRARY = 'xul'
diff --git a/dom/console/nsIConsoleAPIStorage.idl b/dom/console/nsIConsoleAPIStorage.idl
new file mode 100644
index 000000000..6ee218af2
--- /dev/null
+++ b/dom/console/nsIConsoleAPIStorage.idl
@@ -0,0 +1,47 @@
+/* -*- 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"
+
+[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 DOMString aId);
+
+ /**
+ * 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 string aOuterId
+ * This ID is used as 3rd parameters for the console-api-log-event
+ * notification.
+ * @param object aEvent
+ * A JavaScript object you want to store.
+ */
+ void recordEvent(in DOMString aId, in DOMString aOuterId, 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 DOMString aId);
+};
diff --git a/dom/console/nsIConsoleReportCollector.h b/dom/console/nsIConsoleReportCollector.h
new file mode 100644
index 000000000..2d7735747
--- /dev/null
+++ b/dom/console/nsIConsoleReportCollector.h
@@ -0,0 +1,115 @@
+/* -*- 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 "nsTArrayForwardDeclare.h"
+
+class nsACString;
+class nsIDocument;
+class nsString;
+
+#define NS_NSICONSOLEREPORTCOLLECTOR_IID \
+ {0xdd98a481, 0xd2c4, 0x4203, {0x8d, 0xfa, 0x85, 0xbf, 0xd7, 0xdc, 0xd7, 0x05}}
+
+// 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),
+ mozilla::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. 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(nsIDocument* aDocument,
+ 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;
+
+ // Flush all pending reports to the console accroding to window ID. Main
+ // thread only.
+ //
+ // aWindowId A window ID representing where to flush the reports and it's
+ // typically the inner window ID.
+ //
+ // aAction An action to decide whether free the pending reports or not.
+ virtual void
+ FlushReportsByWindowId(uint64_t aWindowId,
+ ReportAction aAction = ReportAction::Forget) = 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 000000000..19582aee9
--- /dev/null
+++ b/dom/console/tests/chrome.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ file_empty.html
+
+[test_console.xul]
diff --git a/dom/console/tests/file_empty.html b/dom/console/tests/file_empty.html
new file mode 100644
index 000000000..495c23ec8
--- /dev/null
+++ b/dom/console/tests/file_empty.html
@@ -0,0 +1 @@
+<!DOCTYPE html><html><body></body></html>
diff --git a/dom/console/tests/mochitest.ini b/dom/console/tests/mochitest.ini
new file mode 100644
index 000000000..2381cb1f1
--- /dev/null
+++ b/dom/console/tests/mochitest.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+support-files =
+ file_empty.html
+
+[test_bug659625.html]
+[test_bug978522.html]
+[test_bug979109.html]
+[test_bug989665.html]
+[test_consoleEmptyStack.html]
+[test_console_binding.html]
+[test_console_proto.html]
diff --git a/dom/console/tests/test_bug659625.html b/dom/console/tests/test_bug659625.html
new file mode 100644
index 000000000..7adf87264
--- /dev/null
+++ b/dom/console/tests/test_bug659625.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=659625
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 659625</title>
+ <script type="application/javascript" 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();
+ ok(storage.getEvents().length === 1,
+ "Only one event remains in consoleAPIStorage");
+ ok(storage.getEvents()[0].level === "clear",
+ "Remaining event has level 'clear'");
+ }
+
+ storage.clearEvents();
+ ok(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");
+ ok(storage.getEvents().length === 6,
+ "5 new console events have been registered for logging variants");
+ clearAndCheckStorage();
+
+ console.trace();
+ ok(storage.getEvents().length === 2,
+ "1 new console event registered for trace");
+ clearAndCheckStorage();
+
+ console.dir({});
+ ok(storage.getEvents().length === 2,
+ "1 new console event registered for dir");
+ clearAndCheckStorage();
+
+ console.count("count-label");
+ console.count("count-label");
+ ok(storage.getEvents().length === 3,
+ "2 new console events registered for 2 count calls");
+ clearAndCheckStorage();
+
+ console.group("group-label")
+ console.log("group-log");
+ ok(storage.getEvents().length === 3,
+ "2 new console events registered for group + log");
+ clearAndCheckStorage();
+
+ console.groupCollapsed("group-collapsed")
+ console.log("group-collapsed-log");
+ ok(storage.getEvents().length === 3,
+ "2 new console events registered for groupCollapsed + log");
+ clearAndCheckStorage();
+
+ console.group("closed-group-label")
+ console.log("group-log");
+ console.groupEnd()
+ ok(storage.getEvents().length === 4,
+ "3 new console events registered for group/groupEnd");
+ clearAndCheckStorage();
+
+ console.time("time-label");
+ console.timeEnd();
+ ok(storage.getEvents().length === 3,
+ "2 new console events registered for time/timeEnd");
+ clearAndCheckStorage();
+
+ console.timeStamp("timestamp-label");
+ ok(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 000000000..33d1d56a8
--- /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 type="application/javascript" 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: function() {
+ console.log('%s', {
+ toString: function() {
+ 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 000000000..dc3ee5814
--- /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 type="application/javascript" 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 000000000..298274d7a
--- /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 type="application/javascript" 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">
+
+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.xul b/dom/console/tests/test_console.xul
new file mode 100644
index 000000000..4c34e2f46
--- /dev/null
+++ b/dom/console/tests/test_console.xul
@@ -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 type="application/javascript" 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 000000000..be9dafb86
--- /dev/null
+++ b/dom/console/tests/test_consoleEmptyStack.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>Test for empty stack in console</title>
+ <script type="application/javascript" 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 000000000..764c9954f
--- /dev/null
+++ b/dom/console/tests/test_console_binding.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test Console binding</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+
+function consoleListener() {
+ SpecialPowers.addObserver(this, "console-api-log-event", false);
+}
+
+var order = 0;
+consoleListener.prototype = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "console-api-log-event") {
+ var obj = aSubject.wrappedJSObject;
+ if (order+1 == parseInt(obj.arguments[0])) {
+ ok(true, "Message received: " + obj.arguments[0]);
+ order++;
+ }
+
+ if (order == 3) {
+ SpecialPowers.removeObserver(this, "console-api-log-event");
+ SimpleTest.finish();
+ return;
+ }
+ }
+ }
+}
+
+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 000000000..b492a8926
--- /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 type="application/javascript" 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>