/* -*- 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/TimerClamping.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_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); monotonicTimer = workerPrivate->TimeStampToDOMHighRes(TimeStamp::Now()); } } 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