michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 sw=2 et tw=78: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsCOMPtr.h" michael@0: #include "nsIDocument.h" michael@0: #include "nsIScriptTimeoutHandler.h" michael@0: #include "nsIXPConnect.h" michael@0: #include "nsJSUtils.h" michael@0: #include "nsContentUtils.h" michael@0: #include "nsError.h" michael@0: #include "nsGlobalWindow.h" michael@0: #include "nsIContentSecurityPolicy.h" michael@0: #include "mozilla/Attributes.h" michael@0: #include "mozilla/Likely.h" michael@0: #include michael@0: #include "mozilla/dom/FunctionBinding.h" michael@0: #include "nsAXPCNativeCallContext.h" michael@0: michael@0: static const char kSetIntervalStr[] = "setInterval"; michael@0: static const char kSetTimeoutStr[] = "setTimeout"; michael@0: michael@0: using namespace mozilla; michael@0: using namespace mozilla::dom; michael@0: michael@0: // Our JS nsIScriptTimeoutHandler implementation. michael@0: class nsJSScriptTimeoutHandler MOZ_FINAL : public nsIScriptTimeoutHandler michael@0: { michael@0: public: michael@0: // nsISupports michael@0: NS_DECL_CYCLE_COLLECTING_ISUPPORTS michael@0: NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(nsJSScriptTimeoutHandler) michael@0: michael@0: nsJSScriptTimeoutHandler(); michael@0: // This will call SwapElements on aArguments with an empty array. michael@0: nsJSScriptTimeoutHandler(nsGlobalWindow *aWindow, Function& aFunction, michael@0: FallibleTArray >& aArguments, michael@0: ErrorResult& aError); michael@0: nsJSScriptTimeoutHandler(JSContext* aCx, nsGlobalWindow *aWindow, michael@0: const nsAString& aExpression, bool* aAllowEval, michael@0: ErrorResult& aError); michael@0: ~nsJSScriptTimeoutHandler(); michael@0: michael@0: virtual const char16_t *GetHandlerText(); michael@0: virtual Function* GetCallback() michael@0: { michael@0: return mFunction; michael@0: } michael@0: virtual void GetLocation(const char **aFileName, uint32_t *aLineNo) michael@0: { michael@0: *aFileName = mFileName.get(); michael@0: *aLineNo = mLineNo; michael@0: } michael@0: michael@0: virtual const nsTArray& GetArgs() michael@0: { michael@0: return mArgs; michael@0: } michael@0: michael@0: nsresult Init(nsGlobalWindow *aWindow, bool *aIsInterval, michael@0: int32_t *aInterval, bool* aAllowEval); michael@0: michael@0: void ReleaseJSObjects(); michael@0: michael@0: private: michael@0: // filename, line number and JS language version string of the michael@0: // caller of setTimeout() michael@0: nsCString mFileName; michael@0: uint32_t mLineNo; michael@0: nsTArray > mArgs; michael@0: michael@0: // The expression to evaluate or function to call. If mFunction is non-null michael@0: // it should be used, else use mExpr. michael@0: nsString mExpr; michael@0: nsRefPtr mFunction; michael@0: }; michael@0: michael@0: michael@0: // nsJSScriptTimeoutHandler michael@0: // QueryInterface implementation for nsJSScriptTimeoutHandler michael@0: NS_IMPL_CYCLE_COLLECTION_CLASS(nsJSScriptTimeoutHandler) michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsJSScriptTimeoutHandler) michael@0: tmp->ReleaseJSObjects(); michael@0: NS_IMPL_CYCLE_COLLECTION_UNLINK_END michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(nsJSScriptTimeoutHandler) michael@0: if (MOZ_UNLIKELY(cb.WantDebugInfo())) { michael@0: nsAutoCString name("nsJSScriptTimeoutHandler"); michael@0: if (tmp->mFunction) { michael@0: JSFunction* fun = michael@0: JS_GetObjectFunction(js::UncheckedUnwrap(tmp->mFunction->Callable())); michael@0: if (fun && JS_GetFunctionId(fun)) { michael@0: JSFlatString *funId = JS_ASSERT_STRING_IS_FLAT(JS_GetFunctionId(fun)); michael@0: size_t size = 1 + JS_PutEscapedFlatString(nullptr, 0, funId, 0); michael@0: char *funIdName = new char[size]; michael@0: if (funIdName) { michael@0: JS_PutEscapedFlatString(funIdName, size, funId, 0); michael@0: name.AppendLiteral(" ["); michael@0: name.Append(funIdName); michael@0: delete[] funIdName; michael@0: name.AppendLiteral("]"); michael@0: } michael@0: } michael@0: } else { michael@0: name.AppendLiteral(" ["); michael@0: name.Append(tmp->mFileName); michael@0: name.AppendLiteral(":"); michael@0: name.AppendInt(tmp->mLineNo); michael@0: name.AppendLiteral("]"); michael@0: } michael@0: cb.DescribeRefCountedNode(tmp->mRefCnt.get(), name.get()); michael@0: } michael@0: else { michael@0: NS_IMPL_CYCLE_COLLECTION_DESCRIBE(nsJSScriptTimeoutHandler, michael@0: tmp->mRefCnt.get()) michael@0: } michael@0: michael@0: if (tmp->mFunction) { michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFunction) michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS michael@0: } michael@0: NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsJSScriptTimeoutHandler) michael@0: for (uint32_t i = 0; i < tmp->mArgs.Length(); ++i) { michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mArgs[i]) michael@0: } michael@0: NS_IMPL_CYCLE_COLLECTION_TRACE_END michael@0: michael@0: NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsJSScriptTimeoutHandler) michael@0: NS_INTERFACE_MAP_ENTRY(nsIScriptTimeoutHandler) michael@0: NS_INTERFACE_MAP_ENTRY(nsISupports) michael@0: NS_INTERFACE_MAP_END michael@0: michael@0: NS_IMPL_CYCLE_COLLECTING_ADDREF(nsJSScriptTimeoutHandler) michael@0: NS_IMPL_CYCLE_COLLECTING_RELEASE(nsJSScriptTimeoutHandler) michael@0: michael@0: static bool michael@0: CheckCSPForEval(JSContext* aCx, nsGlobalWindow* aWindow, ErrorResult& aError) michael@0: { michael@0: // if CSP is enabled, and setTimeout/setInterval was called with a string, michael@0: // disable the registration and log an error michael@0: nsCOMPtr doc = aWindow->GetExtantDoc(); michael@0: if (!doc) { michael@0: // if there's no document, we don't have to do anything. michael@0: return true; michael@0: } michael@0: michael@0: nsCOMPtr csp; michael@0: aError = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); michael@0: if (aError.Failed()) { michael@0: return false; michael@0: } michael@0: michael@0: if (!csp) { michael@0: return true; michael@0: } michael@0: michael@0: bool allowsEval = true; michael@0: bool reportViolation = false; michael@0: aError = csp->GetAllowsEval(&reportViolation, &allowsEval); michael@0: if (aError.Failed()) { michael@0: return false; michael@0: } michael@0: michael@0: if (reportViolation) { michael@0: // TODO : need actual script sample in violation report. michael@0: NS_NAMED_LITERAL_STRING(scriptSample, michael@0: "call to eval() or related function blocked by CSP"); michael@0: michael@0: // Get the calling location. michael@0: uint32_t lineNum = 0; michael@0: const char *fileName; michael@0: nsAutoString fileNameString; michael@0: if (nsJSUtils::GetCallingLocation(aCx, &fileName, &lineNum)) { michael@0: AppendUTF8toUTF16(fileName, fileNameString); michael@0: } else { michael@0: fileNameString.AssignLiteral("unknown"); michael@0: } michael@0: michael@0: csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, michael@0: fileNameString, scriptSample, lineNum, michael@0: EmptyString(), EmptyString()); michael@0: } michael@0: michael@0: return allowsEval; michael@0: } michael@0: michael@0: nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler() : michael@0: mLineNo(0) michael@0: { michael@0: } michael@0: michael@0: nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(nsGlobalWindow *aWindow, michael@0: Function& aFunction, michael@0: FallibleTArray >& aArguments, michael@0: ErrorResult& aError) : michael@0: mLineNo(0), michael@0: mFunction(&aFunction) michael@0: { michael@0: if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { michael@0: // This window was already closed, or never properly initialized, michael@0: // don't let a timer be scheduled on such a window. michael@0: aError.Throw(NS_ERROR_NOT_INITIALIZED); michael@0: return; michael@0: } michael@0: michael@0: mozilla::HoldJSObjects(this); michael@0: mArgs.SwapElements(aArguments); michael@0: } michael@0: michael@0: nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx, michael@0: nsGlobalWindow *aWindow, michael@0: const nsAString& aExpression, michael@0: bool* aAllowEval, michael@0: ErrorResult& aError) : michael@0: mLineNo(0), michael@0: mExpr(aExpression) michael@0: { michael@0: if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { michael@0: // This window was already closed, or never properly initialized, michael@0: // don't let a timer be scheduled on such a window. michael@0: aError.Throw(NS_ERROR_NOT_INITIALIZED); michael@0: return; michael@0: } michael@0: michael@0: *aAllowEval = CheckCSPForEval(aCx, aWindow, aError); michael@0: if (aError.Failed() || !*aAllowEval) { michael@0: return; michael@0: } michael@0: michael@0: // Get the calling location. michael@0: const char *filename; michael@0: if (nsJSUtils::GetCallingLocation(aCx, &filename, &mLineNo)) { michael@0: mFileName.Assign(filename); michael@0: } michael@0: } michael@0: michael@0: nsJSScriptTimeoutHandler::~nsJSScriptTimeoutHandler() michael@0: { michael@0: ReleaseJSObjects(); michael@0: } michael@0: michael@0: void michael@0: nsJSScriptTimeoutHandler::ReleaseJSObjects() michael@0: { michael@0: if (mFunction) { michael@0: mFunction = nullptr; michael@0: mArgs.Clear(); michael@0: mozilla::DropJSObjects(this); michael@0: } michael@0: } michael@0: michael@0: nsresult michael@0: nsJSScriptTimeoutHandler::Init(nsGlobalWindow *aWindow, bool *aIsInterval, michael@0: int32_t *aInterval, bool *aAllowEval) michael@0: { michael@0: if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { michael@0: // This window was already closed, or never properly initialized, michael@0: // don't let a timer be scheduled on such a window. michael@0: michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: } michael@0: michael@0: nsAXPCNativeCallContext *ncc = nullptr; michael@0: nsresult rv = nsContentUtils::XPConnect()-> michael@0: GetCurrentNativeCallContext(&ncc); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (!ncc) michael@0: return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: JSContext *cx = nullptr; michael@0: michael@0: rv = ncc->GetJSContext(&cx); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: uint32_t argc; michael@0: JS::Value *argv = nullptr; michael@0: michael@0: ncc->GetArgc(&argc); michael@0: ncc->GetArgvPtr(&argv); michael@0: michael@0: JS::Rooted expr(cx); michael@0: JS::Rooted funobj(cx); michael@0: michael@0: if (argc < 1) { michael@0: ::JS_ReportError(cx, "Function %s requires at least 2 parameter", michael@0: *aIsInterval ? kSetIntervalStr : kSetTimeoutStr); michael@0: return NS_ERROR_DOM_TYPE_ERR; michael@0: } michael@0: michael@0: int32_t interval = 0; michael@0: if (argc > 1) { michael@0: JS::Rooted arg(cx, argv[1]); michael@0: michael@0: if (!JS::ToInt32(cx, arg, &interval)) { michael@0: ::JS_ReportError(cx, michael@0: "Second argument to %s must be a millisecond interval", michael@0: aIsInterval ? kSetIntervalStr : kSetTimeoutStr); michael@0: return NS_ERROR_DOM_TYPE_ERR; michael@0: } michael@0: } michael@0: michael@0: if (argc == 1) { michael@0: // If no interval was specified, treat this like a timeout, to avoid michael@0: // setting an interval of 0 milliseconds. michael@0: *aIsInterval = false; michael@0: } michael@0: michael@0: JS::Rooted arg(cx, argv[0]); michael@0: switch (::JS_TypeOfValue(cx, arg)) { michael@0: case JSTYPE_FUNCTION: michael@0: funobj = &arg.toObject(); michael@0: break; michael@0: michael@0: case JSTYPE_STRING: michael@0: case JSTYPE_OBJECT: michael@0: { michael@0: JSString *str = JS::ToString(cx, arg); michael@0: if (!str) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: expr = ::JS_FlattenString(cx, str); michael@0: if (!expr) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: argv[0] = JS::StringValue(str); michael@0: } michael@0: break; michael@0: michael@0: default: michael@0: ::JS_ReportError(cx, "useless %s call (missing quotes around argument?)", michael@0: *aIsInterval ? kSetIntervalStr : kSetTimeoutStr); michael@0: michael@0: // Return an error that nsGlobalWindow can recognize and turn into NS_OK. michael@0: return NS_ERROR_DOM_TYPE_ERR; michael@0: } michael@0: michael@0: if (expr) { michael@0: // if CSP is enabled, and setTimeout/setInterval was called with a string, michael@0: // disable the registration and log an error michael@0: ErrorResult error; michael@0: *aAllowEval = CheckCSPForEval(cx, aWindow, error); michael@0: if (error.Failed() || !*aAllowEval) { michael@0: return error.ErrorCode(); michael@0: } michael@0: michael@0: mExpr.Append(JS_GetFlatStringChars(expr), michael@0: JS_GetStringLength(JS_FORGET_STRING_FLATNESS(expr))); michael@0: michael@0: // Get the calling location. michael@0: const char *filename; michael@0: if (nsJSUtils::GetCallingLocation(cx, &filename, &mLineNo)) { michael@0: mFileName.Assign(filename); michael@0: } michael@0: } else if (funobj) { michael@0: *aAllowEval = true; michael@0: michael@0: mozilla::HoldJSObjects(this); michael@0: michael@0: mFunction = new Function(funobj, GetIncumbentGlobal()); michael@0: michael@0: // Create our arg array. argc is the number of arguments passed michael@0: // to setTimeout or setInterval; the first two are our callback michael@0: // and the delay, so only arguments after that need to go in our michael@0: // array. michael@0: // std::max(argc - 2, 0) wouldn't work right because argc is unsigned. michael@0: uint32_t argCount = std::max(argc, 2u) - 2; michael@0: michael@0: FallibleTArray > args; michael@0: if (!args.SetCapacity(argCount)) { michael@0: // No need to drop here, since we already have a non-null mFunction michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: for (uint32_t idx = 0; idx < argCount; ++idx) { michael@0: *args.AppendElement() = argv[idx + 2]; michael@0: } michael@0: args.SwapElements(mArgs); michael@0: } else { michael@0: NS_WARNING("No func and no expr - why are we here?"); michael@0: } michael@0: *aInterval = interval; michael@0: return NS_OK; michael@0: } michael@0: michael@0: const char16_t * michael@0: nsJSScriptTimeoutHandler::GetHandlerText() michael@0: { michael@0: NS_ASSERTION(!mFunction, "No expression, so no handler text!"); michael@0: return mExpr.get(); michael@0: } michael@0: michael@0: nsresult NS_CreateJSTimeoutHandler(nsGlobalWindow *aWindow, michael@0: bool *aIsInterval, michael@0: int32_t *aInterval, michael@0: nsIScriptTimeoutHandler **aRet) michael@0: { michael@0: *aRet = nullptr; michael@0: nsRefPtr handler = new nsJSScriptTimeoutHandler(); michael@0: bool allowEval; michael@0: nsresult rv = handler->Init(aWindow, aIsInterval, aInterval, &allowEval); michael@0: if (NS_FAILED(rv) || !allowEval) { michael@0: return rv; michael@0: } michael@0: michael@0: handler.forget(aRet); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: already_AddRefed michael@0: NS_CreateJSTimeoutHandler(nsGlobalWindow *aWindow, Function& aFunction, michael@0: const Sequence& aArguments, michael@0: ErrorResult& aError) michael@0: { michael@0: FallibleTArray > args; michael@0: if (!args.AppendElements(aArguments)) { michael@0: aError.Throw(NS_ERROR_OUT_OF_MEMORY); michael@0: return 0; michael@0: } michael@0: michael@0: nsRefPtr handler = michael@0: new nsJSScriptTimeoutHandler(aWindow, aFunction, args, aError); michael@0: return aError.Failed() ? nullptr : handler.forget(); michael@0: } michael@0: michael@0: already_AddRefed michael@0: NS_CreateJSTimeoutHandler(JSContext* aCx, nsGlobalWindow *aWindow, michael@0: const nsAString& aExpression, ErrorResult& aError) michael@0: { michael@0: ErrorResult rv; michael@0: bool allowEval = false; michael@0: nsRefPtr handler = michael@0: new nsJSScriptTimeoutHandler(aCx, aWindow, aExpression, &allowEval, rv); michael@0: if (rv.Failed() || !allowEval) { michael@0: return nullptr; michael@0: } michael@0: michael@0: return handler.forget(); michael@0: }