diff options
Diffstat (limited to 'dom/console')
-rw-r--r-- | dom/console/Console.cpp | 2440 | ||||
-rw-r--r-- | dom/console/Console.h | 404 | ||||
-rw-r--r-- | dom/console/ConsoleAPI.manifest | 2 | ||||
-rw-r--r-- | dom/console/ConsoleAPIStorage.js | 161 | ||||
-rw-r--r-- | dom/console/ConsoleReportCollector.cpp | 190 | ||||
-rw-r--r-- | dom/console/ConsoleReportCollector.h | 84 | ||||
-rw-r--r-- | dom/console/moz.build | 45 | ||||
-rw-r--r-- | dom/console/nsIConsoleAPIStorage.idl | 47 | ||||
-rw-r--r-- | dom/console/nsIConsoleReportCollector.h | 115 | ||||
-rw-r--r-- | dom/console/tests/chrome.ini | 6 | ||||
-rw-r--r-- | dom/console/tests/file_empty.html | 1 | ||||
-rw-r--r-- | dom/console/tests/mochitest.ini | 11 | ||||
-rw-r--r-- | dom/console/tests/test_bug659625.html | 92 | ||||
-rw-r--r-- | dom/console/tests/test_bug978522.html | 32 | ||||
-rw-r--r-- | dom/console/tests/test_bug979109.html | 32 | ||||
-rw-r--r-- | dom/console/tests/test_bug989665.html | 21 | ||||
-rw-r--r-- | dom/console/tests/test_console.xul | 35 | ||||
-rw-r--r-- | dom/console/tests/test_consoleEmptyStack.html | 27 | ||||
-rw-r--r-- | dom/console/tests/test_console_binding.html | 42 | ||||
-rw-r--r-- | dom/console/tests/test_console_proto.html | 17 |
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> |