/* -*- 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 "mozilla/dom/Console.h" #include "mozilla/dom/ConsoleBinding.h" #include "mozilla/dom/Exceptions.h" #include "nsCycleCollectionParticipant.h" #include "nsDocument.h" #include "nsDOMNavigationTiming.h" #include "nsGlobalWindow.h" #include "nsJSUtils.h" #include "nsPerformance.h" #include "nsIConsoleAPIStorage.h" #include "nsIDOMWindowUtils.h" #include "nsIInterfaceRequestorUtils.h" #include "nsILoadContext.h" #include "nsIServiceManager.h" #include "nsISupportsPrimitives.h" #include "nsIWebNavigation.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 // The console API methods are async and their action is executed later. This // delay tells how much later. #define CALL_DELAY 15 // milliseconds // This constant tells how many messages to process in a single timer execution. #define MESSAGES_IN_INTERVAL 1500 using namespace mozilla::dom::exceptions; namespace mozilla { namespace dom { class ConsoleCallData { public: ConsoleCallData() : mMethodName(Console::MethodLog) , mPrivate(false) , mTimeStamp(JS_Now()) , mMonotonicTimer(0) { } void Initialize(JSContext* aCx, Console::MethodName aName, const nsAString& aString, const Sequence& aArguments) { mGlobal = JS::CurrentGlobalOrNull(aCx); mMethodName = aName; mMethodString = aString; for (uint32_t i = 0; i < aArguments.Length(); ++i) { mArguments.AppendElement(aArguments[i]); } } JS::Heap mGlobal; Console::MethodName mMethodName; bool mPrivate; int64_t mTimeStamp; DOMHighResTimeStamp mMonotonicTimer; nsString mMethodString; nsTArray> mArguments; Sequence mStack; }; // This class is used to clear any exception at the end of this method. class ClearException { public: ClearException(JSContext* aCx) : mCx(aCx) { } ~ClearException() { JS_ClearPendingException(mCx); } private: JSContext* mCx; }; NS_IMPL_CYCLE_COLLECTION_CLASS(Console) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Console) NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) NS_IMPL_CYCLE_COLLECTION_UNLINK(mTimer) NS_IMPL_CYCLE_COLLECTION_UNLINK(mStorage) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER tmp->mQueuedCalls.Clear(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Console) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTimer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorage) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(Console) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER for (uint32_t i = 0; i < tmp->mQueuedCalls.Length(); ++i) { NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mQueuedCalls[i].mGlobal); for (uint32_t j = 0; j < tmp->mQueuedCalls[i].mArguments.Length(); ++j) { NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mQueuedCalls[i].mArguments[j]); } } 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_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsITimerCallback) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback) NS_INTERFACE_MAP_END Console::Console(nsPIDOMWindow* aWindow) : mWindow(aWindow) , mOuterID(0) , mInnerID(0) { if (mWindow) { MOZ_ASSERT(mWindow->IsInnerWindow()); mInnerID = mWindow->WindowID(); nsPIDOMWindow* outerWindow = mWindow->GetOuterWindow(); MOZ_ASSERT(outerWindow); mOuterID = outerWindow->WindowID(); } if (NS_IsMainThread()) { nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->AddObserver(this, "inner-window-destroyed", false); } } SetIsDOMBinding(); mozilla::HoldJSObjects(this); } Console::~Console() { mozilla::DropJSObjects(this); } NS_IMETHODIMP Console::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread()); if (strcmp(aTopic, "inner-window-destroyed")) { return NS_OK; } nsCOMPtr wrapper = do_QueryInterface(aSubject); NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); uint64_t innerID; nsresult rv = wrapper->GetData(&innerID); NS_ENSURE_SUCCESS(rv, rv); if (innerID == mInnerID) { nsCOMPtr obs = do_GetService("@mozilla.org/observer-service;1"); if (obs) { obs->RemoveObserver(this, "inner-window-destroyed"); } mQueuedCalls.Clear(); mTimerRegistry.Clear(); if (mTimer) { mTimer->Cancel(); mTimer = nullptr; } } return NS_OK; } JSObject* Console::WrapObject(JSContext* aCx, JS::Handle aScope) { return ConsoleBinding::Wrap(aCx, aScope, this); } #define METHOD(name, string) \ void \ Console::name(JSContext* aCx, const Sequence& aData) \ { \ Method(aCx, 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") void Console::Trace(JSContext* aCx) { const Sequence data; Method(aCx, MethodTrace, NS_LITERAL_STRING("trace"), data); } // Displays an interactive listing of all the properties of an object. METHOD(Dir, "dir"); METHOD(Group, "group") METHOD(GroupCollapsed, "groupCollapsed") METHOD(GroupEnd, "groupEnd") void Console::Time(JSContext* aCx, const JS::Handle aTime) { Sequence data; SequenceRooter rooter(aCx, &data); if (!aTime.isUndefined()) { data.AppendElement(aTime); } Method(aCx, MethodTime, NS_LITERAL_STRING("time"), data); } void Console::TimeEnd(JSContext* aCx, const JS::Handle aTime) { Sequence data; SequenceRooter rooter(aCx, &data); if (!aTime.isUndefined()) { data.AppendElement(aTime); } Method(aCx, MethodTimeEnd, NS_LITERAL_STRING("timeEnd"), data); } void Console::Profile(JSContext* aCx, const Sequence& aData, ErrorResult& aRv) { ProfileMethod(aCx, NS_LITERAL_STRING("profile"), aData, aRv); } void Console::ProfileEnd(JSContext* aCx, const Sequence& aData, ErrorResult& aRv) { ProfileMethod(aCx, NS_LITERAL_STRING("profileEnd"), aData, aRv); } void Console::ProfileMethod(JSContext* aCx, const nsAString& aAction, const Sequence& aData, ErrorResult& aRv) { RootedDictionary event(aCx); event.mAction = aAction; event.mArguments.Construct(); Sequence& sequence = event.mArguments.Value(); for (uint32_t i = 0; i < aData.Length(); ++i) { sequence.AppendElement(aData[i]); } JS::Rooted eventValue(aCx); if (!event.ToObject(aCx, JS::NullPtr(), &eventValue)) { aRv.Throw(NS_ERROR_FAILURE); return; } JS::Rooted eventObj(aCx, &eventValue.toObject()); MOZ_ASSERT(eventObj); if (!JS_DefineProperty(aCx, eventObj, "wrappedJSObject", eventValue, nullptr, nullptr, JSPROP_ENUMERATE)) { aRv.Throw(NS_ERROR_FAILURE); return; } nsXPConnect* xpc = nsXPConnect::XPConnect(); nsCOMPtr wrapper; const nsIID& iid = NS_GET_IID(nsISupports); if (NS_FAILED(xpc->WrapJS(aCx, eventObj, iid, getter_AddRefs(wrapper)))) { aRv.Throw(NS_ERROR_FAILURE); return; } nsCOMPtr obs = do_GetService("@mozilla.org/observer-service;1"); if (obs) { obs->NotifyObservers(wrapper, "console-api-profiler", nullptr); } } void Console::Assert(JSContext* aCx, bool aCondition, const Sequence& aData) { if (!aCondition) { Method(aCx, MethodAssert, NS_LITERAL_STRING("assert"), aData); } } METHOD(Count, "count") void Console::__noSuchMethod__() { // Nothing to do. } // Queue a call to a console method. See the CALL_DELAY constant. void Console::Method(JSContext* aCx, MethodName aMethodName, const nsAString& aMethodString, const Sequence& aData) { // This RAII class removes the last element of the mQueuedCalls if something // goes wrong. class RAII { public: RAII(nsTArray& aArray) : mArray(aArray) , mUnfinished(true) { } ~RAII() { if (mUnfinished) { mArray.RemoveElementAt(mArray.Length() - 1); } } void Finished() { mUnfinished = false; } private: nsTArray& mArray; bool mUnfinished; }; ConsoleCallData& callData = *mQueuedCalls.AppendElement(); callData.Initialize(aCx, aMethodName, aMethodString, aData); RAII raii(mQueuedCalls); if (mWindow) { nsCOMPtr webNav = do_GetInterface(mWindow); if (!webNav) { Throw(aCx, NS_ERROR_FAILURE); return; } nsCOMPtr loadContext = do_QueryInterface(webNav); MOZ_ASSERT(loadContext); loadContext->GetUsePrivateBrowsing(&callData.mPrivate); } uint32_t maxDepth = aMethodName == MethodTrace ? DEFAULT_MAX_STACKTRACE_DEPTH : 1; nsCOMPtr stack = CreateStack(aCx, maxDepth); if (!stack) { Throw(aCx, NS_ERROR_FAILURE); return; } // nsIStackFrame is not thread-safe so we take what we need and we store in // an array of ConsoleStackEntry objects. do { uint32_t language; nsresult rv = stack->GetLanguage(&language); if (NS_FAILED(rv)) { Throw(aCx, rv); return; } if (language == nsIProgrammingLanguage::JAVASCRIPT || language == nsIProgrammingLanguage::JAVASCRIPT2) { ConsoleStackEntry& data = *callData.mStack.AppendElement(); nsCString string; rv = stack->GetFilename(string); if (NS_FAILED(rv)) { Throw(aCx, rv); return; } CopyUTF8toUTF16(string, data.mFilename); int32_t lineNumber; rv = stack->GetLineNumber(&lineNumber); if (NS_FAILED(rv)) { Throw(aCx, rv); return; } data.mLineNumber = lineNumber; rv = stack->GetName(string); if (NS_FAILED(rv)) { Throw(aCx, rv); return; } CopyUTF8toUTF16(string, data.mFunctionName); data.mLanguage = language; } nsCOMPtr caller; rv = stack->GetCaller(getter_AddRefs(caller)); if (NS_FAILED(rv)) { Throw(aCx, rv); return; } stack.swap(caller); } while (stack); // Monotonic timer for 'time' and 'timeEnd' if ((aMethodName == MethodTime || aMethodName == MethodTimeEnd) && mWindow) { nsGlobalWindow *win = static_cast(mWindow.get()); MOZ_ASSERT(win); ErrorResult rv; nsRefPtr performance = win->GetPerformance(rv); if (rv.Failed()) { Throw(aCx, rv.ErrorCode()); return; } callData.mMonotonicTimer = performance->Now(); } // The operation is completed. RAII class has to be disabled. raii.Finished(); if (!mTimer) { mTimer = do_CreateInstance("@mozilla.org/timer;1"); mTimer->InitWithCallback(this, CALL_DELAY, nsITimer::TYPE_REPEATING_SLACK); } } // Timer callback used to process each of the queued calls. NS_IMETHODIMP Console::Notify(nsITimer *timer) { MOZ_ASSERT(!mQueuedCalls.IsEmpty()); uint32_t i = 0; for (; i < MESSAGES_IN_INTERVAL && i < mQueuedCalls.Length(); ++i) { ProcessCallData(mQueuedCalls[i]); } mQueuedCalls.RemoveElementsAt(0, i); if (mQueuedCalls.IsEmpty()) { mTimer->Cancel(); mTimer = nullptr; } return NS_OK; } void Console::ProcessCallData(ConsoleCallData& aData) { ConsoleStackEntry frame; if (!aData.mStack.IsEmpty()) { frame = aData.mStack[0]; } AutoSafeJSContext cx; ClearException ce(cx); RootedDictionary event(cx); JSAutoCompartment ac(cx, aData.mGlobal); event.mID.Construct(); event.mInnerID.Construct(); if (mWindow) { event.mID.Value().SetAsUnsignedLong() = mOuterID; event.mInnerID.Value().SetAsUnsignedLong() = mInnerID; } else { // If we are in a JSM, the window doesn't exist. event.mID.Value().SetAsString() = NS_LITERAL_STRING("jsm"); event.mInnerID.Value().SetAsString() = frame.mFilename; } event.mLevel = aData.mMethodString; event.mFilename = frame.mFilename; event.mLineNumber = frame.mLineNumber; 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(); ProcessArguments(cx, aData.mArguments, event.mArguments.Value()); break; default: event.mArguments.Construct(); ArgumentsToValueList(aData.mArguments, event.mArguments.Value()); } if (aData.mMethodName == MethodTrace) { event.mStacktrace.Construct(); event.mStacktrace.Value().SwapElements(aData.mStack); } else if (aData.mMethodName == MethodGroup || aData.mMethodName == MethodGroupCollapsed || aData.mMethodName == MethodGroupEnd) { ComposeGroupName(cx, aData.mArguments, event.mGroupName); } else if (aData.mMethodName == MethodTime && !aData.mArguments.IsEmpty()) { event.mTimer = StartTimer(cx, aData.mArguments[0], aData.mMonotonicTimer); } else if (aData.mMethodName == MethodTimeEnd && !aData.mArguments.IsEmpty()) { event.mTimer = StopTimer(cx, aData.mArguments[0], aData.mMonotonicTimer); } else if (aData.mMethodName == MethodCount) { event.mCounter = IncreaseCounter(cx, frame, aData.mArguments); } JS::Rooted eventValue(cx); if (!event.ToObject(cx, JS::NullPtr(), &eventValue)) { Throw(cx, NS_ERROR_FAILURE); return; } JS::Rooted eventObj(cx, &eventValue.toObject()); MOZ_ASSERT(eventObj); if (!JS_DefineProperty(cx, eventObj, "wrappedJSObject", eventValue, nullptr, nullptr, JSPROP_ENUMERATE)) { return; } if (!mStorage) { mStorage = do_GetService("@mozilla.org/consoleAPI-storage;1"); } if (!mStorage) { NS_WARNING("Failed to get the ConsoleAPIStorage service."); return; } nsAutoString innerID; innerID.AppendInt(mInnerID); if (NS_FAILED(mStorage->RecordEvent(innerID, eventValue))) { NS_WARNING("Failed to record a console event."); } nsXPConnect* xpc = nsXPConnect::XPConnect(); nsCOMPtr wrapper; const nsIID& iid = NS_GET_IID(nsISupports); if (NS_FAILED(xpc->WrapJS(cx, eventObj, iid, getter_AddRefs(wrapper)))) { return; } nsCOMPtr obs = do_GetService("@mozilla.org/observer-service;1"); if (obs) { nsAutoString outerID; outerID.AppendInt(mOuterID); obs->NotifyObservers(wrapper, "console-api-log-event", outerID.get()); } } void Console::ProcessArguments(JSContext* aCx, const nsTArray>& aData, Sequence& aSequence) { if (aData.IsEmpty()) { return; } if (aData.Length() == 1 || !aData[0].isString()) { ArgumentsToValueList(aData, aSequence); return; } SequenceRooter rooter(aCx, &aSequence); JS::Rooted format(aCx, aData[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, format)); if (!jsString) { return; } nsDependentJSString string; if (!string.init(aCx, jsString)) { return; } 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 == '%') { output.Append(*start); ++start; continue; } nsAutoString tmp; tmp.Append('%'); int32_t integer = -1; int32_t mantissa = -1; // Let's parse %. for %d and %f if (*start >= '0' && *start <= '9') { integer = 0; do { integer = integer * 10 + *start - '0'; tmp.Append(*start); ++start; } while (*start >= '0' && *start <= '9'); } if (*start == '.') { tmp.Append(*start); ++start; // '.' 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'); } char ch = *start; tmp.Append(ch); ++start; switch (ch) { case 'o': { if (!output.IsEmpty()) { JS::Rooted str(aCx, JS_NewUCStringCopyN(aCx, output.get(), output.Length())); if (!str) { return; } aSequence.AppendElement(JS::StringValue(str)); output.Truncate(); } JS::Rooted v(aCx); if (index < aData.Length()) { v = aData[index++]; } aSequence.AppendElement(v); break; } case 's': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (!jsString) { return; } nsDependentJSString v; if (!v.init(aCx, jsString)) { return; } output.Append(v); } break; case 'd': case 'i': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); int32_t v; if (!JS::ToInt32(aCx, value, &v)) { return; } nsCString format; MakeFormatString(format, integer, mantissa, 'd'); output.AppendPrintf(format.get(), v); } break; case 'f': if (index < aData.Length()) { JS::Rooted value(aCx, aData[index++]); double v; if (!JS::ToNumber(aCx, value, &v)) { return; } nsCString format; MakeFormatString(format, integer, mantissa, 'f'); output.AppendPrintf(format.get(), v); } break; default: output.Append(tmp); break; } } if (!output.IsEmpty()) { JS::Rooted str(aCx, JS_NewUCStringCopyN(aCx, output.get(), output.Length())); if (!str) { return; } aSequence.AppendElement(JS::StringValue(str)); } // The rest of the array, if unused by the format string. for (; index < aData.Length(); ++index) { aSequence.AppendElement(aData[index]); } } void Console::MakeFormatString(nsCString& aFormat, int32_t aInteger, int32_t aMantissa, char aCh) { aFormat.Append("%"); if (aInteger >= 0) { aFormat.AppendInt(aInteger); } if (aMantissa >= 0) { aFormat.Append("."); aFormat.AppendInt(aMantissa); } aFormat.Append(aCh); } void Console::ComposeGroupName(JSContext* aCx, const nsTArray>& aData, nsAString& aName) { for (uint32_t i = 0; i < aData.Length(); ++i) { if (i != 0) { aName.AppendASCII(" "); } JS::Rooted value(aCx, aData[i]); JS::Rooted jsString(aCx, JS::ToString(aCx, value)); if (!jsString) { return; } nsDependentJSString string; if (!string.init(aCx, jsString)) { return; } aName.Append(string); } } JS::Value Console::StartTimer(JSContext* aCx, const JS::Value& aName, DOMHighResTimeStamp aTimestamp) { if (mTimerRegistry.Count() >= MAX_PAGE_TIMERS) { RootedDictionary error(aCx); JS::Rooted value(aCx); if (!error.ToObject(aCx, JS::NullPtr(), &value)) { return JS::UndefinedValue(); } return value; } RootedDictionary timer(aCx); JS::Rooted name(aCx, aName); JS::Rooted jsString(aCx, JS::ToString(aCx, name)); if (!jsString) { return JS::UndefinedValue(); } nsDependentJSString key; if (!key.init(aCx, jsString)) { return JS::UndefinedValue(); } timer.mName = key; DOMHighResTimeStamp entry; if (!mTimerRegistry.Get(key, &entry)) { mTimerRegistry.Put(key, aTimestamp); } else { aTimestamp = entry; } timer.mStarted = aTimestamp; JS::Rooted value(aCx); if (!timer.ToObject(aCx, JS::NullPtr(), &value)) { return JS::UndefinedValue(); } return value; } JS::Value Console::StopTimer(JSContext* aCx, const JS::Value& aName, DOMHighResTimeStamp aTimestamp) { JS::Rooted name(aCx, aName); JS::Rooted jsString(aCx, JS::ToString(aCx, name)); if (!jsString) { return JS::UndefinedValue(); } nsDependentJSString key; if (!key.init(aCx, jsString)) { return JS::UndefinedValue(); } DOMHighResTimeStamp entry; if (!mTimerRegistry.Get(key, &entry)) { return JS::UndefinedValue(); } mTimerRegistry.Remove(key); RootedDictionary timer(aCx); timer.mName = key; timer.mDuration = aTimestamp - entry; JS::Rooted value(aCx); if (!timer.ToObject(aCx, JS::NullPtr(), &value)) { return JS::UndefinedValue(); } return value; } void Console::ArgumentsToValueList(const nsTArray>& aData, Sequence& aSequence) { for (uint32_t i = 0; i < aData.Length(); ++i) { aSequence.AppendElement(aData[i]); } } JS::Value Console::IncreaseCounter(JSContext* aCx, const ConsoleStackEntry& aFrame, const nsTArray>& aArguments) { ClearException ce(aCx); nsAutoString key; nsAutoString label; if (!aArguments.IsEmpty()) { JS::Rooted labelValue(aCx, aArguments[0]); JS::Rooted jsString(aCx, JS::ToString(aCx, labelValue)); nsDependentJSString string; if (jsString && string.init(aCx, jsString)) { label = string; key = string; } } if (key.IsEmpty()) { key.Append(aFrame.mFilename); key.Append(NS_LITERAL_STRING(":")); key.AppendInt(aFrame.mLineNumber); } uint32_t count = 0; if (!mCounterRegistry.Get(key, &count)) { if (mCounterRegistry.Count() >= MAX_PAGE_COUNTERS) { RootedDictionary error(aCx); JS::Rooted value(aCx); if (!error.ToObject(aCx, JS::NullPtr(), &value)) { return JS::UndefinedValue(); } return value; } } ++count; mCounterRegistry.Put(key, count); RootedDictionary data(aCx); data.mLabel = label; data.mCount = count; JS::Rooted value(aCx); if (!data.ToObject(aCx, JS::NullPtr(), &value)) { return JS::UndefinedValue(); } return value; } } // namespace dom } // namespace mozilla